@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
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 +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- 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 +4 -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 +2 -0
- package/dist/types/session/agent-session.d.ts +22 -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 +24 -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/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/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 +5 -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/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 +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -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 +321 -184
- 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/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- 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/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
|
|
|
@@ -5471,6 +5471,11 @@ export const SETTINGS_SCHEMA = {
|
|
|
5471
5471
|
default: undefined,
|
|
5472
5472
|
},
|
|
5473
5473
|
|
|
5474
|
+
"searxng.safesearch": {
|
|
5475
|
+
type: "number",
|
|
5476
|
+
default: undefined,
|
|
5477
|
+
},
|
|
5478
|
+
|
|
5474
5479
|
"commit.mapReduceEnabled": { type: "boolean", default: true },
|
|
5475
5480
|
|
|
5476
5481
|
"commit.mapReduceMinFiles": { type: "number", default: 4 },
|
package/src/cursor.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type {
|
|
|
18
18
|
ToolResultMessage,
|
|
19
19
|
} from "@oh-my-pi/pi-ai";
|
|
20
20
|
import {
|
|
21
|
+
omitUndefinedArgs,
|
|
21
22
|
piEscapeRegexLiteral,
|
|
22
23
|
piGrepSkip,
|
|
23
24
|
piJoinPath,
|
|
@@ -239,7 +240,11 @@ async function executeTool(
|
|
|
239
240
|
return createToolResultMessage(toolCallId, toolName, result, true);
|
|
240
241
|
}
|
|
241
242
|
|
|
242
|
-
|
|
243
|
+
// Same rule as synthesizeCursorExecToolCall: optional kwargs must be absent,
|
|
244
|
+
// not `undefined`, or ArkType validation rejects the call.
|
|
245
|
+
const toolArgs = omitUndefinedArgs(args);
|
|
246
|
+
|
|
247
|
+
options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs });
|
|
243
248
|
|
|
244
249
|
let result: AgentToolResult<unknown>;
|
|
245
250
|
let isError = false;
|
|
@@ -254,7 +259,7 @@ async function executeTool(
|
|
|
254
259
|
type: "tool_execution_update",
|
|
255
260
|
toolCallId,
|
|
256
261
|
toolName,
|
|
257
|
-
args,
|
|
262
|
+
args: toolArgs,
|
|
258
263
|
partialResult: sanitizedResult,
|
|
259
264
|
});
|
|
260
265
|
}
|
|
@@ -263,7 +268,7 @@ async function executeTool(
|
|
|
263
268
|
try {
|
|
264
269
|
result = await tool.execute(
|
|
265
270
|
toolCallId,
|
|
266
|
-
|
|
271
|
+
toolArgs as Record<string, unknown>,
|
|
267
272
|
undefined,
|
|
268
273
|
onUpdate,
|
|
269
274
|
options.getToolContext?.(),
|
|
@@ -509,11 +514,11 @@ export class CursorExecHandlers implements ICursorExecHandlers {
|
|
|
509
514
|
}
|
|
510
515
|
|
|
511
516
|
const timeoutSeconds = args.timeout && args.timeout > 0 ? args.timeout : undefined;
|
|
512
|
-
const toolArgs
|
|
517
|
+
const toolArgs = omitUndefinedArgs({
|
|
513
518
|
command: args.command,
|
|
514
519
|
cwd: args.workingDirectory || undefined,
|
|
515
520
|
timeout: timeoutSeconds,
|
|
516
|
-
};
|
|
521
|
+
});
|
|
517
522
|
|
|
518
523
|
this.options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args: toolArgs });
|
|
519
524
|
|
|
@@ -15,43 +15,81 @@ import { calculateDepth, createSourceMeta } from "./helpers";
|
|
|
15
15
|
const PROVIDER_ID = "agents-md";
|
|
16
16
|
const DISPLAY_NAME = "AGENTS.md";
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Compare paths while tolerating Windows drive casing.
|
|
20
|
+
*/
|
|
21
|
+
function samePath(left: string, right: string): boolean {
|
|
22
|
+
const normalizedLeft = path.resolve(left);
|
|
23
|
+
const normalizedRight = path.resolve(right);
|
|
24
|
+
return process.platform === "win32"
|
|
25
|
+
? normalizedLeft.toLowerCase() === normalizedRight.toLowerCase()
|
|
26
|
+
: normalizedLeft === normalizedRight;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Return whether `child` is at or below `parent`.
|
|
31
|
+
*/
|
|
32
|
+
function isWithin(parent: string, child: string): boolean {
|
|
33
|
+
const normalizedParent = path.resolve(parent);
|
|
34
|
+
const normalizedChild = path.resolve(child);
|
|
35
|
+
const relative = path.relative(
|
|
36
|
+
process.platform === "win32" ? normalizedParent.toLowerCase() : normalizedParent,
|
|
37
|
+
process.platform === "win32" ? normalizedChild.toLowerCase() : normalizedChild,
|
|
38
|
+
);
|
|
39
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
40
|
+
}
|
|
41
|
+
|
|
18
42
|
/**
|
|
19
43
|
* Load standalone AGENTS.md files.
|
|
44
|
+
*
|
|
45
|
+
* When a repository is nested below the user's home directory, continue past
|
|
46
|
+
* the Git root to discover workspace-level AGENTS.md files, but stop before
|
|
47
|
+
* loading the home directory's own AGENTS.md as project context.
|
|
20
48
|
*/
|
|
21
|
-
async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
|
|
49
|
+
export async function loadAgentsMd(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
|
|
22
50
|
const items: ContextFile[] = [];
|
|
23
51
|
const warnings: string[] = [];
|
|
52
|
+
const home = path.resolve(ctx.home);
|
|
53
|
+
const cwd = path.resolve(ctx.cwd);
|
|
54
|
+
const repoRoot = ctx.repoRoot ? path.resolve(ctx.repoRoot) : null;
|
|
55
|
+
const filesystemRoot = path.parse(cwd).root;
|
|
56
|
+
const cwdIsUnderHome = isWithin(home, cwd);
|
|
57
|
+
const repoIsUnderHome = repoRoot !== null && isWithin(home, repoRoot);
|
|
58
|
+
const scanToHome = repoRoot !== null && cwdIsUnderHome && repoIsUnderHome;
|
|
59
|
+
const boundary = scanToHome ? home : (repoRoot ?? (cwdIsUnderHome ? home : filesystemRoot));
|
|
60
|
+
const includeBoundary = repoRoot === null ? cwdIsUnderHome : !samePath(boundary, home);
|
|
61
|
+
const excludeHome = scanToHome;
|
|
24
62
|
|
|
25
|
-
|
|
26
|
-
let current = ctx.cwd;
|
|
27
|
-
|
|
63
|
+
let current = cwd;
|
|
28
64
|
while (true) {
|
|
29
|
-
const
|
|
30
|
-
const
|
|
65
|
+
const atBoundary = samePath(current, boundary);
|
|
66
|
+
const atHome = excludeHome && samePath(current, home);
|
|
67
|
+
if (!(atHome || (atBoundary && !includeBoundary))) {
|
|
68
|
+
const candidate = path.join(current, "AGENTS.md");
|
|
69
|
+
const content = await readFile(candidate);
|
|
31
70
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
71
|
+
if (content !== null) {
|
|
72
|
+
const parent = path.dirname(candidate);
|
|
73
|
+
const baseName = parent.split(path.sep).pop() ?? "";
|
|
35
74
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
75
|
+
if (!baseName.startsWith(".")) {
|
|
76
|
+
const fileDir = path.dirname(candidate);
|
|
77
|
+
const calculatedDepth = calculateDepth(cwd, fileDir, path.sep);
|
|
39
78
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
items.push({
|
|
80
|
+
path: candidate,
|
|
81
|
+
content,
|
|
82
|
+
level: "project",
|
|
83
|
+
depth: calculatedDepth,
|
|
84
|
+
_source: createSourceMeta(PROVIDER_ID, candidate, "project"),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
47
87
|
}
|
|
48
88
|
}
|
|
89
|
+
if (atBoundary) break;
|
|
49
90
|
|
|
50
|
-
if (current === (ctx.repoRoot ?? ctx.home)) break; // scanned repo root or home, stop
|
|
51
|
-
|
|
52
|
-
// Move to parent directory
|
|
53
91
|
const parent = path.dirname(current);
|
|
54
|
-
if (parent === current) break;
|
|
92
|
+
if (parent === current) break;
|
|
55
93
|
current = parent;
|
|
56
94
|
}
|
|
57
95
|
|