@caupulican/pi-adaptative 0.81.32 → 0.81.37

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 (339) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/dist/bundled-resources/extensions/tmux-agent-manager/README.md +69 -0
  3. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts +53 -0
  4. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -0
  5. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +1282 -0
  6. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -0
  7. package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +1453 -0
  8. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/builder-validator.json +14 -0
  9. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/claude-trio.json +32 -0
  10. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/full-provider-review.json +15 -0
  11. package/dist/bundled-resources/extensions/tmux-agent-manager/templates/provider-prompt-smoke.json +14 -0
  12. package/dist/bundled-resources/prompts/extensionify.md +5 -4
  13. package/dist/bundled-resources/prompts/skillify.md +5 -5
  14. package/dist/bundled-resources/skills/pi-harness-learning/SKILL.md +6 -3
  15. package/dist/cli/args.d.ts.map +1 -1
  16. package/dist/cli/args.js +10 -8
  17. package/dist/cli/args.js.map +1 -1
  18. package/dist/config.d.ts +5 -1
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +10 -2
  21. package/dist/config.js.map +1 -1
  22. package/dist/core/agent-session.d.ts +12 -0
  23. package/dist/core/agent-session.d.ts.map +1 -1
  24. package/dist/core/agent-session.js +42 -2
  25. package/dist/core/agent-session.js.map +1 -1
  26. package/dist/core/autonomy/approval-gate.d.ts.map +1 -1
  27. package/dist/core/autonomy/approval-gate.js +2 -0
  28. package/dist/core/autonomy/approval-gate.js.map +1 -1
  29. package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -1
  30. package/dist/core/autonomy/foreground-envelope.js +2 -0
  31. package/dist/core/autonomy/foreground-envelope.js.map +1 -1
  32. package/dist/core/autonomy/gates.d.ts.map +1 -1
  33. package/dist/core/autonomy/gates.js +9 -2
  34. package/dist/core/autonomy/gates.js.map +1 -1
  35. package/dist/core/autonomy/risk-assessment.d.ts.map +1 -1
  36. package/dist/core/autonomy/risk-assessment.js +2 -0
  37. package/dist/core/autonomy/risk-assessment.js.map +1 -1
  38. package/dist/core/background-lane-controller.d.ts +12 -3
  39. package/dist/core/background-lane-controller.d.ts.map +1 -1
  40. package/dist/core/background-lane-controller.js +67 -26
  41. package/dist/core/background-lane-controller.js.map +1 -1
  42. package/dist/core/bash-execution-controller.d.ts +9 -4
  43. package/dist/core/bash-execution-controller.d.ts.map +1 -1
  44. package/dist/core/bash-execution-controller.js +7 -6
  45. package/dist/core/bash-execution-controller.js.map +1 -1
  46. package/dist/core/bash-executor.d.ts +2 -0
  47. package/dist/core/bash-executor.d.ts.map +1 -1
  48. package/dist/core/bash-executor.js +10 -5
  49. package/dist/core/bash-executor.js.map +1 -1
  50. package/dist/core/context/context-store-retention.d.ts +4 -1
  51. package/dist/core/context/context-store-retention.d.ts.map +1 -1
  52. package/dist/core/context/context-store-retention.js +42 -151
  53. package/dist/core/context/context-store-retention.js.map +1 -1
  54. package/dist/core/context-pipeline.d.ts.map +1 -1
  55. package/dist/core/context-pipeline.js +6 -6
  56. package/dist/core/context-pipeline.js.map +1 -1
  57. package/dist/core/default-tool-surface.d.ts +7 -0
  58. package/dist/core/default-tool-surface.d.ts.map +1 -1
  59. package/dist/core/default-tool-surface.js +29 -10
  60. package/dist/core/default-tool-surface.js.map +1 -1
  61. package/dist/core/doctor.d.ts +10 -10
  62. package/dist/core/doctor.d.ts.map +1 -1
  63. package/dist/core/doctor.js +30 -20
  64. package/dist/core/doctor.js.map +1 -1
  65. package/dist/core/exec.d.ts +2 -0
  66. package/dist/core/exec.d.ts.map +1 -1
  67. package/dist/core/exec.js +26 -39
  68. package/dist/core/exec.js.map +1 -1
  69. package/dist/core/extensions/loader.d.ts +9 -2
  70. package/dist/core/extensions/loader.d.ts.map +1 -1
  71. package/dist/core/extensions/loader.js +105 -21
  72. package/dist/core/extensions/loader.js.map +1 -1
  73. package/dist/core/extensions/types.d.ts +13 -3
  74. package/dist/core/extensions/types.d.ts.map +1 -1
  75. package/dist/core/extensions/types.js +3 -0
  76. package/dist/core/extensions/types.js.map +1 -1
  77. package/dist/core/footer-data-provider.d.ts.map +1 -1
  78. package/dist/core/footer-data-provider.js +2 -0
  79. package/dist/core/footer-data-provider.js.map +1 -1
  80. package/dist/core/goals/goal-state.d.ts +10 -0
  81. package/dist/core/goals/goal-state.d.ts.map +1 -1
  82. package/dist/core/goals/goal-state.js +37 -0
  83. package/dist/core/goals/goal-state.js.map +1 -1
  84. package/dist/core/goals/goal-tool-core.d.ts +11 -0
  85. package/dist/core/goals/goal-tool-core.d.ts.map +1 -1
  86. package/dist/core/goals/goal-tool-core.js +51 -1
  87. package/dist/core/goals/goal-tool-core.js.map +1 -1
  88. package/dist/core/improvement-loop.d.ts.map +1 -1
  89. package/dist/core/improvement-loop.js +26 -5
  90. package/dist/core/improvement-loop.js.map +1 -1
  91. package/dist/core/memory/memory-manager.d.ts.map +1 -1
  92. package/dist/core/memory/memory-manager.js +1 -0
  93. package/dist/core/memory/memory-manager.js.map +1 -1
  94. package/dist/core/model-capability.d.ts.map +1 -1
  95. package/dist/core/model-capability.js +2 -0
  96. package/dist/core/model-capability.js.map +1 -1
  97. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  98. package/dist/core/model-router/tool-escalation.js +17 -3
  99. package/dist/core/model-router/tool-escalation.js.map +1 -1
  100. package/dist/core/models/fitness-store.d.ts.map +1 -1
  101. package/dist/core/models/fitness-store.js +1 -1
  102. package/dist/core/models/fitness-store.js.map +1 -1
  103. package/dist/core/models/local-runtime.d.ts +6 -4
  104. package/dist/core/models/local-runtime.d.ts.map +1 -1
  105. package/dist/core/models/local-runtime.js +108 -36
  106. package/dist/core/models/local-runtime.js.map +1 -1
  107. package/dist/core/output-guard.d.ts.map +1 -1
  108. package/dist/core/output-guard.js +23 -8
  109. package/dist/core/output-guard.js.map +1 -1
  110. package/dist/core/package-manager.d.ts.map +1 -1
  111. package/dist/core/package-manager.js +44 -59
  112. package/dist/core/package-manager.js.map +1 -1
  113. package/dist/core/profile-resource-selection.d.ts +16 -0
  114. package/dist/core/profile-resource-selection.d.ts.map +1 -1
  115. package/dist/core/profile-resource-selection.js +41 -0
  116. package/dist/core/profile-resource-selection.js.map +1 -1
  117. package/dist/core/python-runtime.d.ts +46 -0
  118. package/dist/core/python-runtime.d.ts.map +1 -0
  119. package/dist/core/python-runtime.js +157 -0
  120. package/dist/core/python-runtime.js.map +1 -0
  121. package/dist/core/reflection-controller.d.ts.map +1 -1
  122. package/dist/core/reflection-controller.js +18 -1
  123. package/dist/core/reflection-controller.js.map +1 -1
  124. package/dist/core/reload-blockers.d.ts +1 -0
  125. package/dist/core/reload-blockers.d.ts.map +1 -1
  126. package/dist/core/reload-blockers.js +6 -2
  127. package/dist/core/reload-blockers.js.map +1 -1
  128. package/dist/core/resource-loader.d.ts +2 -0
  129. package/dist/core/resource-loader.d.ts.map +1 -1
  130. package/dist/core/resource-loader.js +50 -28
  131. package/dist/core/resource-loader.js.map +1 -1
  132. package/dist/core/runtime-builder.d.ts +4 -0
  133. package/dist/core/runtime-builder.d.ts.map +1 -1
  134. package/dist/core/runtime-builder.js +31 -9
  135. package/dist/core/runtime-builder.js.map +1 -1
  136. package/dist/core/security/untrusted-boundary.d.ts +2 -2
  137. package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
  138. package/dist/core/security/untrusted-boundary.js +4 -3
  139. package/dist/core/security/untrusted-boundary.js.map +1 -1
  140. package/dist/core/settings-manager.d.ts.map +1 -1
  141. package/dist/core/settings-manager.js +3 -1
  142. package/dist/core/settings-manager.js.map +1 -1
  143. package/dist/core/slash-commands.d.ts.map +1 -1
  144. package/dist/core/slash-commands.js +9 -1
  145. package/dist/core/slash-commands.js.map +1 -1
  146. package/dist/core/system-prompt.d.ts.map +1 -1
  147. package/dist/core/system-prompt.js +13 -2
  148. package/dist/core/system-prompt.js.map +1 -1
  149. package/dist/core/tasks/session-task-state.d.ts +10 -0
  150. package/dist/core/tasks/session-task-state.d.ts.map +1 -0
  151. package/dist/core/tasks/session-task-state.js +29 -0
  152. package/dist/core/tasks/session-task-state.js.map +1 -0
  153. package/dist/core/tasks/task-command.d.ts +30 -0
  154. package/dist/core/tasks/task-command.d.ts.map +1 -0
  155. package/dist/core/tasks/task-command.js +79 -0
  156. package/dist/core/tasks/task-command.js.map +1 -0
  157. package/dist/core/tasks/task-state.d.ts +71 -0
  158. package/dist/core/tasks/task-state.d.ts.map +1 -0
  159. package/dist/core/tasks/task-state.js +372 -0
  160. package/dist/core/tasks/task-state.js.map +1 -0
  161. package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -1
  162. package/dist/core/tool-selection/tool-selection-controller.js +1 -1
  163. package/dist/core/tool-selection/tool-selection-controller.js.map +1 -1
  164. package/dist/core/toolkit/script-runner.d.ts.map +1 -1
  165. package/dist/core/toolkit/script-runner.js +58 -24
  166. package/dist/core/toolkit/script-runner.js.map +1 -1
  167. package/dist/core/tools/bash.d.ts +20 -2
  168. package/dist/core/tools/bash.d.ts.map +1 -1
  169. package/dist/core/tools/bash.js +132 -220
  170. package/dist/core/tools/bash.js.map +1 -1
  171. package/dist/core/tools/extensionify.d.ts +2 -1
  172. package/dist/core/tools/extensionify.d.ts.map +1 -1
  173. package/dist/core/tools/extensionify.js +11 -8
  174. package/dist/core/tools/extensionify.js.map +1 -1
  175. package/dist/core/tools/find.d.ts.map +1 -1
  176. package/dist/core/tools/find.js +57 -48
  177. package/dist/core/tools/find.js.map +1 -1
  178. package/dist/core/tools/git-filter.d.ts.map +1 -1
  179. package/dist/core/tools/git-filter.js +20 -32
  180. package/dist/core/tools/git-filter.js.map +1 -1
  181. package/dist/core/tools/goal.d.ts +1 -1
  182. package/dist/core/tools/goal.d.ts.map +1 -1
  183. package/dist/core/tools/goal.js +10 -3
  184. package/dist/core/tools/goal.js.map +1 -1
  185. package/dist/core/tools/grep.d.ts.map +1 -1
  186. package/dist/core/tools/grep.js +92 -80
  187. package/dist/core/tools/grep.js.map +1 -1
  188. package/dist/core/tools/index.d.ts +9 -6
  189. package/dist/core/tools/index.d.ts.map +1 -1
  190. package/dist/core/tools/index.js +24 -9
  191. package/dist/core/tools/index.js.map +1 -1
  192. package/dist/core/tools/output-accumulator.d.ts +2 -0
  193. package/dist/core/tools/output-accumulator.d.ts.map +1 -1
  194. package/dist/core/tools/output-accumulator.js +7 -4
  195. package/dist/core/tools/output-accumulator.js.map +1 -1
  196. package/dist/core/tools/python.d.ts +64 -0
  197. package/dist/core/tools/python.d.ts.map +1 -0
  198. package/dist/core/tools/python.js +241 -0
  199. package/dist/core/tools/python.js.map +1 -0
  200. package/dist/core/tools/read.d.ts.map +1 -1
  201. package/dist/core/tools/read.js +4 -3
  202. package/dist/core/tools/read.js.map +1 -1
  203. package/dist/core/tools/shell-contract-router.d.ts +13 -0
  204. package/dist/core/tools/shell-contract-router.d.ts.map +1 -0
  205. package/dist/core/tools/shell-contract-router.js +295 -0
  206. package/dist/core/tools/shell-contract-router.js.map +1 -0
  207. package/dist/core/tools/task-steps.d.ts +46 -0
  208. package/dist/core/tools/task-steps.d.ts.map +1 -0
  209. package/dist/core/tools/task-steps.js +160 -0
  210. package/dist/core/tools/task-steps.js.map +1 -0
  211. package/dist/index.d.ts +2 -1
  212. package/dist/index.d.ts.map +1 -1
  213. package/dist/index.js +2 -1
  214. package/dist/index.js.map +1 -1
  215. package/dist/main.d.ts +2 -0
  216. package/dist/main.d.ts.map +1 -1
  217. package/dist/main.js +23 -4
  218. package/dist/main.js.map +1 -1
  219. package/dist/migrations.d.ts.map +1 -1
  220. package/dist/migrations.js +18 -4
  221. package/dist/migrations.js.map +1 -1
  222. package/dist/modes/interactive/auth-dialogs-controller.d.ts +10 -0
  223. package/dist/modes/interactive/auth-dialogs-controller.d.ts.map +1 -1
  224. package/dist/modes/interactive/auth-dialogs-controller.js +94 -27
  225. package/dist/modes/interactive/auth-dialogs-controller.js.map +1 -1
  226. package/dist/modes/interactive/auto-learn-controller.d.ts.map +1 -1
  227. package/dist/modes/interactive/auto-learn-controller.js +2 -1
  228. package/dist/modes/interactive/auto-learn-controller.js.map +1 -1
  229. package/dist/modes/interactive/components/bordered-loader.d.ts +1 -0
  230. package/dist/modes/interactive/components/bordered-loader.d.ts.map +1 -1
  231. package/dist/modes/interactive/components/bordered-loader.js +8 -0
  232. package/dist/modes/interactive/components/bordered-loader.js.map +1 -1
  233. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  234. package/dist/modes/interactive/components/extension-editor.js +6 -15
  235. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  236. package/dist/modes/interactive/components/login-dialog.d.ts +3 -1
  237. package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  238. package/dist/modes/interactive/components/login-dialog.js +20 -8
  239. package/dist/modes/interactive/components/login-dialog.js.map +1 -1
  240. package/dist/modes/interactive/components/profile-resource-editor.d.ts +4 -1
  241. package/dist/modes/interactive/components/profile-resource-editor.d.ts.map +1 -1
  242. package/dist/modes/interactive/components/profile-resource-editor.js +22 -39
  243. package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
  244. package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
  245. package/dist/modes/interactive/components/session-selector.js +1 -1
  246. package/dist/modes/interactive/components/session-selector.js.map +1 -1
  247. package/dist/modes/interactive/config-backup.d.ts +1 -0
  248. package/dist/modes/interactive/config-backup.d.ts.map +1 -1
  249. package/dist/modes/interactive/config-backup.js +14 -3
  250. package/dist/modes/interactive/config-backup.js.map +1 -1
  251. package/dist/modes/interactive/editor-overlay-host.d.ts +12 -0
  252. package/dist/modes/interactive/editor-overlay-host.d.ts.map +1 -1
  253. package/dist/modes/interactive/editor-overlay-host.js +55 -0
  254. package/dist/modes/interactive/editor-overlay-host.js.map +1 -1
  255. package/dist/modes/interactive/extension-ui-host.d.ts +2 -1
  256. package/dist/modes/interactive/extension-ui-host.d.ts.map +1 -1
  257. package/dist/modes/interactive/extension-ui-host.js +161 -96
  258. package/dist/modes/interactive/extension-ui-host.js.map +1 -1
  259. package/dist/modes/interactive/external-editor.d.ts.map +1 -1
  260. package/dist/modes/interactive/external-editor.js +23 -28
  261. package/dist/modes/interactive/external-editor.js.map +1 -1
  262. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  263. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  264. package/dist/modes/interactive/interactive-mode.js +26 -8
  265. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  266. package/dist/modes/interactive/profile-menu-controller.d.ts +5 -1
  267. package/dist/modes/interactive/profile-menu-controller.d.ts.map +1 -1
  268. package/dist/modes/interactive/profile-menu-controller.js +118 -24
  269. package/dist/modes/interactive/profile-menu-controller.js.map +1 -1
  270. package/dist/modes/interactive/resource-shell-commands.d.ts +1 -0
  271. package/dist/modes/interactive/resource-shell-commands.d.ts.map +1 -1
  272. package/dist/modes/interactive/resource-shell-commands.js +5 -1
  273. package/dist/modes/interactive/resource-shell-commands.js.map +1 -1
  274. package/dist/modes/interactive/session-flow-commands.d.ts +10 -0
  275. package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
  276. package/dist/modes/interactive/session-flow-commands.js +143 -7
  277. package/dist/modes/interactive/session-flow-commands.js.map +1 -1
  278. package/dist/modes/interactive/session-io-commands.d.ts.map +1 -1
  279. package/dist/modes/interactive/session-io-commands.js +80 -50
  280. package/dist/modes/interactive/session-io-commands.js.map +1 -1
  281. package/dist/modes/interactive/startup-checks.d.ts.map +1 -1
  282. package/dist/modes/interactive/startup-checks.js +14 -17
  283. package/dist/modes/interactive/startup-checks.js.map +1 -1
  284. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  285. package/dist/modes/rpc/rpc-client.js +20 -19
  286. package/dist/modes/rpc/rpc-client.js.map +1 -1
  287. package/dist/package-manager-cli.d.ts.map +1 -1
  288. package/dist/package-manager-cli.js +10 -19
  289. package/dist/package-manager-cli.js.map +1 -1
  290. package/dist/utils/child-process.d.ts +13 -7
  291. package/dist/utils/child-process.d.ts.map +1 -1
  292. package/dist/utils/child-process.js +56 -3
  293. package/dist/utils/child-process.js.map +1 -1
  294. package/dist/utils/clipboard-image.d.ts.map +1 -1
  295. package/dist/utils/clipboard-image.js +3 -2
  296. package/dist/utils/clipboard-image.js.map +1 -1
  297. package/dist/utils/clipboard.d.ts.map +1 -1
  298. package/dist/utils/clipboard.js +17 -10
  299. package/dist/utils/clipboard.js.map +1 -1
  300. package/dist/utils/external-editor-command.d.ts +8 -0
  301. package/dist/utils/external-editor-command.d.ts.map +1 -0
  302. package/dist/utils/external-editor-command.js +60 -0
  303. package/dist/utils/external-editor-command.js.map +1 -0
  304. package/dist/utils/paths.d.ts.map +1 -1
  305. package/dist/utils/paths.js +10 -2
  306. package/dist/utils/paths.js.map +1 -1
  307. package/dist/utils/shell.d.ts +6 -8
  308. package/dist/utils/shell.d.ts.map +1 -1
  309. package/dist/utils/shell.js +61 -70
  310. package/dist/utils/shell.js.map +1 -1
  311. package/dist/utils/tools-manager.d.ts +23 -3
  312. package/dist/utils/tools-manager.d.ts.map +1 -1
  313. package/dist/utils/tools-manager.js +117 -21
  314. package/dist/utils/tools-manager.js.map +1 -1
  315. package/dist/utils/work-directory.d.ts +36 -0
  316. package/dist/utils/work-directory.d.ts.map +1 -0
  317. package/dist/utils/work-directory.js +462 -0
  318. package/dist/utils/work-directory.js.map +1 -0
  319. package/docs/index.md +4 -0
  320. package/docs/python.md +43 -0
  321. package/docs/resources.md +2 -2
  322. package/docs/rpc.md +1 -1
  323. package/docs/settings.md +7 -3
  324. package/docs/task-steps.md +90 -0
  325. package/docs/termux.md +7 -0
  326. package/docs/tmux-agent-manager.md +48 -0
  327. package/docs/usage.md +5 -6
  328. package/docs/windows.md +104 -8
  329. package/docs/work-directory.md +41 -0
  330. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  331. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  332. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  333. package/examples/extensions/sandbox/package-lock.json +2 -2
  334. package/examples/extensions/sandbox/package.json +1 -1
  335. package/examples/extensions/with-deps/package-lock.json +2 -2
  336. package/examples/extensions/with-deps/package.json +1 -1
  337. package/npm-shrinkwrap.json +32 -37
  338. package/package.json +9 -7
  339. package/scripts/postinstall-python-runtime.mjs +20 -0
