@crouton-kit/crouter 0.3.31 → 0.3.33

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 (443) hide show
  1. package/README.md +8 -0
  2. package/dist/build-root.js +1 -0
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -2
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +2 -2
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +5 -5
  6. package/dist/builtin-memory/crouter-development/personas.md +3 -4
  7. package/dist/builtin-memory/crouter-development/plugins.md +18 -18
  8. package/dist/builtin-memory/design.md +4 -1
  9. package/dist/builtin-memory/development.md +4 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/nodes-and-canvas.md +6 -6
  12. package/dist/builtin-memory/internal/storage-tiers.md +5 -5
  13. package/dist/builtin-memory/planning.md +4 -1
  14. package/dist/builtin-memory/product.md +80 -0
  15. package/dist/builtin-memory/spec.md +4 -1
  16. package/dist/builtin-personas/advisor/PERSONA.md +10 -0
  17. package/dist/builtin-personas/design/PERSONA.md +2 -2
  18. package/dist/builtin-personas/design/orchestrator.md +3 -3
  19. package/dist/builtin-personas/developer/PERSONA.md +1 -1
  20. package/dist/builtin-personas/developer/orchestrator.md +2 -2
  21. package/dist/builtin-personas/explore/PERSONA.md +3 -3
  22. package/dist/builtin-personas/explore/orchestrator.md +4 -2
  23. package/dist/builtin-personas/general/PERSONA.md +1 -1
  24. package/dist/builtin-personas/general/orchestrator.md +1 -1
  25. package/dist/builtin-personas/orchestration-kernel.md +7 -14
  26. package/dist/builtin-personas/plan/PERSONA.md +1 -1
  27. package/dist/builtin-personas/plan/orchestrator.md +2 -2
  28. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -1
  29. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -1
  30. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -1
  31. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -1
  32. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -1
  33. package/dist/builtin-personas/product/PERSONA.md +18 -0
  34. package/dist/builtin-personas/product/orchestrator.md +14 -0
  35. package/dist/builtin-personas/product/teardown/PERSONA.md +13 -0
  36. package/dist/builtin-personas/review/PERSONA.md +1 -1
  37. package/dist/builtin-personas/review/orchestrator.md +1 -1
  38. package/dist/builtin-personas/runtime-base.md +5 -0
  39. package/dist/builtin-personas/spec/PERSONA.md +1 -1
  40. package/dist/builtin-personas/spec/orchestrator.md +2 -2
  41. package/dist/builtin-personas/spec/requirements/PERSONA.md +1 -1
  42. package/dist/builtin-views/canvas/core.mjs +82 -1
  43. package/dist/builtin-views/canvas/tui.mjs +9 -5
  44. package/dist/builtin-views/canvas/web.jsx +3 -2
  45. package/dist/builtin-views/chat/core.mjs +725 -0
  46. package/dist/builtin-views/chat/text.mjs +101 -0
  47. package/dist/builtin-views/chat/tui.mjs +368 -0
  48. package/dist/builtin-views/chat/web.jsx +367 -0
  49. package/dist/builtin-views/prompt-review/core.mjs +863 -0
  50. package/dist/builtin-views/prompt-review/text.mjs +15 -0
  51. package/dist/builtin-views/prompt-review/tui.mjs +196 -0
  52. package/dist/builtin-views/prompt-review/web.jsx +484 -0
  53. package/dist/builtin-views/settings/core.mjs +397 -0
  54. package/dist/builtin-views/settings/text.mjs +40 -0
  55. package/dist/builtin-views/settings/tui.mjs +95 -0
  56. package/dist/builtin-views/settings/web.jsx +167 -0
  57. package/dist/cli.js +16 -3
  58. package/dist/clients/attach/__tests__/autocomplete-and-bash-mode.test.js +96 -0
  59. package/dist/clients/attach/__tests__/bash-bang-routing.test.js +58 -0
  60. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.d.ts +1 -0
  61. package/dist/clients/attach/__tests__/bundle-pi-tui-dedup.test.js +56 -0
  62. package/dist/clients/attach/__tests__/titled-editor.test.js +22 -1
  63. package/dist/clients/attach/attach-cmd.d.ts +18 -0
  64. package/dist/clients/attach/attach-cmd.js +1723 -795
  65. package/dist/clients/attach/canvas-panels.js +2 -2
  66. package/dist/clients/attach/chat-view.d.ts +10 -0
  67. package/dist/clients/attach/chat-view.js +23 -0
  68. package/dist/clients/attach/config-load.d.ts +8 -4
  69. package/dist/clients/attach/config-load.js +2 -0
  70. package/dist/clients/attach/extension-dialogs.d.ts +2 -3
  71. package/dist/clients/attach/extension-dialogs.js +7 -9
  72. package/dist/clients/attach/graph-overlay.js +3 -2
  73. package/dist/clients/attach/input-controller.d.ts +4 -3
  74. package/dist/clients/attach/input-controller.js +24 -3
  75. package/dist/clients/attach/slash-commands.d.ts +7 -7
  76. package/dist/clients/attach/slash-commands.js +38 -14
  77. package/dist/clients/attach/titled-editor.js +18 -14
  78. package/dist/clients/attach/view-socket.d.ts +2 -1
  79. package/dist/clients/attach/view-socket.js +27 -8
  80. package/dist/clients/web/__tests__/source-cache.test.d.ts +1 -0
  81. package/dist/clients/web/__tests__/source-cache.test.js +32 -0
  82. package/dist/clients/web/dev-server.js +1 -0
  83. package/dist/clients/web/events.js +9 -11
  84. package/dist/clients/web/server.d.ts +4 -3
  85. package/dist/clients/web/server.js +138 -31
  86. package/dist/clients/web/source-cache.d.ts +10 -0
  87. package/dist/clients/web/source-cache.js +57 -0
  88. package/dist/clients/web/web-cmd.js +28 -9
  89. package/dist/commands/__tests__/human.test.js +30 -28
  90. package/dist/commands/canvas-history/search.js +1 -1
  91. package/dist/commands/canvas-history.js +5 -8
  92. package/dist/commands/canvas-issue.d.ts +2 -0
  93. package/dist/commands/canvas-issue.js +148 -0
  94. package/dist/commands/canvas-prune.js +19 -0
  95. package/dist/commands/canvas-rebuild-index.d.ts +2 -0
  96. package/dist/commands/canvas-rebuild-index.js +57 -0
  97. package/dist/commands/canvas-snapshot.d.ts +2 -0
  98. package/dist/commands/canvas-snapshot.js +48 -0
  99. package/dist/commands/canvas-tmux-spread.d.ts +2 -0
  100. package/dist/commands/canvas-tmux-spread.js +188 -0
  101. package/dist/commands/canvas.d.ts +1 -0
  102. package/dist/commands/canvas.js +16 -1
  103. package/dist/commands/chord.js +143 -48
  104. package/dist/commands/daemon.js +3 -3
  105. package/dist/commands/human/prompts.d.ts +0 -1
  106. package/dist/commands/human/prompts.js +39 -54
  107. package/dist/commands/human/queue.d.ts +12 -1
  108. package/dist/commands/human/queue.js +468 -73
  109. package/dist/commands/human/shared.d.ts +3 -4
  110. package/dist/commands/human/shared.js +3 -3
  111. package/dist/commands/human.js +11 -10
  112. package/dist/commands/memory/find.js +3 -2
  113. package/dist/commands/memory/list.js +4 -3
  114. package/dist/commands/memory/read.js +3 -3
  115. package/dist/commands/memory/shared.d.ts +1 -4
  116. package/dist/commands/memory/shared.js +6 -9
  117. package/dist/commands/memory/write.js +3 -3
  118. package/dist/commands/memory.js +4 -7
  119. package/dist/commands/node-context.d.ts +2 -0
  120. package/dist/commands/node-context.js +172 -0
  121. package/dist/commands/node-snapshot.d.ts +2 -0
  122. package/dist/commands/node-snapshot.js +123 -0
  123. package/dist/commands/node.js +228 -52
  124. package/dist/commands/pkg/plugin-inspect.js +6 -6
  125. package/dist/commands/pkg/plugin-manage.js +1 -1
  126. package/dist/commands/pkg/plugin.js +2 -2
  127. package/dist/commands/pkg.js +2 -2
  128. package/dist/commands/push.d.ts +2 -0
  129. package/dist/commands/push.js +79 -11
  130. package/dist/commands/revive.js +74 -1
  131. package/dist/commands/search/answer.d.ts +1 -0
  132. package/dist/commands/search/answer.js +50 -0
  133. package/dist/commands/search/contents.d.ts +1 -0
  134. package/dist/commands/search/contents.js +96 -0
  135. package/dist/commands/search/exa.d.ts +53 -0
  136. package/dist/commands/search/exa.js +155 -0
  137. package/dist/commands/search/puremd.d.ts +11 -0
  138. package/dist/commands/search/puremd.js +53 -0
  139. package/dist/commands/search/web.d.ts +1 -0
  140. package/dist/commands/search/web.js +65 -0
  141. package/dist/commands/search.d.ts +2 -0
  142. package/dist/commands/search.js +24 -0
  143. package/dist/commands/sys/__tests__/sync-import.test.d.ts +1 -0
  144. package/dist/commands/sys/__tests__/sync-import.test.js +72 -0
  145. package/dist/commands/sys/config.js +59 -10
  146. package/dist/commands/sys/doctor.js +56 -4
  147. package/dist/commands/sys/prompt-review.d.ts +1 -0
  148. package/dist/commands/sys/prompt-review.js +178 -0
  149. package/dist/commands/sys/promptstudio.d.ts +2 -0
  150. package/dist/commands/sys/promptstudio.js +65 -0
  151. package/dist/commands/sys/settings.d.ts +2 -0
  152. package/dist/commands/sys/settings.js +16 -0
  153. package/dist/commands/sys/sync.js +311 -159
  154. package/dist/commands/sys/sysprompt.d.ts +1 -0
  155. package/dist/commands/sys/sysprompt.js +86 -0
  156. package/dist/commands/sys.js +9 -5
  157. package/dist/commands/view-new.js +2 -2
  158. package/dist/core/__tests__/artifact-paths.test.d.ts +1 -0
  159. package/dist/core/__tests__/artifact-paths.test.js +44 -0
  160. package/dist/core/__tests__/broker-preflight.test.d.ts +1 -0
  161. package/dist/core/__tests__/broker-preflight.test.js +85 -0
  162. package/dist/core/__tests__/broker-sdk-wiring.test.js +15 -0
  163. package/dist/core/__tests__/canvas.test.js +37 -1
  164. package/dist/core/__tests__/chat-view-reconnect.test.d.ts +1 -0
  165. package/dist/core/__tests__/chat-view-reconnect.test.js +70 -0
  166. package/dist/core/__tests__/child-death-wake.test.js +11 -2
  167. package/dist/core/__tests__/close.test.js +39 -1
  168. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +1 -0
  169. package/dist/core/__tests__/connection-reconnect-resume.test.js +37 -0
  170. package/dist/core/__tests__/context-intro.test.js +48 -14
  171. package/dist/core/__tests__/daemon-boot.test.js +182 -9
  172. package/dist/core/__tests__/editor-label.test.d.ts +1 -0
  173. package/dist/core/__tests__/editor-label.test.js +26 -0
  174. package/dist/core/__tests__/fault-marker.test.d.ts +1 -0
  175. package/dist/core/__tests__/fault-marker.test.js +112 -0
  176. package/dist/core/__tests__/fault-retry-rewind.test.d.ts +1 -0
  177. package/dist/core/__tests__/fault-retry-rewind.test.js +123 -0
  178. package/dist/core/__tests__/fixtures/fake-engine.d.ts +30 -4
  179. package/dist/core/__tests__/fixtures/fake-engine.js +156 -17
  180. package/dist/core/__tests__/fixtures/fake-pi-host.js +10 -1
  181. package/dist/core/__tests__/full/human-new-window-regression.test.js +1 -1
  182. package/dist/core/__tests__/helpers/harness.d.ts +1 -1
  183. package/dist/core/__tests__/helpers/harness.js +9 -3
  184. package/dist/core/__tests__/human-stranded-deliver.test.js +38 -1
  185. package/dist/core/__tests__/human-surface-target.test.js +1 -1
  186. package/dist/core/__tests__/kickoff.test.js +18 -19
  187. package/dist/core/__tests__/memory-resolver.test.js +12 -1
  188. package/dist/core/__tests__/migration.test.js +49 -14
  189. package/dist/core/__tests__/model-ladders.test.d.ts +1 -0
  190. package/dist/core/__tests__/model-ladders.test.js +160 -0
  191. package/dist/core/__tests__/node-env.test.d.ts +1 -0
  192. package/dist/core/__tests__/node-env.test.js +26 -0
  193. package/dist/core/__tests__/push-final-guard.test.js +7 -1
  194. package/dist/core/__tests__/relaunch-root.test.js +9 -9
  195. package/dist/core/__tests__/revive.test.js +36 -6
  196. package/dist/core/__tests__/spawn-root.test.js +24 -1
  197. package/dist/core/__tests__/stop-guard.test.js +10 -0
  198. package/dist/core/__tests__/stranded-relaunch.test.d.ts +1 -0
  199. package/dist/core/__tests__/stranded-relaunch.test.js +72 -0
  200. package/dist/core/__tests__/tmux-surface.test.js +5 -1
  201. package/dist/core/__tests__/unknown-path.test.js +19 -19
  202. package/dist/core/__tests__/yield-ensures-daemon.test.d.ts +1 -0
  203. package/dist/core/__tests__/yield-ensures-daemon.test.js +54 -0
  204. package/dist/core/artifact.d.ts +2 -33
  205. package/dist/core/artifact.js +27 -87
  206. package/dist/core/bootstrap.js +2 -0
  207. package/dist/core/canvas/browse/__tests__/model.test.js +35 -1
  208. package/dist/core/canvas/browse/app.js +87 -17
  209. package/dist/core/canvas/browse/model.d.ts +10 -1
  210. package/dist/core/canvas/browse/model.js +37 -1
  211. package/dist/core/canvas/browse/render.js +35 -33
  212. package/dist/core/canvas/canvas.d.ts +7 -0
  213. package/dist/core/canvas/canvas.js +54 -5
  214. package/dist/core/canvas/db.js +35 -8
  215. package/dist/core/canvas/labels.d.ts +6 -8
  216. package/dist/core/canvas/labels.js +8 -11
  217. package/dist/core/canvas/nav-model.d.ts +5 -4
  218. package/dist/core/canvas/nav-model.js +31 -17
  219. package/dist/core/canvas/paths.d.ts +18 -1
  220. package/dist/core/canvas/paths.js +37 -2
  221. package/dist/core/canvas/render.d.ts +25 -0
  222. package/dist/core/canvas/render.js +96 -13
  223. package/dist/core/canvas/status-glyph.d.ts +35 -0
  224. package/dist/core/canvas/status-glyph.js +104 -0
  225. package/dist/core/canvas/types.d.ts +2 -2
  226. package/dist/core/config.js +114 -16
  227. package/dist/core/fault-classifier.d.ts +41 -0
  228. package/dist/core/fault-classifier.js +110 -0
  229. package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +1 -0
  230. package/dist/core/feed/__tests__/empty-node-guard.test.js +45 -0
  231. package/dist/core/feed/feed.js +8 -0
  232. package/dist/core/feed/inbox.d.ts +15 -0
  233. package/dist/core/feed/inbox.js +40 -14
  234. package/dist/core/frontmatter.d.ts +1 -11
  235. package/dist/core/frontmatter.js +1 -55
  236. package/dist/core/hearth/config.d.ts +2 -0
  237. package/dist/core/hearth/config.js +75 -0
  238. package/dist/core/hearth/guest-env.d.ts +2 -0
  239. package/dist/core/hearth/guest-env.js +11 -0
  240. package/dist/core/hearth/index.d.ts +5 -0
  241. package/dist/core/hearth/index.js +5 -0
  242. package/dist/core/hearth/provider.d.ts +21 -0
  243. package/dist/core/hearth/provider.js +10 -0
  244. package/dist/core/hearth/providers/blaxel-bootstrap.d.ts +10 -0
  245. package/dist/core/hearth/providers/blaxel-bootstrap.js +39 -0
  246. package/dist/core/hearth/providers/blaxel-home.d.ts +11 -0
  247. package/dist/core/hearth/providers/blaxel-home.js +126 -0
  248. package/dist/core/hearth/providers/blaxel.d.ts +26 -0
  249. package/dist/core/hearth/providers/blaxel.js +208 -0
  250. package/dist/core/hearth/providers/types.d.ts +72 -0
  251. package/dist/core/hearth/providers/types.js +1 -0
  252. package/dist/core/hearth/registry.d.ts +15 -0
  253. package/dist/core/hearth/registry.js +179 -0
  254. package/dist/core/hearth/types.d.ts +121 -0
  255. package/dist/core/hearth/types.js +1 -0
  256. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +1 -0
  257. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +79 -0
  258. package/dist/core/{skill-sync → host-exports}/builtins.d.ts +21 -13
  259. package/dist/core/host-exports/builtins.js +68 -0
  260. package/dist/core/{skill-sync → host-exports}/export.d.ts +9 -6
  261. package/dist/core/{skill-sync → host-exports}/export.js +37 -14
  262. package/dist/core/io.js +1 -1
  263. package/dist/core/log.d.ts +9 -0
  264. package/dist/core/log.js +113 -0
  265. package/dist/core/memory-resolver.d.ts +8 -8
  266. package/dist/core/memory-resolver.js +15 -28
  267. package/dist/core/personas/index.d.ts +4 -4
  268. package/dist/core/personas/index.js +2 -2
  269. package/dist/core/personas/loader.d.ts +51 -0
  270. package/dist/core/personas/loader.js +54 -50
  271. package/dist/core/personas/resolve.d.ts +43 -27
  272. package/dist/core/personas/resolve.js +336 -94
  273. package/dist/core/runtime/auth-reload.d.ts +7 -0
  274. package/dist/core/runtime/auth-reload.js +69 -0
  275. package/dist/core/runtime/bearings.d.ts +35 -15
  276. package/dist/core/runtime/bearings.js +305 -44
  277. package/dist/core/runtime/branded-host.d.ts +4 -2
  278. package/dist/core/runtime/branded-host.js +66 -4
  279. package/dist/core/runtime/broker-protocol.d.ts +38 -3
  280. package/dist/core/runtime/broker.js +343 -18
  281. package/dist/core/runtime/close.js +37 -29
  282. package/dist/core/runtime/connectivity.d.ts +12 -0
  283. package/dist/core/runtime/connectivity.js +73 -0
  284. package/dist/core/runtime/fault-recovery-nudge.d.ts +2 -0
  285. package/dist/core/runtime/fault-recovery-nudge.js +2 -0
  286. package/dist/core/runtime/fault-recovery.d.ts +23 -0
  287. package/dist/core/runtime/fault-recovery.js +74 -0
  288. package/dist/core/runtime/fault.d.ts +25 -0
  289. package/dist/core/runtime/fault.js +176 -0
  290. package/dist/core/runtime/front-door.d.ts +1 -1
  291. package/dist/core/runtime/front-door.js +2 -2
  292. package/dist/core/runtime/host.d.ts +10 -0
  293. package/dist/core/runtime/host.js +71 -9
  294. package/dist/core/runtime/kickoff.js +31 -45
  295. package/dist/core/runtime/launch.d.ts +32 -18
  296. package/dist/core/runtime/launch.js +148 -43
  297. package/dist/core/runtime/model-swap.d.ts +18 -0
  298. package/dist/core/runtime/model-swap.js +95 -0
  299. package/dist/core/runtime/naming.d.ts +7 -0
  300. package/dist/core/runtime/naming.js +61 -9
  301. package/dist/core/runtime/nodes.js +6 -1
  302. package/dist/core/runtime/persona.js +16 -17
  303. package/dist/core/runtime/placement.d.ts +36 -31
  304. package/dist/core/runtime/placement.js +149 -64
  305. package/dist/core/runtime/promote.d.ts +2 -0
  306. package/dist/core/runtime/promote.js +23 -3
  307. package/dist/core/runtime/recycle.js +6 -4
  308. package/dist/core/runtime/reset.d.ts +5 -2
  309. package/dist/core/runtime/reset.js +2 -2
  310. package/dist/core/runtime/revive.d.ts +4 -0
  311. package/dist/core/runtime/revive.js +27 -9
  312. package/dist/core/runtime/roadmap.d.ts +1 -1
  313. package/dist/core/runtime/roadmap.js +1 -1
  314. package/dist/core/runtime/spawn.d.ts +2 -2
  315. package/dist/core/runtime/spawn.js +30 -6
  316. package/dist/core/runtime/stop-guard.js +6 -1
  317. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  318. package/dist/core/runtime/tmux-chrome.js +1 -1
  319. package/dist/core/runtime/tmux.d.ts +29 -6
  320. package/dist/core/runtime/tmux.js +134 -80
  321. package/dist/core/runtime/view-socket-client.d.ts +46 -0
  322. package/dist/core/runtime/view-socket-client.js +203 -0
  323. package/dist/core/substrate/index.d.ts +1 -1
  324. package/dist/core/substrate/index.js +1 -1
  325. package/dist/core/substrate/on-read.js +16 -4
  326. package/dist/core/substrate/render.d.ts +10 -13
  327. package/dist/core/substrate/render.js +56 -51
  328. package/dist/core/tui/host.js +83 -18
  329. package/dist/core/view/__tests__/transport-cache.test.d.ts +1 -0
  330. package/dist/core/view/__tests__/transport-cache.test.js +62 -0
  331. package/dist/core/view/contract.d.ts +20 -0
  332. package/dist/core/view/stream-local.d.ts +3 -0
  333. package/dist/core/view/stream-local.js +184 -0
  334. package/dist/core/view/transport-cache.d.ts +8 -0
  335. package/dist/core/view/transport-cache.js +38 -0
  336. package/dist/core/view/transport-local.js +4 -0
  337. package/dist/core/view/transport.d.ts +7 -1
  338. package/dist/daemon/crtrd.d.ts +3 -19
  339. package/dist/daemon/crtrd.js +139 -178
  340. package/dist/daemon/manage.d.ts +18 -1
  341. package/dist/daemon/manage.js +54 -9
  342. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.d.ts +1 -0
  343. package/dist/hearth/wake-proxy/__tests__/anthropic-oauth.test.js +289 -0
  344. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.d.ts +1 -0
  345. package/dist/hearth/wake-proxy/__tests__/config-timeout.test.js +34 -0
  346. package/dist/hearth/wake-proxy/__tests__/guest-source.test.d.ts +1 -0
  347. package/dist/hearth/wake-proxy/__tests__/guest-source.test.js +203 -0
  348. package/dist/hearth/wake-proxy/__tests__/hardening.test.d.ts +1 -0
  349. package/dist/hearth/wake-proxy/__tests__/hardening.test.js +59 -0
  350. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.d.ts +1 -0
  351. package/dist/hearth/wake-proxy/__tests__/hearth-status-route.test.js +372 -0
  352. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.d.ts +1 -0
  353. package/dist/hearth/wake-proxy/__tests__/http-running-route-fast-path.test.js +258 -0
  354. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.d.ts +1 -0
  355. package/dist/hearth/wake-proxy/__tests__/model-auth-routes.test.js +437 -0
  356. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.d.ts +1 -0
  357. package/dist/hearth/wake-proxy/__tests__/source-bridge-gate.test.js +455 -0
  358. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.d.ts +1 -0
  359. package/dist/hearth/wake-proxy/__tests__/static-asset-allowlist.test.js +15 -0
  360. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.d.ts +1 -0
  361. package/dist/hearth/wake-proxy/__tests__/warm-path-concurrency.test.js +141 -0
  362. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.d.ts +1 -0
  363. package/dist/hearth/wake-proxy/__tests__/ws-teardown-accounting.test.js +143 -0
  364. package/dist/hearth/wake-proxy/anthropic-oauth.d.ts +58 -0
  365. package/dist/hearth/wake-proxy/anthropic-oauth.js +189 -0
  366. package/dist/hearth/wake-proxy/auth.d.ts +22 -0
  367. package/dist/hearth/wake-proxy/auth.js +128 -0
  368. package/dist/hearth/wake-proxy/config.d.ts +2 -0
  369. package/dist/hearth/wake-proxy/config.js +151 -0
  370. package/dist/hearth/wake-proxy/guest-source.d.ts +14 -0
  371. package/dist/hearth/wake-proxy/guest-source.js +130 -0
  372. package/dist/hearth/wake-proxy/hearth-status.d.ts +44 -0
  373. package/dist/hearth/wake-proxy/hearth-status.js +267 -0
  374. package/dist/hearth/wake-proxy/home.d.ts +67 -0
  375. package/dist/hearth/wake-proxy/home.js +297 -0
  376. package/dist/hearth/wake-proxy/main.d.ts +1 -0
  377. package/dist/hearth/wake-proxy/main.js +134 -0
  378. package/dist/hearth/wake-proxy/model-auth.d.ts +13 -0
  379. package/dist/hearth/wake-proxy/model-auth.js +345 -0
  380. package/dist/hearth/wake-proxy/proxy.d.ts +35 -0
  381. package/dist/hearth/wake-proxy/proxy.js +716 -0
  382. package/dist/hearth/wake-proxy/public-source-gate.d.ts +9 -0
  383. package/dist/hearth/wake-proxy/public-source-gate.js +409 -0
  384. package/dist/hearth/wake-proxy/redact.d.ts +1 -0
  385. package/dist/hearth/wake-proxy/redact.js +17 -0
  386. package/dist/hearth/wake-proxy/server.d.ts +11 -0
  387. package/dist/hearth/wake-proxy/server.js +142 -0
  388. package/dist/hearth/wake-proxy/state.d.ts +18 -0
  389. package/dist/hearth/wake-proxy/state.js +342 -0
  390. package/dist/hearth/wake-proxy/types.d.ts +76 -0
  391. package/dist/hearth/wake-proxy/types.js +1 -0
  392. package/dist/index.js +3 -2
  393. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +6 -5
  394. package/dist/pi-extensions/canvas-context-intro.js +7 -7
  395. package/dist/pi-extensions/canvas-doc-substrate.js +19 -22
  396. package/dist/pi-extensions/canvas-nav.js +15 -8
  397. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  398. package/dist/pi-extensions/canvas-stophook.js +78 -11
  399. package/dist/types.d.ts +39 -20
  400. package/dist/types.js +38 -13
  401. package/dist/web/runtime.js +141 -42
  402. package/dist/web/transport-http.js +7 -5
  403. package/dist/web/transport-stream.d.ts +3 -0
  404. package/dist/web/transport-stream.js +204 -0
  405. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  406. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  407. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  408. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  409. package/dist/web-client/assets/fraunces-latin-ext-wght-normal-Ca2vKHc0.woff2 +0 -0
  410. package/dist/web-client/assets/fraunces-latin-wght-normal-ukD16Tqj.woff2 +0 -0
  411. package/dist/web-client/assets/fraunces-vietnamese-wght-normal-CnvboYUG.woff2 +0 -0
  412. package/dist/web-client/assets/index-BZUxTkv5.css +2 -0
  413. package/dist/web-client/assets/index-D36PNBj4.js +80 -0
  414. package/dist/web-client/assets/instrument-sans-latin-ext-wght-normal-B5bTHO_g.woff2 +0 -0
  415. package/dist/web-client/assets/instrument-sans-latin-wght-normal-BbzFLZTg.woff2 +0 -0
  416. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  417. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  418. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  419. package/dist/web-client/index.html +3 -2
  420. package/package.json +32 -10
  421. package/dist/core/__tests__/error-stall-recycle.test.js +0 -141
  422. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +0 -57
  423. package/dist/core/skill-sync/builtins.js +0 -112
  424. package/dist/core/skill-sync/claude-plugins.d.ts +0 -23
  425. package/dist/core/skill-sync/claude-plugins.js +0 -71
  426. package/dist/core/skill-sync/engine.d.ts +0 -42
  427. package/dist/core/skill-sync/engine.js +0 -633
  428. package/dist/core/skill-sync/manifest.d.ts +0 -64
  429. package/dist/core/skill-sync/manifest.js +0 -181
  430. package/dist/core/skill-sync/profile.d.ts +0 -76
  431. package/dist/core/skill-sync/profile.js +0 -173
  432. package/dist/core/skill-sync/snapshot.d.ts +0 -57
  433. package/dist/core/skill-sync/snapshot.js +0 -120
  434. package/dist/pi-extensions/canvas-commands.d.ts +0 -37
  435. package/dist/pi-extensions/canvas-commands.js +0 -113
  436. package/dist/pi-extensions/canvas-resume.d.ts +0 -21
  437. package/dist/pi-extensions/canvas-resume.js +0 -83
  438. package/dist/pi-extensions/canvas-view.d.ts +0 -21
  439. package/dist/pi-extensions/canvas-view.js +0 -76
  440. package/dist/web-client/assets/index-BUvQb4hR.css +0 -2
  441. package/dist/web-client/assets/index-ClLQXYAE.js +0 -10
  442. /package/dist/{core/__tests__/error-stall-recycle.test.d.ts → clients/attach/__tests__/autocomplete-and-bash-mode.test.d.ts} +0 -0
  443. /package/dist/{core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts → clients/attach/__tests__/bash-bang-routing.test.d.ts} +0 -0
