vellum 0.2.1 → 0.2.2

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 (349) hide show
  1. package/README.md +15 -2
  2. package/bun.lock +5 -2
  3. package/package.json +4 -2
  4. package/scripts/capture-x-graphql.ts +562 -0
  5. package/scripts/ipc/check-swift-decoder-drift.ts +2 -1
  6. package/scripts/test.sh +5 -0
  7. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +133 -34
  8. package/src/__tests__/account-registry.test.ts +2 -1
  9. package/src/__tests__/agent-heartbeat-service.test.ts +250 -0
  10. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  11. package/src/__tests__/asset-search-tool.test.ts +23 -22
  12. package/src/__tests__/attachments-store.test.ts +56 -127
  13. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  14. package/src/__tests__/browser-skill-endstate.test.ts +4 -3
  15. package/src/__tests__/call-bridge.test.ts +385 -0
  16. package/src/__tests__/call-constants.test.ts +40 -0
  17. package/src/__tests__/call-orchestrator.test.ts +130 -4
  18. package/src/__tests__/call-recovery.test.ts +518 -0
  19. package/src/__tests__/call-routes-http.test.ts +459 -0
  20. package/src/__tests__/call-state-machine.test.ts +143 -0
  21. package/src/__tests__/call-store.test.ts +216 -1
  22. package/src/__tests__/cli-discover.test.ts +1 -1
  23. package/src/__tests__/commit-message-enrichment-service.test.ts +148 -7
  24. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  25. package/src/__tests__/computer-use-tools.test.ts +250 -0
  26. package/src/__tests__/config-schema.test.ts +299 -3
  27. package/src/__tests__/conflict-store.test.ts +2 -1
  28. package/src/__tests__/contacts-tools.test.ts +331 -0
  29. package/src/__tests__/conversation-store.test.ts +30 -32
  30. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  31. package/src/__tests__/date-context.test.ts +373 -0
  32. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  33. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  34. package/src/__tests__/followup-tools.test.ts +303 -0
  35. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  36. package/src/__tests__/intent-routing.test.ts +64 -57
  37. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  38. package/src/__tests__/ipc-snapshot.test.ts +62 -28
  39. package/src/__tests__/llm-usage-store.test.ts +3 -8
  40. package/src/__tests__/media-generate-image.test.ts +1 -1
  41. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  42. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  43. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  44. package/src/__tests__/playbook-tools.test.ts +342 -0
  45. package/src/__tests__/profile-compiler.test.ts +2 -1
  46. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  47. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  48. package/src/__tests__/recurrence-engine.test.ts +69 -0
  49. package/src/__tests__/recurrence-types.test.ts +71 -0
  50. package/src/__tests__/registry.test.ts +5 -3
  51. package/src/__tests__/relay-server.test.ts +633 -0
  52. package/src/__tests__/reminder-store.test.ts +6 -3
  53. package/src/__tests__/reminder.test.ts +43 -77
  54. package/src/__tests__/run-orchestrator-assistant-events.test.ts +8 -4
  55. package/src/__tests__/run-orchestrator.test.ts +4 -4
  56. package/src/__tests__/runtime-attachment-metadata.test.ts +7 -6
  57. package/src/__tests__/runtime-runs-http.test.ts +4 -4
  58. package/src/__tests__/runtime-runs.test.ts +4 -4
  59. package/src/__tests__/schedule-store.test.ts +482 -0
  60. package/src/__tests__/schedule-tools.test.ts +700 -0
  61. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  62. package/src/__tests__/server-history-render.test.ts +14 -13
  63. package/src/__tests__/session-error.test.ts +28 -0
  64. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  65. package/src/__tests__/session-queue.test.ts +71 -48
  66. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  67. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  68. package/src/__tests__/signup-e2e.test.ts +2 -1
  69. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  70. package/src/__tests__/skill-script-runner.test.ts +159 -0
  71. package/src/__tests__/speaker-identification.test.ts +52 -0
  72. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  73. package/src/__tests__/subagent-tools.test.ts +141 -41
  74. package/src/__tests__/task-compiler.test.ts +2 -1
  75. package/src/__tests__/task-runner.test.ts +2 -1
  76. package/src/__tests__/task-scheduler.test.ts +2 -1
  77. package/src/__tests__/task-tools.test.ts +49 -56
  78. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  79. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  80. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  81. package/src/__tests__/tool-executor.test.ts +13 -17
  82. package/src/__tests__/turn-commit.test.ts +218 -3
  83. package/src/__tests__/twilio-provider.test.ts +143 -0
  84. package/src/__tests__/twilio-routes.test.ts +789 -0
  85. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  86. package/src/__tests__/view-image-tool.test.ts +217 -0
  87. package/src/__tests__/workspace-git-service.test.ts +186 -0
  88. package/src/__tests__/workspace-heartbeat-service.test.ts +13 -3
  89. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  90. package/src/bundler/app-bundler.ts +12 -8
  91. package/src/calls/call-bridge.ts +95 -0
  92. package/src/calls/call-constants.ts +43 -5
  93. package/src/calls/call-domain.ts +276 -0
  94. package/src/calls/call-orchestrator.ts +43 -17
  95. package/src/calls/call-recovery.ts +207 -0
  96. package/src/calls/call-state-machine.ts +68 -0
  97. package/src/calls/call-store.ts +192 -5
  98. package/src/calls/relay-server.ts +41 -4
  99. package/src/calls/speaker-identification.ts +213 -0
  100. package/src/calls/twilio-provider.ts +10 -6
  101. package/src/calls/twilio-routes.ts +90 -76
  102. package/src/calls/types.ts +1 -1
  103. package/src/cli/config-commands.ts +334 -0
  104. package/src/cli/core-commands.ts +776 -0
  105. package/src/cli/doordash.ts +251 -1
  106. package/src/cli/ipc-client.ts +82 -0
  107. package/src/cli/map.ts +246 -0
  108. package/src/cli/twitter.ts +575 -0
  109. package/src/cli.ts +7 -5
  110. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  111. package/src/commands/cc-command-registry.ts +209 -0
  112. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  113. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  114. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  115. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  116. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  117. package/src/config/bundled-skills/document/SKILL.md +18 -0
  118. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  119. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  120. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  121. package/src/config/bundled-skills/doordash/SKILL.md +82 -23
  122. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  123. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  124. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  125. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  126. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  127. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +1 -23
  128. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  129. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  130. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  131. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  132. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  133. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  134. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  135. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  136. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  137. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  138. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  139. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  140. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  141. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  142. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  143. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  144. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  145. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  146. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  147. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  148. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  149. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  150. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  151. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  152. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  153. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  154. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  155. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  156. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  157. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  158. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  159. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  160. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  161. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  162. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  163. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  164. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  165. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  166. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  167. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  168. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  169. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  170. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  171. package/src/config/defaults.ts +33 -0
  172. package/src/config/loader.ts +4 -1
  173. package/src/config/schema.ts +161 -1
  174. package/src/config/system-prompt.ts +61 -16
  175. package/src/config/templates/IDENTITY.md +7 -0
  176. package/src/config/types.ts +4 -0
  177. package/src/contacts/contact-store.ts +4 -4
  178. package/src/daemon/assistant-attachments.ts +10 -0
  179. package/src/daemon/classifier.ts +3 -1
  180. package/src/daemon/computer-use-session.ts +3 -1
  181. package/src/daemon/date-context.ts +136 -0
  182. package/src/daemon/handlers/apps.ts +16 -1
  183. package/src/daemon/handlers/browser.ts +54 -0
  184. package/src/daemon/handlers/computer-use.ts +7 -1
  185. package/src/daemon/handlers/config.ts +163 -5
  186. package/src/daemon/handlers/diagnostics.ts +5 -1
  187. package/src/daemon/handlers/documents.ts +18 -29
  188. package/src/daemon/handlers/home-base.ts +5 -1
  189. package/src/daemon/handlers/index.ts +40 -277
  190. package/src/daemon/handlers/misc.ts +9 -1
  191. package/src/daemon/handlers/publish.ts +6 -1
  192. package/src/daemon/handlers/sessions.ts +65 -12
  193. package/src/daemon/handlers/shared.ts +36 -1
  194. package/src/daemon/handlers/signing.ts +37 -0
  195. package/src/daemon/handlers/skills.ts +20 -6
  196. package/src/daemon/handlers/subagents.ts +8 -3
  197. package/src/daemon/handlers/twitter-auth.ts +169 -0
  198. package/src/daemon/handlers/work-items.ts +384 -68
  199. package/src/daemon/ipc-contract-inventory.json +28 -4
  200. package/src/daemon/ipc-contract.ts +133 -37
  201. package/src/daemon/ipc-protocol.ts +7 -2
  202. package/src/daemon/lifecycle.ts +21 -0
  203. package/src/daemon/main.ts +10 -4
  204. package/src/daemon/ride-shotgun-handler.ts +74 -10
  205. package/src/daemon/server.ts +143 -26
  206. package/src/daemon/session-agent-loop.ts +887 -0
  207. package/src/daemon/session-attachments.ts +28 -5
  208. package/src/daemon/session-error.ts +24 -3
  209. package/src/daemon/session-lifecycle.ts +147 -0
  210. package/src/daemon/session-media-retry.ts +147 -0
  211. package/src/daemon/session-messaging.ts +145 -0
  212. package/src/daemon/session-notifiers.ts +164 -0
  213. package/src/daemon/session-process.ts +2 -2
  214. package/src/daemon/session-queue-manager.ts +1 -0
  215. package/src/daemon/session-runtime-assembly.ts +52 -0
  216. package/src/daemon/session-skill-tools.ts +124 -5
  217. package/src/daemon/session-slash.ts +3 -0
  218. package/src/daemon/session-surfaces.ts +77 -2
  219. package/src/daemon/session-tool-setup.ts +216 -2
  220. package/src/daemon/session-usage.ts +0 -2
  221. package/src/daemon/session.ts +114 -1404
  222. package/src/daemon/video-thumbnail.ts +60 -0
  223. package/src/doordash/client.ts +121 -27
  224. package/src/doordash/queries.ts +1 -2
  225. package/src/export/formatter.ts +3 -1
  226. package/src/followups/followup-store.ts +4 -2
  227. package/src/followups/types.ts +6 -0
  228. package/src/hooks/templates.ts +1 -1
  229. package/src/index.ts +32 -1153
  230. package/src/memory/attachments-store.ts +28 -83
  231. package/src/memory/channel-delivery-store.ts +7 -21
  232. package/src/memory/clarification-resolver.ts +6 -5
  233. package/src/memory/contradiction-checker.ts +3 -2
  234. package/src/memory/conversation-key-store.ts +10 -29
  235. package/src/memory/conversation-store.ts +2 -1
  236. package/src/memory/db.ts +96 -2
  237. package/src/memory/entity-extractor.ts +6 -3
  238. package/src/memory/items-extractor.ts +5 -4
  239. package/src/memory/jobs-store.ts +3 -2
  240. package/src/memory/llm-usage-store.ts +1 -2
  241. package/src/memory/runs-store.ts +1 -2
  242. package/src/memory/schema.ts +23 -2
  243. package/src/messaging/style-analyzer.ts +3 -2
  244. package/src/messaging/thread-summarizer.ts +8 -12
  245. package/src/messaging/triage-engine.ts +4 -2
  246. package/src/providers/openrouter/client.ts +20 -0
  247. package/src/providers/registry.ts +8 -0
  248. package/src/runtime/http-server.ts +108 -20
  249. package/src/runtime/routes/attachment-routes.ts +2 -3
  250. package/src/runtime/routes/call-routes.ts +140 -0
  251. package/src/runtime/routes/channel-routes.ts +5 -10
  252. package/src/runtime/routes/conversation-routes.ts +5 -5
  253. package/src/runtime/routes/run-routes.ts +2 -2
  254. package/src/runtime/run-orchestrator.ts +9 -3
  255. package/src/schedule/recurrence-engine.ts +138 -0
  256. package/src/schedule/recurrence-types.ts +67 -0
  257. package/src/schedule/schedule-store.ts +102 -57
  258. package/src/schedule/scheduler.ts +9 -6
  259. package/src/security/oauth2.ts +29 -4
  260. package/src/security/secret-allowlist.ts +46 -0
  261. package/src/skills/clawhub.ts +1 -1
  262. package/src/subagent/manager.ts +40 -8
  263. package/src/swarm/backend-claude-code.ts +64 -9
  264. package/src/swarm/worker-prompts.ts +2 -1
  265. package/src/tasks/SPEC.md +34 -28
  266. package/src/tasks/ephemeral-permissions.ts +16 -7
  267. package/src/tasks/task-compiler.ts +5 -4
  268. package/src/tasks/task-runner.ts +10 -5
  269. package/src/tasks/task-scheduler.ts +1 -1
  270. package/src/tasks/tool-sanitizer.ts +36 -0
  271. package/src/tools/assets/search.ts +4 -4
  272. package/src/tools/browser/api-map.ts +220 -0
  273. package/src/tools/browser/auto-navigate.ts +270 -0
  274. package/src/tools/browser/browser-execution.ts +2 -1
  275. package/src/tools/browser/browser-manager.ts +2 -2
  276. package/src/tools/browser/network-recorder.ts +5 -4
  277. package/src/tools/browser/x-auto-navigate.ts +207 -0
  278. package/src/tools/calls/call-end.ts +17 -67
  279. package/src/tools/calls/call-start.ts +24 -85
  280. package/src/tools/calls/call-status.ts +35 -51
  281. package/src/tools/claude-code/claude-code.ts +77 -11
  282. package/src/tools/contacts/contact-merge.ts +46 -78
  283. package/src/tools/contacts/contact-search.ts +35 -79
  284. package/src/tools/contacts/contact-upsert.ts +35 -108
  285. package/src/tools/credentials/vault.ts +20 -4
  286. package/src/tools/document/document-tool.ts +71 -144
  287. package/src/tools/executor.ts +129 -10
  288. package/src/tools/followups/followup_create.ts +46 -88
  289. package/src/tools/followups/followup_list.ts +34 -74
  290. package/src/tools/followups/followup_resolve.ts +31 -66
  291. package/src/tools/host-terminal/cli-discover.ts +2 -1
  292. package/src/tools/host-terminal/host-shell.ts +10 -0
  293. package/src/tools/memory/handlers.ts +5 -4
  294. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  295. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  296. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  297. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  298. package/src/tools/network/web-fetch.ts +18 -6
  299. package/src/tools/playbooks/index.ts +4 -5
  300. package/src/tools/playbooks/playbook-create.ts +3 -47
  301. package/src/tools/playbooks/playbook-delete.ts +1 -25
  302. package/src/tools/playbooks/playbook-list.ts +1 -28
  303. package/src/tools/playbooks/playbook-update.ts +3 -51
  304. package/src/tools/reminder/reminder.ts +5 -78
  305. package/src/tools/schedule/create.ts +69 -74
  306. package/src/tools/schedule/delete.ts +21 -47
  307. package/src/tools/schedule/list.ts +55 -74
  308. package/src/tools/schedule/update.ts +77 -84
  309. package/src/tools/subagent/abort.ts +29 -58
  310. package/src/tools/subagent/message.ts +30 -63
  311. package/src/tools/subagent/read.ts +53 -84
  312. package/src/tools/subagent/spawn.ts +43 -82
  313. package/src/tools/subagent/status.ts +42 -71
  314. package/src/tools/swarm/delegate.ts +2 -1
  315. package/src/tools/tasks/index.ts +8 -8
  316. package/src/tools/tasks/task-delete.ts +60 -88
  317. package/src/tools/tasks/task-list.ts +31 -52
  318. package/src/tools/tasks/task-run.ts +72 -108
  319. package/src/tools/tasks/task-save.ts +33 -65
  320. package/src/tools/tasks/work-item-enqueue.ts +183 -215
  321. package/src/tools/tasks/work-item-list.ts +33 -63
  322. package/src/tools/tasks/work-item-remove.ts +45 -97
  323. package/src/tools/tasks/work-item-update.ts +91 -163
  324. package/src/tools/terminal/backends/native.ts +3 -1
  325. package/src/tools/tool-manifest.ts +0 -62
  326. package/src/tools/types.ts +6 -0
  327. package/src/tools/ui-surface/definitions.ts +3 -1
  328. package/src/tools/watch/screen-watch.ts +3 -1
  329. package/src/tools/watcher/create.ts +52 -98
  330. package/src/tools/watcher/delete.ts +20 -46
  331. package/src/tools/watcher/digest.ts +36 -70
  332. package/src/tools/watcher/list.ts +49 -79
  333. package/src/tools/watcher/update.ts +45 -91
  334. package/src/twitter/client.ts +690 -0
  335. package/src/twitter/session.ts +91 -0
  336. package/src/usage/types.ts +0 -1
  337. package/src/util/truncate.ts +6 -0
  338. package/src/watcher/providers/slack.ts +2 -1
  339. package/src/watcher/watcher-store.ts +3 -2
  340. package/src/work-items/work-item-store.ts +27 -2
  341. package/src/workspace/commit-message-enrichment-service.ts +31 -7
  342. package/src/workspace/git-service.ts +87 -22
  343. package/src/workspace/provider-commit-message-generator.ts +242 -0
  344. package/src/workspace/turn-commit.ts +62 -3
  345. package/src/tools/contacts/index.ts +0 -4
  346. package/src/tools/document/index.ts +0 -5
  347. package/src/tools/followups/index.ts +0 -3
  348. package/src/tools/subagent/index.ts +0 -5
  349. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
