@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2

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 (198) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/dist/cli.js +3556 -3504
  3. package/dist/types/advisor/advise-tool.d.ts +12 -1
  4. package/dist/types/advisor/runtime.d.ts +41 -1
  5. package/dist/types/cli/args.d.ts +2 -0
  6. package/dist/types/cli/update-cli.d.ts +4 -1
  7. package/dist/types/cli/usage-cli.d.ts +3 -0
  8. package/dist/types/cli/usage-error.d.ts +4 -0
  9. package/dist/types/config/api-key-resolver.d.ts +2 -2
  10. package/dist/types/config/model-registry.d.ts +3 -3
  11. package/dist/types/config/model-resolver.d.ts +8 -1
  12. package/dist/types/config/models-config.d.ts +1 -1
  13. package/dist/types/config/settings-schema.d.ts +10 -0
  14. package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
  15. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
  16. package/dist/types/eval/backend.d.ts +3 -3
  17. package/dist/types/eval/bridge-timeout.d.ts +9 -1
  18. package/dist/types/eval/js/context-manager.d.ts +5 -3
  19. package/dist/types/eval/js/process-entry.d.ts +6 -0
  20. package/dist/types/eval/js/worker-core.d.ts +15 -1
  21. package/dist/types/eval/py/spawn-options.d.ts +10 -0
  22. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  23. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  24. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  25. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  26. package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
  27. package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
  28. package/dist/types/goals/guided-setup.d.ts +12 -0
  29. package/dist/types/internal-urls/history-protocol.d.ts +3 -2
  30. package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
  31. package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
  32. package/dist/types/main.d.ts +1 -0
  33. package/dist/types/mcp/oauth-discovery.d.ts +2 -0
  34. package/dist/types/mcp/oauth-flow.d.ts +2 -0
  35. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
  36. package/dist/types/modes/components/agent-hub.d.ts +10 -0
  37. package/dist/types/modes/components/dynamic-border.d.ts +5 -3
  38. package/dist/types/modes/components/login-dialog.d.ts +2 -0
  39. package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
  40. package/dist/types/modes/components/read-tool-group.d.ts +0 -2
  41. package/dist/types/modes/components/transcript-container.d.ts +3 -2
  42. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  43. package/dist/types/modes/interactive-mode.d.ts +1 -0
  44. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  45. package/dist/types/modes/orchestrate.d.ts +1 -1
  46. package/dist/types/modes/rpc/host-tools.d.ts +2 -0
  47. package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
  48. package/dist/types/modes/types.d.ts +1 -0
  49. package/dist/types/modes/ultrathink.d.ts +1 -1
  50. package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
  51. package/dist/types/modes/workflow.d.ts +1 -1
  52. package/dist/types/session/agent-session.d.ts +6 -0
  53. package/dist/types/session/exit-diagnostics.d.ts +11 -0
  54. package/dist/types/session/messages.d.ts +15 -0
  55. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
  56. package/dist/types/subprocess/worker-client.d.ts +6 -0
  57. package/dist/types/tools/bash-skill-urls.d.ts +1 -0
  58. package/dist/types/tools/grep.d.ts +0 -2
  59. package/dist/types/tools/read.d.ts +0 -4
  60. package/dist/types/web/search/provider.d.ts +10 -3
  61. package/dist/types/web/search/providers/codex.d.ts +5 -4
  62. package/package.json +12 -12
  63. package/src/advisor/__tests__/advisor.test.ts +917 -42
  64. package/src/advisor/advise-tool.ts +17 -1
  65. package/src/advisor/runtime.ts +270 -67
  66. package/src/autolearn/controller.ts +15 -3
  67. package/src/autoresearch/dashboard.ts +2 -2
  68. package/src/cli/args.ts +12 -0
  69. package/src/cli/auth-broker-cli.ts +30 -11
  70. package/src/cli/auth-gateway-cli.ts +5 -1
  71. package/src/cli/config-cli.ts +15 -3
  72. package/src/cli/dry-balance-cli.ts +14 -4
  73. package/src/cli/flag-tables.ts +21 -7
  74. package/src/cli/update-cli.ts +62 -11
  75. package/src/cli/usage-cli.ts +58 -5
  76. package/src/cli/usage-error.ts +7 -0
  77. package/src/cli.ts +23 -1
  78. package/src/commands/acp.ts +11 -2
  79. package/src/commands/launch.ts +12 -3
  80. package/src/commands/token.ts +3 -1
  81. package/src/config/api-key-resolver.ts +12 -3
  82. package/src/config/config-file.ts +30 -12
  83. package/src/config/model-registry.ts +7 -7
  84. package/src/config/model-resolver.ts +21 -7
  85. package/src/config/models-config.ts +1 -1
  86. package/src/config/settings-schema.ts +10 -0
  87. package/src/cursor.ts +2 -0
  88. package/src/discovery/claude-plugins.ts +9 -3
  89. package/src/discovery/omp-plugins.ts +6 -2
  90. package/src/discovery/substitute-plugin-root.ts +32 -0
  91. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  92. package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
  93. package/src/eval/__tests__/js-context-manager.test.ts +158 -1
  94. package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
  95. package/src/eval/__tests__/prelude-agent.test.ts +20 -0
  96. package/src/eval/__tests__/process-entry-import.test.ts +27 -0
  97. package/src/eval/agent-bridge.ts +121 -116
  98. package/src/eval/backend.ts +3 -3
  99. package/src/eval/bridge-timeout.ts +20 -2
  100. package/src/eval/executor-base.ts +85 -7
  101. package/src/eval/jl/kernel.ts +2 -1
  102. package/src/eval/js/context-manager.ts +109 -32
  103. package/src/eval/js/process-entry.ts +27 -0
  104. package/src/eval/js/shared/runtime.ts +1 -1
  105. package/src/eval/js/worker-core.ts +70 -9
  106. package/src/eval/js/worker-entry.ts +1 -1
  107. package/src/eval/py/__tests__/prelude.test.ts +72 -0
  108. package/src/eval/py/kernel.ts +2 -1
  109. package/src/eval/py/prelude.py +28 -1
  110. package/src/eval/py/spawn-options.ts +13 -0
  111. package/src/eval/py/tool-bridge.ts +13 -14
  112. package/src/eval/rb/kernel.ts +2 -1
  113. package/src/exec/bash-executor.ts +30 -43
  114. package/src/extensibility/custom-tools/types.ts +3 -0
  115. package/src/extensibility/extensions/runner.ts +3 -0
  116. package/src/extensibility/extensions/types.ts +3 -0
  117. package/src/extensibility/extensions/wrapper.ts +18 -18
  118. package/src/extensibility/plugins/bun-git-cache.ts +91 -0
  119. package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
  120. package/src/extensibility/plugins/manager.ts +28 -7
  121. package/src/goals/guided-setup.ts +29 -1
  122. package/src/internal-urls/history-protocol.ts +95 -15
  123. package/src/internal-urls/memory-protocol.ts +13 -9
  124. package/src/internal-urls/registry-helpers.ts +50 -1
  125. package/src/launch/broker.ts +38 -25
  126. package/src/lsp/client.ts +7 -1
  127. package/src/main.ts +29 -0
  128. package/src/mcp/oauth-discovery.ts +20 -1
  129. package/src/mcp/oauth-flow.ts +3 -1
  130. package/src/mcp/tool-bridge.ts +57 -6
  131. package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
  132. package/src/modes/components/agent-dashboard.ts +2 -2
  133. package/src/modes/components/agent-hub.ts +15 -2
  134. package/src/modes/components/agent-transcript-viewer.ts +2 -2
  135. package/src/modes/components/chat-transcript-builder.ts +26 -4
  136. package/src/modes/components/dynamic-border.ts +9 -6
  137. package/src/modes/components/extensions/extension-list.ts +2 -2
  138. package/src/modes/components/hook-selector.ts +10 -4
  139. package/src/modes/components/login-dialog.ts +5 -0
  140. package/src/modes/components/mcp-add-wizard.ts +5 -0
  141. package/src/modes/components/plan-review-overlay.ts +11 -11
  142. package/src/modes/components/read-tool-group.ts +1 -8
  143. package/src/modes/components/status-line/component.ts +10 -1
  144. package/src/modes/components/transcript-container.ts +110 -7
  145. package/src/modes/controllers/command-controller.ts +12 -4
  146. package/src/modes/controllers/event-controller.ts +80 -15
  147. package/src/modes/controllers/input-controller.ts +4 -2
  148. package/src/modes/controllers/mcp-command-controller.ts +6 -7
  149. package/src/modes/controllers/selector-controller.ts +24 -5
  150. package/src/modes/controllers/todo-command-controller.ts +18 -14
  151. package/src/modes/interactive-mode.ts +7 -3
  152. package/src/modes/magic-keyword-boundary.ts +23 -0
  153. package/src/modes/orchestrate.ts +6 -5
  154. package/src/modes/print-mode.ts +9 -0
  155. package/src/modes/prompt-action-autocomplete.ts +6 -1
  156. package/src/modes/rpc/host-tools.ts +15 -0
  157. package/src/modes/rpc/rpc-mode.ts +123 -48
  158. package/src/modes/types.ts +1 -1
  159. package/src/modes/ultrathink.ts +6 -5
  160. package/src/modes/utils/transcript-render-helpers.ts +54 -0
  161. package/src/modes/utils/ui-helpers.ts +27 -1
  162. package/src/modes/workflow.ts +6 -5
  163. package/src/prompts/advisor/system.md +1 -0
  164. package/src/prompts/system/system-prompt.md +1 -0
  165. package/src/prompts/tools/eval.md +2 -2
  166. package/src/prompts/tools/grep.md +1 -2
  167. package/src/prompts/tools/read.md +2 -4
  168. package/src/sdk.ts +61 -34
  169. package/src/session/agent-session.ts +283 -39
  170. package/src/session/exit-diagnostics.ts +108 -0
  171. package/src/session/messages.test.ts +66 -0
  172. package/src/session/messages.ts +37 -0
  173. package/src/session/streaming-output.ts +40 -12
  174. package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
  175. package/src/slash-commands/helpers/logout.ts +23 -3
  176. package/src/slash-commands/helpers/usage-report.ts +14 -2
  177. package/src/subprocess/worker-client.ts +9 -2
  178. package/src/system-prompt.test.ts +36 -0
  179. package/src/system-prompt.ts +1 -1
  180. package/src/task/executor.ts +8 -0
  181. package/src/task/render.test.ts +36 -0
  182. package/src/task/render.ts +55 -43
  183. package/src/tools/bash-skill-urls.ts +4 -1
  184. package/src/tools/bash.ts +1 -0
  185. package/src/tools/browser/registry.ts +17 -3
  186. package/src/tools/eval.ts +14 -9
  187. package/src/tools/gh.ts +3 -1
  188. package/src/tools/grep.ts +5 -45
  189. package/src/tools/path-utils.ts +7 -1
  190. package/src/tools/read.ts +23 -74
  191. package/src/tools/write.ts +82 -9
  192. package/src/tools/yield.ts +29 -1
  193. package/src/utils/title-generator.ts +10 -6
  194. package/src/web/search/index.ts +39 -22
  195. package/src/web/search/provider.ts +33 -16
  196. package/src/web/search/providers/codex.ts +68 -21
  197. package/src/web/search/providers/perplexity-auth.ts +20 -11
  198. package/src/web/search/providers/perplexity.ts +14 -2
