@oh-my-pi/pi-coding-agent 15.4.3 → 15.5.1

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 (136) hide show
  1. package/CHANGELOG.md +81 -5
  2. package/dist/types/cli/args.d.ts +2 -0
  3. package/dist/types/cli/auth-broker-cli.d.ts +1 -1
  4. package/dist/types/commands/launch.d.ts +8 -0
  5. package/dist/types/config/settings-schema.d.ts +42 -1
  6. package/dist/types/edit/index.d.ts +2 -0
  7. package/dist/types/extensibility/custom-tools/types.d.ts +8 -2
  8. package/dist/types/extensibility/hooks/types.d.ts +4 -0
  9. package/dist/types/hashline/executor.d.ts +6 -3
  10. package/dist/types/lsp/index.d.ts +9 -1
  11. package/dist/types/mcp/client.d.ts +2 -1
  12. package/dist/types/mcp/oauth-discovery.d.ts +4 -3
  13. package/dist/types/mcp/timeout.d.ts +9 -0
  14. package/dist/types/mcp/types.d.ts +1 -1
  15. package/dist/types/sdk.d.ts +2 -0
  16. package/dist/types/session/streaming-output.d.ts +1 -1
  17. package/dist/types/task/index.d.ts +2 -0
  18. package/dist/types/task/types.d.ts +4 -0
  19. package/dist/types/tools/approval.d.ts +46 -0
  20. package/dist/types/tools/ask.d.ts +1 -0
  21. package/dist/types/tools/ast-edit.d.ts +2 -0
  22. package/dist/types/tools/ast-grep.d.ts +1 -0
  23. package/dist/types/tools/bash.d.ts +11 -1
  24. package/dist/types/tools/browser.d.ts +2 -0
  25. package/dist/types/tools/calculator.d.ts +1 -0
  26. package/dist/types/tools/checkpoint.d.ts +2 -0
  27. package/dist/types/tools/debug.d.ts +9 -1
  28. package/dist/types/tools/eval.d.ts +2 -0
  29. package/dist/types/tools/find.d.ts +10 -0
  30. package/dist/types/tools/gh.d.ts +2 -1
  31. package/dist/types/tools/hindsight-recall.d.ts +1 -0
  32. package/dist/types/tools/hindsight-reflect.d.ts +1 -0
  33. package/dist/types/tools/hindsight-retain.d.ts +1 -0
  34. package/dist/types/tools/inspect-image.d.ts +1 -0
  35. package/dist/types/tools/irc.d.ts +1 -0
  36. package/dist/types/tools/job.d.ts +1 -0
  37. package/dist/types/tools/read.d.ts +1 -0
  38. package/dist/types/tools/recipe/index.d.ts +1 -0
  39. package/dist/types/tools/render-mermaid.d.ts +1 -0
  40. package/dist/types/tools/resolve.d.ts +1 -0
  41. package/dist/types/tools/search-tool-bm25.d.ts +1 -0
  42. package/dist/types/tools/search.d.ts +1 -0
  43. package/dist/types/tools/ssh.d.ts +2 -0
  44. package/dist/types/tools/todo-write.d.ts +1 -0
  45. package/dist/types/tools/write.d.ts +2 -0
  46. package/dist/types/tools/yield.d.ts +1 -0
  47. package/dist/types/web/search/index.d.ts +1 -0
  48. package/package.json +7 -7
  49. package/src/cli/args.ts +14 -0
  50. package/src/cli/auth-broker-cli.ts +171 -22
  51. package/src/commands/auth-broker.ts +3 -0
  52. package/src/commands/launch.ts +16 -0
  53. package/src/config/mcp-schema.json +2 -2
  54. package/src/config/model-registry.ts +19 -4
  55. package/src/config/prompt-templates.ts +0 -125
  56. package/src/config/settings-schema.ts +59 -1
  57. package/src/config/settings.ts +2 -1
  58. package/src/dap/session.ts +35 -2
  59. package/src/discovery/builtin.ts +2 -2
  60. package/src/discovery/mcp-json.ts +1 -1
  61. package/src/edit/index.ts +26 -0
  62. package/src/edit/modes/patch.ts +1 -1
  63. package/src/edit/streaming.ts +12 -2
  64. package/src/exec/bash-executor.ts +6 -2
  65. package/src/extensibility/custom-commands/bundled/review/index.ts +18 -14
  66. package/src/extensibility/custom-tools/types.ts +16 -2
  67. package/src/extensibility/extensions/wrapper.ts +36 -1
  68. package/src/extensibility/hooks/types.ts +8 -1
  69. package/src/hashline/apply.ts +47 -2
  70. package/src/hashline/executor.ts +46 -24
  71. package/src/internal-urls/docs-index.generated.ts +8 -7
  72. package/src/lsp/edits.ts +82 -29
  73. package/src/lsp/index.ts +38 -1
  74. package/src/lsp/utils.ts +1 -1
  75. package/src/main.ts +6 -0
  76. package/src/mcp/client.ts +8 -6
  77. package/src/mcp/oauth-discovery.ts +120 -32
  78. package/src/mcp/oauth-flow.ts +34 -6
  79. package/src/mcp/timeout.ts +59 -0
  80. package/src/mcp/transports/http.ts +42 -44
  81. package/src/mcp/transports/stdio.ts +8 -5
  82. package/src/mcp/types.ts +1 -1
  83. package/src/modes/components/hook-editor.ts +11 -3
  84. package/src/modes/components/mcp-add-wizard.ts +6 -2
  85. package/src/modes/components/model-selector.ts +33 -11
  86. package/src/modes/controllers/command-controller.ts +6 -4
  87. package/src/modes/controllers/mcp-command-controller.ts +8 -4
  88. package/src/prompts/review-custom-request.md +22 -0
  89. package/src/prompts/review-headless-request.md +16 -0
  90. package/src/prompts/review-request.md +2 -3
  91. package/src/prompts/system/project-prompt.md +4 -0
  92. package/src/prompts/tools/debug.md +1 -0
  93. package/src/prompts/tools/find.md +4 -2
  94. package/src/prompts/tools/hashline.md +43 -93
  95. package/src/sdk.ts +47 -73
  96. package/src/session/agent-session.ts +93 -27
  97. package/src/session/streaming-output.ts +1 -1
  98. package/src/slash-commands/helpers/usage-report.ts +3 -1
  99. package/src/task/executor.ts +11 -0
  100. package/src/task/index.ts +19 -0
  101. package/src/task/render.ts +12 -2
  102. package/src/task/types.ts +4 -0
  103. package/src/tools/approval.ts +185 -0
  104. package/src/tools/ask.ts +1 -0
  105. package/src/tools/ast-edit.ts +25 -1
  106. package/src/tools/ast-grep.ts +1 -0
  107. package/src/tools/bash.ts +69 -1
  108. package/src/tools/browser/tab-supervisor.ts +1 -1
  109. package/src/tools/browser.ts +15 -0
  110. package/src/tools/calculator.ts +1 -0
  111. package/src/tools/checkpoint.ts +2 -0
  112. package/src/tools/debug.ts +38 -0
  113. package/src/tools/eval.ts +15 -0
  114. package/src/tools/find.ts +17 -8
  115. package/src/tools/gh.ts +21 -1
  116. package/src/tools/hindsight-recall.ts +1 -0
  117. package/src/tools/hindsight-reflect.ts +1 -0
  118. package/src/tools/hindsight-retain.ts +1 -0
  119. package/src/tools/image-gen.ts +1 -0
  120. package/src/tools/inspect-image.ts +1 -0
  121. package/src/tools/irc.ts +1 -0
  122. package/src/tools/job.ts +1 -0
  123. package/src/tools/path-utils.ts +14 -1
  124. package/src/tools/read.ts +1 -0
  125. package/src/tools/recipe/index.ts +1 -0
  126. package/src/tools/render-mermaid.ts +1 -0
  127. package/src/tools/report-tool-issue.ts +1 -0
  128. package/src/tools/resolve.ts +1 -0
  129. package/src/tools/review.ts +1 -0
  130. package/src/tools/search-tool-bm25.ts +1 -0
  131. package/src/tools/search.ts +1 -0
  132. package/src/tools/ssh.ts +8 -0
  133. package/src/tools/todo-write.ts +1 -0
  134. package/src/tools/write.ts +12 -1
  135. package/src/tools/yield.ts +1 -0
  136. package/src/web/search/index.ts +2 -0