@@ -0,0 +1,776 @@
1
+ import type { Command } from 'commander';
2
+ import * as net from 'node:net';
3
+ import { spawn } from 'node:child_process';
4
+ import { existsSync, statSync, readFileSync } from 'node:fs';
5
+ import { execSync } from 'node:child_process';
6
+ import { join } from 'node:path';
7
+
8
+ import {
9
+ ensureDaemonRunning,
10
+ startDaemon,
11
+ stopDaemon,
12
+ getDaemonStatus,
13
+ } from '../daemon/lifecycle.js';
14
+ import { startCli } from '../cli.js';
15
+ import { getSocketPath, getRootDir, getDataDir, getDbPath, getLogPath, getWorkspaceDir, getWorkspaceSkillsDir, getWorkspaceHooksDir } from '../util/platform.js';
16
+ import { IpcError } from '../util/errors.js';
17
+ import { getCliLogger } from '../util/logger.js';
18
+ import { timeAgo } from '../util/time.js';
19
+ import { shouldAutoStartDaemon, hasSocketOverride } from '../daemon/connection-policy.js';
20
+ import { loadRawConfig } from '../config/loader.js';
21
+ import { getRecentInvocations } from '../memory/tool-usage-store.js';
22
+ import {
23
+ getConversation,
24
+ getMessages,
25
+ listConversations,
26
+ clearAll as clearAllConversations,
27
+ } from '../memory/conversation-store.js';
28
+ import { initializeDb } from '../memory/db.js';
29
+ import { initQdrantClient } from '../memory/qdrant-client.js';
30
+ import { formatMarkdown, formatJson } from '../export/formatter.js';
31
+ import { getConfig } from '../config/loader.js';
32
+ import { sendOneMessage } from './ipc-client.js';
33
+
34
+ const log = getCliLogger('cli');
35
+
36
+ export function registerDefaultAction(program: Command): void {
37
+ program.action(async () => {
38
+ if (shouldAutoStartDaemon()) {
39
+ await ensureDaemonRunning();
40
+ }
41
+ await startCli();
42
+ });
43
+ }
44
+
45
+ export function registerDaemonCommand(program: Command): void {
46
+ const daemon = program.command('daemon').description('Manage the daemon process');
47
+
48
+ daemon
49
+ .command('start')
50
+ .description('Start the daemon')
51
+ .action(async () => {
52
+ const result = await startDaemon();
53
+ if (result.alreadyRunning) {
54
+ log.info(`Daemon already running (pid ${result.pid})`);
55
+ } else {
56
+ log.info(`Daemon started (pid ${result.pid})`);
57
+ }
58
+ });
59
+
60
+ daemon
61
+ .command('stop')
62
+ .description('Stop the daemon')
63
+ .action(async () => {
64
+ const result = await stopDaemon();
65
+ if (result.stopped) {
66
+ log.info('Daemon stopped');
67
+ } else if (result.reason === 'stop_failed') {
68
+ log.error('Failed to stop daemon — process survived SIGKILL');
69
+ process.exit(1);
70
+ } else {
71
+ log.info('Daemon is not running');
72
+ }
73
+ });
74
+
75
+ daemon
76
+ .command('restart')
77
+ .description('Restart the daemon')
78
+ .action(async () => {
79
+ const stopResult = await stopDaemon();
80
+ if (stopResult.stopped) {
81
+ log.info('Daemon stopped');
82
+ } else if (stopResult.reason === 'stop_failed') {
83
+ log.error('Failed to stop daemon — process survived SIGKILL, cannot restart');
84
+ process.exit(1);
85
+ }
86
+ const startResult = await startDaemon();
87
+ log.info(`Daemon started (pid ${startResult.pid})`);
88
+ });
89
+
90
+ daemon
91
+ .command('status')
92
+ .description('Show daemon status')
93
+ .action(() => {
94
+ const status = getDaemonStatus();
95
+ if (status.running) {
96
+ log.info(`Daemon is running (pid ${status.pid})`);
97
+ } else {
98
+ log.info('Daemon is not running');
99
+ }
100
+ log.info(`Socket path: ${getSocketPath()}${hasSocketOverride() ? ' (override)' : ''}`);
101
+ log.info(`Autostart: ${shouldAutoStartDaemon() ? 'enabled' : 'disabled'}`);
102
+ });
103
+ }
104
+
105
+ export function registerDevCommand(program: Command): void {
106
+ program
107
+ .command('dev')
108
+ .description('Run the daemon in dev mode with auto-restart on file changes')
109
+ .action(async () => {
110
+ const status = getDaemonStatus();
111
+ if (status.running) {
112
+ log.info('Stopping existing daemon...');
113
+ const stopResult = await stopDaemon();
114
+ if (!stopResult.stopped && stopResult.reason === 'stop_failed') {
115
+ log.error('Failed to stop existing daemon — process survived SIGKILL');
116
+ process.exit(1);
117
+ }
118
+ }
119
+
120
+ const mainPath = `${import.meta.dirname}/../daemon/main.ts`;
121
+
122
+ log.info('Starting daemon in dev mode (Ctrl+C to stop)');
123
+
124
+ const repoRoot = join(import.meta.dirname, '..', '..', '..');
125
+ const child = spawn('bun', ['--watch', 'run', mainPath], {
126
+ stdio: 'inherit',
127
+ env: {
128
+ ...process.env,
129
+ BASE_DATA_DIR: repoRoot,
130
+ VELLUM_LOG_STDERR: '1',
131
+ VELLUM_DEBUG: '1',
132
+ },
133
+ });
134
+
135
+ const forward = (signal: NodeJS.Signals) => {
136
+ child.kill(signal);
137
+ };
138
+ process.on('SIGINT', () => forward('SIGINT'));
139
+ process.on('SIGTERM', () => forward('SIGTERM'));
140
+
141
+ child.on('exit', (code) => {
142
+ process.exit(code ?? 0);
143
+ });
144
+ });
145
+ }
146
+
147
+ export function registerSessionsCommand(program: Command): void {
148
+ const sessions = program.command('sessions').description('Manage sessions');
149
+
150
+ sessions
151
+ .command('list')
152
+ .description('List all sessions')
153
+ .action(async () => {
154
+ if (shouldAutoStartDaemon()) await ensureDaemonRunning();
155
+ const response = await sendOneMessage({ type: 'session_list' });
156
+ if (response.type === 'session_list_response') {
157
+ if (response.sessions.length === 0) {
158
+ log.info('No sessions');
159
+ } else {
160
+ for (const s of response.sessions) {
161
+ log.info(` ${s.id} ${s.title} ${timeAgo(s.updatedAt)}`);
162
+ }
163
+ }
164
+ } else if (response.type === 'error') {
165
+ log.error(`Error: ${response.message}`);
166
+ }
167
+ });
168
+
169
+ sessions
170
+ .command('new [title]')
171
+ .description('Create a new session')
172
+ .action(async (title?: string) => {
173
+ if (shouldAutoStartDaemon()) await ensureDaemonRunning();
174
+ const response = await sendOneMessage({
175
+ type: 'session_create',
176
+ title,
177
+ });
178
+ if (response.type === 'session_info') {
179
+ log.info(`Created session: ${response.title} (${response.sessionId})`);
180
+ } else if (response.type === 'error') {
181
+ log.error(`Error: ${response.message}`);
182
+ }
183
+ });
184
+
185
+ sessions
186
+ .command('export [sessionId]')
187
+ .description('Export a conversation as markdown or JSON')
188
+ .option('-f, --format <format>', 'Output format: md or json', 'md')
189
+ .option('-o, --output <file>', 'Write to file instead of stdout')
190
+ .action(async (sessionId?: string, opts?: { format: string; output?: string }) => {
191
+ initializeDb();
192
+ const format = opts?.format ?? 'md';
193
+ if (format !== 'md' && format !== 'json') {
194
+ log.error('Error: format must be "md" or "json"');
195
+ process.exit(1);
196
+ }
197
+
198
+ let id = sessionId;
199
+ if (!id) {
200
+ const all = listConversations(1);
201
+ if (all.length === 0) {
202
+ log.error('No sessions found');
203
+ process.exit(1);
204
+ }
205
+ id = all[0].id;
206
+ }
207
+
208
+ // Support prefix matching for session IDs
209
+ let conversation = getConversation(id);
210
+ if (!conversation) {
211
+ const all = listConversations(Number.MAX_SAFE_INTEGER);
212
+ const match = all.find((c) => c.id.startsWith(id!));
213
+ if (match) {
214
+ conversation = match;
215
+ } else {
216
+ log.error(`Session not found: ${id}`);
217
+ process.exit(1);
218
+ }
219
+ }
220
+
221
+ const msgs = getMessages(conversation.id);
222
+ const exportData = {
223
+ ...conversation,
224
+ messages: msgs.map((m) => ({
225
+ role: m.role,
226
+ content: JSON.parse(m.content),
227
+ createdAt: m.createdAt,
228
+ })),
229
+ };
230
+
231
+ const output = format === 'json'
232
+ ? formatJson(exportData)
233
+ : formatMarkdown(exportData);
234
+
235
+ if (opts?.output) {
236
+ const { writeFileSync } = await import('node:fs');
237
+ writeFileSync(opts.output, output);
238
+ log.info(`Exported to ${opts.output}`);
239
+ } else {
240
+ process.stdout.write(output);
241
+ }
242
+ });
243
+
244
+ sessions
245
+ .command('clear')
246
+ .description('Clear all conversations, messages, and vector data (dev only)')
247
+ .action(async () => {
248
+ log.info('This will permanently delete all conversations, messages, and vector data.');
249
+
250
+ const readline = await import('node:readline');
251
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
252
+ const answer = await new Promise<string>((resolve) => {
253
+ rl.question('Are you sure? (y/N) ', resolve);
254
+ });
255
+ rl.close();
256
+ if (answer.toLowerCase() !== 'y') {
257
+ log.info('Cancelled');
258
+ return;
259
+ }
260
+
261
+ initializeDb();
262
+ const result = clearAllConversations();
263
+ log.info(`Cleared ${result.conversations} conversations, ${result.messages} messages`);
264
+
265
+ // Notify a running daemon to drop its in-memory sessions so it
266
+ // doesn't keep serving stale history from deleted conversation rows.
267
+ try {
268
+ await sendOneMessage({ type: 'sessions_clear' });
269
+ } catch {
270
+ // Daemon may not be running — that's fine, no sessions to invalidate.
271
+ }
272
+
273
+ const config = getConfig();
274
+ const qdrantUrl = process.env.QDRANT_URL?.trim() || config.memory.qdrant.url;
275
+ const qdrant = initQdrantClient({
276
+ url: qdrantUrl,
277
+ collection: config.memory.qdrant.collection,
278
+ vectorSize: config.memory.qdrant.vectorSize,
279
+ onDisk: config.memory.qdrant.onDisk,
280
+ quantization: config.memory.qdrant.quantization,
281
+ });
282
+ const deleted = await qdrant.deleteCollection();
283
+ if (deleted) {
284
+ log.info(`Deleted Qdrant collection "${config.memory.qdrant.collection}"`);
285
+ } else {
286
+ log.info('Qdrant collection not found or not reachable (skipped)');
287
+ }
288
+
289
+ log.info('Done.');
290
+ });
291
+ }
292
+
293
+ export function registerAuditCommand(program: Command): void {
294
+ program
295
+ .command('audit')
296
+ .description('Show recent tool invocations')
297
+ .option('-l, --limit <n>', 'Number of entries to show', '20')
298
+ .action((opts: { limit: string }) => {
299
+ const limit = parseInt(opts.limit, 10) || 20;
300
+ const rows = getRecentInvocations(limit);
301
+ if (rows.length === 0) {
302
+ log.info('No tool invocations recorded');
303
+ return;
304
+ }
305
+ const tsW = 20;
306
+ const toolW = 14;
307
+ const inputW = 30;
308
+ const decW = 8;
309
+ const riskW = 8;
310
+ const durW = 8;
311
+ log.info(
312
+ 'Timestamp'.padEnd(tsW) +
313
+ 'Tool'.padEnd(toolW) +
314
+ 'Input'.padEnd(inputW) +
315
+ 'Decision'.padEnd(decW) +
316
+ 'Risk'.padEnd(riskW) +
317
+ 'Duration',
318
+ );
319
+ log.info('-'.repeat(tsW + toolW + inputW + decW + riskW + durW));
320
+ for (const r of rows) {
321
+ const ts = new Date(r.createdAt).toISOString().slice(0, 19).replace('T', ' ');
322
+ let inputSummary = '';
323
+ try {
324
+ const parsed = JSON.parse(r.input);
325
+ if (parsed.command) inputSummary = parsed.command;
326
+ else if (parsed.path) inputSummary = parsed.path;
327
+ else inputSummary = r.input;
328
+ } catch {
329
+ inputSummary = r.input;
330
+ }
331
+ if (inputSummary.length > inputW - 2) {
332
+ inputSummary = inputSummary.slice(0, inputW - 4) + '..';
333
+ }
334
+ const dur = r.durationMs < 1000 ? `${r.durationMs}ms` : `${(r.durationMs / 1000).toFixed(1)}s`;
335
+ log.info(
336
+ ts.padEnd(tsW) +
337
+ r.toolName.padEnd(toolW) +
338
+ inputSummary.padEnd(inputW) +
339
+ r.decision.padEnd(decW) +
340
+ r.riskLevel.padEnd(riskW) +
341
+ dur,
342
+ );
343
+ }
344
+ });
345
+ }
346
+
347
+ export function registerDoctorCommand(program: Command): void {
348
+ program
349
+ .command('doctor')
350
+ .description('Run diagnostic checks')
351
+ .action(async () => {
352
+ const pass = (label: string) => log.info(` \u2713 ${label}`);
353
+ const fail = (label: string, detail?: string) =>
354
+ log.info(` \u2717 ${label}${detail ? ` — ${detail}` : ''}`);
355
+
356
+ log.info('Vellum Doctor\n');
357
+
358
+ // 0. Connection policy info
359
+ const socketPath = getSocketPath();
360
+ const isOverride = hasSocketOverride();
361
+ const autostart = shouldAutoStartDaemon();
362
+ log.info(` Socket: ${socketPath}${isOverride ? ' (override via VELLUM_DAEMON_SOCKET)' : ''}`);
363
+ log.info(` Autostart: ${autostart ? 'enabled' : 'disabled'}\n`);
364
+
365
+ // 1. Bun installed
366
+ try {
367
+ execSync('bun --version', { stdio: 'pipe' });
368
+ pass('Bun is installed');
369
+ } catch {
370
+ fail('Bun is installed', 'bun not found in PATH');
371
+ }
372
+
373
+ // 2. Provider/API key configured
374
+ const raw = loadRawConfig();
375
+ const provider = typeof raw.provider === 'string' ? raw.provider : 'anthropic';
376
+ const providerEnvVar: Record<string, string> = {
377
+ anthropic: 'ANTHROPIC_API_KEY',
378
+ openai: 'OPENAI_API_KEY',
379
+ gemini: 'GEMINI_API_KEY',
380
+ ollama: 'OLLAMA_API_KEY',
381
+ fireworks: 'FIREWORKS_API_KEY',
382
+ openrouter: 'OPENROUTER_API_KEY',
383
+ };
384
+ const configKey = (raw.apiKeys as Record<string, string> | undefined)?.[provider];
385
+ const envVar = providerEnvVar[provider];
386
+ const envKey = envVar ? process.env[envVar] : undefined;
387
+
388
+ if (provider === 'ollama') {
389
+ pass('Provider configured (Ollama; API key optional)');
390
+ } else if (envKey || configKey) {
391
+ pass('API key configured');
392
+ } else {
393
+ fail(
394
+ 'API key configured',
395
+ envVar
396
+ ? `set ${envVar} or run: vellum config set apiKeys.${provider} <key>`
397
+ : `set API key for provider "${provider}"`,
398
+ );
399
+ }
400
+
401
+ // 3. Daemon reachable
402
+ try {
403
+ const sock = getSocketPath();
404
+ if (!existsSync(sock)) {
405
+ fail('Daemon reachable', 'socket not found (is the daemon running?)');
406
+ } else {
407
+ await new Promise<void>((resolve, reject) => {
408
+ const s = net.createConnection(sock);
409
+ const timer = setTimeout(() => { s.destroy(); reject(new IpcError('timeout')); }, 2000);
410
+ s.on('connect', () => { clearTimeout(timer); s.end(); resolve(); });
411
+ s.on('error', (err) => { clearTimeout(timer); reject(err); });
412
+ });
413
+ pass('Daemon reachable');
414
+ }
415
+ } catch {
416
+ fail('Daemon reachable', 'could not connect to daemon socket');
417
+ }
418
+
419
+ // 4. DB exists and readable
420
+ const dbPath = getDbPath();
421
+ if (existsSync(dbPath)) {
422
+ try {
423
+ const { Database } = await import('bun:sqlite');
424
+ const db = new Database(dbPath, { readonly: true });
425
+ db.query('SELECT 1').get();
426
+ db.close();
427
+ pass('Database exists and readable');
428
+ } catch {
429
+ fail('Database exists and readable', 'file exists but cannot be read');
430
+ }
431
+ } else {
432
+ fail('Database exists and readable', `not found at ${dbPath}`);
433
+ }
434
+
435
+ // 5. ~/.vellum/ directory structure (workspace layout)
436
+ const rootDir = getRootDir();
437
+ const dataDir = getDataDir();
438
+ const workspaceDir = getWorkspaceDir();
439
+ const requiredDirs = [rootDir, workspaceDir, dataDir, `${dataDir}/db`, `${dataDir}/logs`, getWorkspaceSkillsDir(), getWorkspaceHooksDir(), `${rootDir}/protected`];
440
+ const missing = requiredDirs.filter((d) => !existsSync(d));
441
+ if (missing.length === 0) {
442
+ pass('Directory structure exists');
443
+ } else {
444
+ fail('Directory structure exists', `missing: ${missing.join(', ')}`);
445
+ }
446
+
447
+ // 6. Disk space
448
+ try {
449
+ const output = execSync(`df -k "${rootDir}"`, { stdio: 'pipe', encoding: 'utf-8' });
450
+ const lines = output.trim().split('\n');
451
+ if (lines.length >= 2) {
452
+ const cols = lines[1].trim().split(/\s+/);
453
+ const availKB = parseInt(cols[3], 10);
454
+ if (isNaN(availKB)) {
455
+ fail('Disk space', 'could not parse available space');
456
+ } else if (availKB < 100 * 1024) {
457
+ fail('Disk space', `only ${Math.round(availKB / 1024)}MB free (< 100MB)`);
458
+ } else {
459
+ pass(`Disk space (${Math.round(availKB / 1024)}MB free)`);
460
+ }
461
+ } else {
462
+ fail('Disk space', 'unexpected df output');
463
+ }
464
+ } catch {
465
+ fail('Disk space', 'could not check disk space');
466
+ }
467
+
468
+ // 7. Log file size
469
+ const logPath = getLogPath();
470
+ if (existsSync(logPath)) {
471
+ try {
472
+ const logStat = statSync(logPath);
473
+ const logSizeMB = logStat.size / (1024 * 1024);
474
+ if (logSizeMB > 50) {
475
+ fail('Log file size', `${logSizeMB.toFixed(1)}MB (> 50MB)`);
476
+ } else {
477
+ pass(`Log file size (${logSizeMB.toFixed(1)}MB)`);
478
+ }
479
+ } catch {
480
+ fail('Log file size', 'could not stat log file');
481
+ }
482
+ } else {
483
+ pass('Log file size (no log file yet)');
484
+ }
485
+
486
+ // 8. DB integrity check
487
+ if (existsSync(dbPath)) {
488
+ try {
489
+ const { Database } = await import('bun:sqlite');
490
+ const db = new Database(dbPath, { readonly: true });
491
+ const result = db.query('PRAGMA integrity_check').get() as { integrity_check: string } | null;
492
+ db.close();
493
+ if (result?.integrity_check === 'ok') {
494
+ pass('Database integrity check');
495
+ } else {
496
+ fail('Database integrity check', result?.integrity_check ?? 'unknown result');
497
+ }
498
+ } catch (err) {
499
+ fail('Database integrity check', err instanceof Error ? err.message : 'unknown error');
500
+ }
501
+ } else {
502
+ fail('Database integrity check', 'database file not found');
503
+ }
504
+
505
+ // 9. Socket permissions
506
+ const sockPath = getSocketPath();
507
+ if (existsSync(sockPath)) {
508
+ try {
509
+ const sockStat = statSync(sockPath);
510
+ const mode = sockStat.mode & 0o777;
511
+ if (mode === 0o600 || mode === 0o700) {
512
+ pass(`Socket permissions (${mode.toString(8).padStart(4, '0')})`);
513
+ } else {
514
+ fail('Socket permissions', `expected 0600 or 0700, got 0${mode.toString(8)}`);
515
+ }
516
+ } catch {
517
+ fail('Socket permissions', 'could not stat socket');
518
+ }
519
+ } else {
520
+ pass('Socket permissions (socket not present — daemon not running)');
521
+ }
522
+
523
+ // 10. Trust rule syntax
524
+ const trustPath = `${rootDir}/protected/trust.json`;
525
+ if (existsSync(trustPath)) {
526
+ try {
527
+ const rawTrust = readFileSync(trustPath, 'utf-8');
528
+ const data = JSON.parse(rawTrust);
529
+ if (typeof data !== 'object' || data === null) {
530
+ fail('Trust rule syntax', 'trust.json is not a JSON object');
531
+ } else if (typeof data.version !== 'number') {
532
+ fail('Trust rule syntax', 'missing or invalid "version" field');
533
+ } else if (!Array.isArray(data.rules)) {
534
+ fail('Trust rule syntax', 'missing or invalid "rules" array');
535
+ } else {
536
+ const invalid = data.rules.filter(
537
+ (r: unknown) =>
538
+ typeof r !== 'object' || r === null ||
539
+ typeof (r as Record<string, unknown>).tool !== 'string' ||
540
+ typeof (r as Record<string, unknown>).pattern !== 'string' ||
541
+ typeof (r as Record<string, unknown>).scope !== 'string',
542
+ );
543
+ if (invalid.length > 0) {
544
+ fail('Trust rule syntax', `${invalid.length} rule(s) have invalid structure`);
545
+ } else {
546
+ pass(`Trust rule syntax (${data.rules.length} rule(s))`);
547
+ }
548
+ }
549
+ } catch (err) {
550
+ fail('Trust rule syntax', err instanceof Error ? err.message : 'could not parse');
551
+ }
552
+ } else {
553
+ pass('Trust rule syntax (no trust.json yet)');
554
+ }
555
+
556
+ // 11. WASM files
557
+ const wasmFiles = [
558
+ 'node_modules/web-tree-sitter/web-tree-sitter.wasm',
559
+ 'node_modules/tree-sitter-bash/tree-sitter-bash.wasm',
560
+ ];
561
+ let wasmOk = true;
562
+ const missingWasm: string[] = [];
563
+ for (const wasm of wasmFiles) {
564
+ const fullPath = `${import.meta.dirname}/../../${wasm}`;
565
+ if (!existsSync(fullPath)) {
566
+ missingWasm.push(wasm);
567
+ wasmOk = false;
568
+ } else {
569
+ try {
570
+ const wasmStat = statSync(fullPath);
571
+ if (wasmStat.size === 0) {
572
+ missingWasm.push(`${wasm} (empty)`);
573
+ wasmOk = false;
574
+ }
575
+ } catch {
576
+ missingWasm.push(`${wasm} (unreadable)`);
577
+ wasmOk = false;
578
+ }
579
+ }
580
+ }
581
+ if (wasmOk) {
582
+ pass('WASM files present and non-empty');
583
+ } else {
584
+ fail('WASM files', missingWasm.join(', '));
585
+ }
586
+
587
+ // 12. Browser runtime (Playwright + Chromium)
588
+ const { checkBrowserRuntime } = await import('../tools/browser/runtime-check.js');
589
+ const browserStatus = await checkBrowserRuntime();
590
+ if (browserStatus.playwrightAvailable && browserStatus.chromiumInstalled) {
591
+ pass('Browser runtime (Playwright + Chromium)');
592
+ } else if (!browserStatus.playwrightAvailable) {
593
+ fail('Browser runtime', 'playwright not available');
594
+ } else {
595
+ fail('Browser runtime', browserStatus.error ?? 'Chromium not installed');
596
+ }
597
+
598
+ // 13. Sandbox backend diagnostics
599
+ const { runSandboxDiagnostics } = await import('../tools/terminal/sandbox-diagnostics.js');
600
+ const sandbox = runSandboxDiagnostics();
601
+ log.info(`\n Sandbox: ${sandbox.config.enabled ? 'enabled' : 'disabled'}`);
602
+ log.info(` Backend: ${sandbox.config.backend}`);
603
+ log.info(` Reason: ${sandbox.activeBackendReason}`);
604
+ if (sandbox.config.backend === 'docker') {
605
+ log.info(` Image: ${sandbox.config.dockerImage}`);
606
+ }
607
+ log.info('');
608
+ for (const check of sandbox.checks) {
609
+ if (check.ok) {
610
+ pass(check.label);
611
+ } else {
612
+ fail(check.label, check.detail);
613
+ }
614
+ }
615
+ });
616
+ }
617
+
618
+ export function registerCompletionsCommand(program: Command): void {
619
+ program
620
+ .command('completions')
621
+ .argument('<shell>', 'Shell type: bash, zsh, or fish')
622
+ .description('Generate shell completion script (e.g. vellum completions bash >> ~/.bashrc)')
623
+ .action((shell: string) => {
624
+ const subcommands: Record<string, string[]> = {
625
+ daemon: ['start', 'stop', 'restart', 'status'],
626
+ sessions: ['list', 'new', 'export', 'clear'],
627
+ config: ['set', 'get', 'list', 'validate-allowlist'],
628
+ keys: ['list', 'set', 'delete'],
629
+ trust: ['list', 'remove', 'clear'],
630
+ memory: ['status', 'backfill', 'cleanup', 'query', 'rebuild-index'],
631
+ hooks: ['list', 'enable', 'disable', 'install', 'remove'],
632
+ contacts: ['list', 'get', 'merge'],
633
+ autonomy: ['get', 'set'],
634
+ };
635
+ const topLevel = [
636
+ 'daemon', 'dev', 'sessions', 'config', 'keys', 'trust', 'memory',
637
+ 'hooks', 'contacts', 'autonomy', 'audit', 'doctor', 'completions', 'help',
638
+ ];
639
+
640
+ switch (shell) {
641
+ case 'bash':
642
+ process.stdout.write(generateBashCompletion(topLevel, subcommands));
643
+ break;
644
+ case 'zsh':
645
+ process.stdout.write(generateZshCompletion(topLevel, subcommands));
646
+ break;
647
+ case 'fish':
648
+ process.stdout.write(generateFishCompletion(topLevel, subcommands));
649
+ break;
650
+ default:
651
+ log.error(`Unknown shell: ${shell}. Supported shells: bash, zsh, fish`);
652
+ process.exit(1);
653
+ }
654
+ });
655
+ }
656
+
657
+ function generateBashCompletion(
658
+ topLevel: string[],
659
+ subcommands: Record<string, string[]>,
660
+ ): string {
661
+ const subcmdCases = Object.entries(subcommands)
662
+ .map(([cmd, subs]) => ` ${cmd}) COMPREPLY=( $(compgen -W "${subs.join(' ')}" -- "$cur") ) ;;`)
663
+ .join('\n');
664
+
665
+ return `# vellum bash completion
666
+ # Add to ~/.bashrc: eval "$(vellum completions bash)"
667
+ _vellum_completions() {
668
+ local cur prev words cword
669
+ _init_completion || return
670
+
671
+ if [[ $cword -eq 1 ]]; then
672
+ COMPREPLY=( $(compgen -W "${topLevel.join(' ')} --help --version" -- "$cur") )
673
+ return
674
+ fi
675
+
676
+ case "\${words[1]}" in
677
+ ${subcmdCases}
678
+ audit) COMPREPLY=( $(compgen -W "--limit -l" -- "$cur") ) ;;
679
+ completions) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ) ;;
680
+ esac
681
+ }
682
+ complete -F _vellum_completions vellum
683
+ `;
684
+ }
685
+
686
+ function generateZshCompletion(
687
+ topLevel: string[],
688
+ subcommands: Record<string, string[]>,
689
+ ): string {
690
+ const subcmdCases = Object.entries(subcommands)
691
+ .map(([cmd, subs]) => ` ${cmd}) compadd ${subs.join(' ')} ;;`)
692
+ .join('\n');
693
+
694
+ return `#compdef vellum
695
+ # vellum zsh completion
696
+ # Add to ~/.zshrc: eval "$(vellum completions zsh)"
697
+ _vellum() {
698
+ local -a commands
699
+ commands=(
700
+ 'daemon:Manage the daemon process'
701
+ 'dev:Run daemon in dev mode with auto-restart'
702
+ 'sessions:Manage sessions'
703
+ 'config:Manage configuration'
704
+ 'keys:Manage API keys in secure storage'
705
+ 'trust:Manage trust rules'
706
+ 'memory:Manage long-term memory'
707
+ 'hooks:Manage hooks'
708
+ 'contacts:Manage the contact graph'
709
+ 'autonomy:View and configure autonomy tiers'
710
+ 'audit:Show recent tool invocations'
711
+ 'doctor:Run diagnostic checks'
712
+ 'completions:Generate shell completion script'
713
+ 'help:Display help'
714
+ )
715
+
716
+ if (( CURRENT == 2 )); then
717
+ _describe 'command' commands
718
+ _arguments '--help[Show help]' '--version[Show version]'
719
+ return
720
+ fi
721
+
722
+ case "\${words[2]}" in
723
+ ${subcmdCases}
724
+ audit) _arguments '-l[Number of entries]' '--limit[Number of entries]' ;;
725
+ completions) compadd bash zsh fish ;;
726
+ esac
727
+ }
728
+ compdef _vellum vellum
729
+ `;
730
+ }
731
+
732
+ function generateFishCompletion(
733
+ topLevel: string[],
734
+ subcommands: Record<string, string[]>,
735
+ ): string {
736
+ let script = `# vellum fish completion
737
+ # Add to ~/.config/fish/completions/vellum.fish or eval: vellum completions fish | source
738
+ `;
739
+
740
+ script += `complete -c vellum -f\n`;
741
+
742
+ const descriptions: Record<string, string> = {
743
+ daemon: 'Manage the daemon process',
744
+ dev: 'Run daemon in dev mode with auto-restart',
745
+ sessions: 'Manage sessions',
746
+ config: 'Manage configuration',
747
+ keys: 'Manage API keys in secure storage',
748
+ trust: 'Manage trust rules',
749
+ memory: 'Manage long-term memory',
750
+ hooks: 'Manage hooks',
751
+ contacts: 'Manage the contact graph',
752
+ autonomy: 'View and configure autonomy tiers',
753
+ audit: 'Show recent tool invocations',
754
+ doctor: 'Run diagnostic checks',
755
+ completions: 'Generate shell completion script',
756
+ help: 'Display help',
757
+ };
758
+
759
+ for (const cmd of topLevel) {
760
+ const desc = descriptions[cmd] ?? '';
761
+ script += `complete -c vellum -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'\n`;
762
+ }
763
+ script += `complete -c vellum -n '__fish_use_subcommand' -l help -d 'Show help'\n`;
764
+ script += `complete -c vellum -n '__fish_use_subcommand' -l version -d 'Show version'\n`;
765
+
766
+ for (const [cmd, subs] of Object.entries(subcommands)) {
767
+ for (const sub of subs) {
768
+ script += `complete -c vellum -n '__fish_seen_subcommand_from ${cmd}' -a '${sub}'\n`;
769
+ }
770
+ }
771
+
772
+ script += `complete -c vellum -n '__fish_seen_subcommand_from audit' -s l -l limit -d 'Number of entries'\n`;
773
+ script += `complete -c vellum -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'\n`;
774
+
775
+ return script;
776
+ }