@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.
Files changed (134) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
  3. package/dist/cli.js +3069 -3047
  4. package/dist/types/advisor/delta-split.d.ts +24 -0
  5. package/dist/types/advisor/runtime.d.ts +2 -2
  6. package/dist/types/async/job-manager.d.ts +8 -1
  7. package/dist/types/cli/update-cli.d.ts +53 -1
  8. package/dist/types/config/keybindings.d.ts +10 -0
  9. package/dist/types/config/model-resolver.d.ts +15 -2
  10. package/dist/types/config/settings-schema.d.ts +4 -0
  11. package/dist/types/discovery/agents-md.d.ts +10 -1
  12. package/dist/types/eval/runner-cache.d.ts +12 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +12 -3
  14. package/dist/types/extensibility/extensions/types.d.ts +15 -4
  15. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
  16. package/dist/types/lib/xai-http.d.ts +0 -1
  17. package/dist/types/mcp/tool-bridge.d.ts +8 -5
  18. package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/types.d.ts +4 -0
  20. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
  21. package/dist/types/modes/interactive-mode.d.ts +17 -8
  22. package/dist/types/modes/types.d.ts +7 -10
  23. package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
  24. package/dist/types/session/agent-session-types.d.ts +2 -0
  25. package/dist/types/session/agent-session.d.ts +22 -3
  26. package/dist/types/session/messages.d.ts +20 -0
  27. package/dist/types/session/retry-fallback-chains.d.ts +13 -0
  28. package/dist/types/session/session-advisors.d.ts +1 -1
  29. package/dist/types/session/session-history-format.d.ts +10 -0
  30. package/dist/types/session/session-maintenance.d.ts +1 -1
  31. package/dist/types/session/session-tools.d.ts +24 -5
  32. package/dist/types/session/turn-recovery.d.ts +34 -5
  33. package/dist/types/slash-commands/types.d.ts +5 -1
  34. package/dist/types/task/executor.d.ts +1 -1
  35. package/dist/types/tools/approval.d.ts +7 -0
  36. package/dist/types/tools/todo.d.ts +14 -15
  37. package/dist/types/tools/write.d.ts +2 -2
  38. package/dist/types/utils/local-date.d.ts +2 -0
  39. package/dist/types/vibe/runtime.d.ts +1 -1
  40. package/dist/types/web/parallel.d.ts +1 -0
  41. package/dist/types/web/search/providers/brave.d.ts +8 -3
  42. package/dist/types/web/search/providers/codex.d.ts +6 -0
  43. package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
  44. package/dist/types/web/search/providers/jina.d.ts +3 -3
  45. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  46. package/dist/types/web/search/providers/perplexity.d.ts +4 -0
  47. package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
  48. package/package.json +13 -13
  49. package/src/advisor/delta-split.ts +98 -0
  50. package/src/advisor/runtime.ts +321 -69
  51. package/src/async/job-manager.ts +14 -3
  52. package/src/cli/plugin-cli.ts +30 -2
  53. package/src/cli/update-cli.ts +259 -24
  54. package/src/config/keybindings.ts +52 -9
  55. package/src/config/model-resolver.ts +19 -3
  56. package/src/config/settings-schema.ts +5 -0
  57. package/src/cursor.ts +10 -5
  58. package/src/discovery/agents-md.ts +61 -23
  59. package/src/eval/jl/kernel.ts +2 -20
  60. package/src/eval/py/kernel.ts +2 -20
  61. package/src/eval/rb/kernel.ts +2 -20
  62. package/src/eval/runner-cache.ts +41 -0
  63. package/src/exec/non-interactive-env.ts +14 -3
  64. package/src/extensibility/extensions/loader.ts +5 -2
  65. package/src/extensibility/extensions/runner.ts +184 -66
  66. package/src/extensibility/extensions/types.ts +26 -2
  67. package/src/extensibility/extensions/wrapper.ts +13 -7
  68. package/src/extensibility/plugins/marketplace/manager.ts +6 -2
  69. package/src/hindsight/client.ts +1 -1
  70. package/src/lib/xai-http.ts +0 -4
  71. package/src/lsp/client.ts +2 -0
  72. package/src/lsp/servers.ts +1 -1
  73. package/src/mcp/tool-bridge.ts +15 -6
  74. package/src/modes/components/agent-hub-renderer.ts +9 -3
  75. package/src/modes/components/agent-hub.ts +2 -1
  76. package/src/modes/components/status-line/component.ts +58 -6
  77. package/src/modes/components/status-line/segments.ts +12 -1
  78. package/src/modes/components/status-line/types.ts +1 -0
  79. package/src/modes/components/user-message.ts +20 -5
  80. package/src/modes/controllers/event-controller.ts +48 -0
  81. package/src/modes/controllers/extension-ui-controller.ts +14 -7
  82. package/src/modes/controllers/input-controller.ts +25 -6
  83. package/src/modes/interactive-mode.ts +315 -129
  84. package/src/modes/rpc/rpc-frame.ts +13 -5
  85. package/src/modes/theme/tui-adapters.ts +4 -5
  86. package/src/modes/types.ts +13 -7
  87. package/src/modes/utils/hotkeys-markdown.ts +10 -6
  88. package/src/prompts/system/system-prompt.md +1 -1
  89. package/src/registry/persisted-agents.ts +43 -8
  90. package/src/sdk.ts +139 -8
  91. package/src/session/agent-session-types.ts +2 -0
  92. package/src/session/agent-session.ts +66 -10
  93. package/src/session/messages.ts +98 -28
  94. package/src/session/retry-fallback-chains.ts +14 -0
  95. package/src/session/session-advisors.ts +32 -15
  96. package/src/session/session-history-format.ts +15 -1
  97. package/src/session/session-maintenance.ts +8 -8
  98. package/src/session/session-manager.ts +6 -2
  99. package/src/session/session-tools.ts +321 -184
  100. package/src/session/turn-recovery.ts +225 -47
  101. package/src/slash-commands/builtin-modes.ts +41 -12
  102. package/src/slash-commands/types.ts +5 -1
  103. package/src/task/executor.ts +92 -45
  104. package/src/task/structured-subagent.ts +5 -5
  105. package/src/tools/approval.ts +44 -10
  106. package/src/tools/fetch.ts +21 -2
  107. package/src/tools/image-gen.ts +6 -8
  108. package/src/tools/todo.ts +70 -26
  109. package/src/tools/tts.ts +3 -2
  110. package/src/tools/write.ts +7 -3
  111. package/src/utils/local-date.ts +13 -0
  112. package/src/utils/tools-manager.ts +2 -2
  113. package/src/vibe/runtime.ts +22 -14
  114. package/src/web/kagi.ts +91 -34
  115. package/src/web/parallel.ts +11 -2
  116. package/src/web/scrapers/crates-io.ts +2 -2
  117. package/src/web/scrapers/discogs.ts +2 -2
  118. package/src/web/scrapers/docs-rs.ts +2 -2
  119. package/src/web/scrapers/github.ts +2 -2
  120. package/src/web/scrapers/musicbrainz.ts +1 -2
  121. package/src/web/scrapers/pubmed.ts +2 -2
  122. package/src/web/scrapers/sec-edgar.ts +2 -2
  123. package/src/web/search/providers/brave.ts +121 -46
  124. package/src/web/search/providers/codex.ts +88 -12
  125. package/src/web/search/providers/exa.ts +45 -10
  126. package/src/web/search/providers/firecrawl.ts +53 -11
  127. package/src/web/search/providers/gemini.ts +139 -27
  128. package/src/web/search/providers/jina.ts +48 -25
  129. package/src/web/search/providers/parallel.ts +23 -9
  130. package/src/web/search/providers/perplexity.ts +24 -7
  131. package/src/web/search/providers/searxng.ts +77 -1
  132. package/src/web/search/providers/tavily.ts +23 -22
  133. package/src/web/search/providers/tinyfish.ts +44 -10
  134. package/src/web/search/providers/xai.ts +85 -14
