@oh-my-pi/pi-coding-agent 13.19.0 → 14.0.3

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 (205) hide show
  1. package/CHANGELOG.md +277 -2
  2. package/package.json +86 -20
  3. package/scripts/format-prompts.ts +2 -2
  4. package/src/autoresearch/apply-contract-to-state.ts +24 -0
  5. package/src/autoresearch/contract.ts +0 -44
  6. package/src/autoresearch/dashboard.ts +1 -2
  7. package/src/autoresearch/git.ts +91 -0
  8. package/src/autoresearch/helpers.ts +49 -0
  9. package/src/autoresearch/index.ts +28 -187
  10. package/src/autoresearch/prompt.md +26 -9
  11. package/src/autoresearch/state.ts +0 -6
  12. package/src/autoresearch/tools/init-experiment.ts +202 -117
  13. package/src/autoresearch/tools/log-experiment.ts +83 -125
  14. package/src/autoresearch/tools/run-experiment.ts +48 -10
  15. package/src/autoresearch/types.ts +2 -2
  16. package/src/capability/index.ts +4 -2
  17. package/src/cli/file-processor.ts +3 -3
  18. package/src/cli/grep-cli.ts +8 -8
  19. package/src/cli/grievances-cli.ts +78 -0
  20. package/src/cli/read-cli.ts +67 -0
  21. package/src/cli/setup-cli.ts +4 -4
  22. package/src/cli/update-cli.ts +3 -3
  23. package/src/cli.ts +2 -0
  24. package/src/commands/grep.ts +6 -1
  25. package/src/commands/grievances.ts +20 -0
  26. package/src/commands/read.ts +33 -0
  27. package/src/commit/agentic/agent.ts +5 -5
  28. package/src/commit/agentic/index.ts +3 -4
  29. package/src/commit/agentic/tools/analyze-file.ts +3 -3
  30. package/src/commit/agentic/validation.ts +1 -1
  31. package/src/commit/analysis/conventional.ts +4 -4
  32. package/src/commit/analysis/summary.ts +3 -3
  33. package/src/commit/changelog/generate.ts +4 -4
  34. package/src/commit/map-reduce/map-phase.ts +4 -4
  35. package/src/commit/map-reduce/reduce-phase.ts +4 -4
  36. package/src/commit/pipeline.ts +3 -4
  37. package/src/config/model-registry.ts +17 -3
  38. package/src/config/prompt-templates.ts +44 -226
  39. package/src/config/resolve-config-value.ts +4 -2
  40. package/src/config/settings-schema.ts +54 -2
  41. package/src/config/settings.ts +25 -26
  42. package/src/dap/client.ts +674 -0
  43. package/src/dap/config.ts +150 -0
  44. package/src/dap/defaults.json +211 -0
  45. package/src/dap/index.ts +4 -0
  46. package/src/dap/session.ts +1255 -0
  47. package/src/dap/types.ts +600 -0
  48. package/src/debug/log-viewer.ts +3 -2
  49. package/src/discovery/builtin.ts +1 -2
  50. package/src/discovery/codex.ts +2 -2
  51. package/src/discovery/github.ts +2 -1
  52. package/src/discovery/helpers.ts +2 -2
  53. package/src/discovery/opencode.ts +2 -2
  54. package/src/edit/diff.ts +818 -0
  55. package/src/edit/index.ts +309 -0
  56. package/src/edit/line-hash.ts +67 -0
  57. package/src/edit/modes/chunk.ts +454 -0
  58. package/src/{patch → edit/modes}/hashline.ts +741 -361
  59. package/src/{patch/applicator.ts → edit/modes/patch.ts} +420 -117
  60. package/src/{patch/fuzzy.ts → edit/modes/replace.ts} +519 -197
  61. package/src/{patch → edit}/normalize.ts +97 -76
  62. package/src/{patch/shared.ts → edit/renderer.ts} +181 -108
  63. package/src/exec/bash-executor.ts +4 -2
  64. package/src/exec/idle-timeout-watchdog.ts +126 -0
  65. package/src/exec/non-interactive-env.ts +5 -0
  66. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +2 -2
  67. package/src/extensibility/custom-commands/bundled/review/index.ts +36 -15
  68. package/src/extensibility/custom-commands/loader.ts +1 -2
  69. package/src/extensibility/custom-tools/loader.ts +34 -11
  70. package/src/extensibility/extensions/loader.ts +9 -4
  71. package/src/extensibility/extensions/runner.ts +24 -1
  72. package/src/extensibility/extensions/types.ts +1 -1
  73. package/src/extensibility/hooks/loader.ts +5 -6
  74. package/src/extensibility/hooks/types.ts +1 -1
  75. package/src/extensibility/plugins/doctor.ts +2 -1
  76. package/src/extensibility/slash-commands.ts +3 -7
  77. package/src/index.ts +2 -1
  78. package/src/internal-urls/docs-index.generated.ts +11 -11
  79. package/src/ipy/executor.ts +58 -17
  80. package/src/ipy/gateway-coordinator.ts +6 -4
  81. package/src/ipy/kernel.ts +45 -22
  82. package/src/ipy/runtime.ts +2 -2
  83. package/src/lsp/client.ts +7 -4
  84. package/src/lsp/clients/lsp-linter-client.ts +4 -4
  85. package/src/lsp/config.ts +20 -4
  86. package/src/lsp/defaults.json +688 -154
  87. package/src/lsp/index.ts +234 -45
  88. package/src/lsp/lspmux.ts +2 -2
  89. package/src/lsp/startup-events.ts +13 -0
  90. package/src/lsp/types.ts +12 -1
  91. package/src/lsp/utils.ts +8 -1
  92. package/src/main.ts +102 -46
  93. package/src/memories/index.ts +4 -5
  94. package/src/modes/acp/acp-agent.ts +563 -163
  95. package/src/modes/acp/acp-event-mapper.ts +9 -1
  96. package/src/modes/acp/acp-mode.ts +4 -2
  97. package/src/modes/components/agent-dashboard.ts +3 -4
  98. package/src/modes/components/diff.ts +6 -7
  99. package/src/modes/components/read-tool-group.ts +6 -12
  100. package/src/modes/components/session-observer-overlay.ts +21 -12
  101. package/src/modes/components/settings-defs.ts +5 -0
  102. package/src/modes/components/tool-execution.ts +1 -1
  103. package/src/modes/components/welcome.ts +1 -1
  104. package/src/modes/controllers/btw-controller.ts +2 -2
  105. package/src/modes/controllers/command-controller.ts +3 -2
  106. package/src/modes/controllers/input-controller.ts +12 -8
  107. package/src/modes/index.ts +20 -2
  108. package/src/modes/interactive-mode.ts +94 -37
  109. package/src/modes/rpc/host-tools.ts +186 -0
  110. package/src/modes/rpc/rpc-client.ts +178 -13
  111. package/src/modes/rpc/rpc-mode.ts +73 -3
  112. package/src/modes/rpc/rpc-types.ts +53 -1
  113. package/src/modes/theme/theme.ts +80 -8
  114. package/src/modes/types.ts +2 -2
  115. package/src/prompts/review-request.md +6 -0
  116. package/src/prompts/system/system-prompt.md +2 -1
  117. package/src/prompts/tools/chunk-edit.md +223 -0
  118. package/src/prompts/tools/debug.md +43 -0
  119. package/src/prompts/tools/grep.md +3 -0
  120. package/src/prompts/tools/lsp.md +5 -5
  121. package/src/prompts/tools/read-chunk.md +17 -0
  122. package/src/prompts/tools/read.md +19 -5
  123. package/src/sdk.ts +190 -154
  124. package/src/secrets/obfuscator.ts +1 -1
  125. package/src/session/agent-session.ts +306 -256
  126. package/src/session/agent-storage.ts +12 -12
  127. package/src/session/compaction/branch-summarization.ts +3 -3
  128. package/src/session/compaction/compaction.ts +5 -6
  129. package/src/session/compaction/utils.ts +3 -3
  130. package/src/session/history-storage.ts +62 -19
  131. package/src/session/messages.ts +3 -3
  132. package/src/session/session-dump-format.ts +203 -0
  133. package/src/session/session-storage.ts +4 -2
  134. package/src/session/streaming-output.ts +1 -1
  135. package/src/session/tool-choice-queue.ts +213 -0
  136. package/src/slash-commands/builtin-registry.ts +56 -8
  137. package/src/ssh/connection-manager.ts +2 -2
  138. package/src/ssh/sshfs-mount.ts +5 -5
  139. package/src/stt/downloader.ts +4 -4
  140. package/src/stt/recorder.ts +4 -4
  141. package/src/stt/transcriber.ts +2 -2
  142. package/src/system-prompt.ts +21 -13
  143. package/src/task/agents.ts +5 -6
  144. package/src/task/commands.ts +2 -5
  145. package/src/task/executor.ts +4 -4
  146. package/src/task/index.ts +3 -4
  147. package/src/task/template.ts +2 -2
  148. package/src/task/worktree.ts +4 -4
  149. package/src/tools/ask.ts +2 -3
  150. package/src/tools/ast-edit.ts +7 -7
  151. package/src/tools/ast-grep.ts +7 -7
  152. package/src/tools/auto-generated-guard.ts +36 -41
  153. package/src/tools/await-tool.ts +2 -2
  154. package/src/tools/bash.ts +5 -23
  155. package/src/tools/browser.ts +4 -5
  156. package/src/tools/calculator.ts +2 -3
  157. package/src/tools/cancel-job.ts +2 -2
  158. package/src/tools/checkpoint.ts +3 -3
  159. package/src/tools/debug.ts +1007 -0
  160. package/src/tools/exit-plan-mode.ts +2 -3
  161. package/src/tools/fetch.ts +67 -3
  162. package/src/tools/find.ts +4 -5
  163. package/src/tools/fs-cache-invalidation.ts +5 -0
  164. package/src/tools/gemini-image.ts +13 -5
  165. package/src/tools/gh.ts +10 -11
  166. package/src/tools/grep.ts +57 -9
  167. package/src/tools/index.ts +44 -22
  168. package/src/tools/inspect-image.ts +4 -4
  169. package/src/tools/output-meta.ts +1 -1
  170. package/src/tools/python.ts +19 -6
  171. package/src/tools/read.ts +198 -67
  172. package/src/tools/render-mermaid.ts +2 -3
  173. package/src/tools/render-utils.ts +20 -6
  174. package/src/tools/renderers.ts +3 -1
  175. package/src/tools/report-tool-issue.ts +80 -0
  176. package/src/tools/resolve.ts +70 -39
  177. package/src/tools/search-tool-bm25.ts +2 -2
  178. package/src/tools/ssh.ts +2 -2
  179. package/src/tools/todo-write.ts +2 -2
  180. package/src/tools/tool-timeouts.ts +1 -0
  181. package/src/tools/write.ts +5 -6
  182. package/src/tui/tree-list.ts +3 -1
  183. package/src/utils/clipboard.ts +80 -0
  184. package/src/utils/commit-message-generator.ts +2 -3
  185. package/src/utils/edit-mode.ts +49 -0
  186. package/src/utils/file-display-mode.ts +6 -5
  187. package/src/utils/file-mentions.ts +8 -7
  188. package/src/utils/git.ts +4 -4
  189. package/src/utils/image-loading.ts +98 -0
  190. package/src/utils/title-generator.ts +2 -3
  191. package/src/utils/tools-manager.ts +6 -6
  192. package/src/web/scrapers/choosealicense.ts +1 -1
  193. package/src/web/search/index.ts +3 -3
  194. package/src/autoresearch/command-initialize.md +0 -34
  195. package/src/patch/diff.ts +0 -433
  196. package/src/patch/index.ts +0 -888
  197. package/src/patch/parser.ts +0 -532
  198. package/src/patch/types.ts +0 -292
  199. package/src/prompts/agents/oracle.md +0 -77
  200. package/src/tools/pending-action.ts +0 -49
  201. package/src/utils/child-process.ts +0 -88
  202. package/src/utils/frontmatter.ts +0 -117
  203. package/src/utils/image-input.ts +0 -274
  204. package/src/utils/mime.ts +0 -53
  205. package/src/utils/prompt-format.ts +0 -170
