@oh-my-pi/pi-coding-agent 14.9.9 → 15.0.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 (230) hide show
  1. package/CHANGELOG.md +123 -0
  2. package/examples/extensions/plan-mode.ts +0 -1
  3. package/package.json +9 -9
  4. package/scripts/build-binary.ts +5 -0
  5. package/scripts/format-prompts.ts +1 -1
  6. package/src/autoresearch/helpers.ts +17 -0
  7. package/src/autoresearch/tools/log-experiment.ts +9 -17
  8. package/src/autoresearch/tools/run-experiment.ts +2 -17
  9. package/src/capability/skill.ts +7 -0
  10. package/src/cli/args.ts +2 -2
  11. package/src/cli/list-models.ts +1 -1
  12. package/src/cli/shell-cli.ts +3 -13
  13. package/src/cli/update-cli.ts +1 -1
  14. package/src/cli.ts +11 -29
  15. package/src/commands/acp.ts +24 -0
  16. package/src/commands/launch.ts +6 -4
  17. package/src/commit/agentic/prompts/system.md +1 -1
  18. package/src/commit/agentic/tools/propose-changelog.ts +8 -1
  19. package/src/commit/analysis/conventional.ts +8 -66
  20. package/src/commit/map-reduce/reduce-phase.ts +6 -65
  21. package/src/commit/pipeline.ts +2 -2
  22. package/src/commit/shared-llm.ts +89 -0
  23. package/src/config/config-file.ts +210 -0
  24. package/src/config/model-equivalence.ts +8 -11
  25. package/src/config/model-registry.ts +13 -2
  26. package/src/config/model-resolver.ts +31 -4
  27. package/src/config/settings-schema.ts +102 -1
  28. package/src/config/settings.ts +1 -1
  29. package/src/config.ts +3 -219
  30. package/src/edit/index.ts +22 -1
  31. package/src/edit/modes/patch.ts +10 -0
  32. package/src/edit/modes/replace.ts +3 -0
  33. package/src/edit/renderer.ts +17 -1
  34. package/src/eval/js/context-manager.ts +1 -1
  35. package/src/eval/js/executor.ts +3 -0
  36. package/src/eval/js/shared/rewrite-imports.ts +122 -50
  37. package/src/eval/js/shared/runtime.ts +31 -4
  38. package/src/eval/js/tool-bridge.ts +43 -21
  39. package/src/eval/py/executor.ts +5 -0
  40. package/src/exa/factory.ts +2 -2
  41. package/src/exa/mcp-client.ts +74 -1
  42. package/src/exec/bash-executor.ts +5 -1
  43. package/src/export/html/template.generated.ts +1 -1
  44. package/src/export/html/template.js +0 -11
  45. package/src/extensibility/extensions/runner.ts +55 -2
  46. package/src/extensibility/extensions/types.ts +98 -221
  47. package/src/extensibility/hooks/types.ts +89 -314
  48. package/src/extensibility/shared-events.ts +343 -0
  49. package/src/extensibility/skills.ts +42 -1
  50. package/src/goals/index.ts +3 -0
  51. package/src/goals/runtime.ts +500 -0
  52. package/src/goals/state.ts +37 -0
  53. package/src/goals/tools/goal-tool.ts +237 -0
  54. package/src/hashline/anchors.ts +2 -2
  55. package/src/hindsight/mental-models.ts +1 -1
  56. package/src/internal-urls/agent-protocol.ts +1 -20
  57. package/src/internal-urls/artifact-protocol.ts +1 -19
  58. package/src/internal-urls/docs-index.generated.ts +9 -10
  59. package/src/internal-urls/index.ts +1 -0
  60. package/src/internal-urls/issue-pr-protocol.ts +577 -0
  61. package/src/internal-urls/registry-helpers.ts +25 -0
  62. package/src/internal-urls/router.ts +6 -3
  63. package/src/internal-urls/types.ts +22 -1
  64. package/src/main.ts +24 -11
  65. package/src/mcp/oauth-flow.ts +20 -0
  66. package/src/modes/acp/acp-agent.ts +412 -71
  67. package/src/modes/acp/acp-client-bridge.ts +152 -0
  68. package/src/modes/acp/acp-event-mapper.ts +180 -15
  69. package/src/modes/acp/terminal-auth.ts +37 -0
  70. package/src/modes/components/assistant-message.ts +14 -8
  71. package/src/modes/components/bash-execution.ts +24 -63
  72. package/src/modes/components/custom-message.ts +14 -40
  73. package/src/modes/components/eval-execution.ts +27 -57
  74. package/src/modes/components/execution-shared.ts +102 -0
  75. package/src/modes/components/hook-message.ts +17 -49
  76. package/src/modes/components/mcp-add-wizard.ts +26 -5
  77. package/src/modes/components/message-frame.ts +88 -0
  78. package/src/modes/components/model-selector.ts +1 -1
  79. package/src/modes/components/read-tool-group.ts +29 -1
  80. package/src/modes/components/session-observer-overlay.ts +6 -2
  81. package/src/modes/components/session-selector.ts +1 -1
  82. package/src/modes/components/status-line/segments.ts +55 -4
  83. package/src/modes/components/status-line/types.ts +4 -0
  84. package/src/modes/components/status-line.ts +28 -10
  85. package/src/modes/components/tool-execution.ts +7 -8
  86. package/src/modes/controllers/command-controller-shared.ts +108 -0
  87. package/src/modes/controllers/command-controller.ts +27 -10
  88. package/src/modes/controllers/event-controller.ts +60 -18
  89. package/src/modes/controllers/extension-ui-controller.ts +8 -2
  90. package/src/modes/controllers/input-controller.ts +85 -39
  91. package/src/modes/controllers/mcp-command-controller.ts +56 -61
  92. package/src/modes/controllers/ssh-command-controller.ts +18 -57
  93. package/src/modes/interactive-mode.ts +675 -39
  94. package/src/modes/print-mode.ts +16 -86
  95. package/src/modes/rpc/rpc-mode.ts +30 -88
  96. package/src/modes/runtime-init.ts +115 -0
  97. package/src/modes/theme/defaults/dark-poimandres.json +2 -0
  98. package/src/modes/theme/defaults/light-poimandres.json +2 -0
  99. package/src/modes/theme/theme.ts +18 -6
  100. package/src/modes/types.ts +20 -5
  101. package/src/modes/utils/context-usage.ts +13 -13
  102. package/src/modes/utils/ui-helpers.ts +25 -6
  103. package/src/plan-mode/approved-plan.ts +35 -1
  104. package/src/prompts/agents/designer.md +5 -5
  105. package/src/prompts/agents/explore.md +7 -7
  106. package/src/prompts/agents/init.md +9 -9
  107. package/src/prompts/agents/librarian.md +14 -14
  108. package/src/prompts/agents/plan.md +4 -4
  109. package/src/prompts/agents/reviewer.md +5 -5
  110. package/src/prompts/agents/task.md +10 -10
  111. package/src/prompts/commands/orchestrate.md +2 -2
  112. package/src/prompts/compaction/branch-summary.md +3 -3
  113. package/src/prompts/compaction/compaction-short-summary.md +7 -7
  114. package/src/prompts/compaction/compaction-summary-context.md +1 -1
  115. package/src/prompts/compaction/compaction-summary.md +5 -5
  116. package/src/prompts/compaction/compaction-turn-prefix.md +3 -3
  117. package/src/prompts/compaction/compaction-update-summary.md +11 -11
  118. package/src/prompts/goals/goal-budget-limit.md +16 -0
  119. package/src/prompts/goals/goal-continuation.md +28 -0
  120. package/src/prompts/goals/goal-mode-active.md +23 -0
  121. package/src/prompts/memories/consolidation.md +2 -2
  122. package/src/prompts/memories/read-path.md +1 -1
  123. package/src/prompts/memories/stage_one_input.md +1 -1
  124. package/src/prompts/memories/stage_one_system.md +5 -5
  125. package/src/prompts/review-request.md +4 -4
  126. package/src/prompts/system/agent-creation-architect.md +17 -17
  127. package/src/prompts/system/agent-creation-user.md +2 -2
  128. package/src/prompts/system/commit-message-system.md +2 -2
  129. package/src/prompts/system/custom-system-prompt.md +2 -2
  130. package/src/prompts/system/eager-todo.md +6 -6
  131. package/src/prompts/system/handoff-document.md +1 -1
  132. package/src/prompts/system/plan-mode-active.md +25 -24
  133. package/src/prompts/system/plan-mode-approved.md +4 -4
  134. package/src/prompts/system/plan-mode-compact-instructions.md +16 -0
  135. package/src/prompts/system/plan-mode-reference.md +2 -2
  136. package/src/prompts/system/plan-mode-subagent.md +8 -8
  137. package/src/prompts/system/plan-mode-tool-decision-reminder.md +3 -3
  138. package/src/prompts/system/project-prompt.md +4 -4
  139. package/src/prompts/system/subagent-system-prompt.md +7 -7
  140. package/src/prompts/system/subagent-yield-reminder.md +4 -4
  141. package/src/prompts/system/system-prompt.md +72 -71
  142. package/src/prompts/system/ttsr-interrupt.md +1 -1
  143. package/src/prompts/tools/apply-patch.md +1 -1
  144. package/src/prompts/tools/ast-edit.md +3 -3
  145. package/src/prompts/tools/ast-grep.md +3 -3
  146. package/src/prompts/tools/bash.md +6 -0
  147. package/src/prompts/tools/browser.md +3 -3
  148. package/src/prompts/tools/checkpoint.md +3 -3
  149. package/src/prompts/tools/find.md +3 -3
  150. package/src/prompts/tools/github.md +2 -5
  151. package/src/prompts/tools/goal.md +13 -0
  152. package/src/prompts/tools/hashline.md +104 -116
  153. package/src/prompts/tools/image-gen.md +3 -3
  154. package/src/prompts/tools/irc.md +1 -1
  155. package/src/prompts/tools/lsp.md +2 -2
  156. package/src/prompts/tools/patch.md +6 -6
  157. package/src/prompts/tools/read.md +8 -7
  158. package/src/prompts/tools/replace.md +5 -5
  159. package/src/prompts/tools/resolve.md +6 -5
  160. package/src/prompts/tools/retain.md +1 -1
  161. package/src/prompts/tools/rewind.md +2 -2
  162. package/src/prompts/tools/search.md +2 -2
  163. package/src/prompts/tools/ssh.md +2 -2
  164. package/src/prompts/tools/task.md +12 -6
  165. package/src/prompts/tools/web-search.md +2 -2
  166. package/src/prompts/tools/write.md +3 -3
  167. package/src/sdk.ts +81 -17
  168. package/src/session/agent-session.ts +656 -125
  169. package/src/session/blob-store.ts +36 -3
  170. package/src/session/client-bridge.ts +81 -0
  171. package/src/session/compaction/errors.ts +31 -0
  172. package/src/session/compaction/index.ts +1 -0
  173. package/src/session/messages.ts +67 -2
  174. package/src/session/session-manager.ts +131 -12
  175. package/src/session/session-storage.ts +33 -15
  176. package/src/session/streaming-output.ts +309 -13
  177. package/src/slash-commands/acp-builtins.ts +46 -0
  178. package/src/slash-commands/builtin-registry.ts +717 -116
  179. package/src/slash-commands/helpers/context-report.ts +39 -0
  180. package/src/slash-commands/helpers/format.ts +23 -0
  181. package/src/slash-commands/helpers/marketplace-manager.ts +25 -0
  182. package/src/slash-commands/helpers/mcp.ts +532 -0
  183. package/src/slash-commands/helpers/parse.ts +85 -0
  184. package/src/slash-commands/helpers/ssh.ts +193 -0
  185. package/src/slash-commands/helpers/todo.ts +279 -0
  186. package/src/slash-commands/helpers/usage-report.ts +91 -0
  187. package/src/slash-commands/types.ts +126 -0
  188. package/src/ssh/ssh-executor.ts +5 -0
  189. package/src/system-prompt.ts +4 -2
  190. package/src/task/executor.ts +27 -10
  191. package/src/task/index.ts +20 -1
  192. package/src/task/render.ts +27 -18
  193. package/src/task/types.ts +4 -0
  194. package/src/tools/ast-edit.ts +21 -120
  195. package/src/tools/ast-grep.ts +21 -119
  196. package/src/tools/bash-interactive.ts +9 -1
  197. package/src/tools/bash.ts +203 -6
  198. package/src/tools/browser/attach.ts +3 -3
  199. package/src/tools/browser/launch.ts +81 -18
  200. package/src/tools/browser/registry.ts +1 -5
  201. package/src/tools/browser/tab-supervisor.ts +51 -14
  202. package/src/tools/conflict-detect.ts +21 -10
  203. package/src/tools/eval.ts +3 -1
  204. package/src/tools/fetch.ts +15 -4
  205. package/src/tools/find.ts +39 -39
  206. package/src/tools/gh-renderer.ts +0 -12
  207. package/src/tools/gh.ts +689 -182
  208. package/src/tools/github-cache.ts +548 -0
  209. package/src/tools/index.ts +25 -11
  210. package/src/tools/inspect-image.ts +3 -10
  211. package/src/tools/output-meta.ts +176 -37
  212. package/src/tools/path-utils.ts +125 -2
  213. package/src/tools/read.ts +605 -239
  214. package/src/tools/render-utils.ts +92 -0
  215. package/src/tools/renderers.ts +2 -0
  216. package/src/tools/resolve.ts +72 -44
  217. package/src/tools/search.ts +120 -186
  218. package/src/tools/write.ts +67 -10
  219. package/src/tui/code-cell.ts +70 -2
  220. package/src/utils/file-mentions.ts +1 -1
  221. package/src/utils/image-loading.ts +7 -3
  222. package/src/utils/image-resize.ts +32 -43
  223. package/src/vim/parser.ts +0 -17
  224. package/src/vim/render.ts +1 -1
  225. package/src/vim/types.ts +1 -1
  226. package/src/web/search/providers/gemini.ts +35 -95
  227. package/src/prompts/tools/exit-plan-mode.md +0 -6
  228. package/src/tools/exit-plan-mode.ts +0 -97
  229. package/src/utils/fuzzy.ts +0 -108
  230. package/src/utils/image-convert.ts +0 -27
