@code-yeongyu/senpi 2026.7.10 → 2026.7.11

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 (311) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +4 -2
  3. package/dist/core/extensions/builtin/bash-timeout/timeout.js +2 -2
  4. package/dist/core/extensions/builtin/bash-timeout/timeout.js.map +1 -1
  5. package/dist/core/extensions/builtin/compaction/index.d.ts.map +1 -1
  6. package/dist/core/extensions/builtin/compaction/index.js +10 -1
  7. package/dist/core/extensions/builtin/compaction/index.js.map +1 -1
  8. package/dist/core/extensions/builtin/goal/store.d.ts.map +1 -1
  9. package/dist/core/extensions/builtin/goal/store.js +88 -3
  10. package/dist/core/extensions/builtin/goal/store.js.map +1 -1
  11. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts +3 -0
  12. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.d.ts.map +1 -0
  13. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js +83 -0
  14. package/dist/core/extensions/builtin/prompt-preset/gpt-5.6.js.map +1 -0
  15. package/dist/core/extensions/builtin/prompt-preset/presets.d.ts.map +1 -1
  16. package/dist/core/extensions/builtin/prompt-preset/presets.js +6 -0
  17. package/dist/core/extensions/builtin/prompt-preset/presets.js.map +1 -1
  18. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  19. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts.map +1 -1
  20. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  21. package/dist/core/extensions/builtin/prompt-preset/settings.js.map +1 -1
  22. package/dist/core/extensions/builtin/terminal/notify.d.ts +3 -3
  23. package/dist/core/extensions/builtin/terminal/notify.d.ts.map +1 -1
  24. package/dist/core/extensions/builtin/terminal/notify.js +7 -4
  25. package/dist/core/extensions/builtin/terminal/notify.js.map +1 -1
  26. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts +5 -3
  27. package/dist/core/extensions/builtin/terminal/runtime-session.d.ts.map +1 -1
  28. package/dist/core/extensions/builtin/terminal/runtime-session.js +32 -16
  29. package/dist/core/extensions/builtin/terminal/runtime-session.js.map +1 -1
  30. package/dist/core/extensions/builtin/terminal/tools/bash-output.d.ts +1 -1
  31. package/dist/core/extensions/builtin/terminal/tools/bash-output.d.ts.map +1 -1
  32. package/dist/core/extensions/builtin/terminal/tools/bash-output.js +10 -4
  33. package/dist/core/extensions/builtin/terminal/tools/bash-output.js.map +1 -1
  34. package/dist/core/extensions/builtin/todotools/tools/todowrite.d.ts.map +1 -1
  35. package/dist/core/extensions/builtin/todotools/tools/todowrite.js +5 -8
  36. package/dist/core/extensions/builtin/todotools/tools/todowrite.js.map +1 -1
  37. package/dist/core/extensions/runner.d.ts +1 -0
  38. package/dist/core/extensions/runner.d.ts.map +1 -1
  39. package/dist/core/extensions/runner.js +5 -0
  40. package/dist/core/extensions/runner.js.map +1 -1
  41. package/dist/core/extensions/types.d.ts +5 -8
  42. package/dist/core/extensions/types.d.ts.map +1 -1
  43. package/dist/core/extensions/types.js.map +1 -1
  44. package/dist/core/extensions/wrapper.d.ts.map +1 -1
  45. package/dist/core/extensions/wrapper.js +22 -3
  46. package/dist/core/extensions/wrapper.js.map +1 -1
  47. package/dist/core/keybindings.d.ts +8 -3
  48. package/dist/core/keybindings.d.ts.map +1 -1
  49. package/dist/core/keybindings.js +7 -3
  50. package/dist/core/keybindings.js.map +1 -1
  51. package/dist/core/model-registry.d.ts +1 -6
  52. package/dist/core/model-registry.d.ts.map +1 -1
  53. package/dist/core/model-registry.js +19 -6
  54. package/dist/core/model-registry.js.map +1 -1
  55. package/dist/core/settings-manager.d.ts +4 -3
  56. package/dist/core/settings-manager.d.ts.map +1 -1
  57. package/dist/core/settings-manager.js +2 -1
  58. package/dist/core/settings-manager.js.map +1 -1
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.d.ts.map +1 -1
  61. package/dist/index.js +1 -0
  62. package/dist/index.js.map +1 -1
  63. package/dist/modes/interactive/components/custom-editor.js +1 -1
  64. package/dist/modes/interactive/components/custom-editor.js.map +1 -1
  65. package/dist/modes/interactive/components/login-dialog.d.ts +4 -0
  66. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  67. package/dist/modes/interactive/components/login-dialog.js +9 -2
  68. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  69. package/dist/modes/interactive/components/settings-selector.js +1 -1
  70. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  71. package/dist/modes/interactive/components/thinking-selector.js +1 -1
  72. package/dist/modes/interactive/components/thinking-selector.js.map +1 -1
  73. package/dist/modes/interactive/components/tool-execution-fallback.d.ts +6 -0
  74. package/dist/modes/interactive/components/tool-execution-fallback.d.ts.map +1 -0
  75. package/dist/modes/interactive/components/tool-execution-fallback.js +40 -0
  76. package/dist/modes/interactive/components/tool-execution-fallback.js.map +1 -0
  77. package/dist/modes/interactive/components/tool-execution-images.d.ts +23 -0
  78. package/dist/modes/interactive/components/tool-execution-images.d.ts.map +1 -0
  79. package/dist/modes/interactive/components/tool-execution-images.js +104 -0
  80. package/dist/modes/interactive/components/tool-execution-images.js.map +1 -0
  81. package/dist/modes/interactive/components/tool-execution-renderer.d.ts +28 -0
  82. package/dist/modes/interactive/components/tool-execution-renderer.d.ts.map +1 -0
  83. package/dist/modes/interactive/components/tool-execution-renderer.js +135 -0
  84. package/dist/modes/interactive/components/tool-execution-renderer.js.map +1 -0
  85. package/dist/modes/interactive/components/tool-execution-types.d.ts +23 -0
  86. package/dist/modes/interactive/components/tool-execution-types.d.ts.map +1 -0
  87. package/dist/modes/interactive/components/tool-execution-types.js +2 -0
  88. package/dist/modes/interactive/components/tool-execution-types.js.map +1 -0
  89. package/dist/modes/interactive/components/tool-execution.d.ts +13 -39
  90. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  91. package/dist/modes/interactive/components/tool-execution.js +51 -294
  92. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  93. package/dist/modes/interactive/components/tool-renderer-boundary.d.ts +17 -0
  94. package/dist/modes/interactive/components/tool-renderer-boundary.d.ts.map +1 -0
  95. package/dist/modes/interactive/components/tool-renderer-boundary.js +75 -0
  96. package/dist/modes/interactive/components/tool-renderer-boundary.js.map +1 -0
  97. package/dist/modes/interactive/components/tree-selector.d.ts +5 -0
  98. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  99. package/dist/modes/interactive/components/tree-selector.js +50 -17
  100. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  101. package/dist/modes/interactive/interactive-mode.d.ts +1 -2
  102. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  103. package/dist/modes/interactive/interactive-mode.js +44 -40
  104. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  105. package/dist/modes/interactive/theme/dark.json +1 -0
  106. package/dist/modes/interactive/theme/light.json +1 -0
  107. package/dist/modes/interactive/theme/theme-schema.json +6 -2
  108. package/dist/modes/interactive/theme/theme.d.ts +3 -2
  109. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  110. package/dist/modes/interactive/theme/theme.js +11 -4
  111. package/dist/modes/interactive/theme/theme.js.map +1 -1
  112. package/dist/utils/clipboard-native.d.ts +1 -0
  113. package/dist/utils/clipboard-native.d.ts.map +1 -1
  114. package/dist/utils/clipboard-native.js.map +1 -1
  115. package/dist/utils/clipboard.d.ts +2 -0
  116. package/dist/utils/clipboard.d.ts.map +1 -1
  117. package/dist/utils/clipboard.js +13 -0
  118. package/dist/utils/clipboard.js.map +1 -1
  119. package/docs/custom-provider.md +4 -3
  120. package/docs/extensions.md +140 -1
  121. package/docs/keybindings.md +1 -0
  122. package/docs/models.md +45 -4
  123. package/docs/providers.md +3 -0
  124. package/docs/quickstart.md +1 -1
  125. package/docs/rpc.md +2 -2
  126. package/docs/sdk.md +1 -1
  127. package/docs/settings.md +3 -3
  128. package/docs/themes.md +5 -3
  129. package/docs/tui.md +1 -1
  130. package/docs/usage.md +2 -1
  131. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  132. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  133. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  134. package/examples/extensions/gondolin/package-lock.json +2 -2
  135. package/examples/extensions/gondolin/package.json +1 -1
  136. package/examples/extensions/sandbox/package-lock.json +2 -2
  137. package/examples/extensions/sandbox/package.json +1 -1
  138. package/examples/extensions/with-deps/package-lock.json +2 -2
  139. package/examples/extensions/with-deps/package.json +1 -1
  140. package/node_modules/@code-yeongyu/senpi-codemode/CHANGELOG.md +3 -0
  141. package/node_modules/@code-yeongyu/senpi-codemode/package.json +3 -3
  142. package/node_modules/@code-yeongyu/senpi-codemode/src/bridge/protocol.ts +7 -7
  143. package/node_modules/@code-yeongyu/senpi-codemode/src/completion/tool-bridge.ts +3 -0
  144. package/node_modules/@code-yeongyu/senpi-codemode/src/extension/session-manager.ts +69 -8
  145. package/node_modules/@code-yeongyu/senpi-codemode/src/index.ts +39 -13
  146. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/context-manager.ts +191 -148
  147. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker-entry.js +23 -0
  148. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/inline-worker.ts +4 -175
  149. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/kernel-contract.ts +38 -0
  150. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/run-queue.ts +82 -0
  151. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-core.js +7 -3
  152. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/js/worker-host.ts +117 -0
  153. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel-contract.ts +32 -0
  154. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/kernel.ts +231 -199
  155. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/process.ts +119 -0
  156. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/py/transport.ts +237 -0
  157. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-contract.ts +22 -0
  158. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-kernel.ts +214 -189
  159. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-process.ts +174 -0
  160. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-queue.ts +100 -0
  161. package/node_modules/@code-yeongyu/senpi-codemode/src/kernels/shared/subprocess-run.ts +98 -0
  162. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/cell-handler.ts +205 -0
  163. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/eval-tool.ts +208 -203
  164. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/render.ts +215 -25
  165. package/node_modules/@code-yeongyu/senpi-codemode/src/tool/types.ts +1 -0
  166. package/node_modules/@earendil-works/pi-agent-core/README.md +1 -1
  167. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +2 -0
  168. package/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js.map +1 -1
  169. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts +4 -3
  170. package/node_modules/@earendil-works/pi-agent-core/dist/types.d.ts.map +1 -1
  171. package/node_modules/@earendil-works/pi-agent-core/dist/types.js.map +1 -1
  172. package/node_modules/@earendil-works/pi-agent-core/package.json +2 -2
  173. package/node_modules/@earendil-works/pi-ai/README.md +4 -2
  174. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.d.ts.map +1 -1
  175. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js +89 -40
  176. package/node_modules/@earendil-works/pi-ai/dist/api/anthropic-messages.js.map +1 -1
  177. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts +1 -1
  178. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.d.ts.map +1 -1
  179. package/node_modules/@earendil-works/pi-ai/dist/api/azure-openai-responses.js.map +1 -1
  180. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.d.ts.map +1 -1
  181. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js +7 -7
  182. package/node_modules/@earendil-works/pi-ai/dist/api/bedrock-converse-stream.js.map +1 -1
  183. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts +1 -1
  184. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.d.ts.map +1 -1
  185. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +10 -3
  186. package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js.map +1 -1
  187. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts +1 -1
  188. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.d.ts.map +1 -1
  189. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +5 -1
  190. package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js.map +1 -1
  191. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts +3 -1
  192. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.d.ts.map +1 -1
  193. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js +35 -4
  194. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses-shared.js.map +1 -1
  195. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts +1 -1
  196. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.d.ts.map +1 -1
  197. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js +7 -3
  198. package/node_modules/@earendil-works/pi-ai/dist/api/openai-responses.js.map +1 -1
  199. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.d.ts.map +1 -1
  200. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js +8 -3
  201. package/node_modules/@earendil-works/pi-ai/dist/api/simple-options.js.map +1 -1
  202. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.d.ts.map +1 -1
  203. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js +4 -3
  204. package/node_modules/@earendil-works/pi-ai/dist/api/transform-messages.js.map +1 -1
  205. package/node_modules/@earendil-works/pi-ai/dist/compat.d.ts.map +1 -1
  206. package/node_modules/@earendil-works/pi-ai/dist/compat.js +2 -1
  207. package/node_modules/@earendil-works/pi-ai/dist/compat.js.map +1 -1
  208. package/node_modules/@earendil-works/pi-ai/dist/models.d.ts.map +1 -1
  209. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts +483 -61
  210. package/node_modules/@earendil-works/pi-ai/dist/models.generated.d.ts.map +1 -1
  211. package/node_modules/@earendil-works/pi-ai/dist/models.js +13 -4
  212. package/node_modules/@earendil-works/pi-ai/dist/models.js.map +1 -1
  213. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.d.ts.map +1 -1
  214. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +5 -1
  215. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
  216. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts +62 -5
  217. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.d.ts.map +1 -1
  218. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js +32 -20
  219. package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.models.js.map +1 -1
  220. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts +11 -1
  221. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.d.ts.map +1 -1
  222. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js +6 -4
  223. package/node_modules/@earendil-works/pi-ai/dist/providers/anthropic.models.js.map +1 -1
  224. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts +7 -8
  225. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.d.ts.map +1 -1
  226. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js +20 -21
  227. package/node_modules/@earendil-works/pi-ai/dist/providers/azure-openai-responses.models.js.map +1 -1
  228. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts +11 -1
  229. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.d.ts.map +1 -1
  230. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js +6 -4
  231. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-ai-gateway.models.js.map +1 -1
  232. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.d.ts.map +1 -1
  233. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js +9 -6
  234. package/node_modules/@earendil-works/pi-ai/dist/providers/cloudflare-auth.js.map +1 -1
  235. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.d.ts +2 -2
  236. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.d.ts.map +1 -1
  237. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js +2 -2
  238. package/node_modules/@earendil-works/pi-ai/dist/providers/deepseek.models.js.map +1 -1
  239. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts +2 -2
  240. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.d.ts.map +1 -1
  241. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js +2 -2
  242. package/node_modules/@earendil-works/pi-ai/dist/providers/fireworks.models.js.map +1 -1
  243. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts +100 -2
  244. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.d.ts.map +1 -1
  245. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js +64 -5
  246. package/node_modules/@earendil-works/pi-ai/dist/providers/github-copilot.models.js.map +1 -1
  247. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts +56 -0
  248. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.d.ts.map +1 -1
  249. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js +20 -9
  250. package/node_modules/@earendil-works/pi-ai/dist/providers/openai-codex.models.js.map +1 -1
  251. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts +77 -8
  252. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.d.ts.map +1 -1
  253. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js +37 -24
  254. package/node_modules/@earendil-works/pi-ai/dist/providers/openai.models.js.map +1 -1
  255. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.d.ts +3 -3
  256. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.d.ts.map +1 -1
  257. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js +3 -3
  258. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.models.js.map +1 -1
  259. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts +80 -4
  260. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.d.ts.map +1 -1
  261. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js +64 -8
  262. package/node_modules/@earendil-works/pi-ai/dist/providers/opencode.models.js.map +1 -1
  263. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts +25 -22
  264. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.d.ts.map +1 -1
  265. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js +64 -79
  266. package/node_modules/@earendil-works/pi-ai/dist/providers/openrouter.models.js.map +1 -1
  267. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts +45 -1
  268. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.d.ts.map +1 -1
  269. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js +43 -7
  270. package/node_modules/@earendil-works/pi-ai/dist/providers/vercel-ai-gateway.models.js.map +1 -1
  271. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.d.ts +1 -1
  272. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.d.ts.map +1 -1
  273. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js +1 -1
  274. package/node_modules/@earendil-works/pi-ai/dist/providers/zai-coding-cn.models.js.map +1 -1
  275. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.d.ts +1 -1
  276. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.d.ts.map +1 -1
  277. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js +1 -1
  278. package/node_modules/@earendil-works/pi-ai/dist/providers/zai.models.js.map +1 -1
  279. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts +30 -7
  280. package/node_modules/@earendil-works/pi-ai/dist/types.d.ts.map +1 -1
  281. package/node_modules/@earendil-works/pi-ai/dist/types.js.map +1 -1
  282. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.d.ts +9 -0
  283. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.d.ts.map +1 -0
  284. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.js +36 -0
  285. package/node_modules/@earendil-works/pi-ai/dist/utils/deferred-tools.js.map +1 -0
  286. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.d.ts +3 -3
  287. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.d.ts.map +1 -1
  288. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js +35 -14
  289. package/node_modules/@earendil-works/pi-ai/dist/utils/estimate.js.map +1 -1
  290. package/node_modules/@earendil-works/pi-ai/package.json +1 -1
  291. package/node_modules/@earendil-works/pi-pty/package.json +1 -1
  292. package/node_modules/@earendil-works/pi-tui/dist/components/image.d.ts.map +1 -1
  293. package/node_modules/@earendil-works/pi-tui/dist/components/image.js +2 -1
  294. package/node_modules/@earendil-works/pi-tui/dist/components/image.js.map +1 -1
  295. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts +2 -1
  296. package/node_modules/@earendil-works/pi-tui/dist/index.d.ts.map +1 -1
  297. package/node_modules/@earendil-works/pi-tui/dist/index.js +2 -1
  298. package/node_modules/@earendil-works/pi-tui/dist/index.js.map +1 -1
  299. package/node_modules/@earendil-works/pi-tui/dist/keys.d.ts.map +1 -1
  300. package/node_modules/@earendil-works/pi-tui/dist/keys.js +6 -5
  301. package/node_modules/@earendil-works/pi-tui/dist/keys.js.map +1 -1
  302. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts +0 -1
  303. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.d.ts.map +1 -1
  304. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js +0 -9
  305. package/node_modules/@earendil-works/pi-tui/dist/terminal-image.js.map +1 -1
  306. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.d.ts +4 -0
  307. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.d.ts.map +1 -0
  308. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.js +18 -0
  309. package/node_modules/@earendil-works/pi-tui/dist/terminal-text.js.map +1 -0
  310. package/node_modules/@earendil-works/pi-tui/package.json +2 -2
  311. package/package.json +6 -6
