@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
@@ -0,0 +1,16 @@
1
+ import type { TextGenerationPipeline } from "@huggingface/transformers";
2
+
3
+ export function buildCompletionPrompt(
4
+ tokenizer: TextGenerationPipeline["tokenizer"],
5
+ promptText: string,
6
+ systemPrompt?: string,
7
+ ): string {
8
+ const userMessage = { role: "user", content: promptText };
9
+ const chat = systemPrompt?.trim() ? [{ role: "system", content: systemPrompt.trim() }, userMessage] : [userMessage];
10
+ const chatTemplateOptions = {
11
+ add_generation_prompt: true,
12
+ tokenize: false,
13
+ enable_thinking: false,
14
+ };
15
+ return `${tokenizer.apply_chat_template(chat, chatTemplateOptions)}`;
16
+ }
@@ -52,6 +52,12 @@ export interface TinyTitleGenerateOptions {
52
52
  systemPrompt?: string;
53
53
  }
54
54
 
55
+ export interface TinyModelCompletionOptions {
56
+ maxTokens?: number;
57
+ signal?: AbortSignal;
58
+ systemPrompt?: string;
59
+ }
60
+
55
61
  function normalizeTinyTitleGenerateOptions(
56
62
  options: AbortSignal | TinyTitleGenerateOptions | undefined,
57
63
  ): TinyTitleGenerateOptions {
@@ -260,11 +266,7 @@ export class TinyTitleClient {
260
266
  }
261
267
  }
262
268
 