@@ -0,0 +1,126 @@
1
+ export type ExecutionAbortReason = "idle-timeout" | "signal";
2
+
3
+ export interface IdleTimeoutWatchdogOptions {
4
+ timeoutMs?: number;
5
+ signal?: AbortSignal;
6
+ hardTimeoutGraceMs: number;
7
+ onAbort?: (reason: ExecutionAbortReason) => void;
8
+ }
9
+
10
+ export class IdleTimeoutWatchdog {
11
+ #abortController = new AbortController();
12
+ #abortReason?: ExecutionAbortReason;
13
+ #hardTimeoutDeferred = Promise.withResolvers<"hard-timeout">();
14
+ #hardTimeoutGraceMs: number;
15
+ #hardTimeoutTimer?: NodeJS.Timeout;
16
+ #idleTimer?: NodeJS.Timeout;
17
+ #onAbort?: (reason: ExecutionAbortReason) => void;
18
+ #signal?: AbortSignal;
19
+ #signalAbortHandler?: () => void;
20
+ #timeoutMs?: number;
21
+
22
+ constructor(options: IdleTimeoutWatchdogOptions) {
23
+ this.#timeoutMs = options.timeoutMs;
24
+ this.#hardTimeoutGraceMs = options.hardTimeoutGraceMs;
25
+ this.#onAbort = options.onAbort;
26
+ this.#signal = options.signal;
27
+
28
+ if (this.#signal) {
29
+ if (this.#signal.aborted) {
30
+ this.#abort("signal");
31
+ return;
32
+ }
33
+
34
+ this.#signalAbortHandler = () => {
35
+ this.#abort("signal");
36
+ };
37
+ this.#signal.addEventListener("abort", this.#signalAbortHandler, { once: true });
38
+ }
39
+
40
+ this.touch();
41
+ }
42
+
43
+ get abortedBySignal(): boolean {
44
+ return this.#abortReason === "signal";
45
+ }
46
+
47
+ get hardTimeoutPromise(): Promise<"hard-timeout"> {
48
+ return this.#hardTimeoutDeferred.promise;
49
+ }
50
+
51
+ get signal(): AbortSignal {
52
+ return this.#abortController.signal;
53
+ }
54
+
55
+ get timedOut(): boolean {
56
+ return this.#abortReason === "idle-timeout";
57
+ }
58
+
59
+ touch(): void {
60
+ if (this.#abortReason || this.#timeoutMs === undefined || this.#timeoutMs <= 0) {
61
+ return;
62
+ }
63
+
64
+ if (this.#idleTimer) {
65
+ clearTimeout(this.#idleTimer);
66
+ }
67
+
68
+ this.#idleTimer = setTimeout(() => {
69
+ this.#abort("idle-timeout");
70
+ }, this.#timeoutMs);
71
+ }
72
+
73
+ dispose(): void {
74
+ if (this.#idleTimer) {
75
+ clearTimeout(this.#idleTimer);
76
+ this.#idleTimer = undefined;
77
+ }
78
+ if (this.#hardTimeoutTimer) {
79
+ clearTimeout(this.#hardTimeoutTimer);
80
+ this.#hardTimeoutTimer = undefined;
81
+ }
82
+ if (this.#signal && this.#signalAbortHandler) {
83
+ this.#signal.removeEventListener("abort", this.#signalAbortHandler);
84
+ this.#signalAbortHandler = undefined;
85
+ }
86
+ }
87
+
88
+ #abort(reason: ExecutionAbortReason): void {
89
+ if (this.#abortReason) {
90
+ return;
91
+ }
92
+
93
+ this.#abortReason = reason;
94
+
95
+ if (this.#idleTimer) {
96
+ clearTimeout(this.#idleTimer);
97
+ this.#idleTimer = undefined;
98
+ }
99
+
100
+ if (!this.#abortController.signal.aborted) {
101
+ this.#abortController.abort(reason);
102
+ }
103
+
104
+ this.#onAbort?.(reason);
105
+ this.#armHardTimeout();
106
+ }
107
+
108
+ #armHardTimeout(): void {
109
+ if (this.#hardTimeoutTimer || this.#hardTimeoutGraceMs <= 0) {
110
+ return;
111
+ }
112
+
113
+ this.#hardTimeoutTimer = setTimeout(() => {
114
+ this.#hardTimeoutDeferred.resolve("hard-timeout");
115
+ }, this.#hardTimeoutGraceMs);
116
+ }
117
+ }
118
+
119
+ export function formatIdleTimeoutMessage(timeoutMs?: number): string {
120
+ if (timeoutMs === undefined) {
121
+ return "Command timed out without output";
122
+ }
123
+
124
+ const seconds = Math.max(1, Math.round(timeoutMs / 1000));
125
+ return `Command timed out after ${seconds} seconds without output`;
126
+ }
@@ -13,6 +13,11 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
13
13
  AWS_PAGER: "",