@@ -241,7 +241,7 @@ export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
241
241
 
242
242
  switch (cmd.action) {
243
243
  case "list":
244
- handleList(cmd.flags);
244
+ await handleList(cmd.flags);
245
245
  break;
246
246
  case "get":
247
247
  handleGet(cmd.key, cmd.flags);
@@ -261,7 +261,19 @@ export async function runConfigCommand(cmd: ConfigCommandArgs): Promise<void> {
261
261
  }
262
262
  }
263
263
 
264
- function handleList(flags: { json?: boolean }): void {
264
+ async function writeStdout(text: string): Promise<void> {
265
+ const pending = Promise.withResolvers<void>();
266
+ process.stdout.write(text, error => {
267
+ if (error) {
268
+ pending.reject(error);
269
+ return;
270
+ }
271
+ pending.resolve();
272
+ });
273
+ await pending.promise;
274
+ }
275
+
276
+ async function handleList(flags: { json?: boolean }): Promise<void> {
265
277
  const defs = ALL_SETTING_PATHS.map(path => findSettingDef(path)).filter((def): def is CliSettingDef => !!def);
266
278
 
267
279
  if (flags.json) {
@@ -273,7 +285,7 @@ function handleList(flags: { json?: boolean }): void {
273
285
  description: def.description,
274
286
  };
275
287
  }
276
- console.log(JSON.stringify(result, null, 2));
288
+ await writeStdout(`${JSON.stringify(result, null, 2)}\n`);
277
289
  return;
278
290
  }
279
291
 
@@ -214,8 +214,15 @@ function extractAccount(access: {
214
214
  accountId?: string;
215
215
  projectId?: string;
216
216
  enterpriseUrl?: string;
217
+ orgId?: string;
218
+ orgName?: string;
217
219
  }): string {
218
- return access.email ?? access.accountId ?? access.projectId ?? access.enterpriseUrl ?? "(unknown oauth account)";
220
+ const base =
221
+ access.email ?? access.accountId ?? access.projectId ?? access.enterpriseUrl ?? "(unknown oauth account)";
222
+ // Two subscriptions (orgs) can share one email — name the org so per-account
223
+ // bench rows stay tellable apart.
224
+ const org = access.orgName ?? access.orgId;
225
+ return org ? `${base} (${org})` : base;
219
226
  }
220
227
 
221
228
  function getBenchTargetKey(access: {
@@ -225,15 +232,18 @@ function getBenchTargetKey(access: {
225
232
  projectId?: string;
226
233
  enterpriseUrl?: string;
227
234
  accessToken?: string;
235
+ orgId?: string;
228
236
  }): string {
229
- return (
237
+ const base =
230
238
  access.email ??
231
239
  access.accountId ??
232
240
  access.projectId ??
233
241
  access.enterpriseUrl ??
234
242
  (access.credentialId === undefined ? access.accessToken : `credential:${access.credentialId}`) ??
235
- "(unknown oauth account)"
236
- );
243
+ "(unknown oauth account)";
244
+ // Org-qualify: two org-scoped credentials under one email are two distinct
245
+ // benchmark targets, not duplicates.
246
+ return access.orgId ? `${base}|org:${access.orgId}` : base;
237
247
  }
238
248
 
239
249
  function sanitizeBenchText(text: string, width: number): string {
@@ -32,6 +32,7 @@
32
32
 
33
33
  import type { ConfiguredThinkingLevel } from "../thinking";
34
34
  import type { Args } from "./args";
35
+ import { CliUsageError } from "./usage-error";
35
36
 
36
37
  /**
37
38
  * Runtime dependencies injected into setters that need to validate input or
@@ -85,6 +86,24 @@ const setResume: OptionalSetter = (result, value) => {
85
86
  result.resume = value !== undefined ? value : true;
86
87
  };
87
88
 
89
+ const MAX_TIME_DURATION_RE = /^(\d+(?:\.\d+)?)([smh])$/;
90
+
91
+ function maxTimeMultiplier(unit: string | undefined): number {
92
+ if (unit === "h") return 3600;
93
+ if (unit === "m") return 60;
94
+ return 1;
95
+ }
96
+
97
+ function parseMaxTimeSeconds(value: string): number {
98
+ const trimmed = value.trim();
99
+ const duration = MAX_TIME_DURATION_RE.exec(trimmed);
100
+ const seconds = duration ? Number(duration[1]) * maxTimeMultiplier(duration[2]) : Number(trimmed);
101
+ if (Number.isFinite(seconds) && seconds > 0) return seconds;
102
+ throw new CliUsageError(
103
+ `Invalid --max-time value: ${JSON.stringify(value)}. Expected a positive number of seconds or duration like "5s", "10m", "1h".`,
104
+ );
105
+ }
106
+
88
107
  /**
89
108
  * Setters for flags with string values. Most built-ins consume the next argv
90
109
  * token even when it starts with `-`; flags listed in
@@ -127,13 +146,8 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
127
146
  "--plan-yolo-into": (result, value) => {
128
147
  result.planYoloInto = value;
129
148
  },
130
- "--max-time": (result, value, deps) => {
131
- const seconds = Number(value);
132
- if (Number.isFinite(seconds) && seconds > 0) {
133
- result.maxTime = seconds;
134
- } else {
135
- deps.logger.warn("Invalid seconds passed to --max-time", { value });
136
- }
149
+ "--max-time": (result, value) => {
150
+ result.maxTime = parseMaxTimeSeconds(value);
137
151
  },
138
152
  "--api-key": (result, value) => {
139
153
  result.apiKey = value;
@@ -109,6 +109,19 @@ async function getBunGlobalBinDir(): Promise<string | undefined> {
109
109
  }
110
110
  }
111
111
 
112
+ async function getNpmGlobalBinDir(): Promise<string | undefined> {
113
+ if (!$which("npm")) return undefined;
114
+ try {
115
+ const result = await $`npm prefix -g`.quiet().nothrow();
116
+ if (result.exitCode !== 0) return undefined;
117
+ const prefix = result.text().trim();
118
+ if (prefix.length === 0) return undefined;
119
+ return process.platform === "win32" ? prefix : path.join(prefix, "bin");
120
+ } catch {
121
+ return undefined;
122
+ }
123
+ }
124
+
112
125
  async function getHomebrewFormulaPrefix(): Promise<string | undefined> {
113
126
  if (!$which("brew")) return undefined;
114
127
  for (const formula of [HOMEBREW_FORMULA, APP_NAME]) {
@@ -189,26 +202,36 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
189
202
  return isPathInDirectoryLexical(resolvedFile, dirReal);
190
203
  }
191
204
 
192
- type UpdateMethod = "brew" | "mise" | "bun" | "binary";
205
+ type UpdateMethod = "brew" | "mise" | "bun" | "npm" | "binary";
193
206
 
194
207
  interface UpdateMethodResolutionOptions {
195
208
  homebrewPrefix?: string;
196
209
  miseBinDirs?: readonly string[];
197
210
  miseDataDir?: string;
211
+ npmBinDir?: string;
198
212
  }
199
213
 
200
- type UpdateTarget = { method: "brew" } | { method: "mise" } | { method: "bun" } | { method: "binary"; path: string };
214
+ type UpdateTarget =
215
+ | { method: "brew" }
216
+ | { method: "mise" }
217
+ | { method: "bun" }
218
+ | { method: "npm" }
219
+ | { method: "binary"; path: string };
201
220
 
202
221
  function resolveUpdateMethod(
203
222
  ompPath: string,
204
223
  bunBinDir: string | undefined,
205
224
  options: UpdateMethodResolutionOptions = {},
206
225
  ): UpdateMethod {
207
- const { homebrewPrefix, miseBinDirs = [], miseDataDir } = options;
226
+ const { homebrewPrefix, miseBinDirs = [], miseDataDir, npmBinDir } = options;
227
+ const launcherExtension = path.extname(ompPath).toLowerCase();
228
+ const isWindowsScriptLauncher =
229
+ launcherExtension === ".cmd" || launcherExtension === ".ps1" || launcherExtension === ".bat";
208
230
  if (homebrewPrefix && isPathInDirectory(ompPath, path.join(homebrewPrefix, "bin"))) return "brew";
209
231
  if (miseBinDirs.some(dir => isPathInDirectory(ompPath, dir))) return "mise";
210
232
  if (miseDataDir && isPathInDirectory(ompPath, path.join(miseDataDir, "shims"))) return "mise";
211
233
  if (bunBinDir && isPathInDirectory(ompPath, bunBinDir)) return "bun";
234
+ if ((npmBinDir && isPathInDirectory(ompPath, npmBinDir)) || isWindowsScriptLauncher) return "npm";
212
235
  return "binary";
213
236
  }
214
237
 
@@ -221,6 +244,7 @@ export function resolveUpdateMethodForTest(
221
244
  }
222
245
  async function resolveUpdateTarget(): Promise<UpdateTarget> {
223
246
  const bunBinDir = await getBunGlobalBinDir();
247
+ const npmBinDir = await getNpmGlobalBinDir();
224
248
  const homebrewPrefix = await getHomebrewFormulaPrefix();
225
249
  const miseAvailable = $which("mise") !== undefined;
226
250
  const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
@@ -228,7 +252,7 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
228
252
  const ompPath = resolveOmpPath();
229
253
 
230
254
  if (ompPath) {
231
- const method = resolveUpdateMethod(ompPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir });
255
+ const method = resolveUpdateMethod(ompPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir, npmBinDir });
232
256
  if (method === "binary") return { method, path: ompPath };
233
257
  return { method };
234
258
  }
@@ -715,6 +739,14 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
715
739
  }
716
740
  }
717
741
 
742
+ function buildVersionedPackageInstallArgs(expectedVersion: string, nativeTag: string): string[] {
743
+ const args = [`${PACKAGE}@${expectedVersion}`, `${NATIVES_PACKAGE}@${expectedVersion}`];
744
+ if (SUPPORTED_NATIVE_TAGS.has(nativeTag)) {
745
+ args.push(`${NATIVES_PACKAGE}-${nativeTag}@${expectedVersion}`);
746
+ }
747
+ return args;
748
+ }
749
+
718
750
  /**
719
751
  * Build the bun argv used to globally install a specific omp version.
720
752
  *
@@ -746,17 +778,23 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
746
778
  * See #1824.
747
779
  */
748
780
  export function buildBunInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
749
- const args = [
781
+ return [
750
782
  "install",
751
783
  "-g",
752
784
  "--no-cache",
753
785
  `--registry=${NPM_REGISTRY}`,
754
- `${PACKAGE}@${expectedVersion}`,
755
- `${NATIVES_PACKAGE}@${expectedVersion}`,
786
+ ...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
787
+ ];
788
+ }
789
+
790
+ /** Build the npm argv used to update npm-managed global installs. */
791
+ export function buildNpmInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
792
+ const args = [
793
+ "install",
794
+ "-g",
795
+ `--registry=${NPM_REGISTRY}`,
796
+ ...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
756
797
  ];
757
- if (SUPPORTED_NATIVE_TAGS.has(nativeTag)) {
758
- args.push(`${NATIVES_PACKAGE}-${nativeTag}@${expectedVersion}`);
759
- }
760
798
  return args;
761
799
  }
762
800
 
@@ -773,7 +811,7 @@ export function buildMiseForceInstallArgs(expectedVersion: string): string[] {
773
811
  }
774
812
 
775
813
  /**
776
- * Update via bun package manager.
814
+ * Update via package manager.
777
815
  */
778
816
  async function updateViaBun(expectedVersion: string): Promise<void> {
779
817
  console.log(chalk.dim("Updating via bun..."));
@@ -794,6 +832,17 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
794
832
  }
795
833
  }
796
834
 
835
+ async function updateViaNpm(expectedVersion: string): Promise<void> {
836
+ console.log(chalk.dim("Updating via npm..."));
837
+ const args = buildNpmInstallArgs(expectedVersion);
838
+ const result = await $`npm ${args}`.nothrow();
839
+ if (result.exitCode !== 0) {
840
+ throw new Error(`npm install failed with exit code ${result.exitCode}`);
841
+ }
842
+
843
+ await printVerification(expectedVersion);
844
+ }
845
+
797
846
  async function updateViaHomebrew(expectedVersion: string, force: boolean): Promise<void> {
798
847
  console.log(chalk.dim("Updating Homebrew formulae..."));
799
848
  const update = await $`brew update`.nothrow();
@@ -920,6 +969,8 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
920
969
  await updateViaMise(release.version, opts.force);
921
970
  } else if (target.method === "bun") {
922
971
  await updateViaBun(release.version);
972
+ } else if (target.method === "npm") {
973
+ await updateViaNpm(release.version);
923
974
  } else {
924
975
  await updateViaBinaryAt(target.path, release.version);
925
976
  }
@@ -41,6 +41,9 @@ export interface UsageAccountIdentity {
41
41
  accountId?: string;
42
42
  projectId?: string;
43
43
  enterpriseUrl?: string;
44
+ /** Organization/workspace the credential is scoped to (Anthropic multi-subscription). */
45
+ orgId?: string;
46
+ orgName?: string;
44
47
  }
45
48
 
46
49
  /**
@@ -134,6 +137,7 @@ function collectIdentityStrings(reports: UsageReport[], accounts: UsageAccountId
134
137
  add(meta.accountId);
135
138
  add(meta.projectId);
136
139
  add(meta.orgId);
140
+ add(meta.orgName);
137
141
  for (const limit of report.limits) {
138
142
  add(limit.scope.accountId);
139
143
  add(limit.scope.projectId);
@@ -144,6 +148,8 @@ function collectIdentityStrings(reports: UsageReport[], accounts: UsageAccountId
144
148
  add(account.email);
145
149
  add(account.accountId);
146
150
  add(account.projectId);
151
+ add(account.orgId);
152
+ add(account.orgName);
147
153
  add(account.enterpriseUrl);
148
154
  }
149
155
  return values;
@@ -293,9 +299,38 @@ export function collectUnreportedAccounts(
293
299
  const providerReports = byProvider.get(account.provider) ?? [];
294
300
  if (providerReports.length === 0) return true;
295
301
  if (account.type === "api_key") return false;
302
+ // Org-decisive attribution when EITHER side carries an org (Anthropic
303
+ // multi-subscription): two orgs share every other identifier, so an
304
+ // org-scoped account is covered only by its own org's report, and an
305
+ // org-less legacy account is never covered by an org-attributed sibling
306
+ // report — its own fetch failing must surface as "no usage data". The
307
+ // shared org is a GATE, not a match: two Team members share the org id
308
+ // while drawing on per-user pools, so coverage also requires the
309
+ // account's own base identity inside the same-org subset (an org-only
310
+ // account, with no base identifiers, is covered by any same-org
311
+ // report). The email/account fallback below applies only when both
312
+ // sides are org-less.
313
+ const accountOrg = account.orgId?.toLowerCase();
296
314
  const ids = [account.email, account.accountId, account.projectId]
297
315
  .filter((value): value is string => typeof value === "string" && value.length > 0)
298
316
  .map(value => value.toLowerCase());
317
+ const sameOrgReports: UsageReport[] = [];
318
+ let sawReportOrg = false;
319
+ for (const report of providerReports) {
320
+ const metaOrg = report.metadata?.orgId;
321
+ if (typeof metaOrg === "string" && metaOrg) {
322
+ sawReportOrg = true;
323
+ if (accountOrg !== undefined && metaOrg.toLowerCase() === accountOrg) sameOrgReports.push(report);
324
+ }
325
+ }
326
+ if (accountOrg || sawReportOrg) {
327
+ if (!accountOrg || sameOrgReports.length === 0) return true;
328
+ if (ids.length === 0) return false;
329
+ return !sameOrgReports.some(report => {
330
+ const identifiers = reportIdentifiers(report);
331
+ return ids.some(id => identifiers.has(id));
332
+ });
333
+ }
299
334
  if (ids.length === 0) return false;
300
335
  const reported = new Set<string>();
301
336
  let anyIdentified = false;
@@ -309,9 +344,17 @@ export function collectUnreportedAccounts(
309
344
  });
310
345
  }
311
346
 
312
- function accountIdentityLabel(account: UsageAccountIdentity): string {
347
+ /** Compose the account label from parts, masking each part individually so `--redact` cannot be bypassed by the composite string. */
348
+ function accountIdentityLabel(account: UsageAccountIdentity, redaction?: Map<string, string>): string {
313
349
  if (account.type === "api_key") return "API key";
314
- return account.email ?? account.accountId ?? account.projectId ?? account.enterpriseUrl ?? "OAuth account";
350
+ const base = account.email ?? account.accountId ?? account.projectId ?? account.enterpriseUrl ?? "OAuth account";
351
+ const masked = redaction?.get(base) ?? base;
352
+ // orgId fallback: the uuid is the actual scoped identity; a token response
353
+ // can carry it without a display name, and two same-email rows must still
354
+ // be tellable apart.
355
+ const org = account.orgName ?? account.orgId;
356
+ if (!org || org === base) return masked;
357
+ return `${masked} · ${redaction?.get(org) ?? org}`;
315
358
  }
316
359
 
317
360
  function formatAccountHeader(
@@ -324,6 +367,12 @@ function formatAccountHeader(
324
367
  const icon = STATUS_COLOR[status]("●");
325
368
  const label = reportAccountLabel(report, index);
326
369
  let header = `${icon} ${chalk.bold(redaction?.get(label) ?? label)}`;
370
+ const metaOrgName = report.metadata?.orgName;
371
+ const metaOrgId = report.metadata?.orgId;
372
+ const org = typeof metaOrgName === "string" && metaOrgName ? metaOrgName : metaOrgId;
373
+ if (typeof org === "string" && org && org !== label) {
374
+ header += chalk.dim(` · ${redaction?.get(org) ?? org}`);
375
+ }
327
376
  const planType = report.metadata?.planType;
328
377
  if (typeof planType === "string" && planType) header += chalk.dim(` · plan: ${planType}`);
329
378
  const savedResets = report.resetCredits?.availableCount ?? 0;
@@ -520,8 +569,8 @@ export function formatUsageBreakdown(
520
569
  });
521
570
 
522
571
  for (const account of providerUnreported) {
523
- const label = accountIdentityLabel(account);
524
- lines.push(` ${chalk.dim("○")} ${chalk.dim(`${redaction?.get(label) ?? label} — no usage data`)}`);
572
+ const label = accountIdentityLabel(account, redaction);
573
+ lines.push(` ${chalk.dim("○")} ${chalk.dim(`${label} — no usage data`)}`);
525
574
  }
526
575
 
527
576
  const stats = computeProviderWindowStats(providerReports);
@@ -690,6 +739,8 @@ function collectStoredAccounts(authStorage: AuthStorage): UsageAccountIdentity[]
690
739
  accountId: credential.accountId,
691
740
  projectId: credential.projectId,
692
741
  enterpriseUrl: credential.enterpriseUrl,
742
+ orgId: credential.orgId,
743
+ orgName: credential.orgName,
693
744
  });
694
745
  } else {
695
746
  accounts.push({ provider, type: "api_key" });
@@ -726,7 +777,7 @@ function maskIdentity(redaction: Map<string, string>, value: string | undefined)
726
777
  return value === undefined ? undefined : (redaction.get(value) ?? value);
727
778
  }
