@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7

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 (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. package/src/vibe/__tests__/token-rate.test.ts +0 -96
@@ -1,115 +0,0 @@
1
- import { afterEach, describe, expect, it } from "bun:test";
2
- import {
3
- __resetWindowsConsoleProbeCache,
4
- consoleAttachedViaTTY,
5
- hostHasInheritableConsole,
6
- shouldDetachKernel,
7
- shouldHideKernelWindow,
8
- } from "../py/spawn-options";
9
-
10
- describe("shouldDetachKernel", () => {
11
- it("starts POSIX kernels in a new session", () => {
12
- expect(shouldDetachKernel("darwin")).toBe(true);
13
- expect(shouldDetachKernel("linux")).toBe(true);
14
- });
15
-
16
- it("leaves Windows console inheritance to windowsHide", () => {
17
- expect(shouldDetachKernel("win32")).toBe(false);
18
- });
19
- });
20
-
21
- /**
22
- * `shouldHideKernelWindow` decides whether the long-lived Python kernel
23
- * subprocess is spawned with `windowsHide: true`. On Windows, Bun maps that
24
- * option to `CREATE_NO_WINDOW`, which detaches the child from any inherited
25
- * console — breaking both (a) `LoadLibraryExW` for NumPy/pandas native
26
- * extensions and (b) SIGINT delivery via `GenerateConsoleCtrlEvent`. See
27
- * issue #1960. The tests below pin the three layered concerns the PR review
28
- * surfaced:
29
- *
30
- * 1. `shouldHideKernelWindow` — pure predicate over a single boolean.
31
- * 2. `consoleAttachedViaTTY` — the TTY-OR fallback used when the Win32 FFI
32
- * probe is unavailable; covers the partial-redirection cases.
33
- * 3. `hostHasInheritableConsole` — the integration boundary. Off-Windows it
34
- * short-circuits to the TTY fallback; on Windows it is expected to
35
- * consult `kernel32!GetConsoleWindow()` first, which is the authoritative
36
- * signal even for the all-stdio-redirected case.
37
- */
38
- describe("shouldHideKernelWindow", () => {
39
- it("inherits the host console on Windows when one is attached", () => {
40
- // Reporter's repro: omp launched in Windows Terminal, host has a
41
- // console, kernel must inherit so `import pandas` doesn't deadlock in
42
- // `_multiarray_umath` and SIGINT can recover the cell.
43
- expect(shouldHideKernelWindow({ platform: "win32", hostHasInheritableConsole: true })).toBe(false);
44
- });
45
-
46
- it("hides on Windows only when the host has no console at all (true service / daemon)", () => {
47
- // CREATE_NO_WINDOW here suppresses the console window Windows would
48
- // otherwise auto-allocate for the console-app Python kernel.
49
- expect(shouldHideKernelWindow({ platform: "win32", hostHasInheritableConsole: false })).toBe(true);
50
- });
51
-
52
- it("never sets windowsHide off-Windows (the option is a Win32-only flag)", () => {
53
- // On POSIX `windowsHide` is a no-op; the predicate must return false
54
- // everywhere off-Windows so the spawn site matches pre-fix behavior.
55
- expect(shouldHideKernelWindow({ platform: "linux", hostHasInheritableConsole: true })).toBe(false);
56
- expect(shouldHideKernelWindow({ platform: "linux", hostHasInheritableConsole: false })).toBe(false);
57
- expect(shouldHideKernelWindow({ platform: "darwin", hostHasInheritableConsole: true })).toBe(false);
58
- expect(shouldHideKernelWindow({ platform: "darwin", hostHasInheritableConsole: false })).toBe(false);
59
- });
60
- });
61
-
62
- describe("consoleAttachedViaTTY (FFI fallback heuristic)", () => {
63
- // The OR of three TTY signals correctly classifies the realistic shell
64
- // redirection scenarios that motivated widening the check beyond stdout
65
- // in the first review pass (PR #1961). The all-three-redirected case
66
- // (false here) is the gap that the Win32 FFI probe in
67
- // `hostHasInheritableConsole` is meant to close — this fallback is best-
68
- // effort.
69
-
70
- it("treats a fully interactive launch as console-attached", () => {
71
- expect(consoleAttachedViaTTY({ stdinIsTTY: true, stdoutIsTTY: true, stderrIsTTY: true })).toBe(true);
72
- });
73
-
74
- it("treats `omp -p '...' > out.txt` (stdout-only redirect) as console-attached", () => {
75
- // The reviewer's first-pass repro: stdout off the terminal, stdin
76
- // and stderr still attached. OR keeps the console.
77
- expect(consoleAttachedViaTTY({ stdinIsTTY: true, stdoutIsTTY: false, stderrIsTTY: true })).toBe(true);
78
- });
79
-
80
- it("treats stdin-only redirects (`< in.txt`) as console-attached", () => {
81
- expect(consoleAttachedViaTTY({ stdinIsTTY: false, stdoutIsTTY: true, stderrIsTTY: true })).toBe(true);
82
- });
83
-
84
- it("treats stderr-only redirects (`2> err.log`) as console-attached", () => {
85
- expect(consoleAttachedViaTTY({ stdinIsTTY: true, stdoutIsTTY: true, stderrIsTTY: false })).toBe(true);
86
- });
87
-
88
- it("returns false only when none of stdin/stdout/stderr is a TTY", () => {
89
- // This is the gap: a real Windows Terminal session with all three
90
- // streams redirected (`omp ... < in > out 2> err`) lands here.
91
- // `hostHasInheritableConsole` uses the Win32 FFI probe to recover
92
- // the right answer in that scenario; this helper is the fallback.
93
- expect(consoleAttachedViaTTY({ stdinIsTTY: false, stdoutIsTTY: false, stderrIsTTY: false })).toBe(false);
94
- });
95
- });
96
-
97
- describe("hostHasInheritableConsole", () => {
98
- afterEach(() => {
99
- __resetWindowsConsoleProbeCache();
100
- });
101
-
102
- if (process.platform !== "win32") {
103
- it("matches the TTY-OR fallback off-Windows", () => {
104
- // Off-Windows, `windowsHide` is a no-op anyway, but we still
105
- // expose `hostHasInheritableConsole` symmetrically. Confirm it
106
- // degrades to the same OR the call site would compute by hand.
107
- const tty = consoleAttachedViaTTY({
108
- stdinIsTTY: !!process.stdin.isTTY,
109
- stdoutIsTTY: !!process.stdout.isTTY,
110
- stderrIsTTY: !!process.stderr.isTTY,
111
- });
112
- expect(hostHasInheritableConsole()).toBe(tty);
113
- });
114
- }
115
- });
@@ -1,156 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import * as vm from "node:vm";
3
- import { JAVASCRIPT_PRELUDE_SOURCE } from "../js/shared/prelude";
4
-
5
- /**
6
- * The eval `agent()` helper grows a `handle` option that turns its bare
7
- * text result into a DAG node dict carrying the spawned agent's recoverable
8
- * `agent://<id>` handle, so a downstream `pipeline`/`parallel` stage can wire
9
- * the transcript by reference instead of re-inlining it. These lock the node
10
- * shape, backward compatibility of the default path, the schema interaction,
11
- * and the no-`details` fallback (the helper must never throw).
12
- *
13
- * The prelude source is executed verbatim in a throwaway VM context with only
14
- * the host bridge (`__omp_call_tool__`) stubbed — no worker, no kernel — so the
15
- * test runs against the real shipped helper, not a re-implementation.
16
- */
17
- function loadPrelude(callTool: (name: string, args: unknown) => Promise<unknown>): Record<string, unknown> {
18
- const sandbox: Record<string, unknown> = { __omp_call_tool__: callTool };
19
- vm.createContext(sandbox);
20
- vm.runInContext(JAVASCRIPT_PRELUDE_SOURCE, sandbox);
21
- return sandbox;
22
- }
23
-
24
- type AgentHelper = (prompt: string, opts?: Record<string, unknown>) => Promise<unknown>;
25
-
26
- describe("eval js agent() handle", () => {
27
- it("returns a DAG node carrying the agent:// handle when handle is set", async () => {
28
- let seenName: string | undefined;
29
- let seenArgs: Record<string, unknown> | undefined;
30
- const sandbox = loadPrelude(async (name, args) => {
31
- seenName = name;
32
- seenArgs = args as Record<string, unknown>;
33
- return { text: "hello world", details: { agent: "task", id: "abc123", model: "m", structured: false } };
34
- });
35
- const node = await (sandbox.agent as AgentHelper)("say hi", { handle: true });
36
- expect(seenName).toBe("__agent__");
37
- expect(seenArgs?.handle).toBe(true);
38
- expect(node).toEqual({
39
- text: "hello world",
40
- output: "hello world",
41
- handle: "agent://abc123",
42
- id: "abc123",
43
- agent: "task",
44
- });
45
- });
46
-
47
- it("returns bare text by default (backward compatible)", async () => {
48
- const sandbox = loadPrelude(async () => ({
49
- text: "hello world",
50
- details: { agent: "task", id: "abc123", structured: false },
51
- }));
52
- const out = await (sandbox.agent as AgentHelper)("say hi");
53
- expect(out).toBe("hello world");
54
- });
55
-
56
- it("keeps positional isolation controls stable while appending schemaMode", async () => {
57
- let seenArgs: Record<string, unknown> | undefined;
58
- const sandbox = loadPrelude(async (_name, args) => {
59
- seenArgs = args as Record<string, unknown>;
60
- return { text: '{"ok":true}', details: { agent: "task", id: "legacy", structured: false } };
61
- });
62
- const positionalAgent = sandbox.agent as (
63
- prompt: string,
64
- options?: unknown,
65
- ...rest: unknown[]
66
- ) => Promise<unknown>;
67
- const schema = { type: "object", properties: { ok: { type: "boolean" } } };
68
-
69
- await positionalAgent("scout", "reviewer", "p/model", "Legacy", schema, true, false, true, "strict");
70
-
71
- expect(seenArgs).toEqual({
72
- prompt: "scout",
73
- agent: "reviewer",
74
- model: "p/model",
75
- label: "Legacy",
76
- schema,
77
- isolated: true,
78
- apply: false,
79
- merge: true,
80
- schemaMode: "strict",
81
- handle: false,
82
- });
83
- });
84
-
85
- it("carries the parsed object under data when schema and handle combine", async () => {
86
- const payload = JSON.stringify({ k: 1 });
87
- const sandbox = loadPrelude(async () => ({
88
- text: payload,
89
- details: { agent: "task", id: "id-9", structured: true },
90
- }));
91
- const node = (await (sandbox.agent as AgentHelper)("emit", {
92
- schema: { type: "object" },
93
- handle: true,
94
- })) as Record<string, unknown>;
95
- expect(node.handle).toBe("agent://id-9");
96
- expect(node.data).toEqual({ k: 1 });
97
- expect(node.text).toBe(payload);
98
- });
99
-
100
- it("falls back to a null handle without throwing when the bridge omits details", async () => {
101
- const sandbox = loadPrelude(async () => ({ text: "lonely" }));
102
- const node = await (sandbox.agent as AgentHelper)("x", { handle: true });
103
- expect(node).toEqual({ text: "lonely", output: "lonely", handle: null, id: null, agent: null });
104
- });
105
-
106
- it("exposes patchPath/branchName/nestedPatches/changesApplied/isolated/isolationSummary on the handle", async () => {
107
- const payload = JSON.stringify({ ok: true });
108
- const sandbox = loadPrelude(async () => ({
109
- text: payload,
110
- details: {
111
- agent: "task",
112
- id: "iso-1",
113
- structured: true,
114
- isolated: true,
115
- patchPath: "/artifacts/iso-1.patch",
116
- changesApplied: null,
117
- nestedPatches: [{ relativePath: "nested", patch: "diff --git a/file b/file\n" }],
118
- isolationSummary: "Isolation: changes captured at `/artifacts/iso-1.patch` (apply=false). Not applied.",
119
- },
120
- }));
121
- const node = (await (sandbox.agent as AgentHelper)("scout", {
122
- schema: { type: "object" },
123
- isolated: true,
124
- apply: false,
125
- handle: true,
126
- })) as Record<string, unknown>;
127
- expect(node.handle).toBe("agent://iso-1");
128
- expect(node.data).toEqual({ ok: true });
129
- expect(node.isolated).toBe(true);
130
- expect(node.patchPath).toBe("/artifacts/iso-1.patch");
131
- expect(node.nestedPatches).toEqual([{ relativePath: "nested", patch: "diff --git a/file b/file\n" }]);
132
- expect(node.changesApplied).toBeNull();
133
- expect(node.isolationSummary).toContain("/artifacts/iso-1.patch");
134
- expect("branchName" in node).toBe(false);
135
- });
136
- });
137
-
138
- describe("eval js read() URI delegation", () => {
139
- it("appends line selectors to delegated URI paths", async () => {
140
- const calls: Array<{ name: string; args: unknown }> = [];
141
- const sandbox = loadPrelude(async (name, args) => {
142
- calls.push({ name, args });
143
- return { text: "resource contents" };
144
- });
145
-
146
- const result = await vm.runInContext(`read("mcp://server/resource", { offset: 10, limit: 5 })`, sandbox);
147
-
148
- expect(result).toBe("resource contents");
149
- expect(calls).toEqual([
150
- {
151
- name: "read",
152
- args: { path: "mcp://server/resource:10-14" },
153
- },
154
- ]);
155
- });
156
- });
@@ -1,137 +0,0 @@
1
- import { expect, it } from "bun:test";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { TempDir } from "@oh-my-pi/pi-utils";
5
-
6
- it("imports the CLI entry graph without loading dotenv before profile bootstrap", async () => {
7
- using tempDir = TempDir.createSync("@omp-js-process-import-");
8
- await Bun.write(path.join(tempDir.path(), ".env"), "OMP_PROCESS_ENTRY_ENV_PROBE=loaded-too-early\n");
9
- const env = Object.fromEntries(
10
- Object.entries(process.env).filter((entry): entry is [string, string] => typeof entry[1] === "string"),
11
- );
12
- delete env.OMP_PROCESS_ENTRY_ENV_PROBE;
13
- env.HOME = tempDir.path();
14
- const fixture = path.resolve(import.meta.dir, "../../../test/fixtures/js-process-entry-import.ts");
15
- const proc = Bun.spawn([process.execPath, fixture], {
16
- env,
17
- stdout: "pipe",
18
- stderr: "pipe",
19
- });
20
- const [exitCode, stdout, stderr] = await Promise.all([
21
- proc.exited,
22
- new Response(proc.stdout).text(),
23
- new Response(proc.stderr).text(),
24
- ]);
25
- expect(exitCode).toBe(0);
26
- expect(stdout).toBe("");
27
- expect(stderr).toBe("");
28
- });
29
-
30
- async function pingComputerWorker(
31
- entry: string,
32
- id: string,
33
- argv: string[] = ["__omp_worker_computer"],
34
- ): Promise<unknown> {
35
- const worker = new Worker(entry, {
36
- type: "module",
37
- argv,
38
- });
39
- const response = Promise.withResolvers<unknown>();
40
- worker.addEventListener("message", event => response.resolve(event.data));
41
- worker.addEventListener("error", event => response.reject(event.error ?? new Error(event.message)));
42
- worker.postMessage({ type: "ping", id });
43
- try {
44
- return await response.promise;
45
- } finally {
46
- worker.terminate();
47
- }
48
- }
49
-
50
- it("starts ordinary CLI paths without loading the native computer addon", async () => {
51
- const cliPath = path.resolve(import.meta.dir, "../../cli.ts");
52
- for (const args of [
53
- ["--no-addons", cliPath, "--version"],
54
- [cliPath, "--help"],
55
- ]) {
56
- const proc = Bun.spawn([process.execPath, ...args], {
57
- stdout: "pipe",
58
- stderr: "pipe",
59
- });
60
- const [exitCode, stderr] = await Promise.all([proc.exited, new Response(proc.stderr).text()]);
61
- expect(exitCode, `${args.at(-1)}: ${stderr}`).toBe(0);
62
- }
63
- });
64
-
65
- it("dispatches the computer worker through the CLI host selector in a child process", async () => {
66
- const fixture = path.resolve(import.meta.dir, "../../../test/fixtures/computer-worker-cli-selector.ts");
67
- const proc = Bun.spawn([process.execPath, "--no-addons", fixture], {
68
- stdout: "pipe",
69
- stderr: "pipe",
70
- });
71
- const [exitCode, stdout, stderr] = await Promise.all([
72
- proc.exited,
73
- new Response(proc.stdout).text(),
74
- new Response(proc.stderr).text(),
75
- ]);
76
- expect(exitCode, stderr).toBe(0);
77
- expect(stdout).toBe('{"type":"pong","id":"computer-cli-selector"}\n');
78
- });
79
-
80
- it("loads the computer worker module directly outside a declared CLI host", async () => {
81
- const entry = new URL("../../tools/computer/worker-entry.ts", import.meta.url).href;
82
- const response = await pingComputerWorker(entry, "computer-direct-module", []);
83
- expect(response).toEqual({ type: "pong", id: "computer-direct-module" });
84
- });
85
-
86
- it("dispatches the computer worker from a single npm-style host bundle", async () => {
87
- const packageDir = path.resolve(import.meta.dir, "../../..");
88
- const outDir = fs.mkdtempSync(path.join(packageDir, ".computer-worker-bundle-"));
89
- try {
90
- const output = await Bun.build({
91
- entrypoints: [path.join(packageDir, "test/fixtures/computer-worker-bundled-host.ts")],
92
- outdir: outDir,
93
- naming: "cli.js",
94
- target: "bun",
95
- external: ["@oh-my-pi/pi-natives"],
96
- define: { "process.env.PI_BUNDLED": JSON.stringify("true") },
97
- throw: false,
98
- });
99
- expect(output.logs).toEqual([]);
100
- expect(output.outputs.map(file => path.basename(file.path))).toEqual(["cli.js"]);
101
- const response = await pingComputerWorker(output.outputs[0]!.path, "computer-npm-bundle");
102
- expect(response).toEqual({ type: "pong", id: "computer-npm-bundle" });
103
- } finally {
104
- fs.rmSync(outDir, { recursive: true, force: true });
105
- }
106
- });
107
-
108
- it("keeps non-computer selectors isolated in a compiled single-entry worker host", async () => {
109
- using tempDir = TempDir.createSync("@omp-compiled-worker-selector-");
110
- const packageDir = path.resolve(import.meta.dir, "../../..");
111
- const outfile = path.join(tempDir.path(), process.platform === "win32" ? "worker-host.exe" : "worker-host");
112
- const build = Bun.spawn(
113
- [
114
- process.execPath,
115
- "build",
116
- "--compile",
117
- "--target=bun",
118
- `--outfile=${outfile}`,
119
- path.join(packageDir, "test/fixtures/compiled-worker-selector-host.ts"),
120
- ],
121
- { cwd: packageDir, stdout: "pipe", stderr: "pipe" },
122
- );
123
- const [buildExitCode, buildStderr] = await Promise.all([build.exited, new Response(build.stderr).text()]);
124
- expect(buildExitCode, buildStderr).toBe(0);
125
- const proc = Bun.spawn([outfile], {
126
- cwd: packageDir,
127
- stdout: "pipe",
128
- stderr: "pipe",
129
- });
130
- const [exitCode, stdout, stderr] = await Promise.all([
131
- proc.exited,
132
- new Response(proc.stdout).text(),
133
- new Response(proc.stderr).text(),
134
- ]);
135
- expect(exitCode, stderr).toBe(0);
136
- expect(stdout).toBe('{"ok":true,"kind":"pong"}\n');
137
- });
@@ -1,104 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { PYTHON_PRELUDE } from "../prelude";
3
-
4
- const pythonPath = Bun.env.PYTHON ?? "python3";
5
-
6
- async function runPrelude(
7
- code: string,
8
- env: Record<string, string>,
9
- ): Promise<{ stdout: string; stderr: string; exitCode: number }> {
10
- const prelude = PYTHON_PRELUDE.replace(
11
- "from __future__ import annotations",
12
- "from __future__ import annotations\n__omp_display = lambda *args, **kwargs: None",
13
- );
14
- const script = `${prelude}\n${code}`;
15
- const proc = Bun.spawn([pythonPath, "-c", script], {
16
- stdout: "pipe",
17
- stderr: "pipe",
18
- env: { ...process.env, ...env },
19
- });
20
- const [stdout, stderr, exitCode] = await Promise.all([
21
- new Response(proc.stdout).text(),
22
- new Response(proc.stderr).text(),
23
- proc.exited,
24
- ]);
25
- return { stdout, stderr, exitCode };
26
- }
27
-
28
- describe("python prelude", () => {
29
- it("exposes read(path, offset?, limit?) with positional optional args", () => {
30
- // The eval docs advertise `read(path, offset?=1, limit?=None)`. A
31
- // keyword-only signature (`def read(path, *, offset=1, limit=None)`)
32
- // makes `read("file", 10)` raise `TypeError: read() takes 1 positional
33
- // argument but 2 were given`, which agents in the wild repeatedly hit.
34
- // Lock the contract so the helper accepts both positional and keyword
35
- // forms.
36
- const match = PYTHON_PRELUDE.match(/def\s+read\(([^)]+)\)/);
37
- expect(match).not.toBeNull();
38
- const signature = match?.[1] ?? "";
39
- expect(signature).not.toContain("*,");
40
- expect(signature).toContain("offset");
41
- expect(signature).toContain("limit");
42
- });
43
-
44
- it("appends line selectors to delegated URI paths", async () => {
45
- const requests: unknown[] = [];
46
- const server = Bun.serve({
47
- hostname: "127.0.0.1",
48
- port: 0,
49
- fetch: async request => {
50
- requests.push(await request.json());
51
- return Response.json({
52
- ok: true,
53
- value: { text: "resource contents", details: { resolvedPath: "/tmp/resource.txt" } },
54
- });
55
- },
56
- });
57
-
58
- try {
59
- const result = await runPrelude(
60
- [`print(read("artifact://21", 3, 2))`, `print(read("mcp://server/resource", 10, 5))`].join("\n"),
61
- {
62
- PI_TOOL_BRIDGE_URL: server.url.toString(),
63
- PI_TOOL_BRIDGE_TOKEN: "test-token",
64
- PI_TOOL_BRIDGE_SESSION: "test-session",
65
- },
66
- );
67
-
68
- expect(result).toEqual({
69
- stdout: "resource contents\nresource contents\n",
70
- stderr: "",
71
- exitCode: 0,
72
- });
73
- expect(requests).toEqual([
74
- {
75
- session: "test-session",
76
- run: null,
77
- name: "read",
78
- args: { path: "artifact://21:3-4" },
79
- },
80
- {
81
- session: "test-session",
82
- run: null,
83
- name: "read",
84
- args: { path: "mcp://server/resource:10-14" },
85
- },
86
- ]);
87
- } finally {
88
- server.stop(true);
89
- }
90
- });
91
-
92
- it("exposes isolation artifacts on the agent() handle node", () => {
93
- // agent(..., handle=True) is the only escape hatch for
94
- // recovering apply=False patch/branch/nested artifacts (the bare
95
- // schema return is just the parsed object), so the helper MUST
96
- // translate the bridge's camelCase details onto the node — otherwise
97
- // an isolated apply=False workflow loses captured nested patches.
98
- expect(PYTHON_PRELUDE).toContain('("patchPath", "patch_path")');
99
- expect(PYTHON_PRELUDE).toContain('("branchName", "branch_name")');
100
- expect(PYTHON_PRELUDE).toContain('("nestedPatches", "nested_patches")');
101
- expect(PYTHON_PRELUDE).toContain('("changesApplied", "changes_applied")');
102
- expect(PYTHON_PRELUDE).toContain('("isolationSummary", "isolation_summary")');
103
- });
104
- });
@@ -1,157 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import * as path from "node:path";
3
-
4
- interface RunnerFrame {
5
- type?: string;
6
- id?: string;
7
- data?: string;
8
- status?: string;
9
- }
10
-
11
- const pythonPath = Bun.env.PYTHON ?? "python3";
12
- const runnerPath = path.resolve(import.meta.dir, "..", "runner.py");
13
- const repoRoot = path.resolve(import.meta.dir, "../../../../../..");
14
- const encoder = new TextEncoder();
15
-
16
- function shellQuote(value: string): string {
17
- return `'${value.replaceAll("'", `'"'"'`)}'`;
18
- }
19
-
20
- async function runCell(code: string): Promise<RunnerFrame[]> {
21
- const proc = Bun.spawn([pythonPath, "-u", runnerPath], {
22
- cwd: repoRoot,
23
- stdin: "pipe",
24
- stdout: "pipe",
25
- stderr: "pipe",
26
- env: {
27
- ...process.env,
28
- PYTHONUNBUFFERED: "1",
29
- PYTHONIOENCODING: "utf-8",
30
- },
31
- });
32
- const stderr = new Response(proc.stderr).text();
33
- const reader = proc.stdout.getReader();
34
- const decoder = new TextDecoder();
35
- let pending = "";
36
- const frames: RunnerFrame[] = [];
37
-
38
- async function readFrame(): Promise<RunnerFrame> {
39
- while (true) {
40
- const newline = pending.indexOf("\n");
41
- if (newline >= 0) {
42
- const line = pending.slice(0, newline);
43
- pending = pending.slice(newline + 1);
44
- return JSON.parse(line) as RunnerFrame;
45
- }
46
- const { value, done } = await reader.read();
47
- if (done) {
48
- throw new Error(`Python runner exited before done frame: ${await stderr}`);
49
- }
50
- pending += decoder.decode(value, { stream: true });
51
- }
52
- }
53
-
54
- try {
55
- proc.stdin.write(encoder.encode(`${JSON.stringify({ id: "r1", code })}\n`));
56
- proc.stdin.flush();
57
- while (true) {
58
- const frame = await readFrame();
59
- frames.push(frame);
60
- if (frame.type === "done") break;
61
- }
62
- proc.stdin.write(encoder.encode(`${JSON.stringify({ type: "exit" })}\n`));
63
- proc.stdin.end();
64
- const exitCode = await proc.exited;
65
- if (exitCode !== 0) {
66
- throw new Error(`Python runner exited ${exitCode}: ${await stderr}`);
67
- }
68
- return frames;
69
- } finally {
70
- try {
71
- reader.releaseLock();
72
- } catch {
73
- // Reader may already be released by stream closure.
74
- }
75
- try {
76
- proc.kill("SIGKILL");
77
- } catch {
78
- // Process already exited.
79
- }
80
- }
81
- }
82
-
83
- describe("Python runner shell output streaming", () => {
84
- it("streams !cmd output chunks before the child process exits", async () => {
85
- const child = [
86
- "import sys,time",
87
- "sys.stdout.write('first\\n')",
88
- "sys.stdout.flush()",
89
- "time.sleep(0.2)",
90
- "sys.stdout.write('second\\n')",
91
- "sys.stdout.flush()",
92
- ].join(";");
93
- const frames = await runCell(
94
- [
95
- `result = !${pythonPath} -c ${shellQuote(child)}`,
96
- "print('return=' + str(result.returncode) + ' lines=' + repr(list(result)))",
97
- ].join("\n"),
98
- );
99
- const stdout = frames.filter(frame => frame.type === "stdout").map(frame => frame.data);
100
-
101
- expect(stdout[0]).toBe("first\n");
102
- expect(stdout.join("")).toContain("second\n");
103
- expect(stdout.join("")).toContain("return=0 lines=['first', 'second']");
104
- });
105
-
106
- it("caps !cmd output and captured result by line count with a truncation notice", async () => {
107
- const child = ["import sys", "sys.stdout.write(('x' + chr(10)) * 3100)", "sys.stdout.flush()"].join(";");
108
- const frames = await runCell(
109
- [
110
- `result = !${pythonPath} -c ${shellQuote(child)}`,
111
- "print('captured=' + str(len(result)) + ' return=' + str(result.returncode))",
112
- ].join("\n"),
113
- );
114
- const stdout = frames
115
- .filter(frame => frame.type === "stdout")
116
- .map(frame => frame.data)
117
- .join("");
118
-
119
- expect(stdout).toContain("[output truncated: shell helper exceeded");
120
- expect(stdout).toContain("captured=3000 return=0");
121
- expect(stdout).not.toContain("captured=3100");
122
- });
123
-
124
- it("caps newline-free !cmd output by bytes with a truncation notice", async () => {
125
- const child = ["import sys", "sys.stdout.write('z' * (1024 * 1024 + 17))", "sys.stdout.flush()"].join(";");
126
- const frames = await runCell(
127
- [
128
- `result = !${pythonPath} -c ${shellQuote(child)}`,
129
- "print('capturedChars=' + str(len(result.n)) + ' return=' + str(result.returncode))",
130
- ].join("\n"),
131
- );
132
- const stdout = frames
133
- .filter(frame => frame.type === "stdout")
134
- .map(frame => frame.data)
135
- .join("");
136
-
137
- expect(stdout).toContain("[output truncated: shell helper exceeded");
138
- expect(stdout).toContain("capturedChars=1048576 return=0");
139
- expect(stdout).not.toContain("capturedChars=1048593");
140
- });
141
-
142
- it("streams newline-free %%bash output without waiting for EOF", async () => {
143
- const child = [
144
- "import sys,time",
145
- "sys.stdout.write('first')",
146
- "sys.stdout.flush()",
147
- "time.sleep(0.2)",
148
- "sys.stdout.write('second')",
149
- "sys.stdout.flush()",
150
- ].join(";");
151
- const frames = await runCell(`%%bash\n${pythonPath} -c ${shellQuote(child)}`);
152
- const stdout = frames.filter(frame => frame.type === "stdout").map(frame => frame.data);
153
-
154
- expect(stdout[0]).toBe("first");
155
- expect(stdout.join("")).toBe("firstsecond");
156
- });
157
- });