@oh-my-pi/pi-coding-agent 17.3.7 → 17.3.8

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 (109) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/dist/{CHANGELOG-1hmwwt45.md → CHANGELOG-vr9cckb4.md} +60 -0
  3. package/dist/cli.js +2993 -3001
  4. package/dist/docs-index.generated.txt +1 -1
  5. package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
  6. package/dist/types/advisor/advise-tool.d.ts +4 -2
  7. package/dist/types/cli/auth-broker-cli.d.ts +15 -0
  8. package/dist/types/cli/update-cli.d.ts +8 -0
  9. package/dist/types/cli-commands.d.ts +10 -2
  10. package/dist/types/config/settings-schema.d.ts +28 -0
  11. package/dist/types/config/settings.d.ts +9 -0
  12. package/dist/types/extensibility/extensions/runner.d.ts +2 -1
  13. package/dist/types/launch/presence.d.ts +4 -1
  14. package/dist/types/mcp/oauth-credentials.d.ts +23 -0
  15. package/dist/types/mcp/oauth-flow.d.ts +11 -0
  16. package/dist/types/mnemopi/backend.d.ts +12 -0
  17. package/dist/types/modes/components/tool-execution.d.ts +12 -0
  18. package/dist/types/modes/controllers/input-controller.d.ts +2 -0
  19. package/dist/types/modes/interactive-mode.d.ts +25 -3
  20. package/dist/types/modes/types.d.ts +10 -0
  21. package/dist/types/session/agent-session.d.ts +3 -0
  22. package/dist/types/session/prewalk.d.ts +4 -0
  23. package/dist/types/session/session-entries.d.ts +0 -1
  24. package/dist/types/session/session-manager.d.ts +12 -0
  25. package/dist/types/session/session-stats.d.ts +13 -1
  26. package/dist/types/session/skill-title-input.d.ts +13 -0
  27. package/dist/types/subprocess/worker-client.d.ts +7 -4
  28. package/dist/types/task/label.d.ts +2 -0
  29. package/dist/types/task/render.d.ts +2 -0
  30. package/dist/types/tiny/completion-prompt.d.ts +2 -0
  31. package/dist/types/tiny/title-client.d.ts +6 -4
  32. package/dist/types/tiny/title-protocol.d.ts +1 -0
  33. package/dist/types/tiny/worker.d.ts +27 -0
  34. package/dist/types/tools/bash.d.ts +1 -1
  35. package/dist/types/tools/read-format.d.ts +6 -0
  36. package/dist/types/tools/read-summary.d.ts +7 -1
  37. package/dist/types/utils/block-context.d.ts +14 -0
  38. package/dist/types/utils/fetch-timeout.d.ts +15 -0
  39. package/dist/types/utils/git.d.ts +25 -1
  40. package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
  41. package/package.json +13 -13
  42. package/src/advisor/advise-tool.ts +5 -3
  43. package/src/cli/auth-broker-cli.ts +36 -1
  44. package/src/cli/profile-bootstrap.ts +2 -6
  45. package/src/cli/update-cli.ts +63 -11
  46. package/src/cli-commands.ts +61 -7
  47. package/src/commands/completions.ts +2 -1
  48. package/src/commit/agentic/index.ts +15 -2
  49. package/src/commit/git/diff.ts +6 -2
  50. package/src/config/model-resolver.ts +52 -6
  51. package/src/config/models-config.ts +2 -2
  52. package/src/config/settings-schema.ts +33 -0
  53. package/src/config/settings.ts +159 -30
  54. package/src/discovery/helpers.ts +45 -2
  55. package/src/discovery/omp-plugins.ts +2 -1
  56. package/src/discovery/opencode.ts +56 -3
  57. package/src/eval/js/process-entry.ts +4 -4
  58. package/src/export/html/tool-views.generated.js +19 -19
  59. package/src/extensibility/extensions/runner.ts +3 -2
  60. package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
  61. package/src/launch/client.ts +9 -4
  62. package/src/launch/presence.ts +19 -4
  63. package/src/lsp/defaults.json +1 -1
  64. package/src/mcp/manager.ts +41 -20
  65. package/src/mcp/oauth-credentials.ts +38 -0
  66. package/src/mcp/oauth-flow.ts +21 -0
  67. package/src/mcp/tool-bridge.ts +32 -16
  68. package/src/mnemopi/backend.ts +35 -3
  69. package/src/modes/components/model-hub.ts +37 -4
  70. package/src/modes/components/settings-selector.ts +17 -11
  71. package/src/modes/components/tool-execution.ts +97 -29
  72. package/src/modes/components/tree-selector.ts +7 -2
  73. package/src/modes/controllers/event-controller.ts +12 -2
  74. package/src/modes/controllers/input-controller.ts +64 -27
  75. package/src/modes/controllers/mcp-command-controller.ts +13 -4
  76. package/src/modes/interactive-mode.ts +79 -11
  77. package/src/modes/types.ts +11 -0
  78. package/src/prompts/system/memory-extraction-system.md +5 -22
  79. package/src/prompts/system/system-prompt.md +1 -1
  80. package/src/session/agent-session.ts +52 -6
  81. package/src/session/messages.ts +6 -0
  82. package/src/session/prewalk.ts +25 -7
  83. package/src/session/session-entries.ts +0 -1
  84. package/src/session/session-maintenance.ts +10 -1
  85. package/src/session/session-manager.ts +15 -0
  86. package/src/session/session-stats.ts +24 -3
  87. package/src/session/settings-stream-fn.ts +7 -0
  88. package/src/session/skill-title-input.ts +32 -0
  89. package/src/session/turn-recovery.ts +23 -18
  90. package/src/subprocess/worker-client.ts +8 -5
  91. package/src/task/executor.ts +11 -0
  92. package/src/task/index.ts +2 -0
  93. package/src/task/label.ts +14 -1
  94. package/src/task/persisted-revive.ts +13 -0
  95. package/src/task/render.ts +1 -1
  96. package/src/task/structured-subagent.ts +5 -2
  97. package/src/tiny/completion-prompt.ts +16 -0
  98. package/src/tiny/title-client.ts +15 -6
  99. package/src/tiny/title-protocol.ts +8 -1
  100. package/src/tiny/worker.ts +21 -19
  101. package/src/tools/bash.ts +7 -1
  102. package/src/tools/read-format.ts +16 -2
  103. package/src/tools/read-summary.ts +9 -4
  104. package/src/tools/read.ts +306 -72
  105. package/src/utils/block-context.ts +15 -1
  106. package/src/utils/fetch-timeout.ts +33 -0
  107. package/src/utils/git.ts +54 -11
  108. package/src/web/search/providers/browser-page.ts +21 -3
  109. package/src/web/search/providers/tinyfish.ts +26 -0