@@ -14,7 +14,7 @@ import { defineBranch, defineLeaf } from '../core/command.js';
14
14
  import { InputError, readStdinRaw } from '../core/io.js';
15
15
  import { push } from '../core/feed/feed.js';
16
16
  import { getNode, subscribersOf, subscriptionsOf, fullName } from '../core/canvas/index.js';
17
- import { readInboxSince, readCursor, writeCursor, coalesce, } from '../core/feed/inbox.js';
17
+ import { readInboxSince, readInboxEntryById, readCursor, writeCursor, coalesce, } from '../core/feed/inbox.js';
18
18
  function requireCallerNode() {
19
19
  const id = process.env['CRTR_NODE_ID'];
20
20
  if (id === undefined || id.trim() === '') {
@@ -26,6 +26,25 @@ function requireCallerNode() {
26
26
  }
27
27
  return id.trim();
28
28
  }
29
+ // ---------------------------------------------------------------------------
30
+ // push <tier> — one leaf per tier, sharing body resolution
31
+ // ---------------------------------------------------------------------------
32
+ function alreadyFinalizedError(nodeStatus) {
33
+ return new InputError({
34
+ error: 'already_finalized',
35
+ message: `This node is already ${nodeStatus} — \`push final\` finishes a node once and cannot finalize it again.`,
36
+ next: 'Nothing to do here: the node is already finished. Do not push again from this node.',
37
+ });
38
+ }
39
+ export function mapFinalizeTransitionRaceError(nodeStatus, err) {
40
+ if (!(err instanceof Error))
41
+ return null;
42
+ if (!err.message.startsWith("illegal lifecycle transition: 'finalize' from status='"))
43
+ return null;
44
+ if (nodeStatus !== 'done' && nodeStatus !== 'dead' && nodeStatus !== 'canceled')
45
+ return null;
46
+ return alreadyFinalizedError(nodeStatus);
47
+ }
29
48
  const TIER_BLURB = {
30
49
  update: 'routine progress — fans a pointer to subscribers, no forced wake',
31
50
  urgent: 'force-wake subscribers (inbox tier urgent)',
@@ -70,19 +89,15 @@ function makeTierLeaf(tier) {
70
89
  if (body === '') {
71
90
  throw new InputError({ error: 'missing_body', message: 'no report body', field: 'body', next: 'Pass the body as an argument or on stdin.' });
72
91
  }
92
+ const node = tier === 'final' ? getNode(nodeId) : null;
73
93
  if (tier === 'final') {
74
- const node = getNode(nodeId);
75
94
  // A 2nd `push final` in one turn (or finalizing an already dead/canceled
76
95
  // node) is an illegal finalize-from-terminal: the underlying transition()
77
96
  // throws a raw Error that surfaces as an `internal` "crtr bug" on stderr.
78
97
  // Catch it here as a CLEAN user-facing error — the node is simply already
79
98
  // finished, so there is nothing to do.
80
99
  if (node !== null && node.status !== 'active' && node.status !== 'idle') {
81
- throw new InputError({
82
- error: 'already_finalized',
83
- message: `This node is already ${node.status} — \`push final\` finishes a node once and cannot finalize it again.`,
84
- next: 'Nothing to do here: the node is already finished. Do not push again from this node.',
85
- });
100
+ throw alreadyFinalizedError(node.status);
86
101
  }
87
102
  // A RESIDENT node with no subscribers is human-driven and has no one to
88
103
  // submit a canonical result to: `push final` fans to subscribers, and a
@@ -102,8 +117,18 @@ function makeTierLeaf(tier) {
102
117
  }
103
118
  }
104
119
  }
105
- const result = await push(nodeId, { kind: tier, body });
106
- return { report_path: result.reportPath, delivered_to: result.deliveredTo, status: tier === 'final' ? 'done' : 'active' };
120
+ try {
121
+ const result = await push(nodeId, { kind: tier, body });
122
+ return { report_path: result.reportPath, delivered_to: result.deliveredTo, status: tier === 'final' ? 'done' : 'active' };
123
+ }
124
+ catch (err) {
125
+ if (tier === 'final') {
126
+ const mapped = mapFinalizeTransitionRaceError(getNode(nodeId)?.status ?? node?.status ?? null, err);
127
+ if (mapped !== null)
128
+ throw mapped;
129
+ }
130
+ throw err;
131
+ }
107
132
  },
108
133
  render: (r) => {
109
134
  const n = Array.isArray(r['delivered_to']) ? r['delivered_to'].length : 0;
@@ -174,6 +199,49 @@ const feedReadLeaf = defineLeaf({
174
199
  },
175
200
  });
176
201
  // ---------------------------------------------------------------------------
202
+ // feed message — read one inbox message's full body back from the jsonl
203
+ // ---------------------------------------------------------------------------
204
+ const feedMessageLeaf = defineLeaf({
205
+ name: 'message',
206
+ description: 'print one inbox entry\'s full body by id',
207
+ whenToUse: 'a digest line ended in `… (clipped — full message: crtr feed message <id>)` — run it to read the rest of that direct message. The full body lives in your inbox.jsonl; this prints it verbatim by the entry\'s short id. Read-only: it does NOT advance your inbox cursor.',
208
+ help: {
209
+ name: 'feed message',
210
+ summary: 'print the full body of one inbox entry, addressed by the short id shown in a clipped digest line',
211
+ params: [
212
+ { kind: 'positional', name: 'id', required: true, constraint: 'The short entry id from the digest\'s `… (full message: …)` pointer.' },
213
+ { kind: 'flag', name: 'node', type: 'string', required: false, constraint: 'Whose inbox to read. Defaults to CRTR_NODE_ID; pass to read another node\'s inbox as an orchestrator.' },
214
+ ],
215
+ output: [
216
+ { name: 'node_id', type: 'string', required: true, constraint: 'Node whose inbox was read.' },
217
+ { name: 'id', type: 'string', required: true, constraint: 'The requested entry id.' },
218
+ { name: 'found', type: 'boolean', required: true, constraint: 'True when an entry with that id exists.' },
219
+ { name: 'from', type: 'string', required: false, constraint: 'Sender of the message, when found.' },
220
+ { name: 'body', type: 'string', required: false, constraint: 'Full message body, when found.' },
221
+ ],
222
+ outputKind: 'object',
223
+ effects: ['Read-only: scans inbox.jsonl. Does NOT advance the cursor.'],
224
+ },
225
+ run: async (input) => {
226
+ const nodeId = typeof input['node'] === 'string' && input['node'].trim() !== ''
227
+ ? input['node'].trim()
228
+ : requireCallerNode();
229
+ const id = input['id'].trim();
230
+ const entry = readInboxEntryById(nodeId, id);
231
+ if (entry === undefined) {
232
+ return { node_id: nodeId, id, found: false };
233
+ }
234
+ const body = typeof entry.data?.['body'] === 'string' ? entry.data['body'] : entry.label;
235
+ return { node_id: nodeId, id, found: true, from: entry.from ?? 'system', body };
236
+ },
237
+ render: (r) => {
238
+ if (r['found'] !== true) {
239
+ return `No inbox entry with id ${r['id']} in ${r['node_id']}'s inbox. Re-read the digest with \`crtr feed read --all\`, or list raw entries to find the right id.`;
240
+ }
241
+ return `Full message ${r['id']} (from ${r['from']}):\n\n${r['body']}`;
242
+ },
243
+ });
244
+ // ---------------------------------------------------------------------------
177
245
  // feed peek — live state of the nodes below you, without draining anything
178
246
  // ---------------------------------------------------------------------------
179
247
  const STATUS_GLYPH = {
@@ -310,8 +378,8 @@ export function registerFeed() {
310
378
  help: {
311
379
  name: 'feed',
312
380
  summary: 'read the per-node inbox feed',
313
- model: 'Each node has an inbox.jsonl that accumulates ~30-token pointers from publishers it subscribes to. The watcher drains this inbox to wake you, so the wake message you receive ALREADY IS the coalesced digest \u2014 dereference the reports that matter by reading their ref paths (a push carries a ref; a direct message inlines its full body). `feed read` is for PROACTIVELY polling before a wake (it advances the cursor); after a wake it reads empty because the cursor already moved \u2014 use `--all` to re-read history. An empty feed is normal while workers run \u2014 a worker that has not pushed leaves no pointer \u2014 so use `feed peek` to see the live state of the nodes below you (and whether to yield) without draining anything.',
381
+ model: 'Each node has an inbox.jsonl that accumulates ~30-token pointers from publishers it subscribes to. The watcher drains this inbox to wake you, so the wake message you receive ALREADY IS the coalesced digest \u2014 dereference the reports that matter by reading their ref paths (a push carries a ref; a direct message inlines a bounded body preview, and when that clips it ends in `crtr feed message <id>` to read the full text back from the jsonl). `feed read` is for PROACTIVELY polling before a wake (it advances the cursor); after a wake it reads empty because the cursor already moved \u2014 use `--all` to re-read history. An empty feed is normal while workers run \u2014 a worker that has not pushed leaves no pointer \u2014 so use `feed peek` to see the live state of the nodes below you (and whether to yield) without draining anything.',
314
382
  },
315
- children: [feedReadLeaf, feedPeekLeaf],
383
+ children: [feedReadLeaf, feedMessageLeaf, feedPeekLeaf],
316
384
  });
317
385
  }