728
779
 
729
- const IDENTITY_METADATA_KEYS = ["email", "accountId", "projectId", "orgId"] as const;
780
+ const IDENTITY_METADATA_KEYS = ["email", "accountId", "projectId", "orgId", "orgName"] as const;
730
781
 
731
782
  /** Mask identity fields in a raw-stripped report for `--redact --json`. */
732
783
  function redactReportForJson(
@@ -830,6 +881,8 @@ export async function runUsageCommand(cmd: UsageCommandArgs): Promise<void> {
830
881
  accountId: maskIdentity(redaction, account.accountId),
831
882
  projectId: maskIdentity(redaction, account.projectId),
832
883
  enterpriseUrl: maskIdentity(redaction, account.enterpriseUrl),
884
+ orgId: maskIdentity(redaction, account.orgId),
885
+ orgName: maskIdentity(redaction, account.orgName),
833
886
  }));
834
887
  }
835
888
  const capacity: Record<string, ProviderWindowStat[]> = {};
@@ -0,0 +1,7 @@
1
+ /** Command-line input error that should be rendered without a stack trace. */
2
+ export class CliUsageError extends Error {
3
+ constructor(message: string) {
4
+ super(message);
5
+ this.name = "CliUsageError";
6
+ }
7
+ }
package/src/cli.ts CHANGED
@@ -108,6 +108,7 @@ const TINY_WORKER_ARG = "__omp_worker_tiny_inference";
108
108
  const STATS_SYNC_WORKER_ARG = "__omp_worker_stats_sync";
