@xyit/wellspring 0.0.0-stage → 1.1.0

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 (464) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +853 -2
  3. package/dist/agent/pending-asks.d.ts +20 -0
  4. package/dist/agent/pending-asks.js +70 -0
  5. package/dist/agent/pending-asks.js.map +1 -0
  6. package/dist/agent/task-registry.d.ts +14 -0
  7. package/dist/agent/task-registry.js +80 -0
  8. package/dist/agent/task-registry.js.map +1 -0
  9. package/dist/agent/tool-context.d.ts +30 -0
  10. package/dist/agent/tool-context.js +10 -0
  11. package/dist/agent/tool-context.js.map +1 -0
  12. package/dist/agent/usage.d.ts +40 -0
  13. package/dist/agent/usage.js +122 -0
  14. package/dist/agent/usage.js.map +1 -0
  15. package/dist/api/host/agent.d.ts +165 -0
  16. package/dist/api/host/agent.js +5 -0
  17. package/dist/api/host/agent.js.map +1 -0
  18. package/dist/api/host/events.d.ts +2 -0
  19. package/dist/api/host/events.js +6 -0
  20. package/dist/api/host/events.js.map +1 -0
  21. package/dist/api/host/exec.d.ts +36 -0
  22. package/dist/api/host/exec.js +10 -0
  23. package/dist/api/host/exec.js.map +1 -0
  24. package/dist/api/host/fs.d.ts +24 -0
  25. package/dist/api/host/fs.js +7 -0
  26. package/dist/api/host/fs.js.map +1 -0
  27. package/dist/api/host/hooks.d.ts +210 -0
  28. package/dist/api/host/hooks.js +401 -0
  29. package/dist/api/host/hooks.js.map +1 -0
  30. package/dist/api/host/host.d.ts +36 -0
  31. package/dist/api/host/host.js +53 -0
  32. package/dist/api/host/host.js.map +1 -0
  33. package/dist/api/host/http.d.ts +58 -0
  34. package/dist/api/host/http.js +6 -0
  35. package/dist/api/host/http.js.map +1 -0
  36. package/dist/api/host/index.d.ts +15 -0
  37. package/dist/api/host/index.js +20 -0
  38. package/dist/api/host/index.js.map +1 -0
  39. package/dist/api/host/models.d.ts +65 -0
  40. package/dist/api/host/models.js +8 -0
  41. package/dist/api/host/models.js.map +1 -0
  42. package/dist/api/host/network.d.ts +22 -0
  43. package/dist/api/host/network.js +7 -0
  44. package/dist/api/host/network.js.map +1 -0
  45. package/dist/api/host/plugin.d.ts +138 -0
  46. package/dist/api/host/plugin.js +6 -0
  47. package/dist/api/host/plugin.js.map +1 -0
  48. package/dist/api/host/plugins.d.ts +49 -0
  49. package/dist/api/host/plugins.js +6 -0
  50. package/dist/api/host/plugins.js.map +1 -0
  51. package/dist/api/host/pty.d.ts +55 -0
  52. package/dist/api/host/pty.js +21 -0
  53. package/dist/api/host/pty.js.map +1 -0
  54. package/dist/api/host/session.d.ts +5 -0
  55. package/dist/api/host/session.js +52 -0
  56. package/dist/api/host/session.js.map +1 -0
  57. package/dist/api/host/version.d.ts +2 -0
  58. package/dist/api/host/version.js +3 -0
  59. package/dist/api/host/version.js.map +1 -0
  60. package/dist/api/host/workspace.d.ts +38 -0
  61. package/dist/api/host/workspace.js +9 -0
  62. package/dist/api/host/workspace.js.map +1 -0
  63. package/dist/api/plugin/http-client.d.ts +42 -0
  64. package/dist/api/plugin/http-client.js +364 -0
  65. package/dist/api/plugin/http-client.js.map +1 -0
  66. package/dist/api/plugin/http-protocol.d.ts +152 -0
  67. package/dist/api/plugin/http-protocol.js +49 -0
  68. package/dist/api/plugin/http-protocol.js.map +1 -0
  69. package/dist/api/result/result.d.ts +16 -0
  70. package/dist/api/result/result.js +43 -0
  71. package/dist/api/result/result.js.map +1 -0
  72. package/dist/commands/agent-turn.d.ts +63 -0
  73. package/dist/commands/agent-turn.js +687 -0
  74. package/dist/commands/agent-turn.js.map +1 -0
  75. package/dist/commands/ask-prompt.d.ts +6 -0
  76. package/dist/commands/ask-prompt.js +49 -0
  77. package/dist/commands/ask-prompt.js.map +1 -0
  78. package/dist/commands/config.d.ts +2 -0
  79. package/dist/commands/config.js +283 -0
  80. package/dist/commands/config.js.map +1 -0
  81. package/dist/commands/conversation.d.ts +51 -0
  82. package/dist/commands/conversation.js +630 -0
  83. package/dist/commands/conversation.js.map +1 -0
  84. package/dist/commands/doctor.d.ts +2 -0
  85. package/dist/commands/doctor.js +77 -0
  86. package/dist/commands/doctor.js.map +1 -0
  87. package/dist/commands/exec-approval.d.ts +3 -0
  88. package/dist/commands/exec-approval.js +80 -0
  89. package/dist/commands/exec-approval.js.map +1 -0
  90. package/dist/commands/help.d.ts +2 -0
  91. package/dist/commands/help.js +35 -0
  92. package/dist/commands/help.js.map +1 -0
  93. package/dist/commands/key.d.ts +2 -0
  94. package/dist/commands/key.js +226 -0
  95. package/dist/commands/key.js.map +1 -0
  96. package/dist/commands/plugin.d.ts +2 -0
  97. package/dist/commands/plugin.js +190 -0
  98. package/dist/commands/plugin.js.map +1 -0
  99. package/dist/commands/session.d.ts +2 -0
  100. package/dist/commands/session.js +167 -0
  101. package/dist/commands/session.js.map +1 -0
  102. package/dist/commands/shared.d.ts +35 -0
  103. package/dist/commands/shared.js +66 -0
  104. package/dist/commands/shared.js.map +1 -0
  105. package/dist/commands/start.d.ts +2 -0
  106. package/dist/commands/start.js +75 -0
  107. package/dist/commands/start.js.map +1 -0
  108. package/dist/commands/stop.d.ts +2 -0
  109. package/dist/commands/stop.js +24 -0
  110. package/dist/commands/stop.js.map +1 -0
  111. package/dist/commands/version.d.ts +2 -0
  112. package/dist/commands/version.js +33 -0
  113. package/dist/commands/version.js.map +1 -0
  114. package/dist/config/codes.d.ts +13 -0
  115. package/dist/config/codes.js +15 -0
  116. package/dist/config/codes.js.map +1 -0
  117. package/dist/config/credentials.d.ts +67 -0
  118. package/dist/config/credentials.js +214 -0
  119. package/dist/config/credentials.js.map +1 -0
  120. package/dist/config/defaults.d.ts +5 -0
  121. package/dist/config/defaults.js +31 -0
  122. package/dist/config/defaults.js.map +1 -0
  123. package/dist/config/index.d.ts +5 -0
  124. package/dist/config/index.js +10 -0
  125. package/dist/config/index.js.map +1 -0
  126. package/dist/config/loader.d.ts +10 -0
  127. package/dist/config/loader.js +68 -0
  128. package/dist/config/loader.js.map +1 -0
  129. package/dist/config/providers.d.ts +30 -0
  130. package/dist/config/providers.js +97 -0
  131. package/dist/config/providers.js.map +1 -0
  132. package/dist/config/types.d.ts +75 -0
  133. package/dist/config/types.js +4 -0
  134. package/dist/config/types.js.map +1 -0
  135. package/dist/config/validate.d.ts +4 -0
  136. package/dist/config/validate.js +391 -0
  137. package/dist/config/validate.js.map +1 -0
  138. package/dist/display/markdown.d.ts +7 -0
  139. package/dist/display/markdown.js +116 -0
  140. package/dist/display/markdown.js.map +1 -0
  141. package/dist/display/tool-label.d.ts +6 -0
  142. package/dist/display/tool-label.js +81 -0
  143. package/dist/display/tool-label.js.map +1 -0
  144. package/dist/event/event.d.ts +27 -0
  145. package/dist/event/event.js +152 -0
  146. package/dist/event/event.js.map +1 -0
  147. package/dist/index.d.ts +2 -0
  148. package/dist/index.js +308 -0
  149. package/dist/index.js.map +1 -0
  150. package/dist/input/codes.d.ts +15 -0
  151. package/dist/input/codes.js +20 -0
  152. package/dist/input/codes.js.map +1 -0
  153. package/dist/input/commands.d.ts +10 -0
  154. package/dist/input/commands.js +65 -0
  155. package/dist/input/commands.js.map +1 -0
  156. package/dist/input/help.d.ts +8 -0
  157. package/dist/input/help.js +51 -0
  158. package/dist/input/help.js.map +1 -0
  159. package/dist/input/index.d.ts +6 -0
  160. package/dist/input/index.js +10 -0
  161. package/dist/input/index.js.map +1 -0
  162. package/dist/input/parse-options.d.ts +11 -0
  163. package/dist/input/parse-options.js +51 -0
  164. package/dist/input/parse-options.js.map +1 -0
  165. package/dist/input/parse.d.ts +3 -0
  166. package/dist/input/parse.js +119 -0
  167. package/dist/input/parse.js.map +1 -0
  168. package/dist/input/types.d.ts +17 -0
  169. package/dist/input/types.js +2 -0
  170. package/dist/input/types.js.map +1 -0
  171. package/dist/logger/elog4js.d.ts +65 -0
  172. package/dist/logger/elog4js.js +305 -0
  173. package/dist/logger/elog4js.js.map +1 -0
  174. package/dist/logger/logger.d.ts +37 -0
  175. package/dist/logger/logger.js +196 -0
  176. package/dist/logger/logger.js.map +1 -0
  177. package/dist/network/index.d.ts +2 -0
  178. package/dist/network/index.js +6 -0
  179. package/dist/network/index.js.map +1 -0
  180. package/dist/network/openai.d.ts +117 -0
  181. package/dist/network/openai.js +443 -0
  182. package/dist/network/openai.js.map +1 -0
  183. package/dist/network/proxy.d.ts +9 -0
  184. package/dist/network/proxy.js +72 -0
  185. package/dist/network/proxy.js.map +1 -0
  186. package/dist/network/web.d.ts +34 -0
  187. package/dist/network/web.js +152 -0
  188. package/dist/network/web.js.map +1 -0
  189. package/dist/platform/clipboard.d.ts +15 -0
  190. package/dist/platform/clipboard.js +82 -0
  191. package/dist/platform/clipboard.js.map +1 -0
  192. package/dist/platform/exec.d.ts +28 -0
  193. package/dist/platform/exec.js +95 -0
  194. package/dist/platform/exec.js.map +1 -0
  195. package/dist/platform/file.d.ts +38 -0
  196. package/dist/platform/file.js +197 -0
  197. package/dist/platform/file.js.map +1 -0
  198. package/dist/platform/index.d.ts +7 -0
  199. package/dist/platform/index.js +12 -0
  200. package/dist/platform/index.js.map +1 -0
  201. package/dist/platform/notification.d.ts +14 -0
  202. package/dist/platform/notification.js +82 -0
  203. package/dist/platform/notification.js.map +1 -0
  204. package/dist/platform/open-external.d.ts +10 -0
  205. package/dist/platform/open-external.js +105 -0
  206. package/dist/platform/open-external.js.map +1 -0
  207. package/dist/platform/os.d.ts +20 -0
  208. package/dist/platform/os.js +86 -0
  209. package/dist/platform/os.js.map +1 -0
  210. package/dist/platform/path.d.ts +32 -0
  211. package/dist/platform/path.js +154 -0
  212. package/dist/platform/path.js.map +1 -0
  213. package/dist/plugin/access-guard.d.ts +1 -0
  214. package/dist/plugin/access-guard.js +40 -0
  215. package/dist/plugin/access-guard.js.map +1 -0
  216. package/dist/plugin/agent-service.d.ts +25 -0
  217. package/dist/plugin/agent-service.js +809 -0
  218. package/dist/plugin/agent-service.js.map +1 -0
  219. package/dist/plugin/agent-tools.d.ts +12 -0
  220. package/dist/plugin/agent-tools.js +31 -0
  221. package/dist/plugin/agent-tools.js.map +1 -0
  222. package/dist/plugin/capabilities.d.ts +26 -0
  223. package/dist/plugin/capabilities.js +192 -0
  224. package/dist/plugin/capabilities.js.map +1 -0
  225. package/dist/plugin/console-guard.d.ts +2 -0
  226. package/dist/plugin/console-guard.js +52 -0
  227. package/dist/plugin/console-guard.js.map +1 -0
  228. package/dist/plugin/data-access.d.ts +15 -0
  229. package/dist/plugin/data-access.js +57 -0
  230. package/dist/plugin/data-access.js.map +1 -0
  231. package/dist/plugin/fs.d.ts +3 -0
  232. package/dist/plugin/fs.js +93 -0
  233. package/dist/plugin/fs.js.map +1 -0
  234. package/dist/plugin/host-alias.d.ts +2 -0
  235. package/dist/plugin/host-alias.js +57 -0
  236. package/dist/plugin/host-alias.js.map +1 -0
  237. package/dist/plugin/index.d.ts +7 -0
  238. package/dist/plugin/index.js +11 -0
  239. package/dist/plugin/index.js.map +1 -0
  240. package/dist/plugin/install.d.ts +17 -0
  241. package/dist/plugin/install.js +163 -0
  242. package/dist/plugin/install.js.map +1 -0
  243. package/dist/plugin/loader.d.ts +45 -0
  244. package/dist/plugin/loader.js +739 -0
  245. package/dist/plugin/loader.js.map +1 -0
  246. package/dist/plugin/manage.d.ts +31 -0
  247. package/dist/plugin/manage.js +271 -0
  248. package/dist/plugin/manage.js.map +1 -0
  249. package/dist/plugin/models-service.d.ts +15 -0
  250. package/dist/plugin/models-service.js +317 -0
  251. package/dist/plugin/models-service.js.map +1 -0
  252. package/dist/plugin/network-service.d.ts +10 -0
  253. package/dist/plugin/network-service.js +87 -0
  254. package/dist/plugin/network-service.js.map +1 -0
  255. package/dist/plugin/package-manager.d.ts +46 -0
  256. package/dist/plugin/package-manager.js +148 -0
  257. package/dist/plugin/package-manager.js.map +1 -0
  258. package/dist/plugin/paths.d.ts +6 -0
  259. package/dist/plugin/paths.js +15 -0
  260. package/dist/plugin/paths.js.map +1 -0
  261. package/dist/plugin/registry.d.ts +17 -0
  262. package/dist/plugin/registry.js +82 -0
  263. package/dist/plugin/registry.js.map +1 -0
  264. package/dist/plugin/runtime.d.ts +9 -0
  265. package/dist/plugin/runtime.js +43 -0
  266. package/dist/plugin/runtime.js.map +1 -0
  267. package/dist/plugin/scaffold.d.ts +13 -0
  268. package/dist/plugin/scaffold.js +132 -0
  269. package/dist/plugin/scaffold.js.map +1 -0
  270. package/dist/plugin/state.d.ts +6 -0
  271. package/dist/plugin/state.js +38 -0
  272. package/dist/plugin/state.js.map +1 -0
  273. package/dist/plugin/zip.d.ts +7 -0
  274. package/dist/plugin/zip.js +138 -0
  275. package/dist/plugin/zip.js.map +1 -0
  276. package/dist/sandbox/audit.d.ts +12 -0
  277. package/dist/sandbox/audit.js +20 -0
  278. package/dist/sandbox/audit.js.map +1 -0
  279. package/dist/sandbox/backends/bwrap.d.ts +28 -0
  280. package/dist/sandbox/backends/bwrap.js +197 -0
  281. package/dist/sandbox/backends/bwrap.js.map +1 -0
  282. package/dist/sandbox/backends/direct.d.ts +7 -0
  283. package/dist/sandbox/backends/direct.js +32 -0
  284. package/dist/sandbox/backends/direct.js.map +1 -0
  285. package/dist/sandbox/bridge-state.d.ts +13 -0
  286. package/dist/sandbox/bridge-state.js +12 -0
  287. package/dist/sandbox/bridge-state.js.map +1 -0
  288. package/dist/sandbox/index.d.ts +5 -0
  289. package/dist/sandbox/index.js +6 -0
  290. package/dist/sandbox/index.js.map +1 -0
  291. package/dist/sandbox/manager.d.ts +8 -0
  292. package/dist/sandbox/manager.js +325 -0
  293. package/dist/sandbox/manager.js.map +1 -0
  294. package/dist/sandbox/memory-limit.d.ts +44 -0
  295. package/dist/sandbox/memory-limit.js +193 -0
  296. package/dist/sandbox/memory-limit.js.map +1 -0
  297. package/dist/sandbox/plugin-client.d.ts +80 -0
  298. package/dist/sandbox/plugin-client.js +1601 -0
  299. package/dist/sandbox/plugin-client.js.map +1 -0
  300. package/dist/sandbox/plugin-host.d.ts +1 -0
  301. package/dist/sandbox/plugin-host.js +1001 -0
  302. package/dist/sandbox/plugin-host.js.map +1 -0
  303. package/dist/sandbox/protocol.d.ts +43 -0
  304. package/dist/sandbox/protocol.js +96 -0
  305. package/dist/sandbox/protocol.js.map +1 -0
  306. package/dist/sandbox/rpc-timeouts.d.ts +28 -0
  307. package/dist/sandbox/rpc-timeouts.js +15 -0
  308. package/dist/sandbox/rpc-timeouts.js.map +1 -0
  309. package/dist/sandbox/supervisor.d.ts +31 -0
  310. package/dist/sandbox/supervisor.js +114 -0
  311. package/dist/sandbox/supervisor.js.map +1 -0
  312. package/dist/sandbox/types.d.ts +124 -0
  313. package/dist/sandbox/types.js +17 -0
  314. package/dist/sandbox/types.js.map +1 -0
  315. package/dist/service/control.d.ts +32 -0
  316. package/dist/service/control.js +117 -0
  317. package/dist/service/control.js.map +1 -0
  318. package/dist/session/compress.d.ts +3 -0
  319. package/dist/session/compress.js +42 -0
  320. package/dist/session/compress.js.map +1 -0
  321. package/dist/session/index.d.ts +4 -0
  322. package/dist/session/index.js +8 -0
  323. package/dist/session/index.js.map +1 -0
  324. package/dist/session/registry.d.ts +4 -0
  325. package/dist/session/registry.js +12 -0
  326. package/dist/session/registry.js.map +1 -0
  327. package/dist/session/replay.d.ts +10 -0
  328. package/dist/session/replay.js +109 -0
  329. package/dist/session/replay.js.map +1 -0
  330. package/dist/session/session.d.ts +40 -0
  331. package/dist/session/session.js +108 -0
  332. package/dist/session/session.js.map +1 -0
  333. package/dist/session/store.d.ts +58 -0
  334. package/dist/session/store.js +382 -0
  335. package/dist/session/store.js.map +1 -0
  336. package/dist/session/types.d.ts +56 -0
  337. package/dist/session/types.js +3 -0
  338. package/dist/session/types.js.map +1 -0
  339. package/dist/workspace/index.d.ts +7 -0
  340. package/dist/workspace/index.js +17 -0
  341. package/dist/workspace/index.js.map +1 -0
  342. package/dist/workspace/registry.d.ts +25 -0
  343. package/dist/workspace/registry.js +293 -0
  344. package/dist/workspace/registry.js.map +1 -0
  345. package/package.json +73 -5
  346. package/scripts/build-node-pty.mjs +55 -0
  347. package/scripts/rewrite-dts.mjs +36 -0
  348. package/scripts/run-tests.mjs +41 -0
  349. package/scripts/verify-sandbox.ts +98 -0
  350. package/src/agent/pending-asks.ts +88 -0
  351. package/src/agent/task-registry.ts +108 -0
  352. package/src/agent/tool-context.ts +47 -0
  353. package/src/agent/usage.ts +170 -0
  354. package/src/api/host/agent.ts +162 -0
  355. package/src/api/host/events.ts +7 -0
  356. package/src/api/host/exec.ts +47 -0
  357. package/src/api/host/fs.ts +33 -0
  358. package/src/api/host/hooks.ts +635 -0
  359. package/src/api/host/host.ts +99 -0
  360. package/src/api/host/http.ts +79 -0
  361. package/src/api/host/index.ts +20 -0
  362. package/src/api/host/models.ts +81 -0
  363. package/src/api/host/network.ts +32 -0
  364. package/src/api/host/plugin.ts +158 -0
  365. package/src/api/host/plugins.ts +59 -0
  366. package/src/api/host/pty.ts +73 -0
  367. package/src/api/host/session.ts +61 -0
  368. package/src/api/host/version.ts +2 -0
  369. package/src/api/host/workspace.ts +51 -0
  370. package/src/api/plugin/http-client.ts +492 -0
  371. package/src/api/plugin/http-protocol.ts +231 -0
  372. package/src/api/result/result.ts +52 -0
  373. package/src/commands/agent-turn.ts +817 -0
  374. package/src/commands/ask-prompt.ts +52 -0
  375. package/src/commands/config.ts +306 -0
  376. package/src/commands/conversation.ts +777 -0
  377. package/src/commands/doctor.ts +92 -0
  378. package/src/commands/exec-approval.ts +91 -0
  379. package/src/commands/help.ts +41 -0
  380. package/src/commands/key.ts +281 -0
  381. package/src/commands/plugin.ts +222 -0
  382. package/src/commands/session.ts +179 -0
  383. package/src/commands/shared.ts +103 -0
  384. package/src/commands/start.ts +88 -0
  385. package/src/commands/stop.ts +24 -0
  386. package/src/commands/version.ts +40 -0
  387. package/src/config/codes.ts +17 -0
  388. package/src/config/credentials.ts +302 -0
  389. package/src/config/defaults.ts +32 -0
  390. package/src/config/index.ts +10 -0
  391. package/src/config/loader.ts +80 -0
  392. package/src/config/providers.ts +133 -0
  393. package/src/config/types.ts +88 -0
  394. package/src/config/validate.ts +458 -0
  395. package/src/display/markdown.ts +137 -0
  396. package/src/display/tool-label.ts +87 -0
  397. package/src/event/event.ts +171 -0
  398. package/src/index.ts +344 -0
  399. package/src/input/codes.ts +21 -0
  400. package/src/input/commands.ts +76 -0
  401. package/src/input/help.ts +60 -0
  402. package/src/input/index.ts +10 -0
  403. package/src/input/parse-options.ts +66 -0
  404. package/src/input/parse.ts +138 -0
  405. package/src/input/types.ts +19 -0
  406. package/src/logger/elog4js.ts +366 -0
  407. package/src/logger/logger.ts +251 -0
  408. package/src/network/index.ts +6 -0
  409. package/src/network/openai.ts +571 -0
  410. package/src/network/proxy.ts +80 -0
  411. package/src/network/web.ts +206 -0
  412. package/src/platform/clipboard.ts +107 -0
  413. package/src/platform/exec.ts +128 -0
  414. package/src/platform/file.ts +235 -0
  415. package/src/platform/index.ts +12 -0
  416. package/src/platform/notification.ts +108 -0
  417. package/src/platform/open-external.ts +132 -0
  418. package/src/platform/os.ts +101 -0
  419. package/src/platform/path.ts +214 -0
  420. package/src/plugin/access-guard.ts +46 -0
  421. package/src/plugin/agent-service.ts +969 -0
  422. package/src/plugin/agent-tools.ts +43 -0
  423. package/src/plugin/capabilities.ts +240 -0
  424. package/src/plugin/console-guard.ts +66 -0
  425. package/src/plugin/data-access.ts +70 -0
  426. package/src/plugin/fs.ts +109 -0
  427. package/src/plugin/host-alias.ts +61 -0
  428. package/src/plugin/index.ts +11 -0
  429. package/src/plugin/install.ts +213 -0
  430. package/src/plugin/loader.ts +952 -0
  431. package/src/plugin/manage.ts +338 -0
  432. package/src/plugin/models-service.ts +383 -0
  433. package/src/plugin/network-service.ts +102 -0
  434. package/src/plugin/package-manager.ts +214 -0
  435. package/src/plugin/paths.ts +19 -0
  436. package/src/plugin/registry.ts +103 -0
  437. package/src/plugin/runtime.ts +57 -0
  438. package/src/plugin/scaffold.ts +158 -0
  439. package/src/plugin/state.ts +52 -0
  440. package/src/plugin/zip.ts +156 -0
  441. package/src/sandbox/audit.ts +38 -0
  442. package/src/sandbox/backends/bwrap.ts +251 -0
  443. package/src/sandbox/backends/direct.ts +43 -0
  444. package/src/sandbox/bridge-state.ts +29 -0
  445. package/src/sandbox/index.ts +5 -0
  446. package/src/sandbox/manager.ts +406 -0
  447. package/src/sandbox/memory-limit.ts +245 -0
  448. package/src/sandbox/plugin-client.ts +1881 -0
  449. package/src/sandbox/plugin-host.ts +1282 -0
  450. package/src/sandbox/protocol.ts +142 -0
  451. package/src/sandbox/pty-bridge.py +154 -0
  452. package/src/sandbox/rpc-timeouts.ts +43 -0
  453. package/src/sandbox/supervisor.ts +172 -0
  454. package/src/sandbox/types.ts +149 -0
  455. package/src/service/control.ts +155 -0
  456. package/src/session/compress.ts +47 -0
  457. package/src/session/index.ts +8 -0
  458. package/src/session/registry.ts +17 -0
  459. package/src/session/replay.ts +132 -0
  460. package/src/session/session.ts +134 -0
  461. package/src/session/store.ts +468 -0
  462. package/src/session/types.ts +61 -0
  463. package/src/workspace/index.ts +21 -0
  464. package/src/workspace/registry.ts +356 -0
