@ashlr/hub 2.2.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 (477) hide show
  1. package/CHANGELOG.md +1158 -0
  2. package/LICENSE +21 -0
  3. package/README.md +833 -0
  4. package/bin/ashlr +8 -0
  5. package/dist/api/core.d.ts +28 -0
  6. package/dist/api/core.js +42 -0
  7. package/dist/api/core.js.map +1 -0
  8. package/dist/api/index.d.ts +9 -0
  9. package/dist/api/index.js +9 -0
  10. package/dist/api/index.js.map +1 -0
  11. package/dist/api/plugin.d.ts +12 -0
  12. package/dist/api/plugin.js +13 -0
  13. package/dist/api/plugin.js.map +1 -0
  14. package/dist/api/types.d.ts +9 -0
  15. package/dist/api/types.js +9 -0
  16. package/dist/api/types.js.map +1 -0
  17. package/dist/cli/args.d.ts +20 -0
  18. package/dist/cli/args.js +23 -0
  19. package/dist/cli/args.js.map +1 -0
  20. package/dist/cli/ask.d.ts +22 -0
  21. package/dist/cli/ask.js +240 -0
  22. package/dist/cli/ask.js.map +1 -0
  23. package/dist/cli/audit.d.ts +26 -0
  24. package/dist/cli/audit.js +199 -0
  25. package/dist/cli/audit.js.map +1 -0
  26. package/dist/cli/backlog.d.ts +26 -0
  27. package/dist/cli/backlog.js +322 -0
  28. package/dist/cli/backlog.js.map +1 -0
  29. package/dist/cli/completions.d.ts +18 -0
  30. package/dist/cli/completions.js +156 -0
  31. package/dist/cli/completions.js.map +1 -0
  32. package/dist/cli/daemon.d.ts +31 -0
  33. package/dist/cli/daemon.js +334 -0
  34. package/dist/cli/daemon.js.map +1 -0
  35. package/dist/cli/demo-sandbox.d.ts +78 -0
  36. package/dist/cli/demo-sandbox.js +227 -0
  37. package/dist/cli/demo-sandbox.js.map +1 -0
  38. package/dist/cli/demo.d.ts +76 -0
  39. package/dist/cli/demo.js +285 -0
  40. package/dist/cli/demo.js.map +1 -0
  41. package/dist/cli/digest.d.ts +46 -0
  42. package/dist/cli/digest.js +352 -0
  43. package/dist/cli/digest.js.map +1 -0
  44. package/dist/cli/doctor-init.d.ts +28 -0
  45. package/dist/cli/doctor-init.js +335 -0
  46. package/dist/cli/doctor-init.js.map +1 -0
  47. package/dist/cli/genome.d.ts +62 -0
  48. package/dist/cli/genome.js +1061 -0
  49. package/dist/cli/genome.js.map +1 -0
  50. package/dist/cli/gh.d.ts +27 -0
  51. package/dist/cli/gh.js +322 -0
  52. package/dist/cli/gh.js.map +1 -0
  53. package/dist/cli/goals.d.ts +55 -0
  54. package/dist/cli/goals.js +667 -0
  55. package/dist/cli/goals.js.map +1 -0
  56. package/dist/cli/health.d.ts +56 -0
  57. package/dist/cli/health.js +407 -0
  58. package/dist/cli/health.js.map +1 -0
  59. package/dist/cli/help.d.ts +51 -0
  60. package/dist/cli/help.js +436 -0
  61. package/dist/cli/help.js.map +1 -0
  62. package/dist/cli/inbox.d.ts +28 -0
  63. package/dist/cli/inbox.js +589 -0
  64. package/dist/cli/inbox.js.map +1 -0
  65. package/dist/cli/index.d.ts +41 -0
  66. package/dist/cli/index.js +1208 -0
  67. package/dist/cli/index.js.map +1 -0
  68. package/dist/cli/knowledge.d.ts +33 -0
  69. package/dist/cli/knowledge.js +575 -0
  70. package/dist/cli/knowledge.js.map +1 -0
  71. package/dist/cli/mcp.d.ts +26 -0
  72. package/dist/cli/mcp.js +487 -0
  73. package/dist/cli/mcp.js.map +1 -0
  74. package/dist/cli/models.d.ts +26 -0
  75. package/dist/cli/models.js +435 -0
  76. package/dist/cli/models.js.map +1 -0
  77. package/dist/cli/new.d.ts +32 -0
  78. package/dist/cli/new.js +565 -0
  79. package/dist/cli/new.js.map +1 -0
  80. package/dist/cli/notify.d.ts +20 -0
  81. package/dist/cli/notify.js +188 -0
  82. package/dist/cli/notify.js.map +1 -0
  83. package/dist/cli/onboard.d.ts +85 -0
  84. package/dist/cli/onboard.js +305 -0
  85. package/dist/cli/onboard.js.map +1 -0
  86. package/dist/cli/open.d.ts +38 -0
  87. package/dist/cli/open.js +86 -0
  88. package/dist/cli/open.js.map +1 -0
  89. package/dist/cli/orient.d.ts +14 -0
  90. package/dist/cli/orient.js +110 -0
  91. package/dist/cli/orient.js.map +1 -0
  92. package/dist/cli/picker.d.ts +21 -0
  93. package/dist/cli/picker.js +177 -0
  94. package/dist/cli/picker.js.map +1 -0
  95. package/dist/cli/plugins.d.ts +21 -0
  96. package/dist/cli/plugins.js +394 -0
  97. package/dist/cli/plugins.js.map +1 -0
  98. package/dist/cli/preflight.d.ts +29 -0
  99. package/dist/cli/preflight.js +131 -0
  100. package/dist/cli/preflight.js.map +1 -0
  101. package/dist/cli/pulse.d.ts +26 -0
  102. package/dist/cli/pulse.js +482 -0
  103. package/dist/cli/pulse.js.map +1 -0
  104. package/dist/cli/reflect.d.ts +42 -0
  105. package/dist/cli/reflect.js +381 -0
  106. package/dist/cli/reflect.js.map +1 -0
  107. package/dist/cli/run.d.ts +29 -0
  108. package/dist/cli/run.js +687 -0
  109. package/dist/cli/run.js.map +1 -0
  110. package/dist/cli/sandbox.d.ts +20 -0
  111. package/dist/cli/sandbox.js +345 -0
  112. package/dist/cli/sandbox.js.map +1 -0
  113. package/dist/cli/seams.d.ts +41 -0
  114. package/dist/cli/seams.js +167 -0
  115. package/dist/cli/seams.js.map +1 -0
  116. package/dist/cli/serve.d.ts +25 -0
  117. package/dist/cli/serve.js +249 -0
  118. package/dist/cli/serve.js.map +1 -0
  119. package/dist/cli/ship.d.ts +33 -0
  120. package/dist/cli/ship.js +433 -0
  121. package/dist/cli/ship.js.map +1 -0
  122. package/dist/cli/spec.d.ts +19 -0
  123. package/dist/cli/spec.js +478 -0
  124. package/dist/cli/spec.js.map +1 -0
  125. package/dist/cli/swarm.d.ts +42 -0
  126. package/dist/cli/swarm.js +1365 -0
  127. package/dist/cli/swarm.js.map +1 -0
  128. package/dist/cli/telemetry.d.ts +26 -0
  129. package/dist/cli/telemetry.js +364 -0
  130. package/dist/cli/telemetry.js.map +1 -0
  131. package/dist/cli/tui.d.ts +17 -0
  132. package/dist/cli/tui.js +52 -0
  133. package/dist/cli/tui.js.map +1 -0
  134. package/dist/cli/ui.d.ts +53 -0
  135. package/dist/cli/ui.js +66 -0
  136. package/dist/cli/ui.js.map +1 -0
  137. package/dist/cli/update.d.ts +38 -0
  138. package/dist/cli/update.js +682 -0
  139. package/dist/cli/update.js.map +1 -0
  140. package/dist/cli/vercel.d.ts +20 -0
  141. package/dist/cli/vercel.js +193 -0
  142. package/dist/cli/vercel.js.map +1 -0
  143. package/dist/cli/verify-safety.d.ts +96 -0
  144. package/dist/cli/verify-safety.js +509 -0
  145. package/dist/cli/verify-safety.js.map +1 -0
  146. package/dist/cli/wire.d.ts +22 -0
  147. package/dist/cli/wire.js +205 -0
  148. package/dist/cli/wire.js.map +1 -0
  149. package/dist/core/classify.d.ts +51 -0
  150. package/dist/core/classify.js +450 -0
  151. package/dist/core/classify.js.map +1 -0
  152. package/dist/core/config.d.ts +63 -0
  153. package/dist/core/config.js +474 -0
  154. package/dist/core/config.js.map +1 -0
  155. package/dist/core/daemon/loop.d.ts +74 -0
  156. package/dist/core/daemon/loop.js +618 -0
  157. package/dist/core/daemon/loop.js.map +1 -0
  158. package/dist/core/daemon/state.d.ts +66 -0
  159. package/dist/core/daemon/state.js +197 -0
  160. package/dist/core/daemon/state.js.map +1 -0
  161. package/dist/core/dashboard.d.ts +40 -0
  162. package/dist/core/dashboard.js +463 -0
  163. package/dist/core/dashboard.js.map +1 -0
  164. package/dist/core/digest/build.d.ts +51 -0
  165. package/dist/core/digest/build.js +230 -0
  166. package/dist/core/digest/build.js.map +1 -0
  167. package/dist/core/digest/deliver.d.ts +47 -0
  168. package/dist/core/digest/deliver.js +230 -0
  169. package/dist/core/digest/deliver.js.map +1 -0
  170. package/dist/core/digest/store.d.ts +57 -0
  171. package/dist/core/digest/store.js +223 -0
  172. package/dist/core/digest/store.js.map +1 -0
  173. package/dist/core/doctor-fix.d.ts +21 -0
  174. package/dist/core/doctor-fix.js +440 -0
  175. package/dist/core/doctor-fix.js.map +1 -0
  176. package/dist/core/doctor.d.ts +18 -0
  177. package/dist/core/doctor.js +806 -0
  178. package/dist/core/doctor.js.map +1 -0
  179. package/dist/core/env-bridge.d.ts +64 -0
  180. package/dist/core/env-bridge.js +103 -0
  181. package/dist/core/env-bridge.js.map +1 -0
  182. package/dist/core/genome/capture.d.ts +49 -0
  183. package/dist/core/genome/capture.js +352 -0
  184. package/dist/core/genome/capture.js.map +1 -0
  185. package/dist/core/genome/consolidate.d.ts +38 -0
  186. package/dist/core/genome/consolidate.js +426 -0
  187. package/dist/core/genome/consolidate.js.map +1 -0
  188. package/dist/core/genome/export.d.ts +29 -0
  189. package/dist/core/genome/export.js +102 -0
  190. package/dist/core/genome/export.js.map +1 -0
  191. package/dist/core/genome/playbook.d.ts +33 -0
  192. package/dist/core/genome/playbook.js +320 -0
  193. package/dist/core/genome/playbook.js.map +1 -0
  194. package/dist/core/genome/recall.d.ts +45 -0
  195. package/dist/core/genome/recall.js +293 -0
  196. package/dist/core/genome/recall.js.map +1 -0
  197. package/dist/core/genome/store.d.ts +62 -0
  198. package/dist/core/genome/store.js +711 -0
  199. package/dist/core/genome/store.js.map +1 -0
  200. package/dist/core/git.d.ts +34 -0
  201. package/dist/core/git.js +115 -0
  202. package/dist/core/git.js.map +1 -0
  203. package/dist/core/goals/advance.d.ts +82 -0
  204. package/dist/core/goals/advance.js +263 -0
  205. package/dist/core/goals/advance.js.map +1 -0
  206. package/dist/core/goals/planner.d.ts +58 -0
  207. package/dist/core/goals/planner.js +233 -0
  208. package/dist/core/goals/planner.js.map +1 -0
  209. package/dist/core/goals/store.d.ts +128 -0
  210. package/dist/core/goals/store.js +442 -0
  211. package/dist/core/goals/store.js.map +1 -0
  212. package/dist/core/inbox/apply.d.ts +36 -0
  213. package/dist/core/inbox/apply.js +407 -0
  214. package/dist/core/inbox/apply.js.map +1 -0
  215. package/dist/core/inbox/notify-proposal.d.ts +13 -0
  216. package/dist/core/inbox/notify-proposal.js +21 -0
  217. package/dist/core/inbox/notify-proposal.js.map +1 -0
  218. package/dist/core/inbox/store.d.ts +67 -0
  219. package/dist/core/inbox/store.js +258 -0
  220. package/dist/core/inbox/store.js.map +1 -0
  221. package/dist/core/index-engine.d.ts +62 -0
  222. package/dist/core/index-engine.js +486 -0
  223. package/dist/core/index-engine.js.map +1 -0
  224. package/dist/core/integrations/desktop-notify.d.ts +18 -0
  225. package/dist/core/integrations/desktop-notify.js +42 -0
  226. package/dist/core/integrations/desktop-notify.js.map +1 -0
  227. package/dist/core/integrations/editors.d.ts +50 -0
  228. package/dist/core/integrations/editors.js +216 -0
  229. package/dist/core/integrations/editors.js.map +1 -0
  230. package/dist/core/integrations/github.d.ts +66 -0
  231. package/dist/core/integrations/github.js +332 -0
  232. package/dist/core/integrations/github.js.map +1 -0
  233. package/dist/core/integrations/identity.d.ts +29 -0
  234. package/dist/core/integrations/identity.js +359 -0
  235. package/dist/core/integrations/identity.js.map +1 -0
  236. package/dist/core/integrations/notify.d.ts +23 -0
  237. package/dist/core/integrations/notify.js +96 -0
  238. package/dist/core/integrations/notify.js.map +1 -0
  239. package/dist/core/integrations/vercel.d.ts +37 -0
  240. package/dist/core/integrations/vercel.js +192 -0
  241. package/dist/core/integrations/vercel.js.map +1 -0
  242. package/dist/core/knowledge/ask.d.ts +34 -0
  243. package/dist/core/knowledge/ask.js +325 -0
  244. package/dist/core/knowledge/ask.js.map +1 -0
  245. package/dist/core/knowledge/graph.d.ts +51 -0
  246. package/dist/core/knowledge/graph.js +564 -0
  247. package/dist/core/knowledge/graph.js.map +1 -0
  248. package/dist/core/knowledge/index.d.ts +74 -0
  249. package/dist/core/knowledge/index.js +586 -0
  250. package/dist/core/knowledge/index.js.map +1 -0
  251. package/dist/core/learn/playbooks.d.ts +84 -0
  252. package/dist/core/learn/playbooks.js +241 -0
  253. package/dist/core/learn/playbooks.js.map +1 -0
  254. package/dist/core/learn/reflect.d.ts +87 -0
  255. package/dist/core/learn/reflect.js +435 -0
  256. package/dist/core/learn/reflect.js.map +1 -0
  257. package/dist/core/learn/store.d.ts +51 -0
  258. package/dist/core/learn/store.js +165 -0
  259. package/dist/core/learn/store.js.map +1 -0
  260. package/dist/core/learn/tuning.d.ts +48 -0
  261. package/dist/core/learn/tuning.js +201 -0
  262. package/dist/core/learn/tuning.js.map +1 -0
  263. package/dist/core/lifecycle/scaffold.d.ts +43 -0
  264. package/dist/core/lifecycle/scaffold.js +260 -0
  265. package/dist/core/lifecycle/scaffold.js.map +1 -0
  266. package/dist/core/lifecycle/ship.d.ts +48 -0
  267. package/dist/core/lifecycle/ship.js +513 -0
  268. package/dist/core/lifecycle/ship.js.map +1 -0
  269. package/dist/core/lifecycle/templates.d.ts +20 -0
  270. package/dist/core/lifecycle/templates.js +605 -0
  271. package/dist/core/lifecycle/templates.js.map +1 -0
  272. package/dist/core/mcp-gateway.d.ts +59 -0
  273. package/dist/core/mcp-gateway.js +385 -0
  274. package/dist/core/mcp-gateway.js.map +1 -0
  275. package/dist/core/mcp-native.d.ts +53 -0
  276. package/dist/core/mcp-native.js +507 -0
  277. package/dist/core/mcp-native.js.map +1 -0
  278. package/dist/core/mcp-registry.d.ts +36 -0
  279. package/dist/core/mcp-registry.js +180 -0
  280. package/dist/core/mcp-registry.js.map +1 -0
  281. package/dist/core/observability/budget-alert.d.ts +18 -0
  282. package/dist/core/observability/budget-alert.js +90 -0
  283. package/dist/core/observability/budget-alert.js.map +1 -0
  284. package/dist/core/observability/estimate.d.ts +27 -0
  285. package/dist/core/observability/estimate.js +188 -0
  286. package/dist/core/observability/estimate.js.map +1 -0
  287. package/dist/core/observability/forecast.d.ts +19 -0
  288. package/dist/core/observability/forecast.js +101 -0
  289. package/dist/core/observability/forecast.js.map +1 -0
  290. package/dist/core/observability/governance.d.ts +28 -0
  291. package/dist/core/observability/governance.js +101 -0
  292. package/dist/core/observability/governance.js.map +1 -0
  293. package/dist/core/observability/otlp.d.ts +88 -0
  294. package/dist/core/observability/otlp.js +217 -0
  295. package/dist/core/observability/otlp.js.map +1 -0
  296. package/dist/core/observability/rollup.d.ts +35 -0
  297. package/dist/core/observability/rollup.js +311 -0
  298. package/dist/core/observability/rollup.js.map +1 -0
  299. package/dist/core/observability/telemetry-sink.d.ts +63 -0
  300. package/dist/core/observability/telemetry-sink.js +350 -0
  301. package/dist/core/observability/telemetry-sink.js.map +1 -0
  302. package/dist/core/observability/usage-source.d.ts +60 -0
  303. package/dist/core/observability/usage-source.js +347 -0
  304. package/dist/core/observability/usage-source.js.map +1 -0
  305. package/dist/core/onboard.d.ts +27 -0
  306. package/dist/core/onboard.js +288 -0
  307. package/dist/core/onboard.js.map +1 -0
  308. package/dist/core/orient.d.ts +23 -0
  309. package/dist/core/orient.js +135 -0
  310. package/dist/core/orient.js.map +1 -0
  311. package/dist/core/phantom.d.ts +23 -0
  312. package/dist/core/phantom.js +279 -0
  313. package/dist/core/phantom.js.map +1 -0
  314. package/dist/core/plugins/host-api.d.ts +29 -0
  315. package/dist/core/plugins/host-api.js +113 -0
  316. package/dist/core/plugins/host-api.js.map +1 -0
  317. package/dist/core/plugins/integrity.d.ts +36 -0
  318. package/dist/core/plugins/integrity.js +73 -0
  319. package/dist/core/plugins/integrity.js.map +1 -0
  320. package/dist/core/plugins/manifest.d.ts +31 -0
  321. package/dist/core/plugins/manifest.js +316 -0
  322. package/dist/core/plugins/manifest.js.map +1 -0
  323. package/dist/core/plugins/registry.d.ts +87 -0
  324. package/dist/core/plugins/registry.js +415 -0
  325. package/dist/core/plugins/registry.js.map +1 -0
  326. package/dist/core/plugins/types.d.ts +182 -0
  327. package/dist/core/plugins/types.js +40 -0
  328. package/dist/core/plugins/types.js.map +1 -0
  329. package/dist/core/plugins/wrappers.d.ts +40 -0
  330. package/dist/core/plugins/wrappers.js +229 -0
  331. package/dist/core/plugins/wrappers.js.map +1 -0
  332. package/dist/core/portfolio/backlog.d.ts +40 -0
  333. package/dist/core/portfolio/backlog.js +177 -0
  334. package/dist/core/portfolio/backlog.js.map +1 -0
  335. package/dist/core/portfolio/scanners.d.ts +21 -0
  336. package/dist/core/portfolio/scanners.js +600 -0
  337. package/dist/core/portfolio/scanners.js.map +1 -0
  338. package/dist/core/providers.d.ts +34 -0
  339. package/dist/core/providers.js +250 -0
  340. package/dist/core/providers.js.map +1 -0
  341. package/dist/core/quality/conventions.d.ts +35 -0
  342. package/dist/core/quality/conventions.js +267 -0
  343. package/dist/core/quality/conventions.js.map +1 -0
  344. package/dist/core/quality/fixes.d.ts +56 -0
  345. package/dist/core/quality/fixes.js +209 -0
  346. package/dist/core/quality/fixes.js.map +1 -0
  347. package/dist/core/quality/health.d.ts +69 -0
  348. package/dist/core/quality/health.js +350 -0
  349. package/dist/core/quality/health.js.map +1 -0
  350. package/dist/core/quality/store.d.ts +56 -0
  351. package/dist/core/quality/store.js +195 -0
  352. package/dist/core/quality/store.js.map +1 -0
  353. package/dist/core/readiness.d.ts +112 -0
  354. package/dist/core/readiness.js +431 -0
  355. package/dist/core/readiness.js.map +1 -0
  356. package/dist/core/run/agent-loop.d.ts +40 -0
  357. package/dist/core/run/agent-loop.js +291 -0
  358. package/dist/core/run/agent-loop.js.map +1 -0
  359. package/dist/core/run/budget.d.ts +47 -0
  360. package/dist/core/run/budget.js +113 -0
  361. package/dist/core/run/budget.js.map +1 -0
  362. package/dist/core/run/engines.d.ts +75 -0
  363. package/dist/core/run/engines.js +199 -0
  364. package/dist/core/run/engines.js.map +1 -0
  365. package/dist/core/run/model-manager.d.ts +64 -0
  366. package/dist/core/run/model-manager.js +339 -0
  367. package/dist/core/run/model-manager.js.map +1 -0
  368. package/dist/core/run/orchestrator.d.ts +100 -0
  369. package/dist/core/run/orchestrator.js +1515 -0
  370. package/dist/core/run/orchestrator.js.map +1 -0
  371. package/dist/core/run/provider-client.d.ts +46 -0
  372. package/dist/core/run/provider-client.js +796 -0
  373. package/dist/core/run/provider-client.js.map +1 -0
  374. package/dist/core/run/retry.d.ts +19 -0
  375. package/dist/core/run/retry.js +68 -0
  376. package/dist/core/run/retry.js.map +1 -0
  377. package/dist/core/run/router.d.ts +50 -0
  378. package/dist/core/run/router.js +257 -0
  379. package/dist/core/run/router.js.map +1 -0
  380. package/dist/core/run/self-heal.d.ts +52 -0
  381. package/dist/core/run/self-heal.js +181 -0
  382. package/dist/core/run/self-heal.js.map +1 -0
  383. package/dist/core/run/streaming.d.ts +31 -0
  384. package/dist/core/run/streaming.js +122 -0
  385. package/dist/core/run/streaming.js.map +1 -0
  386. package/dist/core/run/verify.d.ts +30 -0
  387. package/dist/core/run/verify.js +204 -0
  388. package/dist/core/run/verify.js.map +1 -0
  389. package/dist/core/sandbox/audit.d.ts +31 -0
  390. package/dist/core/sandbox/audit.js +169 -0
  391. package/dist/core/sandbox/audit.js.map +1 -0
  392. package/dist/core/sandbox/policy.d.ts +61 -0
  393. package/dist/core/sandbox/policy.js +211 -0
  394. package/dist/core/sandbox/policy.js.map +1 -0
  395. package/dist/core/sandbox/worktree.d.ts +175 -0
  396. package/dist/core/sandbox/worktree.js +673 -0
  397. package/dist/core/sandbox/worktree.js.map +1 -0
  398. package/dist/core/seams/backlog.d.ts +47 -0
  399. package/dist/core/seams/backlog.js +47 -0
  400. package/dist/core/seams/backlog.js.map +1 -0
  401. package/dist/core/seams/daemon-coordinator.d.ts +72 -0
  402. package/dist/core/seams/daemon-coordinator.js +76 -0
  403. package/dist/core/seams/daemon-coordinator.js.map +1 -0
  404. package/dist/core/seams/genome.d.ts +45 -0
  405. package/dist/core/seams/genome.js +53 -0
  406. package/dist/core/seams/genome.js.map +1 -0
  407. package/dist/core/seams/identity.d.ts +40 -0
  408. package/dist/core/seams/identity.js +44 -0
  409. package/dist/core/seams/identity.js.map +1 -0
  410. package/dist/core/seams/inbox.d.ts +60 -0
  411. package/dist/core/seams/inbox.js +66 -0
  412. package/dist/core/seams/inbox.js.map +1 -0
  413. package/dist/core/seams/index.d.ts +20 -0
  414. package/dist/core/seams/index.js +21 -0
  415. package/dist/core/seams/index.js.map +1 -0
  416. package/dist/core/seams/portfolio.d.ts +50 -0
  417. package/dist/core/seams/portfolio.js +61 -0
  418. package/dist/core/seams/portfolio.js.map +1 -0
  419. package/dist/core/seams/registry.d.ts +42 -0
  420. package/dist/core/seams/registry.js +128 -0
  421. package/dist/core/seams/registry.js.map +1 -0
  422. package/dist/core/seams/run-swarm.d.ts +66 -0
  423. package/dist/core/seams/run-swarm.js +74 -0
  424. package/dist/core/seams/run-swarm.js.map +1 -0
  425. package/dist/core/seams/types.d.ts +123 -0
  426. package/dist/core/seams/types.js +35 -0
  427. package/dist/core/seams/types.js.map +1 -0
  428. package/dist/core/spec/spec-store.d.ts +62 -0
  429. package/dist/core/spec/spec-store.js +359 -0
  430. package/dist/core/spec/spec-store.js.map +1 -0
  431. package/dist/core/swarm/gate.d.ts +45 -0
  432. package/dist/core/swarm/gate.js +114 -0
  433. package/dist/core/swarm/gate.js.map +1 -0
  434. package/dist/core/swarm/planner.d.ts +32 -0
  435. package/dist/core/swarm/planner.js +293 -0
  436. package/dist/core/swarm/planner.js.map +1 -0
  437. package/dist/core/swarm/rollback.d.ts +56 -0
  438. package/dist/core/swarm/rollback.js +266 -0
  439. package/dist/core/swarm/rollback.js.map +1 -0
  440. package/dist/core/swarm/runner.d.ts +62 -0
  441. package/dist/core/swarm/runner.js +1263 -0
  442. package/dist/core/swarm/runner.js.map +1 -0
  443. package/dist/core/swarm/sign.d.ts +71 -0
  444. package/dist/core/swarm/sign.js +362 -0
  445. package/dist/core/swarm/sign.js.map +1 -0
  446. package/dist/core/swarm/store.d.ts +52 -0
  447. package/dist/core/swarm/store.js +195 -0
  448. package/dist/core/swarm/store.js.map +1 -0
  449. package/dist/core/tidy.d.ts +32 -0
  450. package/dist/core/tidy.js +354 -0
  451. package/dist/core/tidy.js.map +1 -0
  452. package/dist/core/tools-registry.d.ts +16 -0
  453. package/dist/core/tools-registry.js +308 -0
  454. package/dist/core/tools-registry.js.map +1 -0
  455. package/dist/core/types.d.ts +2545 -0
  456. package/dist/core/types.js +9 -0
  457. package/dist/core/types.js.map +1 -0
  458. package/dist/core/web/api.d.ts +53 -0
  459. package/dist/core/web/api.js +698 -0
  460. package/dist/core/web/api.js.map +1 -0
  461. package/dist/core/web/public/app.js +1906 -0
  462. package/dist/core/web/public/index.html +721 -0
  463. package/dist/core/web/public/styles.css +2007 -0
  464. package/dist/core/web/server.d.ts +18 -0
  465. package/dist/core/web/server.js +122 -0
  466. package/dist/core/web/server.js.map +1 -0
  467. package/dist/core/web/static.d.ts +18 -0
  468. package/dist/core/web/static.js +122 -0
  469. package/dist/core/web/static.js.map +1 -0
  470. package/dist/tui/app.d.ts +33 -0
  471. package/dist/tui/app.js +350 -0
  472. package/dist/tui/app.js.map +1 -0
  473. package/dist/tui/render.d.ts +20 -0
  474. package/dist/tui/render.js +558 -0
  475. package/dist/tui/render.js.map +1 -0
  476. package/package.json +80 -0
  477. package/schema/config.schema.json +223 -0