14
14
  HOMEBREW_PAGER: "cat",
15
15
  LESS: "FRX",
16
+ // Disable terminal features that can block the process.
17
+ TERM: "dumb",
18
+ GPG_TTY: "not a tty",
19
+ NO_COLOR: "1",
20
+ PYTHONUNBUFFERED: "1",
16
21
  // Disable editor and terminal credential prompts.
17
22
  GIT_EDITOR: "true",
18
23
  VISUAL: "true",
@@ -1,4 +1,4 @@
1
- import { renderPromptTemplate } from "../../../../config/prompt-templates";
1
+ import { prompt } from "@oh-my-pi/pi-utils";
2
2
  import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
3
3
  import type { HookCommandContext } from "../../../../extensibility/hooks/types";
4
4
  import ciGreenRequestTemplate from "../../../../prompts/ci-green-request.md" with { type: "text" };
@@ -20,6 +20,6 @@ export class GreenCommand implements CustomCommand {
20
20
 
21
21
  async execute(_args: string[], _ctx: HookCommandContext): Promise<string> {
22
22
  const headTag = await getHeadTag(this.api);
23
- return renderPromptTemplate(ciGreenRequestTemplate, { headTag });
23
+ return prompt.render(ciGreenRequestTemplate, { headTag });
24
24
  }
25
25
  }