@@ -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
- async function resolveUpdateTarget(): Promise<UpdateTarget> {
437
- const bunBinDir = await getBunGlobalBinDir();
438
- const npmBinDir = await getNpmGlobalBinDir();
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 = (await response.json()) as { version: string };
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 the resolved omp binary and check if it reports the expected version.
852
+ * Run a specific binary and check if it reports the expected version.
792
853
  */
793
- async function verifyInstalledVersion(expectedVersion: string): Promise<InstalledVersionVerification> {
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 $`${ompPath} --version`.quiet().nothrow();
798
- if (result.exitCode !== 0) return { ok: false, path: ompPath };
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: ompPath };
862
+ return { ok: actual === expectedVersion, actual, path: binaryPath };
804
863
  } catch {
805
- return { ok: false, path: ompPath };
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 target = await resolveUpdateTarget();
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
- await updateViaBun(release.version);
1153
- } else if (target.method === "npm") {
1154
- await updateViaNpm(release.version);
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
- const MODIFIER_LABELS: Record<string, string> = {
659
- ctrl: "Ctrl",
660
- shift: "Shift",
661
- alt: "Alt",
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
- const modifier = MODIFIER_LABELS[lower];
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
- return key.split("+").map(formatKeyPart).join("+");
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
- /** Return the raw selector source that supplies the effective agent patterns. */
1142
- export function resolveAgentModelSource(options: AgentModelPatternResolutionOptions): string | string[] | undefined {
1143
- return resolveEffectiveAgentModelSelection(options).source;
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
- options.emitEvent?.({ type: "tool_execution_start", toolCallId, toolName, args });
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
- args as Record<string, unknown>,
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: Record<string, unknown> = {
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
- // Walk up from cwd looking for AGENTS.md files
26
- let current = ctx.cwd;
27
-
63
+ let current = cwd;
28
64
  while (true) {
29
- const candidate = path.join(current, "AGENTS.md");
30
- const content = await readFile(candidate);
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
- if (content !== null) {
33
- const parent = path.dirname(candidate);
34
- const baseName = parent.split(path.sep).pop() ?? "";
71
+ if (content !== null) {
72
+ const parent = path.dirname(candidate);
73
+ const baseName = parent.split(path.sep).pop() ?? "";
35
74
 
36
- if (!baseName.startsWith(".")) {
37
- const fileDir = path.dirname(candidate);
38
- const calculatedDepth = calculateDepth(ctx.cwd, fileDir, path.sep);
75
+ if (!baseName.startsWith(".")) {
76
+ const fileDir = path.dirname(candidate);
77
+ const calculatedDepth = calculateDepth(cwd, fileDir, path.sep);
39
78
 
40
- items.push({
41
- path: candidate,
42
- content,
43
- level: "project",
44
- depth: calculatedDepth,
45
- _source: createSourceMeta(PROVIDER_ID, candidate, "project"),
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; // Reached filesystem root
92
+ if (parent === current) break;
55
93
  current = parent;
56
94
  }
57
95