@@ -14,6 +14,9 @@ import { reviveNode } from '../core/runtime/revive.js';
14
14
  import { listDisconnected, reviveAll } from '../core/runtime/revive-all.js';
15
15
  import { waitForBrokerViewSocket } from '../core/runtime/placement.js';
16
16
  import { getNode, fullName } from '../core/canvas/index.js';
17
+ import { isPidAlive } from '../core/canvas/pid.js';
18
+ import { readFault } from '../core/runtime/fault.js';
19
+ import { isBusy } from '../core/runtime/busy.js';
17
20
  // ---------------------------------------------------------------------------
18
21
  // revive node
19
22
  // ---------------------------------------------------------------------------
@@ -47,6 +50,14 @@ export const reviveLeaf = defineLeaf({
47
50
  default: false,
48
51
  constraint: 'When set, start a clean pi session (no --session). Default: resume the saved conversation. Ignored with --all (which always resumes).',
49
52
  },
53
+ {
54
+ kind: 'flag',
55
+ name: 'now',
56
+ type: 'bool',
57
+ required: false,
58
+ default: false,
59
+ constraint: 'On-demand kick of an active-fault node (parked on a provider-owned fault, broker still alive). Its broker pid is alive, so an ordinary revive no-ops (double-launch guard) — --now SIGTERMs the live broker instead, so the daemon\'s crash→grace→resume path brings it back in ~20s instead of waiting out the auto-recovery grace. Only valid for a node that has a live daemon-owned auto provider fault and a live pid. Mutually exclusive with --all/--fresh.',
60
+ },
50
61
  // --force is a DELIBERATELY UNDOCUMENTED confirmation gate for --all (hidden
51
62
  // from -h; see FlagParam.hidden). Silas's intent (2026-06-13): a mass revive
52
63
  // can have large, unforeseen consequences, so the agent must NOT be able to
@@ -73,6 +84,7 @@ export const reviveLeaf = defineLeaf({
73
84
  { name: 'candidates', type: 'string', required: false, constraint: '--all preview: the node ids that WOULD be revived (newline-joined). Empty when none are disconnected.' },
74
85
  { name: 'revived', type: 'string', required: false, constraint: '--all sweep: the node ids whose broker engine was relaunched (newline-joined).' },
75
86
  { name: 'failed', type: 'string', required: false, constraint: '--all sweep: node ids whose relaunch threw, with the error (newline-joined). Absent when none failed.' },
87
+ { name: 'kicked', type: 'boolean', required: false, constraint: '--now only: true when the hanging node\'s live broker was SIGTERM\'d (the daemon resumes it on the saved session within ~20s). Absent for an ordinary revive.' },
76
88
  ],
77
89
  outputKind: 'object',
78
90
  effects: [
@@ -84,8 +96,16 @@ export const reviveLeaf = defineLeaf({
84
96
  },
85
97
  run: async (input) => {
86
98
  const all = input['all'] ?? false;
99
+ const now = input['now'] ?? false;
87
100
  const nodeId = input['node'];
88
101
  if (all) {
102
+ if (now) {
103
+ throw new InputError({
104
+ error: 'conflicting_args',
105
+ message: '`--now` kicks ONE hanging node; it cannot combine with `--all`.',
106
+ next: 'Run `crtr canvas revive <node> --now` for the hanging node, or `crtr canvas revive --all` (no --now).',
107
+ });
108
+ }
89
109
  if (nodeId !== undefined) {
90
110
  throw new InputError({
91
111
  error: 'conflicting_args',
@@ -103,6 +123,13 @@ export const reviveLeaf = defineLeaf({
103
123
  });
104
124
  }
105
125
  const fresh = input['fresh'] ?? false;
126
+ if (now && fresh) {
127
+ throw new InputError({
128
+ error: 'conflicting_args',
129
+ message: '`--now` kicks a live hanging broker (SIGTERM → daemon resumes the SAVED session); `--fresh` is meaningless there.',
130
+ next: 'Run `crtr canvas revive <node> --now` (resumes), or `crtr canvas revive <node> --fresh` (no --now) to relaunch clean.',
131
+ });
132
+ }
106
133
  // Validate the node exists before attempting revival.
107
134
  const meta = getNode(nodeId);
108
135
  if (meta === null) {
@@ -112,11 +139,57 @@ export const reviveLeaf = defineLeaf({
112
139
  next: 'List nodes with `crtr node inspect list`.',
113
140
  });
114
141
  }
142
+ // --now: the on-demand kick for an active fault. The broker is ALIVE (so
143
+ // reviveNode would no-op the double-launch guard) — SIGTERM it so the daemon's
144
+ // ordinary crash→grace→resume path recovers it on the saved session, the same
145
+ // thing the daemon does at the auto-recovery grace, just on demand. Gated on
146
+ // a live pid and a daemon-owned provider fault so it can't be used to nuke a
147
+ // healthy node.
148
+ if (now) {
149
+ const pid = meta.pi_pid;
150
+ const stall = readFault(nodeId);
151
+ if (pid == null || !isPidAlive(pid)) {
152
+ throw new InputError({
153
+ error: 'not_hanging',
154
+ message: `${nodeId} has no live broker — nothing to kick. --now is for a HANGING node (live broker parked on an active fault).`,
155
+ next: 'Revive a dormant node with `crtr canvas revive ' + nodeId + '` (no --now).',
156
+ });
157
+ }
158
+ if (stall === null || stall.link !== 'pi→provider' || stall.retry.by !== 'daemon' || stall.retry.disposition !== 'auto') {
159
+ throw new InputError({
160
+ error: 'not_hanging',
161
+ message: `${nodeId} is live but not an active daemon-owned provider fault — --now refuses to SIGTERM a healthy node.`,
162
+ next: 'Use --now only on a node the canvas shows as hanging (⚠). For a routine relaunch, omit --now.',
163
+ });
164
+ }
165
+ // Mirror the daemon's faultRecoveryVerdict, which never kills a BUSY engine: a
166
+ // node mid-turn (busy marker present) is making progress, not wedged —
167
+ // refuse rather than SIGTERM it mid-flight.
168
+ if (isBusy(nodeId)) {
169
+ throw new InputError({
170
+ error: 'busy',
171
+ message: `${nodeId} is mid-turn right now (busy) — --now refuses to SIGTERM a working engine; it has recovered on its own.`,
172
+ next: 'Re-check the canvas — if it\'s no longer marked hanging (⚠), nothing to do.',
173
+ });
174
+ }
175
+ try {
176
+ process.kill(pid, 'SIGTERM');
177
+ }
178
+ catch {
179
+ /* best-effort: the broker may have just died; the daemon owns the resume */
180
+ }
181
+ return {
182
+ window: null,
183
+ session: meta.tmux_session ?? null,
184
+ kicked: true,
185
+ message: `Sent SIGTERM to ${fullName(meta)} (${nodeId}, pid ${pid}). The daemon will resume it on the saved session within ~20s (crash→grace→resume).`,
186
+ };
187
+ }
115
188
  const result = reviveNode(nodeId, { resume: !fresh });
116
189
  // Revive returns once the broker process is launched, which can be seconds
117
190
  // before its view.sock listens. Callers (attach, the web shell's Wake) dial
118
191
  // immediately on return, so block here until the socket accepts.
119
- const ready = waitForBrokerViewSocket(nodeId);
192
+ const ready = await waitForBrokerViewSocket(nodeId, result.launch?.exited);
120
193
  return {
121
194
  window: result.window ?? undefined,
122
195
  session: result.session,
@@ -0,0 +1 @@
1
+ export declare const answerLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,50 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { exaAnswer } from './exa.js';
3
+ export const answerLeaf = defineLeaf({
4
+ name: 'answer',
5
+ description: 'get one grounded, cited answer to a question',
6
+ whenToUse: 'you have a specific question and want a single synthesized answer grounded in sources, rather than a ranked list of pages to read yourself. Best for factual lookups and "what/who/when" questions where you want the conclusion plus its citations. Reach for `web` instead when you want to browse and judge the raw results, or when the task is open-ended research rather than one answerable question.',
7
+ help: {
8
+ name: 'search answer',
9
+ summary: 'grounded answer via Exa — one synthesized natural-language answer plus the sources it cites',
10
+ params: [
11
+ { kind: 'positional', name: 'question', required: true, constraint: 'The question to answer. Natural language; phrase it as a question.' },
12
+ ],
13
+ output: [
14
+ { name: 'answer', type: 'string', required: true, constraint: 'The synthesized natural-language answer grounded in the cited sources.' },
15
+ { name: 'citations', type: 'object[]', required: true, constraint: 'The sources the answer draws on, each: title and url.' },
16
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next command — read a citation in full, or fall back to raw results.' },
17
+ ],
18
+ outputKind: 'object',
19
+ effects: ['Sends one answer request to the Exa API (network). No local state changes.'],
20
+ },
21
+ run: async (input) => {
22
+ const question = input['question'];
23
+ const res = await exaAnswer({ query: question });
24
+ return {
25
+ answer: res.answer ?? '',
26
+ citations: res.citations ?? [],
27
+ follow_up: 'Read any cited source in full with `crtr search contents <url>`. Want raw ranked results instead of a synthesized answer? Use `crtr search web`.',
28
+ };
29
+ },
30
+ render: (result) => {
31
+ const answer = result['answer'];
32
+ const citations = result['citations'];
33
+ const followUp = result['follow_up'];
34
+ if (answer.trim() === '') {
35
+ return `No answer was returned.\n\n${followUp}`;
36
+ }
37
+ const lines = [answer.trim(), ''];
38
+ if (citations.length > 0) {
39
+ lines.push(`Sources (${citations.length}):`);
40
+ citations.forEach((c, i) => {
41
+ const title = c.title !== undefined && c.title !== '' ? c.title : '(untitled)';
42
+ const url = c.url !== undefined ? ` — ${c.url}` : '';
43
+ lines.push(`${i + 1}. ${title}${url}`);
44
+ });
45
+ lines.push('');
46
+ }
47
+ lines.push(followUp);
48
+ return lines.join('\n');
49
+ },
50
+ });
@@ -0,0 +1 @@
1
+ export declare const contentsLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,96 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { usage } from '../../core/errors.js';
3
+ import { exaContents, parseUrls, renderResult, TEXT_MAX_CHARACTERS, } from './exa.js';
4
+ import { puremdFetch } from './puremd.js';
5
+ export const contentsLeaf = defineLeaf({
6
+ name: 'contents',
7
+ description: 'extract clean content from URLs you already have',
8
+ whenToUse: 'you already hold one or more URLs — from a prior `search web`/`answer`, a database, an RSS feed, or user input — and need their cleaned content or highlights. This does NOT search; it only extracts from the URLs you give it. Reach for `web` when you still need to find the pages, and `answer` when you want a synthesized response rather than raw page content.',
9
+ help: {
10
+ name: 'search contents',
11
+ summary: 'content extraction via Exa — cleaned highlights or full text for URLs you already have',
12
+ params: [
13
+ { kind: 'positional', name: 'urls', required: true, constraint: 'One or more URLs to extract, separated by commas or whitespace.' },
14
+ { kind: 'flag', name: 'text', type: 'bool', required: false, constraint: `Return cleaned full page text (capped at ${TEXT_MAX_CHARACTERS} characters per URL) instead of highlight excerpts. Off by default.` },
15
+ { kind: 'flag', name: 'max-age-hours', type: 'int', required: false, constraint: 'Maximum acceptable age of cached content, in hours; content older than this is freshly crawled. 0 forces a fresh crawl every time. Omit to use cache when available and crawl as fallback.' },
16
+ ],
17
+ output: [
18
+ { name: 'results', type: 'object[]', required: true, constraint: 'One per successfully extracted URL, each: title, url, and either highlight excerpts (default) or capped full text (--text).' },
19
+ { name: 'failures', type: 'object[]', required: true, constraint: 'URLs that could not be fetched by Exa or the pure.md fallback, each: url and reason.' },
20
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next command for retrying failures or refreshing stale content.' },
21
+ ],
22
+ outputKind: 'object',
23
+ effects: ['Sends one contents request to the Exa API (network). URLs Exa fails to fetch are retried through the pure.md markdown service. No local state changes.'],
24
+ },
25
+ run: async (input) => {
26
+ const urls = parseUrls(input['urls']);
27
+ if (urls.length === 0)
28
+ throw usage('no URLs provided', { next: 'Pass one or more URLs separated by commas or whitespace.' });
29
+ const wantText = input['text'];
30
+ const maxAgeHours = input['maxAgeHours'];
31
+ const body = { urls };
32
+ if (wantText)
33
+ body['text'] = { maxCharacters: TEXT_MAX_CHARACTERS };
34
+ else
35
+ body['highlights'] = true;
36
+ if (maxAgeHours !== undefined)
37
+ body['maxAgeHours'] = maxAgeHours;
38
+ const res = await exaContents(body);
39
+ const results = res.results ?? [];
40
+ const fetched = new Set(results.map((r) => r.url).filter((u) => u !== undefined));
41
+ const exaFailures = [];
42
+ for (const s of res.statuses ?? []) {
43
+ if (s.status === 'success')
44
+ continue;
45
+ const url = s.id ?? '(unknown url)';
46
+ if (fetched.has(url))
47
+ continue;
48
+ const reason = typeof s.error === 'string'
49
+ ? s.error
50
+ : s.error?.tag ?? s.status ?? 'unknown error';
51
+ exaFailures.push({ url, reason });
52
+ }
53
+ // Fall back to pure.md for every URL Exa could not fetch. A pure.md success
54
+ // becomes a normal text result (flagged so the renderer can show its source);
55
+ // only URLs both services miss remain genuine failures.
56
+ const failures = [];
57
+ const fallbacks = await Promise.all(exaFailures.map(async (f) => ({ failure: f, pure: await puremdFetch(f.url, TEXT_MAX_CHARACTERS) })));
58
+ for (const { failure, pure } of fallbacks) {
59
+ if (pure.ok) {
60
+ results.push({ url: failure.url, title: failure.url, text: pure.text, source: 'pure.md' });
61
+ }
62
+ else {
63
+ failures.push({ url: failure.url, reason: `${failure.reason} (pure.md fallback: ${pure.reason})` });
64
+ }
65
+ }
66
+ return {
67
+ results,
68
+ failures,
69
+ follow_up: 'Stale or empty content? Re-run with --max-age-hours 0 to force a fresh crawl. Need to find more pages? Use `crtr search web`.',
70
+ };
71
+ },
72
+ render: (result) => {
73
+ const results = result['results'];
74
+ const failures = result['failures'];
75
+ const followUp = result['follow_up'];
76
+ const lines = [];
77
+ if (results.length === 0) {
78
+ lines.push('No content extracted.');
79
+ }
80
+ else {
81
+ lines.push(`Extracted ${results.length} URL${results.length === 1 ? '' : 's'}:`, '');
82
+ results.forEach((r, i) => {
83
+ lines.push(renderResult(r, i + 1));
84
+ lines.push('');
85
+ });
86
+ }
87
+ if (failures.length > 0) {
88
+ lines.push(`Failed (${failures.length}):`);
89
+ for (const f of failures)
90
+ lines.push(`- ${f.url} — ${f.reason}`);
91
+ lines.push('');
92
+ }
93
+ lines.push(followUp);
94
+ return lines.join('\n');
95
+ },
96
+ });
@@ -0,0 +1,53 @@
1
+ /** Resolve the Exa API key: EXA_API_KEY env first, then ~/.crouter/exa.key.
2
+ * Never prompts and never proceeds keyless — throws a usage error naming both
3
+ * options when neither is present. */
4
+ export declare function getApiKey(): string;
5
+ /** One Exa result row as returned by /search and /contents. Fields are present
6
+ * only when the API supplies them. */
7
+ export interface ExaResult {
8
+ title?: string;
9
+ url?: string;
10
+ publishedDate?: string;
11
+ author?: string;
12
+ highlights?: string[];
13
+ text?: string;
14
+ score?: number;
15
+ /** Set when the row came from a fallback fetcher (e.g. 'pure.md') rather than Exa. */
16
+ source?: string;
17
+ }
18
+ export interface ExaSearchResponse {
19
+ results?: ExaResult[];
20
+ }
21
+ export interface ExaAnswerResponse {
22
+ answer?: string;
23
+ citations?: Array<{
24
+ title?: string;
25
+ url?: string;
26
+ }>;
27
+ }
28
+ /** Per-URL fetch status returned by /contents (and /search livecrawls). */
29
+ export interface ExaStatus {
30
+ id?: string;
31
+ status?: string;
32
+ error?: {
33
+ tag?: string;
34
+ httpStatusCode?: number;
35
+ } | string;
36
+ }
37
+ export interface ExaContentsResponse {
38
+ results?: ExaResult[];
39
+ statuses?: ExaStatus[];
40
+ }
41
+ export declare function exaSearch(body: Record<string, unknown>): Promise<ExaSearchResponse>;
42
+ export declare function exaAnswer(body: Record<string, unknown>): Promise<ExaAnswerResponse>;
43
+ export declare function exaContents(body: Record<string, unknown>): Promise<ExaContentsResponse>;
44
+ /** Cap (characters) applied to full-text extraction so a single call cannot
45
+ * blow up the caller's context. Highlights remain the default content mode. */
46
+ export declare const TEXT_MAX_CHARACTERS = 4000;
47
+ /** Split a positional URL argument on commas or whitespace into a clean list. */
48
+ export declare function parseUrls(raw: string): string[];
49
+ /** Split a comma-separated domain flag into a clean list, or undefined when empty. */
50
+ export declare function parseDomains(raw: string | undefined): string[] | undefined;
51
+ /** Render one result as agent-ready markdown: a numbered heading with title +
52
+ * metadata line, then highlights as bullets or capped text as a blockquote. */
53
+ export declare function renderResult(r: ExaResult, index: number): string;
@@ -0,0 +1,155 @@
1
+ // The only module that talks to api.exa.ai. Owns API-key resolution, the three
2
+ // endpoint calls (/search, /answer, /contents), request shaping, and translation
3
+ // of HTTP/transport failures into the crtr error taxonomy. Leaves never build
4
+ // HTTP requests directly. Uses the Node global fetch — no HTTP dependency.
5
+ import { join } from 'node:path';
6
+ import { readFileSync, existsSync } from 'node:fs';
7
+ import { usage, network } from '../../core/errors.js';
8
+ import { classify } from '../../core/fault-classifier.js';
9
+ import { clearFaultForCurrentNode, recordFaultForCurrentNode } from '../../core/runtime/fault.js';
10
+ import { userScopeRoot } from '../../core/scope.js';
11
+ const EXA_BASE = 'https://api.exa.ai';
12
+ function endpointToOp(endpoint) {
13
+ switch (endpoint) {
14
+ case '/search':
15
+ return 'search /search';
16
+ case '/answer':
17
+ return 'search /answer';
18
+ case '/contents':
19
+ return 'search /contents';
20
+ default:
21
+ return `search ${endpoint}`;
22
+ }
23
+ }
24
+ /** Resolve the Exa API key: EXA_API_KEY env first, then ~/.crouter/exa.key.
25
+ * Never prompts and never proceeds keyless — throws a usage error naming both
26
+ * options when neither is present. */
27
+ export function getApiKey() {
28
+ const env = process.env['EXA_API_KEY'];
29
+ if (env !== undefined && env.trim() !== '')
30
+ return env.trim();
31
+ const keyFile = join(userScopeRoot(), 'exa.key');
32
+ if (existsSync(keyFile)) {
33
+ const fromFile = readFileSync(keyFile, 'utf8').trim();
34
+ if (fromFile !== '')
35
+ return fromFile;
36
+ }
37
+ throw usage('no Exa API key found', {
38
+ next: `Set the EXA_API_KEY environment variable, or write the key to ${keyFile}.`,
39
+ });
40
+ }
41
+ /** POST a JSON body to an Exa endpoint and return the parsed JSON. Any non-2xx
42
+ * response or transport failure becomes a crtr network error carrying Exa's
43
+ * status/message and a concrete recovery hint. */
44
+ async function exaPost(endpoint, body) {
45
+ const apiKey = getApiKey();
46
+ let res;
47
+ try {
48
+ res = await fetch(`${EXA_BASE}${endpoint}`, {
49
+ method: 'POST',
50
+ headers: { 'x-api-key': apiKey, 'content-type': 'application/json' },
51
+ body: JSON.stringify(body),
52
+ });
53
+ }
54
+ catch (err) {
55
+ const msg = err instanceof Error ? err.message : String(err);
56
+ const message = `Exa request failed: ${msg}`;
57
+ const classified = classify('cli→exa', err);
58
+ recordFaultForCurrentNode({
59
+ link: 'cli→exa',
60
+ op: endpointToOp(endpoint),
61
+ kind: classified.kind,
62
+ retry: { disposition: classified.disposition },
63
+ message,
64
+ });
65
+ throw network(message, {
66
+ next: 'Check network connectivity and retry. If it persists, simplify the query or reduce --num.',
67
+ });
68
+ }
69
+ if (!res.ok) {
70
+ let detail = '';
71
+ try {
72
+ detail = (await res.text()).slice(0, 500);
73
+ }
74
+ catch {
75
+ /* body unreadable — status line is enough */
76
+ }
77
+ const message = `Exa returned ${res.status} ${res.statusText}${detail ? `: ${detail}` : ''}`;
78
+ const classified = classify('cli→exa', {
79
+ status: res.status,
80
+ statusText: res.statusText,
81
+ ok: res.ok,
82
+ message: detail,
83
+ });
84
+ recordFaultForCurrentNode({
85
+ link: 'cli→exa',
86
+ op: endpointToOp(endpoint),
87
+ kind: classified.kind,
88
+ retry: { disposition: classified.disposition },
89
+ message,
90
+ });
91
+ throw network(message, {
92
+ received: `HTTP ${res.status}`,
93
+ next: res.status === 401
94
+ ? 'The API key was rejected. Verify EXA_API_KEY or ~/.crouter/exa.key.'
95
+ : 'Retry; if it persists, simplify the query, reduce --num, or drop domain filters.',
96
+ });
97
+ }
98
+ clearFaultForCurrentNode({ link: 'cli→exa' });
99
+ return (await res.json());
100
+ }
101
+ export function exaSearch(body) {
102
+ return exaPost('/search', body);
103
+ }
104
+ export function exaAnswer(body) {
105
+ return exaPost('/answer', body);
106
+ }
107
+ export function exaContents(body) {
108
+ return exaPost('/contents', body);
109
+ }
110
+ /** Cap (characters) applied to full-text extraction so a single call cannot
111
+ * blow up the caller's context. Highlights remain the default content mode. */
112
+ export const TEXT_MAX_CHARACTERS = 4000;
113
+ /** Split a positional URL argument on commas or whitespace into a clean list. */
114
+ export function parseUrls(raw) {
115
+ return raw
116
+ .split(/[\s,]+/)
117
+ .map((u) => u.trim())
118
+ .filter((u) => u !== '');
119
+ }
120
+ /** Split a comma-separated domain flag into a clean list, or undefined when empty. */
121
+ export function parseDomains(raw) {
122
+ if (raw === undefined)
123
+ return undefined;
124
+ const list = raw
125
+ .split(',')
126
+ .map((d) => d.trim())
127
+ .filter((d) => d !== '');
128
+ return list.length > 0 ? list : undefined;
129
+ }
130
+ /** Render one result as agent-ready markdown: a numbered heading with title +
131
+ * metadata line, then highlights as bullets or capped text as a blockquote. */
132
+ export function renderResult(r, index) {
133
+ const lines = [];
134
+ const title = r.title !== undefined && r.title !== '' ? r.title : '(untitled)';
135
+ lines.push(`### ${index}. ${title}`);
136
+ const meta = [];
137
+ if (typeof r.url === 'string' && r.url !== '')
138
+ meta.push(r.url);
139
+ if (typeof r.publishedDate === 'string' && r.publishedDate !== '')
140
+ meta.push(r.publishedDate.slice(0, 10));
141
+ if (typeof r.author === 'string' && r.author !== '')
142
+ meta.push(r.author);
143
+ if (typeof r.source === 'string' && r.source !== '')
144
+ meta.push(`via ${r.source}`);
145
+ if (meta.length > 0)
146
+ lines.push(meta.join(' · '));
147
+ if (r.highlights !== undefined && r.highlights.length > 0) {
148
+ for (const h of r.highlights)
149
+ lines.push(`- ${h.replace(/\s+/g, ' ').trim()}`);
150
+ }
151
+ else if (r.text !== undefined && r.text !== '') {
152
+ lines.push(r.text.trim());
153
+ }
154
+ return lines.join('\n');
155
+ }
@@ -0,0 +1,11 @@
1
+ export type PuremdResult = {
2
+ ok: true;
3
+ text: string;
4
+ } | {
5
+ ok: false;
6
+ reason: string;
7
+ };
8
+ /** Fetch a single URL's content as markdown via pure.md, capped at maxChars.
9
+ * Never throws — any transport or HTTP failure becomes { ok: false, reason }
10
+ * so the caller can keep the URL in its failures list. */
11
+ export declare function puremdFetch(url: string, maxChars: number): Promise<PuremdResult>;