@@ -0,0 +1,796 @@
1
+ /**
2
+ * provider-client.ts — thin chat client over the ACTIVE local provider.
3
+ *
4
+ * LOCAL-FIRST: refuses cloud providers unless --allow-cloud is passed (and the
5
+ * relevant API key is present). Supports Ollama (/api/chat native) and LM
6
+ * Studio (/v1/chat/completions OpenAI shape). Detects tool-call capability and
7
+ * degrades to plain chat when unsupported.
8
+ */
9
+ import { getProviderRegistry } from '../providers.js';
10
+ // ---------------------------------------------------------------------------
11
+ // Known cloud provider identifiers
12
+ // ---------------------------------------------------------------------------
13
+ const CLOUD_PROVIDERS = new Set([
14
+ 'anthropic',
15
+ 'openai',
16
+ 'gemini',
17
+ 'cohere',
18
+ 'groq',
19
+ 'mistral',
20
+ 'azure',
21
+ ]);
22
+ const CLOUD_PROVIDER_ENV = {
23
+ anthropic: 'ANTHROPIC_API_KEY',
24
+ openai: 'OPENAI_API_KEY',
25
+ gemini: 'GEMINI_API_KEY',
26
+ cohere: 'COHERE_API_KEY',
27
+ groq: 'GROQ_API_KEY',
28
+ mistral: 'MISTRAL_API_KEY',
29
+ azure: 'AZURE_OPENAI_API_KEY',
30
+ };
31
+ function isCloudProvider(id) {
32
+ return CLOUD_PROVIDERS.has(id.toLowerCase());
33
+ }
34
+ // ---------------------------------------------------------------------------
35
+ // Token estimation
36
+ // ---------------------------------------------------------------------------
37
+ /**
38
+ * Approximate token count for a string (~4 chars/token heuristic).
39
+ */
40
+ export function estimateTokens(text) {
41
+ return Math.max(1, Math.ceil(text.length / 4));
42
+ }
43
+ // ---------------------------------------------------------------------------
44
+ // Models that reliably support tool calls via Ollama
45
+ // (additive heuristic — safe to extend; false negatives just degrade to chat)
46
+ // ---------------------------------------------------------------------------
47
+ const TOOL_CAPABLE_MODEL_PATTERNS = [
48
+ /llama3/i,
49
+ /llama-3/i,
50
+ /mistral/i,
51
+ /mixtral/i,
52
+ /qwen/i,
53
+ /deepseek/i,
54
+ /command-r/i,
55
+ /hermes/i,
56
+ /functionary/i,
57
+ /nexusraven/i,
58
+ /tool/i,
59
+ ];
60
+ function modelSupportsTools(modelName) {
61
+ return TOOL_CAPABLE_MODEL_PATTERNS.some((pat) => pat.test(modelName));
62
+ }
63
+ /**
64
+ * Pick the best model from a list of available model names.
65
+ * Prefers a coder/capable model, falls back to first available.
66
+ */
67
+ function pickModel(models, explicit) {
68
+ if (models.length === 0)
69
+ return explicit?.trim() || 'default';
70
+ // 1. Explicit override. An EXPLICIT argument (passed in by a provider-aware
71
+ // caller such as the M15 router) takes precedence over the ASHLR_MODEL env
72
+ // var; the env var remains supported for the --model flag / global override.
73
+ // Passing the model explicitly avoids mutating shared process.env under
74
+ // concurrent tasks (no env race). Exact match wins, else first substring
75
+ // match, else trust the caller's string verbatim.
76
+ const override = (explicit?.trim() || process.env.ASHLR_MODEL?.trim()) || undefined;
77
+ if (override) {
78
+ return (models.find((m) => m === override) ??
79
+ models.find((m) => m.toLowerCase().includes(override.toLowerCase())) ??
80
+ override);
81
+ }
82
+ // 2. Local-first & cost-efficient default: prefer the SMALLEST/fastest chat model so runs start
83
+ // instantly instead of cold-loading a 70B (which times out). Skip embedding-only models;
84
+ // bias slightly toward coder models among equal sizes.
85
+ const isEmbed = (m) => /embed|bge|e5|nomic/i.test(m);
86
+ const pool = models.filter((m) => !isEmbed(m));
87
+ const ranked = (pool.length ? pool : models).slice();
88
+ const sizeOf = (m) => {
89
+ const b = m.match(/(\d+(?:\.\d+)?)\s*b\b/i); // "3b" / "7b" / "72b"
90
+ if (b)
91
+ return parseFloat(b[1]);
92
+ if (/mini|small|tiny|nano|phi/i.test(m))
93
+ return 3; // unlabeled small models
94
+ return 999; // unknown size -> deprioritize
95
+ };
96
+ const score = (m) => sizeOf(m) - (/coder|code/i.test(m) ? 0.5 : 0);
97
+ ranked.sort((a, b) => score(a) - score(b));
98
+ return ranked[0];
99
+ }
100
+ // ---------------------------------------------------------------------------
101
+ // Fetch with timeout
102
+ // ---------------------------------------------------------------------------
103
+ const FETCH_TIMEOUT_MS = 30_000; // 30s for chat completions
104
+ async function fetchWithTimeout(url, init, timeoutMs = FETCH_TIMEOUT_MS) {
105
+ const controller = new AbortController();
106
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
107
+ try {
108
+ return await fetch(url, { ...init, signal: controller.signal });
109
+ }
110
+ finally {
111
+ clearTimeout(timer);
112
+ }
113
+ }
114
+ /**
115
+ * Open a streaming connection whose AbortController stays alive AFTER the
116
+ * response headers arrive, so the caller can keep it as an idle-watchdog
117
+ * over the body read loop (fetchWithTimeout clears its timer the instant
118
+ * headers resolve, leaving the body read unbounded — see the streaming loops).
119
+ *
120
+ * The connect timer bounds time-to-headers; the returned controller lets the
121
+ * read loop abort a stalled/trickling body. Caller MUST clear the connect timer
122
+ * (clearConnectTimer) once headers are in, then arm its own idle watchdog.
123
+ */
124
+ async function fetchStream(url, init, connectTimeoutMs) {
125
+ const controller = new AbortController();
126
+ const timer = setTimeout(() => controller.abort(), connectTimeoutMs);
127
+ let cleared = false;
128
+ const clearConnectTimer = () => {
129
+ if (!cleared) {
130
+ cleared = true;
131
+ clearTimeout(timer);
132
+ }
133
+ };
134
+ try {
135
+ const response = await fetch(url, { ...init, signal: controller.signal });
136
+ return { response, controller, clearConnectTimer };
137
+ }
138
+ catch (err) {
139
+ clearConnectTimer();
140
+ throw err;
141
+ }
142
+ }
143
+ /**
144
+ * Read one chunk from a stream reader under a per-read idle deadline.
145
+ * If no bytes arrive within idleMs, aborts the underlying request (so the
146
+ * read rejects) and surfaces a timeout error. This bounds the ONLY otherwise
147
+ * unbounded wait in M11: a provider that returns 200 headers then stalls
148
+ * mid-stream (or never sends done). On timeout the caller's catch falls back
149
+ * to non-streaming chat() (itself FETCH_TIMEOUT-bounded).
150
+ */
151
+ async function readWithIdleTimeout(reader, controller, idleMs) {
152
+ let timer;
153
+ const idle = new Promise((_, reject) => {
154
+ timer = setTimeout(() => {
155
+ controller.abort();
156
+ reject(new Error(`stream idle timeout after ${idleMs}ms`));
157
+ }, idleMs);
158
+ });
159
+ try {
160
+ return await Promise.race([reader.read(), idle]);
161
+ }
162
+ finally {
163
+ if (timer)
164
+ clearTimeout(timer);
165
+ }
166
+ }
167
+ // ---------------------------------------------------------------------------
168
+ // Ollama chat client
169
+ // ---------------------------------------------------------------------------
170
+ /**
171
+ * Parse tool calls from an Ollama response message.
172
+ * Ollama returns tool_calls as an array on the message object.
173
+ */
174
+ function parseOllamaToolCalls(message) {
175
+ const rawCalls = message['tool_calls'];
176
+ if (!Array.isArray(rawCalls) || rawCalls.length === 0)
177
+ return undefined;
178
+ const parsed = [];
179
+ for (const tc of rawCalls) {
180
+ if (typeof tc !== 'object' || tc === null)
181
+ continue;
182
+ const call = tc;
183
+ const fn = call['function'];
184
+ if (!fn)
185
+ continue;
186
+ const name = typeof fn['name'] === 'string' ? fn['name'] : 'unknown';
187
+ const args = fn['arguments'] ?? {};
188
+ // Generate a stable id from index (Ollama doesn't assign ids)
189
+ const id = `call_${parsed.length}`;
190
+ parsed.push({ id, name, arguments: args });
191
+ }
192
+ return parsed.length > 0 ? parsed : undefined;
193
+ }
194
+ /**
195
+ * Build Ollama messages array from ChatMessage[].
196
+ * Reused by both chat() and chatStream().
197
+ */
198
+ function toOllamaMessages(messages) {
199
+ return messages.map((m) => {
200
+ if (m.role === 'tool') {
201
+ return {
202
+ role: 'tool',
203
+ content: m.content,
204
+ tool_call_id: m.toolCallId,
205
+ name: m.name,
206
+ };
207
+ }
208
+ return { role: m.role, content: m.content };
209
+ });
210
+ }
211
+ // Streaming timeout is longer — the first token may take a few seconds.
212
+ const STREAM_TIMEOUT_MS = 60_000; // 60s for streaming requests
213
+ function buildOllamaClient(baseUrl, model, supportsTools) {
214
+ const chatUrl = baseUrl.replace(/\/+$/, '') + '/api/chat';
215
+ return {
216
+ id: 'ollama',
217
+ supportsTools,
218
+ async chat(messages, tools) {
219
+ // Map ChatMessage roles to Ollama format
220
+ // Ollama uses role: 'system'|'user'|'assistant'|'tool'
221
+ const ollamaMessages = toOllamaMessages(messages);
222
+ const body = {
223
+ model,
224
+ messages: ollamaMessages,
225
+ stream: false,
226
+ };
227
+ // Only send tools if supported and tools are provided
228
+ if (supportsTools && tools && tools.length > 0) {
229
+ body['tools'] = tools;
230
+ }
231
+ let response;
232
+ try {
233
+ response = await fetchWithTimeout(chatUrl, {
234
+ method: 'POST',
235
+ headers: { 'Content-Type': 'application/json' },
236
+ body: JSON.stringify(body),
237
+ });
238
+ }
239
+ catch (err) {
240
+ const msg = err instanceof Error ? err.message : String(err);
241
+ throw new Error(`Ollama fetch failed: ${msg}`);
242
+ }
243
+ if (!response.ok) {
244
+ const errText = await response.text().catch(() => '');
245
+ throw new Error(`Ollama HTTP ${response.status}: ${errText}`);
246
+ }
247
+ let data;
248
+ try {
249
+ data = (await response.json());
250
+ }
251
+ catch {
252
+ throw new Error('Ollama returned non-JSON response');
253
+ }
254
+ const d = data;
255
+ const message = (d['message'] ?? {});
256
+ const content = typeof message['content'] === 'string' ? message['content'] : '';
257
+ // Parse usage from response
258
+ const promptEval = typeof d['prompt_eval_count'] === 'number' ? d['prompt_eval_count'] : 0;
259
+ const evalCount = typeof d['eval_count'] === 'number' ? d['eval_count'] : 0;
260
+ const tokensIn = promptEval > 0
261
+ ? promptEval
262
+ : estimateTokens(messages.map((m) => m.content).join(' '));
263
+ const tokensOut = evalCount > 0 ? evalCount : estimateTokens(content);
264
+ const toolCalls = parseOllamaToolCalls(message);
265
+ return {
266
+ content,
267
+ toolCalls,
268
+ usage: { tokensIn, tokensOut },
269
+ };
270
+ },
271
+ async chatStream(messages, tools, onDelta) {
272
+ // Attempt streaming; fall back to chat() on any error.
273
+ try {
274
+ const ollamaMessages = toOllamaMessages(messages);
275
+ const body = {
276
+ model,
277
+ messages: ollamaMessages,
278
+ stream: true,
279
+ };
280
+ if (supportsTools && tools && tools.length > 0) {
281
+ body['tools'] = tools;
282
+ }
283
+ let response;
284
+ let streamController;
285
+ try {
286
+ const opened = await fetchStream(chatUrl, {
287
+ method: 'POST',
288
+ headers: { 'Content-Type': 'application/json' },
289
+ body: JSON.stringify(body),
290
+ }, STREAM_TIMEOUT_MS);
291
+ response = opened.response;
292
+ streamController = opened.controller;
293
+ // Headers are in; stop the connect timer. The read loop below arms its
294
+ // own per-read idle watchdog via streamController so a stalled body
295
+ // can't hang indefinitely.
296
+ opened.clearConnectTimer();
297
+ }
298
+ catch (err) {
299
+ const msg = err instanceof Error ? err.message : String(err);
300
+ throw new Error(`Ollama stream fetch failed: ${msg}`);
301
+ }
302
+ if (!response.ok) {
303
+ const errText = await response.text().catch(() => '');
304
+ throw new Error(`Ollama stream HTTP ${response.status}: ${errText}`);
305
+ }
306
+ if (!response.body) {
307
+ throw new Error('Ollama stream response has no body');
308
+ }
309
+ // Parse NDJSON: each line is a complete JSON object.
310
+ // Accumulate content, tool_calls from the last message chunk, and usage
311
+ // from the final done=true line.
312
+ let accContent = '';
313
+ let finalMessage = {};
314
+ let tokensIn = 0;
315
+ let tokensOut = 0;
316
+ const reader = response.body.getReader();
317
+ const decoder = new TextDecoder();
318
+ let lineBuffer = '';
319
+ try {
320
+ while (true) {
321
+ const { done, value } = await readWithIdleTimeout(reader, streamController, STREAM_TIMEOUT_MS);
322
+ if (done)
323
+ break;
324
+ lineBuffer += decoder.decode(value, { stream: true });
325
+ // Process all complete lines (delimited by '\n')
326
+ let nlIdx;
327
+ while ((nlIdx = lineBuffer.indexOf('\n')) !== -1) {
328
+ const line = lineBuffer.slice(0, nlIdx).trim();
329
+ lineBuffer = lineBuffer.slice(nlIdx + 1);
330
+ if (!line)
331
+ continue;
332
+ let chunk;
333
+ try {
334
+ chunk = JSON.parse(line);
335
+ }
336
+ catch {
337
+ // Skip malformed lines
338
+ continue;
339
+ }
340
+ const chunkMsg = (chunk['message'] ?? {});
341
+ const delta = typeof chunkMsg['content'] === 'string' ? chunkMsg['content'] : '';
342
+ if (delta) {
343
+ accContent += delta;
344
+ onDelta(delta);
345
+ }
346
+ // The final chunk (done:true) carries usage counters and the
347
+ // complete message (including any tool_calls).
348
+ if (chunk['done'] === true) {
349
+ finalMessage = chunkMsg;
350
+ const promptEval = typeof chunk['prompt_eval_count'] === 'number'
351
+ ? chunk['prompt_eval_count']
352
+ : 0;
353
+ const evalCount = typeof chunk['eval_count'] === 'number' ? chunk['eval_count'] : 0;
354
+ tokensIn =
355
+ promptEval > 0
356
+ ? promptEval
357
+ : estimateTokens(messages.map((m) => m.content).join(' '));
358
+ tokensOut = evalCount > 0 ? evalCount : estimateTokens(accContent);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ finally {
364
+ reader.releaseLock();
365
+ }
366
+ // Estimate usage if we never saw a done line
367
+ if (tokensIn === 0) {
368
+ tokensIn = estimateTokens(messages.map((m) => m.content).join(' '));
369
+ }
370
+ if (tokensOut === 0) {
371
+ tokensOut = estimateTokens(accContent);
372
+ }
373
+ const toolCalls = parseOllamaToolCalls(finalMessage);
374
+ return {
375
+ content: accContent,
376
+ toolCalls,
377
+ usage: { tokensIn, tokensOut },
378
+ };
379
+ }
380
+ catch {
381
+ // Streaming failed — fall back to non-streaming chat() and emit as one delta.
382
+ const result = await this.chat(messages, tools);
383
+ if (result.content) {
384
+ onDelta(result.content);
385
+ }
386
+ return result;
387
+ }
388
+ },
389
+ };
390
+ }
391
+ // ---------------------------------------------------------------------------
392
+ // LM Studio / OpenAI-compat client
393
+ // ---------------------------------------------------------------------------
394
+ /**
395
+ * Parse tool calls from an OpenAI-compat response choice message.
396
+ */
397
+ function parseOpenAIToolCalls(message) {
398
+ const rawCalls = message['tool_calls'];
399
+ if (!Array.isArray(rawCalls) || rawCalls.length === 0)
400
+ return undefined;
401
+ const parsed = [];
402
+ for (const tc of rawCalls) {
403
+ if (typeof tc !== 'object' || tc === null)
404
+ continue;
405
+ const call = tc;
406
+ const id = typeof call['id'] === 'string' ? call['id'] : `call_${parsed.length}`;
407
+ const fn = call['function'];
408
+ if (!fn)
409
+ continue;
410
+ const name = typeof fn['name'] === 'string' ? fn['name'] : 'unknown';
411
+ let args = fn['arguments'] ?? {};
412
+ // Arguments may be a JSON string — parse it
413
+ if (typeof args === 'string') {
414
+ try {
415
+ args = JSON.parse(args);
416
+ }
417
+ catch {
418
+ // Keep as string if parse fails
419
+ }
420
+ }
421
+ parsed.push({ id, name, arguments: args });
422
+ }
423
+ return parsed.length > 0 ? parsed : undefined;
424
+ }
425
+ /**
426
+ * Build OpenAI-compat messages array from ChatMessage[].
427
+ * Reused by both chat() and chatStream() in the LM Studio client.
428
+ */
429
+ function toOpenAIMessages(messages) {
430
+ return messages.map((m) => {
431
+ if (m.role === 'tool') {
432
+ return {
433
+ role: 'tool',
434
+ content: m.content,
435
+ tool_call_id: m.toolCallId ?? '',
436
+ name: m.name,
437
+ };
438
+ }
439
+ return { role: m.role, content: m.content };
440
+ });
441
+ }
442
+ function buildLmStudioClient(baseUrl, model, supportsTools) {
443
+ const chatUrl = baseUrl.replace(/\/+$/, '') + '/v1/chat/completions';
444
+ return {
445
+ id: 'lmstudio',
446
+ supportsTools,
447
+ async chat(messages, tools) {
448
+ // Map ChatMessage roles to OpenAI shape
449
+ const openaiMessages = toOpenAIMessages(messages);
450
+ const body = {
451
+ model,
452
+ messages: openaiMessages,
453
+ stream: false,
454
+ };
455
+ if (supportsTools && tools && tools.length > 0) {
456
+ body['tools'] = tools;
457
+ body['tool_choice'] = 'auto';
458
+ }
459
+ let response;
460
+ try {
461
+ response = await fetchWithTimeout(chatUrl, {
462
+ method: 'POST',
463
+ headers: { 'Content-Type': 'application/json' },
464
+ body: JSON.stringify(body),
465
+ });
466
+ }
467
+ catch (err) {
468
+ const msg = err instanceof Error ? err.message : String(err);
469
+ throw new Error(`LM Studio fetch failed: ${msg}`);
470
+ }
471
+ if (!response.ok) {
472
+ const errText = await response.text().catch(() => '');
473
+ throw new Error(`LM Studio HTTP ${response.status}: ${errText}`);
474
+ }
475
+ let data;
476
+ try {
477
+ data = (await response.json());
478
+ }
479
+ catch {
480
+ throw new Error('LM Studio returned non-JSON response');
481
+ }
482
+ const d = data;
483
+ const choices = Array.isArray(d['choices']) ? d['choices'] : [];
484
+ const firstChoice = (choices[0] ?? {});
485
+ const message = (firstChoice['message'] ?? {});
486
+ const content = typeof message['content'] === 'string' ? message['content'] : '';
487
+ // Parse usage
488
+ const usageRaw = (d['usage'] ?? {});
489
+ const promptTokens = typeof usageRaw['prompt_tokens'] === 'number' ? usageRaw['prompt_tokens'] : 0;
490
+ const completionTokens = typeof usageRaw['completion_tokens'] === 'number' ? usageRaw['completion_tokens'] : 0;
491
+ const tokensIn = promptTokens > 0
492
+ ? promptTokens
493
+ : estimateTokens(messages.map((m) => m.content).join(' '));
494
+ const tokensOut = completionTokens > 0 ? completionTokens : estimateTokens(content);
495
+ const toolCalls = parseOpenAIToolCalls(message);
496
+ return {
497
+ content,
498
+ toolCalls,
499
+ usage: { tokensIn, tokensOut },
500
+ };
501
+ },
502
+ async chatStream(messages, tools, onDelta) {
503
+ // Attempt SSE streaming; fall back to chat() on any error.
504
+ try {
505
+ const openaiMessages = toOpenAIMessages(messages);
506
+ const body = {
507
+ model,
508
+ messages: openaiMessages,
509
+ stream: true,
510
+ };
511
+ if (supportsTools && tools && tools.length > 0) {
512
+ body['tools'] = tools;
513
+ body['tool_choice'] = 'auto';
514
+ }
515
+ let response;
516
+ let streamController;
517
+ try {
518
+ const opened = await fetchStream(chatUrl, {
519
+ method: 'POST',
520
+ headers: { 'Content-Type': 'application/json' },
521
+ body: JSON.stringify(body),
522
+ }, STREAM_TIMEOUT_MS);
523
+ response = opened.response;
524
+ streamController = opened.controller;
525
+ // Headers are in; stop the connect timer. The read loop below arms its
526
+ // own per-read idle watchdog via streamController so a stalled body
527
+ // can't hang indefinitely.
528
+ opened.clearConnectTimer();
529
+ }
530
+ catch (err) {
531
+ const msg = err instanceof Error ? err.message : String(err);
532
+ throw new Error(`LM Studio stream fetch failed: ${msg}`);
533
+ }
534
+ if (!response.ok) {
535
+ const errText = await response.text().catch(() => '');
536
+ throw new Error(`LM Studio stream HTTP ${response.status}: ${errText}`);
537
+ }
538
+ if (!response.body) {
539
+ throw new Error('LM Studio stream response has no body');
540
+ }
541
+ // Parse SSE: lines prefixed with "data: "; "[DONE]" marks end.
542
+ // Accumulate content deltas; collect tool_call fragments from the last
543
+ // non-[DONE] chunk; extract usage from the final chunk when present.
544
+ let accContent = '';
545
+ // tool_calls may arrive as fragments across SSE chunks; accumulate index-keyed.
546
+ const toolCallFragments = {};
547
+ let tokensIn = 0;
548
+ let tokensOut = 0;
549
+ const reader = response.body.getReader();
550
+ const decoder = new TextDecoder();
551
+ let lineBuffer = '';
552
+ try {
553
+ while (true) {
554
+ const { done, value } = await readWithIdleTimeout(reader, streamController, STREAM_TIMEOUT_MS);
555
+ if (done)
556
+ break;
557
+ lineBuffer += decoder.decode(value, { stream: true });
558
+ let nlIdx;
559
+ while ((nlIdx = lineBuffer.indexOf('\n')) !== -1) {
560
+ const raw = lineBuffer.slice(0, nlIdx).trim();
561
+ lineBuffer = lineBuffer.slice(nlIdx + 1);
562
+ // SSE lines either start with "data: " or are blank/comment lines.
563
+ if (!raw.startsWith('data:'))
564
+ continue;
565
+ const payload = raw.slice(5).trim(); // strip "data:" prefix
566
+ if (payload === '[DONE]')
567
+ continue;
568
+ let chunk;
569
+ try {
570
+ chunk = JSON.parse(payload);
571
+ }
572
+ catch {
573
+ continue;
574
+ }
575
+ // Extract usage when present (some providers send on the last chunk).
576
+ const usageRaw = chunk['usage'];
577
+ if (usageRaw) {
578
+ const pt = typeof usageRaw['prompt_tokens'] === 'number'
579
+ ? usageRaw['prompt_tokens']
580
+ : 0;
581
+ const ct = typeof usageRaw['completion_tokens'] === 'number'
582
+ ? usageRaw['completion_tokens']
583
+ : 0;
584
+ if (pt > 0)
585
+ tokensIn = pt;
586
+ if (ct > 0)
587
+ tokensOut = ct;
588
+ }
589
+ const choices = Array.isArray(chunk['choices']) ? chunk['choices'] : [];
590
+ const firstChoice = (choices[0] ?? {});
591
+ const delta = (firstChoice['delta'] ?? {});
592
+ // Accumulate content delta
593
+ const contentChunk = typeof delta['content'] === 'string' ? delta['content'] : '';
594
+ if (contentChunk) {
595
+ accContent += contentChunk;
596
+ onDelta(contentChunk);
597
+ }
598
+ // Accumulate tool_call fragments (OpenAI streaming splits them across chunks)
599
+ if (Array.isArray(delta['tool_calls'])) {
600
+ for (const tc of delta['tool_calls']) {
601
+ if (typeof tc !== 'object' || tc === null)
602
+ continue;
603
+ const t = tc;
604
+ const idx = typeof t['index'] === 'number' ? t['index'] : 0;
605
+ if (!toolCallFragments[idx]) {
606
+ toolCallFragments[idx] = { id: '', name: '', argsStr: '' };
607
+ }
608
+ if (typeof t['id'] === 'string') {
609
+ toolCallFragments[idx].id = t['id'];
610
+ }
611
+ const fn = t['function'];
612
+ if (fn) {
613
+ if (typeof fn['name'] === 'string') {
614
+ toolCallFragments[idx].name += fn['name'];
615
+ }
616
+ if (typeof fn['arguments'] === 'string') {
617
+ toolCallFragments[idx].argsStr += fn['arguments'];
618
+ }
619
+ }
620
+ }
621
+ }
622
+ }
623
+ }
624
+ }
625
+ finally {
626
+ reader.releaseLock();
627
+ }
628
+ // Fill in usage estimates if not reported by the server
629
+ if (tokensIn === 0) {
630
+ tokensIn = estimateTokens(messages.map((m) => m.content).join(' '));
631
+ }
632
+ if (tokensOut === 0) {
633
+ tokensOut = estimateTokens(accContent);
634
+ }
635
+ // Reconstruct tool calls from accumulated fragments
636
+ const toolCalls = Object.keys(toolCallFragments).length > 0
637
+ ? Object.entries(toolCallFragments).map(([, frag]) => {
638
+ let args = frag.argsStr;
639
+ if (frag.argsStr) {
640
+ try {
641
+ args = JSON.parse(frag.argsStr);
642
+ }
643
+ catch {
644
+ // keep as raw string if parse fails
645
+ }
646
+ }
647
+ return {
648
+ id: frag.id || `call_${Math.random().toString(36).slice(2)}`,
649
+ name: frag.name,
650
+ arguments: args,
651
+ };
652
+ })
653
+ : undefined;
654
+ return {
655
+ content: accContent,
656
+ toolCalls,
657
+ usage: { tokensIn, tokensOut },
658
+ };
659
+ }
660
+ catch {
661
+ // Streaming failed — fall back to non-streaming chat() and emit as one delta.
662
+ const result = await this.chat(messages, tools);
663
+ if (result.content) {
664
+ onDelta(result.content);
665
+ }
666
+ return result;
667
+ }
668
+ },
669
+ };
670
+ }
671
+ // ---------------------------------------------------------------------------
672
+ // Public API
673
+ // ---------------------------------------------------------------------------
674
+ /**
675
+ * Build a chat client over the ACTIVE LOCAL provider for `cfg`.
676
+ *
677
+ * LOCAL-FIRST guardrail:
678
+ * - If the active provider is a cloud provider and !allowCloud → throws.
679
+ * - If allowCloud is true but the cloud provider's API key is absent → throws.
680
+ * - If no provider is active → throws.
681
+ *
682
+ * Provider detection:
683
+ * - 'ollama' → POST <ollamaUrl>/api/chat (native Ollama API)
684
+ * - 'lmstudio' → POST <lmstudioUrl>/v1/chat/completions (OpenAI shape)
685
+ *
686
+ * Tool-call detection:
687
+ * - Ollama: enabled for models whose names match known tool-capable patterns.
688
+ * - LM Studio: enabled by default (model capability not reliably detectable
689
+ * without a live call; degrades gracefully on failure).
690
+ *
691
+ * Provider-aware routing (M15):
692
+ * - `opts.provider` forces a specific provider id (e.g. the cloud provider a
693
+ * RouteDecision escalated to) instead of defaulting to registry.activeProvider.
694
+ * This is REQUIRED for cloud escalation to actually target the routed cloud
695
+ * provider rather than silently re-running on the local active provider.
696
+ * The same local-first guards apply: a cloud provider id still throws unless
697
+ * allowCloud AND its key is present (and cloud completions are implemented).
698
+ * - `opts.model` forces the model name (threaded into pickModel) without
699
+ * mutating the shared process.env.ASHLR_MODEL — avoiding the env race under
700
+ * concurrent tasks. ASHLR_MODEL is still honored when opts.model is absent.
701
+ */
702
+ export async function getActiveClient(cfg, opts) {
703
+ const registry = await getProviderRegistry(cfg);
704
+ // Honor an explicitly-requested provider (router decision) over the chain's
705
+ // first-up provider. This is what makes a cloud escalation actually route to
706
+ // the cloud provider instead of falling through to the local active one.
707
+ const activeId = opts.provider ?? registry.activeProvider;
708
+ if (activeId === null) {
709
+ throw new Error('local-first: no provider is reachable. Start Ollama or LM Studio, or pass --allow-cloud to use a cloud provider.');
710
+ }
711
+ // ---- Cloud-provider guard ----
712
+ if (isCloudProvider(activeId)) {
713
+ if (!opts.allowCloud) {
714
+ throw new Error(`local-first: no local model available; the active provider is '${activeId}' (cloud). ` +
715
+ `Pass --allow-cloud to use it.`);
716
+ }
717
+ // allowCloud is true — verify the API key is present
718
+ const envVar = CLOUD_PROVIDER_ENV[activeId.toLowerCase()];
719
+ if (envVar) {
720
+ const key = process.env[envVar];
721
+ if (!key || key.trim().length === 0) {
722
+ throw new Error(`local-first: --allow-cloud passed but ${envVar} is not set. ` +
723
+ `Set the API key for '${activeId}' and retry.`);
724
+ }
725
+ }
726
+ // Cloud provider with key — not yet implemented in this client (local-first focus).
727
+ // Throw a clear informational error rather than silently doing nothing.
728
+ throw new Error(`local-first: cloud provider '${activeId}' was requested (--allow-cloud) but ` +
729
+ `this client does not yet implement cloud completions. ` +
730
+ `Configure Ollama or LM Studio for local inference.`);
731
+ }
732
+ // ---- Local provider: Ollama ----
733
+ if (activeId === 'ollama') {
734
+ const endpoint = registry.providers.find((p) => p.id === 'ollama');
735
+ if (!endpoint?.up) {
736
+ throw new Error(`local-first: Ollama was selected as the active provider but is not reachable at ${cfg.models.ollama}. ` +
737
+ `Ensure Ollama is running ('ollama serve').`);
738
+ }
739
+ const model = pickModel(endpoint.models, opts.model);
740
+ const supportsTools = modelSupportsTools(model);
741
+ const baseUrl = cfg.models.ollama.replace(/\/+$/, '');
742
+ return buildOllamaClient(baseUrl, model, supportsTools);
743
+ }
744
+ // ---- Local provider: LM Studio ----
745
+ if (activeId === 'lmstudio') {
746
+ const endpoint = registry.providers.find((p) => p.id === 'lmstudio');
747
+ if (!endpoint?.up) {
748
+ throw new Error(`local-first: LM Studio was selected as the active provider but is not reachable at ${cfg.models.lmstudio}. ` +
749
+ `Ensure LM Studio is running with the local server enabled.`);
750
+ }
751
+ const model = pickModel(endpoint.models, opts.model);
752
+ // LM Studio (OpenAI compat) — assume tool support; degrade on error in chat()
753
+ const supportsTools = true;
754
+ const baseUrl = cfg.models.lmstudio.replace(/\/+$/, '');
755
+ return buildLmStudioClient(baseUrl, model, supportsTools);
756
+ }
757
+ // ---- M33: plugin-contributed provider ----
758
+ // Strictly additive: this branch is reachable ONLY when the resolved id
759
+ // matches an enabled plugin provider — builtin routing above is untouched.
760
+ // Cloud-tier plugin providers sit behind the SAME local-first gates as
761
+ // builtin cloud providers (--allow-cloud + declared key env present).
762
+ try {
763
+ const { getPluginProviders } = await import('../plugins/registry.js');
764
+ const pluginProviders = await getPluginProviders(cfg);
765
+ const spec = pluginProviders.find((p) => p.id === activeId);
766
+ if (spec) {
767
+ if (spec.tier === 'cloud') {
768
+ if (!opts.allowCloud) {
769
+ throw new Error(`local-first: provider '${activeId}' is a cloud-tier plugin provider. Pass --allow-cloud to use it.`);
770
+ }
771
+ for (const envKey of spec.envKeys ?? []) {
772
+ const v = process.env[envKey];
773
+ if (!v || v.trim().length === 0) {
774
+ throw new Error(`local-first: --allow-cloud passed but ${envKey} is not set for plugin provider '${activeId}'.`);
775
+ }
776
+ }
777
+ }
778
+ const client = (await spec.createClient({ model: opts.model }));
779
+ // Runtime shape check — a plugin returning garbage must fail loudly here,
780
+ // not deep inside the agent loop.
781
+ if (!client || typeof client.chat !== 'function' || typeof client.id !== 'string') {
782
+ throw new Error(`plugin provider '${activeId}' returned an invalid client (missing id/chat).`);
783
+ }
784
+ return client;
785
+ }
786
+ }
787
+ catch (err) {
788
+ // A real gate/validation error must surface; only a missing plugin layer
789
+ // falls through to the unknown-provider error below.
790
+ if (err instanceof Error && !err.message.includes('Cannot find module'))
791
+ throw err;
792
+ }
793
+ // ---- Unknown local provider ----
794
+ throw new Error(`local-first: unknown provider '${activeId}'. Only 'ollama', 'lmstudio', and enabled plugin providers are supported.`);
795
+ }
796
+ //# sourceMappingURL=provider-client.js.map