@@ -4,15 +4,34 @@
4
4
  * RuntimeBuilder still registers additional opt-in tools, while model capability and UAC profile
5
5
  * filters may only narrow this request. Keeping the SDK and direct-runtime paths on one list avoids
6
6
  * silently losing goal/delegation/toolkit capabilities in SDK-created interactive sessions.
7
+ *
8
+ * `bash` is the stable agent shell contract on every platform; its finite grammar routes to
9
+ * PowerShell on Windows. `python` is a separate bounded, uv-managed execution contract.
7
10
  */
8
- export const DEFAULT_ACTIVE_TOOL_NAMES = [
9
- "read",
10
- "bash",
11
- "edit",
12
- "write",
13
- "context_audit",
14
- "goal",
15
- "delegate",
16
- "run_toolkit_script",
17
- ];
11
+ export function getDefaultActiveToolNames(_platform = process.platform) {
12
+ return [
13
+ "read",
14
+ "bash",
15
+ "python",
16
+ "edit",
17
+ "write",
18
+ "context_audit",
19
+ "goal",
20
+ "task_steps",
21
+ "delegate",
22
+ "run_toolkit_script",
23
+ ];
24
+ }
25
+ /** Current-process default tool request. */
26
+ export const DEFAULT_ACTIVE_TOOL_NAMES = getDefaultActiveToolNames();
27
+ /** Map legacy/platform-specific shell names to the stable Bash-like agent contract. */
28
+ export function mapToolNamesForPlatform(names, _platform = process.platform) {
29
+ const mapped = [];
30
+ for (const name of names) {
31
+ const resolved = name === "powershell" ? "bash" : name;
32
+ if (!mapped.includes(resolved))
33
+ mapped.push(resolved);
34
+ }
35
+ return mapped;
36
+ }
18
37
  //# sourceMappingURL=default-tool-surface.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"default-tool-surface.js","sourceRoot":"","sources":["../../src/core/default-tool-surface.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,yBAAyB,GAAsB;IAC3D,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,eAAe;IACf,MAAM;IACN,UAAU;IACV,oBAAoB;CACpB,CAAC","sourcesContent":["/**\n * Default built-in tool request shared by every AgentSession construction path.\n *\n * RuntimeBuilder still registers additional opt-in tools, while model capability and UAC profile\n * filters may only narrow this request. Keeping the SDK and direct-runtime paths on one list avoids\n * silently losing goal/delegation/toolkit capabilities in SDK-created interactive sessions.\n */\nexport const DEFAULT_ACTIVE_TOOL_NAMES: readonly string[] = [\n\t\"read\",\n\t\"bash\",\n\t\"edit\",\n\t\"write\",\n\t\"context_audit\",\n\t\"goal\",\n\t\"delegate\",\n\t\"run_toolkit_script\",\n];\n"]}