@@ -1,241 +1,266 @@
1
- import type { ChildProcessWithoutNullStreams } from "node:child_process";
2
- import { spawn as nodeSpawn } from "node:child_process";
3
- import { createInterface } from "node:readline";
4
- import type { BridgeConnectionConfig, HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
5
- import { decodeBridgeFrame, encodeBridgeFrame } from "../../bridge/protocol.ts";
6
-
7
- export interface KernelRunInput {
8
- readonly cellId: string;
9
- readonly code: string;
10
- readonly timeoutMs?: number;
11
- }
12
-
13
- export type KernelResult = Extract<KernelToHostMessage, { type: "result" }>;
14
- export type ToolCallMessage = Extract<KernelToHostMessage, { type: "tool-call" }>;
1
+ import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
2
+ import { decodeBridgeFrame, encodeBridgeFrame, isKernelToHostMessage } from "../../bridge/protocol.ts";
3
+ import type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
4
+ import { type SubprocessLike, SubprocessProcess, type SubprocessSpawn, spawnSubprocess } from "./subprocess-process.ts";
5
+ import { SubprocessRunQueue } from "./subprocess-queue.ts";
6
+ import {
7
+ CellInterruptedError,
8
+ failureResult,
9
+ KernelClosedError,
10
+ KernelClosingError,
11
+ KernelExitedError,
12
+ KernelProcessError,
13
+ KernelResetError,
14
+ KernelRetirementError,
15
+ KernelStartupError,
16
+ type PendingRun,
17
+ timeoutResult,
18
+ } from "./subprocess-run.ts";
15
19
 
16
- export interface SubprocessLike {
17
- readonly pid?: number;
18
- readonly stdin: { write(chunk: string): unknown };
19
- readonly stdout: NodeJS.ReadableStream;
20
- readonly stderr: NodeJS.ReadableStream;
21
- once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
22
- kill(signal?: NodeJS.Signals): boolean;
23
- }
24
-
25
- export type SubprocessSpawn = (
26
- command: string,
27
- args: readonly string[],
28
- options: { readonly cwd?: string; readonly env?: NodeJS.ProcessEnv },
29
- ) => SubprocessLike;
30
-
31
- export interface SubprocessKernelOptions {
32
- readonly command: string;
33
- readonly args: readonly string[];
34
- readonly cwd?: string;
35
- readonly env?: NodeJS.ProcessEnv;
36
- readonly sessionId: string;
37
- readonly connection: BridgeConnectionConfig;
38
- readonly spawn?: SubprocessSpawn;
39
- readonly onMessage?: (message: KernelToHostMessage) => void;
40
- }
20
+ export type { KernelResult, KernelRunInput, SubprocessKernelOptions, ToolCallMessage } from "./subprocess-contract.ts";
21
+ export type { SubprocessLike, SubprocessSpawn };
41
22
 
42
23
  export class SubprocessKernel {
43
- private process!: SubprocessLike;
44
24
  private readonly options: SubprocessKernelOptions;
45
25
  private readonly onMessage?: (message: KernelToHostMessage) => void;
46
- private readonly processes = new Set<SubprocessLike>();
47
- private readonly exitPromises = new WeakMap<SubprocessLike, Promise<void>>();
48
- private readonly terminatingProcesses = new WeakSet<SubprocessLike>();
49
- private readonly pendingRuns = new Map<
50
- string,
51
- { readonly resolve: (message: KernelResult) => void; readonly timer: NodeJS.Timeout | null }
52
- >();
53
- private readonly retiringProcesses = new WeakSet<SubprocessLike>();
54
- private readonly pendingCalls: ToolCallMessage[] = [];
55
- private readonly callWaiters: ((message: ToolCallMessage) => void)[] = [];
26
+ private readonly runs = new SubprocessRunQueue();
27
+ private process: SubprocessProcess | null = null;
28
+ private retirementPromise: Promise<void> | null = null;
29
+ private retirementProcess: SubprocessProcess | null = null;
30
+ private retirementFailure: Error | null = null;
31
+ private failure: Error | null = null;
56
32
  private closed = false;
57
33
 
58
34
  constructor(options: SubprocessKernelOptions) {
59
35
  this.options = options;
60
36
  this.onMessage = options.onMessage;
61
- this.process = this.spawnProcess();
37
+ this.spawnProcess();
62
38
  }
63
39
 
64
40
  run(input: KernelRunInput): Promise<KernelResult> {
65
- if (this.closed) throw new Error("Kernel is closed");
66
- const message: HostToKernelMessage = {
67
- type: "run",
68
- cellId: input.cellId,
69
- code: input.code,
70
- timeoutMs: input.timeoutMs,
71
- };
72
- this.send(message);
73
- return new Promise((resolve) => {
74
- const timeoutMs = input.timeoutMs;
75
- const timer =
76
- timeoutMs === undefined
77
- ? null
78
- : setTimeout(() => {
79
- this.pendingRuns.delete(input.cellId);
80
- resolve({
81
- type: "result",
82
- cellId: input.cellId,
83
- ok: false,
84
- error: { message: `Cell timed out after ${timeoutMs}ms` },
85
- durationMs: timeoutMs,
86
- });
87
- this.restartProcess();
88
- }, timeoutMs);
89
- this.pendingRuns.set(input.cellId, { resolve, timer });
90
- });
41
+ if (this.closed) throw new KernelClosedError();
42
+ const run = this.runs.enqueue(input);
43
+ this.pumpRuns();
44
+ return run;
45
+ }
46
+
47
+ async interrupt(reason = "interrupted"): Promise<void> {
48
+ if (this.closed) return;
49
+ if (!this.runs.active) {
50
+ if (!this.retirementPromise) return;
51
+ const queued = this.runs.takeWaiting();
52
+ if (queued) this.runs.settle(queued, failureResult(queued, new CellInterruptedError(reason)));
53
+ return;
54
+ }
55
+ const process = this.process;
56
+ process?.retire();
57
+ this.runs.clearToolCalls();
58
+ const run = this.runs.active;
59
+ if (!run) return;
60
+ this.runs.releaseActive(run);
61
+ this.runs.settle(run, failureResult(run, new CellInterruptedError(reason)));
62
+ const signal = globalThis.process.platform === "win32" ? "SIGTERM" : "SIGINT";
63
+ await this.restartProcess(process, signal, 5_000);
64
+ if (this.failure) throw this.failure;
91
65
  }
92
66
 
93
67
  nextToolCall(): Promise<ToolCallMessage> {
94
- const queued = this.pendingCalls.shift();
95
- if (queued !== undefined) return Promise.resolve(queued);
96
- return new Promise((resolve) => this.callWaiters.push(resolve));
68
+ return this.runs.nextToolCall();
97
69
  }
98
70
 
99
71
  deliverToolReply(message: Extract<HostToKernelMessage, { type: "tool-reply" }>): void {
100
- this.send(message);
72
+ this.process?.send(encodeBridgeFrame(message));
101
73
  }
102
74
 
103
75
  async reset(): Promise<void> {
104
- this.restartProcess();
105
- this.pendingRuns.clear();
106
- this.pendingCalls.length = 0;
107
- this.callWaiters.length = 0;
76
+ if (this.closed) throw new KernelClosedError();
77
+ this.settleAll(new KernelResetError());
78
+ this.runs.clearToolCalls();
79
+ await this.restartProcess(this.process);
80
+ if (this.failure) throw this.failure;
108
81
  }
109
82
 
110
83
  async close(): Promise<void> {
111
- if (this.closed) return;
84
+ const wasClosed = this.closed;
112
85
  this.closed = true;
113
- this.send({ type: "close" });
114
- await Promise.all([...this.processes].map((process) => this.terminateProcess(process)));
115
- }
116
-
117
- private attachReaders(): void {
118
- const activeProcess = this.process;
119
- this.processes.add(activeProcess);
120
- const exitPromise = new Promise<void>((resolve) => {
121
- activeProcess.once("exit", () => {
122
- this.processes.delete(activeProcess);
123
- resolve();
124
- });
125
- });
126
- this.exitPromises.set(activeProcess, exitPromise);
127
- const stdout = createInterface({ input: this.process.stdout });
128
- stdout.on("line", (line) => {
129
- if (this.process !== activeProcess) return;
130
- this.handleLine(`${line}\n`);
131
- });
132
- this.process.stderr.on("data", (chunk) => {
133
- if (this.process !== activeProcess) return;
134
- this.handleMessage({ type: "text", stream: "stderr", data: String(chunk) });
135
- });
136
- this.process.once("exit", (code, signal) => {
137
- if (this.retiringProcesses.has(activeProcess)) return;
138
- if (this.process !== activeProcess) return;
139
- this.closed = true;
140
- const error = { message: `Kernel exited before completing the cell (${signal ?? code ?? "unknown"})` };
141
- for (const [cellId, pending] of this.pendingRuns) {
142
- if (pending.timer) clearTimeout(pending.timer);
143
- pending.resolve({ type: "result", cellId, ok: false, error, durationMs: 0 });
144
- }
145
- this.pendingRuns.clear();
86
+ if (!wasClosed) {
87
+ this.settleAll(new KernelClosingError());
88
+ this.runs.clearToolCalls();
89
+ }
90
+ if (this.retirementPromise) {
91
+ await this.retirementPromise;
92
+ if (this.retirementFailure) throw this.retirementFailure;
93
+ return;
94
+ }
95
+ const process = this.process;
96
+ if (!process) return;
97
+ const closeFrame = wasClosed ? undefined : encodeBridgeFrame({ type: "close" });
98
+ if (!(await process.shutdown(closeFrame))) {
99
+ const error = new KernelRetirementError();
100
+ this.retirementFailure = error;
101
+ throw error;
102
+ }
103
+ if (this.process === process) this.process = null;
104
+ this.retirementFailure = null;
105
+ }
106
+
107
+ private pumpRuns(): void {
108
+ const process = this.process;
109
+ if (this.closed || this.runs.active || !process || process.isRetiring) return;
110
+ const run = this.runs.startNext(performance.now());
111
+ if (!run) return;
112
+ const timeoutMs = run.input.timeoutMs;
113
+ if (timeoutMs !== undefined) run.timer = setTimeout(() => this.handleTimeout(run, timeoutMs), timeoutMs);
114
+ try {
115
+ process.send(encodeBridgeFrame({ type: "run", cellId: run.input.cellId, code: run.input.code, timeoutMs }));
116
+ } catch (error) {
117
+ this.failClosed(new KernelProcessError(error instanceof Error ? error.message : String(error)));
118
+ }
119
+ }
120
+
121
+ private handleTimeout(run: PendingRun, timeoutMs: number): void {
122
+ if (this.runs.active !== run || run.settled) return;
123
+ const process = this.process;
124
+ process?.retire();
125
+ this.runs.clearToolCalls();
126
+ this.runs.releaseActive(run);
127
+ this.runs.settle(run, timeoutResult(run, timeoutMs));
128
+ void this.restartProcess(process);
129
+ }
130
+
131
+ private spawnProcess(): void {
132
+ const child = spawnSubprocess(this.options.spawn, this.options);
133
+ const process = new SubprocessProcess(child, {
134
+ onLine: (source, line) => this.handleLine(source, line),
135
+ onStderr: (source, data) => this.handleMessage(source, { type: "text", stream: "stderr", data }),
136
+ onExit: (source, code, signal) => this.handleExit(source, code, signal),
137
+ onError: (source, error) => {
138
+ if (this.accepts(source)) this.failClosed(new KernelProcessError(error.message));
139
+ },
146
140
  });
141
+ this.process = process;
142
+ try {
143
+ process.send(
144
+ encodeBridgeFrame({ type: "init", sessionId: this.options.sessionId, connection: this.options.connection }),
145
+ );
146
+ } catch (error) {
147
+ const failure = new KernelStartupError(error instanceof Error ? error.message : String(error));
148
+ this.failClosed(failure);
149
+ throw failure;
150
+ }
147
151
  }
148
152
 
149
- private handleLine(line: string): void {
153
+ private handleLine(process: SubprocessProcess, line: string): void {
154
+ if (!this.accepts(process)) return;
150
155
  const decoded = decodeBridgeFrame(line);
151
156
  if (!decoded.ok) {
152
- this.handleMessage({ type: "text", stream: "stderr", data: `${decoded.error.message}\n` });
157
+ this.handleMessage(process, { type: "text", stream: "stderr", data: `${decoded.error.message}\n` });
153
158
  return;
154
159
  }
155
- this.handleMessage(decoded.message as KernelToHostMessage);
160
+ if (isKernelToHostMessage(decoded.message)) this.handleMessage(process, decoded.message);
156
161
  }
157
162
 
158
- private handleMessage(message: KernelToHostMessage): void {
159
- this.onMessage?.(message);
160
- if (message.type === "tool-call") {
161
- const waiter = this.callWaiters.shift();
162
- if (waiter) waiter(message);
163
- else this.pendingCalls.push(message);
164
- return;
165
- }
166
- if (message.type === "result") {
167
- const pending = this.pendingRuns.get(message.cellId);
168
- if (pending?.timer) clearTimeout(pending.timer);
169
- pending?.resolve(message);
170
- this.pendingRuns.delete(message.cellId);
171
- }
163
+ private handleMessage(process: SubprocessProcess, message: KernelToHostMessage): void {
164
+ if (!this.accepts(process)) return;
165
+ if (this.runs.handleMessage(message, this.onMessage)) this.pumpRuns();
172
166
  }
173
167
 
174
- private send(message: HostToKernelMessage): void {
175
- this.process.stdin.write(encodeBridgeFrame(message));
168
+ private handleExit(process: SubprocessProcess, code: number | null, signal: NodeJS.Signals | null): void {
169
+ if (this.process !== process) return;
170
+ if (process.isRetiring) {
171
+ this.process = null;
172
+ this.retirementFailure = null;
173
+ return;
174
+ }
175
+ this.process = null;
176
+ this.failClosed(new KernelExitedError(signal ?? code ?? "unknown"));
176
177
  }
177
178
 
178
- private restartProcess(): void {
179
- const activeProcess = this.process;
180
- this.retiringProcesses.add(activeProcess);
181
- this.process = this.spawnProcess();
182
- void this.terminateProcess(activeProcess);
179
+ private accepts(process: SubprocessProcess): boolean {
180
+ return !this.closed && this.process === process && !process.isRetiring;
183
181
  }
184
182
 
185
- private spawnProcess(): SubprocessLike {
186
- const spawn = this.options.spawn ?? defaultSpawn;
187
- this.closed = false;
188
- const child = spawn(this.options.command, this.options.args, { cwd: this.options.cwd, env: this.options.env });
189
- this.process = child;
190
- this.attachReaders();
191
- this.send({ type: "init", sessionId: this.options.sessionId, connection: this.options.connection });
192
- return child;
183
+ private restartProcess(
184
+ process: SubprocessProcess | null,
185
+ initialSignal: NodeJS.Signals = "SIGTERM",
186
+ escalationMs = 1_500,
187
+ ): Promise<void> {
188
+ if (!process || this.closed) return Promise.resolve();
189
+ if (this.retirementPromise) return this.retirementPromise;
190
+ process.retire();
191
+ const retirement = this.replaceAfterRetirement(process, initialSignal, escalationMs);
192
+ return this.trackRetirement(process, retirement);
193
193
  }
194
194
 
195
- private async terminateProcess(process: SubprocessLike): Promise<void> {
196
- if (!this.terminatingProcesses.has(process)) {
197
- this.terminatingProcesses.add(process);
198
- this.killProcess(process, "SIGTERM");
195
+ private async replaceAfterRetirement(
196
+ process: SubprocessProcess,
197
+ initialSignal: NodeJS.Signals,
198
+ escalationMs: number,
199
+ ): Promise<void> {
200
+ const termination = await process.terminateSafely(initialSignal, escalationMs);
201
+ if (!termination.ok) {
202
+ const error = new KernelProcessError(termination.error.message);
203
+ this.retirementFailure = error;
204
+ this.failClosed(error);
205
+ return;
199
206
  }
200
- const exitPromise = this.exitPromises.get(process);
201
- if (!exitPromise) return;
202
- let forceTimer: NodeJS.Timeout | undefined;
207
+ if (!termination.exited) {
208
+ const error = new KernelRetirementError();
209
+ this.retirementFailure = error;
210
+ this.failClosed(error);
211
+ return;
212
+ }
213
+ if (this.process === process) this.process = null;
214
+ this.retirementFailure = null;
215
+ if (this.closed) return;
203
216
  try {
204
- await Promise.race([
205
- exitPromise,
206
- new Promise<void>((resolve) => {
207
- forceTimer = setTimeout(() => {
208
- this.killProcess(process, "SIGKILL");
209
- resolve();
210
- }, 1_500);
211
- }),
212
- ]);
213
- await exitPromise;
214
- } finally {
215
- if (forceTimer) clearTimeout(forceTimer);
217
+ this.spawnProcess();
218
+ } catch (error) {
219
+ if (!(error instanceof KernelStartupError)) {
220
+ this.failClosed(new KernelStartupError(error instanceof Error ? error.message : String(error)));
221
+ }
222
+ return;
216
223
  }
224
+ this.pumpRuns();
225
+ }
226
+
227
+ private settleAll(error: Error): void {
228
+ this.runs.settleAll(error);
229
+ }
230
+
231
+ private failClosed(error: Error): void {
232
+ const process = this.process;
233
+ this.failure = error;
234
+ this.closed = true;
235
+ process?.retire();
236
+ this.runs.clearToolCalls();
237
+ this.settleAll(error);
238
+ if (!process || this.retirementProcess === process) return;
239
+ this.trackRetirement(process, this.terminateOwnedProcess(process));
217
240
  }
218
241
 
219
- private killProcess(process: SubprocessLike, signal: NodeJS.Signals): void {
220
- if (process.pid !== undefined) {
221
- try {
222
- globalThis.process.kill(-process.pid, signal);
223
- return;
224
- } catch {}
242
+ private async terminateOwnedProcess(process: SubprocessProcess): Promise<void> {
243
+ const termination = await process.terminateSafely();
244
+ if (!termination.ok) {
245
+ this.retirementFailure = new KernelProcessError(termination.error.message);
246
+ return;
247
+ }
248
+ if (!termination.exited) {
249
+ this.retirementFailure = new KernelRetirementError();
250
+ return;
225
251
  }
226
- process.kill(signal);
252
+ if (this.process === process) this.process = null;
253
+ this.retirementFailure = null;
227
254
  }
228
- }
229
255
 
230
- function defaultSpawn(
231
- command: string,
232
- args: readonly string[],
233
- options: { readonly cwd?: string; readonly env?: NodeJS.ProcessEnv },
234
- ): SubprocessLike {
235
- return nodeSpawn(command, [...args], {
236
- cwd: options.cwd,
237
- detached: true,
238
- env: options.env,
239
- stdio: "pipe",
240
- }) as ChildProcessWithoutNullStreams;
256
+ private trackRetirement(process: SubprocessProcess, retirement: Promise<void>): Promise<void> {
257
+ this.retirementProcess = process;
258
+ this.retirementPromise = retirement;
259
+ void retirement.then(() => {
260
+ if (this.retirementPromise !== retirement) return;
261
+ this.retirementPromise = null;
262
+ this.retirementProcess = null;
263
+ });
264
+ return retirement;
265
+ }
241
266
  }
@@ -0,0 +1,174 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { createInterface, type Interface as ReadlineInterface } from "node:readline";
3
+
4
+ export interface SubprocessLike {
5
+ readonly pid?: number;
6
+ readonly stdin: { write(chunk: string): unknown };
7
+ readonly stdout: NodeJS.ReadableStream;
8
+ readonly stderr: NodeJS.ReadableStream;
9
+ on(event: "error", listener: (error: Error) => void): this;
10
+ once(event: "exit", listener: (code: number | null, signal: NodeJS.Signals | null) => void): this;
11
+ removeListener(event: "error", listener: (error: Error) => void): this;
12
+ kill(signal?: NodeJS.Signals): boolean;
13
+ }
14
+
15
+ export type SubprocessSpawn = (
16
+ command: string,
17
+ args: readonly string[],
18
+ options: { readonly cwd?: string; readonly env?: NodeJS.ProcessEnv },
19
+ ) => SubprocessLike;
20
+
21
+ export interface SubprocessSpawnRequest {
22
+ readonly command: string;
23
+ readonly args: readonly string[];
24
+ readonly cwd?: string;
25
+ readonly env?: NodeJS.ProcessEnv;
26
+ }
27
+
28
+ interface SubprocessProcessHandlers {
29
+ readonly onLine: (process: SubprocessProcess, line: string) => void;
30
+ readonly onStderr: (process: SubprocessProcess, data: string) => void;
31
+ readonly onExit: (process: SubprocessProcess, code: number | null, signal: NodeJS.Signals | null) => void;
32
+ readonly onError: (process: SubprocessProcess, error: Error) => void;
33
+ }
34
+
35
+ export type SubprocessTermination =
36
+ | { readonly ok: true; readonly exited: boolean }
37
+ | { readonly ok: false; readonly error: Error };
38
+
39
+ const gracefulExitWaitMs = 1_500;
40
+ const forcedExitWaitMs = 500;
41
+
42
+ export class SubprocessProcess {
43
+ readonly child: SubprocessLike;
44
+ private readonly handlers: SubprocessProcessHandlers;
45
+ private readonly exit = Promise.withResolvers<void>();
46
+ private readonly stdoutReader: ReadlineInterface;
47
+ private readonly stderrListener: (chunk: string | Buffer) => void;
48
+ private readonly errorListener: (error: Error) => void;
49
+ private exited = false;
50
+ private retiring = false;
51
+ private outputDetached = false;
52
+ private errorReported = false;
53
+ private terminationPromise: Promise<boolean> | null = null;
54
+
55
+ constructor(child: SubprocessLike, handlers: SubprocessProcessHandlers) {
56
+ this.child = child;
57
+ this.handlers = handlers;
58
+ this.stdoutReader = createInterface({ input: child.stdout });
59
+ this.stderrListener = (chunk) => {
60
+ if (!this.retiring) this.handlers.onStderr(this, String(chunk));
61
+ };
62
+ this.errorListener = (error) => {
63
+ if (this.errorReported) return;
64
+ this.errorReported = true;
65
+ this.handlers.onError(this, error);
66
+ };
67
+ this.stdoutReader.on("line", (line) => {
68
+ if (!this.retiring) this.handlers.onLine(this, `${line}\n`);
69
+ });
70
+ child.stderr.on("data", this.stderrListener);
71
+ child.on("error", this.errorListener);
72
+ child.once("exit", (code, signal) => {
73
+ this.exited = true;
74
+ this.detachOutput();
75
+ child.removeListener("error", this.errorListener);
76
+ this.exit.resolve();
77
+ this.handlers.onExit(this, code, signal);
78
+ });
79
+ }
80
+
81
+ get isRetiring(): boolean {
82
+ return this.retiring;
83
+ }
84
+
85
+ send(frame: string): boolean {
86
+ if (this.retiring) return false;
87
+ this.child.stdin.write(frame);
88
+ return true;
89
+ }
90
+
91
+ retire(): void {
92
+ if (this.retiring) return;
93
+ this.retiring = true;
94
+ this.detachOutput();
95
+ }
96
+
97
+ terminate(initialSignal: NodeJS.Signals = "SIGTERM", escalationMs = gracefulExitWaitMs): Promise<boolean> {
98
+ if (this.exited) return Promise.resolve(true);
99
+ if (this.terminationPromise) return this.terminationPromise;
100
+ this.terminationPromise = this.performTermination(initialSignal, escalationMs);
101
+ return this.terminationPromise;
102
+ }
103
+
104
+ async terminateSafely(initialSignal?: NodeJS.Signals, escalationMs?: number): Promise<SubprocessTermination> {
105
+ try {
106
+ return { ok: true, exited: await this.terminate(initialSignal, escalationMs) };
107
+ } catch (error) {
108
+ return { ok: false, error: error instanceof Error ? error : new Error(String(error)) };
109
+ }
110
+ }
111
+
112
+ async shutdown(frame?: string): Promise<boolean> {
113
+ if (frame !== undefined) {
114
+ try {
115
+ this.send(frame);
116
+ } catch (error) {
117
+ if (!(error instanceof Error)) throw error;
118
+ }
119
+ }
120
+ this.retire();
121
+ return await this.terminate();
122
+ }
123
+
124
+ private async performTermination(initialSignal: NodeJS.Signals, escalationMs: number): Promise<boolean> {
125
+ this.retire();
126
+ if (this.exited) return true;
127
+ this.kill(initialSignal);
128
+ if (await this.waitForExit(escalationMs)) return true;
129
+ this.kill("SIGKILL");
130
+ return await this.waitForExit(forcedExitWaitMs);
131
+ }
132
+
133
+ private async waitForExit(timeoutMs: number): Promise<boolean> {
134
+ if (this.exited) return true;
135
+ let timer: NodeJS.Timeout | undefined;
136
+ const timeout = new Promise<false>((resolve) => {
137
+ timer = setTimeout(() => resolve(false), timeoutMs);
138
+ });
139
+ try {
140
+ return await Promise.race([this.exit.promise.then(() => true as const), timeout]);
141
+ } finally {
142
+ if (timer) clearTimeout(timer);
143
+ }
144
+ }
145
+
146
+ private detachOutput(): void {
147
+ if (this.outputDetached) return;
148
+ this.outputDetached = true;
149
+ this.stdoutReader.close();
150
+ this.child.stderr.removeListener("data", this.stderrListener);
151
+ }
152
+
153
+ private kill(signal: NodeJS.Signals): void {
154
+ if (this.child.pid !== undefined) {
155
+ try {
156
+ globalThis.process.kill(-this.child.pid, signal);
157
+ return;
158
+ } catch (error) {
159
+ if (!(error instanceof Error)) throw error;
160
+ }
161
+ }
162
+ this.child.kill(signal);
163
+ }
164
+ }
165
+
166
+ export function spawnSubprocess(spawn: SubprocessSpawn | undefined, request: SubprocessSpawnRequest): SubprocessLike {
167
+ if (spawn) return spawn(request.command, request.args, { cwd: request.cwd, env: request.env });
168
+ return nodeSpawn(request.command, [...request.args], {
169
+ cwd: request.cwd,
170
+ detached: true,
171
+ env: request.env,
172
+ stdio: ["pipe", "pipe", "pipe"],
173
+ });
174
+ }