@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-za420td8.md} +63 -0
- package/dist/cli.js +3658 -3627
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +14 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +24 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +35 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/builtin-names.d.ts +1 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/think.d.ts +38 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/gallery-fixtures/agentic.ts +16 -0
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +16 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/controllers/selector-controller.ts +5 -0
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +146 -9
- package/src/session/agent-session-types.ts +4 -0
- package/src/session/agent-session.ts +91 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +355 -183
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/builtin-names.ts +1 -2
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/index.ts +8 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/think.ts +62 -0
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
package/src/async/job-manager.ts
CHANGED
|
@@ -5,6 +5,8 @@ const DELIVERY_RETRY_MAX_MS = 30_000;
|
|
|
5
5
|
const DELIVERY_RETRY_JITTER_MS = 200;
|
|
6
6
|
const DEFAULT_RETENTION_MS = 5 * 60 * 1000;
|
|
7
7
|
const DEFAULT_MAX_RUNNING_JOBS = 15;
|
|
8
|
+
/** Abort reason used only when the owning session shuts down the entire manager. */
|
|
9
|
+
export const ASYNC_JOB_MANAGER_SHUTDOWN_REASON = Symbol("AsyncJobManager shutdown");
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Adaptive ("smart") `hub` poll-wait ladder (ms). A tight poll loop climbs
|
|
@@ -414,11 +416,20 @@ export class AsyncJobManager {
|
|
|
414
416
|
* Cancel running jobs. With `filter.ownerId` set, cancels only jobs the
|
|
415
417
|
* matching agent registered; with no filter, cancels every running job
|
|
416
418
|
* (used by `dispose()` to nuke the manager's state).
|
|
419
|
+
*
|
|
420
|
+
* `reason` is forwarded to each job's `AbortController.abort`, so a session
|
|
421
|
+
* teardown can tag its owned jobs with {@link ASYNC_JOB_MANAGER_SHUTDOWN_REASON}
|
|
422
|
+
* before `dispose()` runs — the task executor reads it to park (not
|
|
423
|
+
* tombstone) a subagent interrupted purely by process shutdown.
|
|
417
424
|
*/
|
|
418
|
-
cancelAll(filter?: AsyncJobFilter): void {
|
|
425
|
+
cancelAll(filter?: AsyncJobFilter, reason?: unknown): void {
|
|
426
|
+
this.#cancelJobs(filter, reason);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
#cancelJobs(filter?: AsyncJobFilter, reason?: unknown): void {
|
|
419
430
|
for (const job of this.getRunningJobs(filter)) {
|
|
420
431
|
job.status = "cancelled";
|
|
421
|
-
job.abortController.abort();
|
|
432
|
+
job.abortController.abort(reason);
|
|
422
433
|
this.#scheduleEviction(job.id);
|
|
423
434
|
}
|
|
424
435
|
}
|
|
@@ -584,7 +595,7 @@ export class AsyncJobManager {
|
|
|
584
595
|
async dispose(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
585
596
|
this.#disposed = true;
|
|
586
597
|
this.#clearEvictionTimers();
|
|
587
|
-
this
|
|
598
|
+
this.#cancelJobs(undefined, ASYNC_JOB_MANAGER_SHUTDOWN_REASON);
|
|
588
599
|
const timeoutMs = Math.max(options?.timeoutMs ?? 3_000, 0);
|
|
589
600
|
const deadline = Date.now() + timeoutMs;
|
|
590
601
|
const jobsSettled = await this.#waitForAllUntil(deadline);
|
|
@@ -361,6 +361,22 @@ export const agenticFixtures: Record<string, GalleryFixture> = {
|
|
|
361
361
|
},
|
|
362
362
|
},
|
|
363
363
|
|
|
364
|
+
think: {
|
|
365
|
+
label: "Think",
|
|
366
|
+
// Streaming: scratchpad text still arriving.
|
|
367
|
+
streamingArgs: {
|
|
368
|
+
thoughts: "The retry loop re-reads the config after every failure — that explains the doubled latency.",
|
|
369
|
+
},
|
|
370
|
+
args: {
|
|
371
|
+
thoughts:
|
|
372
|
+
"The retry loop re-reads the config after every failure — that explains the doubled latency. Cache the parsed config outside the loop, then re-check the invalidation path before answering.",
|
|
373
|
+
},
|
|
374
|
+
result: {
|
|
375
|
+
content: [{ type: "text", text: "------" }],
|
|
376
|
+
details: { recorded: true },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
|
|
364
380
|
hub_jobs: {
|
|
365
381
|
label: "Hub jobs",
|
|
366
382
|
renderer: "hub",
|
package/src/cli/plugin-cli.ts
CHANGED
|
@@ -464,7 +464,7 @@ async function handleInstall(
|
|
|
464
464
|
async function handleUninstall(
|
|
465
465
|
manager: PluginManager,
|
|
466
466
|
packages: string[],
|
|
467
|
-
flags: { json?: boolean; scope?: "user" | "project" },
|
|
467
|
+
flags: { json?: boolean; dryRun?: boolean; scope?: "user" | "project" },
|
|
468
468
|
): Promise<void> {
|
|
469
469
|
if (packages.length === 0) {
|
|
470
470
|
console.error(chalk.red(`Usage: ${APP_NAME} plugin uninstall <package> ...`));
|
|
@@ -477,7 +477,35 @@ async function handleUninstall(
|
|
|
477
477
|
const installedPlugins = new Set((await mktMgr.listInstalledPlugins()).map(p => p.id));
|
|
478
478
|
|
|
479
479
|
for (const name of packages) {
|
|
480
|
-
|
|
480
|
+
const viaMarketplace = installedPlugins.has(name);
|
|
481
|
+
|
|
482
|
+
if (flags.dryRun) {
|
|
483
|
+
if (viaMarketplace) {
|
|
484
|
+
try {
|
|
485
|
+
await mktMgr.uninstallPlugin(name, flags.scope, { dryRun: true });
|
|
486
|
+
} catch (err) {
|
|
487
|
+
console.error(chalk.red(`${theme.status.error} Failed to uninstall ${name}: ${err}`));
|
|
488
|
+
process.exit(1);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Marketplace dry-runs validate the requested scope before reporting.
|
|
493
|
+
if (flags.json) {
|
|
494
|
+
console.log(
|
|
495
|
+
JSON.stringify({
|
|
496
|
+
dryRun: true,
|
|
497
|
+
action: "uninstall",
|
|
498
|
+
plugin: name,
|
|
499
|
+
source: viaMarketplace ? "marketplace" : "npm",
|
|
500
|
+
}),
|
|
501
|
+
);
|
|
502
|
+
} else {
|
|
503
|
+
console.log(chalk.dim(`[dry-run] Would uninstall ${name}`));
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
if (viaMarketplace) {
|
|
481
509
|
// Exact match against installed marketplace plugin IDs (name@marketplace)
|
|
482
510
|
try {
|
|
483
511
|
await mktMgr.uninstallPlugin(name, flags.scope);
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -63,9 +63,14 @@ function currentNativeTag(): string {
|
|
|
63
63
|
return `${process.platform}-${process.arch}`;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/** Distribution channel advertised by a release's published npm manifest. */
|
|
67
|
+
export type ReleaseDist = "npm" | "binary";
|
|
68
|
+
|
|
66
69
|
interface ReleaseInfo {
|
|
67
70
|
tag: string;
|
|
68
71
|
version: string;
|
|
72
|
+
/** Parsed `omp.dist` from the registry manifest; undefined when absent. */
|
|
73
|
+
dist?: ReleaseDist;
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
export interface ReleaseBinaryAsset {
|
|
@@ -80,6 +85,47 @@ function isRecord(value: unknown): value is Record<string, unknown> {
|
|
|
80
85
|
return typeof value === "object" && value !== null;
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Parse the `omp.dist` field from a published package manifest.
|
|
90
|
+
*
|
|
91
|
+
* Forward-compatibility contract with future releases: a release that is not
|
|
92
|
+
* installable as an npm package (e.g. a native rewrite) publishes
|
|
93
|
+
* `"omp": { "dist": "binary" }` in its package.json. Any value other than
|
|
94
|
+
* "npm" — including values this updater does not know yet — maps to "binary"
|
|
95
|
+
* so already-deployed updaters never run a package-manager install against a
|
|
96
|
+
* release that no longer supports it.
|
|
97
|
+
*/
|
|
98
|
+
export function resolveReleaseDist(manifest: unknown): ReleaseDist | undefined {
|
|
99
|
+
if (!isRecord(manifest) || !isRecord(manifest.omp)) return undefined;
|
|
100
|
+
const dist = manifest.omp.dist;
|
|
101
|
+
if (dist === undefined) return undefined;
|
|
102
|
+
return dist === "npm" ? "npm" : "binary";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function majorVersion(version: string): number {
|
|
106
|
+
const major = Number.parseInt(version, 10);
|
|
107
|
+
return Number.isNaN(major) ? 0 : major;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether the update must bypass bun/npm and install the release binary.
|
|
112
|
+
*
|
|
113
|
+
* An explicit `omp.dist` wins in both directions. Without one, a release with
|
|
114
|
+
* a higher major than the running build is assumed not npm-installable: the
|
|
115
|
+
* runtime may have changed out from under the package layout, and the pinned
|
|
116
|
+
* `@oh-my-pi/pi-natives*` companions ({@link buildBunInstallArgs}) may not
|
|
117
|
+
* exist at that version, which would strand bun/npm-managed installs behind a
|
|
118
|
+
* hard install failure. Homebrew and mise installs are unaffected — both
|
|
119
|
+
* already pull GitHub release binaries.
|
|
120
|
+
*/
|
|
121
|
+
export function shouldForceBinaryUpdate(
|
|
122
|
+
release: { version: string; dist?: ReleaseDist },
|
|
123
|
+
currentVersion: string = VERSION,
|
|
124
|
+
): boolean {
|
|
125
|
+
if (release.dist !== undefined) return release.dist === "binary";
|
|
126
|
+
return majorVersion(release.version) > majorVersion(currentVersion);
|
|
127
|
+
}
|
|
128
|
+
|
|
83
129
|
/**
|
|
84
130
|
* Select and validate the binary asset from GitHub release metadata.
|
|
85
131
|
*/
|
|
@@ -394,9 +440,9 @@ interface UpdateMethodResolutionOptions {
|
|
|
394
440
|
type UpdateTarget =
|
|
395
441
|
| { method: "brew" }
|
|
396
442
|
| { method: "mise" }
|
|
397
|
-
| { method: "bun" }
|
|
398
|
-
| { method: "npm" }
|
|
399
|
-
| { method: "binary"; path: string };
|
|
443
|
+
| { method: "bun"; path?: string }
|
|
444
|
+
| { method: "npm"; path?: string }
|
|
445
|
+
| { method: "binary"; path: string; replacesSymlink: boolean };
|
|
400
446
|
|
|
401
447
|
function resolveUpdateMethod(
|
|
402
448
|
ompPath: string,
|
|
@@ -433,9 +479,18 @@ export function resolveUpdateMethodForTest(
|
|
|
433
479
|
): UpdateMethod {
|
|
434
480
|
return resolveUpdateMethod(ompPath, bunBinDir, options);
|
|
435
481
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
482
|
+
/**
|
|
483
|
+
* Resolve how the running install should be updated.
|
|
484
|
+
*
|
|
485
|
+
* `allowPackageManagers: false` skips the `bun pm bin -g` / `npm prefix -g`
|
|
486
|
+
* probes entirely — used for binary-only releases, where routing through a
|
|
487
|
+
* package manager is never valid and the probes would be wasted subprocesses.
|
|
488
|
+
* Homebrew/mise detection always runs: both managers install GitHub release
|
|
489
|
+
* binaries and stay valid regardless of how the release is distributed.
|
|
490
|
+
*/
|
|
491
|
+
async function resolveUpdateTarget(options: { allowPackageManagers: boolean }): Promise<UpdateTarget> {
|
|
492
|
+
const bunBinDir = options.allowPackageManagers ? await getBunGlobalBinDir() : undefined;
|
|
493
|
+
const npmBinDir = options.allowPackageManagers ? await getNpmGlobalBinDir() : undefined;
|
|
439
494
|
const homebrewPrefix = await getHomebrewFormulaPrefix();
|
|
440
495
|
const miseAvailable = $which("mise") !== undefined;
|
|
441
496
|
const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
|
|
@@ -448,9 +503,11 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
448
503
|
// overlaps the installer's default (~/.local/bin), that file type — not
|
|
449
504
|
// directory containment — distinguishes a binary install from npm/bun.
|
|
450
505
|
let ompIsRegularFile = false;
|
|
506
|
+
let ompIsSymlink = false;
|
|
451
507
|
try {
|
|
452
508
|
const stat = fs.lstatSync(ompPath);
|
|
453
509
|
ompIsRegularFile = stat.isFile() && !stat.isSymbolicLink();
|
|
510
|
+
ompIsSymlink = stat.isSymbolicLink();
|
|
454
511
|
} catch {}
|
|
455
512
|
const method = resolveUpdateMethod(ompPath, bunBinDir, {
|
|
456
513
|
homebrewPrefix,
|
|
@@ -459,7 +516,8 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
459
516
|
npmBinDir,
|
|
460
517
|
ompIsRegularFile,
|
|
461
518
|
});
|
|
462
|
-
if (method === "binary") return { method, path: ompPath };
|
|
519
|
+
if (method === "binary") return { method, path: ompPath, replacesSymlink: ompIsSymlink };
|
|
520
|
+
if (method === "bun" || method === "npm") return { method, path: ompPath };
|
|
463
521
|
return { method };
|
|
464
522
|
}
|
|
465
523
|
|
|
@@ -488,13 +546,16 @@ async function getLatestRelease(): Promise<ReleaseInfo> {
|
|
|
488
546
|
throw new Error(`Failed to fetch release info: ${response.statusText}`);
|
|
489
547
|
}
|
|
490
548
|
|
|
491
|
-
const data =
|
|
549
|
+
const data: unknown = await response.json();
|
|
550
|
+
if (!isRecord(data) || typeof data.version !== "string") {
|
|
551
|
+
throw new Error("Malformed npm registry response: missing version");
|
|
552
|
+
}
|
|
492
553
|
const version = data.version;
|
|
493
|
-
const tag = `v${version}`;
|
|
494
554
|
|
|
495
555
|
return {
|
|
496
|
-
tag
|
|
556
|
+
tag: `v${version}`,
|
|
497
557
|
version,
|
|
558
|
+
dist: resolveReleaseDist(data),
|
|
498
559
|
};
|
|
499
560
|
}
|
|
500
561
|
|
|
@@ -788,24 +849,31 @@ function resolveOmpPath(): string | undefined {
|
|
|
788
849
|
}
|
|
789
850
|
|
|
790
851
|
/**
|
|
791
|
-
* Run
|
|
852
|
+
* Run a specific binary and check if it reports the expected version.
|
|
792
853
|
*/
|
|
793
|
-
async function
|
|
794
|
-
const ompPath = resolveOmpPath();
|
|
795
|
-
if (!ompPath) return { ok: false };
|
|
854
|
+
async function verifyBinaryAtPath(binaryPath: string, expectedVersion: string): Promise<InstalledVersionVerification> {
|
|
796
855
|
try {
|
|
797
|
-
const result = await $`${
|
|
798
|
-
if (result.exitCode !== 0) return { ok: false, path:
|
|
856
|
+
const result = await $`${binaryPath} --version`.quiet().nothrow();
|
|
857
|
+
if (result.exitCode !== 0) return { ok: false, path: binaryPath };
|
|
799
858
|
const output = result.text().trim();
|
|
800
859
|
// Output format: "omp/X.Y.Z"
|
|
801
860
|
const match = output.match(/\/(\d+\.\d+\.\d+)/);
|
|
802
861
|
const actual = match?.[1];
|
|
803
|
-
return { ok: actual === expectedVersion, actual, path:
|
|
862
|
+
return { ok: actual === expectedVersion, actual, path: binaryPath };
|
|
804
863
|
} catch {
|
|
805
|
-
return { ok: false, path:
|
|
864
|
+
return { ok: false, path: binaryPath };
|
|
806
865
|
}
|
|
807
866
|
}
|
|
808
867
|
|
|
868
|
+
/**
|
|
869
|
+
* Run the PATH-resolved omp binary and check if it reports the expected version.
|
|
870
|
+
*/
|
|
871
|
+
async function verifyInstalledVersion(expectedVersion: string): Promise<InstalledVersionVerification> {
|
|
872
|
+
const ompPath = resolveOmpPath();
|
|
873
|
+
if (!ompPath) return { ok: false };
|
|
874
|
+
return await verifyBinaryAtPath(ompPath, expectedVersion);
|
|
875
|
+
}
|
|
876
|
+
|
|
809
877
|
function printVerifiedVersion(expectedVersion: string): void {
|
|
810
878
|
console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
|
|
811
879
|
}
|
|
@@ -1108,6 +1176,145 @@ export async function updateViaBinaryAt(
|
|
|
1108
1176
|
console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
|
|
1109
1177
|
}
|
|
1110
1178
|
|
|
1179
|
+
/**
|
|
1180
|
+
* In-place forwarder bodies, by shim extension, for launchers that cannot be
|
|
1181
|
+
* renamed aside during a script-shim takeover; each execs the sibling
|
|
1182
|
+
* `omp.exe`. Rewriting matters for the shims that outrank `.exe` at command
|
|
1183
|
+
* resolution: PowerShell prefers `.ps1` and Git Bash resolves the
|
|
1184
|
+
* extensionless sh shim first, so leaving the old body behind would keep
|
|
1185
|
+
* launching the replaced install.
|
|
1186
|
+
*/
|
|
1187
|
+
const SHIM_FORWARDERS: Record<string, string> = {
|
|
1188
|
+
"": `#!/bin/sh\nexec "$(dirname "$0")/${APP_NAME}.exe" "$@"\n`,
|
|
1189
|
+
".cmd": `@"%~dp0${APP_NAME}.exe" %*\r\n`,
|
|
1190
|
+
".bat": `@"%~dp0${APP_NAME}.exe" %*\r\n`,
|
|
1191
|
+
".ps1": `& "$PSScriptRoot\\${APP_NAME}.exe" @args\nexit $LASTEXITCODE\n`,
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
/**
|
|
1195
|
+
* Take over a Windows script-launcher install for a binary-only release.
|
|
1196
|
+
*
|
|
1197
|
+
* npm-managed Windows installs are launched through script shims
|
|
1198
|
+
* (`omp`/`omp.cmd`/`omp.ps1`) that cannot be overwritten with a native
|
|
1199
|
+
* executable. The release binary is installed as `omp.exe` beside them and
|
|
1200
|
+
* the shims are then renamed aside: cmd.exe would already prefer `.exe` via
|
|
1201
|
+
* PATHEXT, but PowerShell resolves `.ps1` first, so the takeover only sticks
|
|
1202
|
+
* once the shims are out of the way. A working launcher exists at every
|
|
1203
|
+
* step — the exe lands before any shim moves, a shim that refuses to move
|
|
1204
|
+
* (a running `.cmd` can be renamed but may be held open some other way) is
|
|
1205
|
+
* rewritten in place as a forwarder to the exe, and a failed version
|
|
1206
|
+
* verification moves everything back.
|
|
1207
|
+
*/
|
|
1208
|
+
export async function updateViaShimTakeover(
|
|
1209
|
+
shimPath: string,
|
|
1210
|
+
expectedVersion: string,
|
|
1211
|
+
options: {
|
|
1212
|
+
binaryName?: string;
|
|
1213
|
+
fetchImpl?: Fetch;
|
|
1214
|
+
githubToken?: string;
|
|
1215
|
+
verifyBinary?: typeof verifyBinaryAtPath;
|
|
1216
|
+
} = {},
|
|
1217
|
+
): Promise<void> {
|
|
1218
|
+
const binaryName = options.binaryName ?? getBinaryName();
|
|
1219
|
+
const launcherDir = path.dirname(shimPath);
|
|
1220
|
+
const exePath = path.join(launcherDir, `${APP_NAME}.exe`);
|
|
1221
|
+
const tempPath = `${exePath}.new`;
|
|
1222
|
+
const asset = await getReleaseBinaryAsset(expectedVersion, binaryName, options.fetchImpl, options.githubToken);
|
|
1223
|
+
console.log(chalk.dim(`Downloading ${binaryName}…`));
|
|
1224
|
+
await downloadVerifiedBinary({
|
|
1225
|
+
url: asset.url,
|
|
1226
|
+
targetPath: tempPath,
|
|
1227
|
+
expectedSize: asset.size,
|
|
1228
|
+
expectedDigest: asset.digest,
|
|
1229
|
+
fetchImpl: options.fetchImpl,
|
|
1230
|
+
});
|
|
1231
|
+
console.log(chalk.dim(`Verified ${asset.digest}`));
|
|
1232
|
+
|
|
1233
|
+
console.log(chalk.dim(`Installing ${APP_NAME}.exe beside the script launcher...`));
|
|
1234
|
+
await fs.promises.rename(tempPath, exePath);
|
|
1235
|
+
// Retire the shims so PATH resolution lands on the new exe. Renamed, not
|
|
1236
|
+
// deleted: restorable on verification failure, and Windows permits
|
|
1237
|
+
// renaming a batch file that is still executing. A shim that cannot be
|
|
1238
|
+
// renamed (held open without delete sharing) is rewritten in place as a
|
|
1239
|
+
// forwarder to the exe — write and rename take different Windows locks,
|
|
1240
|
+
// so one can succeed where the other fails.
|
|
1241
|
+
const backupSuffix = `${Date.now()}.${process.pid}.bak`;
|
|
1242
|
+
const retired: Array<{ launcher: string; backup: string }> = [];
|
|
1243
|
+
const forwarded: Array<{ launcher: string; original: string }> = [];
|
|
1244
|
+
const stuck: string[] = [];
|
|
1245
|
+
for (const ext of ["", ".cmd", ".ps1", ".bat"]) {
|
|
1246
|
+
const launcher = path.join(launcherDir, `${APP_NAME}${ext}`);
|
|
1247
|
+
const backup = `${launcher}.${backupSuffix}`;
|
|
1248
|
+
try {
|
|
1249
|
+
await fs.promises.rename(launcher, backup);
|
|
1250
|
+
retired.push({ launcher, backup });
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
if (isEnoent(err)) continue;
|
|
1253
|
+
try {
|
|
1254
|
+
const original = await Bun.file(launcher).text();
|
|
1255
|
+
await Bun.write(launcher, SHIM_FORWARDERS[ext]);
|
|
1256
|
+
forwarded.push({ launcher, original });
|
|
1257
|
+
} catch {
|
|
1258
|
+
stuck.push(launcher);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Verify the exe by its explicit path: $which cached the shim path when
|
|
1264
|
+
// the update target was resolved, and the shim was just renamed away, so
|
|
1265
|
+
// a PATH re-resolution here would test a file that no longer exists.
|
|
1266
|
+
const verify = options.verifyBinary ?? verifyBinaryAtPath;
|
|
1267
|
+
const verification = await verify(exePath, expectedVersion);
|
|
1268
|
+
if (!verification.ok) {
|
|
1269
|
+
for (const { launcher, backup } of retired) {
|
|
1270
|
+
try {
|
|
1271
|
+
await fs.promises.rename(backup, launcher);
|
|
1272
|
+
} catch {}
|
|
1273
|
+
}
|
|
1274
|
+
for (const { launcher, original } of forwarded) {
|
|
1275
|
+
try {
|
|
1276
|
+
await Bun.write(launcher, original);
|
|
1277
|
+
} catch {}
|
|
1278
|
+
}
|
|
1279
|
+
await unlinkIfExists(exePath);
|
|
1280
|
+
throw new Error(
|
|
1281
|
+
`${formatVerificationFailure(verification, expectedVersion)}; restored previous ${APP_NAME} launcher`,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
for (const { backup } of retired) {
|
|
1285
|
+
await removeBackupBestEffort(backup);
|
|
1286
|
+
}
|
|
1287
|
+
// Reclaim exe backups and retired-shim leftovers from earlier attempts.
|
|
1288
|
+
for (const ext of [".exe", "", ".cmd", ".ps1", ".bat"]) {
|
|
1289
|
+
await sweepStaleBackups(path.join(launcherDir, `${APP_NAME}${ext}`));
|
|
1290
|
+
}
|
|
1291
|
+
for (const { launcher } of forwarded) {
|
|
1292
|
+
console.log(chalk.dim(`Converted ${launcher} to a forwarder (it could not be removed).`));
|
|
1293
|
+
}
|
|
1294
|
+
for (const launcher of stuck) {
|
|
1295
|
+
console.log(
|
|
1296
|
+
chalk.yellow(
|
|
1297
|
+
`Could not retire ${launcher}; shells that prefer it may keep launching the old version until it is deleted manually.`,
|
|
1298
|
+
),
|
|
1299
|
+
);
|
|
1300
|
+
}
|
|
1301
|
+
printVerifiedVersion(expectedVersion);
|
|
1302
|
+
console.log(chalk.dim(`Restart ${APP_NAME} to use the new version`));
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Platform-appropriate installer one-liner for recovery instructions.
|
|
1307
|
+
*
|
|
1308
|
+
* Forces the installer's binary mode (`--binary` / `-Binary`): the default
|
|
1309
|
+
* mode prefers a bun-based install whenever bun is present, which would send
|
|
1310
|
+
* a user recovering from a binary-only release straight back through bun.
|
|
1311
|
+
*/
|
|
1312
|
+
function installerHint(): string {
|
|
1313
|
+
return process.platform === "win32"
|
|
1314
|
+
? "& ([scriptblock]::Create((irm https://omp.sh/install.ps1))) -Binary"
|
|
1315
|
+
: "curl -fsSL https://omp.sh/install | sh -s -- --binary";
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1111
1318
|
/**
|
|
1112
1319
|
* Run the update command.
|
|
1113
1320
|
*/
|
|
@@ -1141,19 +1348,47 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
|
|
|
1141
1348
|
return;
|
|
1142
1349
|
}
|
|
1143
1350
|
|
|
1144
|
-
// Choose update method based on the prioritized omp binary in PATH
|
|
1351
|
+
// Choose update method based on the prioritized omp binary in PATH. For
|
|
1352
|
+
// binary-only releases the package managers are never consulted: a bun/npm
|
|
1353
|
+
// symlink resolves to method "binary" and is replaced in place, keeping the
|
|
1354
|
+
// same PATH entry live.
|
|
1145
1355
|
try {
|
|
1146
|
-
const
|
|
1356
|
+
const forceBinary = shouldForceBinaryUpdate(release);
|
|
1357
|
+
const target = await resolveUpdateTarget({ allowPackageManagers: !forceBinary });
|
|
1147
1358
|
if (target.method === "brew") {
|
|
1148
1359
|
await updateViaHomebrew(release.version, opts.force);
|
|
1149
1360
|
} else if (target.method === "mise") {
|
|
1150
1361
|
await updateViaMise(release.version, opts.force);
|
|
1151
|
-
} else if (target.method === "bun") {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1362
|
+
} else if (target.method === "bun" || target.method === "npm") {
|
|
1363
|
+
if (forceBinary) {
|
|
1364
|
+
// Reachable in forced mode only through a Windows script
|
|
1365
|
+
// launcher resolved from PATH (the bun/npm bin-dir probes are
|
|
1366
|
+
// skipped), so the launcher path is always known.
|
|
1367
|
+
if (!target.path) throw new Error(`Could not resolve ${APP_NAME} launcher path in PATH`);
|
|
1368
|
+
console.log(chalk.dim("This release ships as a standalone binary; replacing the script launcher."));
|
|
1369
|
+
await updateViaShimTakeover(target.path, release.version);
|
|
1370
|
+
console.log(
|
|
1371
|
+
chalk.yellow(
|
|
1372
|
+
`This install is no longer managed by ${target.method}. Removing the old global package may delete this launcher; if it does, reinstall with: ${installerHint()}`,
|
|
1373
|
+
),
|
|
1374
|
+
);
|
|
1375
|
+
} else if (target.method === "bun") {
|
|
1376
|
+
await updateViaBun(release.version);
|
|
1377
|
+
} else {
|
|
1378
|
+
await updateViaNpm(release.version);
|
|
1379
|
+
}
|
|
1155
1380
|
} else {
|
|
1381
|
+
if (forceBinary && target.replacesSymlink) {
|
|
1382
|
+
console.log(chalk.dim("Replacing the package-manager launcher with the standalone binary."));
|
|
1383
|
+
}
|
|
1156
1384
|
await updateViaBinaryAt(target.path, release.version);
|
|
1385
|
+
if (forceBinary && target.replacesSymlink) {
|
|
1386
|
+
console.log(
|
|
1387
|
+
chalk.yellow(
|
|
1388
|
+
`This install is no longer managed by bun/npm. Removing the old global package may delete this launcher; if it does, reinstall with: ${installerHint()}`,
|
|
1389
|
+
),
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1157
1392
|
}
|
|
1158
1393
|
} catch (err) {
|
|
1159
1394
|
console.error(chalk.red(`Update failed: ${err}`));
|
|
@@ -654,12 +654,52 @@ export class KeybindingsManager extends TuiKeybindingsManager {
|
|
|
654
654
|
|
|
655
655
|
/**
|
|
656
656
|
* Key hint formatting utilities for UI labels.
|
|
657
|
+
*
|
|
658
|
+
* Modifier labels are platform-aware: macOS names the physical keys `Option`
|
|
659
|
+
* (`alt`) and `Cmd` (`super`), so rendering `Alt`/`Super` there would name keys
|
|
660
|
+
* absent from a Mac keyboard. Every other platform keeps `Alt`/`Super`.
|
|
657
661
|
*/
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
662
|
+
|
|
663
|
+
/**
|
|
664
|
+
* Platform override for key-hint rendering; `undefined` resolves to the host
|
|
665
|
+
* `process.platform`. Mirrors `setKittyProtocolActive` in the TUI keys module:
|
|
666
|
+
* a single seam that keeps hint output deterministic in tests without mutating
|
|
667
|
+
* the global `process.platform`.
|
|
668
|
+
*/
|
|
669
|
+
let keyHintPlatformOverride: NodeJS.Platform | undefined;
|
|
670
|
+
|
|
671
|
+
/** Pin the platform used to render modifier labels (test seam). */
|
|
672
|
+
export function setKeyHintPlatform(platform: NodeJS.Platform | undefined): void {
|
|
673
|
+
keyHintPlatformOverride = platform;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
/** Platform currently used for key-hint rendering. */
|
|
677
|
+
export function keyHintPlatform(): NodeJS.Platform {
|
|
678
|
+
return keyHintPlatformOverride ?? process.platform;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
type Modifier = "ctrl" | "shift" | "alt" | "super";
|
|
682
|
+
|
|
683
|
+
function isModifier(part: string): part is Modifier {
|
|
684
|
+
return part === "ctrl" || part === "shift" || part === "alt" || part === "super";
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Human label for a modifier, using each platform's own key names. `ctrl` and
|
|
689
|
+
* `shift` are the same everywhere; `alt`/`super` become `Option`/`Cmd` on macOS.
|
|
690
|
+
*/
|
|
691
|
+
export function modifierLabel(mod: Modifier, platform: NodeJS.Platform = keyHintPlatform()): string {
|
|
692
|
+
switch (mod) {
|
|
693
|
+
case "ctrl":
|
|
694
|
+
return "Ctrl";
|
|
695
|
+
case "shift":
|
|
696
|
+
return "Shift";
|
|
697
|
+
case "alt":
|
|
698
|
+
return platform === "darwin" ? "Option" : "Alt";
|
|
699
|
+
case "super":
|
|
700
|
+
return platform === "darwin" ? "Cmd" : "Super";
|
|
701
|
+
}
|
|
702
|
+
}
|
|
663
703
|
|
|
664
704
|
const KEY_LABELS: Record<string, string> = {
|
|
665
705
|
esc: "Esc",
|
|
@@ -680,10 +720,9 @@ const KEY_LABELS: Record<string, string> = {
|
|
|
680
720
|
right: "Right",
|
|
681
721
|
};
|
|
682
722
|
|
|
683
|
-
function formatKeyPart(part: string): string {
|
|
723
|
+
function formatKeyPart(part: string, platform: NodeJS.Platform): string {
|
|
684
724
|
const lower = part.toLowerCase();
|
|
685
|
-
|
|
686
|
-
if (modifier) return modifier;
|
|
725
|
+
if (isModifier(lower)) return modifierLabel(lower, platform);
|
|
687
726
|
const label = KEY_LABELS[lower];
|
|
688
727
|
if (label) return label;
|
|
689
728
|
if (part.length === 1) return part.toUpperCase();
|
|
@@ -691,7 +730,11 @@ function formatKeyPart(part: string): string {
|
|
|
691
730
|
}
|
|
692
731
|
|
|
693
732
|
export function formatKeyHint(key: KeyId): string {
|
|
694
|
-
|
|
733
|
+
const platform = keyHintPlatform();
|
|
734
|
+
return key
|
|
735
|
+
.split("+")
|
|
736
|
+
.map(part => formatKeyPart(part, platform))
|
|
737
|
+
.join("+");
|
|
695
738
|
}
|
|
696
739
|
|
|
697
740
|
export function formatKeyHints(keys: KeyId | KeyId[]): string {
|
|
@@ -1138,14 +1138,30 @@ function resolveEffectiveAgentModelSelection(
|
|
|
1138
1138
|
return { patterns: resolveConfiguredModelPatterns(fallback, settings) };
|
|
1139
1139
|
}
|
|
1140
1140
|
|
|
1141
|
-
/**
|
|
1142
|
-
export
|
|
1143
|
-
|
|
1141
|
+
/** Effective agent model patterns paired with the pre-expansion role alias behind them. */
|
|
1142
|
+
export interface AgentModelSelection {
|
|
1143
|
+
/** Expanded model patterns to spawn with. */
|
|
1144
|
+
patterns: string[];
|
|
1145
|
+
/** Role alias the patterns came from (`@task` -> `task`), when the source named one. */
|
|
1146
|
+
role: string | undefined;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Resolve an agent's model patterns together with the role identity they were
|
|
1151
|
+
* expanded from. Spawn paths MUST take both from this single call: the child's
|
|
1152
|
+
* inherited retry-fallback chain is keyed off the role, which the expansion
|
|
1153
|
+
* discards, and deriving the two halves separately is how they drift apart.
|
|
1154
|
+
*/
|
|
1155
|
+
export function resolveAgentModelSelection(options: AgentModelPatternResolutionOptions): AgentModelSelection {
|
|
1156
|
+
const { source, patterns } = resolveEffectiveAgentModelSelection(options);
|
|
1157
|
+
return { patterns, role: resolveExplicitModelRole(source, options.settings) };
|
|
1144
1158
|
}
|
|
1145
1159
|
|
|
1160
|
+
/** Effective agent model patterns alone, for callers with no interest in role identity. */
|
|
1146
1161
|
export function resolveAgentModelPatterns(options: AgentModelPatternResolutionOptions): string[] {
|
|
1147
1162
|
return resolveEffectiveAgentModelSelection(options).patterns;
|
|
1148
1163
|
}
|
|
1164
|
+
|
|
1149
1165
|
/** Default prewalk hand-off target when no explicit target is configured. */
|
|
1150
1166
|
export const DEFAULT_PREWALK_TARGET = "@smol";
|
|
1151
1167
|
|
|
@@ -1139,6 +1139,17 @@ export const SETTINGS_SCHEMA = {
|
|
|
1139
1139
|
},
|
|
1140
1140
|
},
|
|
1141
1141
|
|
|
1142
|
+
externalThinking: {
|
|
1143
|
+
type: "boolean",
|
|
1144
|
+
default: false,
|
|
1145
|
+
ui: {
|
|
1146
|
+
tab: "model",
|
|
1147
|
+
group: "Thinking",
|
|
1148
|
+
label: "External Thinking",
|
|
1149
|
+
description: "Use a private think tool and send reasoning effort off to GPT Responses models",
|
|
1150
|
+
},
|
|
1151
|
+
},
|
|
1152
|
+
|
|
1142
1153
|
"model.loopGuard.enabled": {
|
|
1143
1154
|
type: "boolean",
|
|
1144
1155
|
default: true,
|
|
@@ -5471,6 +5482,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
5471
5482
|
default: undefined,
|
|
5472
5483
|
},
|
|
5473
5484
|
|
|
5485
|
+
"searxng.safesearch": {
|
|
5486
|
+
type: "number",
|
|
5487
|
+
default: undefined,
|
|
5488
|
+
},
|
|
5489
|
+
|
|
5474
5490
|
"commit.mapReduceEnabled": { type: "boolean", default: true },
|
|
5475
5491
|
|
|
5476
5492
|
"commit.mapReduceMinFiles": { type: "number", default: 4 },
|