1
+ {"version":3,"file":"default-tool-surface.js","sourceRoot":"","sources":["../../src/core/default-tool-surface.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,MAAM,UAAU,yBAAyB,CAAC,SAAS,GAAoB,OAAO,CAAC,QAAQ,EAAqB;IAC3G,OAAO;QACN,MAAM;QACN,MAAM;QACN,QAAQ;QACR,MAAM;QACN,OAAO;QACP,eAAe;QACf,MAAM;QACN,YAAY;QACZ,UAAU;QACV,oBAAoB;KACpB,CAAC;AAAA,CACF;AAED,4CAA4C;AAC5C,MAAM,CAAC,MAAM,yBAAyB,GAAsB,yBAAyB,EAAE,CAAC;AAExF,uFAAuF;AACvF,MAAM,UAAU,uBAAuB,CACtC,KAAwB,EACxB,SAAS,GAAoB,OAAO,CAAC,QAAQ,EAClC;IACX,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,QAAQ,GAAG,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QACvD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YAAE,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,MAAM,CAAC;AAAA,CACd","sourcesContent":["/**\n * Default built-in tool request shared by every AgentSession construction path.\n *\n * RuntimeBuilder still registers additional opt-in tools, while model capability and UAC profile\n * filters may only narrow this request. Keeping the SDK and direct-runtime paths on one list avoids\n * silently losing goal/delegation/toolkit capabilities in SDK-created interactive sessions.\n *\n * `bash` is the stable agent shell contract on every platform; its finite grammar routes to\n * PowerShell on Windows. `python` is a separate bounded, uv-managed execution contract.\n */\nexport function getDefaultActiveToolNames(_platform: NodeJS.Platform = process.platform): readonly string[] {\n\treturn [\n\t\t\"read\",\n\t\t\"bash\",\n\t\t\"python\",\n\t\t\"edit\",\n\t\t\"write\",\n\t\t\"context_audit\",\n\t\t\"goal\",\n\t\t\"task_steps\",\n\t\t\"delegate\",\n\t\t\"run_toolkit_script\",\n\t];\n}\n\n/** Current-process default tool request. */\nexport const DEFAULT_ACTIVE_TOOL_NAMES: readonly string[] = getDefaultActiveToolNames();\n\n/** Map legacy/platform-specific shell names to the stable Bash-like agent contract. */\nexport function mapToolNamesForPlatform(\n\tnames: readonly string[],\n\t_platform: NodeJS.Platform = process.platform,\n): string[] {\n\tconst mapped: string[] = [];\n\tfor (const name of names) {\n\t\tconst resolved = name === \"powershell\" ? \"bash\" : name;\n\t\tif (!mapped.includes(resolved)) mapped.push(resolved);\n\t}\n\treturn mapped;\n}\n"]}
@@ -1,5 +1,6 @@
1
- import { type FffInstallOutcome, type SystemToolStatus } from "../utils/tools-manager.ts";
1
+ import { type FffInstallOutcome } from "../utils/tools-manager.ts";
2
2
  import { OllamaRuntime } from "./models/local-runtime.ts";
3
+ import { type PythonRuntimeOutcome } from "./python-runtime.ts";
3
4
  /**
4
5
  * Environment doctor: verifies required tooling and installs what it safely
5
6
  * can, instead of leaving provisioning entirely to lazy first-use (the exact
@@ -8,13 +9,10 @@ import { OllamaRuntime } from "./models/local-runtime.ts";
8
9
  * for the underlying lazy-install fix; this module is the proactive half).
9
10
  *
10
11
  * Two tool kinds, two different postures:
11
- * - "managed": pi already owns the install path (fff-node into
12
- * ~/.pi/agent/bin via tools-manager.ts's ensureFffNodePackage). The doctor
13
- * actually attempts the install when missing.
14
- * - "system": pi does not own the install (ripgrep, ollama, python). GUIDE
15
- * MODE only -- exact manual steps are reported, never executed, never
16
- * `curl | sh`, never `sudo`. This matches the harness's deliberate
17
- * no-silent-installer stance for tooling outside its own managed bin dir.
12
+ * - "managed": pi owns provisioning and attempts it when absent (fff-node,
13
+ * plus pinned uv and the Python interpreter resolved through it).
14
+ * - "system": pi does not own the install (ripgrep and ollama). GUIDE MODE
15
+ * only -- exact manual steps are reported, never executed.
18
16
  */
19
17
  export type DoctorToolKind = "managed" | "system";
20
18
  export interface DoctorCheck {
@@ -41,7 +39,9 @@ export interface DoctorDeps {
41
39
  ensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;
42
40
  getLastFffInstallOutcome: () => FffInstallOutcome | undefined;
43
41
  getRgPath: () => string | null;
44
- detectPython: () => SystemToolStatus;
42
+ ensurePythonRuntime: (options: {
43
+ silent: boolean;
44
+ }) => Promise<PythonRuntimeOutcome>;
45
45
  /** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */
46
46
  probeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;
47
47
  ollamaRuntime: DoctorOllamaRuntime;
@@ -62,5 +62,5 @@ export declare function formatDoctorReport(report: DoctorReport): string;
62
62
  * succeeds. Must never fail the update itself: any error here is swallowed
63
63
  * and reported as a skipped check, not surfaced as an update failure.
64
64
  */
65
- export declare function runUpdatePreflight(): Promise<void>;
65
+ export declare function runUpdatePreflight(deps?: DoctorDeps): Promise<void>;
66
66
  //# sourceMappingURL=doctor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAGN,KAAK,iBAAiB,EAKtB,KAAK,gBAAgB,EACrB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAE1D;;;;;;;;;;;;;;;GAeG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElD,MAAM,WAAW,WAAW;IAC3B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,qHAAqH;AACrH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,cAAc,CAAC,CAAC;AAEjF,MAAM,WAAW,UAAU;IAC1B,2BAA2B,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IACvD,oBAAoB,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzE,wBAAwB,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAC;IAC9D,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,gBAAgB,CAAC;IACrC,8IAA8I;IAC9I,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;IACvF,aAAa,EAAE,mBAAmB,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAChC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AAyHD,wBAAsB,SAAS,CAC9B,IAAI,GAAE,UAA2B,EACjC,OAAO,GAAE,gBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAIvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAW/D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAOxD","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tdetectPython,\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n\ttype SystemToolStatus,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi already owns the install path (fff-node into\n * ~/.pi/agent/bin via tools-manager.ts's ensureFffNodePackage). The doctor\n * actually attempts the install when missing.\n * - \"system\": pi does not own the install (ripgrep, ollama, python). GUIDE\n * MODE only -- exact manual steps are reported, never executed, never\n * `curl | sh`, never `sudo`. This matches the harness's deliberate\n * no-silent-installer stance for tooling outside its own managed bin dir.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tdetectPython: () => SystemToolStatus;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tdetectPython: () => detectPython(),\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nconst PYTHON_GUIDE = [\n\t\"python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install python3\",\n\t\" - Debian/Ubuntu: apt install python3\",\n\t\" - Arch: pacman -S python\",\n\t\" - Or download from https://www.python.org/downloads/\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkPython(deps: DoctorDeps): DoctorCheck {\n\tconst status = deps.detectPython();\n\tif (!status.present) {\n\t\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: false, guide: PYTHON_GUIDE };\n\t}\n\tconst detail = status.version ? `${status.command} (${status.version})` : status.command;\n\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: true, detail };\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama] = await Promise.all([checkFffNode(deps, silent), checkOllama(deps)]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, checkPython(deps)] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor();\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"AAEA,OAAO,EAEN,KAAK,iBAAiB,EAKtB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAErF;;;;;;;;;;;;GAYG;AAEH,MAAM,MAAM,cAAc,GAAG,SAAS,GAAG,QAAQ,CAAC;AAElD,MAAM,WAAW,WAAW;IAC3B,yEAAyE;IACzE,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,uFAAuF;IACvF,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC5B,MAAM,EAAE,WAAW,EAAE,CAAC;CACtB;AAED,qHAAqH;AACrH,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,aAAa,EAAE,QAAQ,GAAG,cAAc,CAAC,CAAC;AAEjF,MAAM,WAAW,UAAU;IAC1B,2BAA2B,EAAE,MAAM,OAAO,GAAG,SAAS,CAAC;IACvD,oBAAoB,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,KAAK,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IACzE,wBAAwB,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAC;IAC9D,SAAS,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,oBAAoB,CAAC,CAAC;IACrF,8IAA8I;IAC9I,YAAY,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,KAAK,MAAM,GAAG,SAAS,CAAC;IACvF,aAAa,EAAE,mBAAmB,CAAC;CACnC;AAED,MAAM,WAAW,gBAAgB;IAChC;;;;;OAKG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;CACjB;AA6HD,wBAAsB,SAAS,CAC9B,IAAI,GAAE,UAA2B,EACjC,OAAO,GAAE,gBAAqB,GAC5B,OAAO,CAAC,YAAY,CAAC,CAQvB;AAED,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,GAAG,MAAM,CAW/D;AAED;;;;GAIG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,GAAE,UAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAOzF","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\nimport { ensurePythonRuntime, type PythonRuntimeOutcome } from \"./python-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi owns provisioning and attempts it when absent (fff-node,\n * plus pinned uv and the Python interpreter resolved through it).\n * - \"system\": pi does not own the install (ripgrep and ollama). GUIDE MODE\n * only -- exact manual steps are reported, never executed.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tensurePythonRuntime: (options: { silent: boolean }) => Promise<PythonRuntimeOutcome>;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tensurePythonRuntime,\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** MANAGED tool: uv and Python share one deduplicated runtime manager with bounded provisioning. */\nasync function checkPython(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\tconst outcome = await deps.ensurePythonRuntime({ silent });\n\tif (outcome.status !== \"ready\") {\n\t\treturn {\n\t\t\tid: \"python\",\n\t\t\tlabel: \"Python (uv-managed)\",\n\t\t\tkind: \"managed\",\n\t\t\tpresent: false,\n\t\t\tdetail: outcome.reason,\n\t\t};\n\t}\n\treturn {\n\t\tid: \"python\",\n\t\tlabel: \"Python (uv-managed)\",\n\t\tkind: \"managed\",\n\t\tpresent: true,\n\t\tinstallAttempted: outcome.pythonInstalled,\n\t\tdetail: `uv: ${outcome.uvPath}; python: ${outcome.pythonPath}${outcome.pythonInstalled ? \" (installed just now)\" : \"\"}`,\n\t};\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama, python] = await Promise.all([\n\t\tcheckFffNode(deps, silent),\n\t\tcheckOllama(deps),\n\t\tcheckPython(deps, silent),\n\t]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, python] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(deps: DoctorDeps = realDoctorDeps): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor(deps);\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
@@ -1,13 +1,14 @@
1
1
  import chalk from "chalk";
2
2
  import { getAgentDir } from "../config.js";
3
- import { detectPython, ensureFffNodePackage, getLastFffInstallOutcome, getToolPath, loadAvailableFffNodePackage, probeVersion, } from "../utils/tools-manager.js";
3
+ import { ensureFffNodePackage, getLastFffInstallOutcome, getToolPath, loadAvailableFffNodePackage, probeVersion, } from "../utils/tools-manager.js";
4
4
  import { OllamaRuntime } from "./models/local-runtime.js";
5
+ import { ensurePythonRuntime } from "./python-runtime.js";
5
6
  const realDoctorDeps = {
6
7
  loadAvailableFffNodePackage,
7
8
  ensureFffNodePackage,
8
9
  getLastFffInstallOutcome,
9
10
  getRgPath: () => getToolPath("rg"),
10
- detectPython: () => detectPython(),
11
+ ensurePythonRuntime,
11
12
  probeVersion,
12
13
  ollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),
13
14
  };
@@ -18,13 +19,6 @@ const RIPGREP_GUIDE = [
18
19
  " - Arch: pacman -S ripgrep",
19
20
  " - Or download a release: https://github.com/BurntSushi/ripgrep/releases",
20
21
  ];
21
- const PYTHON_GUIDE = [
22
- "python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):",
23
- " - macOS (Homebrew): brew install python3",
24
- " - Debian/Ubuntu: apt install python3",
25
- " - Arch: pacman -S python",
26
- " - Or download from https://www.python.org/downloads/",
27
- ];
28
22
  function describeFffOutcome(outcome) {
29
23
  switch (outcome?.status) {
30
24
  case "already-available":
@@ -103,19 +97,35 @@ async function checkOllama(deps) {
103
97
  detail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? "up" : "down"} at ${status.serverUrl}; active store: ${activeStore}`,
104
98
  };
105
99
  }
106
- /** SYSTEM tool: guide mode only, never installed by the doctor. */
107
- function checkPython(deps) {
108
- const status = deps.detectPython();
109
- if (!status.present) {
110
- return { id: "python", label: "Python (python3)", kind: "system", present: false, guide: PYTHON_GUIDE };
100
+ /** MANAGED tool: uv and Python share one deduplicated runtime manager with bounded provisioning. */
101
+ async function checkPython(deps, silent) {
102
+ const outcome = await deps.ensurePythonRuntime({ silent });
103
+ if (outcome.status !== "ready") {
104
+ return {
105
+ id: "python",
106
+ label: "Python (uv-managed)",
107
+ kind: "managed",
108
+ present: false,
109
+ detail: outcome.reason,
110
+ };
111
111
  }
112
- const detail = status.version ? `${status.command} (${status.version})` : status.command;
113
- return { id: "python", label: "Python (python3)", kind: "system", present: true, detail };
112
+ return {
113
+ id: "python",
114
+ label: "Python (uv-managed)",
115
+ kind: "managed",
116
+ present: true,
117
+ installAttempted: outcome.pythonInstalled,
118
+ detail: `uv: ${outcome.uvPath}; python: ${outcome.pythonPath}${outcome.pythonInstalled ? " (installed just now)" : ""}`,
119
+ };
114
120
  }
115
121
  export async function runDoctor(deps = realDoctorDeps, options = {}) {
116
122
  const silent = options.silent ?? true;
117
- const [fffNode, ollama] = await Promise.all([checkFffNode(deps, silent), checkOllama(deps)]);
118
- return { checks: [fffNode, checkRipgrep(deps), ollama, checkPython(deps)] };
123
+ const [fffNode, ollama, python] = await Promise.all([
124
+ checkFffNode(deps, silent),
125
+ checkOllama(deps),
126
+ checkPython(deps, silent),
127
+ ]);
128
+ return { checks: [fffNode, checkRipgrep(deps), ollama, python] };
119
129
  }
120
130
  export function formatDoctorReport(report) {
121
131
  const lines = [];
@@ -135,9 +145,9 @@ export function formatDoctorReport(report) {
135
145
  * succeeds. Must never fail the update itself: any error here is swallowed
136
146
  * and reported as a skipped check, not surfaced as an update failure.
137
147
  */
138
- export async function runUpdatePreflight() {
148
+ export async function runUpdatePreflight(deps = realDoctorDeps) {
139
149
  try {
140
- const report = await runDoctor();
150
+ const report = await runDoctor(deps);
141
151
  console.log(`\n${formatDoctorReport(report)}\n`);
142
152
  }
143
153
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tdetectPython,\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n\ttype SystemToolStatus,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi already owns the install path (fff-node into\n * ~/.pi/agent/bin via tools-manager.ts's ensureFffNodePackage). The doctor\n * actually attempts the install when missing.\n * - \"system\": pi does not own the install (ripgrep, ollama, python). GUIDE\n * MODE only -- exact manual steps are reported, never executed, never\n * `curl | sh`, never `sudo`. This matches the harness's deliberate\n * no-silent-installer stance for tooling outside its own managed bin dir.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tdetectPython: () => SystemToolStatus;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tdetectPython: () => detectPython(),\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nconst PYTHON_GUIDE = [\n\t\"python3 was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install python3\",\n\t\" - Debian/Ubuntu: apt install python3\",\n\t\" - Arch: pacman -S python\",\n\t\" - Or download from https://www.python.org/downloads/\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkPython(deps: DoctorDeps): DoctorCheck {\n\tconst status = deps.detectPython();\n\tif (!status.present) {\n\t\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: false, guide: PYTHON_GUIDE };\n\t}\n\tconst detail = status.version ? `${status.command} (${status.version})` : status.command;\n\treturn { id: \"python\", label: \"Python (python3)\", kind: \"system\", present: true, detail };\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama] = await Promise.all([checkFffNode(deps, silent), checkOllama(deps)]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, checkPython(deps)] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor();\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
1
+ {"version":3,"file":"doctor.js","sourceRoot":"","sources":["../../src/core/doctor.ts"],"names":[],"mappings":"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","sourcesContent":["import chalk from \"chalk\";\nimport { getAgentDir } from \"../config.ts\";\nimport {\n\tensureFffNodePackage,\n\ttype FffInstallOutcome,\n\tgetLastFffInstallOutcome,\n\tgetToolPath,\n\tloadAvailableFffNodePackage,\n\tprobeVersion,\n} from \"../utils/tools-manager.ts\";\nimport { OllamaRuntime } from \"./models/local-runtime.ts\";\nimport { ensurePythonRuntime, type PythonRuntimeOutcome } from \"./python-runtime.ts\";\n\n/**\n * Environment doctor: verifies required tooling and installs what it safely\n * can, instead of leaving provisioning entirely to lazy first-use (the exact\n * gap behind \"ran `pi-adaptative update` on another machine and fff-node\n * never got installed\" -- see fff-lazy-install.test.ts/fff-search-tools.test.ts\n * for the underlying lazy-install fix; this module is the proactive half).\n *\n * Two tool kinds, two different postures:\n * - \"managed\": pi owns provisioning and attempts it when absent (fff-node,\n * plus pinned uv and the Python interpreter resolved through it).\n * - \"system\": pi does not own the install (ripgrep and ollama). GUIDE MODE\n * only -- exact manual steps are reported, never executed.\n */\n\nexport type DoctorToolKind = \"managed\" | \"system\";\n\nexport interface DoctorCheck {\n\t/** Stable identifier, e.g. \"fff-node\", \"ripgrep\", \"ollama\", \"python\". */\n\tid: string;\n\t/** Human-readable label for the report. */\n\tlabel: string;\n\tkind: DoctorToolKind;\n\tpresent: boolean;\n\t/** Version/path/status summary, present or not. */\n\tdetail?: string;\n\t/** Only set for a \"managed\" tool: whether an install was just attempted. */\n\tinstallAttempted?: boolean;\n\t/** Only set for a \"system\" tool that's missing: exact manual steps, never executed. */\n\tguide?: string[];\n}\n\nexport interface DoctorReport {\n\tchecks: DoctorCheck[];\n}\n\n/** Minimal slice of OllamaRuntime the doctor needs -- lets tests inject a fake without touching the real runtime. */\nexport type DoctorOllamaRuntime = Pick<OllamaRuntime, \"detect\" | \"installGuide\">;\n\nexport interface DoctorDeps {\n\tloadAvailableFffNodePackage: () => unknown | undefined;\n\tensureFffNodePackage: (silent?: boolean) => Promise<unknown | undefined>;\n\tgetLastFffInstallOutcome: () => FffInstallOutcome | undefined;\n\tgetRgPath: () => string | null;\n\tensurePythonRuntime: (options: { silent: boolean }) => Promise<PythonRuntimeOutcome>;\n\t/** Best-effort `<command> --version` probe; undefined if it can't be run. Used only to enrich a status line, never for presence detection. */\n\tprobeVersion: (command: string, versionArgs?: readonly string[]) => string | undefined;\n\tollamaRuntime: DoctorOllamaRuntime;\n}\n\nexport interface RunDoctorOptions {\n\t/**\n\t * Whether the fff-node managed install (if one is attempted) stays quiet.\n\t * Default true, matching runUpdatePreflight's existing background\n\t * behavior. The interactive `doctor` command passes `false` so a\n\t * multi-second install doesn't read as a silent hang.\n\t */\n\tsilent?: boolean;\n}\n\nconst realDoctorDeps: DoctorDeps = {\n\tloadAvailableFffNodePackage,\n\tensureFffNodePackage,\n\tgetLastFffInstallOutcome,\n\tgetRgPath: () => getToolPath(\"rg\"),\n\tensurePythonRuntime,\n\tprobeVersion,\n\tollamaRuntime: new OllamaRuntime({ agentDir: getAgentDir() }),\n};\n\nconst RIPGREP_GUIDE = [\n\t\"ripgrep (rg) was not found. Pi never runs installers itself -- manual steps (user-level, no sudo):\",\n\t\" - macOS (Homebrew): brew install ripgrep\",\n\t\" - Debian/Ubuntu: apt install ripgrep\",\n\t\" - Arch: pacman -S ripgrep\",\n\t\" - Or download a release: https://github.com/BurntSushi/ripgrep/releases\",\n];\n\nfunction describeFffOutcome(outcome: FffInstallOutcome | undefined): string {\n\tswitch (outcome?.status) {\n\t\tcase \"already-available\":\n\t\t\treturn \"already available\";\n\t\tcase \"installed\":\n\t\t\treturn \"installed just now\";\n\t\tcase \"offline\":\n\t\t\treturn \"offline mode enabled, skipped\";\n\t\tcase \"unsupported-platform\":\n\t\t\treturn \"no native build for this platform\";\n\t\tcase \"install-failed\":\n\t\t\treturn `install failed: ${outcome.reason}`;\n\t\tdefault:\n\t\t\treturn \"unavailable\";\n\t}\n}\n\n/** First line only -- a compiled-features/build-flags tail (e.g. rg --version) is noise in a one-line status report. */\nfunction firstLine(text: string | undefined): string | undefined {\n\treturn text?.split(\"\\n\")[0];\n}\n\n/** MANAGED tool: pi owns this install path, so the doctor actually attempts it when missing. */\nasync function checkFffNode(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\t// Delegate entirely to ensureFffNodePackage rather than re-deriving \"is it\n\t// already there\" here: that function (and its offline/unsupported-platform/\n\t// cooldown handling, see tools-manager.ts) is the single source of truth for\n\t// whether an install should even be attempted -- the doctor must not\n\t// duplicate or second-guess that logic, only report its outcome honestly.\n\tconst installed = await deps.ensureFffNodePackage(silent);\n\tconst outcome = deps.getLastFffInstallOutcome();\n\treturn {\n\t\tid: \"fff-node\",\n\t\tlabel: \"FFF native search (@ff-labs/fff-node)\",\n\t\tkind: \"managed\",\n\t\tpresent: Boolean(installed),\n\t\tinstallAttempted: outcome !== undefined && outcome.status !== \"already-available\",\n\t\tdetail: describeFffOutcome(outcome),\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor. */\nfunction checkRipgrep(deps: DoctorDeps): DoctorCheck {\n\tconst path = deps.getRgPath();\n\tif (!path) {\n\t\treturn { id: \"ripgrep\", label: \"ripgrep (rg)\", kind: \"system\", present: false, guide: RIPGREP_GUIDE };\n\t}\n\tconst version = firstLine(deps.probeVersion(path));\n\treturn {\n\t\tid: \"ripgrep\",\n\t\tlabel: \"ripgrep (rg)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: version ? `${path} (${version})` : path,\n\t};\n}\n\n/** SYSTEM tool: guide mode only, never installed by the doctor (OllamaRuntime.installGuide() is itself already guide-mode-only). */\nasync function checkOllama(deps: DoctorDeps): Promise<DoctorCheck> {\n\tconst status = await deps.ollamaRuntime.detect();\n\tif (!status.binaryPath) {\n\t\treturn {\n\t\t\tid: \"ollama\",\n\t\t\tlabel: \"Ollama (local model runtime)\",\n\t\t\tkind: \"system\",\n\t\t\tpresent: false,\n\t\t\tguide: deps.ollamaRuntime.installGuide(),\n\t\t};\n\t}\n\tconst version = firstLine(deps.probeVersion(status.binaryPath));\n\tconst versionSuffix = version ? `, ${version}` : \"\";\n\tconst activeStore = status.activeStore\n\t\t? `${status.activeStore.path} [${status.activeStore.kind}, ${status.activeStore.modelCount} model(s)]`\n\t\t: `none; pi-owned store ${status.ownedModelsDir} has ${status.ownedStore.modelCount} model(s)`;\n\treturn {\n\t\tid: \"ollama\",\n\t\tlabel: \"Ollama (local model runtime)\",\n\t\tkind: \"system\",\n\t\tpresent: true,\n\t\tdetail: `binary: ${status.binaryPath} [${status.binarySource}]${versionSuffix}; server: ${status.serverUp ? \"up\" : \"down\"} at ${status.serverUrl}; active store: ${activeStore}`,\n\t};\n}\n\n/** MANAGED tool: uv and Python share one deduplicated runtime manager with bounded provisioning. */\nasync function checkPython(deps: DoctorDeps, silent: boolean): Promise<DoctorCheck> {\n\tconst outcome = await deps.ensurePythonRuntime({ silent });\n\tif (outcome.status !== \"ready\") {\n\t\treturn {\n\t\t\tid: \"python\",\n\t\t\tlabel: \"Python (uv-managed)\",\n\t\t\tkind: \"managed\",\n\t\t\tpresent: false,\n\t\t\tdetail: outcome.reason,\n\t\t};\n\t}\n\treturn {\n\t\tid: \"python\",\n\t\tlabel: \"Python (uv-managed)\",\n\t\tkind: \"managed\",\n\t\tpresent: true,\n\t\tinstallAttempted: outcome.pythonInstalled,\n\t\tdetail: `uv: ${outcome.uvPath}; python: ${outcome.pythonPath}${outcome.pythonInstalled ? \" (installed just now)\" : \"\"}`,\n\t};\n}\n\nexport async function runDoctor(\n\tdeps: DoctorDeps = realDoctorDeps,\n\toptions: RunDoctorOptions = {},\n): Promise<DoctorReport> {\n\tconst silent = options.silent ?? true;\n\tconst [fffNode, ollama, python] = await Promise.all([\n\t\tcheckFffNode(deps, silent),\n\t\tcheckOllama(deps),\n\t\tcheckPython(deps, silent),\n\t]);\n\treturn { checks: [fffNode, checkRipgrep(deps), ollama, python] };\n}\n\nexport function formatDoctorReport(report: DoctorReport): string {\n\tconst lines: string[] = [];\n\tfor (const check of report.checks) {\n\t\tconst status = check.present ? chalk.green(\"[OK]\") : chalk.yellow(\"[MISSING]\");\n\t\tconst detail = check.detail ? ` -- ${check.detail}` : \"\";\n\t\tlines.push(`${status} ${check.label}${detail}`);\n\t\tif (!check.present && check.guide) {\n\t\t\tfor (const guideLine of check.guide) lines.push(` ${guideLine}`);\n\t\t}\n\t}\n\treturn lines.join(\"\\n\");\n}\n\n/**\n * Best-effort preflight meant to be called right after `pi-adaptative update`\n * succeeds. Must never fail the update itself: any error here is swallowed\n * and reported as a skipped check, not surfaced as an update failure.\n */\nexport async function runUpdatePreflight(deps: DoctorDeps = realDoctorDeps): Promise<void> {\n\ttry {\n\t\tconst report = await runDoctor(deps);\n\t\tconsole.log(`\\n${formatDoctorReport(report)}\\n`);\n\t} catch (error) {\n\t\tconsole.log(chalk.dim(`(environment check skipped: ${error instanceof Error ? error.message : String(error)})`));\n\t}\n}\n"]}
@@ -11,6 +11,8 @@ export interface ExecOptions {
11
11
  timeout?: number;
12
12
  /** Working directory */
13
13
  cwd?: string;
14
+ /** Environment for the child process. Defaults to process.env. */
15
+ env?: NodeJS.ProcessEnv;
14
16
  /**
15
17
  * Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).
16
18
  * Output is kept as a rolling tail; when exceeded, the oldest output is dropped
@@ -1 +1 @@
1
- {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAUH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,wCAAwC;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,IAAI,MAAM,CAAC;IACf,SAAS,IAAI,OAAO,CAAC;CACrB;AAED,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAyB/E;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CAmFrB","sourcesContent":["/**\n * Shared command execution utilities for extensions and custom tools.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { killTree } from \"@caupulican/pi-agent-core/node\";\nimport { waitForChildProcess } from \"../utils/child-process.ts\";\n\n/** Default per-stream retention for command output, in UTF-16 code units (~bytes for ASCII). */\nconst DEFAULT_EXEC_MAX_BUFFER = 16 * 1024 * 1024;\n\n/**\n * Options for executing shell commands.\n */\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the command */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Working directory */\n\tcwd?: string;\n\t/**\n\t * Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).\n\t * Output is kept as a rolling tail; when exceeded, the oldest output is dropped\n\t * and the matching truncation flag is set on the result. Defaults to 16 MiB.\n\t */\n\tmaxBuffer?: number;\n}\n\n/**\n * Result of executing a shell command.\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\tkilled: boolean;\n\t/** True when stdout exceeded maxBuffer and only its tail was retained. */\n\tstdoutTruncated: boolean;\n\t/** True when stderr exceeded maxBuffer and only its tail was retained. */\n\tstderrTruncated: boolean;\n\t/** Present when the process failed to spawn (e.g. ENOENT); code is 1 in that case. */\n\terrorMessage?: string;\n}\n\nexport interface RollingOutputBuffer {\n\tpush(chunk: string): void;\n\ttext(): string;\n\ttruncated(): boolean;\n}\n\n/** Bounded child-process output accumulator: keeps a rolling tail of at most maxUnits UTF-16 units. */\nexport function createRollingOutputBuffer(maxUnits: number): RollingOutputBuffer {\n\tconst chunks: string[] = [];\n\tlet units = 0;\n\tlet truncated = false;\n\treturn {\n\t\tpush(chunk: string): void {\n\t\t\tchunks.push(chunk);\n\t\t\tunits += chunk.length;\n\t\t\twhile (units > maxUnits && chunks.length > 1) {\n\t\t\t\tunits -= chunks.shift()?.length ?? 0;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tif (units > maxUnits) {\n\t\t\t\tchunks[0] = chunks[0].slice(-maxUnits);\n\t\t\t\tunits = chunks[0].length;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t},\n\t\ttext(): string {\n\t\t\treturn chunks.join(\"\");\n\t\t},\n\t\ttruncated(): boolean {\n\t\t\treturn truncated;\n\t\t},\n\t};\n}\n\n/**\n * Execute a shell command and return stdout/stderr/code.\n * Supports timeout and abort signal. Output retention per stream is bounded\n * (rolling tail, see ExecOptions.maxBuffer) so a chatty child process cannot\n * grow the host heap without bound.\n */\nexport async function execCommand(\n\tcommand: string,\n\targs: string[],\n\tcwd: string,\n\toptions?: ExecOptions,\n): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t});\n\n\t\tlet spawnError: string | undefined;\n\t\tproc.once(\"error\", (err) => {\n\t\t\tspawnError = err.message;\n\t\t});\n\n\t\tconst maxBuffer =\n\t\t\toptions?.maxBuffer !== undefined && options.maxBuffer > 0 ? options.maxBuffer : DEFAULT_EXEC_MAX_BUFFER;\n\t\tconst stdout = createRollingOutputBuffer(maxBuffer);\n\t\tconst stderr = createRollingOutputBuffer(maxBuffer);\n\t\tconst stdoutDecoder = new StringDecoder(\"utf8\");\n\t\tconst stderrDecoder = new StringDecoder(\"utf8\");\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed && proc.pid) {\n\t\t\t\tkilled = true;\n\t\t\t\tvoid killTree(proc.pid, { graceMs: 5000 });\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tstdout.push(stdoutDecoder.write(data));\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr.push(stderrDecoder.write(data));\n\t\t});\n\n\t\tconst settle = (code: number) => {\n\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\tstderr.push(stderrDecoder.end());\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout: stdout.text(),\n\t\t\t\tstderr: stderr.text(),\n\t\t\t\tcode,\n\t\t\t\tkilled,\n\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t...(spawnError !== undefined ? { errorMessage: spawnError } : {}),\n\t\t\t});\n\t\t};\n\n\t\t// Wait for process termination without hanging on inherited stdio handles\n\t\t// held open by detached descendants.\n\t\twaitForChildProcess(proc)\n\t\t\t.then((code) => settle(code ?? 0))\n\t\t\t.catch((err) => {\n\t\t\t\tif (spawnError === undefined) {\n\t\t\t\t\tspawnError = err instanceof Error ? err.message : String(err);\n\t\t\t\t}\n\t\t\t\tsettle(1);\n\t\t\t});\n\t});\n}\n"]}
1
+ {"version":3,"file":"exec.d.ts","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"AAAA;;GAEG;AAWH;;GAEG;AACH,MAAM,WAAW,WAAW;IAC3B,wCAAwC;IACxC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,8BAA8B;IAC9B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,kEAAkE;IAClE,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,OAAO,CAAC;IAChB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,eAAe,EAAE,OAAO,CAAC;IACzB,sFAAsF;IACtF,YAAY,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,mBAAmB;IACnC,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,IAAI,MAAM,CAAC;IACf,SAAS,IAAI,OAAO,CAAC;CACrB;AAED,uGAAuG;AACvG,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,MAAM,GAAG,mBAAmB,CAyB/E;AAED;;;;;GAKG;AACH,wBAAsB,WAAW,CAChC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CAmErB","sourcesContent":["/**\n * Shared command execution utilities for extensions and custom tools.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { waitForChildProcessWithTermination } from \"../utils/child-process.ts\";\n\n/** Default per-stream retention for command output, in UTF-16 code units (~bytes for ASCII). */\nconst DEFAULT_EXEC_MAX_BUFFER = 16 * 1024 * 1024;\nconst DEFAULT_EXEC_TIMEOUT_MS = 10 * 60_000;\nconst EXEC_KILL_GRACE_MS = 5_000;\n\n/**\n * Options for executing shell commands.\n */\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the command */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Working directory */\n\tcwd?: string;\n\t/** Environment for the child process. Defaults to process.env. */\n\tenv?: NodeJS.ProcessEnv;\n\t/**\n\t * Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).\n\t * Output is kept as a rolling tail; when exceeded, the oldest output is dropped\n\t * and the matching truncation flag is set on the result. Defaults to 16 MiB.\n\t */\n\tmaxBuffer?: number;\n}\n\n/**\n * Result of executing a shell command.\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\tkilled: boolean;\n\t/** True when stdout exceeded maxBuffer and only its tail was retained. */\n\tstdoutTruncated: boolean;\n\t/** True when stderr exceeded maxBuffer and only its tail was retained. */\n\tstderrTruncated: boolean;\n\t/** Present when the process failed to spawn (e.g. ENOENT); code is 1 in that case. */\n\terrorMessage?: string;\n}\n\nexport interface RollingOutputBuffer {\n\tpush(chunk: string): void;\n\ttext(): string;\n\ttruncated(): boolean;\n}\n\n/** Bounded child-process output accumulator: keeps a rolling tail of at most maxUnits UTF-16 units. */\nexport function createRollingOutputBuffer(maxUnits: number): RollingOutputBuffer {\n\tconst chunks: string[] = [];\n\tlet units = 0;\n\tlet truncated = false;\n\treturn {\n\t\tpush(chunk: string): void {\n\t\t\tchunks.push(chunk);\n\t\t\tunits += chunk.length;\n\t\t\twhile (units > maxUnits && chunks.length > 1) {\n\t\t\t\tunits -= chunks.shift()?.length ?? 0;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tif (units > maxUnits) {\n\t\t\t\tchunks[0] = chunks[0].slice(-maxUnits);\n\t\t\t\tunits = chunks[0].length;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t},\n\t\ttext(): string {\n\t\t\treturn chunks.join(\"\");\n\t\t},\n\t\ttruncated(): boolean {\n\t\t\treturn truncated;\n\t\t},\n\t};\n}\n\n/**\n * Execute a shell command and return stdout/stderr/code.\n * Supports timeout and abort signal. Output retention per stream is bounded\n * (rolling tail, see ExecOptions.maxBuffer) so a chatty child process cannot\n * grow the host heap without bound.\n */\nexport async function execCommand(\n\tcommand: string,\n\targs: string[],\n\tcwd: string,\n\toptions?: ExecOptions,\n): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tenv: options?.env,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t});\n\n\t\tlet spawnError: string | undefined;\n\t\tproc.once(\"error\", (err) => {\n\t\t\tspawnError = err.message;\n\t\t});\n\n\t\tconst maxBuffer =\n\t\t\toptions?.maxBuffer !== undefined && options.maxBuffer > 0 ? options.maxBuffer : DEFAULT_EXEC_MAX_BUFFER;\n\t\tconst stdout = createRollingOutputBuffer(maxBuffer);\n\t\tconst stderr = createRollingOutputBuffer(maxBuffer);\n\t\tconst stdoutDecoder = new StringDecoder(\"utf8\");\n\t\tconst stderrDecoder = new StringDecoder(\"utf8\");\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tstdout.push(stdoutDecoder.write(data));\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr.push(stderrDecoder.write(data));\n\t\t});\n\n\t\t// Settle from the child's terminal event or a bounded abort/timeout escalation.\n\t\t// This never waits for a second close event after termination has been acknowledged.\n\t\twaitForChildProcessWithTermination(proc, {\n\t\t\tsignal: options?.signal,\n\t\t\ttimeoutMs: options?.timeout && options.timeout > 0 ? options.timeout : DEFAULT_EXEC_TIMEOUT_MS,\n\t\t\tkillGraceMs: EXEC_KILL_GRACE_MS,\n\t\t})\n\t\t\t.then((terminal) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: terminal.code ?? 1,\n\t\t\t\t\tkilled: terminal.reason !== \"exited\",\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\t...(spawnError !== undefined ? { errorMessage: spawnError } : {}),\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tif (spawnError === undefined) {\n\t\t\t\t\tspawnError = err instanceof Error ? err.message : String(err);\n\t\t\t\t}\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: 1,\n\t\t\t\t\tkilled: false,\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\terrorMessage: spawnError,\n\t\t\t\t});\n\t\t\t});\n\t});\n}\n"]}
package/dist/core/exec.js CHANGED
@@ -3,10 +3,11 @@
3
3
  */
4
4
  import { spawn } from "node:child_process";
5
5
  import { StringDecoder } from "node:string_decoder";
6
- import { killTree } from "@caupulican/pi-agent-core/node";
7
- import { waitForChildProcess } from "../utils/child-process.js";
6
+ import { waitForChildProcessWithTermination } from "../utils/child-process.js";
8
7
  /** Default per-stream retention for command output, in UTF-16 code units (~bytes for ASCII). */
9
8
  const DEFAULT_EXEC_MAX_BUFFER = 16 * 1024 * 1024;
9
+ const DEFAULT_EXEC_TIMEOUT_MS = 10 * 60_000;
10
+ const EXEC_KILL_GRACE_MS = 5_000;
10
11
  /** Bounded child-process output accumulator: keeps a rolling tail of at most maxUnits UTF-16 units. */
11
12
  export function createRollingOutputBuffer(maxUnits) {
12
13
  const chunks = [];
@@ -44,6 +45,7 @@ export async function execCommand(command, args, cwd, options) {
44
45
  return new Promise((resolve) => {
45
46
  const proc = spawn(command, args, {
46
47
  cwd,
48
+ env: options?.env,
47
49
  shell: false,
48
50
  stdio: ["ignore", "pipe", "pipe"],
49
51
  detached: process.platform !== "win32",
@@ -57,62 +59,47 @@ export async function execCommand(command, args, cwd, options) {
57
59
  const stderr = createRollingOutputBuffer(maxBuffer);
58
60
  const stdoutDecoder = new StringDecoder("utf8");
59
61
  const stderrDecoder = new StringDecoder("utf8");
60
- let killed = false;
61
- let timeoutId;
62
- const killProcess = () => {
63
- if (!killed && proc.pid) {
64
- killed = true;
65
- void killTree(proc.pid, { graceMs: 5000 });
66
- }
67
- };
68
- // Handle abort signal
69
- if (options?.signal) {
70
- if (options.signal.aborted) {
71
- killProcess();
72
- }
73
- else {
74
- options.signal.addEventListener("abort", killProcess, { once: true });
75
- }
76
- }
77
- // Handle timeout
78
- if (options?.timeout && options.timeout > 0) {
79
- timeoutId = setTimeout(() => {
80
- killProcess();
81
- }, options.timeout);
82
- }
83
62
  proc.stdout?.on("data", (data) => {
84
63
  stdout.push(stdoutDecoder.write(data));
85
64
  });
86
65
  proc.stderr?.on("data", (data) => {
87
66
  stderr.push(stderrDecoder.write(data));
88
67
  });
89
- const settle = (code) => {
68
+ // Settle from the child's terminal event or a bounded abort/timeout escalation.
69
+ // This never waits for a second close event after termination has been acknowledged.
70
+ waitForChildProcessWithTermination(proc, {
71
+ signal: options?.signal,
72
+ timeoutMs: options?.timeout && options.timeout > 0 ? options.timeout : DEFAULT_EXEC_TIMEOUT_MS,
73
+ killGraceMs: EXEC_KILL_GRACE_MS,
74
+ })
75
+ .then((terminal) => {
90
76
  stdout.push(stdoutDecoder.end());
91
77
  stderr.push(stderrDecoder.end());
92
- if (timeoutId)
93
- clearTimeout(timeoutId);
94
- if (options?.signal) {
95
- options.signal.removeEventListener("abort", killProcess);
96
- }
97
78
  resolve({
98
79
  stdout: stdout.text(),
99
80
  stderr: stderr.text(),
100
- code,
101
- killed,
81
+ code: terminal.code ?? 1,
82
+ killed: terminal.reason !== "exited",
102
83
  stdoutTruncated: stdout.truncated(),
103
84
  stderrTruncated: stderr.truncated(),
104
85
  ...(spawnError !== undefined ? { errorMessage: spawnError } : {}),
105
86
  });
106
- };
107
- // Wait for process termination without hanging on inherited stdio handles
108
- // held open by detached descendants.
109
- waitForChildProcess(proc)
110
- .then((code) => settle(code ?? 0))
87
+ })
111
88
  .catch((err) => {
89
+ stdout.push(stdoutDecoder.end());
90
+ stderr.push(stderrDecoder.end());
112
91
  if (spawnError === undefined) {
113
92
  spawnError = err instanceof Error ? err.message : String(err);
114
93
  }
115
- settle(1);
94
+ resolve({
95
+ stdout: stdout.text(),
96
+ stderr: stderr.text(),
97
+ code: 1,
98
+ killed: false,
99
+ stdoutTruncated: stdout.truncated(),
100
+ stderrTruncated: stderr.truncated(),
101
+ errorMessage: spawnError,
102
+ });
116
103
  });
117
104
  });
118
105
  }
@@ -1 +1 @@
1
- {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared command execution utilities for extensions and custom tools.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { killTree } from \"@caupulican/pi-agent-core/node\";\nimport { waitForChildProcess } from \"../utils/child-process.ts\";\n\n/** Default per-stream retention for command output, in UTF-16 code units (~bytes for ASCII). */\nconst DEFAULT_EXEC_MAX_BUFFER = 16 * 1024 * 1024;\n\n/**\n * Options for executing shell commands.\n */\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the command */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Working directory */\n\tcwd?: string;\n\t/**\n\t * Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).\n\t * Output is kept as a rolling tail; when exceeded, the oldest output is dropped\n\t * and the matching truncation flag is set on the result. Defaults to 16 MiB.\n\t */\n\tmaxBuffer?: number;\n}\n\n/**\n * Result of executing a shell command.\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\tkilled: boolean;\n\t/** True when stdout exceeded maxBuffer and only its tail was retained. */\n\tstdoutTruncated: boolean;\n\t/** True when stderr exceeded maxBuffer and only its tail was retained. */\n\tstderrTruncated: boolean;\n\t/** Present when the process failed to spawn (e.g. ENOENT); code is 1 in that case. */\n\terrorMessage?: string;\n}\n\nexport interface RollingOutputBuffer {\n\tpush(chunk: string): void;\n\ttext(): string;\n\ttruncated(): boolean;\n}\n\n/** Bounded child-process output accumulator: keeps a rolling tail of at most maxUnits UTF-16 units. */\nexport function createRollingOutputBuffer(maxUnits: number): RollingOutputBuffer {\n\tconst chunks: string[] = [];\n\tlet units = 0;\n\tlet truncated = false;\n\treturn {\n\t\tpush(chunk: string): void {\n\t\t\tchunks.push(chunk);\n\t\t\tunits += chunk.length;\n\t\t\twhile (units > maxUnits && chunks.length > 1) {\n\t\t\t\tunits -= chunks.shift()?.length ?? 0;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tif (units > maxUnits) {\n\t\t\t\tchunks[0] = chunks[0].slice(-maxUnits);\n\t\t\t\tunits = chunks[0].length;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t},\n\t\ttext(): string {\n\t\t\treturn chunks.join(\"\");\n\t\t},\n\t\ttruncated(): boolean {\n\t\t\treturn truncated;\n\t\t},\n\t};\n}\n\n/**\n * Execute a shell command and return stdout/stderr/code.\n * Supports timeout and abort signal. Output retention per stream is bounded\n * (rolling tail, see ExecOptions.maxBuffer) so a chatty child process cannot\n * grow the host heap without bound.\n */\nexport async function execCommand(\n\tcommand: string,\n\targs: string[],\n\tcwd: string,\n\toptions?: ExecOptions,\n): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t});\n\n\t\tlet spawnError: string | undefined;\n\t\tproc.once(\"error\", (err) => {\n\t\t\tspawnError = err.message;\n\t\t});\n\n\t\tconst maxBuffer =\n\t\t\toptions?.maxBuffer !== undefined && options.maxBuffer > 0 ? options.maxBuffer : DEFAULT_EXEC_MAX_BUFFER;\n\t\tconst stdout = createRollingOutputBuffer(maxBuffer);\n\t\tconst stderr = createRollingOutputBuffer(maxBuffer);\n\t\tconst stdoutDecoder = new StringDecoder(\"utf8\");\n\t\tconst stderrDecoder = new StringDecoder(\"utf8\");\n\t\tlet killed = false;\n\t\tlet timeoutId: NodeJS.Timeout | undefined;\n\n\t\tconst killProcess = () => {\n\t\t\tif (!killed && proc.pid) {\n\t\t\t\tkilled = true;\n\t\t\t\tvoid killTree(proc.pid, { graceMs: 5000 });\n\t\t\t}\n\t\t};\n\n\t\t// Handle abort signal\n\t\tif (options?.signal) {\n\t\t\tif (options.signal.aborted) {\n\t\t\t\tkillProcess();\n\t\t\t} else {\n\t\t\t\toptions.signal.addEventListener(\"abort\", killProcess, { once: true });\n\t\t\t}\n\t\t}\n\n\t\t// Handle timeout\n\t\tif (options?.timeout && options.timeout > 0) {\n\t\t\ttimeoutId = setTimeout(() => {\n\t\t\t\tkillProcess();\n\t\t\t}, options.timeout);\n\t\t}\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tstdout.push(stdoutDecoder.write(data));\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr.push(stderrDecoder.write(data));\n\t\t});\n\n\t\tconst settle = (code: number) => {\n\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\tstderr.push(stderrDecoder.end());\n\t\t\tif (timeoutId) clearTimeout(timeoutId);\n\t\t\tif (options?.signal) {\n\t\t\t\toptions.signal.removeEventListener(\"abort\", killProcess);\n\t\t\t}\n\t\t\tresolve({\n\t\t\t\tstdout: stdout.text(),\n\t\t\t\tstderr: stderr.text(),\n\t\t\t\tcode,\n\t\t\t\tkilled,\n\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t...(spawnError !== undefined ? { errorMessage: spawnError } : {}),\n\t\t\t});\n\t\t};\n\n\t\t// Wait for process termination without hanging on inherited stdio handles\n\t\t// held open by detached descendants.\n\t\twaitForChildProcess(proc)\n\t\t\t.then((code) => settle(code ?? 0))\n\t\t\t.catch((err) => {\n\t\t\t\tif (spawnError === undefined) {\n\t\t\t\t\tspawnError = err instanceof Error ? err.message : String(err);\n\t\t\t\t}\n\t\t\t\tsettle(1);\n\t\t\t});\n\t});\n}\n"]}
1
+ {"version":3,"file":"exec.js","sourceRoot":"","sources":["../../src/core/exec.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared command execution utilities for extensions and custom tools.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport { waitForChildProcessWithTermination } from \"../utils/child-process.ts\";\n\n/** Default per-stream retention for command output, in UTF-16 code units (~bytes for ASCII). */\nconst DEFAULT_EXEC_MAX_BUFFER = 16 * 1024 * 1024;\nconst DEFAULT_EXEC_TIMEOUT_MS = 10 * 60_000;\nconst EXEC_KILL_GRACE_MS = 5_000;\n\n/**\n * Options for executing shell commands.\n */\nexport interface ExecOptions {\n\t/** AbortSignal to cancel the command */\n\tsignal?: AbortSignal;\n\t/** Timeout in milliseconds */\n\ttimeout?: number;\n\t/** Working directory */\n\tcwd?: string;\n\t/** Environment for the child process. Defaults to process.env. */\n\tenv?: NodeJS.ProcessEnv;\n\t/**\n\t * Maximum output retained per stream, in UTF-16 code units (~bytes for ASCII).\n\t * Output is kept as a rolling tail; when exceeded, the oldest output is dropped\n\t * and the matching truncation flag is set on the result. Defaults to 16 MiB.\n\t */\n\tmaxBuffer?: number;\n}\n\n/**\n * Result of executing a shell command.\n */\nexport interface ExecResult {\n\tstdout: string;\n\tstderr: string;\n\tcode: number;\n\tkilled: boolean;\n\t/** True when stdout exceeded maxBuffer and only its tail was retained. */\n\tstdoutTruncated: boolean;\n\t/** True when stderr exceeded maxBuffer and only its tail was retained. */\n\tstderrTruncated: boolean;\n\t/** Present when the process failed to spawn (e.g. ENOENT); code is 1 in that case. */\n\terrorMessage?: string;\n}\n\nexport interface RollingOutputBuffer {\n\tpush(chunk: string): void;\n\ttext(): string;\n\ttruncated(): boolean;\n}\n\n/** Bounded child-process output accumulator: keeps a rolling tail of at most maxUnits UTF-16 units. */\nexport function createRollingOutputBuffer(maxUnits: number): RollingOutputBuffer {\n\tconst chunks: string[] = [];\n\tlet units = 0;\n\tlet truncated = false;\n\treturn {\n\t\tpush(chunk: string): void {\n\t\t\tchunks.push(chunk);\n\t\t\tunits += chunk.length;\n\t\t\twhile (units > maxUnits && chunks.length > 1) {\n\t\t\t\tunits -= chunks.shift()?.length ?? 0;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t\tif (units > maxUnits) {\n\t\t\t\tchunks[0] = chunks[0].slice(-maxUnits);\n\t\t\t\tunits = chunks[0].length;\n\t\t\t\ttruncated = true;\n\t\t\t}\n\t\t},\n\t\ttext(): string {\n\t\t\treturn chunks.join(\"\");\n\t\t},\n\t\ttruncated(): boolean {\n\t\t\treturn truncated;\n\t\t},\n\t};\n}\n\n/**\n * Execute a shell command and return stdout/stderr/code.\n * Supports timeout and abort signal. Output retention per stream is bounded\n * (rolling tail, see ExecOptions.maxBuffer) so a chatty child process cannot\n * grow the host heap without bound.\n */\nexport async function execCommand(\n\tcommand: string,\n\targs: string[],\n\tcwd: string,\n\toptions?: ExecOptions,\n): Promise<ExecResult> {\n\treturn new Promise((resolve) => {\n\t\tconst proc = spawn(command, args, {\n\t\t\tcwd,\n\t\t\tenv: options?.env,\n\t\t\tshell: false,\n\t\t\tstdio: [\"ignore\", \"pipe\", \"pipe\"],\n\t\t\tdetached: process.platform !== \"win32\",\n\t\t});\n\n\t\tlet spawnError: string | undefined;\n\t\tproc.once(\"error\", (err) => {\n\t\t\tspawnError = err.message;\n\t\t});\n\n\t\tconst maxBuffer =\n\t\t\toptions?.maxBuffer !== undefined && options.maxBuffer > 0 ? options.maxBuffer : DEFAULT_EXEC_MAX_BUFFER;\n\t\tconst stdout = createRollingOutputBuffer(maxBuffer);\n\t\tconst stderr = createRollingOutputBuffer(maxBuffer);\n\t\tconst stdoutDecoder = new StringDecoder(\"utf8\");\n\t\tconst stderrDecoder = new StringDecoder(\"utf8\");\n\n\t\tproc.stdout?.on(\"data\", (data: Buffer) => {\n\t\t\tstdout.push(stdoutDecoder.write(data));\n\t\t});\n\n\t\tproc.stderr?.on(\"data\", (data: Buffer) => {\n\t\t\tstderr.push(stderrDecoder.write(data));\n\t\t});\n\n\t\t// Settle from the child's terminal event or a bounded abort/timeout escalation.\n\t\t// This never waits for a second close event after termination has been acknowledged.\n\t\twaitForChildProcessWithTermination(proc, {\n\t\t\tsignal: options?.signal,\n\t\t\ttimeoutMs: options?.timeout && options.timeout > 0 ? options.timeout : DEFAULT_EXEC_TIMEOUT_MS,\n\t\t\tkillGraceMs: EXEC_KILL_GRACE_MS,\n\t\t})\n\t\t\t.then((terminal) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: terminal.code ?? 1,\n\t\t\t\t\tkilled: terminal.reason !== \"exited\",\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\t...(spawnError !== undefined ? { errorMessage: spawnError } : {}),\n\t\t\t\t});\n\t\t\t})\n\t\t\t.catch((err) => {\n\t\t\t\tstdout.push(stdoutDecoder.end());\n\t\t\t\tstderr.push(stderrDecoder.end());\n\t\t\t\tif (spawnError === undefined) {\n\t\t\t\t\tspawnError = err instanceof Error ? err.message : String(err);\n\t\t\t\t}\n\t\t\t\tresolve({\n\t\t\t\t\tstdout: stdout.text(),\n\t\t\t\t\tstderr: stderr.text(),\n\t\t\t\t\tcode: 1,\n\t\t\t\t\tkilled: false,\n\t\t\t\t\tstdoutTruncated: stdout.truncated(),\n\t\t\t\t\tstderrTruncated: stderr.truncated(),\n\t\t\t\t\terrorMessage: spawnError,\n\t\t\t\t});\n\t\t\t});\n\t});\n}\n"]}
@@ -15,9 +15,14 @@ export declare function createExtensionRuntime(): ExtensionRuntime;
15
15
  * previous generation's handlers subscribed, pinning the old module graph in memory
16
16
  * and double-processing events.
17
17
  */
18
- export declare function disposeExtensionEventSubscriptions(extensions: Extension[]): Promise<void>;
18
+ export declare function disposeExtensionEventSubscriptions(extensions: Extension[], options?: {
19
+ deactivate?: boolean;
20
+ timeoutMs?: number;
21
+ }): Promise<void>;
19
22
  export declare function loadExtension(extensionPath: string, cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, opts?: {
20
23
  fresh?: boolean;
24
+ factoryTimeoutMs?: number;
25
+ moduleTimeoutMs?: number;
21
26
  }): Promise<{
22
27
  extension: Extension | null;
23
28
  error: string | null;
@@ -25,7 +30,9 @@ export declare function loadExtension(extensionPath: string, cwd: string, eventB
25
30
  /**
26
31
  * Create an Extension from an inline factory function.
27
32
  */
28
- export declare function loadExtensionFromFactory(factory: ExtensionFactory, cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, extensionPath?: string): Promise<Extension>;
33
+ export declare function loadExtensionFromFactory(factory: ExtensionFactory, cwd: string, eventBus: EventBus, runtime: ExtensionRuntime, extensionPath?: string, options?: {
34
+ factoryTimeoutMs?: number;
35
+ }): Promise<Extension>;
29
36
  /**
30
37
  * Load extensions from paths.
31
38
  */