package/src/utils/git.ts CHANGED
@@ -21,6 +21,8 @@ export interface GitCommandResult {
21
21
  exitCode: number;
22
22
  stdout: string;
23
23
  stderr: string;
24
+ /** True when stdout or stderr hit {@link GIT_COMMAND_OUTPUT_LIMIT_BYTES} and the captured text is incomplete. */
25
+ truncated: boolean;
24
26
  }
25
27
 
26
28
  export interface GitRepository {
@@ -66,6 +68,7 @@ export interface DiffOptions {
66
68
  readonly numstat?: boolean;
67
69
  readonly signal?: AbortSignal;
68
70
  readonly stat?: boolean;
71
+ readonly requireComplete?: boolean;
69
72
  }
70
73
 
71
74
  export interface StatusOptions {
@@ -173,6 +176,29 @@ export class GitCommandError extends Error {
173
176
  }
174
177
  }
175
178
 
179
+ /**
180
+ * A git subprocess produced more output than {@link GIT_COMMAND_OUTPUT_LIMIT_BYTES}
181
+ * and its captured stdout was truncated. Thrown only for callers that opt into
182
+ * completeness via `diff({ requireComplete: true })`, where operating on a partial
183
+ * diff would silently corrupt downstream parsing — e.g. the split-commit builder,
184
+ * which would otherwise throw a misleading "No diff found" for files sorting after
185
+ * a large binary blob whose base85 payload pushed the diff past the cap.
186
+ */
187
+ export class GitOutputTruncatedError extends Error {
188
+ readonly args: readonly string[];
189
+ readonly result: GitCommandResult;
190
+
191
+ constructor(args: readonly string[], result: GitCommandResult) {
192
+ const limitMiB = Math.round(GIT_COMMAND_OUTPUT_LIMIT_BYTES / (1024 * 1024));
193
+ super(
194
+ `git ${args.join(" ")} produced more than ${limitMiB} MiB of output; the captured result is truncated and incomplete.`,
195
+ );
196
+ this.name = "GitOutputTruncatedError";
197
+ this.args = [...args];
198
+ this.result = result;
199
+ }
200
+ }
201
+
176
202
  // ════════════════════════════════════════════════════════════════════════════
177
203
  // Internal: Core execution
178
204
  // ════════════════════════════════════════════════════════════════════════════
@@ -312,7 +338,10 @@ async function waitForExitWithTimeout(
312
338
  }
313
339
  }
314
340
 