109
109
  const TAB_WORKER_ARG = "__omp_worker_tab";
110
110
  const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
111
+ const JS_EVAL_PROCESS_ARG = "__omp_worker_js_eval_process";
111
112
  const STT_WORKER_ARG = "__omp_worker_stt";
112
113
  const TTS_WORKER_ARG = "__omp_worker_tts";
113
114
  const MNEMOPI_EMBED_WORKER_ARG = "__omp_worker_mnemopi_embed";
@@ -156,6 +157,14 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
156
157
  await import("./eval/js/worker-entry");
157
158
  return true;
158
159
  }
160
+ if (arg === JS_EVAL_PROCESS_ARG) {
161
+ const { startJsEvalProcess } = await import("./eval/js/process-entry");
162
+ // The JS evaluator forwards user-controlled payloads (tool-call args,
163
+ // display outputs); a non-serializable one must fail that cell, not
164
+ // SIGKILL the kernel and erase the eval session's state.
165
+ await runIpcSubprocessWorker(startJsEvalProcess, { rethrowConnectedSendErrors: true });
166
+ return true;
167
+ }
159
168
  if (arg === STT_WORKER_ARG) {
160
169
  const { startSttWorker } = await import("./stt/asr-worker");
161
170
  await runIpcSubprocessWorker(startSttWorker);
@@ -196,6 +205,18 @@ async function runIpcSubprocessWorker<In, Out>(
196
205
  sendAndFlush(message: Out): Promise<void>;
197
206
  onMessage(handler: (message: In) => void): () => void;
198
207
  }) => void,