263
- async complete(
264
- modelKey: string,
265
- prompt: string,
266
- options: { maxTokens?: number; signal?: AbortSignal } = {},
267
- ): Promise<string | null> {
269
+ async complete(modelKey: string, prompt: string, options: TinyModelCompletionOptions = {}): Promise<string | null> {
268
270
  if (!isTinyMemoryLocalModelKey(modelKey)) return null;
269
271
  if (options.signal?.aborted || this.#failedModels.has(modelKey)) return null;
270
272
 
@@ -281,7 +283,14 @@ export class TinyTitleClient {
281
283
  };
282
284
  options.signal?.addEventListener("abort", abort, { once: true });
283
285
  try {
284
- worker.send({ type: "complete", id, modelKey, prompt, maxTokens: options.maxTokens });
286
+ worker.send({
287
+ type: "complete",
288
+ id,
289
+ modelKey,
290
+ prompt,
291
+ maxTokens: options.maxTokens,
292
+ systemPrompt: options.systemPrompt,
293
+ });
285
294
  return await promise;
286
295
  } finally {
287
296
  options.signal?.removeEventListener("abort", abort);
@@ -30,7 +30,14 @@ export interface TinyTitleProgressEvent {
30
30
  export type TinyTitleWorkerInbound =
31
31
  | { type: "ping"; id: string }
32
32
  | { type: "generate"; id: string; modelKey: TinyTitleLocalModelKey; message: string; systemPrompt?: string }
33
- | { type: "complete"; id: string; modelKey: TinyLocalModelKey; prompt: string; maxTokens?: number }
33
+ | {
34
+ type: "complete";
35
+ id: string;
36
+ modelKey: TinyLocalModelKey;
37
+ prompt: string;
38
+ maxTokens?: number;
39
+ systemPrompt?: string;
40
+ }
34
41
  | { type: "download"; id: string; modelKey: TinyLocalModelKey };
35
42
 
36
43
  export type TinyTitleWorkerOutbound =
@@ -19,6 +19,7 @@ import {
19
19
  sendProgress,
20
20
  type TransformersRuntimeMetadata,
21
21
  } from "../subprocess/worker-runtime";
22
+ import { buildCompletionPrompt } from "./completion-prompt";
22
23
  import { resolveTinyModelDevicePreference, type TinyModelDevice, tinyModelDeviceLoadOrder } from "./device";
23
24
  import { resolveTinyModelDtypeOverride, type TinyModelDtype } from "./dtype";
24
25
  import { formatTitleUserMessage } from "./message-preproc";
@@ -42,7 +43,7 @@ const TINY_TITLE_SYSTEM_PROMPT = prompt.render(titleSystemPrompt);
42
43
  const tinyModelDevicePreference = resolveTinyModelDevicePreference();
43
44
  const tinyModelDtypeOverride = resolveTinyModelDtypeOverride();
44
45
 
45
- interface TransformersRuntime extends TransformersRuntimeMetadata {
46
+ export interface TransformersRuntime extends TransformersRuntimeMetadata {
46
47
  env: {
47
48
  cacheDir?: string;
48
49
  allowLocalModels?: boolean;
@@ -79,7 +80,13 @@ function getTinyTitleRuntimeDir(): string {
79
80
  );
80
81
  }
81
82
 
82
- function createStopOnTextCriteria(
83
+ /** Stops generation at the first occurrence of `text` in the *generated* tokens.
84
+ *
85
+ * The window must be anchored to the generation boundary, not to the end of the
86
+ * whole sequence: a prompt that itself contains the stop string (chat-level
87
+ * few-shot examples ending in `</title>`, for instance) would otherwise match on
88
+ * prompt tokens and stop before the model emits anything. */
89
+ export function createStopOnTextCriteria(
83
90
  transformers: TransformersRuntime,
84
91
  tokenizer: TextGenerationPipeline["tokenizer"],
85
92
  text: string,
@@ -87,6 +94,8 @@ function createStopOnTextCriteria(
87
94
  class StopOnTextCriteria extends transformers.StoppingCriteria {
88
95
  #tokenizer: TextGenerationPipeline["tokenizer"];
89
96
  #text: string;
97
+ /** First generated index per batch entry, captured on the first call. */
98
+ #generatedStarts: number[] = [];
90
99
 
91
100
  constructor() {
92
101
  super();
@@ -95,8 +104,10 @@ function createStopOnTextCriteria(
95
104
  }
96
105
 
97
106
  override _call(inputIds: number[][]): boolean[] {
98
- return inputIds.map(ids => {
99
- const tail = ids.slice(-STOP_DECODE_WINDOW_TOKENS);
107
+ return inputIds.map((ids, index) => {
108
+ const generatedStart = this.#generatedStarts[index] ?? Math.max(0, ids.length - 1);
109
+ this.#generatedStarts[index] = generatedStart;
110
+ const tail = ids.slice(Math.max(generatedStart, ids.length - STOP_DECODE_WINDOW_TOKENS));
100
111
  const decoded = this.#tokenizer.decode(tail, {
101
112
  skip_special_tokens: false,
102
113
  clean_up_tokenization_spaces: false,
@@ -265,21 +276,10 @@ async function generateTitle(
265
276
  return extractTinyTitle(output[0]?.generated_text ?? "", message);
266
277
  }
267
278
 
268
- function buildCompletionPrompt(generator: TextGenerationPipeline, promptText: string): string {
269
- const chat = [{ role: "user", content: promptText }];
270
- const chatTemplateOptions = {
271
- add_generation_prompt: true,
272
- tokenize: false,
273
- enable_thinking: false,
274
- };
275
- return `${generator.tokenizer.apply_chat_template(chat, chatTemplateOptions)}`;
276
- }
277
-
278
279
  /**
279
- * Generic single-turn completion used by Mnemopi memory tasks (fact extraction
280
- * and consolidation). The caller (Mnemopi) supplies the full task prompt; we
281
- * wrap it as the user turn, decode greedily, and return the raw text for the
282
- * caller's own parser. Output is capped to keep local inference latency bounded.
280
+ * Completion path for Mnemopi memory tasks. Extraction can carry a dedicated
281
+ * system prompt and user payload; consolidation retains the generic user-only
282
+ * prompt. Output is capped to keep local inference latency bounded.
283
283
  */
284
284
  async function generateCompletion(
285
285
  transport: TinyTitleTransport,
@@ -287,9 +287,10 @@ async function generateCompletion(
287
287
  modelKey: TinyLocalModelKey,
288
288
  promptText: string,
289
289
  maxTokens: number | undefined,
290
+ systemPrompt: string | undefined,
290
291
  ): Promise<string | null> {
291
292
  const generator = await loadPipeline(modelKey, transport, requestId);
292
- const text = buildCompletionPrompt(generator, promptText);
293
+ const text = buildCompletionPrompt(generator.tokenizer, promptText, systemPrompt);
293
294
  const requested = maxTokens ?? MEMORY_COMPLETION_DEFAULT_MAX_NEW_TOKENS;
294
295
  const maxNewTokens = Math.min(Math.max(1, requested), COMPLETION_MAX_NEW_TOKENS);
295
296
  const output = (await generator(text, {
@@ -332,6 +333,7 @@ async function handleQueuedRequest(
332
333
  request.modelKey,
333
334
  request.prompt,
334
335
  request.maxTokens,
336
+ request.systemPrompt,
335
337
  );
336
338
  transport.send({ type: "completion", id: request.id, text });
337
339
  return;
package/src/tools/bash.ts CHANGED
@@ -166,7 +166,13 @@ function shellBuiltinsDisabled(settings: Settings): boolean {
166
166
  */
167
167
  export const CRITICAL_BASH_PATTERNS = [
168
168
  // Recursive destruction.
169
- /\brm\s+-[a-z]*[rRfF][a-z]*\s+\//i, // rm -rf /, rm -fr /, rm -r /, rm -f /…
169
+ // Options may sit on either side of the recursive/force flag, so only that flag is pinned and
170
+ // any other options are skipped: `rm -rf /`, `rm -rf -- /`, `rm --recursive --force /`,
171
+ // `rm -rf -v /`, `rm -v -rf /`. An absolute target is still required.
172
+ /\brm\s+(?:-\S+\s+)*(?:-[a-z]*[rRfF][a-z]*|--recursive|--force)\s+(?:-\S+\s+)*\//i,
173
+ // `--no-preserve-root` defeats coreutils' own refusal to recurse on `/`, so it is critical
174
+ // wherever it appears — including forms this list would otherwise reach only via the target.
175
+ /\brm\s+(?:-\S+\s+)*--no-preserve-root\b/i,
170
176
  /\bsudo\s+rm\b/i, // any `sudo rm`.
171
177
  /\bchmod\s+-R\s+[0-7]+\s+\//i, // `chmod -R 777 /`.
172
178
  /\bchmod\s+-R\s+[ugoa+\-=rwxXst,]+\s+\//, // `chmod -R u+x /`, `chmod -R u+rwx,o+w /etc` (symbolic mode, root target).
@@ -72,7 +72,20 @@ export async function readHashlineHeaderContext(
72
72
  absolutePath: string,
73
73
  cwd: string,
74
74
  ): Promise<HashlineHeaderContext> {
75
- const fullText = await Bun.file(absolutePath).text();
75
+ return hashlineHeaderContextForText(session, absolutePath, cwd, await Bun.file(absolutePath).text());
76
+ }
77
+
78
+ /**
79
+ * {@link readHashlineHeaderContext} for a caller that already holds the file's
80
+ * full text, so the file is not reopened just to hash it. Line endings are
81
+ * normalized here, exactly as the reading variant does.
82
+ */
83
+ export function hashlineHeaderContextForText(
84
+ session: ToolSession,
85
+ absolutePath: string,
86
+ cwd: string,
87
+ fullText: string,
88
+ ): HashlineHeaderContext {
76
89
  const context = recordFullHashlineContext(
77
90
  session,
78
91
  absolutePath,
@@ -392,6 +405,7 @@ export function buildInMemoryTextResult(
392
405
  const buildLineEntries = (endLineDisplay: number): LineEntry[] =>
393
406
  buildLineEntriesWithBlockContext(allLines, [{ startLine: startLineDisplay, endLine: endLineDisplay }], {
394
407
  path: options.sourcePath,
408
+ text,
395
409
  });
396
410
 
397
411
  let outputText: string;
@@ -534,7 +548,7 @@ export function buildInMemoryMultiRangeResult(
534
548
  if (options.raw === true) {
535
549
  outputText = rawParts.length > 0 ? rawParts.join("\n\n…\n\n") : "";
536
550
  } else if (visibleSpans.length > 0) {
537
- const entries = buildLineEntriesWithBlockContext(allLines, visibleSpans, { path: options.sourcePath });
551
+ const entries = buildLineEntriesWithBlockContext(allLines, visibleSpans, { path: options.sourcePath, text });
538
552
  if (shouldAddHashLines) seenLines = lineNumbersFromEntries(entries);
539
553
  const firstLine = entries.find(entry => entry.kind === "line");
540
554
  if (firstLine?.kind === "line") {
@@ -52,21 +52,26 @@ export function routeReadThroughBridge(
52
52
  if (!bridge?.capabilities.readTextFile || !bridge.readTextFile) return undefined;
53
53
  return bridge.readTextFile({ path: absolutePath, ...options });
54
54
  }
55
+ /**
56
+ * Structural summary of `absolutePath`, or `null` when the file is too large,
57
+ * too short, or unparseable. `diskText` lets a caller that already read the file
58
+ * hand those bytes over instead of forcing a second read; an ACP bridge still
59
+ * wins, since the editor's buffer is the source of truth.
60
+ */
55
61
  export async function trySummarize(
56
62
  session: ToolSession,
57
63
  absolutePath: string,
58
64
  fileSize: number,
59
65
  signal?: AbortSignal,
66
+ diskText?: string,
60
67
  ): Promise<SummaryResult | null> {
61
68
  if (fileSize > MAX_SUMMARY_BYTES) return null;
62
69
 
63
70
  try {
64
71
  throwIfAborted(signal);
65
72
  const bridgePromise = routeReadThroughBridge(session, absolutePath);
66
- const code =
67
- bridgePromise !== undefined
68
- ? await bridgePromise.catch(() => Bun.file(absolutePath).text())
69
- : await Bun.file(absolutePath).text();
73
+ const readDisk = async () => diskText ?? (await Bun.file(absolutePath).text());
74
+ const code = bridgePromise !== undefined ? await bridgePromise.catch(readDisk) : await readDisk();
70
75
  throwIfAborted(signal);
71
76
  const lineCount = countTextLines(code);
72
77
  if (lineCount > MAX_SUMMARY_LINES) return null;