315
- async function readCappedText(stream: ReadableStream<Uint8Array>, maxBytes: number): Promise<string> {
341
+ async function readCappedText(
342
+ stream: ReadableStream<Uint8Array>,
343
+ maxBytes: number,
344
+ ): Promise<{ text: string; truncated: boolean }> {
316
345
  const reader = stream.getReader();
317
346
  const decoder = new TextDecoder();
318
347
  const chunks: string[] = [];
@@ -335,7 +364,7 @@ async function readCappedText(stream: ReadableStream<Uint8Array>, maxBytes: numb
335
364
  }
336
365
  chunks.push(decoder.decode());
337
366
  if (truncated) chunks.push(GIT_OUTPUT_TRUNCATED_MARKER);
338
- return chunks.join("");
367
+ return { text: chunks.join(""), truncated };
339
368
  } finally {
340
369
  reader.releaseLock();
341
370
  }
@@ -372,10 +401,15 @@ async function collectSubprocessResult(
372
401
  void stdoutPromise.catch(() => undefined);
373
402
  void stderrPromise.catch(() => undefined);
374
403
  await Promise.all([cancelOutput(stdoutStream), cancelOutput(stderrStream)]);
375
- return { exitCode: GIT_COMMAND_TIMEOUT_EXIT_CODE, stdout: "", stderr: exit.stderr };
404
+ return { exitCode: GIT_COMMAND_TIMEOUT_EXIT_CODE, stdout: "", stderr: exit.stderr, truncated: false };
376
405
  }
377
406
  const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
378
- return { exitCode: exit.exitCode ?? 0, stdout, stderr };
407
+ return {
408
+ exitCode: exit.exitCode ?? 0,
409
+ stdout: stdout.text,
410
+ stderr: stderr.text,
411
+ truncated: stdout.truncated || stderr.truncated,
412
+ };
379
413
  }
380
414
 
381
415
  interface CommandOptions {
@@ -496,7 +530,7 @@ async function git(cwd: string, args: readonly string[], options: CommandOptions
496
530
  // A deleted/nonexistent cwd also surfaces as a spawn ENOENT; only blame
497
531
  // the binary when the working directory actually exists.
498
532
  const stderr = fs.existsSync(cwd) ? "git is not installed." : `working directory does not exist: ${cwd}`;
499
- return { exitCode: GIT_SPAWN_ENOENT_EXIT_CODE, stdout: "", stderr };
533
+ return { exitCode: GIT_SPAWN_ENOENT_EXIT_CODE, stdout: "", stderr, truncated: false };
500
534
  }
501
535
  throw err;
502
536
  }
@@ -1240,7 +1274,11 @@ export const diff = Object.assign(
1240
1274
  if (options.allowFailure) {
1241
1275
  return (await git(cwd, args, { env: options.env, readOnly: true, signal: options.signal })).stdout;
1242
1276
  }
1243
- return runText(cwd, args, { env: options.env, readOnly: true, signal: options.signal });
1277
+ const result = await runChecked(cwd, args, { env: options.env, readOnly: true, signal: options.signal });
1278
+ if (options.requireComplete && result.truncated) {
1279
+ throw new GitOutputTruncatedError(args, result);
1280
+ }
1281
+ return result.stdout;
1244
1282
  },
1245
1283
  {
1246
1284
  /** List changed file paths. */
@@ -2005,12 +2043,17 @@ export const patch = {
2005
2043
  }
2006
2044
  },
2007
2045
 
2008
- /** Join patch parts into a single patch string. */
2046
+ /**
2047
+ * Join patch parts into a single patch string.
2048
+ *
2049
+ * Each part is terminated with a single `\n` if it lacks one, then parts are
2050
+ * concatenated verbatim — matching git's native multi-file diff layout. Parts
2051
+ * are NOT separated by an extra blank line and trailing newlines are NOT
2052
+ * stripped: a `GIT binary patch` block ends in a blank line that
2053
+ * `git apply --binary` requires, and stripping it corrupts the patch (#8899).
2054
+ */
2009
2055
  join(parts: string[]): string {
2010
- return `${parts
2011
- .map(part => (part.endsWith("\n") ? part : `${part}\n`))
2012
- .join("\n")
2013
- .replace(/\n+$/, "")}\n`;
2056
+ return parts.map(part => (part.endsWith("\n") ? part : `${part}\n`)).join("");
2014
2057
  },
2015
2058
  };
2016
2059
 
@@ -34,6 +34,13 @@ export interface BrowserFetchOptions {
34
34
  browser?: BrowserFallbackOptions;
35
35
  }
36
36
 