208
+ options?: {
209
+ /**
210
+ * Rethrow send failures while the IPC channel is still connected instead
211
+ * of shutting down. A connected-channel failure means this particular
212
+ * message could not be serialized (e.g. a JS eval cell passed a function
213
+ * into tool args, a DataCloneError under advanced serialization) — the
214
+ * caller must see that error, exactly as Worker `postMessage` would
215
+ * deliver it, rather than losing the whole worker and its state.
216
+ * Channel-gone failures still shut down.
217
+ */
218
+ rethrowConnectedSendErrors?: boolean;
219
+ },
199
220
  ): Promise<void> {
200
221
  const { promise: shuttingDown, resolve: shutdown } = Promise.withResolvers<void>();
201
222
  type IpcSend = (this: NodeJS.Process, message: unknown, callback?: (error: Error | null) => void) => boolean;
@@ -211,7 +232,8 @@ async function runIpcSubprocessWorker<In, Out>(
211
232
  }
212
233
  try {
213
234
  sender.call(process, message);
214
- } catch {
235
+ } catch (error) {
236
+ if (options?.rethrowConnectedSendErrors && process.connected) throw error;
215
237
  shutdown();
216
238
  }
217
239
  };
@@ -5,7 +5,7 @@
5
5
  * ACP terminal-auth flag asks the same command to open the interactive TUI.
