feat: Bun 1.3.14

- Raised the Bun minimum version to >=1.3.14 across package metadata, install scripts, and changelog notes.
- Removed the Photon native image pipeline and added SIXEL-based `sixel` support in pi-natives.
- Migrated coding-agent image handling and resizing to `Bun.Image`, including updated tests and a JPEG quality bump to 80.
- Added HTTP/2 fetch bootstrap with HTTPS-only fallback and updated Bun build flags for autoload suppression/`--keep-names`.
This commit is contained in:
can1357
2026-05-13 13:16:43 +02:00
parent 1087e7cdb9
commit af07faec11
41 changed files with 266 additions and 428 deletions
Generated
-33
View File
@@ -506,8 +506,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
]
@@ -1672,16 +1670,6 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jobserver"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom 0.3.4",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.98"
@@ -1746,16 +1734,6 @@ dependencies = [
"libc",
]
[[package]]
name = "libwebp-sys"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733"
dependencies = [
"cc",
"glob",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@@ -2492,7 +2470,6 @@ dependencies = [
"toml",
"unicode-segmentation",
"unicode-width",
"webp",
"winreg 0.56.0",
"xxhash-rust",
]
@@ -4428,16 +4405,6 @@ dependencies = [
"string_cache_codegen",
]
[[package]]
name = "webp"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c071456adef4aca59bf6a583c46b90ff5eb0b4f758fc347cea81290288f37ce1"
dependencies = [
"image",
"libwebp-sys",
]
[[package]]
name = "weezl"
version = "0.1.12"
-1
View File
@@ -280,7 +280,6 @@ syntect = { version = "5.3", default-features = false, features = [
"default-themes",
"regex-fancy",
] }
webp = "0.3"
# ──────────────────────────────────────────────────────────────────────────────
# Markup Conversion
+4 -4
View File
@@ -134,7 +134,7 @@
"@oh-my-pi/pi-utils": "workspace:*",
},
"devDependencies": {
"@types/bun": "^1.3.13",
"@types/bun": "^1.3.14",
},
"peerDependencies": {
"@oh-my-pi/pi-coding-agent": "^13",
@@ -226,7 +226,7 @@
"@tailwindcss/node": "^4.2.4",
"@types/babel__generator": "^7.27.0",
"@types/babel__traverse": "^7.28.0",
"@types/bun": "^1.3.13",
"@types/bun": "^1.3.14",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/turndown": "5.0.6",
@@ -714,7 +714,7 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/bun@1.3.13", "", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="],
@@ -804,7 +804,7 @@
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-types": ["bun-types@1.3.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
+1
View File
@@ -2,6 +2,7 @@ telemetry = false
[install]
minimumReleaseAge = 259200 # 3 days in seconds
minimumReleaseAgeExcludes = ["@types/bun"]
linker = "hoisted"
exact = true
saveTextLockfile = true
-1
View File
@@ -50,7 +50,6 @@ tokio-util.workspace = true
toml.workspace = true
unicode-segmentation.workspace = true
unicode-width.workspace = true
webp.workspace = true
xxhash-rust.workspace = true
[target.'cfg(unix)'.dependencies]
-190
View File
@@ -1,190 +0,0 @@
//! Minimal image processing API for resizing and format conversion.
//!
//! Provides only the subset of functionality needed:
//! - Load image from bytes (PNG, JPEG, WebP, GIF)
//! - Get dimensions
//! - Resize with configurable filter
//! - Export as PNG, JPEG, WebP, or GIF
use std::{io::Cursor, sync::Arc};
use icy_sixel::{EncodeOptions, sixel_encode};
use image::{
DynamicImage, ImageFormat as StdImageFormat, ImageReader, codecs::jpeg::JpegEncoder,
imageops::FilterType,
};
use napi::bindgen_prelude::*;
use napi_derive::napi;
use crate::task;
/// Output format for [`PhotonImage::encode`].
#[napi]
pub enum ImageFormat {
/// PNG encoded bytes.
PNG = 0,
/// JPEG encoded bytes.
JPEG = 1,
/// WebP encoded bytes.
WEBP = 2,
/// GIF encoded bytes.
GIF = 3,
}
/// Sampling filter for resize operations.
#[napi]
pub enum SamplingFilter {
/// Nearest-neighbor sampling (fast, low quality).
Nearest = 1,
/// Triangle filter (linear interpolation).
Triangle = 2,
/// Catmull-Rom filter with sharper edges.
CatmullRom = 3,
/// Gaussian filter for smoother results.
Gaussian = 4,
/// Lanczos3 filter for high-quality downscaling.
Lanczos3 = 5,
}
impl From<SamplingFilter> for FilterType {
fn from(filter: SamplingFilter) -> Self {
match filter {
SamplingFilter::Nearest => Self::Nearest,
SamplingFilter::Triangle => Self::Triangle,
SamplingFilter::CatmullRom => Self::CatmullRom,
SamplingFilter::Gaussian => Self::Gaussian,
SamplingFilter::Lanczos3 => Self::Lanczos3,
}
}
}
/// Image container for native interop.
#[napi]
pub struct PhotonImage {
/// Shared decoded image data.
img: Arc<DynamicImage>,
}
type ImageTask = task::Promise<PhotonImage>;
#[napi]
impl PhotonImage {
/// Create a new `PhotonImage` from encoded image bytes (PNG, JPEG, WebP,
/// GIF). Returns the decoded image handle on success.
///
/// # Errors
/// Returns an error if the image format cannot be detected or decoded.
#[napi]
pub fn parse(bytes: Uint8Array) -> ImageTask {
let bytes = bytes.as_ref().to_vec();
task::blocking("image.decode", (), move |_| -> Result<Self> {
let img = decode_image_from_bytes(&bytes)?;
Ok(Self { img: Arc::new(img) })
})
}
/// Get the image width in pixels.
#[napi(getter)]
pub fn get_width(&self) -> u32 {
self.img.width()
}
/// Get the image height in pixels.
#[napi(getter)]
pub fn get_height(&self) -> u32 {
self.img.height()
}
/// Encode the image to bytes in the specified format.
///
/// # Errors
/// Returns an error if encoding fails or format is invalid.
#[napi]
pub fn encode(&self, format: ImageFormat, quality: u8) -> task::Promise<Vec<u8>> {
let img = Arc::clone(&self.img);
task::blocking("image.encode", (), move |_| encode_image(&img, format, quality))
}
/// Resize the image to the specified pixel dimensions using the filter.
/// Returns a new `PhotonImage` containing the resized image.
#[napi]
pub fn resize(&self, width: u32, height: u32, filter: SamplingFilter) -> ImageTask {
let img = Arc::clone(&self.img);
task::blocking("image.resize", (), move |_| {
Ok(Self { img: Arc::new(img.resize_exact(width, height, filter.into())) })
})
}
}
/// Encode image bytes into a SIXEL escape sequence for terminal rendering.
///
/// The input image is decoded and resized to the requested pixel dimensions
/// before encoding.
///
/// # Errors
/// Returns an error if decoding, resizing, or SIXEL encoding fails.
#[napi]
pub fn encode_sixel(
bytes: Uint8Array,
target_width_px: u32,
target_height_px: u32,
) -> Result<String> {
if target_width_px == 0 || target_height_px == 0 {
return Err(Error::from_reason("Target SIXEL dimensions must be greater than zero"));
}
let source = decode_image_from_bytes(bytes.as_ref())?;
let resized = if source.width() == target_width_px && source.height() == target_height_px {
source
} else {
source.resize_exact(target_width_px, target_height_px, FilterType::Lanczos3)
};
let rgba = resized.to_rgba8();
let options = EncodeOptions::default();
sixel_encode(rgba.as_raw(), target_width_px as usize, target_height_px as usize, &options)
.map_err(|err| Error::from_reason(format!("Failed to encode SIXEL: {err}")))
}
fn decode_image_from_bytes(bytes: &[u8]) -> Result<DynamicImage> {
let reader = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|e| Error::from_reason(format!("Failed to detect image format: {e}")))?;
reader
.decode()
.map_err(|e| Error::from_reason(format!("Failed to decode image: {e}")))
}
fn encode_image(img: &DynamicImage, format: ImageFormat, quality: u8) -> Result<Vec<u8>> {
let (w, h) = (img.width(), img.height());
match format {
ImageFormat::PNG => {
let mut buffer = Vec::with_capacity((w * h * 4) as usize);
img.write_to(&mut Cursor::new(&mut buffer), StdImageFormat::Png)
.map_err(|e| Error::from_reason(format!("Failed to encode PNG: {e}")))?;
Ok(buffer)
},
ImageFormat::JPEG => {
let mut buffer = Vec::with_capacity((w * h * 3) as usize);
let encoder = JpegEncoder::new_with_quality(&mut buffer, quality);
img.write_with_encoder(encoder)
.map_err(|e| Error::from_reason(format!("Failed to encode JPEG: {e}")))?;
Ok(buffer)
},
ImageFormat::WEBP => {
// Lossy WebP via libwebp (the `image` crate's bundled WebP encoder is
// lossless-only). quality is 0..=100; libwebp accepts an f32 in the same
// range.
let encoder = webp::Encoder::from_image(img)
.map_err(|e| Error::from_reason(format!("Failed to prepare WebP encoder: {e}")))?;
let memory = encoder.encode(quality as f32);
Ok(memory.to_vec())
},
ImageFormat::GIF => {
let mut buffer = Vec::with_capacity((w * h) as usize);
img.write_to(&mut Cursor::new(&mut buffer), StdImageFormat::Gif)
.map_err(|e| Error::from_reason(format!("Failed to encode GIF: {e}")))?;
Ok(buffer)
},
}
}
+3 -3
View File
@@ -3,7 +3,7 @@
//! # Overview
//! High-performance primitives for clipboard access, grep, file discovery,
//! ANSI-aware text measurement, syntax highlighting, HTML-to-Markdown
//! conversion, and image processing.
//! conversion, and terminal SIXEL encoding.
//!
//! # Example
//! ```ignore
@@ -15,7 +15,7 @@
//!
//! # Architecture
//! ```text
//! JS (packages/natives) -> N-API -> Rust modules (clipboard/fd/glob/grep/html/highlight/image/text)
//! JS (packages/natives) -> N-API -> Rust modules (clipboard/fd/glob/grep/html/highlight/sixel/text)
//! ```
#![allow(clippy::trailing_empty_array, reason = "generated by napi macro")]
@@ -35,8 +35,8 @@ pub mod glob_util;
pub mod grep;
pub mod highlight;
pub mod html;
pub mod image;
pub mod keys;
pub mod sixel;
pub use pi_ast::language;
pub mod power;
+54
View File
@@ -0,0 +1,54 @@
//! SIXEL terminal-image encoding.
//!
//! Decodes an encoded image (PNG, JPEG, WebP, GIF), resizes to the target
//! pixel dimensions, and encodes the result as a SIXEL escape sequence.
//!
//! General-purpose image processing (decode/resize/encode for files and
//! buffers) lives in `Bun.Image` on the JS side; this module exists only
//! because SIXEL is a terminal-display protocol with no equivalent there.
use std::io::Cursor;
use icy_sixel::{EncodeOptions, sixel_encode};
use image::{DynamicImage, ImageReader, imageops::FilterType};
use napi::bindgen_prelude::*;
use napi_derive::napi;
/// Encode image bytes into a SIXEL escape sequence for terminal rendering.
///
/// The input image is decoded and resized to the requested pixel dimensions
/// before encoding.
///
/// # Errors
/// Returns an error if decoding, resizing, or SIXEL encoding fails.
#[napi]
pub fn encode_sixel(
bytes: Uint8Array,
target_width_px: u32,
target_height_px: u32,
) -> Result<String> {
if target_width_px == 0 || target_height_px == 0 {
return Err(Error::from_reason("Target SIXEL dimensions must be greater than zero"));
}
let source = decode_image_from_bytes(bytes.as_ref())?;
let resized = if source.width() == target_width_px && source.height() == target_height_px {
source
} else {
source.resize_exact(target_width_px, target_height_px, FilterType::Lanczos3)
};
let rgba = resized.to_rgba8();
let options = EncodeOptions::default();
sixel_encode(rgba.as_raw(), target_width_px as usize, target_height_px as usize, &options)
.map_err(|err| Error::from_reason(format!("Failed to encode SIXEL: {err}")))
}
fn decode_image_from_bytes(bytes: &[u8]) -> Result<DynamicImage> {
let reader = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|e| Error::from_reason(format!("Failed to detect image format: {e}")))?;
reader
.decode()
.map_err(|e| Error::from_reason(format!("Failed to decode image: {e}")))
}
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "omp-monorepo",
"private": true,
"type": "module",
"packageManager": "bun@1.3.13",
"packageManager": "bun@1.3.14",
"workspaces": {
"packages": [
"packages/*"
@@ -35,7 +35,7 @@
"@tailwindcss/node": "^4.2.4",
"@types/babel__generator": "^7.27.0",
"@types/babel__traverse": "^7.28.0",
"@types/bun": "^1.3.13",
"@types/bun": "^1.3.14",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@types/turndown": "5.0.6",
+3
View File
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Raised the minimum required Bun version from >=1.3.7 to >=1.3.14
## [14.9.5] - 2026-05-12
+1 -1
View File
@@ -44,7 +44,7 @@
"@types/bun": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"files": [
"src",
+5 -1
View File
@@ -1,9 +1,13 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Increased the minimum Bun runtime version to `>=1.3.14` for the `@aws-?` package
### Added
- Added `installH2Fetch` to patch `globalThis.fetch` so HTTPS requests attempt HTTP/2 over ALPN with automatic HTTP/1.1 fallback when HTTP/2 is unsupported
- Added priority service-tier traffic to the `premiumRequests` accounting on OpenAI and OpenAI Codex providers. Sending `serviceTier: "priority"` now increments `usage.premiumRequests` by 1 per request, matching the existing GitHub Copilot premium-request budget semantics so downstream consumers (e.g. the `omp stats` "Premium Reqs" card and `/usage`) reflect priority traffic alongside Copilot premium calls.
## [15.0.0] - 2026-05-13
@@ -2348,4 +2352,4 @@ _Dedicated to Peter's shoulder ([@steipete](https://twitter.com/steipete))_
## [0.9.4] - 2025-11-26
Initial release with multi-provider LLM support.
Initial release with multi-provider LLM support.
+1 -1
View File
@@ -61,7 +61,7 @@
"@types/bun": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"files": [
"src",
+1
View File
@@ -37,6 +37,7 @@ export * from "./usage/zai";
export * from "./utils/anthropic-auth";
export * from "./utils/discovery";
export * from "./utils/event-stream";
export * from "./utils/h2-fetch";
export * from "./utils/overflow";
export * from "./utils/retry";
export * from "./utils/schema";
+47
View File
@@ -0,0 +1,47 @@
/**
* Patch `globalThis.fetch` to advertise HTTP/2 in TLS ALPN, with transparent
* HTTP/1.1 fallback when the server doesn't select `h2`.
*
* Bun's HTTP/2 client is gated on `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT`,
* read by the native runtime before any JS executes; assigning to
* `process.env` from inside JS is a no-op. Per-request `protocol: "http2"`
* activates h2 over TLS ALPN and rejects with `error.code === "HTTP2Unsupported"`
* if the server picks anything else, so we catch and retry without the hint.
*
* Bun negotiates h2 via ALPN over TLS only (no h2c), so plain `http://` URLs
* skip the attempt entirely — avoids the throw/retry round-trip for localhost.
*
* Idempotent.
*/
const installed: unique symbol = Symbol.for("oh-my-pi.h2fetch.installed");
interface PatchedFetch {
[installed]?: true;
}
export function installH2Fetch(): void {
const original = globalThis.fetch as typeof fetch & PatchedFetch;
if (original[installed]) return;
const wrapper = async function h2fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
if (!isHttps(input)) return original(input, init);
try {
return await original(input, { ...init, protocol: "http2" });
} catch (err) {
if ((err as { code?: unknown }).code !== "HTTP2Unsupported") throw err;
return original(input, init);
}
} as typeof fetch & PatchedFetch;
// Preserve `fetch.preconnect` and any other statics SDK code might poke at.
Object.assign(wrapper, original);
wrapper[installed] = true;
globalThis.fetch = wrapper;
}
function isHttps(input: string | URL | Request): boolean {
if (typeof input === "string") return input.startsWith("https:");
if (input instanceof URL) return input.protocol === "https:";
return input.url.startsWith("https:");
}
+6
View File
@@ -1,13 +1,19 @@
# Changelog
## [Unreleased]
### Added
- Added middle elision for streaming tool outputs (bash, ssh, python, js eval) and post-execution tool result spill. When `tools.artifactHeadBytes` is set (default 20 KB), large outputs now keep both the first N KB and the last N KB with an inline `[… N lines elided (M KB) …]` marker between them, instead of dropping everything before the trailing tail. Setting `tools.artifactHeadBytes = 0` reverts to the previous tail-only behavior. The full output is still mirrored to the session artifact (`artifact://<id>`) regardless of elision mode. Exposes `truncateMiddle` and `formatMiddleElisionMarker` from `@oh-my-pi/pi-coding-agent/session/streaming-output`, extends `OutputSinkOptions` with `headBytes`, and adds `direction: "middle"` plus `headRange` / `tailRange` / `elidedLines` / `elidedBytes` to `TruncationMeta`.
- Added per-line column cap shared across streaming tool outputs (`bash`, `ssh`, `python`, `js eval`) and the `read` tool. Lines wider than `tools.outputMaxColumns` bytes (default **768**) are ellipsis-truncated at write time and remaining bytes up to the next `\n` are dropped — bounded memory even on multi-MB single-line outputs (e.g. `cat /dev/urandom`). The cap lives on `OutputSink` as the new `maxColumns` option, persists state across chunk boundaries so split-mid-line writes still respect the budget, and exposes `columnDroppedBytes` / `columnTruncatedLines` on `OutputSummary`. Middle-elision byte math subtracts column drops so the "elided from middle" count stays honest. `read` reuses the same setting but trims its already-collected lines via `truncateLine`. Skipped when the read selector is `:raw`. The artifact file (`artifact://<id>`) keeps the full uncapped stream. Set `tools.outputMaxColumns = 0` to disable.
- Added Bun HTTP/2 fetch opt-in. Dev scripts (`bun run dev`, `bun run stats`) now pass `bun --experimental-http2-fetch` so every `fetch()` advertises `h2` in the TLS ALPN list and falls back to HTTP/1.1 when the server doesn't select it. Multiplexing collapses parallel requests to the same origin onto one TLS connection. For the installed `omp` binary, export `BUN_FEATURE_FLAG_EXPERIMENTAL_HTTP2_CLIENT=1` in your shell to enable the same behavior (the flag has to be set before Bun starts; `process.env` from inside JS is too late). Requires Bun **1.3.14**.
### Changed
- Raised the image downscaling default JPEG quality from 75 to 80 in `resizeImage` output generation
- Changed image resize metadata notes from coordinate-scale hints to a simple `Image resized from <original> to <displayed>` message and hide the note when the resized dimensions are unchanged
- Removed `utils/image-convert.ts` and its `convertToPng` helper; callers now inline `new Bun.Image(bytes).png().toBase64()` from [`Bun.Image`](https://bun.com/docs/runtime/image) (Bun 1.3.14+).
- Changed image decode/resize/encode in `utils/image-resize.ts` from the native `PhotonImage` binding to [`Bun.Image`](https://bun.com/docs/runtime/image). Same PNG/JPEG/WebP quality+dimension ladder, but pipelines run off-thread on Bun's statically-linked codecs with no native-addon round-trip. Bumped the minimum Bun runtime requirement to **1.3.14**.
- Changed `search` pagination in multi-file scopes so `skip` now skips entire files and pages results in groups of up to 20 files, with output guiding the next `skip` value via `Showing files X-Y of N`
- Changed multi-file search result selection to cap each file at 20 matches and round-robin across files, so one noisy file no longer suppresses visibility of hits in other files and truncation now reports per-file limits
- Changed search truncation metadata/renderer output from match/result-based limits to file-based limits (`fileLimitReached`, `perFileLimitReached`) and updated truncation labels accordingly
+1 -1
View File
@@ -74,7 +74,7 @@
"@types/bun": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"files": [
"src",
@@ -33,6 +33,11 @@ async function main(): Promise<void> {
"bun",
"build",
"--compile",
"--no-compile-autoload-bunfig",
"--no-compile-autoload-dotenv",
"--no-compile-autoload-tsconfig",
"--no-compile-autoload-package-json",
"--keep-names",
"--define",
'process.env.PI_COMPILED="true"',
"--external",
+8
View File
@@ -1,6 +1,14 @@
#!/usr/bin/env bun
import { installH2Fetch } from "@oh-my-pi/pi-ai";
import { APP_NAME, MIN_BUN_VERSION, procmgr, VERSION } from "@oh-my-pi/pi-utils";
// Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
// discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
// can't toggle from JS, so we patch globalThis.fetch to pass
// `protocol: "http2"` per request, with transparent HTTP/1.1 fallback on
// `HTTP2Unsupported`. See @oh-my-pi/pi-ai/utils/h2-fetch for details.
installH2Fetch();
// Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
// Otherwise every child bun process (subagents, plugin installs, ptree spawns,
// etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
@@ -4,7 +4,6 @@ import { formatNumber } from "@oh-my-pi/pi-utils";
import { settings } from "../../config/settings";
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
import { resolveImageOptions } from "../../tools/render-utils";
import { convertToPng } from "../../utils/image-convert";
/**
* Component that renders a complete assistant message
@@ -76,14 +75,15 @@ export class AssistantMessageComponent extends Container {
const key = `${toolCallId}:${index}`;
if (this.#convertedKittyImages.has(key) || this.#kittyConversionsInFlight.has(key)) continue;
this.#kittyConversionsInFlight.add(key);
convertToPng(image.data, image.mimeType)
.then(converted => {
new Bun.Image(Buffer.from(image.data, "base64"))
.png()
.toBase64()
.then(data => {
this.#kittyConversionsInFlight.delete(key);
if (!converted) return;
this.#convertedKittyImages.set(key, {
type: "image",
data: converted.data,
mimeType: converted.mimeType,
data,
mimeType: "image/png",
});
if (this.#lastMessage) {
this.updateContent(this.#lastMessage);
@@ -32,7 +32,6 @@ import {
import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
import { toolRenderers } from "../../tools/renderers";
import { renderStatusLine } from "../../tui";
import { convertToPng } from "../../utils/image-convert";
import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
import { renderDiff } from "./diff";
@@ -295,13 +294,13 @@ export class ToolExecutionComponent extends Container {
// Convert async - catch errors from processing
const index = i;
convertToPng(img.data, img.mimeType)
.then(converted => {
if (converted) {
this.#convertedImages.set(index, converted);
this.#updateDisplay();
this.#ui.requestRender();
}
new Bun.Image(Buffer.from(img.data, "base64"))
.png()
.toBase64()
.then(data => {
this.#convertedImages.set(index, { data, mimeType: "image/png" });
this.#updateDisplay();
this.#ui.requestRender();
})
.catch(() => {
// Ignore conversion failures - display will use original image format
@@ -1,27 +0,0 @@
import { ImageFormat, PhotonImage } from "@oh-my-pi/pi-natives";
/**
* Convert image to PNG format for terminal display.
* Kitty graphics protocol requires PNG format (f=100).
*/
export async function convertToPng(
base64Data: string,
mimeType: string,
): Promise<{ data: string; mimeType: string } | null> {
// Already PNG, no conversion needed
if (mimeType === "image/png") {
return { data: base64Data, mimeType };
}
try {
const image = await PhotonImage.parse(new Uint8Array(Buffer.from(base64Data, "base64")));
const pngBuffer = await image.encode(ImageFormat.PNG, 100);
return {
data: Buffer.from(pngBuffer).toBase64(),
mimeType: "image/png",
};
} catch {
// Conversion failed
return null;
}
}
@@ -2,7 +2,6 @@ import * as fs from "node:fs/promises";
import type { ImageContent } from "@oh-my-pi/pi-ai";
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
import { resolveReadPath } from "../tools/path-utils";
import { convertToPng } from "./image-convert";
import { formatDimensionNote, resizeImage } from "./image-resize";
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
@@ -42,8 +41,13 @@ export async function ensureSupportedImageInput(image: ImageContent): Promise<Im
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
return image;
}
const converted = await convertToPng(image.data, image.mimeType);
return converted ? { type: "image", data: converted.data, mimeType: converted.mimeType } : null;
try {
const bytes = Buffer.from(image.data, "base64");
const data = await new Bun.Image(bytes).png().toBase64();
return { type: "image", data, mimeType: "image/png" };
} catch {
return null;
}
}
export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
+32 -44
View File
@@ -1,11 +1,10 @@
import type { ImageContent } from "@oh-my-pi/pi-ai";
import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
export interface ImageResizeOptions {
maxWidth?: number; // Default: 1568
maxHeight?: number; // Default: 1568
maxBytes?: number; // Default: 500KB
jpegQuality?: number; // Default: 75
maxWidth?: number;
maxHeight?: number;
maxBytes?: number;
jpegQuality?: number;
}
export interface ResizedImage {
@@ -24,12 +23,12 @@ export interface ResizedImage {
const DEFAULT_MAX_BYTES = 500 * 1024;
const DEFAULT_OPTIONS: Required<ImageResizeOptions> = {
// 1568px — Anthropic downscales anything larger; OpenAI tiles at 768px;
// sending bigger pixels wastes bandwidth the model never sees.
// Anthropic's "internal recommended size" — Claude internally caps images at
// 1568px on the longest edge before vision processing.
maxWidth: 1568,
maxHeight: 1568,
maxBytes: DEFAULT_MAX_BYTES,
jpegQuality: 75,
jpegQuality: 80,
};
/** Pick the smallest of N encoded buffers. */
@@ -48,43 +47,34 @@ Buffer.prototype.toBase64 = function (this: Buffer) {
/**
* Resize and recompress an image to fit within the specified max dimensions and file size.
*
* Defaults target Anthropic's internal 1568px downscale threshold and produce small
* lossy JPEG output suitable for tool-call payloads (~100–500KB typical).
*
* Strategy:
* 1. Fast path — if input already fits dimensions AND is at <=25% of byte budget,
* return as-is. Avoids re-encoding tiny icons/diagrams.
* 2. Resize to maxWidth/maxHeight, encode both PNG and JPEG at default quality,
* pick whichever is smaller. PNG wins for line art / few-color UI; JPEG wins
* for photographic content.
* 3. If still too large, JPEG-only quality ladder (PNG quality is a no-op).
* 4. If still too large, progressively reduce dimensions and retry the JPEG ladder.
* 5. Last resort: ship the smallest variant produced.
* 1. Probe metadata. If already within all limits, return original.
* 2. Resize to fit max dimensions and encode at high quality across PNG/JPEG/WebP — return smallest.
* 3. If still too large, walk a lossy JPEG/WebP quality ladder.
* 4. If still too large, walk a dimension-scale ladder × quality ladder.
* 5. If still too large, return the smallest variant produced.
*
* On any decode failure, returns the original bytes unchanged with wasResized=false.
* Backed by `Bun.Image`: a chainable native pipeline that runs decode/transform/encode
* off the JS thread when the terminal (`.bytes()`) is awaited.
*/
export async function resizeImage(img: ImageContent, options?: ImageResizeOptions): Promise<ResizedImage> {
const opts = { ...DEFAULT_OPTIONS, ...options };
const inputBuffer = Buffer.from(img.data, "base64");
try {
const image = await PhotonImage.parse(inputBuffer);
const { width: originalWidth, height: originalHeight, format } = await new Bun.Image(inputBuffer).metadata();
const sourceMime = img.mimeType ?? `image/${format}`;
const originalWidth = image.width;
const originalHeight = image.height;
const format = img.mimeType?.split("/")[1] ?? "png";
// Check if already within all limits (dimensions AND size)
const originalSize = inputBuffer.length;
// Fast path: skip if already within dimensions AND well under budget.
// Fast path: already within dimensions AND well under budget.
// Threshold is 1/4 of budget — if already that compact, don't re-encode.
// Avoids wasted work on tiny icons/diagrams while ensuring larger PNGs
// still get JPEG-compressed.
const originalSize = inputBuffer.length;
const comfortableSize = opts.maxBytes / 4;
if (originalWidth <= opts.maxWidth && originalHeight <= opts.maxHeight && originalSize <= comfortableSize) {
return {
buffer: inputBuffer,
mimeType: img.mimeType ?? `image/${format}`,
mimeType: sourceMime,
originalWidth,
originalHeight,
width: originalWidth,
@@ -117,14 +107,11 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
height: number,
quality: number,
): Promise<{ buffer: Uint8Array; mimeType: string }> {
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
const [pngBuffer, jpegBuffer, webpBuffer] = await Promise.all([
resized.encode(ImageFormat.PNG, quality),
resized.encode(ImageFormat.JPEG, quality),
resized.encode(ImageFormat.WEBP, quality),
new Bun.Image(inputBuffer).resize(width, height).png().bytes(),
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
]);
return pickSmallest(
{ buffer: pngBuffer, mimeType: "image/png" },
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
@@ -140,10 +127,9 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
height: number,
quality: number,
): Promise<{ buffer: Uint8Array; mimeType: string }> {
const resized = await image.resize(width, height, SamplingFilter.Lanczos3);
const [jpegBuffer, webpBuffer] = await Promise.all([
resized.encode(ImageFormat.JPEG, quality),
resized.encode(ImageFormat.WEBP, quality),
new Bun.Image(inputBuffer).resize(width, height).jpeg({ quality }).bytes(),
new Bun.Image(inputBuffer).resize(width, height).webp({ quality }).bytes(),
]);
return pickSmallest(
{ buffer: jpegBuffer, mimeType: "image/jpeg" },
@@ -159,7 +145,7 @@ export async function resizeImage(img: ImageContent, options?: ImageResizeOption
let finalWidth = targetWidth;
let finalHeight = targetHeight;
// First attempt: resize to target, try both PNG and JPEG, pick smaller
// First attempt: resize to target, try PNG/JPEG/WebP, pick smallest
best = await encodeSmallest(targetWidth, targetHeight, opts.jpegQuality);
if (best.buffer.length <= opts.maxBytes) {
@@ -264,9 +250,11 @@ export function formatDimensionNote(result: ResizedImage): string | undefined {
if (!result.wasResized) {
return undefined;
}
const scale = result.originalWidth / result.width;
return `[Image: original ${result.originalWidth}x${result.originalHeight}, displayed at ${result.width}x${
result.height
}. Multiply coordinates by ${scale.toFixed(2)} to map to original image.]`;
if (!result.originalWidth || !result.originalHeight || !result.width || !result.height) {
return undefined;
}
if (result.width === result.originalWidth && result.height === result.originalHeight) {
return undefined;
}
return `Image resized from ${result.originalWidth}x${result.originalHeight} to ${result.width}x${result.height}.`;
}
@@ -1,20 +1,43 @@
import { afterEach, describe, expect, test, vi } from "bun:test";
import * as imageConvert from "../src/utils/image-convert";
import { describe, expect, test } from "bun:test";
import { ensureSupportedImageInput } from "../src/utils/image-loading";
// 1x1 red PNG (69 bytes). Bun.Image sniffs format from bytes, so we can pass
// this with a non-supported MIME type and the conversion path runs over the
// real native pipeline.
const RED_1X1_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC";
describe("ensureSupportedImageInput", () => {
afterEach(() => {
vi.restoreAllMocks();
test("passes supported mime types through unchanged", async () => {
const input = { type: "image" as const, data: RED_1X1_PNG_BASE64, mimeType: "image/png" };
const result = await ensureSupportedImageInput(input);
expect(result).toEqual(input);
});
test("converts unsupported image input to png", async () => {
const convertToPngSpy = vi
.spyOn(imageConvert, "convertToPng")
.mockResolvedValue({ data: "pngdata", mimeType: "image/png" });
const result = await ensureSupportedImageInput({
type: "image",
data: RED_1X1_PNG_BASE64,
mimeType: "image/bmp",
});
expect(result).not.toBeNull();
expect(result?.mimeType).toBe("image/png");
// PNG re-encode of a 1x1 image must yield a valid (non-empty) PNG signature
// (`89 50 4E 47`) when decoded from base64.
const bytes = Buffer.from(result!.data, "base64");
expect(bytes.length).toBeGreaterThan(0);
expect(bytes[0]).toBe(0x89);
expect(bytes[1]).toBe(0x50);
expect(bytes[2]).toBe(0x4e);
expect(bytes[3]).toBe(0x47);
});
const result = await ensureSupportedImageInput({ type: "image", data: "bmpdata", mimeType: "image/bmp" });
expect(convertToPngSpy).toHaveBeenCalledWith("bmpdata", "image/bmp");
expect(result).toEqual({ type: "image", data: "pngdata", mimeType: "image/png" });
test("returns null when input bytes are not a decodable image", async () => {
const result = await ensureSupportedImageInput({
type: "image",
data: Buffer.from("not an image").toString("base64"),
mimeType: "image/bmp",
});
expect(result).toBeNull();
});
});
@@ -1,17 +1,15 @@
import { describe, expect, it } from "bun:test";
import { ImageFormat, PhotonImage, SamplingFilter } from "@oh-my-pi/pi-natives";
import { resizeImage } from "../../src/utils/image-resize";
// 1x1 red PNG (69 bytes) — used as a Photon seed to synthesize larger fixtures
// 1x1 red PNG (69 bytes) — used as a Bun.Image seed to synthesize larger fixtures
// without checking binary blobs into the repo.
const RED_1X1_PNG_BASE64 =
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR4nGP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC";
async function makeRedPng(width: number, height: number): Promise<string> {
const seed = await PhotonImage.parse(new Uint8Array(Buffer.from(RED_1X1_PNG_BASE64, "base64")));
const upscaled = await seed.resize(width, height, SamplingFilter.Nearest);
const bytes = await upscaled.encode(ImageFormat.PNG, 100);
return Buffer.from(bytes).toBase64();
const seed = Buffer.from(RED_1X1_PNG_BASE64, "base64");
const upscaled = await new Bun.Image(seed).resize(width, height, { filter: "nearest" }).png().bytes();
return Buffer.from(upscaled).toBase64();
}
describe("resizeImage defaults", () => {
+5
View File
@@ -1,6 +1,11 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Raised the minimum required Bun runtime version to >=1.3.14
- Removed `PhotonImage` class, `ImageFormat` enum, and `SamplingFilter` enum from native exports. General-purpose image decode/resize/encode now uses [`Bun.Image`](https://bun.com/docs/runtime/image), which ships in Bun 1.3.14+ with statically-linked libjpeg-turbo, libspng, and libwebp plus SIMD geometry kernels — same operations, zero native-addon footprint. `encodeSixel` stays (no Bun equivalent for the SIXEL terminal protocol).
- Removed `webp` Rust workspace dependency along with `PhotonImage`'s WebP encoder.
## [14.9.9] - 2026-05-12
### Breaking Changes
+10 -7
View File
@@ -6,12 +6,17 @@ Native Rust functionality via N-API.
- **Grep**: Regex-based search powered by ripgrep's engine with native file walking and matching
- **Find**: Glob-based file/directory discovery with gitignore support (pure TypeScript via `globPaths`)
- **Image**: Image processing via photon-rs (resize, format conversion) exposed through N-API
- **SIXEL**: Terminal image encoding for SIXEL-capable terminals (decode, resize, encode in one pass)
General-purpose image processing (decode/resize/encode for files and buffers)
lives in [`Bun.Image`](https://bun.com/docs/runtime/image) on the JS side; this
crate only ships the SIXEL encoder because no built-in equivalent exists for
that terminal protocol.
## Usage
```typescript
import { grep, find, PhotonImage, SamplingFilter, ImageFormat } from "@oh-my-pi/pi-natives";
import { grep, find, encodeSixel } from "@oh-my-pi/pi-natives";
// Grep for a pattern
const results = await grep({
@@ -28,10 +33,8 @@ const files = await find({
fileType: "file",
});
// Image processing
const image = await PhotonImage.parse(bytes);
const resized = await image.resize(800, 600, SamplingFilter.Lanczos3);
const pngBytes = await resized.encode(ImageFormat.PNG, 100);
// SIXEL encode for a terminal cell box (px)
const sequence = encodeSixel(pngBytes, widthPx, heightPx);
```
## Building
@@ -49,7 +52,7 @@ bun run check
```
crates/pi-natives/ # Rust source (workspace member)
src/lib.rs # N-API exports
src/image.rs # Image processing (photon-rs)
src/sixel.rs # SIXEL terminal-image encoding
Cargo.toml # Rust dependencies
native/ # Native addon binaries
pi_natives.<platform>-<arch>-modern.node # x64 modern ISA (AVX2)
-54
View File
@@ -39,34 +39,6 @@ export declare class MacOSPowerAssertion {
stop(): void
}
/** Image container for native interop. */
export declare class PhotonImage {
/**
* Create a new `PhotonImage` from encoded image bytes (PNG, JPEG, WebP,
* GIF). Returns the decoded image handle on success.
*
* # Errors
* Returns an error if the image format cannot be detected or decoded.
*/
static parse(bytes: Uint8Array): ImageTask
/** Get the image width in pixels. */
get width(): number
/** Get the image height in pixels. */
get height(): number
/**
* Encode the image to bytes in the specified format.
*
* # Errors
* Returns an error if encoding fails or format is invalid.
*/
encode(format: ImageFormat, quality: number): Promise<Array<number>>
/**
* Resize the image to the specified pixel dimensions using the filter.
* Returns a new `PhotonImage` containing the resized image.
*/
resize(width: number, height: number, filter: SamplingFilter): ImageTask
}
/** Stable process reference. */
export declare class Process {
/** Open a stable process reference from a PID. */
@@ -742,18 +714,6 @@ export interface HtmlToMarkdownOptions {
skipImages?: boolean
}
/** Output format for [`PhotonImage::encode`]. */
export declare enum ImageFormat {
/** PNG encoded bytes. */
PNG = 0,
/** JPEG encoded bytes. */
JPEG = 1,
/** WebP encoded bytes. */
WEBP = 2,
/** GIF encoded bytes. */
GIF = 3
}
/**
* Invalidate the filesystem scan cache.
*
@@ -1149,20 +1109,6 @@ export interface PtyStartOptions {
*/
export declare function readImageFromClipboard(): Promise<ClipboardImage | undefined | null>
/** Sampling filter for resize operations. */
export declare enum SamplingFilter {
/** Nearest-neighbor sampling (fast, low quality). */
Nearest = 1,
/** Triangle filter (linear interpolation). */
Triangle = 2,
/** Catmull-Rom filter with sharper edges. */
CatmullRom = 3,
/** Gaussian filter for smoother results. */
Gaussian = 4,
/** Lanczos3 filter for high-quality downscaling. */
Lanczos3 = 5
}
/**
* Strip ANSI escape sequences, remove control characters / lone surrogates,
* and normalize line endings.
-14
View File
@@ -208,7 +208,6 @@ const nativeBindings = loadNative();
// classes
export const MacAppearanceObserver = nativeBindings.MacAppearanceObserver;
export const MacOSPowerAssertion = nativeBindings.MacOSPowerAssertion;
export const PhotonImage = nativeBindings.PhotonImage;
export const Process = nativeBindings.Process;
export const PtySession = nativeBindings.PtySession;
export const Shell = nativeBindings.Shell;
@@ -282,12 +281,6 @@ export const GrepOutputMode = {
Count: "count",
FilesWithMatches: "filesWithMatches",
};
export const ImageFormat = {
PNG: 0,
JPEG: 1,
WEBP: 2,
GIF: 3,
};
export const IsoBackendKind = {
Apfs: 0,
Btrfs: 1,
@@ -316,11 +309,4 @@ export const ProcessStatus = {
Running: "running",
Exited: "exited",
};
export const SamplingFilter = {
Nearest: 1,
Triangle: 2,
CatmullRom: 3,
Gaussian: 4,
Lanczos3: 5,
};
// --- end generated native exports ---
+1 -1
View File
@@ -44,7 +44,7 @@
"@types/bun": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"napi": {
"binaryName": "pi_natives",
+3
View File
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Raised the minimum required Bun version to >=1.3.14 in package metadata
### Changed
+1 -1
View File
@@ -55,7 +55,7 @@
"postcss": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"files": [
"src",
+2 -2
View File
@@ -34,13 +34,13 @@
"@oh-my-pi/pi-utils": "workspace:*"
},
"devDependencies": {
"@types/bun": "^1.3.13"
"@types/bun": "^1.3.14"
},
"peerDependencies": {
"@oh-my-pi/pi-coding-agent": "^13"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"omp": {
"extensions": [
+3
View File
@@ -1,6 +1,9 @@
# Changelog
## [Unreleased]
### Breaking Changes
- Increased the minimum required Bun version for the TUI package from >=1.3.7 to >=1.3.14
## [14.9.8] - 2026-05-12
+1 -1
View File
@@ -47,7 +47,7 @@
"@xterm/headless": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"files": [
"src",
@@ -45,6 +45,6 @@
"@types/bun": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
}
}
+1 -1
View File
@@ -41,7 +41,7 @@
"@oh-my-pi/pi-natives": "catalog:"
},
"engines": {
"bun": ">=1.3.7"
"bun": ">=1.3.14"
},
"files": [
"src"
+4 -1
View File
@@ -106,7 +106,7 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
console.log(`Building ${target.outfile}...`);
await embedNative(target);
if (isDryRun) {
console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --define process.env.PI_COMPILED="true" --root . --external mupdf --target=${target.target} ${entrypoint} --outfile ${target.outfile}`);
console.log(`DRY RUN bun build --compile --no-compile-autoload-bunfig --no-compile-autoload-dotenv --no-compile-autoload-tsconfig --no-compile-autoload-package-json --keep-names --define process.env.PI_COMPILED="true" --root . --external mupdf --target=${target.target} ${entrypoint} --outfile ${target.outfile}`);
return;
}
@@ -120,6 +120,9 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
"--compile",
"--no-compile-autoload-bunfig",
"--no-compile-autoload-dotenv",
"--no-compile-autoload-tsconfig",
"--no-compile-autoload-package-json",
"--keep-names",
"--define",
'process.env.PI_COMPILED="true"',
"--root",
+1 -1
View File
@@ -20,7 +20,7 @@ $Repo = "can1357/oh-my-pi"
$Package = "@oh-my-pi/pi-coding-agent"
$InstallDir = if ($env:PI_INSTALL_DIR) { $env:PI_INSTALL_DIR } else { "$env:LOCALAPPDATA\omp" }
$BinaryName = "omp-windows-x64.exe"
$MinimumBunVersion = "1.3.7"
$MinimumBunVersion = "1.3.14"
function Test-BunInstalled {
try {
+1 -1
View File
@@ -13,7 +13,7 @@ set -e
REPO="can1357/oh-my-pi"
PACKAGE="@oh-my-pi/pi-coding-agent"
INSTALL_DIR="${PI_INSTALL_DIR:-$HOME/.local/bin}"
MIN_BUN_VERSION="1.3.7"
MIN_BUN_VERSION="1.3.14"
# Parse arguments
MODE=""