@@ -1,9 +1,10 @@
1
1
  import { parse as babelParse } from "@babel/parser";
2
2
 
3
- // Static ESM `import` declarations are not valid inside vm.runInContext (script-mode parsing).
4
- // We rewrite top-level imports to dynamic-import expressions in the user-supplied source so
5
- // pasted ESM runs verbatim. A real parser keeps imports embedded in string literals, template
6
- // literals, or comments intact.
3
+ // Static ESM `import` declarations are not valid inside vm.runInContext (script-mode parsing),
4
+ // and dynamic `import(...)` would otherwise resolve specifiers against the worker module's URL
5
+ // instead of the session cwd. We rewrite both forms so they route through the worker-injected
6
+ // `__omp_import__` helper, which resolves the specifier against the active session cwd. A real
7
+ // parser keeps imports embedded in string literals, template literals, or comments intact.
7
8
 
8
9
  type BabelImportDeclaration = {
9
10
  type: "ImportDeclaration";
@@ -25,26 +26,78 @@ type BabelLexicalDecl =
25
26
  | { type: "VariableDeclaration"; kind: "const" | "let" | "var"; start: number; end: number }
26
27
  | { type: "ClassDeclaration"; start: number; end: number; id: { start: number; end: number; name: string } | null };
27
28
 
28
- function buildDynamicImportCall(sourceLiteral: string, withClause: string | undefined): string {
29
+ type BabelExpressionStatement = {
30
+ type: "ExpressionStatement";
31
+ start: number;
32
+ end: number;
33
+ expression?: { type?: string };
34
+ };
35
+
36
+ type BabelProgramNode = BabelImportDeclaration | BabelLexicalDecl | BabelExpressionStatement | { type: string };
37
+
38
+ type BabelNode = { type: string; start: number; end: number; [key: string]: unknown };
39
+
40
+ function parseProgram(code: string): { program: { body: ReadonlyArray<BabelProgramNode> } } | null {
41
+ try {
42
+ return babelParse(code, {
43
+ sourceType: "module",
44
+ allowAwaitOutsideFunction: true,
45
+ allowReturnOutsideFunction: true,
46
+ allowImportExportEverywhere: true,
47
+ allowNewTargetOutsideFunction: true,
48
+ allowSuperOutsideMethod: true,
49
+ allowUndeclaredExports: true,
50
+ errorRecovery: true,
51
+ }) as unknown as { program: { body: ReadonlyArray<BabelProgramNode> } };
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ function buildOmpImportCall(sourceLiteral: string, optionsLiteral: string | undefined): string {
29
58
  // Route every static import through the worker-injected `__omp_import__` helper so the
30
59
  // specifier resolves against the session cwd (and `with`-attribute imports keep working).
31
- return withClause ? `__omp_import__(${sourceLiteral}, ${withClause})` : `__omp_import__(${sourceLiteral})`;
60
+ return optionsLiteral ? `__omp_import__(${sourceLiteral}, ${optionsLiteral})` : `__omp_import__(${sourceLiteral})`;
32
61
  }
33
62
 
34
- function buildWithClause(node: BabelImportDeclaration): string | undefined {
63
+ // Walks every node in `root`, depth-first, invoking `visit` on each one. Skips Babel's
64
+ // non-AST bookkeeping fields so we don't recurse into source locations or comment arrays.
65
+ function walkNodes(root: unknown, visit: (node: BabelNode) => void): void {
66
+ const stack: unknown[] = [root];
67
+ while (stack.length > 0) {
68
+ const current = stack.pop();
69
+ if (!current || typeof current !== "object") continue;
70
+ if (Array.isArray(current)) {
71
+ for (let i = current.length - 1; i >= 0; i--) stack.push(current[i]);
72
+ continue;
73
+ }
74
+ const node = current as Record<string, unknown>;
75
+ if (typeof node.type === "string") visit(node as unknown as BabelNode);
76
+ for (const key in node) {
77
+ if (key === "loc" || key === "extra" || key === "range") continue;
78
+ if (key === "leadingComments" || key === "trailingComments" || key === "innerComments") continue;
79
+ const value = node[key];
80
+ if (value && typeof value === "object") stack.push(value);
81
+ }
82
+ }
83
+ }
84
+
85
+ function buildOptionsLiteral(node: BabelImportDeclaration): string | undefined {
35
86
  const attrs = node.attributes;
36
87
  if (!attrs || attrs.length === 0) return undefined;
37
88
  const pairs = attrs.map(attr => {
38
89
  const key = attr.key.type === "Identifier" ? attr.key.name : JSON.stringify(attr.key.value);
39
90
  return `${key}: ${JSON.stringify(attr.value.value)}`;
40
91
  });
41
- return `{ ${pairs.join(", ")} }`;
92
+ // Native dynamic import takes options as `{ with: { ... } }`. `__omp_import__` forwards the
93
+ // options bag verbatim, so we wrap the attribute pairs accordingly.
94
+ return `{ with: { ${pairs.join(", ")} } }`;
42
95
  }
43
96
 
44
97
  function rewriteImportNode(node: BabelImportDeclaration): string {
45
98
  const sourceLiteral = JSON.stringify(node.source.value);
46
- const withClause = buildWithClause(node);
47
- const importCall = buildDynamicImportCall(sourceLiteral, withClause);
99
+ const optionsLiteral = buildOptionsLiteral(node);
100
+ const importCall = buildOmpImportCall(sourceLiteral, optionsLiteral);
48
101
 
49
102
  let defaultName: string | undefined;
50
103
  let namespaceName: string | undefined;
@@ -73,37 +126,43 @@ function rewriteImportNode(node: BabelImportDeclaration): string {
73
126
  return `await ${importCall};`;
74
127
  }
75
128
 
76
- export function rewriteStaticImports(code: string): string {
129
+ export function rewriteImports(code: string): string {
77
130
  if (!code.includes("import")) return code;
78
131
 
79
- let ast: { program: { body: ReadonlyArray<{ type: string }> } };
80
- try {
81
- ast = babelParse(code, {
82
- sourceType: "module",
83
- allowAwaitOutsideFunction: true,
84
- allowReturnOutsideFunction: true,
85
- allowImportExportEverywhere: true,
86
- allowNewTargetOutsideFunction: true,
87
- allowSuperOutsideMethod: true,
88
- allowUndeclaredExports: true,
89
- errorRecovery: true,
90
- }) as unknown as typeof ast;
91
- } catch {
132
+ const ast = parseProgram(code);
133
+ if (!ast) {
92
134
  // Parser bailed entirely — let the VM surface the real syntax error.
93
135
  return code;
94
136
  }
95
137
 
96
- const imports: BabelImportDeclaration[] = [];
138
+ type Edit = { start: number; end: number; text: string };
139
+ const edits: Edit[] = [];
140
+
141
+ // Top-level static `import` declarations become `await __omp_import__(...)` calls.
97
142
  for (const node of ast.program.body) {
98
- if (node.type === "ImportDeclaration") imports.push(node as unknown as BabelImportDeclaration);
143
+ if (node.type !== "ImportDeclaration") continue;
144
+ const decl = node as unknown as BabelImportDeclaration;
145
+ edits.push({ start: decl.start, end: decl.end, text: rewriteImportNode(decl) });
99
146
  }
100
- if (imports.length === 0) return code;
147
+
148
+ // Dynamic `import(...)` expressions (anywhere) get their callee swapped for `__omp_import__`
149
+ // so the specifier resolves against the session cwd instead of the worker module's URL.
150
+ walkNodes(ast, node => {
151
+ if (node.type !== "CallExpression") return;
152
+ const call = node as unknown as { callee?: { type?: string; start?: number; end?: number } };
153
+ const callee = call.callee;
154
+ if (!callee || callee.type !== "Import" || typeof callee.start !== "number" || typeof callee.end !== "number")
155
+ return;
156
+ edits.push({ start: callee.start, end: callee.end, text: "__omp_import__" });
157
+ });
158
+
159
+ if (edits.length === 0) return code;
101
160
 
102
161
  // Splice from the back so earlier offsets stay valid.
103
- imports.sort((a, b) => b.start - a.start);
162
+ edits.sort((a, b) => b.start - a.start);
104
163
  let result = code;
105
- for (const node of imports) {
106
- result = result.slice(0, node.start) + rewriteImportNode(node) + result.slice(node.end);
164
+ for (const edit of edits) {
165
+ result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
107
166
  }
108
167
  return result;
109
168
  }
@@ -121,22 +180,11 @@ export function rewriteStaticImports(code: string): string {
121
180
  * Nested declarations (inside functions, blocks, classes) are left alone \u2014 they're
122
181
  * scoped to their enclosing function/block regardless of `var` vs `let`/`const`.
123
182
  */
124
- export function demoteTopLevelLexicals(code: string): string {
183
+ function demoteTopLevelLexicals(code: string): string {
125
184
  if (!/\b(?:const|let|class)\b/.test(code)) return code;
126
185
 
127
- let ast: { program: { body: ReadonlyArray<{ type: string }> } };
128
- try {
129
- ast = babelParse(code, {
130
- sourceType: "module",
131
- allowAwaitOutsideFunction: true,
132
- allowReturnOutsideFunction: true,
133
- allowImportExportEverywhere: true,
134
- allowNewTargetOutsideFunction: true,
135
- allowSuperOutsideMethod: true,
136
- allowUndeclaredExports: true,
137
- errorRecovery: true,
138
- }) as unknown as typeof ast;
139
- } catch {
186
+ const ast = parseProgram(code);
187
+ if (!ast) {
140
188
  return code;
141
189
  }
142
190
 
@@ -172,6 +220,24 @@ export function demoteTopLevelLexicals(code: string): string {
172
220
  return result;
173
221
  }
174
222
 
223
+ function returnFinalExpression(code: string): { source: string; returned: boolean } {
224
+ const ast = parseProgram(code);
225
+ const body = ast?.program.body;
226
+ if (!body) return { source: code, returned: false };
227
+ let lastIndex = body.length - 1;
228
+ while (lastIndex >= 0 && body[lastIndex]?.type === "EmptyStatement") lastIndex--;
229
+ const last = lastIndex >= 0 ? body[lastIndex] : undefined;
230
+ if (last?.type !== "ExpressionStatement") return { source: code, returned: false };
231
+
232
+ const expression = last as BabelExpressionStatement;
233
+ const prefix = code.slice(0, expression.start);
234
+ const statement = code.slice(expression.start, expression.end);
235
+ const suffix = code.slice(expression.end);
236
+ const semicolonMatch = statement.match(/;\s*$/);
237
+ const trimmedStatement = semicolonMatch ? statement.slice(0, semicolonMatch.index) : statement;
238
+ return { source: `${prefix}__omp_set_final_expr__((${trimmedStatement}));${suffix}`, returned: true };
239
+ }
240
+
175
241
  /**
176
242
  * Strip TypeScript syntax (type annotations, `interface`, `as`, `satisfies`, generics in
177
243
  * call expressions, etc.) before the import/lexical rewriters parse the code. We use Bun's
@@ -182,7 +248,7 @@ export function demoteTopLevelLexicals(code: string): string {
182
248
  * common case avoids an extra transpile pass. We detect "looks like TS" with a cheap regex
183
249
  * before invoking the transpiler.
184
250
  */
185
- export function stripTypeScript(code: string): string {
251
+ function stripTypeScript(code: string): string {
186
252
  if (!LOOKS_LIKE_TS.test(code)) return code;
187
253
  try {
188
254
  return new Bun.Transpiler({ loader: "ts" }).transformSync(code);
@@ -198,14 +264,20 @@ export function stripTypeScript(code: string): string {
198
264
  const LOOKS_LIKE_TS =
199
265
  /(?:\binterface\s+\w|\btype\s+\w+\s*=|\b(?:as|satisfies)\s+(?:[A-Z]|\bconst\b)|:\s*(?:string|number|boolean|any|unknown|void|never|object|[A-Z]\w*)\b|<\s*[A-Z]\w*\s*[,>])/;
200
266
 
201
- export function wrapCode(code: string): { source: string; asyncWrapped: boolean } {
202
- const rewritten = demoteTopLevelLexicals(rewriteStaticImports(stripTypeScript(code)));
203
- const needsAsyncWrapper = /\bawait\b|\breturn\b/.test(rewritten);
267
+ export function wrapCode(code: string): { source: string; asyncWrapped: boolean; finalExpressionReturned: boolean } {
268
+ const stripped = stripTypeScript(code);
269
+ const finalExpression = returnFinalExpression(stripped);
270
+ const rewritten = {
271
+ source: demoteTopLevelLexicals(rewriteImports(finalExpression.source)),
272
+ returned: finalExpression.returned,
273
+ };
274
+ const needsAsyncWrapper = /\bawait\b|\breturn\b/.test(rewritten.source);
204
275
  if (!needsAsyncWrapper) {
205
- return { source: rewritten, asyncWrapped: false };
276
+ return { source: rewritten.source, asyncWrapped: false, finalExpressionReturned: rewritten.returned };
206
277
  }
207
278
  return {
208
- source: `(async () => {\n${rewritten}\n})()`,
279
+ source: `(async () => {\n${rewritten.source}\n})()`,
209
280
  asyncWrapped: true,
281
+ finalExpressionReturned: rewritten.returned,
210
282
  };
211
283
  }
@@ -4,6 +4,8 @@ import * as path from "node:path";
4
4
  import { pathToFileURL } from "node:url";
5
5
  import * as util from "node:util";
6
6
 
7
+ import { logger } from "@oh-my-pi/pi-utils";
8
+
7
9
  import { ToolError } from "../../../tools/tool-errors";
8
10
  import { createHelpers, type HelperBundle } from "./helpers";
9
11
  import { awaitMaybePromise, indirectEval } from "./indirect-eval";
@@ -48,6 +50,8 @@ export class JsRuntime {
48
50
  readonly sessionId: string;
49
51
  #env: Map<string, string>;
50
52
  #getHooks: () => RuntimeHooks | null;
53
+ #finalExpressionSet = false;
54
+ #finalExpressionValue: unknown;
51
55
 
52
56
  constructor(opts: RuntimeOptions) {
53
57
  this.#cwd = opts.initialCwd;
@@ -82,8 +86,20 @@ export class JsRuntime {
82
86
  }
83
87
 
84
88
  async run(code: string, filename?: string): Promise<unknown> {
89
+ this.#finalExpressionSet = false;
90
+ this.#finalExpressionValue = undefined;
85
91
  const wrapped = wrapCode(code);
86
92
  const value = indirectEval(wrapped.source, filename);
93
+ if (wrapped.finalExpressionReturned) {
94
+ const awaited = await awaitMaybePromise(value);
95
+ if (this.#finalExpressionSet) {
96
+ const finalValue = this.#finalExpressionValue;
97
+ this.#finalExpressionSet = false;
98
+ this.#finalExpressionValue = undefined;
99
+ return await awaitMaybePromise(finalValue);
100
+ }
101
+ return awaited;
102
+ }
87
103
  return await awaitMaybePromise(value);
88
104
  }
89
105
 
@@ -97,7 +113,14 @@ export class JsRuntime {
97
113
  hooks.onDisplay({ type: "image", data: record.data, mimeType: record.mimeType });
98
114
  return;
99
115
  }
100
- hooks.onDisplay({ type: "json", data: structuredClone(value) });
116
+ try {
117
+ hooks.onDisplay({ type: "json", data: structuredClone(value) });
118
+ } catch (err) {
119
+ logger.debug("js displayValue: value is not structured-cloneable, falling back to text", {
120
+ error: err instanceof Error ? err.message : String(err),
121
+ });
122
+ hooks.onText(`${Object.prototype.toString.call(value)}\n`);
123
+ }
101
124
  return;
102
125
  }
103
126
  hooks.onText(`${String(value)}\n`);
@@ -112,9 +135,9 @@ export class JsRuntime {
112
135
  if (!hooks) throw new ToolError("Tool calls are only valid inside an active run");
113
136
  return await hooks.callTool(name, args);
114
137
  },
115
- __omp_import__: async (source: string, attrs?: Record<string, string>) => {
138
+ __omp_import__: async (source: string, options?: ImportCallOptions) => {
116
139
  const target = resolveImportSpecifier(this.#cwd, source);
117
- return attrs ? await import(target, { with: attrs }) : await import(target);
140
+ return options !== undefined ? await import(target, options) : await import(target);
118
141
  },
119
142
  __omp_emit_status__: (op: string, data: Record<string, unknown> = {}) => {
120
143
  const event: JsStatusEvent = { op, ...data };
@@ -126,6 +149,10 @@ export class JsRuntime {
126
149
  this.#getHooks()?.onText(text.endsWith("\n") ? text : `${text}\n`);
127
150
  },
128
151
  __omp_display__: (value: unknown) => this.displayValue(value),
152
+ __omp_set_final_expr__: (value: unknown) => {
153
+ this.#finalExpressionSet = true;
154
+ this.#finalExpressionValue = value;
155
+ },
129
156
  webcrypto: crypto,
130
157
  // `process` is intentionally not overridden — user code gets the host worker's real
131
158
  // `process` object. Subsetting it caused segfaults in workers that share state with
@@ -152,7 +179,7 @@ function buildRequire(cwd: string): NodeJS.Require {
152
179
  }
153
180
 
154
181
  /**
155
- * Resolve an import specifier emitted by `rewriteStaticImports` against the active session
182
+ * Resolve an import specifier emitted by `rewriteImports` against the active session
156
183
  * cwd. Relative paths (`./`, `../`, `/`) and bare specifiers (`pkg`, `@scope/pkg`) both go
157
184
  * through `Bun.resolveSync` rooted at the cwd so user-pasted ESM behaves as if it lived in
158
185
  * the project — not next to the worker module. URL-like specifiers (`file://`, `data:`,
@@ -17,7 +17,17 @@ type ToolValue =
17
17
  text: string;
18
18
  details?: unknown;
19
19
  images?: Array<{ mimeType: string; data: string }>;
20
+ hasError?: boolean;
20
21
  };
22
+ function toolResultHasError(result: AgentToolResult): boolean {
23
+ if ((result as { isError?: unknown }).isError === true) {
24
+ return true;
25
+ }
26
+ if (!(result.details && typeof result.details === "object")) {
27
+ return false;
28
+ }
29
+ return (result.details as { isError?: unknown }).isError === true;
30
+ }
21
31
 
22
32
  function getTool(session: ToolSession, name: string): AgentTool {
23
33
  const tool = session.getToolByName?.(name);
@@ -38,7 +48,13 @@ function normalizeArgs(args: unknown): unknown {
38
48
  return record;
39
49
  }
40
50
 
41
- function summarizeToolResult(name: string, args: unknown, result: AgentToolResult, text: string): JsStatusEvent {
51
+ function summarizeToolResult(
52
+ name: string,
53
+ args: unknown,
54
+ result: AgentToolResult,
55
+ text: string,
56
+ hasError: boolean,
57
+ ): JsStatusEvent {
42
58
  const record = (args && typeof args === "object" ? (args as Record<string, unknown>) : {}) as Record<
43
59
  string,
44
60
  unknown
@@ -46,39 +62,41 @@ function summarizeToolResult(name: string, args: unknown, result: AgentToolResul
46
62
  const details = (
47
63
  result.details && typeof result.details === "object" ? (result.details as Record<string, unknown>) : {}
48
64
  ) as Record<string, unknown>;
65
+ const withError = (event: JsStatusEvent): JsStatusEvent =>
66
+ hasError ? { ...event, hasError: true, error: text.slice(0, 500) } : event;
49
67
 
50
68
  switch (name) {
51
69
  case "read":
52
- return { op: "read", path: record.path, chars: text.length, preview: text.slice(0, 500) };
70
+ return withError({ op: "read", path: record.path, chars: text.length, preview: text.slice(0, 500) });
53
71
  case "write":
54
- return {
72
+ return withError({
55
73
  op: "write",
56
74
  path: record.path,
57
75
  chars: typeof record.content === "string" ? record.content.length : 0,
58
- };
76
+ });
59
77
  case "grep":
60
- return {
78
+ return withError({
61
79
  op: "grep",
62
80
  pattern: record.pattern,
63
81
  path: record.path,
64
82
  count: details.matchCount ?? undefined,
65
- };
83
+ });
66
84
  case "find":
67
- return {
85
+ return withError({
68
86
  op: "find",
69
87
  pattern: record.pattern,
70
88
  count: details.fileCount ?? undefined,
71
89
  matches: Array.isArray(details.files) ? details.files.slice(0, 20) : undefined,
72
- };
90
+ });
73
91
  case "bash":
74
- return {
92
+ return withError({
75
93
  op: "run",
76
94
  cmd: record.command,
77
95
  code: typeof details.exitCode === "number" ? details.exitCode : undefined,
78
96
  output: text.slice(0, 500),
79
- };
97
+ });
80
98
  default:
81
- return { op: name, chars: text.length };
99
+ return withError({ op: name, chars: text.length });
82
100
  }
83
101
  }
84
102
 
@@ -97,21 +115,25 @@ export async function callSessionTool(name: string, args: unknown, options: Tool
97
115
  content.type === "image" && typeof content.mimeType === "string" && typeof content.data === "string",
98
116
  );
99
117
  const text = textBlocks.map(block => block.text).join("");
100
- options.emitStatus?.(summarizeToolResult(name, normalizedArgs, result, text));
101
- if (result.details === undefined && imageBlocks.length === 0) {
118
+ const hasError = toolResultHasError(result);
119
+ options.emitStatus?.(summarizeToolResult(name, normalizedArgs, result, text, hasError));
120
+ if (result.details === undefined && imageBlocks.length === 0 && !hasError) {
102
121
  return text;
103
122
  }
104
- return {
123
+ const value: Exclude<ToolValue, string> = {
105
124
  text,
106
125
  details: result.details,
107
- images:
108
- imageBlocks.length > 0
109
- ? imageBlocks.map(block => ({
110
- mimeType: block.mimeType,
111
- data: block.data,
112
- }))
113
- : undefined,
114
126
  };
127
+ if (imageBlocks.length > 0) {
128
+ value.images = imageBlocks.map(block => ({
129
+ mimeType: block.mimeType,
130
+ data: block.data,
131
+ }));
132
+ }
133
+ if (hasError) {
134
+ value.hasError = true;
135
+ }
136
+ return value;
115
137
  } catch (error) {
116
138
  options.emitStatus?.({
117
139
  op: name,
@@ -1,6 +1,8 @@
1
1
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
2
+ import { Settings } from "../../config/settings";
2
3
  import { OutputSink } from "../../session/streaming-output";
3
4
  import type { ToolSession } from "../../tools";
5
+ import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../../tools/output-meta";
4
6
  import type { JsStatusEvent } from "../js/shared/types";
5
7
  import type { KernelDisplayOutput } from "./display";
6
8
  import {
@@ -815,10 +817,13 @@ async function executeWithKernel(
815
817
  code: string,
816
818
  options: PythonExecutorOptions | undefined,
817
819
  ): Promise<PythonResult> {
820
+ const settings = await Settings.init();
818
821
  const sink = new OutputSink({
819
822
  onChunk: options?.onChunk,
820
823
  artifactPath: options?.artifactPath,
821
824
  artifactId: options?.artifactId,
825
+ headBytes: resolveOutputSinkHeadBytes(settings),
826
+ maxColumns: resolveOutputMaxColumns(settings),
822
827
  });
823
828
  const displayOutputs: KernelDisplayOutput[] = [];
824
829
  const deadlineMs = getExecutionDeadlineMs(options);
@@ -3,7 +3,7 @@
3
3
  */
4
4
  import type { TObject, TProperties } from "@sinclair/typebox";
5
5
  import type { CustomTool } from "../extensibility/custom-tools/types";
6
- import { callExaTool, findApiKey, formatSearchResults, isSearchResponse } from "./mcp-client";
6
+ import { callExaTool, findApiKey, formatGenericResponse, formatSearchResults, isSearchResponse } from "./mcp-client";
7
7
  import type { ExaRenderDetails } from "./types";
8
8
 
9
9
  /** Creates an Exa tool with standardized API key handling, error wrapping, and optional search response formatting. */
@@ -44,7 +44,7 @@ export function createExaTool(
44
44
  }
45
45
 
46
46
  return {
47
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
47
+ content: [{ type: "text" as const, text: formatGenericResponse(response) }],
48
48
  details: { raw: response, toolName: name },
49
49
  };
50
50
  } catch (error) {
@@ -174,6 +174,79 @@ export function formatSearchResults(data: ExaSearchResponse): string {
174
174
 
175
175
  return output.trim();
176
176
  }
177
+ /**
178
+ * Format a non-search MCP response as human-readable text.
179
+ * Handles objects, arrays, primitives, and common MCP response shapes.
180
+ */
181
+ export function formatGenericResponse(data: unknown): string {
182
+ if (data === null || data === undefined) return "No result.";
183
+ if (typeof data === "string") return data;
184
+ if (typeof data === "number" || typeof data === "boolean") return String(data);
185
+
186
+ if (Array.isArray(data)) {
187
+ if (data.length === 0) return "(empty)";
188
+ const parts: string[] = [];
189
+ for (let i = 0; i < data.length; i++) {
190
+ const item = data[i];
191
+ if (typeof item === "object" && item !== null) {
192
+ const record = item as Record<string, unknown>;
193
+ const title = (record.title ?? record.name ?? record.id ?? `Item ${i + 1}`) as string;
194
+ parts.push(`\n### ${title}`);
195
+ for (const [k, v] of Object.entries(record)) {
196
+ if (["title", "name", "id"].includes(k)) continue;
197
+ parts.push(`- **${k}:** ${formatValue(v)}`);
198
+ }
199
+ } else {
200
+ parts.push(`- ${formatValue(item)}`);
201
+ }
202
+ }
203
+ return parts.join("\n");
204
+ }
205
+
206
+ if (typeof data === "object") {
207
+ const record = data as Record<string, unknown>;
208
+ if (record.content && Array.isArray(record.content)) {
209
+ // MCP-style content array — extract text blocks
210
+ const texts = record.content
211
+ .filter(
212
+ (c: unknown): c is { type: string; text?: string } =>
213
+ typeof c === "object" && c !== null && (c as Record<string, unknown>)?.type === "text",
214
+ )
215
+ .map(c => c.text ?? "")
216
+ .filter(Boolean);
217
+ if (texts.length > 0) return texts.join("\n");
218
+ }
219
+
220
+ const lines: string[] = [];
221
+ for (const [k, v] of Object.entries(record)) {
222
+ if (k === "content") continue; // handled above
223
+ if (v === null || v === undefined) continue;
224
+ if (typeof v === "object") {
225
+ const formatted = formatGenericResponse(v);
226
+ if (formatted) lines.push(`- **${k}:**\n${indent(formatted, 2)}`);
227
+ } else {
228
+ lines.push(`- **${k}:** ${formatValue(v)}`);
229
+ }
230
+ }
231
+ return lines.join("\n") || "(empty)";
232
+ }
233
+
234
+ return String(data);
235
+ }
236
+
237
+ function formatValue(v: unknown): string {
238
+ if (v === null || v === undefined) return "—";
239
+ if (typeof v === "object") return JSON.stringify(v);
240
+ return String(v);
241
+ }
242
+
243
+ function indent(text: string, spaces: number): string {
244
+ const pad = " ".repeat(spaces);
245
+ return text
246
+ .split("\n")
247
+ .map(line => pad + line)
248
+ .join("\n");
249
+ }
177
250
 
178
251
  /** Check if result is a search response */
179
252
  export function isSearchResponse(data: unknown): data is ExaSearchResponse {
@@ -260,7 +333,7 @@ export class MCPWrappedTool implements CustomTool<TSchema, ExaRenderDetails> {
260
333
  }
261
334
 
262
335
  return {
263
- content: [{ type: "text" as const, text: JSON.stringify(response, null, 2) }],
336
+ content: [{ type: "text" as const, text: formatGenericResponse(response) }],
264
337
  details: { raw: response, toolName: this.config.name },
265
338
  };
266
339
  } catch (error) {
@@ -7,6 +7,7 @@ import * as fs from "node:fs/promises";
7
7
  import { executeShell, type MinimizerOptions, Shell } from "@oh-my-pi/pi-natives";
8
8
  import { Settings, type ShellMinimizerSettings } from "../config/settings";
9
9
  import { OutputSink } from "../session/streaming-output";
10
+ import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
10
11
  import { getOrCreateSnapshot } from "../utils/shell-snapshot";
11
12
  import { NON_INTERACTIVE_ENV } from "./non-interactive-env";
12
13
 
@@ -64,7 +65,8 @@ async function resolveShellCwd(cwd: string | undefined): Promise<string | undefi
64
65
  }
65
66
  }
66
67
 
67
- function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined {
68
+ /** Translate `ShellMinimizerSettings` into native `MinimizerOptions`, or `undefined` when disabled. */
69
+ export function buildMinimizerOptions(group: ShellMinimizerSettings): MinimizerOptions | undefined {
68
70
  if (!group.enabled) return undefined;
69
71
  return {
70
72
  enabled: true,
@@ -94,6 +96,8 @@ export async function executeBash(command: string, options?: BashExecutorOptions
94
96
  onChunk: options?.onChunk,
95
97
  artifactPath: options?.artifactPath,
96
98
  artifactId: options?.artifactId,
99
+ headBytes: resolveOutputSinkHeadBytes(settings),
100
+ maxColumns: resolveOutputMaxColumns(settings),
97
101
  // Throttle the streaming preview callback to avoid saturating the
98
102
  // event loop when commands produce massive output (e.g. seq 1 50M).
99
103
  chunkThrottleMs: options?.onChunk ? 50 : 0,