6
6
  */
7
7
  import { Command } from "@oh-my-pi/pi-utils/cli";
8
- import { parseArgs } from "../cli/args";
8
+ import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
9
9
  import { runRootCommand } from "../main";
10
10
  import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
11
11
 
@@ -15,7 +15,16 @@ export default class Acp extends Command {
15
15
 
16
16
  async run(): Promise<void> {
17
17
  const { args, terminalAuth } = prepareAcpTerminalAuthArgs(this.argv);
18
- const parsed = parseArgs(args);
18
+ let parsed: ParsedArgs;
19
+ try {
20
+ parsed = parseArgs(args);
21
+ } catch (error) {
22
+ if (reportCliUsageError(error)) {
23
+ process.exitCode = 2;
24
+ return;
25
+ }
26
+ throw error;
27
+ }
19
28
  if (!terminalAuth) {
20
29
  parsed.mode = "acp";
21
30
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { APP_NAME } from "@oh-my-pi/pi-utils";
6
6
  import { Args, Command, Flags } from "@oh-my-pi/pi-utils/cli";
7
- import { parseArgs } from "../cli/args";
7
+ import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
8
8
  import { runRootCommand } from "../main";
9
9
  import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
10
10
  import { CLI_THINKING_LEVELS } from "../thinking";
@@ -157,7 +157,7 @@ export default class Index extends Command {
157
157
  description: "Include thinking blocks in print mode text output",
158
158
  }),
159
159
  "max-time": Flags.string({
160
- description: "Stop the session after this many seconds",
160
+ description: "Stop the session after this duration (e.g., 600, 10m, 1h)",
161
161
  }),
162
162
  // `--auto-approve` / `--yolo`: declared here so oclif's auto-generated `--help` lists it.
163
163
  // Runtime parsing happens in `cli/args.ts parseArgs` (line 176 in that file) — `runRootCommand`
@@ -193,7 +193,16 @@ export default class Index extends Command {
193
193
 
194
194
  async run(): Promise<void> {
195
195
  const { args } = prepareAcpTerminalAuthArgs(this.argv);
196
- const parsed = parseArgs(args);
196
+ let parsed: ParsedArgs;
197
+ try {
198
+ parsed = parseArgs(args);
199
+ } catch (error) {
200
+ if (reportCliUsageError(error)) {
201
+ process.exitCode = 2;
202
+ return;
203
+ }
204
+ throw error;
205
+ }
197
206
  await runRootCommand(parsed, args);
198
207
  }
199
208
  }
@@ -64,12 +64,14 @@ export default class Token extends Command {
64
64
  }
65
65
  if (flags.list) {
66
66
  for (const acct of accounts) {
67
- const label =
67
+ const base =
68
68
  acct.email ??
69
69
  acct.accountId ??
70
70
  acct.projectId ??
71
71
  acct.enterpriseUrl ??
72
72
  `credential #${acct.credentialId}`;
73
+ const org = acct.orgName ?? acct.orgId;
74
+ const label = org && org !== base ? `${base} (${org})` : base;
73
75
  process.stdout.write(`${acct.position + 1}. ${label}\n`);
74
76
  }
75
77
  return;
@@ -1,4 +1,5 @@
1
- import type { Api, ApiKeyResolver, AuthStorage, Model } from "@oh-my-pi/pi-ai";
1
+ import { type Api, type ApiKeyResolver, type AuthStorage, isUsageLimitOutcome, type Model } from "@oh-my-pi/pi-ai";
2
+ import * as AIError from "@oh-my-pi/pi-ai/error";
2
3
 
3
4
  /** Model slice accepted by the model-form `resolver(model, sessionId)` overload. */
4
5
  export type ApiKeyResolverModel = Pick<Model<Api>, "provider" | "baseUrl" | "id">;
@@ -27,7 +28,7 @@ export interface ApiKeyResolverRegistry {
27
28
  /**
28
29
  * Build an {@link ApiKeyResolver} implementing the central a/b/c auth-retry
29
30
  * policy: initial → resolve; step (b) → force-refresh same account; step (c)
30
- * → rotate to a sibling credential, then re-resolve.
31
+ * → rotate to a sibling and re-resolve, unless quota exhaustion has no sibling.
31
32
  *
32
33
  * Two call forms: `resolver(provider, options?)` for provider-scoped keys,
33
34
  * and `resolver(model, sessionId?)` which derives `baseUrl`/`modelId` from
@@ -59,12 +60,20 @@ export function createApiKeyResolver(
59
60
  // sibling exists we switch immediately; the precise no-sibling backoff
60
61
  // is owned by `markUsageLimitReached` (default + server usage-report
61
62
  // reset) and the outer whole-turn retry layer.
62
- await registry.authStorage.rotateSessionCredential(provider, sessionId, {
63
+ const switched = await registry.authStorage.rotateSessionCredential(provider, sessionId, {
63
64
  error,
64
65
  modelId,
65
66
  signal,
66
67
  apiKey: previousKey,
67
68
  });
69
+ if (!switched) {
70
+ const status = AIError.status(error);
71
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : undefined;
72
+ // No sibling for an account-quota failure: stop so the outer
73
+ // whole-turn retry layer can honor the recorded backoff. A hard
74
+ // auth decline can instead mean a peer refreshed the bearer.
75
+ if (AIError.isUsageLimit(error) || isUsageLimitOutcome(status, message)) return undefined;
76
+ }
68
77
  return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId });
69
78
  }
70
79
  return registry.getApiKeyForProvider(provider, sessionId, { baseUrl, modelId, forceRefresh: true, signal });