@@ -0,0 +1,969 @@
1
+ /*
2
+ * 宿主 Agent 服务:给 sandbox 插件提供聊天入口。
3
+ * 复用 CLI 的会话存储与 Agent 轮次,不经过 stdout/readline。
4
+ */
5
+
6
+ import { spawn } from "node:child_process"
7
+ import { randomUUID } from "node:crypto"
8
+ import { accessSync, constants } from "node:fs"
9
+ import { readdir, stat } from "node:fs/promises"
10
+ import { homedir } from "node:os"
11
+ import { basename, delimiter, isAbsolute, join, relative, resolve, sep } from "node:path"
12
+
13
+ import type {
14
+ HostAgentApi,
15
+ HostAgentApprovalRequest,
16
+ HostAgentAskRequest,
17
+ HostAgentChatInput,
18
+ HostAgentChatResult,
19
+ HostAgentStreamEvent,
20
+ HostAgentToolEvent,
21
+ HostWorkspaceCheck,
22
+ HostWorkspaceFileEntry,
23
+ HostWorkspaceFilesResult,
24
+ } from "../api/host/agent.ts"
25
+ import type { ExecApprovalRequest } from "../commands/agent-turn.ts"
26
+ import type { CommandContext } from "../commands/shared.ts"
27
+ import { executeTurn } from "../commands/conversation.ts"
28
+ import { answerAsk, cancelSessionAsks } from "../agent/pending-asks.ts"
29
+ import { credentialsFile } from "../config/credentials.ts"
30
+ import { readTextIfExists } from "../platform/file.ts"
31
+ import { DEFAULT_SETTINGS } from "../config/defaults.ts"
32
+ import { loadSettings, SETTING_FILE_NAME } from "../config/loader.ts"
33
+ import {
34
+ effectiveModel,
35
+ effectiveReasoning,
36
+ providerApiKey,
37
+ providerOpenAISettings,
38
+ requestProvider,
39
+ type ResolvedModelProvider,
40
+ } from "../config/providers.ts"
41
+ import type { AppSetting, SandboxAccess } from "../config/types.ts"
42
+ import { getElog } from "../logger/elog4js.ts"
43
+ import type { Logger } from "pino"
44
+ import { OpenAIClient } from "../network/openai.ts"
45
+ import { createProxyFetch } from "../network/proxy.ts"
46
+ import type { ResolvedPaths } from "../platform/path.ts"
47
+ import type { AgentToolRegistry } from "./agent-tools.ts"
48
+ import type { PluginRegistry } from "./registry.ts"
49
+ import { getSession, registerSession } from "../session/registry.ts"
50
+ import { Session } from "../session/session.ts"
51
+ import { SessionStore } from "../session/store.ts"
52
+
53
+ export interface HostAgentServiceOptions {
54
+ readonly settings: AppSetting
55
+ readonly paths: ResolvedPaths
56
+ readonly logger: Logger
57
+ readonly registry: PluginRegistry
58
+ readonly agentTools: AgentToolRegistry
59
+ readonly cwd?: string
60
+ }
61
+
62
+ const SANDBOX_ACCESS_VALUES: readonly SandboxAccess[] = [
63
+ "read-only",
64
+ "workspace-write",
65
+ "danger-full-access",
66
+ ]
67
+
68
+ /** 插件的 host.agent.* RPC 可能带上来路不明的 permission,这里按白名单校验 */
69
+ function turnPermission(input: HostAgentChatInput, fallback: SandboxAccess): SandboxAccess {
70
+ const value: unknown = input.permission
71
+ if (value === undefined || value === null) {
72
+ return fallback
73
+ }
74
+ if (!SANDBOX_ACCESS_VALUES.includes(value as SandboxAccess)) {
75
+ throw new Error(`permission 不合法:${String(value)}`)
76
+ }
77
+ return value as SandboxAccess
78
+ }
79
+
80
+ export interface HostAgentService extends HostAgentApi {
81
+ chat(input: HostAgentChatInput): Promise<HostAgentChatResult>
82
+ }
83
+
84
+ /** 打开宿主系统的目录选择对话框;浏览器无法提供原生对话框的绝对路径 */
85
+ interface NativeCommandResult {
86
+ readonly code: number | null
87
+ readonly stdout: string
88
+ readonly stderr: string
89
+ }
90
+
91
+ function runNativeCommand(command: string, args: readonly string[]): Promise<NativeCommandResult> {
92
+ return new Promise((resolvePromise) => {
93
+ let settled = false
94
+ let stdout = ""
95
+ let stderr = ""
96
+ const child = spawn(command, [...args], { stdio: ["ignore", "pipe", "pipe"] })
97
+ const timer = setTimeout(() => {
98
+ child.kill("SIGTERM")
99
+ }, 120_000)
100
+ const finish = (code: number | null): void => {
101
+ if (settled) {
102
+ return
103
+ }
104
+ settled = true
105
+ clearTimeout(timer)
106
+ resolvePromise({ code, stdout, stderr })
107
+ }
108
+ child.stdout?.on("data", (chunk: Buffer) => {
109
+ stdout += chunk.toString("utf8")
110
+ })
111
+ child.stderr?.on("data", (chunk: Buffer) => {
112
+ stderr += chunk.toString("utf8")
113
+ })
114
+ child.once("error", () => {
115
+ finish(null)
116
+ })
117
+ child.once("close", (code) => {
118
+ finish(code)
119
+ })
120
+ })
121
+ }
122
+
123
+ function hasExecutable(name: string): boolean {
124
+ for (const dir of (process.env["PATH"] ?? "").split(delimiter)) {
125
+ if (dir === "") {
126
+ continue
127
+ }
128
+ try {
129
+ accessSync(join(dir, name), constants.X_OK)
130
+ return true
131
+ } catch {
132
+ /* 继续找 */
133
+ }
134
+ }
135
+ return false
136
+ }
137
+
138
+ /**
139
+ * 目录选择框没返回路径时,区分「用户取消」与「对话框根本没起来」
140
+ *
141
+ * 以前所有失败都报「已取消」,界面因此什么都不提示:
142
+ * zenity 打不开显示器时会带着退出码 0 和空的 stdout 退出(只往 stderr 写一行 GTK 警告),
143
+ * 看起来和用户按了取消一模一样。这里按退出码归因,让上层能把真因说出来。
144
+ */
145
+ function pickerOutcome(command: string, result: NativeCommandResult): HostWorkspaceCheck {
146
+ const cancelled: HostWorkspaceCheck = { ok: false, root: "", name: "", error: "已取消" }
147
+ // 用户关掉窗口:zenity / kdialog 退出码 1;osascript 是 AppleScript 的 -128
148
+ if (result.code === 1 || result.stderr.includes("-128")) {
149
+ return cancelled
150
+ }
151
+ if (result.code === null) {
152
+ return { ok: false, root: "", name: "", error: `无法启动 ${command}(未找到或不可执行)` }
153
+ }
154
+ const detail =
155
+ result.stderr
156
+ .split("\n")
157
+ .map((line) => line.trim())
158
+ .filter((line) => line !== "")
159
+ .pop() ?? ""
160
+ return {
161
+ ok: false,
162
+ root: "",
163
+ name: "",
164
+ error:
165
+ detail === ""
166
+ ? `${command} 没有返回目录(退出码 ${result.code})`
167
+ : `${command} 无法显示选择框:${detail}`,
168
+ }
169
+ }
170
+
171
+ const IGNORED_DIRECTORIES = new Set([
172
+ ".git",
173
+ ".hg",
174
+ ".svn",
175
+ "node_modules",
176
+ ".pnpm-store",
177
+ ".yarn",
178
+ ".npm",
179
+ ".next",
180
+ ".nuxt",
181
+ ".svelte-kit",
182
+ ".output",
183
+ ".vercel",
184
+ ".expo",
185
+ ".dart_tool",
186
+ "dist",
187
+ "build",
188
+ "out",
189
+ "target",
190
+ "coverage",
191
+ ".cache",
192
+ ".turbo",
193
+ ".parcel-cache",
194
+ ".gradle",
195
+ ".mvn",
196
+ ".venv",
197
+ "venv",
198
+ "Pods",
199
+ ".terraform",
200
+ "__pycache__",
201
+ ".tox",
202
+ ".mypy_cache",
203
+ ".pytest_cache",
204
+ ".ruff_cache",
205
+ ".idea",
206
+ ".vscode",
207
+ ])
208
+
209
+ const MAX_FILE_SCAN_ENTRIES = 20_000
210
+ const MAX_FILE_RESULTS = 30
211
+ const MAX_FILE_CANDIDATES = 2_000
212
+ const MAX_SEARCH_DEPTH = 12
213
+ const MAX_SEARCH_QUERY_CHARS = 200
214
+ const MAX_INTERJECT_CHARS = 32_000
215
+ /** 单次搜索的时间软上限:慢盘或超大仓库里不拖住宿主事件循环 */
216
+ const MAX_SEARCH_MILLISECONDS = 500
217
+ /** 浏览单个目录时最多列出的条目数 */
218
+ const MAX_BROWSE_ENTRIES = 200
219
+
220
+ async function validateWorkspacePath(path: string): Promise<HostWorkspaceCheck> {
221
+ const value = typeof path === "string" ? path.trim() : ""
222
+ if (value === "") {
223
+ return { ok: false, root: "", name: "", error: "路径不能为空" }
224
+ }
225
+ const resolved = resolve(value)
226
+ const name = basename(resolved)
227
+ try {
228
+ const info = await stat(resolved)
229
+ if (!info.isDirectory()) {
230
+ return { ok: false, root: resolved, name, error: "不是目录" }
231
+ }
232
+ } catch {
233
+ return { ok: false, root: resolved, name, error: "目录不存在" }
234
+ }
235
+ return { ok: true, root: resolved, name: name === "" ? resolved : name }
236
+ }
237
+
238
+ /** query 的字符是否按顺序出现在 text 里(大小写已归一) */
239
+ function isSubsequence(query: string, text: string): boolean {
240
+ let cursor = 0
241
+ for (const char of text) {
242
+ if (char === query[cursor]) {
243
+ cursor += 1
244
+ if (cursor === query.length) {
245
+ return true
246
+ }
247
+ }
248
+ }
249
+ return false
250
+ }
251
+
252
+ /** 路径段匹配等级:0 全等、1 前缀、2 包含;allowSubsequence 时再加 3 子序列 */
253
+ function segmentScore(segment: string, needle: string, allowSubsequence: boolean): number | null {
254
+ if (segment === needle) {
255
+ return 0
256
+ }
257
+ if (segment.startsWith(needle)) {
258
+ return 1
259
+ }
260
+ if (segment.includes(needle)) {
261
+ return 2
262
+ }
263
+ if (allowSubsequence && isSubsequence(needle, segment)) {
264
+ return 3
265
+ }
266
+ return null
267
+ }
268
+
269
+ /**
270
+ * 单一关键字的打分:越小越靠前。
271
+ * 0 文件名全等、1 文件名前缀、2 文件名某段前缀 / 扩展名、3 文件名包含、
272
+ * 4 路径段命中、5 路径包含、6 文件名子序列;不匹配返回 null。
273
+ * 路径子序列(任何文件都能拼出短查询)噪声太大,不参与匹配。
274
+ */
275
+ function singleKeyScore(name: string, path: string, query: string): number | null {
276
+ const needle = query.toLowerCase()
277
+ if (needle === "") {
278
+ return 0
279
+ }
280
+ const nameLower = name.toLowerCase()
281
+ if (nameLower === needle) {
282
+ return 0
283
+ }
284
+ if (nameLower.startsWith(needle)) {
285
+ return 1
286
+ }
287
+ const segments = nameLower.split(/[.\-_\s]+/).filter((segment) => segment !== "")
288
+ if (segments.some((segment) => segment.startsWith(needle))) {
289
+ return 2
290
+ }
291
+ // ".ts" 这类只写扩展名的查询
292
+ if (needle.startsWith(".") && nameLower.endsWith(needle)) {
293
+ return 2
294
+ }
295
+ if (nameLower.includes(needle)) {
296
+ return 3
297
+ }
298
+ const pathLower = path.toLowerCase()
299
+ if (
300
+ pathLower.startsWith(needle) ||
301
+ pathLower.split("/").some((segment) => segment.startsWith(needle))
302
+ ) {
303
+ return 4
304
+ }
305
+ if (pathLower.includes(needle)) {
306
+ return 5
307
+ }
308
+ if (isSubsequence(needle, nameLower)) {
309
+ return 6
310
+ }
311
+ return null
312
+ }
313
+
314
+ /**
315
+ * 带斜杠的查询按路径段链式匹配(a/b/c):前几段必须依次命中目录段,
316
+ * 最后一段命中文件名或目录名。这样 `@assets/app` 也能找到 `src/assets/app.ts`,
317
+ * 不必先输入工作区根目录下的完整路径。
318
+ */
319
+ function chainScore(
320
+ pathSegments: readonly string[],
321
+ querySegments: readonly string[],
322
+ ): number | null {
323
+ const nameSegment = pathSegments[pathSegments.length - 1]
324
+ const lastQuery = querySegments[querySegments.length - 1]
325
+ if (nameSegment === undefined || lastQuery === undefined) {
326
+ return null
327
+ }
328
+ const lastNameScore = segmentScore(nameSegment, lastQuery, true)
329
+ if (lastNameScore === null) {
330
+ return null
331
+ }
332
+ let score = lastNameScore
333
+ let cursor = 0
334
+ for (let index = 0; index < querySegments.length - 1; index += 1) {
335
+ const needle = querySegments[index]
336
+ if (needle === undefined) {
337
+ return null
338
+ }
339
+ let matched = -1
340
+ for (let position = cursor; position < pathSegments.length - 1; position += 1) {
341
+ const segment = pathSegments[position]
342
+ if (segment === undefined) {
343
+ continue
344
+ }
345
+ const matchedScore = segmentScore(segment, needle, false)
346
+ if (matchedScore === null) {
347
+ continue
348
+ }
349
+ matched = position
350
+ score += matchedScore
351
+ break
352
+ }
353
+ if (matched === -1) {
354
+ return null
355
+ }
356
+ cursor = matched + 1
357
+ }
358
+ return score
359
+ }
360
+
361
+ interface FileCandidate extends HostWorkspaceFileEntry {
362
+ readonly depth: number
363
+ readonly score: number
364
+ }
365
+
366
+ /** 目录浏览:只列一层,目录在前,按名称排序 */
367
+ async function browseDirectory(root: string, directory: string): Promise<HostWorkspaceFilesResult> {
368
+ let dirents
369
+ try {
370
+ dirents = await readdir(directory, { withFileTypes: true })
371
+ } catch {
372
+ return { ok: true, root, entries: [], truncated: false }
373
+ }
374
+ dirents.sort((left, right) => {
375
+ const leftRank = left.isDirectory() ? 0 : 1
376
+ const rightRank = right.isDirectory() ? 0 : 1
377
+ if (leftRank !== rightRank) {
378
+ return leftRank - rightRank
379
+ }
380
+ return left.name.localeCompare(right.name)
381
+ })
382
+ const entries: HostWorkspaceFileEntry[] = []
383
+ let truncated = false
384
+ for (const dirent of dirents) {
385
+ if (dirent.isSymbolicLink()) {
386
+ continue
387
+ }
388
+ if (dirent.name === "." || dirent.name === "..") {
389
+ continue
390
+ }
391
+ const isDirectory = dirent.isDirectory()
392
+ // 根目录一层里藏起来的依赖/构建目录不展示;显式进入时仍可浏览
393
+ if (isDirectory && directory === root && IGNORED_DIRECTORIES.has(dirent.name)) {
394
+ continue
395
+ }
396
+ if (entries.length >= MAX_BROWSE_ENTRIES) {
397
+ truncated = true
398
+ break
399
+ }
400
+ entries.push({
401
+ path: relative(root, join(directory, dirent.name)).split(sep).join("/"),
402
+ name: dirent.name,
403
+ type: isDirectory ? "directory" : "file",
404
+ })
405
+ }
406
+ return { ok: true, root, entries, truncated }
407
+ }
408
+
409
+ function compareCandidates(left: FileCandidate, right: FileCandidate): number {
410
+ if (left.score !== right.score) {
411
+ return left.score - right.score
412
+ }
413
+ if (left.type !== right.type) {
414
+ return left.type === "directory" ? -1 : 1
415
+ }
416
+ if (left.depth !== right.depth) {
417
+ return left.depth - right.depth
418
+ }
419
+ return left.path.localeCompare(right.path)
420
+ }
421
+
422
+ /**
423
+ * 在工作区里按文件名 / 路径搜索(广度优先,浅层条目优先进入扫描预算)。
424
+ * - query 为空或以 `/` 结尾且路径存在:只列该目录一层
425
+ * - 单关键字(`app`):文件名优先,其次路径段 / 路径包含 / 子序列
426
+ * - 带斜杠(`assets/app`):按路径段链式匹配,不要求前缀目录真实存在
427
+ */
428
+ export async function searchWorkspaceFiles(
429
+ rootPath: string,
430
+ query: string,
431
+ ): Promise<HostWorkspaceFilesResult> {
432
+ const check = await validateWorkspacePath(rootPath)
433
+ if (!check.ok) {
434
+ return {
435
+ ok: false,
436
+ root: "",
437
+ entries: [],
438
+ truncated: false,
439
+ error: check.error ?? "目录不可用",
440
+ }
441
+ }
442
+ const root = check.root
443
+ const raw = (typeof query === "string" ? query : "").trim().replaceAll("\\", "/")
444
+ if (raw.length > MAX_SEARCH_QUERY_CHARS) {
445
+ return {
446
+ ok: false,
447
+ root,
448
+ entries: [],
449
+ truncated: false,
450
+ error: "搜索词过长",
451
+ }
452
+ }
453
+ const withoutLeading = raw.replace(/^\/+/, "")
454
+ const segments = withoutLeading
455
+ .split("/")
456
+ .filter((segment) => segment !== "" && segment !== ".")
457
+ if (segments.length === 0) {
458
+ return await browseDirectory(root, root)
459
+ }
460
+
461
+ // 输入了完整目录路径(以 / 结尾)时直接列出它的内容
462
+ if (withoutLeading.endsWith("/")) {
463
+ const target = resolve(root, segments.join("/"))
464
+ const rel = relative(root, target)
465
+ if (!rel.startsWith("..") && !isAbsolute(rel)) {
466
+ try {
467
+ const info = await stat(target)
468
+ if (info.isDirectory()) {
469
+ return await browseDirectory(root, target)
470
+ }
471
+ } catch {
472
+ /* 目录不存在,退回模糊搜索 */
473
+ }
474
+ }
475
+ }
476
+
477
+ const querySegments = segments.map((segment) => segment.toLowerCase())
478
+ const singleNeedle = querySegments.length === 1 ? (querySegments[0] ?? null) : null
479
+ const candidates: FileCandidate[] = []
480
+ const deadline = performance.now() + MAX_SEARCH_MILLISECONDS
481
+ let visited = 0
482
+ let truncated = false
483
+ const queue: Array<{ readonly dir: string; readonly depth: number }> = [{ dir: root, depth: 0 }]
484
+ while (queue.length > 0) {
485
+ if (visited >= MAX_FILE_SCAN_ENTRIES || performance.now() > deadline) {
486
+ truncated = true
487
+ break
488
+ }
489
+ const current = queue.shift()
490
+ if (current === undefined) {
491
+ break
492
+ }
493
+ let dirents
494
+ try {
495
+ dirents = await readdir(current.dir, { withFileTypes: true })
496
+ } catch {
497
+ continue
498
+ }
499
+ dirents.sort((left, right) => left.name.localeCompare(right.name))
500
+ for (const dirent of dirents) {
501
+ if (visited >= MAX_FILE_SCAN_ENTRIES || candidates.length >= MAX_FILE_CANDIDATES) {
502
+ truncated = true
503
+ break
504
+ }
505
+ visited += 1
506
+ if (dirent.isSymbolicLink()) {
507
+ continue
508
+ }
509
+ const isDirectory = dirent.isDirectory()
510
+ if (isDirectory && IGNORED_DIRECTORIES.has(dirent.name)) {
511
+ continue
512
+ }
513
+ const rel = relative(root, join(current.dir, dirent.name)).split(sep).join("/")
514
+ const score =
515
+ singleNeedle === null
516
+ ? chainScore(rel.split("/"), querySegments)
517
+ : singleKeyScore(dirent.name, rel, singleNeedle)
518
+ if (score !== null) {
519
+ candidates.push({
520
+ path: rel,
521
+ name: dirent.name,
522
+ type: isDirectory ? "directory" : "file",
523
+ depth: current.depth,
524
+ score,
525
+ })
526
+ }
527
+ if (isDirectory && current.depth < MAX_SEARCH_DEPTH) {
528
+ queue.push({ dir: join(current.dir, dirent.name), depth: current.depth + 1 })
529
+ }
530
+ }
531
+ }
532
+ candidates.sort(compareCandidates)
533
+ const entries = candidates.slice(0, MAX_FILE_RESULTS).map((entry) => ({
534
+ path: entry.path,
535
+ name: entry.name,
536
+ type: entry.type,
537
+ }))
538
+ const limited = candidates.length > MAX_FILE_RESULTS
539
+ return { ok: true, root, entries, truncated: truncated || limited }
540
+ }
541
+
542
+ export function createHostAgentService(options: HostAgentServiceOptions): HostAgentService {
543
+ const cwd = options.cwd ?? process.cwd()
544
+ const store = new SessionStore()
545
+ const queues = new Map<string, Promise<unknown>>()
546
+ /**
547
+ * 每个会话当前这一轮的 AbortController
548
+ *
549
+ * stop() 除了标记停止,还要把正在进行的模型请求打断——
550
+ * 否则「停止」只是等这一轮自己跑完,慢模型要等很久。
551
+ */
552
+ const turnAborts = new Map<string, AbortController>()
553
+ const pendingApprovals = new Map<
554
+ string,
555
+ {
556
+ readonly sessionId: string
557
+ readonly resolve: (approved: boolean) => void
558
+ readonly timer: ReturnType<typeof setTimeout>
559
+ }
560
+ >()
561
+ const APPROVAL_TIMEOUT_MS = 120_000
562
+
563
+ const settleApproval = (approvalId: string, approved: boolean): boolean => {
564
+ const pending = pendingApprovals.get(approvalId)
565
+ if (pending === undefined) {
566
+ return false
567
+ }
568
+ pendingApprovals.delete(approvalId)
569
+ clearTimeout(pending.timer)
570
+ pending.resolve(approved)
571
+ return true
572
+ }
573
+
574
+ /** 停止一轮时把它挂着的提权请求按拒绝了结,避免循环卡在等待里 */
575
+ const settleSessionApprovals = (sessionId: string): void => {
576
+ for (const [approvalId, pending] of [...pendingApprovals]) {
577
+ if (pending.sessionId !== sessionId) {
578
+ continue
579
+ }
580
+ pendingApprovals.delete(approvalId)
581
+ clearTimeout(pending.timer)
582
+ pending.resolve(false)
583
+ }
584
+ }
585
+
586
+ const settingsPath = join(options.paths.configDir, SETTING_FILE_NAME)
587
+
588
+ // 每轮都读一次配置与凭据:网页/CLI 改供应商、模型、Key 后不需要重启服务。
589
+ // 配置文件不存在时用启动时传入的配置(测试与 --config 覆盖场景依赖这一点)。
590
+ const currentSettings = async (): Promise<AppSetting> => {
591
+ try {
592
+ if ((await readTextIfExists(settingsPath)) === null) {
593
+ return options.settings ?? DEFAULT_SETTINGS
594
+ }
595
+ const loaded = await loadSettings(settingsPath)
596
+ return loaded.getData() ?? options.settings ?? DEFAULT_SETTINGS
597
+ } catch {
598
+ return options.settings ?? DEFAULT_SETTINGS
599
+ }
600
+ }
601
+
602
+ const createProviderClient = (
603
+ settings: AppSetting,
604
+ provider: ResolvedModelProvider,
605
+ ): OpenAIClient => {
606
+ let proxyFetch: ReturnType<typeof createProxyFetch> | undefined
607
+ try {
608
+ proxyFetch = createProxyFetch(settings.network.proxy)
609
+ } catch {
610
+ proxyFetch = undefined
611
+ }
612
+ // 自定义供应商没有 key 时不能悄悄回落到内置 OpenAI key:显式传空串让创建失败
613
+ const apiKey = providerApiKey(provider, options.paths.configDir) ?? ""
614
+ const result = OpenAIClient.create({
615
+ settings: providerOpenAISettings(settings, provider),
616
+ apiKey,
617
+ credentialsFile: credentialsFile(options.paths.configDir),
618
+ logger: options.logger.child({ module: "openai" }),
619
+ fetch: proxyFetch,
620
+ })
621
+ const created = result.getData()
622
+ if (!result.isSuccess() || created === null) {
623
+ throw new Error(result.getCode() ?? "openai.missing-api-key")
624
+ }
625
+ return created
626
+ }
627
+
628
+ const context = (
629
+ settings: AppSetting,
630
+ client: OpenAIClient,
631
+ model: string,
632
+ provider: ResolvedModelProvider,
633
+ ): CommandContext => ({
634
+ global: { json: true, quiet: true, help: false, version: false },
635
+ settings,
636
+ settingsFile: settingsPath,
637
+ configCode: null,
638
+ paths: options.paths,
639
+ logger: options.logger,
640
+ elog: getElog(),
641
+ registry: options.registry,
642
+ agentTools: options.agentTools,
643
+ openai: client,
644
+ activeProvider: provider.id,
645
+ activeModel: model,
646
+ loadPlugins: async () => [],
647
+ })
648
+
649
+ /** 校验工作目录:存在且是目录 */
650
+ const validateWorkspace = async (path: string): Promise<HostWorkspaceCheck> => {
651
+ return await validateWorkspacePath(path)
652
+ }
653
+
654
+ /** 打开原生目录选择对话框:Linux 用 zenity/kdialog,macOS 用 osascript,Windows 用 FolderBrowserDialog */
655
+ const pickDirectory = async (): Promise<HostWorkspaceCheck> => {
656
+ const title = "选择工作区文件夹"
657
+ if (process.platform === "darwin") {
658
+ const result = await runNativeCommand("osascript", [
659
+ "-e",
660
+ `POSIX path of (choose folder with prompt "${title}")`,
661
+ ])
662
+ const selected = result.stdout.trim()
663
+ if (result.code === 0 && selected !== "") {
664
+ return await validateWorkspace(selected)
665
+ }
666
+ return pickerOutcome("osascript", result)
667
+ }
668
+ if (process.platform === "win32") {
669
+ const script = [
670
+ "Add-Type -AssemblyName System.Windows.Forms",
671
+ "$dialog = New-Object System.Windows.Forms.FolderBrowserDialog",
672
+ `$dialog.Description = '${title}'`,
673
+ "if ($dialog.ShowDialog() -eq 'OK') { Write-Output $dialog.SelectedPath }",
674
+ ].join("; ")
675
+ const result = await runNativeCommand("powershell.exe", [
676
+ "-NoProfile",
677
+ "-NonInteractive",
678
+ "-Command",
679
+ script,
680
+ ])
681
+ const selected = result.stdout.trim()
682
+ if (result.code === 0 && selected !== "") {
683
+ return await validateWorkspace(selected)
684
+ }
685
+ return pickerOutcome("powershell", result)
686
+ }
687
+ if (hasExecutable("zenity")) {
688
+ const result = await runNativeCommand("zenity", [
689
+ "--file-selection",
690
+ "--directory",
691
+ "--title",
692
+ title,
693
+ ])
694
+ const selected = result.stdout.trim()
695
+ return result.code === 0 && selected !== ""
696
+ ? await validateWorkspace(selected)
697
+ : pickerOutcome("zenity", result)
698
+ }
699
+ if (hasExecutable("kdialog")) {
700
+ const result = await runNativeCommand("kdialog", [
701
+ "--getexistingdirectory",
702
+ homedir(),
703
+ "--title",
704
+ title,
705
+ ])
706
+ const selected = result.stdout.trim()
707
+ return result.code === 0 && selected !== ""
708
+ ? await validateWorkspace(selected)
709
+ : pickerOutcome("kdialog", result)
710
+ }
711
+ return {
712
+ ok: false,
713
+ root: "",
714
+ name: "",
715
+ error: "没有可用的系统文件夹对话框(Linux 需要 zenity 或 kdialog)",
716
+ }
717
+ }
718
+
719
+ const chatOnce = async (
720
+ input: HostAgentChatInput,
721
+ onDelta?: (text: string) => void,
722
+ onStep?: (event: { readonly turn: number; readonly step: number }) => void,
723
+ onApproval?: (request: HostAgentApprovalRequest) => void,
724
+ onReasoning?: (text: string) => void,
725
+ onTool?: (event: HostAgentToolEvent) => void,
726
+ onAsk?: (request: HostAgentAskRequest) => void,
727
+ onSession?: (sessionId: string) => void,
728
+ ): Promise<HostAgentChatResult> => {
729
+ const message = input.message.trim()
730
+ if (message === "") {
731
+ throw new Error("消息不能为空")
732
+ }
733
+ const settings = await currentSettings()
734
+ const provider = requestProvider(settings, input.provider)
735
+ const model = effectiveModel(settings, provider, input.model)
736
+ const reasoning = effectiveReasoning(settings, provider, input.reasoning)
737
+ const client = createProviderClient(settings, provider)
738
+ const permission = turnPermission(input, settings.sandbox.access)
739
+ if (permission !== settings.sandbox.access) {
740
+ // 插件可以在单轮里提升权限(例如网页的权限切换器);记录下来便于审计
741
+ options.logger.warn(
742
+ {
743
+ requested: permission,
744
+ configured: settings.sandbox.access,
745
+ sessionId: input.sessionId ?? null,
746
+ },
747
+ permission === "danger-full-access"
748
+ ? "插件本轮请求 danger-full-access 权限"
749
+ : "插件本轮覆盖 sandbox 权限",
750
+ )
751
+ }
752
+ let turnCwd = cwd
753
+ if (input.cwd !== undefined) {
754
+ const workspace = await validateWorkspace(input.cwd)
755
+ if (!workspace.ok) {
756
+ throw new Error(`工作区不可用:${workspace.error ?? "目录不存在"}`)
757
+ }
758
+ turnCwd = workspace.root
759
+ }
760
+ const references = Array.isArray(input.references)
761
+ ? input.references
762
+ .filter(
763
+ (item): item is string =>
764
+ typeof item === "string" && item !== "" && item.length <= 512,
765
+ )
766
+ .slice(0, 20)
767
+ : []
768
+ const messageForAgent =
769
+ references.length === 0
770
+ ? message
771
+ : `${message}\n\n引用的工作区路径:\n${references.map((item) => `- ${item}`).join("\n")}\n需要时请按路径读取。`
772
+ const data =
773
+ input.sessionId === undefined
774
+ ? await store.create({ cwd: turnCwd, model })
775
+ : await store.open(input.sessionId, turnCwd)
776
+ registerSession(new Session(data, store))
777
+ onSession?.(data.meta.id)
778
+ const requestApproval =
779
+ onApproval === undefined
780
+ ? undefined
781
+ : async (request: ExecApprovalRequest): Promise<boolean> => {
782
+ const approvalId = `apv_${randomUUID()}`
783
+ return await new Promise<boolean>((resolvePromise) => {
784
+ const timer = setTimeout(() => {
785
+ pendingApprovals.delete(approvalId)
786
+ resolvePromise(false)
787
+ }, APPROVAL_TIMEOUT_MS)
788
+ pendingApprovals.set(approvalId, {
789
+ sessionId: data.meta.id,
790
+ resolve: (approved) => resolvePromise(approved),
791
+ timer,
792
+ })
793
+ try {
794
+ onApproval({
795
+ approvalId,
796
+ command: request.command,
797
+ argv: request.argv,
798
+ cwd: request.cwd,
799
+ currentPermission: request.currentPermission,
800
+ requestedPermission: request.requestedPermission,
801
+ justification: request.justification,
802
+ ...(request.network === true ? { network: true } : {}),
803
+ ...(request.background === true ? { background: true } : {}),
804
+ ...(typeof request.stdin === "string" && request.stdin !== ""
805
+ ? { stdin: request.stdin }
806
+ : {}),
807
+ ...(typeof request.timeoutMs === "number"
808
+ ? { timeoutMs: request.timeoutMs }
809
+ : {}),
810
+ })
811
+ } catch {
812
+ pendingApprovals.delete(approvalId)
813
+ clearTimeout(timer)
814
+ resolvePromise(false)
815
+ }
816
+ })
817
+ }
818
+ const turnAbort = new AbortController()
819
+ turnAborts.set(data.meta.id, turnAbort)
820
+ let result
821
+ try {
822
+ result = await executeTurn({
823
+ ctx: context(settings, client, model, provider),
824
+ signal: turnAbort.signal,
825
+ client,
826
+ store,
827
+ session: data,
828
+ mode: "run",
829
+ input: messageForAgent,
830
+ model,
831
+ reasoning,
832
+ system: input.system,
833
+ stream: false,
834
+ display: false,
835
+ allowExec: false,
836
+ permission,
837
+ onDelta,
838
+ onReasoning,
839
+ onTool,
840
+ onStep,
841
+ approveExec: requestApproval,
842
+ ...(onAsk === undefined
843
+ ? // 没有界面通道(agent.chat)时立即跳过提问,避免一轮对话挂到超时
844
+ { askUser: async () => null }
845
+ : { onAsk }),
846
+ })
847
+ } finally {
848
+ turnAborts.delete(data.meta.id)
849
+ }
850
+ if (!result.isSuccess()) {
851
+ throw new Error(result.getCode() ?? "agent.failed")
852
+ }
853
+ const turn = result.getData()
854
+ if (turn === null) {
855
+ throw new Error("agent.empty-result")
856
+ }
857
+ return {
858
+ sessionId: turn.sessionId,
859
+ turn: turn.turn,
860
+ steps: turn.steps,
861
+ reply: turn.reply,
862
+ model: turn.model,
863
+ usage: turn.usage,
864
+ }
865
+ }
866
+
867
+ const enqueue = async (
868
+ input: HostAgentChatInput,
869
+ onDelta?: (text: string) => void,
870
+ onStep?: (event: { readonly turn: number; readonly step: number }) => void,
871
+ onApproval?: (request: HostAgentApprovalRequest) => void,
872
+ onReasoning?: (text: string) => void,
873
+ onTool?: (event: HostAgentToolEvent) => void,
874
+ onAsk?: (request: HostAgentAskRequest) => void,
875
+ onSession?: (sessionId: string) => void,
876
+ ): Promise<HostAgentChatResult> => {
877
+ const key = `${input.cwd ?? ""}|${input.sessionId ?? "new"}`
878
+ const previous = queues.get(key) ?? Promise.resolve()
879
+ const next = previous.then(
880
+ async () =>
881
+ await chatOnce(
882
+ input,
883
+ onDelta,
884
+ onStep,
885
+ onApproval,
886
+ onReasoning,
887
+ onTool,
888
+ onAsk,
889
+ onSession,
890
+ ),
891
+ async () =>
892
+ await chatOnce(
893
+ input,
894
+ onDelta,
895
+ onStep,
896
+ onApproval,
897
+ onReasoning,
898
+ onTool,
899
+ onAsk,
900
+ onSession,
901
+ ),
902
+ )
903
+ queues.set(
904
+ key,
905
+ next.catch(() => undefined),
906
+ )
907
+ const result = await next
908
+ if (input.sessionId === undefined) {
909
+ queues.delete(key)
910
+ queues.set(result.sessionId, Promise.resolve(result))
911
+ }
912
+ return result
913
+ }
914
+
915
+ return {
916
+ async chat(input) {
917
+ return await enqueue(input)
918
+ },
919
+ validateWorkspace,
920
+ pickDirectory,
921
+ async searchWorkspaceFiles(root, query) {
922
+ return await searchWorkspaceFiles(root, query)
923
+ },
924
+ async stream(input, onEvent: (event: HostAgentStreamEvent) => void) {
925
+ return await enqueue(
926
+ input,
927
+ (text) => onEvent({ type: "delta", text }),
928
+ (event) => onEvent({ type: "step", turn: event.turn, step: event.step }),
929
+ (request) => onEvent({ type: "approval", request }),
930
+ (text) => onEvent({ type: "reasoning", text }),
931
+ (event) => onEvent({ type: "tool", ...event }),
932
+ (request) => onEvent({ type: "ask", request }),
933
+ (sessionId) => onEvent({ type: "session", sessionId }),
934
+ )
935
+ },
936
+ async interject(sessionId, message) {
937
+ const content = typeof message === "string" ? message.trim() : ""
938
+ if (content === "") {
939
+ return false
940
+ }
941
+ const session = getSession(sessionId)
942
+ // 只有正在跑的一轮才能插话;其余情况交给调用方按新一轮处理
943
+ if (session === null || !session.active) {
944
+ return false
945
+ }
946
+ await session.message(content.slice(0, MAX_INTERJECT_CHARS), "interject")
947
+ return true
948
+ },
949
+ async stop(sessionId) {
950
+ const session = getSession(sessionId)
951
+ if (session === null || !session.active) {
952
+ return false
953
+ }
954
+ session.requestStop()
955
+ // 挂着等人的请求就地了结,循环才能读到停止标记
956
+ cancelSessionAsks(sessionId)
957
+ settleSessionApprovals(sessionId)
958
+ // 打断正在进行的模型请求:已收到的正文照常保留,本轮按「已停止」收尾
959
+ turnAborts.get(sessionId)?.abort()
960
+ return true
961
+ },
962
+ async respondApproval(approvalId, approved) {
963
+ return settleApproval(approvalId, approved)
964
+ },
965
+ async respondAsk(askId, answer) {
966
+ return answerAsk(askId, answer)
967
+ },
968
+ }
969
+ }