37
+ /**
38
+ * Upper bound on `page.close()` during teardown. A dead CDP session leaves
39
+ * puppeteer's close pending forever; `.catch()` only covers rejection, not a
40
+ * hang, so cleanup needs its own deadline (issue #8865).
41
+ */
42
+ const PAGE_CLOSE_TIMEOUT_MS = 5_000;
43
+
37
44
  async function fetchHtmlPage(url: string, options: BrowserFetchOptions, fetchImpl: FetchImpl): Promise<LoadedHtmlPage> {
38
45
  const response = await fetchImpl(url, {
39
46
  ...options.init,
@@ -74,8 +81,14 @@ async function browseHtmlPage(
74
81
  try {
75
82
  const activePage = await untilAborted(signal, () => handle.browser.newPage());
76
83
  page = activePage;
77
- await applyViewport(activePage);
78
- await applyStealthPatches(handle.browser, activePage, handle.stealth);
84
+ // Viewport and stealth setup talk to the same CDP session as the
85
+ // navigations below but were previously awaited raw. When the shared
86
+ // daemon or the page's target dies mid-setup, those puppeteer calls
87
+ // never settle and the provider promise hangs past
88
+ // SEARCH_HARD_TIMEOUT_MS — the abort signal had no listener at these
89
+ // await points (issue #8865).
90
+ await untilAborted(signal, () => applyViewport(activePage));
91
+ await untilAborted(signal, () => applyStealthPatches(handle.browser, activePage, handle.stealth));
79
92
  if (homeUrl) {
80
93
  await untilAborted(signal, () =>
81
94
  activePage.goto(homeUrl, { waitUntil: "domcontentloaded", timeout: timeoutMs }),
@@ -102,7 +115,12 @@ async function browseHtmlPage(
102
115
  }
103
116
  throw new Error("Browser fallback exhausted without a response");
104
117
  } finally {
105
- await page?.close().catch(() => undefined);
118
+ // Teardown must complete even when the caller's signal already fired
119
+ // (navigating away from a dead session leaves `close()` pending), so
120
+ // bound it with a fresh deadline instead of reusing `signal`.
121
+ if (page) {
122
+ await untilAborted(AbortSignal.timeout(PAGE_CLOSE_TIMEOUT_MS), () => page!.close()).catch(() => undefined);
123
+ }
106
124
  await releaseBrowser(handle, { kill: false });
107
125
  }
108
126
  }
@@ -35,6 +35,10 @@ export interface TinyFishSearchParams {
35
35
  page?: number;
36
36
  include_domains?: string[];
37
37
  exclude_domains?: string[];
38
+ /** ISO 3166-1 alpha-2 region, e.g. `IT`. Geolocates results. */
39
+ location?: string;
40
+ /** ISO 639-1 language, e.g. `it`. */
41
+ language?: string;
38
42
  signal?: AbortSignal;
39
43
  timeoutMs?: number;
40
44
  fetch?: FetchImpl;
@@ -74,6 +78,12 @@ async function callTinyFishSearch(apiKey: string, params: TinyFishSearchParams):
74
78
  if (params.exclude_domains?.length) {
75
79
  url.searchParams.set("exclude_domains", params.exclude_domains.join(","));
76
80
  }
81
+ if (params.location) {
82
+ url.searchParams.set("location", params.location);
83
+ }
84
+ if (params.language) {
85
+ url.searchParams.set("language", params.language);
86
+ }
77
87
  if (params.num_results !== undefined) {
78
88
  url.searchParams.set("num_results", String(params.num_results));
79
89
  }
@@ -133,6 +143,19 @@ function siteHosts(sites: readonly string[]): string[] {
133
143
  return [...hosts];
134
144
  }
135
145
 
146
+ /**
147
+ * Derive TinyFish `location` (ISO 3166-1 alpha-2, uppercase) and `language`
148
+ * (ISO 639-1, lowercase) from a parsed `lang:` directive. The region subtag is
149
+ * optional: `lang:it` yields language only, `lang:it-it` yields both. Non-region
150
+ * subtags (e.g. the script in `zh-hans`) never become a location.
151
+ */
152
+ function tinyFishLocale(lang: string | undefined): { location?: string; language?: string } {
153
+ if (!lang) return {};
154
+ const match = /^([a-z]{2})(?:[-_]([a-z]{2}))?(?:[-_]|$)/.exec(lang.toLowerCase());
155
+ if (!match) return {};
156
+ return { language: match[1], location: match[2]?.toUpperCase() };
157
+ }
158
+
136
159
  /** Execute TinyFish web search. */
137
160
  export async function searchTinyFish(params: SearchParams): Promise<SearchResponse> {
138
161
  const numResults = clampNumResults(params.numSearchResults ?? params.limit, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
@@ -152,6 +175,9 @@ export async function searchTinyFish(params: SearchParams): Promise<SearchRespon
152
175
  if (includeDomains.length > 0) tinyFishParams.include_domains = includeDomains;
153
176
  if (excludeDomains.length > 0) tinyFishParams.exclude_domains = excludeDomains;
154
177
  }
178
+ const { location, language } = tinyFishLocale(parsed.lang);
179
+ if (location) tinyFishParams.location = location;
180
+ if (language) tinyFishParams.language = language;
155
181
  const keyOrResolver: ApiKey = params.authStorage.resolver("tinyfish", {
156
182
  sessionId: params.sessionId,
157
183
  });