@@ -11,7 +11,7 @@
11
11
  * rich context for the orchestrating agent to distribute work across
12
12
  * multiple reviewer agents based on diff weight and locality.
13
13
  */
14
- import { renderPromptTemplate } from "../../../../config/prompt-templates";
14
+ import { prompt } from "@oh-my-pi/pi-utils";
15
15
  import type { CustomCommand, CustomCommandAPI } from "../../../../extensibility/custom-commands/types";
16
16
  import type { HookCommandContext } from "../../../../extensibility/hooks/types";
17
17
  import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
@@ -197,7 +197,7 @@ const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than t
197
197
  /**
198
198
  * Build the full review prompt with diff stats and distribution guidance.
199
199
  */
200
- function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): string {
200
+ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, additionalInstructions?: string): string {
201
201
  const agentCount = getRecommendedAgentCount(stats);
202
202
  const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
203
203
  const totalLines = stats.totalAdded + stats.totalRemoved;
@@ -209,7 +209,7 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
209
209
  hunksPreview: skipDiff ? getDiffPreview(f.hunks, linesPerFile) : "",
210
210
  }));
211
211
 
212
- return renderPromptTemplate(reviewRequestTemplate, {
212
+ return prompt.render(reviewRequestTemplate, {
213
213
  mode,
214
214
  files: filesWithExt,
215
215
  excluded: stats.excluded,
@@ -221,6 +221,7 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string): str
221
221
  skipDiff,
222
222
  rawDiff: rawDiff.trim(),
223
223
  linesPerFile,
224
+ additionalInstructions,
224
225
  });
225
226
  }