@@ -139,9 +139,16 @@ export interface HookUIContext {
139
139
  * Supports Ctrl+G to open external editor ($VISUAL or $EDITOR).
140
140
  * @param title - Title describing what is being edited
141
141
  * @param prefill - Optional initial text
142
+ * @param options - Optional dialog controls such as an abort signal
143
+ * @param editorOptions - Optional editor behavior; `promptStyle` makes Enter submit and Shift+Enter insert a newline
142
144
  * @returns Edited text, or undefined if cancelled (Escape)
143
145
  */
144
- editor(title: string, prefill?: string, options?: { signal?: AbortSignal }): Promise<string | undefined>;
146
+ editor(
147
+ title: string,
148
+ prefill?: string,
149
+ options?: { signal?: AbortSignal },
150
+ editorOptions?: { promptStyle?: boolean },
151
+ ): Promise<string | undefined>;
145
152
 
146
153
  /**
147
154
  * Get the current theme for styling text with ANSI codes.
@@ -264,6 +264,44 @@ function countMatchingSingleStructuralSuffixBoundary(
264
264
  return shouldDropSingleStructuralBoundary(replacement, replacement.slice(0, -1), expectedBalance) ? 1 : 0;
265
265
  }
266
266
 
267
+ /**
268
+ * Single-line non-structural boundary duplicate detector for replacement
269
+ * groups. Mirrors the same boundary check the pure-insert absorber uses for
270
+ * `ANCHOR↓` (leading) / `ANCHOR↑` (trailing) inserts, but applied to the
271
+ * top/bottom edges of an `A-B:payload` range. Catches mistakes like
272
+ * `103-138:const X = …` where line 102 already reads `const X = …` and the
273
+ * user really meant `103-138!` (delete only).
274
+ *
275
+ * Gated by `options.autoDropPureInsertDuplicates`: the existing 2+-line block
276
+ * absorb already runs unconditionally, and the structural single-line
277
+ * absorber is balance-validated; a non-structural single-line duplicate is
278
+ * ambiguous (could be an intentional `2:foo` over a line that happens to
279
+ * sit next to another `foo`), so we only fire when the user has opted in.
280
+ */
281
+ function countMatchingSingleNonStructuralPrefixDuplicate(
282
+ fileLines: string[],
283
+ startLine: number,
284
+ replacement: string[],
285
+ ): number {
286
+ if (replacement.length === 0 || startLine <= 1) return 0;
287
+ const line = replacement[0];
288
+ if (isStructuralClosingBoundaryLine(line)) return 0;
289
+ if (fileLines[startLine - 2] !== line) return 0;
290
+ return 1;
291
+ }
292
+
293
+ function countMatchingSingleNonStructuralSuffixDuplicate(
294
+ fileLines: string[],
295
+ endLine: number,
296
+ replacement: string[],
297
+ ): number {
298
+ if (replacement.length === 0 || endLine >= fileLines.length) return 0;
299
+ const line = replacement[replacement.length - 1];
300
+ if (isStructuralClosingBoundaryLine(line)) return 0;
301
+ if (fileLines[endLine] !== line) return 0;
302
+ return 1;
303
+ }
304
+
267
305
  function hasExternalTargets(lines: Iterable<number>, externalTargetLines: Set<number>): boolean {
268
306
  for (const line of lines) {
269
307
  if (externalTargetLines.has(line)) return true;
@@ -552,12 +590,19 @@ function absorbReplacementBoundaryDuplicates(
552
590
  const deletedBalance = computeDelimiterBalance(
553
591
  group.deletes.map(deleteEdit => fileLines[deleteEdit.anchor.line - 1] ?? ""),
554
592
  );
593
+ const optInSingleLineAbsorb = options.autoDropPureInsertDuplicates === true;
555
594
  const prefixCount =
556
595
  countMatchingPrefixBlock(fileLines, startLine, group.replacement) ||
557
- countMatchingSingleStructuralPrefixBoundary(fileLines, startLine, group.replacement, deletedBalance);
596
+ countMatchingSingleStructuralPrefixBoundary(fileLines, startLine, group.replacement, deletedBalance) ||
597
+ (optInSingleLineAbsorb
598
+ ? countMatchingSingleNonStructuralPrefixDuplicate(fileLines, startLine, group.replacement)
599
+ : 0);
558
600
  const suffixCount =
559
601
  countMatchingSuffixBlock(fileLines, endLine, group.replacement) ||
560
- countMatchingSingleStructuralSuffixBoundary(fileLines, endLine, group.replacement, deletedBalance);
602
+ countMatchingSingleStructuralSuffixBoundary(fileLines, endLine, group.replacement, deletedBalance) ||
603
+ (optInSingleLineAbsorb
604
+ ? countMatchingSingleNonStructuralSuffixDuplicate(fileLines, endLine, group.replacement)
605
+ : 0);
561
606
  const prefixLines = contiguousRange(startLine - prefixCount, prefixCount);
562
607
  const suffixLines = contiguousRange(endLine + 1, suffixCount);
563
608
  const safePrefixCount = hasExternalTargets(prefixLines, allTargetLines) ? 0 : prefixCount;
@@ -30,7 +30,6 @@ type PendingOp =
30
30
  interface Pending {
31
31
  op: PendingOp;
32
32
  payload: string[];
33
- pendingBlanks: number;
34
33
  }
35
34
 
36
35
  /**
@@ -81,16 +80,16 @@ export class HashlineExecutor {
81
80
  this.#terminated = true;
82
81
  return;
83
82
  case "header":
84
- this.#flushPending(false);
83
+ this.#flushPending();
85
84
  return;
86
85
  case "blank":
87
- if (this.#pending) this.#pending.pendingBlanks++;
86
+ if (this.#pending) this.#pending.payload.push("");
88
87
  return;
89
88
  case "payload":
90
89
  this.#handlePayload(token.text, token.lineNum);
91
90
  return;
92
91
  case "op-delete":
93
- this.#flushPending(false);
92
+ this.#flushPending();
94
93
  if (token.trailingPayload) {
95
94
  throw new Error(
96
95
  `line ${token.lineNum}: ${HL_OP_DELETE} deletes only. Payload is forbidden after ${HL_OP_DELETE}; use ${HL_OP_REPLACE} to replace.`,
@@ -102,32 +101,34 @@ export class HashlineExecutor {
102
101
  }
103
102
  return;
104
103
  case "op-insert":
105
- this.#flushPending(false);
104
+ this.#flushPending();
106
105
  this.#pending = {
107
106
  op: { kind: "insert", cursor: token.cursor, lineNum: token.lineNum },
108
107
  payload: [token.inlineBody ?? ""],
109
- pendingBlanks: 0,
110
108
  };
111
109
  return;
112
110
  case "op-replace":
113
- this.#flushPending(false);
111
+ this.#flushPending();
114
112
  validateRangeOrder(token.range, token.lineNum);
115
113
  this.#pending = {
116
114
  op: { kind: "replace", range: token.range, lineNum: token.lineNum },
117
115
  payload: [token.inlineBody ?? ""],
118
- pendingBlanks: 0,
119
116
  };
120
117
  return;
121
118
  }
122
119
  }
123
120
 
124
121
  /**
125
- * Flush any open pending op (including its trailing blank lines, which
126
- * are payload-significant) and return the accumulated edits and
127
- * warnings. The executor is single-use; reset() is required for reuse.
122
+ * Flush any open pending op (with its full accumulated payload, blanks
123
+ * included) and return the accumulated edits and warnings. The executor
124
+ * is single-use; reset() is required for reuse.
125
+ * Throws if two replace/delete ops target the same line — that pattern
126
+ * means the diff is painting a before/after picture instead of stating
127
+ * the final state, and applying both would silently duplicate content.
128
128
  */
129
129
  end(): { edits: HashlineEdit[]; warnings: string[] } {
130
- this.#flushPending(true);
130
+ this.#flushPending();
131
+ this.#validateNoOverlappingDeletes();
131
132
  return { edits: this.#edits, warnings: this.#warnings };
132
133
  }
133
134
 
@@ -140,16 +141,44 @@ export class HashlineExecutor {
140
141
  this.#terminated = false;
141
142
  }
142
143
 
144
+ /**
145
+ * Each `:` / `!` op contributes a delete edit per line in its range; if
146
+ * any line ends up targeted by deletes originating from two different
147
+ * source ops (distinguished by their `lineNum`), the patch is internally
148
+ * inconsistent. Common shape: a "before" `A-B:` followed by an "after"
149
+ * `A-B:` over the same range, or an `A-B:` that overlaps a later `N!` /
150
+ * `N:`. The applier would run both literally and the file would end up
151
+ * with two copies of the line, not a chosen winner.
152
+ */
153
+ #validateNoOverlappingDeletes(): void {
154
+ const sourceLinesByAnchor = new Map<number, number[]>();
155
+ for (const edit of this.#edits) {
156
+ if (edit.kind !== "delete") continue;
157
+ let sourceLines = sourceLinesByAnchor.get(edit.anchor.line);
158
+ if (sourceLines === undefined) {
159
+ sourceLines = [];
160
+ sourceLinesByAnchor.set(edit.anchor.line, sourceLines);
161
+ }
162
+ if (!sourceLines.includes(edit.lineNum)) sourceLines.push(edit.lineNum);
163
+ }
164
+ for (const [anchorLine, sourceLines] of sourceLinesByAnchor) {
165
+ if (sourceLines.length < 2) continue;
166
+ const [firstOp, secondOp] = [...sourceLines].sort((a, b) => a - b);
167
+ throw new Error(
168
+ `line ${secondOp}: anchor line ${anchorLine} is already targeted by the ${HL_OP_REPLACE}/${HL_OP_DELETE} op on line ${firstOp}. ` +
169
+ `Issue ONE op per range; payload is only the final desired content, never a before/after pair.`,
170
+ );
171
+ }
172
+ }
173
+
143
174
  #handlePayload(text: string, lineNum: number): void {
144
175
  if (this.#pending) {
145
- this.#flushPendingBlanks();
146
176
  this.#pending.payload.push(text);
147
177
  return;
148
178
  }
149
179
 
150
- // Whitespace-only payload outside any pending op is a visual
151
- // separator (matches the legacy outer-loop isBlankLine skip);
152
- // only fully-empty lines arrive as `blank` tokens.
180
+ // Whitespace-only payload outside any pending op is silently dropped;
181
+ // fully empty lines arrive as `blank` tokens.
153
182
  if (text.trim().length === 0) return;
154
183
  // Orphan payload outside any pending op: pick the most specific
155
184
  // diagnostic so the model sees the actionable hint.
@@ -174,16 +203,9 @@ export class HashlineExecutor {
174
203
  );
175
204
  }
176
205
 
177
- #flushPendingBlanks(): void {
178
- if (!this.#pending) return;
179
- for (let count = 0; count < this.#pending.pendingBlanks; count++) this.#pending.payload.push("");
180
- this.#pending.pendingBlanks = 0;
181
- }
182
-
183
- #flushPending(includeTrailingBlanks: boolean): void {
206
+ #flushPending(): void {
184
207
  const pending = this.#pending;
185
208
  if (!pending) return;
186
- if (includeTrailingBlanks) this.#flushPendingBlanks();
187
209
 
188
210
  const { op, payload } = pending;
189
211
  const linesToInsert = payload;