From 1fb16a744b1c15c3c4a2bc27faba7ca487ad134f Mon Sep 17 00:00:00 2001 From: MAakber <121965090+MAakber@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:52:23 +0800 Subject: [PATCH 1/5] add tui status and fast toggle --- README.md | 18 + fast-status-state.test.ts | 238 +++++ fast-status-state.ts | 97 ++ fast-toggle.test.ts | 74 ++ fast-toggle.ts | 80 ++ index.ts | 8 +- package-lock.json | 1906 ++++++++++++++++++++++++++++++++++++- package.json | 37 +- tsconfig.json | 6 +- tui.tsx | 55 ++ 10 files changed, 2492 insertions(+), 27 deletions(-) create mode 100644 fast-status-state.test.ts create mode 100644 fast-status-state.ts create mode 100644 fast-toggle.test.ts create mode 100644 fast-toggle.ts create mode 100644 tui.tsx diff --git a/README.md b/README.md index 52993a4..c809287 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ An OpenCode plugin that adds `"service_tier": "priority"` to Codex requests when - Mirrors Codex Fast mode, which is documented as 1.5x faster at 2x credit cost - Leaves all non-Codex requests untouched - Persists a single global `enabled` flag in `~/.config/opencode/opencodex-fast.jsonc` +- Shows a `fast` indicator beside the session prompt while fast mode is enabled +- Supports `Ctrl+Y` in the base TUI mode to toggle fast mode globally ## Commands @@ -29,3 +31,19 @@ Add to your OpenCode config: "plugin": ["opencodex-fast@latest"], } ``` + +OpenCode 1.18.1 and newer can also load the package's TUI entry point. Add the +same package to your TUI config to show the status indicator: + +```jsonc +// tui.jsonc +{ + "plugin": ["opencodex-fast@latest"], +} +``` + +The server and TUI entry points are separate, so the plugin must be listed in +both files when configuring it manually. The TUI integration requires OpenCode +1.18.1+. Press `Ctrl+Y` in base mode to toggle the persisted global state; the +indicator uses the active theme's warning color and remains hidden while fast +mode is off. diff --git a/fast-status-state.test.ts b/fast-status-state.test.ts new file mode 100644 index 0000000..57eb5a8 --- /dev/null +++ b/fast-status-state.test.ts @@ -0,0 +1,238 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + createFastStateMonitor, + FAST_STATUS_POLL_MS, + parseFastState, + type FastStateMonitor, +} from "./fast-status-state.js"; + +type ReadResult = + | { readonly kind: "content"; readonly value: string } + | { readonly kind: "error" }; + +type Harness = { + readonly monitor: FastStateMonitor; + readonly tick: () => Promise; + readonly renderCount: () => number; + readonly readCount: () => number; + readonly intervalMs: () => number | undefined; + readonly stopCount: () => number; +}; + +function createHarness(reads: readonly ReadResult[]): Harness { + let index = 0; + let renders = 0; + let stops = 0; + let scheduledPoll: (() => Promise) | undefined; + let scheduledInterval: number | undefined; + + const monitor = createFastStateMonitor({ + path: "C:/config/opencodex-fast.jsonc", + async readText(): Promise { + const result = reads[index]; + index += 1; + if (result === undefined) { + throw new Error("Test read sequence exhausted"); + } + if (result.kind === "error") { + throw new Error("Transient read failure"); + } + return result.value; + }, + onChange(): void { + renders += 1; + }, + schedule(poll, intervalMs): () => void { + scheduledPoll = poll; + scheduledInterval = intervalMs; + return () => { + stops += 1; + }; + }, + }); + + return { + monitor, + async tick(): Promise { + const poll = scheduledPoll; + assert.ok(poll, "monitor should schedule polling"); + await poll(); + }, + renderCount: () => renders, + readCount: () => index, + intervalMs: () => scheduledInterval, + stopCount: () => stops, + }; +} + +test("parseFastState returns enabled when valid booleans are provided", () => { + const enabled = '\uFEFF{ "enabled": true }'; + const disabled = '{ "enabled": false, "other": "ignored" }'; + + const enabledResult = parseFastState(enabled); + const disabledResult = parseFastState(disabled); + + assert.deepEqual(enabledResult, { kind: "valid", enabled: true }); + assert.deepEqual(disabledResult, { kind: "valid", enabled: false }); +}); + +test("parseFastState accepts JSONC comments and trailing commas", () => { + const commented = '{ /* persisted fast mode */ "enabled": true }'; + const trailingComma = '{ "enabled": false, }'; + + const commentedResult = parseFastState(commented); + const trailingCommaResult = parseFastState(trailingComma); + + assert.deepEqual(commentedResult, { kind: "valid", enabled: true }); + assert.deepEqual(trailingCommaResult, { kind: "valid", enabled: false }); +}); + +test("parseFastState rejects malformed and wrong-shaped input", () => { + const invalidContents = ["{", "null", "[]", "{}", '{ "enabled": "true" }']; + + const results = invalidContents.map(parseFastState); + + assert.deepEqual( + results, + invalidContents.map(() => ({ kind: "invalid" })), + ); +}); + +test("monitor starts disabled when the initial state is invalid", async () => { + const harness = createHarness([{ kind: "content", value: "{" }]); + + await harness.monitor.start(); + + assert.equal(harness.monitor.isEnabled(), false); + assert.equal(harness.renderCount(), 0); + assert.equal(harness.intervalMs(), FAST_STATUS_POLL_MS); +}); + +test("monitor starts disabled when the initial state is unreadable", async () => { + const harness = createHarness([{ kind: "error" }]); + + await harness.monitor.start(); + + assert.equal(harness.monitor.isEnabled(), false); + assert.equal(harness.renderCount(), 0); + assert.equal(harness.intervalMs(), FAST_STATUS_POLL_MS); +}); + +test("monitor renders only for valid boolean transitions", async () => { + const harness = createHarness([ + { kind: "content", value: '{ "enabled": true }' }, + { kind: "content", value: '{ "enabled": true }' }, + { kind: "content", value: '{ "enabled": false }' }, + ]); + + await harness.monitor.start(); + await harness.tick(); + await harness.tick(); + + assert.equal(harness.monitor.isEnabled(), false); + assert.equal(harness.renderCount(), 2); +}); + +test("monitor preserves last-known-good state across failed polls", async () => { + const harness = createHarness([ + { kind: "content", value: '{ "enabled": true }' }, + { kind: "content", value: "{" }, + { kind: "error" }, + { kind: "content", value: '{ "enabled": false }' }, + ]); + + await harness.monitor.start(); + await harness.tick(); + assert.equal(harness.monitor.isEnabled(), true); + await harness.tick(); + assert.equal(harness.monitor.isEnabled(), true); + await harness.tick(); + + assert.equal(harness.monitor.isEnabled(), false); + assert.equal(harness.renderCount(), 2); +}); + +test("monitor skips overlapping refreshes", async () => { + let resolveRead: ((content: string) => void) | undefined; + let reads = 0; + const monitor = createFastStateMonitor({ + path: "C:/config/opencodex-fast.jsonc", + readText: () => { + reads += 1; + return new Promise((resolve) => { + resolveRead = resolve; + }); + }, + onChange(): void {}, + schedule: () => () => {}, + }); + + const first = monitor.refresh(); + const second = monitor.refresh(); + + assert.equal(reads, 1); + resolveRead?.('{ "enabled": true }'); + await Promise.all([first, second]); + assert.equal(monitor.isEnabled(), true); +}); + +test("monitor recovers after a synchronous read throw", async () => { + let reads = 0; + const monitor = createFastStateMonitor({ + path: "C:/config/opencodex-fast.jsonc", + readText: () => { + reads += 1; + if (reads === 1) throw new Error("Synchronous read failure"); + return Promise.resolve('{ "enabled": true }'); + }, + onChange(): void {}, + schedule: () => () => {}, + }); + + await monitor.refresh(); + await monitor.refresh(); + + assert.equal(monitor.isEnabled(), true); +}); + +test("monitor ignores an in-flight read after disposal", async () => { + let resolveRead: ((content: string) => void) | undefined; + let renders = 0; + const monitor = createFastStateMonitor({ + path: "C:/config/opencodex-fast.jsonc", + readText: () => + new Promise((resolve) => { + resolveRead = resolve; + }), + onChange(): void { + renders += 1; + }, + schedule: () => () => {}, + }); + + const refresh = monitor.refresh(); + monitor.dispose(); + resolveRead?.('{ "enabled": true }'); + await refresh; + + assert.equal(monitor.isEnabled(), false); + assert.equal(renders, 0); +}); + +test("monitor disposal is idempotent and blocks later polling work", async () => { + const harness = createHarness([ + { kind: "content", value: '{ "enabled": false }' }, + { kind: "content", value: '{ "enabled": true }' }, + ]); + await harness.monitor.start(); + + harness.monitor.dispose(); + harness.monitor.dispose(); + await harness.tick(); + + assert.equal(harness.stopCount(), 1); + assert.equal(harness.readCount(), 1); + assert.equal(harness.renderCount(), 0); +}); diff --git a/fast-status-state.ts b/fast-status-state.ts new file mode 100644 index 0000000..0f028bb --- /dev/null +++ b/fast-status-state.ts @@ -0,0 +1,97 @@ +import { parse, type ParseError } from "jsonc-parser"; +import { z } from "zod"; + +export const FAST_STATUS_POLL_MS = 500; + +const FastStateSchema = z.object({ + enabled: z.boolean(), +}); + +export type ParsedFastState = + | { readonly kind: "valid"; readonly enabled: boolean } + | { readonly kind: "invalid" }; + +export type FastStateMonitorOptions = { + readonly path: string; + readonly readText: (path: string) => Promise; + readonly onChange: (enabled: boolean) => void; + readonly schedule: ( + poll: () => Promise, + intervalMs: number, + ) => () => void; +}; + +export type FastStateMonitor = { + readonly isEnabled: () => boolean; + readonly start: () => Promise; + readonly refresh: () => Promise; + readonly dispose: () => void; +}; + +export function parseFastState(content: string): ParsedFastState { + const normalized = + content.charCodeAt(0) === 0xfeff ? content.slice(1) : content; + const errors: ParseError[] = []; + const decoded: unknown = parse(normalized, errors, { + allowTrailingComma: true, + }); + if (errors.length > 0) return { kind: "invalid" }; + + const parsed = FastStateSchema.safeParse(decoded); + return parsed.success + ? { kind: "valid", enabled: parsed.data.enabled } + : { kind: "invalid" }; +} + +export function createFastStateMonitor( + options: FastStateMonitorOptions, +): FastStateMonitor { + let enabled = false; + let disposed = false; + let started = false; + let refreshing = false; + let stop: (() => void) | undefined; + + async function refresh(): Promise { + if (disposed || refreshing) return; + refreshing = true; + + let readResult: + | { readonly kind: "readable"; readonly content: string } + | { readonly kind: "unreadable" }; + try { + const content = await options.readText(options.path); + readResult = { kind: "readable", content }; + } catch { + readResult = { kind: "unreadable" }; + } finally { + refreshing = false; + } + if (disposed || readResult.kind === "unreadable") return; + + const parsed = parseFastState(readResult.content); + if (parsed.kind === "invalid" || parsed.enabled === enabled) return; + + enabled = parsed.enabled; + options.onChange(enabled); + } + + return { + isEnabled: () => enabled, + async start(): Promise { + if (started || disposed) return; + started = true; + await refresh(); + if (disposed) return; + stop = options.schedule(refresh, FAST_STATUS_POLL_MS); + }, + refresh, + dispose(): void { + if (disposed) return; + disposed = true; + const currentStop = stop; + stop = undefined; + currentStop?.(); + }, + }; +} diff --git a/fast-toggle.test.ts b/fast-toggle.test.ts new file mode 100644 index 0000000..d52dc21 --- /dev/null +++ b/fast-toggle.test.ts @@ -0,0 +1,74 @@ +import assert from "node:assert/strict"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; + +import { + createFastToggleLayer, + FAST_TOGGLE_COMMAND, + readPersistedFastState, + togglePersistedFastState, +} from "./fast-toggle.js"; + +async function withStatePath( + run: (path: string) => Promise, +): Promise { + const directory = await mkdtemp(join(tmpdir(), "opencodex-fast-")); + try { + await run(join(directory, "nested", "opencodex-fast.jsonc")); + } finally { + await rm(directory, { recursive: true, force: true }); + } +} + +test("togglePersistedFastState writes false to true atomically", async () => { + await withStatePath(async (path) => { + const existingPath = join(dirname(path), "opencodex-fast.jsonc"); + await mkdir(dirname(existingPath), { recursive: true }); + await writeFile(existingPath, '{ "enabled": false }'); + + assert.equal(await togglePersistedFastState(existingPath), true); + assert.equal(await readPersistedFastState(existingPath), true); + assert.deepEqual(JSON.parse(await readFile(existingPath, "utf8")), { + enabled: true, + }); + }); +}); + +test("togglePersistedFastState writes true to false", async () => { + await withStatePath(async (path) => { + const existingPath = join(dirname(path), "opencodex-fast.jsonc"); + await mkdir(dirname(existingPath), { recursive: true }); + await writeFile(existingPath, '{ /* JSONC */ "enabled": true, }'); + + assert.equal(await togglePersistedFastState(existingPath), false); + assert.equal(await readPersistedFastState(existingPath), false); + }); +}); + +test("toggle layer registers the base Ctrl+Y command and refreshes after invoke", async () => { + await withStatePath(async (path) => { + let refreshes = 0; + const layer = createFastToggleLayer(path, async () => { + refreshes += 1; + }); + + assert.equal(layer.mode, "base"); + assert.deepEqual(layer.bindings, [ + { + key: "ctrl+y", + cmd: FAST_TOGGLE_COMMAND, + desc: "Toggle Fast mode", + }, + ]); + assert.equal(layer.commands[0].name, FAST_TOGGLE_COMMAND); + assert.equal(layer.commands[0].title, "Toggle Fast mode"); + assert.equal(layer.commands[0].category, "Plugin"); + assert.equal(layer.commands[0].namespace, "palette"); + + await layer.commands[0].run(); + assert.equal(await readPersistedFastState(path), true); + assert.equal(refreshes, 1); + }); +}); diff --git a/fast-toggle.ts b/fast-toggle.ts new file mode 100644 index 0000000..e98def6 --- /dev/null +++ b/fast-toggle.ts @@ -0,0 +1,80 @@ +import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import { dirname } from "node:path"; + +import { parseFastState } from "./fast-status-state.js"; + +export const FAST_TOGGLE_COMMAND = "opencodex-fast.toggle"; +export const FAST_TOGGLE_TITLE = "Toggle Fast mode"; + +export type FastToggleCommand = { + readonly name: typeof FAST_TOGGLE_COMMAND; + readonly title: typeof FAST_TOGGLE_TITLE; + readonly category: "Plugin"; + readonly namespace: "palette"; + readonly run: () => Promise; +}; + +export type FastToggleLayer = { + readonly mode: "base"; + readonly commands: readonly [FastToggleCommand]; + readonly bindings: readonly [{ + readonly key: "ctrl+y"; + readonly cmd: typeof FAST_TOGGLE_COMMAND; + readonly desc: typeof FAST_TOGGLE_TITLE; + }]; +}; + +export async function readPersistedFastState(path: string): Promise { + try { + const parsed = parseFastState(await readFile(path, "utf8")); + return parsed.kind === "valid" && parsed.enabled; + } catch { + return false; + } +} + +export async function togglePersistedFastState(path: string): Promise { + const enabled = !(await readPersistedFastState(path)); + await mkdir(dirname(path), { recursive: true }); + + const tempPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`; + await writeFile(tempPath, `${JSON.stringify({ enabled }, null, 2)}\n`, "utf8"); + await rename(tempPath, path); + return enabled; +} + +export function createFastToggleLayer( + path: string, + refresh: () => Promise, +): FastToggleLayer { + let inFlight = false; + + return { + mode: "base", + commands: [ + { + name: FAST_TOGGLE_COMMAND, + title: FAST_TOGGLE_TITLE, + category: "Plugin", + namespace: "palette", + async run(): Promise { + if (inFlight) return; + inFlight = true; + try { + await togglePersistedFastState(path); + await refresh(); + } finally { + inFlight = false; + } + }, + }, + ], + bindings: [ + { + key: "ctrl+y", + cmd: FAST_TOGGLE_COMMAND, + desc: FAST_TOGGLE_TITLE, + }, + ], + }; +} diff --git a/index.ts b/index.ts index 19c30df..62653c7 100644 --- a/index.ts +++ b/index.ts @@ -8,6 +8,7 @@ import { import { homedir } from "node:os"; import { dirname, join } from "node:path"; import type { Plugin } from "@opencode-ai/plugin"; +import { parseFastState } from "./fast-status-state.js"; const FAST_ON_MESSAGE = "Fast mode is now ON."; const FAST_OFF_MESSAGE = "Fast mode is now OFF."; @@ -63,9 +64,8 @@ function readState(): boolean { return false; } - const raw = readFileSync(STATE_PATH, "utf8"); - const parsed = JSON.parse(raw) as { enabled?: unknown }; - return parsed.enabled === true; + const parsed = parseFastState(readFileSync(STATE_PATH, "utf8")); + return parsed.kind === "valid" && parsed.enabled; } catch { return false; } @@ -74,6 +74,7 @@ function readState(): boolean { function maybeInjectPriority(init: any, input: any): any { const url = resolveUrl(input); if (!isCodexUrl(url)) return init; + fastEnabled = readState(); if (!fastEnabled) return init; const body = parseBody(init?.body); @@ -110,6 +111,7 @@ async function sendIgnoredMessage( } function getFastMessage(modeArg?: string): string { + fastEnabled = readState(); const normalized = modeArg?.toLowerCase(); if (normalized === "on") { diff --git a/package-lock.json b/package-lock.json index 6742618..6c84549 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,41 +7,1810 @@ "": { "name": "opencodex-fast", "version": "0.1.1", + "dependencies": { + "jsonc-parser": "^3.3.1", + "zod": "^4.1.8" + }, "devDependencies": { - "@opencode-ai/plugin": "^1.1.49", + "@opencode-ai/plugin": "^1.18.1", + "@opentui/solid": "^0.4.3", "@types/node": "^25.1.0", + "solid-js": "1.9.12", "typescript": "^5.9.3" }, - "peerDependencies": { - "@opencode-ai/plugin": ">=0.13.7" + "peerDependencies": { + "@opencode-ai/plugin": ">=0.13.7", + "@opentui/solid": ">=0.4.3", + "solid-js": ">=1.9.0" + }, + "peerDependenciesMeta": { + "@opentui/solid": { + "optional": true + }, + "solid-js": { + "optional": true + } + } + }, + "node_modules/@ai-sdk/provider": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-3.0.8.tgz", + "integrity": "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.18.1.tgz", + "integrity": "sha512-qE2Jg4Jh7NU18Wl69zoCz7fOvHQxKmUXkSq4akAqCixYDrGO7heYG/wfvafVSsx0n6jWhCdIN8cToNU6G4lKfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ai-sdk/provider": "3.0.8", + "@opencode-ai/sdk": "1.18.1", + "effect": "4.0.0-beta.83", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.4.3", + "@opentui/keymap": ">=0.4.3", + "@opentui/solid": ">=0.4.3" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.18.1.tgz", + "integrity": "sha512-2mX9SXSDVtZ9i2yCOiXDXKx981EJp/ObUtaThUNn16jkjfwX/1j8r+UR/BKUmBLYq+zle7TtpXreXuO+gtVjKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@opentui/core": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core/-/core-0.4.3.tgz", + "integrity": "sha512-rrJfAk13tALDqldYjhc78eWQ+aKq1iknJgffIOg3OwyZoqQo+p6gtuqyhmWvXIfQzlNUbpgpCPcxbXlhMnlaHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bun-ffi-structs": "0.2.4", + "diff": "9.0.0", + "marked": "17.0.1", + "string-width": "7.2.0", + "strip-ansi": "7.1.2" + }, + "optionalDependencies": { + "@opentui/core-darwin-arm64": "0.4.3", + "@opentui/core-darwin-x64": "0.4.3", + "@opentui/core-linux-arm64": "0.4.3", + "@opentui/core-linux-arm64-musl": "0.4.3", + "@opentui/core-linux-x64": "0.4.3", + "@opentui/core-linux-x64-musl": "0.4.3", + "@opentui/core-win32-arm64": "0.4.3", + "@opentui/core-win32-x64": "0.4.3" + }, + "peerDependencies": { + "web-tree-sitter": "0.25.10" + } + }, + "node_modules/@opentui/core-darwin-arm64": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-darwin-arm64/-/core-darwin-arm64-0.4.3.tgz", + "integrity": "sha512-p5+7AAxpxGuDGagyQfewKtmTFnN7THvTVY4FyKqUtJomNaHdQXPHztapNNzMx0DGWbwOUbVKzpL+yc3CZY3chQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@opentui/core-darwin-x64": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-darwin-x64/-/core-darwin-x64-0.4.3.tgz", + "integrity": "sha512-+fh0vEUE0lwVC7RW5ijYLRlTLp5NfvCRj8SzxDVd7IL2j2ssB6YXcfIbXq2EW7UGnrejwPRXf1tgUrIXW9KmOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@opentui/core-linux-arm64": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-arm64/-/core-linux-arm64-0.4.3.tgz", + "integrity": "sha512-gl6qA5QJy6u8Cbt7gOtHbhhfMZ4qQDb0kEwFXHcMGmbnKzz4OHoq74D6tNjyvSQB9saoC7C6C0tvn2DcJOuNog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-linux-arm64-musl": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-arm64-musl/-/core-linux-arm64-musl-0.4.3.tgz", + "integrity": "sha512-8p8g8/AEq/xFGpQ7XcIFKcAqjc0QwsZcv+Ll9RbCDpUA56FGH6jfLDir0KYTNTgYXJTIrBIENI9K46VuxMUMQA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-linux-x64": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-x64/-/core-linux-x64-0.4.3.tgz", + "integrity": "sha512-dXpJitiZdYE3hq2Pvx6e9I0uPQSOcnaLLp1pDgWAHv+3kvKSHEX//9Yr/pV/Ua6qqT7p+2D/K4vXNap/NKVo2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-linux-x64-musl": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-x64-musl/-/core-linux-x64-musl-0.4.3.tgz", + "integrity": "sha512-/QiFpCrpU2O7vy8QYmLIQYbvAtKDgmqcVjR7dGtqSzkiQk3ktNJoo5RozG7ueXnjung1Wp0nKldKxo2Csg/OrA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-win32-arm64": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-win32-arm64/-/core-win32-arm64-0.4.3.tgz", + "integrity": "sha512-Mx2zuOjrhm/z2SDS6RExIyjP/SnN/8QhhagxURUw0jQi/NssGSeAllu1cBAFFnhobJL5QLTE4FU4CRhUK9svgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opentui/core-win32-x64": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/core-win32-x64/-/core-win32-x64-0.4.3.tgz", + "integrity": "sha512-NuoqvWKGXaYnmlqvu7Gg2lLI6yVMnS9OfWBvxp+7Q+McSgHFSTQmYBXaPpvQ8HikpQXE1nCeMPtuSG4PdZHe2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opentui/solid": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@opentui/solid/-/solid-0.4.3.tgz", + "integrity": "sha512-RcV0+S8HMdXOASyr7HmJUBuTUIaFPzAxMDa44VftS5C2JUgrmAuWo0Njv1q3TWRB1owjHnyKhEfWGKq7A82wxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "7.28.0", + "@babel/preset-typescript": "7.27.1", + "@opentui/core": "0.4.3", + "babel-plugin-module-resolver": "5.0.2", + "babel-preset-solid": "1.9.12", + "entities": "7.0.1", + "s-js": "^0.4.9" + }, + "peerDependencies": { + "solid-js": "1.9.12" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.40.7", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.7.tgz", + "integrity": "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-plugin-module-resolver": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", + "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-babel-config": "^2.1.1", + "glob": "^9.3.3", + "pkg-up": "^3.1.0", + "reselect": "^4.1.7", + "resolve": "^1.22.8" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz", + "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.40.6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "solid-js": "^1.9.12" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.43", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.43.tgz", + "integrity": "sha512-AjYpR78kDWAY3Efj+cDTFH9t9SCoL7OoTp1BOb0mQV7S+6CiLwnWM3FyxhJtdPufDFKzmCSFoUncKjWgJEZTCQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.6.tgz", + "integrity": "sha512-FQBYNK15VMslhLHpA7+n+n1GOlF1kId2xcCg7/j95f24AOF6VDYMNH4mFxF7KuaTdv627faazpOAjFzMrfJOUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.10.42", + "caniuse-lite": "^1.0.30001803", + "electron-to-chromium": "^1.5.389", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bun-ffi-structs": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/bun-ffi-structs/-/bun-ffi-structs-0.2.4.tgz", + "integrity": "sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": "^5" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001805", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001805.tgz", + "integrity": "sha512-52noaS3DubycKSXaU30TwPGIp+POyQSUVa5jBEq3vkRkY0kjyb3LQgvhU6WGyCcyXqVLWO0Cw0Q6BSdD0kUfVA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.83", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.83.tgz", + "integrity": "sha512-0wsak8RtgGAr9UWSbVDgJHZcUqMSvicHcvaZv1MbMM7MCGgW4Rn/137J1MHQbwYPcwYGxT/IqehFd+UbYuj78w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.8.0", + "find-my-way-ts": "^0.1.6", + "ini": "^7.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^2.0.1", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^14.0.0", + "yaml": "^2.9.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.392", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.392.tgz", + "integrity": "sha512-1yQq3VQCZRwsnYc67Oc+1fge6Lwtn0hzi6zmEVkB61Zx21kTbwJAW4dFLadl5Rc1tKhG/kSpYXnfiAhu0f0a1g==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-check": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.9.0.tgz", + "integrity": "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-babel-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.1.2.tgz", + "integrity": "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.3" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ini": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-7.0.0.tgz", + "integrity": "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" } }, - "node_modules/@opencode-ai/plugin": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.3.tgz", - "integrity": "sha512-pxI4LanjnQb8sUd/zfQilzlGHyrdjmZuQ1XsUFbm+rij4yq0mUPtXcPGfuZJBEcGchKn57tA3/LB6RmipLQpXg==", + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "license": "MIT", "dependencies": { - "@opencode-ai/sdk": "1.3.3", - "zod": "4.1.8" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "node_modules/@opencode-ai/sdk": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.3.tgz", - "integrity": "sha512-qg7DwEVUpZArsYajs0DcaHqmIYB3EfHCuuTdMJir7Yc976DUWDfLR/5y9h8fKb9HAMJXe4TZkAIEr0/3OmK67g==", + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/minimatch": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz", + "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, "license": "MIT" }, - "node_modules/@types/node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", - "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "node_modules/msgpackr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", + "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", "dev": true, "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, "dependencies": { - "undici-types": "~7.18.0" + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multipasta": { + "version": "0.2.8", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.8.tgz", + "integrity": "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.51", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz", + "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.2.tgz", + "integrity": "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/s-js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/s-js/-/s-js-0.4.9.tgz", + "integrity": "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.5.tgz", + "integrity": "sha512-bSjOuPcwPKLSJNhr9+bZxA20nQxVle5J5MNsYRVE6cIg7KpRLXGupymePavu0jrxlPiPsr4xGZSB8yUY2sH2sw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.5.tgz", + "integrity": "sha512-+BDhqYM6CEn3x09v44dpa9p6974FuUB2dxk+Ctn04k0cO1Zt6QODTXfmEZK0eBaTe/fJBvP4NMGuNJ+R8T+QMg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/solid-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", + "integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.5.0", + "seroval-plugins": "~1.5.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toml": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz", + "integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" } }, "node_modules/typescript": { @@ -65,11 +1834,110 @@ "dev": true, "license": "MIT" }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/web-tree-sitter": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz", + "integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "@types/emscripten": "^1.40.0" + }, + "peerDependenciesMeta": { + "@types/emscripten": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/zod": { "version": "4.1.8", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 8bb86f9..3a70a57 100644 --- a/package.json +++ b/package.json @@ -5,22 +5,53 @@ "description": "OpenCode plugin that adds Codex priority service tier behind /fast", "main": "./dist/index.js", "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./server": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./tui": { + "types": "./dist/tui.d.ts", + "import": "./dist/tui.js" + } + }, "files": [ "dist", "README.md" ], "scripts": { - "clean": "rm -rf dist", + "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", "build": "npm run clean && tsc", + "test": "npm run build && node --test dist/fast-status-state.test.js dist/fast-toggle.test.js", "prepack": "npm run build", "dev": "opencode plugin dev" }, + "dependencies": { + "jsonc-parser": "^3.3.1", + "zod": "^4.1.8" + }, "peerDependencies": { - "@opencode-ai/plugin": ">=0.13.7" + "@opencode-ai/plugin": ">=0.13.7", + "@opentui/solid": ">=0.4.3", + "solid-js": ">=1.9.0" + }, + "peerDependenciesMeta": { + "@opentui/solid": { + "optional": true + }, + "solid-js": { + "optional": true + } }, "devDependencies": { - "@opencode-ai/plugin": "^1.1.49", + "@opencode-ai/plugin": "^1.18.1", + "@opentui/solid": "^0.4.3", "@types/node": "^25.1.0", + "solid-js": "1.9.12", "typescript": "^5.9.3" } } diff --git a/tsconfig.json b/tsconfig.json index 206b18f..2ddee11 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,8 +14,10 @@ "forceConsistentCasingInFileNames": true, "declaration": true, "sourceMap": true, - "types": ["node"] + "types": ["node"], + "jsx": "react-jsx", + "jsxImportSource": "@opentui/solid" }, - "include": ["index.ts"], + "include": ["index.ts", "tui.tsx", "fast-status-state.ts", "fast-toggle.ts", "fast-status-state.test.ts", "fast-toggle.test.ts"], "exclude": ["node_modules", "dist"] } diff --git a/tui.tsx b/tui.tsx new file mode 100644 index 0000000..d49d160 --- /dev/null +++ b/tui.tsx @@ -0,0 +1,55 @@ +/** @jsxImportSource @opentui/solid */ +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"; +import { createSignal } from "solid-js"; + +import { createFastStateMonitor } from "./fast-status-state.js"; +import { createFastToggleLayer } from "./fast-toggle.js"; + +const tui: TuiPlugin = async (api) => { + const [enabled, setEnabled] = createSignal(false); + const monitor = createFastStateMonitor({ + path: join(api.state.path.config, "opencodex-fast.jsonc"), + readText: (path) => readFile(path, "utf8"), + onChange(nextEnabled): void { + setEnabled(nextEnabled); + api.renderer.requestRender(); + }, + schedule(poll, intervalMs): () => void { + const interval = setInterval(() => { + void poll(); + }, intervalMs); + return () => clearInterval(interval); + }, + }); + + api.keymap.registerLayer( + createFastToggleLayer( + join(api.state.path.config, "opencodex-fast.jsonc"), + monitor.refresh, + ), + ); + + api.slots.register({ + order: 90, + slots: { + session_prompt_right() { + return enabled() ? ( + fast + ) : null; + }, + }, + }); + + api.lifecycle.onDispose(monitor.dispose); + await monitor.start(); +}; + +const plugin: TuiPluginModule = { + id: "opencodex-fast-status", + tui, +}; + +export default plugin; From 54c3ff2bd74fb356b0b5d658c35a3a18eb71c07f Mon Sep 17 00:00:00 2001 From: MAakber <121965090+MAakber@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:29:29 +0800 Subject: [PATCH 2/5] remove test scaffolding --- fast-status-state.test.ts | 238 -------------------------------------- fast-toggle.test.ts | 74 ------------ package.json | 1 - tsconfig.json | 2 +- 4 files changed, 1 insertion(+), 314 deletions(-) delete mode 100644 fast-status-state.test.ts delete mode 100644 fast-toggle.test.ts diff --git a/fast-status-state.test.ts b/fast-status-state.test.ts deleted file mode 100644 index 57eb5a8..0000000 --- a/fast-status-state.test.ts +++ /dev/null @@ -1,238 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "node:test"; - -import { - createFastStateMonitor, - FAST_STATUS_POLL_MS, - parseFastState, - type FastStateMonitor, -} from "./fast-status-state.js"; - -type ReadResult = - | { readonly kind: "content"; readonly value: string } - | { readonly kind: "error" }; - -type Harness = { - readonly monitor: FastStateMonitor; - readonly tick: () => Promise; - readonly renderCount: () => number; - readonly readCount: () => number; - readonly intervalMs: () => number | undefined; - readonly stopCount: () => number; -}; - -function createHarness(reads: readonly ReadResult[]): Harness { - let index = 0; - let renders = 0; - let stops = 0; - let scheduledPoll: (() => Promise) | undefined; - let scheduledInterval: number | undefined; - - const monitor = createFastStateMonitor({ - path: "C:/config/opencodex-fast.jsonc", - async readText(): Promise { - const result = reads[index]; - index += 1; - if (result === undefined) { - throw new Error("Test read sequence exhausted"); - } - if (result.kind === "error") { - throw new Error("Transient read failure"); - } - return result.value; - }, - onChange(): void { - renders += 1; - }, - schedule(poll, intervalMs): () => void { - scheduledPoll = poll; - scheduledInterval = intervalMs; - return () => { - stops += 1; - }; - }, - }); - - return { - monitor, - async tick(): Promise { - const poll = scheduledPoll; - assert.ok(poll, "monitor should schedule polling"); - await poll(); - }, - renderCount: () => renders, - readCount: () => index, - intervalMs: () => scheduledInterval, - stopCount: () => stops, - }; -} - -test("parseFastState returns enabled when valid booleans are provided", () => { - const enabled = '\uFEFF{ "enabled": true }'; - const disabled = '{ "enabled": false, "other": "ignored" }'; - - const enabledResult = parseFastState(enabled); - const disabledResult = parseFastState(disabled); - - assert.deepEqual(enabledResult, { kind: "valid", enabled: true }); - assert.deepEqual(disabledResult, { kind: "valid", enabled: false }); -}); - -test("parseFastState accepts JSONC comments and trailing commas", () => { - const commented = '{ /* persisted fast mode */ "enabled": true }'; - const trailingComma = '{ "enabled": false, }'; - - const commentedResult = parseFastState(commented); - const trailingCommaResult = parseFastState(trailingComma); - - assert.deepEqual(commentedResult, { kind: "valid", enabled: true }); - assert.deepEqual(trailingCommaResult, { kind: "valid", enabled: false }); -}); - -test("parseFastState rejects malformed and wrong-shaped input", () => { - const invalidContents = ["{", "null", "[]", "{}", '{ "enabled": "true" }']; - - const results = invalidContents.map(parseFastState); - - assert.deepEqual( - results, - invalidContents.map(() => ({ kind: "invalid" })), - ); -}); - -test("monitor starts disabled when the initial state is invalid", async () => { - const harness = createHarness([{ kind: "content", value: "{" }]); - - await harness.monitor.start(); - - assert.equal(harness.monitor.isEnabled(), false); - assert.equal(harness.renderCount(), 0); - assert.equal(harness.intervalMs(), FAST_STATUS_POLL_MS); -}); - -test("monitor starts disabled when the initial state is unreadable", async () => { - const harness = createHarness([{ kind: "error" }]); - - await harness.monitor.start(); - - assert.equal(harness.monitor.isEnabled(), false); - assert.equal(harness.renderCount(), 0); - assert.equal(harness.intervalMs(), FAST_STATUS_POLL_MS); -}); - -test("monitor renders only for valid boolean transitions", async () => { - const harness = createHarness([ - { kind: "content", value: '{ "enabled": true }' }, - { kind: "content", value: '{ "enabled": true }' }, - { kind: "content", value: '{ "enabled": false }' }, - ]); - - await harness.monitor.start(); - await harness.tick(); - await harness.tick(); - - assert.equal(harness.monitor.isEnabled(), false); - assert.equal(harness.renderCount(), 2); -}); - -test("monitor preserves last-known-good state across failed polls", async () => { - const harness = createHarness([ - { kind: "content", value: '{ "enabled": true }' }, - { kind: "content", value: "{" }, - { kind: "error" }, - { kind: "content", value: '{ "enabled": false }' }, - ]); - - await harness.monitor.start(); - await harness.tick(); - assert.equal(harness.monitor.isEnabled(), true); - await harness.tick(); - assert.equal(harness.monitor.isEnabled(), true); - await harness.tick(); - - assert.equal(harness.monitor.isEnabled(), false); - assert.equal(harness.renderCount(), 2); -}); - -test("monitor skips overlapping refreshes", async () => { - let resolveRead: ((content: string) => void) | undefined; - let reads = 0; - const monitor = createFastStateMonitor({ - path: "C:/config/opencodex-fast.jsonc", - readText: () => { - reads += 1; - return new Promise((resolve) => { - resolveRead = resolve; - }); - }, - onChange(): void {}, - schedule: () => () => {}, - }); - - const first = monitor.refresh(); - const second = monitor.refresh(); - - assert.equal(reads, 1); - resolveRead?.('{ "enabled": true }'); - await Promise.all([first, second]); - assert.equal(monitor.isEnabled(), true); -}); - -test("monitor recovers after a synchronous read throw", async () => { - let reads = 0; - const monitor = createFastStateMonitor({ - path: "C:/config/opencodex-fast.jsonc", - readText: () => { - reads += 1; - if (reads === 1) throw new Error("Synchronous read failure"); - return Promise.resolve('{ "enabled": true }'); - }, - onChange(): void {}, - schedule: () => () => {}, - }); - - await monitor.refresh(); - await monitor.refresh(); - - assert.equal(monitor.isEnabled(), true); -}); - -test("monitor ignores an in-flight read after disposal", async () => { - let resolveRead: ((content: string) => void) | undefined; - let renders = 0; - const monitor = createFastStateMonitor({ - path: "C:/config/opencodex-fast.jsonc", - readText: () => - new Promise((resolve) => { - resolveRead = resolve; - }), - onChange(): void { - renders += 1; - }, - schedule: () => () => {}, - }); - - const refresh = monitor.refresh(); - monitor.dispose(); - resolveRead?.('{ "enabled": true }'); - await refresh; - - assert.equal(monitor.isEnabled(), false); - assert.equal(renders, 0); -}); - -test("monitor disposal is idempotent and blocks later polling work", async () => { - const harness = createHarness([ - { kind: "content", value: '{ "enabled": false }' }, - { kind: "content", value: '{ "enabled": true }' }, - ]); - await harness.monitor.start(); - - harness.monitor.dispose(); - harness.monitor.dispose(); - await harness.tick(); - - assert.equal(harness.stopCount(), 1); - assert.equal(harness.readCount(), 1); - assert.equal(harness.renderCount(), 0); -}); diff --git a/fast-toggle.test.ts b/fast-toggle.test.ts deleted file mode 100644 index d52dc21..0000000 --- a/fast-toggle.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import assert from "node:assert/strict"; -import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; -import { test } from "node:test"; - -import { - createFastToggleLayer, - FAST_TOGGLE_COMMAND, - readPersistedFastState, - togglePersistedFastState, -} from "./fast-toggle.js"; - -async function withStatePath( - run: (path: string) => Promise, -): Promise { - const directory = await mkdtemp(join(tmpdir(), "opencodex-fast-")); - try { - await run(join(directory, "nested", "opencodex-fast.jsonc")); - } finally { - await rm(directory, { recursive: true, force: true }); - } -} - -test("togglePersistedFastState writes false to true atomically", async () => { - await withStatePath(async (path) => { - const existingPath = join(dirname(path), "opencodex-fast.jsonc"); - await mkdir(dirname(existingPath), { recursive: true }); - await writeFile(existingPath, '{ "enabled": false }'); - - assert.equal(await togglePersistedFastState(existingPath), true); - assert.equal(await readPersistedFastState(existingPath), true); - assert.deepEqual(JSON.parse(await readFile(existingPath, "utf8")), { - enabled: true, - }); - }); -}); - -test("togglePersistedFastState writes true to false", async () => { - await withStatePath(async (path) => { - const existingPath = join(dirname(path), "opencodex-fast.jsonc"); - await mkdir(dirname(existingPath), { recursive: true }); - await writeFile(existingPath, '{ /* JSONC */ "enabled": true, }'); - - assert.equal(await togglePersistedFastState(existingPath), false); - assert.equal(await readPersistedFastState(existingPath), false); - }); -}); - -test("toggle layer registers the base Ctrl+Y command and refreshes after invoke", async () => { - await withStatePath(async (path) => { - let refreshes = 0; - const layer = createFastToggleLayer(path, async () => { - refreshes += 1; - }); - - assert.equal(layer.mode, "base"); - assert.deepEqual(layer.bindings, [ - { - key: "ctrl+y", - cmd: FAST_TOGGLE_COMMAND, - desc: "Toggle Fast mode", - }, - ]); - assert.equal(layer.commands[0].name, FAST_TOGGLE_COMMAND); - assert.equal(layer.commands[0].title, "Toggle Fast mode"); - assert.equal(layer.commands[0].category, "Plugin"); - assert.equal(layer.commands[0].namespace, "palette"); - - await layer.commands[0].run(); - assert.equal(await readPersistedFastState(path), true); - assert.equal(refreshes, 1); - }); -}); diff --git a/package.json b/package.json index 3a70a57..887a770 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "scripts": { "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"", "build": "npm run clean && tsc", - "test": "npm run build && node --test dist/fast-status-state.test.js dist/fast-toggle.test.js", "prepack": "npm run build", "dev": "opencode plugin dev" }, diff --git a/tsconfig.json b/tsconfig.json index 2ddee11..393c3f1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,6 @@ "jsx": "react-jsx", "jsxImportSource": "@opentui/solid" }, - "include": ["index.ts", "tui.tsx", "fast-status-state.ts", "fast-toggle.ts", "fast-status-state.test.ts", "fast-toggle.test.ts"], + "include": ["index.ts", "tui.tsx", "fast-status-state.ts", "fast-toggle.ts"], "exclude": ["node_modules", "dist"] } From a4d3a2fa98672baf7a6a5121ba777e523b120bec Mon Sep 17 00:00:00 2001 From: MAakber <121965090+MAakber@users.noreply.github.com> Date: Thu, 16 Jul 2026 01:38:18 +0800 Subject: [PATCH 3/5] address tui review feedback --- README.md | 27 ++++++++++++++++----------- fast-toggle.ts | 13 +++++++++---- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c809287..bd44cbc 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,11 @@ An OpenCode plugin that adds `"service_tier": "priority"` to Codex requests when - When enabled, injects `service_tier: "priority"` into requests sent to `https://chatgpt.com/backend-api/codex/responses` - Mirrors Codex Fast mode, which is documented as 1.5x faster at 2x credit cost - Leaves all non-Codex requests untouched -- Persists a single global `enabled` flag in `~/.config/opencode/opencodex-fast.jsonc` -- Shows a `fast` indicator beside the session prompt while fast mode is enabled -- Supports `Ctrl+Y` in the base TUI mode to toggle fast mode globally +- Persists a single global `enabled` flag in the OpenCode config directory + (typically `~/.config/opencode/opencodex-fast.jsonc`) +- Shows a `fast` indicator beside the terminal TUI session prompt while fast + mode is enabled +- Supports `Ctrl+Y` in the terminal TUI's base mode to toggle fast mode globally ## Commands @@ -23,7 +25,8 @@ An OpenCode plugin that adds `"service_tier": "priority"` to Codex requests when ## Installation -Add to your OpenCode config: +Add to your OpenCode config to enable `/fast` and Codex request injection in +both OpenCode Desktop and the terminal client: ```jsonc // opencode.jsonc @@ -32,8 +35,9 @@ Add to your OpenCode config: } ``` -OpenCode 1.18.1 and newer can also load the package's TUI entry point. Add the -same package to your TUI config to show the status indicator: +OpenCode 1.18.1 and newer can also load the package's terminal TUI entry point. +Add the same package to your TUI config to show the status indicator and enable +`Ctrl+Y`: ```jsonc // tui.jsonc @@ -42,8 +46,9 @@ same package to your TUI config to show the status indicator: } ``` -The server and TUI entry points are separate, so the plugin must be listed in -both files when configuring it manually. The TUI integration requires OpenCode -1.18.1+. Press `Ctrl+Y` in base mode to toggle the persisted global state; the -indicator uses the active theme's warning color and remains hidden while fast -mode is off. +The server and TUI entry points are separate. List the plugin in both files for +the complete terminal experience; the Desktop app only uses the server entry +from `opencode.jsonc`. The status indicator and `Ctrl+Y` binding are therefore +terminal-only. The TUI integration requires OpenCode 1.18.1+. The indicator +uses the active theme's warning color and remains hidden while fast mode is +off. diff --git a/fast-toggle.ts b/fast-toggle.ts index e98def6..f0dc5f0 100644 --- a/fast-toggle.ts +++ b/fast-toggle.ts @@ -1,4 +1,5 @@ -import { mkdir, readFile, rename, writeFile } from "node:fs/promises"; +import { randomUUID } from "node:crypto"; +import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; import { dirname } from "node:path"; import { parseFastState } from "./fast-status-state.js"; @@ -37,9 +38,13 @@ export async function togglePersistedFastState(path: string): Promise { const enabled = !(await readPersistedFastState(path)); await mkdir(dirname(path), { recursive: true }); - const tempPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`; - await writeFile(tempPath, `${JSON.stringify({ enabled }, null, 2)}\n`, "utf8"); - await rename(tempPath, path); + const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`; + try { + await writeFile(tempPath, `${JSON.stringify({ enabled }, null, 2)}\n`, "utf8"); + await rename(tempPath, path); + } finally { + await unlink(tempPath).catch(() => undefined); + } return enabled; } From 68dac14d9f8f3699be91942b0778f1a83d96ab4e Mon Sep 17 00:00:00 2001 From: MAakber <121965090+MAakber@users.noreply.github.com> Date: Thu, 16 Jul 2026 13:04:05 +0800 Subject: [PATCH 4/5] show fast status on home screen --- README.md | 4 ++-- tui.tsx | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bd44cbc..ab95c18 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ An OpenCode plugin that adds `"service_tier": "priority"` to Codex requests when - Leaves all non-Codex requests untouched - Persists a single global `enabled` flag in the OpenCode config directory (typically `~/.config/opencode/opencodex-fast.jsonc`) -- Shows a `fast` indicator beside the terminal TUI session prompt while fast - mode is enabled +- Shows a `fast` indicator beside the terminal TUI home and session prompts + while fast mode is enabled - Supports `Ctrl+Y` in the terminal TUI's base mode to toggle fast mode globally ## Commands diff --git a/tui.tsx b/tui.tsx index d49d160..d42277e 100644 --- a/tui.tsx +++ b/tui.tsx @@ -35,6 +35,11 @@ const tui: TuiPlugin = async (api) => { api.slots.register({ order: 90, slots: { + home_prompt_right() { + return enabled() ? ( + fast + ) : null; + }, session_prompt_right() { return enabled() ? ( fast From 97a9eace67365a8f7398c115f406fddff7cf7fee Mon Sep 17 00:00:00 2001 From: MAakber <121965090+MAakber@users.noreply.github.com> Date: Thu, 16 Jul 2026 17:59:04 +0800 Subject: [PATCH 5/5] scope fast mode to sessions --- README.md | 64 +++++------- fast-session-state.ts | 24 +++++ fast-status-state.ts | 97 ------------------ fast-toggle.ts | 85 ---------------- index.ts | 230 ++++++++++++++++++------------------------ package-lock.json | 13 +-- package.json | 6 +- tsconfig.json | 2 +- tui.tsx | 166 +++++++++++++++++++++--------- 9 files changed, 269 insertions(+), 418 deletions(-) create mode 100644 fast-session-state.ts delete mode 100644 fast-status-state.ts delete mode 100644 fast-toggle.ts diff --git a/README.md b/README.md index ab95c18..ea92f61 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,36 @@ # opencodex-fast -An OpenCode plugin that adds `"service_tier": "priority"` to Codex requests when `/fast` is enabled globally. - -## What it does - -- Adds a `/fast` command to OpenCode -- When enabled, injects `service_tier: "priority"` into requests sent to `https://chatgpt.com/backend-api/codex/responses` -- Mirrors Codex Fast mode, which is documented as 1.5x faster at 2x credit cost -- Leaves all non-Codex requests untouched -- Persists a single global `enabled` flag in the OpenCode config directory - (typically `~/.config/opencode/opencodex-fast.jsonc`) -- Shows a `fast` indicator beside the terminal TUI home and session prompts - while fast mode is enabled -- Supports `Ctrl+Y` in the terminal TUI's base mode to toggle fast mode globally - -## Commands - -```text -/fast Toggle fast mode globally -/fast on Enable fast mode -/fast off Disable fast mode -/fast status Show current global fast-mode state -``` +An OpenCode plugin that adds `"service_tier": "priority"` to eligible Codex requests for a Fast-enabled chat session. + +## Per-session Fast mode + +- `/fast`, `/fast on`, `/fast off`, and `/fast status` apply only to the current session. +- Session metadata is the source of truth, so the same session stays synchronized in multiple TUI windows. Different sessions remain isolated. +- The server entry marks only Fast-enabled sessions. The request wrapper removes that private marker before forwarding every request and injects priority only for the Codex endpoint (`/backend-api/codex/responses`), leaving OpenAI API-key requests untouched. +- The legacy global `~/.config/opencode/opencodex-fast.jsonc` file is ignored and never modified. + +## Terminal TUI + +Install the TUI entry as well as the server entry to get indicators and `Ctrl+Y`: + +- In a session, `Ctrl+Y` toggles Fast for that session. The indicator is driven by reactive session metadata. +- On the home screen, `Ctrl+Y` arms Fast only in that TUI window. Its indicator shows the local arm/reservation state. The next newly created session in that window inherits Fast before its first request; the arm is one-shot and nonpersistent. +- Home-screen arming does not affect other TUI windows or existing sessions. ## Installation -Add to your OpenCode config to enable `/fast` and Codex request injection in -both OpenCode Desktop and the terminal client: +For OpenCode Desktop or server-only use, add the package only to the OpenCode configuration: ```jsonc -// opencode.jsonc -{ - "plugin": ["opencodex-fast@latest"], -} +// opencode.jsonc (server entry; Desktop and terminal) +{ "plugin": ["opencodex-fast@latest"] } ``` -OpenCode 1.18.1 and newer can also load the package's terminal TUI entry point. -Add the same package to your TUI config to show the status indicator and enable -`Ctrl+Y`: +For the complete terminal experience (session indicators and `Ctrl+Y`), add it to both `opencode.jsonc` and the TUI configuration: ```jsonc -// tui.jsonc -{ - "plugin": ["opencodex-fast@latest"], -} +// tui.jsonc (terminal TUI entry) +{ "plugin": ["opencodex-fast@latest"] } ``` -The server and TUI entry points are separate. List the plugin in both files for -the complete terminal experience; the Desktop app only uses the server entry -from `opencode.jsonc`. The status indicator and `Ctrl+Y` binding are therefore -terminal-only. The TUI integration requires OpenCode 1.18.1+. The indicator -uses the active theme's warning color and remains hidden while fast mode is -off. +OpenCode 1.18.1+ is required for the TUI integration. diff --git a/fast-session-state.ts b/fast-session-state.ts new file mode 100644 index 0000000..31da7b1 --- /dev/null +++ b/fast-session-state.ts @@ -0,0 +1,24 @@ +export const FAST_METADATA_KEY = "opencodex-fast.enabled"; + +export type SessionMetadata = Record; + +export function normalizeMetadata(metadata: unknown): SessionMetadata { + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { + return {}; + } + return { ...metadata }; +} + +export function isFastEnabled(metadata: unknown): boolean { + return normalizeMetadata(metadata)[FAST_METADATA_KEY] === true; +} + +export function withFastEnabled(metadata: unknown, enabled: boolean): SessionMetadata { + const next = normalizeMetadata(metadata); + if (enabled) { + next[FAST_METADATA_KEY] = true; + } else { + delete next[FAST_METADATA_KEY]; + } + return next; +} diff --git a/fast-status-state.ts b/fast-status-state.ts deleted file mode 100644 index 0f028bb..0000000 --- a/fast-status-state.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { parse, type ParseError } from "jsonc-parser"; -import { z } from "zod"; - -export const FAST_STATUS_POLL_MS = 500; - -const FastStateSchema = z.object({ - enabled: z.boolean(), -}); - -export type ParsedFastState = - | { readonly kind: "valid"; readonly enabled: boolean } - | { readonly kind: "invalid" }; - -export type FastStateMonitorOptions = { - readonly path: string; - readonly readText: (path: string) => Promise; - readonly onChange: (enabled: boolean) => void; - readonly schedule: ( - poll: () => Promise, - intervalMs: number, - ) => () => void; -}; - -export type FastStateMonitor = { - readonly isEnabled: () => boolean; - readonly start: () => Promise; - readonly refresh: () => Promise; - readonly dispose: () => void; -}; - -export function parseFastState(content: string): ParsedFastState { - const normalized = - content.charCodeAt(0) === 0xfeff ? content.slice(1) : content; - const errors: ParseError[] = []; - const decoded: unknown = parse(normalized, errors, { - allowTrailingComma: true, - }); - if (errors.length > 0) return { kind: "invalid" }; - - const parsed = FastStateSchema.safeParse(decoded); - return parsed.success - ? { kind: "valid", enabled: parsed.data.enabled } - : { kind: "invalid" }; -} - -export function createFastStateMonitor( - options: FastStateMonitorOptions, -): FastStateMonitor { - let enabled = false; - let disposed = false; - let started = false; - let refreshing = false; - let stop: (() => void) | undefined; - - async function refresh(): Promise { - if (disposed || refreshing) return; - refreshing = true; - - let readResult: - | { readonly kind: "readable"; readonly content: string } - | { readonly kind: "unreadable" }; - try { - const content = await options.readText(options.path); - readResult = { kind: "readable", content }; - } catch { - readResult = { kind: "unreadable" }; - } finally { - refreshing = false; - } - if (disposed || readResult.kind === "unreadable") return; - - const parsed = parseFastState(readResult.content); - if (parsed.kind === "invalid" || parsed.enabled === enabled) return; - - enabled = parsed.enabled; - options.onChange(enabled); - } - - return { - isEnabled: () => enabled, - async start(): Promise { - if (started || disposed) return; - started = true; - await refresh(); - if (disposed) return; - stop = options.schedule(refresh, FAST_STATUS_POLL_MS); - }, - refresh, - dispose(): void { - if (disposed) return; - disposed = true; - const currentStop = stop; - stop = undefined; - currentStop?.(); - }, - }; -} diff --git a/fast-toggle.ts b/fast-toggle.ts deleted file mode 100644 index f0dc5f0..0000000 --- a/fast-toggle.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises"; -import { dirname } from "node:path"; - -import { parseFastState } from "./fast-status-state.js"; - -export const FAST_TOGGLE_COMMAND = "opencodex-fast.toggle"; -export const FAST_TOGGLE_TITLE = "Toggle Fast mode"; - -export type FastToggleCommand = { - readonly name: typeof FAST_TOGGLE_COMMAND; - readonly title: typeof FAST_TOGGLE_TITLE; - readonly category: "Plugin"; - readonly namespace: "palette"; - readonly run: () => Promise; -}; - -export type FastToggleLayer = { - readonly mode: "base"; - readonly commands: readonly [FastToggleCommand]; - readonly bindings: readonly [{ - readonly key: "ctrl+y"; - readonly cmd: typeof FAST_TOGGLE_COMMAND; - readonly desc: typeof FAST_TOGGLE_TITLE; - }]; -}; - -export async function readPersistedFastState(path: string): Promise { - try { - const parsed = parseFastState(await readFile(path, "utf8")); - return parsed.kind === "valid" && parsed.enabled; - } catch { - return false; - } -} - -export async function togglePersistedFastState(path: string): Promise { - const enabled = !(await readPersistedFastState(path)); - await mkdir(dirname(path), { recursive: true }); - - const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`; - try { - await writeFile(tempPath, `${JSON.stringify({ enabled }, null, 2)}\n`, "utf8"); - await rename(tempPath, path); - } finally { - await unlink(tempPath).catch(() => undefined); - } - return enabled; -} - -export function createFastToggleLayer( - path: string, - refresh: () => Promise, -): FastToggleLayer { - let inFlight = false; - - return { - mode: "base", - commands: [ - { - name: FAST_TOGGLE_COMMAND, - title: FAST_TOGGLE_TITLE, - category: "Plugin", - namespace: "palette", - async run(): Promise { - if (inFlight) return; - inFlight = true; - try { - await togglePersistedFastState(path); - await refresh(); - } finally { - inFlight = false; - } - }, - }, - ], - bindings: [ - { - key: "ctrl+y", - cmd: FAST_TOGGLE_COMMAND, - desc: FAST_TOGGLE_TITLE, - }, - ], - }; -} diff --git a/index.ts b/index.ts index 62653c7..4984bd4 100644 --- a/index.ts +++ b/index.ts @@ -1,174 +1,140 @@ -import { - existsSync, - mkdirSync, - readFileSync, - renameSync, - writeFileSync, -} from "node:fs"; -import { homedir } from "node:os"; -import { dirname, join } from "node:path"; import type { Plugin } from "@opencode-ai/plugin"; -import { parseFastState } from "./fast-status-state.js"; -const FAST_ON_MESSAGE = "Fast mode is now ON."; -const FAST_OFF_MESSAGE = "Fast mode is now OFF."; -const FAST_HANDLED_ERROR = "__FAST_HANDLED__"; -const STATE_PATH = join( - process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), - "opencode", - "opencodex-fast.jsonc", -); +import { isFastEnabled, normalizeMetadata, withFastEnabled } from "./fast-session-state.js"; -let fastEnabled = false; +const FAST_ON_MESSAGE = "Fast mode is now ON for this session."; +const FAST_OFF_MESSAGE = "Fast mode is now OFF for this session."; +const FAST_HANDLED_ERROR = "__FAST_HANDLED__"; +const FAST_HEADER = "x-opencodex-fast"; -function ensureStateDir(): void { - mkdirSync(dirname(STATE_PATH), { recursive: true }); -} +type LegacySessionClient = { + get: (input: { path: { id: string } }) => Promise<{ data?: unknown; error?: unknown }>; + update: (input: { path: { id: string }; body: { metadata: Record } }) => Promise<{ error?: unknown }>; + prompt: (input: unknown) => Promise; +}; -function resolveUrl(input: any): string { +function resolveUrl(input: unknown): string { if (typeof input === "string") return input; if (input instanceof URL) return input.href; - return input?.url ?? ""; + return input && typeof input === "object" && "url" in input && typeof input.url === "string" + ? input.url + : ""; } function isCodexUrl(url: string): boolean { - return url.includes("/backend-api/codex/responses"); -} - -function parseBody(body: unknown): Record | null { - if (typeof body !== "string") return null; - try { - const parsed = JSON.parse(body); - if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { - return null; - } - return parsed as Record; + const parsed = new URL(url); + return parsed.protocol === "https:" + && parsed.hostname === "chatgpt.com" + && parsed.pathname.endsWith("/backend-api/codex/responses"); } catch { - return null; + return false; } } -function writeState(enabled: boolean): void { - ensureStateDir(); - const tempPath = `${STATE_PATH}.tmp`; - const content = `${JSON.stringify({ enabled }, null, 2)}\n`; - writeFileSync(tempPath, content, "utf8"); - renameSync(tempPath, STATE_PATH); -} - -function readState(): boolean { +function parseBody(body: unknown): Record | undefined { + if (typeof body !== "string") return undefined; try { - if (!existsSync(STATE_PATH)) { - writeState(false); - return false; - } - - const parsed = parseFastState(readFileSync(STATE_PATH, "utf8")); - return parsed.kind === "valid" && parsed.enabled; + const parsed: unknown = JSON.parse(body); + return parsed && typeof parsed === "object" && !Array.isArray(parsed) + ? parsed as Record + : undefined; } catch { - return false; + return undefined; } } -function maybeInjectPriority(init: any, input: any): any { - const url = resolveUrl(input); - if (!isCodexUrl(url)) return init; - fastEnabled = readState(); - if (!fastEnabled) return init; +/** Removes the private session marker and injects priority only for Codex OAuth requests. */ +export function prepareFastRequest(input: unknown, init?: RequestInit): RequestInit | undefined { + const inputHeaders = input && typeof input === "object" && "headers" in input + ? (input as { headers?: RequestInit["headers"] }).headers + : undefined; + const headers = new Headers(init?.headers ?? inputHeaders); + const enabled = headers.get(FAST_HEADER) === "true"; + headers.delete(FAST_HEADER); + const next: RequestInit = { ...init, headers }; + if (!enabled || !isCodexUrl(resolveUrl(input))) return next; const body = parseBody(init?.body); - if (!body) return init; - if (body.service_tier === "priority") return init; - - return { - ...init, - body: JSON.stringify({ - ...body, - service_tier: "priority", - }), - }; + if (!body || body.service_tier === "priority") return next; + return { ...next, body: JSON.stringify({ ...body, service_tier: "priority" }) }; } -async function sendIgnoredMessage( - client: any, - sessionID: string, - text: string, -): Promise { - await client.session.prompt({ - path: { id: sessionID }, - body: { - noReply: true, - parts: [ - { - type: "text", - text, - ignored: true, - }, - ], - }, - }); +function legacySessionClient(client: unknown): LegacySessionClient { + return (client as { session: LegacySessionClient }).session; } -function getFastMessage(modeArg?: string): string { - fastEnabled = readState(); - const normalized = modeArg?.toLowerCase(); - - if (normalized === "on") { - fastEnabled = true; - writeState(true); - return FAST_ON_MESSAGE; - } - - if (normalized === "off") { - fastEnabled = false; - writeState(false); - return FAST_OFF_MESSAGE; - } - - if (normalized === "status") { - return fastEnabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE; - } +function metadataFromResponse(data: unknown): Record { + if (!data || typeof data !== "object") return {}; + return normalizeMetadata((data as { metadata?: unknown }).metadata); +} - if (fastEnabled) { - fastEnabled = false; - writeState(false); - return FAST_OFF_MESSAGE; - } +async function readSessionMetadata(client: unknown, sessionID: string): Promise> { + const result = await legacySessionClient(client).get({ path: { id: sessionID } }); + if (result.error || !result.data) throw new Error("Could not read session metadata."); + return metadataFromResponse(result.data); +} - fastEnabled = true; - writeState(true); - return FAST_ON_MESSAGE; +async function sendIgnoredMessage(client: unknown, sessionID: string, text: string): Promise { + await legacySessionClient(client).prompt({ + path: { id: sessionID }, + body: { noReply: true, parts: [{ type: "text", text, ignored: true }] }, + }); } const plugin: Plugin = async (ctx) => { - fastEnabled = readState(); const originalFetch = globalThis.fetch; - - globalThis.fetch = async (input: any, init?: any) => { - const nextInit = maybeInjectPriority(init, input); - return originalFetch(input, nextInit); + const queues = new Map>(); + globalThis.fetch = async (input, init) => originalFetch(input, prepareFastRequest(input, init)); + + const queueSessionWrite = ( + sessionID: string, + operation: (metadata: Record) => boolean, + ): Promise => { + const previous = queues.get(sessionID) ?? Promise.resolve(); + let enabled = false; + const next = previous.catch(() => undefined).then(async () => { + const metadata = await readSessionMetadata(ctx.client, sessionID); + enabled = operation(metadata); + const result = await legacySessionClient(ctx.client).update({ + path: { id: sessionID }, + body: { metadata: withFastEnabled(metadata, enabled) }, + }); + if (result.error) throw new Error("Could not update session Fast mode."); + }); + queues.set(sessionID, next); + return next.then(() => enabled).finally(() => { + if (queues.get(sessionID) === next) queues.delete(sessionID); + }); }; return { config: async (opencodeConfig) => { opencodeConfig.command ??= {}; - opencodeConfig.command["fast"] = { - template: "[on|off|status]", - description: "Toggle Codex priority service tier injection", - }; + opencodeConfig.command.fast = { template: "[on|off|status]", description: "Toggle Codex priority service tier for this session" }; }, - - "command.execute.before": async ( - input: { command: string; sessionID: string; arguments: string }, - _output: { parts: any[] }, - ) => { - if (input.command !== "fast") { - return; + "chat.headers": async (input, output) => { + try { + if (isFastEnabled(await readSessionMetadata(ctx.client, input.sessionID))) { + output.headers[FAST_HEADER] = "true"; + } + } catch { + // Fail closed: without session metadata, never mark the request Fast. } - - const message = getFastMessage(input.arguments.trim() || undefined); - await sendIgnoredMessage(ctx.client, input.sessionID, message); + }, + "command.execute.before": async (input) => { + if (input.command !== "fast") return; + const mode = input.arguments.trim().toLowerCase(); + if (mode === "status") { + const enabled = isFastEnabled(await readSessionMetadata(ctx.client, input.sessionID)); + await sendIgnoredMessage(ctx.client, input.sessionID, enabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE); + throw new Error(FAST_HANDLED_ERROR); + } + const enabled = await queueSessionWrite( + input.sessionID, + (metadata) => mode === "on" ? true : mode === "off" ? false : !isFastEnabled(metadata), + ); + await sendIgnoredMessage(ctx.client, input.sessionID, enabled ? FAST_ON_MESSAGE : FAST_OFF_MESSAGE); throw new Error(FAST_HANDLED_ERROR); }, }; diff --git a/package-lock.json b/package-lock.json index 6c84549..d897f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,10 +7,6 @@ "": { "name": "opencodex-fast", "version": "0.1.1", - "dependencies": { - "jsonc-parser": "^3.3.1", - "zod": "^4.1.8" - }, "devDependencies": { "@opencode-ai/plugin": "^1.18.1", "@opentui/solid": "^0.4.3", @@ -19,7 +15,7 @@ "typescript": "^5.9.3" }, "peerDependencies": { - "@opencode-ai/plugin": ">=0.13.7", + "@opencode-ai/plugin": ">=1.18.1", "@opentui/solid": ">=0.4.3", "solid-js": ">=1.9.0" }, @@ -1340,12 +1336,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", - "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", - "license": "MIT" - }, "node_modules/kubernetes-types": { "version": "1.30.0", "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", @@ -1938,6 +1928,7 @@ "version": "4.1.8", "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index 887a770..e4df36c 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,8 @@ "prepack": "npm run build", "dev": "opencode plugin dev" }, - "dependencies": { - "jsonc-parser": "^3.3.1", - "zod": "^4.1.8" - }, "peerDependencies": { - "@opencode-ai/plugin": ">=0.13.7", + "@opencode-ai/plugin": ">=1.18.1", "@opentui/solid": ">=0.4.3", "solid-js": ">=1.9.0" }, diff --git a/tsconfig.json b/tsconfig.json index 393c3f1..f63b30e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,6 +18,6 @@ "jsx": "react-jsx", "jsxImportSource": "@opentui/solid" }, - "include": ["index.ts", "tui.tsx", "fast-status-state.ts", "fast-toggle.ts"], + "include": ["index.ts", "tui.tsx", "fast-session-state.ts"], "exclude": ["node_modules", "dist"] } diff --git a/tui.tsx b/tui.tsx index d42277e..0439912 100644 --- a/tui.tsx +++ b/tui.tsx @@ -1,60 +1,134 @@ /** @jsxImportSource @opentui/solid */ -import { readFile } from "node:fs/promises"; -import { join } from "node:path"; - import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"; import { createSignal } from "solid-js"; -import { createFastStateMonitor } from "./fast-status-state.js"; -import { createFastToggleLayer } from "./fast-toggle.js"; +import { isFastEnabled, normalizeMetadata, withFastEnabled } from "./fast-session-state.js"; -const tui: TuiPlugin = async (api) => { - const [enabled, setEnabled] = createSignal(false); - const monitor = createFastStateMonitor({ - path: join(api.state.path.config, "opencodex-fast.jsonc"), - readText: (path) => readFile(path, "utf8"), - onChange(nextEnabled): void { - setEnabled(nextEnabled); - api.renderer.requestRender(); +const FAST_TOGGLE_COMMAND = "opencodex-fast.toggle"; + +type SessionResult = { data?: { metadata?: unknown }; error?: unknown }; +type SessionCreateParameters = { + directory?: string; + workspace?: string; + parentID?: string; + title?: string; + agent?: string; + model?: { id: string; providerID: string; variant?: string }; + metadata?: Record; + permission?: unknown; + workspaceID?: string; +}; +type SessionClient = { + get: (input: { sessionID: string }) => Promise; + update: (input: { sessionID: string; metadata: Record }) => Promise; + create: (this: SessionClient, input?: SessionCreateParameters, options?: unknown) => Promise; +}; + +export function createHomeFastReservation(onChange: () => void): { + readonly isArmed: () => boolean; + readonly isReserved: () => boolean; + readonly toggle: () => void; + readonly wrap: (client: SessionClient, originalCreate: SessionClient["create"], isHome: () => boolean) => SessionClient["create"]; +} { + let armed = false; + let reserved = false; + let armVersion = 0; + const changed = (): void => onChange(); + + return { + isArmed: () => armed, + isReserved: () => reserved, + toggle(): void { + armVersion += 1; + armed = !armed; + changed(); }, - schedule(poll, intervalMs): () => void { - const interval = setInterval(() => { - void poll(); - }, intervalMs); - return () => clearInterval(interval); + wrap(client, originalCreate, isHome): SessionClient["create"] { + return async function wrappedCreate(parameters = {}, options): Promise { + if (!isHome() || !armed || reserved) { + return originalCreate.call(client, parameters, options); + } + const version = armVersion; + reserved = true; + changed(); + try { + const result = await originalCreate.call(client, { + ...parameters, + metadata: withFastEnabled(parameters.metadata, true), + }, options); + if (armVersion === version) armed = result.error ? true : false; + return result; + } catch (error) { + if (armVersion === version) armed = true; + throw error; + } finally { + reserved = false; + changed(); + } + }; }, - }); + }; +} - api.keymap.registerLayer( - createFastToggleLayer( - join(api.state.path.config, "opencodex-fast.jsonc"), - monitor.refresh, - ), - ); - - api.slots.register({ - order: 90, - slots: { - home_prompt_right() { - return enabled() ? ( - fast - ) : null; - }, - session_prompt_right() { - return enabled() ? ( - fast - ) : null; - }, - }, +const tui: TuiPlugin = async (api) => { + const client = api.client.session as unknown as SessionClient; + const [armed, setArmed] = createSignal(false); + const [reserved, setReserved] = createSignal(false); + const queues = new Map>(); + + const render = (): void => api.renderer.requestRender(); + const reservation = createHomeFastReservation((): void => { + setArmed(reservation.isArmed()); + setReserved(reservation.isReserved()); + render(); }); + const setSessionFast = (sessionID: string): Promise => { + const previous = queues.get(sessionID) ?? Promise.resolve(); + const next = previous.catch(() => undefined).then(async () => { + const current = await client.get({ sessionID }); + if (current.error || !current.data) throw new Error("Could not read session Fast mode."); + const updated = await client.update({ + sessionID, + metadata: withFastEnabled(normalizeMetadata(current.data.metadata), !isFastEnabled(current.data.metadata)), + }); + if (updated.error) throw new Error("Could not update session Fast mode."); + }); + queues.set(sessionID, next); + return next.finally(() => { + if (queues.get(sessionID) === next) queues.delete(sessionID); + }); + }; - api.lifecycle.onDispose(monitor.dispose); - await monitor.start(); -}; + const originalCreate = client.create; + const wrappedCreate = reservation.wrap(client, originalCreate, () => api.route.current.name === "home"); + client.create = wrappedCreate; -const plugin: TuiPluginModule = { - id: "opencodex-fast-status", - tui, + api.keymap.registerLayer({ + commands: [{ name: FAST_TOGGLE_COMMAND, title: "Toggle Fast mode", category: "Plugin", namespace: "palette", async run(): Promise { + if (api.route.current.name === "home") { + reservation.toggle(); + return; + } + const route = api.route.current; + if (route.name !== "session") return; + const sessionID = route.params?.sessionID; + if (typeof sessionID !== "string") return; + try { + await setSessionFast(sessionID); + } catch { + api.ui.toast({ variant: "error", message: "Could not update Fast mode for this session." }); + } + }}], + bindings: [{ key: "ctrl+y", cmd: FAST_TOGGLE_COMMAND, desc: "Toggle Fast mode" }], + }); + api.slots.register({ order: 90, slots: { + home_prompt_right: () => armed() || reserved() ? fast : null, + session_prompt_right: (_context, props) => isFastEnabled(api.state.session.get(props.session_id)?.metadata) ? fast : null, + }}); + api.lifecycle.onDispose(() => { + if (client.create === wrappedCreate) client.create = originalCreate; + }); }; +const plugin: TuiPluginModule = { id: "opencodex-fast", tui }; export default plugin;