226
227
 
@@ -230,17 +231,30 @@ export class ReviewCommand implements CustomCommand {
230
231
 
231
232
  constructor(private api: CustomCommandAPI) {}
232
233
 
233
- async execute(_args: string[], ctx: HookCommandContext): Promise<string | undefined> {
234
+ async execute(args: string[], ctx: HookCommandContext): Promise<string | undefined> {
234
235
  if (!ctx.hasUI) {
235
- return "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
236
+ const base = "Use the Task tool to run the 'reviewer' agent to review recent code changes.";
237
+ return args.length > 0 ? `${base} Focus: ${args.join(" ")}` : base;
236
238
  }
237
239
 
238
- const mode = await ctx.ui.select("Review Mode", [
239
- "1. Review against a base branch (PR Style)",
240
- "2. Review uncommitted changes",
241
- "3. Review a specific commit",
242
- "4. Custom review instructions",
243
- ]);
240
+ // Inline args act as additional instructions appended to the generated prompt.
241
+ // When present, skip option 4 (editor) — the args already provide the instructions.
242
+ const extraInstructions = args.length > 0 ? args.join(" ") : undefined;
243
+
244
+ const menuItems = extraInstructions
245
+ ? [
246
+ "1. Review against a base branch (PR Style)",
247
+ "2. Review uncommitted changes",
248
+ "3. Review a specific commit",
249
+ ]
250
+ : [
251
+ "1. Review against a base branch (PR Style)",
252
+ "2. Review uncommitted changes",
253
+ "3. Review a specific commit",
254
+ "4. Custom review instructions",
255
+ ];
256
+
257
+ const mode = await ctx.ui.select("Review Mode", menuItems);
244
258
 
245
259
  if (!mode) return undefined;
246
260
 
@@ -282,6 +296,7 @@ export class ReviewCommand implements CustomCommand {
282
296
  `Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
283
297
  stats,
284
298
  diffText,
299
+ extraInstructions,
285
300
  );
286
301
  }
287
302
 
@@ -318,7 +333,12 @@ export class ReviewCommand implements CustomCommand {
318
333
  return undefined;
319
334
  }
320
335
 
321
- return buildReviewPrompt("Reviewing uncommitted changes (staged + unstaged)", stats, combinedDiff);
336
+ return buildReviewPrompt(
337
+ "Reviewing uncommitted changes (staged + unstaged)",
338
+ stats,
339
+ combinedDiff,
340
+ extraInstructions,
341
+ );
322
342
  }
323
343
 
324
344
  case 3: {
@@ -354,7 +374,7 @@ export class ReviewCommand implements CustomCommand {
354
374
  return undefined;
355
375
  }
356
376
 
357
- return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText);
377
+ return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText, extraInstructions);
358
378
  }
359
379
 
360
380
  case 4: {
@@ -374,11 +394,12 @@ export class ReviewCommand implements CustomCommand {
374
394
  if (reviewDiff) {
375
395
  const stats = parseDiff(reviewDiff);
376
396
  // Even if all files filtered, include the custom instructions
377
- return `${buildReviewPrompt(
397
+ return buildReviewPrompt(
378
398
  `Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
379
399
  stats,
380
400
  reviewDiff,
381
- )}\n\n### Additional Instructions\n\n${instructions}`;
401
+ instructions,
402
+ );
382
403
  }
383
404
 
384
405
  // No diff available, just pass instructions
@@ -6,7 +6,6 @@
6
6
  */
7
7
  import * as fs from "node:fs";
8
8
  import * as path from "node:path";
9
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
10
9
  import { getAgentDir, getProjectDir, isEnoent, logger } from "@oh-my-pi/pi-utils";
11
10
  import * as typebox from "@sinclair/typebox";
12
11
  import { getConfigDirs } from "../../config";
@@ -184,7 +183,7 @@ export async function loadCustomCommands(options: LoadCustomCommandsOptions = {}
184
183
  exec: (command: string, args: string[], execOptions) =>
185
184
  execCommand(command, args, execOptions?.cwd ?? cwd, execOptions),
186
185
  typebox,
187
- pi: piCodingAgent,
186
+ pi: await import("@oh-my-pi/pi-coding-agent"),
188
187
  };
189
188
 
190
189
  // 1. Load bundled commands first (lowest priority - can be overridden)
@@ -5,7 +5,7 @@
5
5
  * to avoid import resolution issues with custom tools loaded from user directories.
6
6
  */
7
7
  import * as path from "node:path";
8
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
8
+ import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
9
9
  import { logger } from "@oh-my-pi/pi-utils";
10
10
  import * as typebox from "@sinclair/typebox";
11
11
  import { toolCapability } from "../../capability/tool";
@@ -14,7 +14,6 @@ import type { ExecOptions } from "../../exec/exec";
14
14
  import { execCommand } from "../../exec/exec";
15
15
  import type { HookUIContext } from "../../extensibility/hooks/types";
16
16
  import { getAllPluginToolPaths } from "../../extensibility/plugins/loader";
17
- import type { PendingActionStore } from "../../tools/pending-action";
18
17
  import { createNoOpUIContext, resolvePath } from "../utils";
19
18
  import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError } from "./types";
20
19
 
@@ -85,7 +84,17 @@ export class CustomToolLoader {
85
84
  #sharedApi: CustomToolAPI;
86
85
  #seenNames: Set<string>;
87
86
 
88
- constructor(cwd: string, builtInToolNames: string[], pendingActionStore?: PendingActionStore) {
87
+ constructor(
88
+ pi: typeof import("@oh-my-pi/pi-coding-agent"),
89
+ cwd: string,
90
+ builtInToolNames: string[],
91
+ pushPendingAction?: (action: {
92
+ label: string;
93
+ sourceToolName: string;
94
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
95
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
96
+ }) => void,
97
+ ) {
89
98
  this.#sharedApi = {
90
99
  cwd,
91
100
  exec: (command: string, args: string[], options?: ExecOptions) =>
@@ -94,17 +103,16 @@ export class CustomToolLoader {
94
103
  hasUI: false,
95
104
  logger,
96
105
  typebox,
97
- pi: piCodingAgent,
106
+ pi,
98
107
  pushPendingAction: action => {
99
- if (!pendingActionStore) {
108
+ if (!pushPendingAction) {
100
109
  throw new Error("Pending action store unavailable for custom tools in this runtime.");
101
110
  }
102
- pendingActionStore.push({
111
+ pushPendingAction({
103
112
  label: action.label,
104
113
  sourceToolName: action.sourceToolName ?? "custom_tool",
105
114
  apply: action.apply,
106
115
  reject: action.reject,
107
- details: action.details,
108
116
  });
109
117
  },
110
118
  };
@@ -155,9 +163,19 @@ export async function loadCustomTools(
155
163
  pathsWithSources: ToolPathWithSource[],
156
164
  cwd: string,
157
165
  builtInToolNames: string[],
158
- pendingActionStore?: PendingActionStore,
166
+ pushPendingAction?: (action: {
167
+ label: string;
168
+ sourceToolName: string;
169
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
170
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
171
+ }) => void,
159
172
  ) {
160
- const loader = new CustomToolLoader(cwd, builtInToolNames, pendingActionStore);
173
+ const loader = new CustomToolLoader(
174
+ await import("@oh-my-pi/pi-coding-agent"),
175
+ cwd,
176
+ builtInToolNames,
177
+ pushPendingAction,
178
+ );
161
179
  await loader.load(pathsWithSources);
162
180
  return {
163
181
  tools: loader.tools,
@@ -182,7 +200,12 @@ export async function discoverAndLoadCustomTools(
182
200
  configuredPaths: string[],
183
201
  cwd: string,
184
202
  builtInToolNames: string[],
185
- pendingActionStore?: PendingActionStore,
203
+ pushPendingAction?: (action: {
204
+ label: string;
205
+ sourceToolName: string;
206
+ apply(reason: string): Promise<AgentToolResult<unknown>>;
207
+ reject?(reason: string): Promise<AgentToolResult<unknown> | undefined>;
208
+ }) => void,
186
209
  ) {
187
210
  const allPathsWithSources: ToolPathWithSource[] = [];
188
211
  const seen = new Set<string>();
@@ -216,5 +239,5 @@ export async function discoverAndLoadCustomTools(
216
239
  addPath(resolvePath(configPath, cwd), { provider: "config", providerName: "Config", level: "project" });
217
240
  }
218
241
 
219
- return loadCustomTools(allPathsWithSources, cwd, builtInToolNames, pendingActionStore);
242
+ return loadCustomTools(allPathsWithSources, cwd, builtInToolNames, pushPendingAction);
220
243
  }
@@ -6,7 +6,6 @@ import * as fs from "node:fs/promises";
6
6
  import * as path from "node:path";
7
7
  import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
8
  import type { ImageContent, Model, TextContent } from "@oh-my-pi/pi-ai";
9
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
10
9
  import type { KeyId } from "@oh-my-pi/pi-tui";
11
10
  import { hasFsCode, isEacces, isEnoent, logger } from "@oh-my-pi/pi-utils";
12
11
  import type { TSchema } from "@sinclair/typebox";
@@ -102,7 +101,6 @@ export class ExtensionRuntime implements IExtensionRuntime {
102
101
  class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
103
102
  readonly logger = logger;
104
103
  readonly typebox = TypeBox;
105
- readonly pi = piCodingAgent;
106
104
  readonly flagValues = new Map<string, boolean | string>();
107
105
  readonly pendingProviderRegistrations: Array<{
108
106
  name: string;
@@ -111,6 +109,7 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
111
109
  }> = [];
112
110
 
113
111
  constructor(
112
+ public readonly pi: typeof import("@oh-my-pi/pi-coding-agent"),
114
113
  private readonly extension: Extension,
115
114
  private readonly runtime: IExtensionRuntime,
116
115
  private readonly cwd: string,
@@ -265,7 +264,13 @@ async function loadExtension(
265
264
  }
266
265
 
267
266
  const extension = createExtension(extensionPath, resolvedPath);
268
- const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
267
+ const api = new ConcreteExtensionAPI(
268
+ await import("@oh-my-pi/pi-coding-agent"),
269
+ extension,
270
+ runtime,
271
+ cwd,
272
+ eventBus,
273
+ );
269
274
  await factory(api);
270
275
 
271
276
  return { extension, error: null };
@@ -286,7 +291,7 @@ export async function loadExtensionFromFactory(
286
291
  name = "<inline>",
287
292
  ): Promise<Extension> {
288
293
  const extension = createExtension(name, name);
289
- const api = new ConcreteExtensionAPI(extension, runtime, cwd, eventBus);
294
+ const api = new ConcreteExtensionAPI(await import("@oh-my-pi/pi-coding-agent"), extension, runtime, cwd, eventBus);
290
295
  await factory(api);
291
296
  return extension;
292
297
  }
@@ -262,6 +262,10 @@ export class ExtensionRunner {
262
262
  return allFlags;
263
263
  }
264
264
 
265
+ getFlagValues(): Map<string, boolean | string> {
266
+ return new Map(this.runtime.flagValues);
267
+ }
268
+
265
269
  setFlagValue(name: string, value: boolean | string): void {
266
270
  this.runtime.flagValues.set(name, value);
267
271
  }
@@ -695,7 +699,26 @@ export class ExtensionRunner {
695
699
 
696
700
  async emitContext(messages: AgentMessage[]): Promise<AgentMessage[]> {
697
701
  const ctx = this.createContext();
698
- let currentMessages = structuredClone(messages);
702
+
703
+ // Check if any extensions actually have context handlers before cloning
704
+ let hasContextHandlers = false;
705
+ for (const ext of this.extensions) {
706
+ if (ext.handlers.get("context")?.length) {
707
+ hasContextHandlers = true;
708
+ break;
709
+ }
710
+ }
711
+ if (!hasContextHandlers) return messages;
712
+
713
+ let currentMessages: AgentMessage[];
714
+ try {
715
+ currentMessages = structuredClone(messages);
716
+ } catch {
717
+ // Messages may contain non-cloneable objects (e.g. in ToolResultMessage.details
718
+ // or ProviderPayload). Fall back to a shallow array clone — extensions should
719
+ // return new message arrays rather than mutating in place.
720
+ currentMessages = [...messages];
721
+ }
699
722
 
700
723
  for (const ext of this.extensions) {
701
724
  const handlers = ext.handlers.get("context");
@@ -28,11 +28,11 @@ import type { Static, TSchema } from "@sinclair/typebox";
28
28
  import type { Rule } from "../../capability/rule";
29
29
  import type { KeybindingsManager } from "../../config/keybindings";
30
30
  import type { ModelRegistry } from "../../config/model-registry";
31
+ import type { EditToolDetails } from "../../edit";
31
32
  import type { BashResult } from "../../exec/bash-executor";
32
33
  import type { ExecOptions, ExecResult } from "../../exec/exec";
33
34
  import type { PythonResult } from "../../ipy/executor";
34
35
  import type { Theme } from "../../modes/theme/theme";
35
- import type { EditToolDetails } from "../../patch";
36
36
  import type { CompactionPreparation, CompactionResult } from "../../session/compaction";
37
37
  import type { CustomMessage } from "../../session/messages";
38
38
  import type {
@@ -2,7 +2,6 @@
2
2
  * Hook loader - loads TypeScript hook modules using native Bun import.
3
3
  */
4
4
  import * as path from "node:path";
5
- import * as piCodingAgent from "@oh-my-pi/pi-coding-agent";
6
5
  import { logger } from "@oh-my-pi/pi-utils";
7
6
  import * as typebox from "@sinclair/typebox";
8
7
  import { hookCapability } from "../../capability/hook";
@@ -87,16 +86,16 @@ export interface LoadHooksResult {
87
86
  * Create a HookAPI instance that collects handlers, renderers, and commands.
88
87
  * Returns the API, maps, and functions to set handlers later.
89
88
  */
90
- function createHookAPI(
89
+ async function createHookAPI(
91
90
  handlers: Map<string, HandlerFn[]>,
92
91
  cwd: string,
93
- ): {
92
+ ): Promise<{
94
93
  api: HookAPI;
95
94
  messageRenderers: Map<string, HookMessageRenderer>;
96
95
  commands: Map<string, RegisteredCommand>;
97
96
  setSendMessageHandler: (handler: SendMessageHandler) => void;
98
97
  setAppendEntryHandler: (handler: AppendEntryHandler) => void;
99
- } {
98
+ }> {
100
99
  let sendMessageHandler: SendMessageHandler | null = null;
101
100
  let appendEntryHandler: AppendEntryHandler | null = null;
102
101
  const messageRenderers = new Map<string, HookMessageRenderer>();
@@ -137,7 +136,7 @@ function createHookAPI(
137
136
  },
138
137
  logger,
139
138
  typebox,
140
- pi: piCodingAgent,
139
+ pi: await import("@oh-my-pi/pi-coding-agent"),
141
140
  } as HookAPI;
142
141
 
143
142
  return {
@@ -170,7 +169,7 @@ async function loadHook(hookPath: string, cwd: string): Promise<{ hook: LoadedHo
170
169
 
171
170
  // Create handlers map and API
172
171
  const handlers = new Map<string, HandlerFn[]>();
173
- const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = createHookAPI(
172
+ const { api, messageRenderers, commands, setSendMessageHandler, setAppendEntryHandler } = await createHookAPI(
174
173
  handlers,
175
174
  cwd,
176
175
  );
@@ -9,9 +9,9 @@ import type { ImageContent, Message, Model, TextContent, ToolResultMessage } fro
9
9
  import type { Component, TUI } from "@oh-my-pi/pi-tui";
10
10
  import type { Rule } from "../../capability/rule";
11
11
  import type { ModelRegistry } from "../../config/model-registry";
12
+ import type { EditToolDetails } from "../../edit";
12
13
  import type { ExecOptions, ExecResult } from "../../exec/exec";
13
14
  import type { Theme } from "../../modes/theme/theme";
14
- import type { EditToolDetails } from "../../patch";
15
15
  import type { CompactionPreparation, CompactionResult } from "../../session/compaction";
16
16
  import type { HookMessage } from "../../session/messages";
17
17
  import type {
@@ -1,3 +1,4 @@
1
+ import { $which } from "@oh-my-pi/pi-utils";
1
2
  import { theme } from "../../modes/theme/theme";
2
3
  import type { DoctorCheck } from "./types";
3
4
 
@@ -12,7 +13,7 @@ export async function runDoctorChecks(): Promise<DoctorCheck[]> {
12
13
  ];
13
14
 
14
15
  for (const tool of tools) {
15
- const path = Bun.which(tool.name);
16
+ const path = $which(tool.name);
16
17
  checks.push({
17
18
  name: tool.name,
18
19
  status: path ? "ok" : "warning",
@@ -1,10 +1,7 @@
1
1
  import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
2
+ import { parseFrontmatter, prompt } from "@oh-my-pi/pi-utils";
2
3
  import { slashCommandCapability } from "../capability/slash-command";
3
- import {
4
- appendInlineArgsFallback,
5
- renderPromptTemplate,
6
- templateUsesInlineArgPlaceholders,
7
- } from "../config/prompt-templates";
4
+ import { appendInlineArgsFallback, templateUsesInlineArgPlaceholders } from "../config/prompt-templates";
8
5
  import type { SlashCommand } from "../discovery";
9
6
  import { loadCapability } from "../discovery";
10
7
  import {
@@ -14,7 +11,6 @@ import {
14
11
  } from "../slash-commands/builtin-registry";
15
12
  import { EMBEDDED_COMMAND_TEMPLATES } from "../task/commands";
16
13
  import { parseCommandArgs, substituteArgs } from "../utils/command-args";
17
- import { parseFrontmatter } from "../utils/frontmatter";
18
14
 
19
15
  export type SlashCommandSource = "extension" | "prompt" | "skill";
20
16
 
@@ -223,7 +219,7 @@ export function expandSlashCommand(text: string, fileCommands: FileSlashCommand[
223
219
  const argsText = args.join(" ");
224
220
  const usesInlineArgPlaceholders = templateUsesInlineArgPlaceholders(fileCommand.content);
225
221
  const substituted = substituteArgs(fileCommand.content, args);
226
- const rendered = renderPromptTemplate(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
222
+ const rendered = prompt.render(substituted, { args, ARGUMENTS: argsText, arguments: argsText });
227
223
  return appendInlineArgsFallback(rendered, argsText, usesInlineArgPlaceholders);
228
224
  }
229
225
 
package/src/index.ts CHANGED
@@ -16,6 +16,7 @@ export type * from "./config/prompt-templates";
16
16
  export * from "./config/prompt-templates";
17
17
  export type { RetrySettings, SkillsSettings } from "./config/settings";
18
18
  export { Settings, settings } from "./config/settings";
19
+ export * from "./edit/modes/hashline";
19
20
  // Custom commands
20
21
  export type * from "./extensibility/custom-commands/types";
21
22
  export type * from "./extensibility/custom-tools";
@@ -37,7 +38,6 @@ export * from "./modes";
37
38
  export * from "./modes/components";
38
39
  // Theme utilities for custom tools
39
40
  export * from "./modes/theme/theme";
40
- export * from "./patch/hashline";
41
41
  // SDK for programmatic usage
42
42
  export * from "./sdk";
43
43
  export * from "./session/agent-session";
@@ -46,6 +46,7 @@ export * from "./session/auth-storage";
46
46
  // Compaction
47
47
  export * from "./session/compaction";
48
48
  export * from "./session/messages";
49
+ export * from "./session/session-dump-format";
49
50
  export * from "./session/session-manager";
50
51
  export * from "./task/executor";
51
52
  export type * from "./task/types";