vellum 0.2.0 → 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 (361) 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 +161 -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__/app-bundler.test.ts +12 -33
  11. package/src/__tests__/asset-materialize-tool.test.ts +16 -15
  12. package/src/__tests__/asset-search-tool.test.ts +23 -22
  13. package/src/__tests__/attachments-store.test.ts +56 -127
  14. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +5 -4
  15. package/src/__tests__/browser-skill-endstate.test.ts +5 -8
  16. package/src/__tests__/call-bridge.test.ts +385 -0
  17. package/src/__tests__/call-constants.test.ts +40 -0
  18. package/src/__tests__/call-orchestrator.test.ts +454 -0
  19. package/src/__tests__/call-recovery.test.ts +518 -0
  20. package/src/__tests__/call-routes-http.test.ts +459 -0
  21. package/src/__tests__/call-state-machine.test.ts +143 -0
  22. package/src/__tests__/call-state.test.ts +133 -0
  23. package/src/__tests__/call-store.test.ts +691 -0
  24. package/src/__tests__/cli-discover.test.ts +1 -1
  25. package/src/__tests__/commit-message-enrichment-service.test.ts +550 -0
  26. package/src/__tests__/compaction.benchmark.test.ts +176 -0
  27. package/src/__tests__/computer-use-tools.test.ts +250 -0
  28. package/src/__tests__/config-schema.test.ts +348 -3
  29. package/src/__tests__/conflict-store.test.ts +2 -1
  30. package/src/__tests__/contacts-tools.test.ts +331 -0
  31. package/src/__tests__/conversation-store.test.ts +30 -32
  32. package/src/__tests__/credential-security-invariants.test.ts +4 -0
  33. package/src/__tests__/date-context.test.ts +373 -0
  34. package/src/__tests__/db-schedule-syntax-migration.test.ts +129 -0
  35. package/src/__tests__/doordash-session.test.ts +9 -0
  36. package/src/__tests__/fixtures/media-reuse-fixtures.ts +3 -3
  37. package/src/__tests__/followup-tools.test.ts +303 -0
  38. package/src/__tests__/handlers-twitter-config.test.ts +718 -0
  39. package/src/__tests__/intent-routing.test.ts +64 -57
  40. package/src/__tests__/ipc-roundtrip.benchmark.test.ts +237 -0
  41. package/src/__tests__/ipc-snapshot.test.ts +96 -28
  42. package/src/__tests__/llm-usage-store.test.ts +3 -8
  43. package/src/__tests__/media-generate-image.test.ts +1 -1
  44. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -7
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +430 -0
  46. package/src/__tests__/parallel-tool.benchmark.test.ts +294 -0
  47. package/src/__tests__/playbook-tools.test.ts +342 -0
  48. package/src/__tests__/profile-compiler.test.ts +2 -1
  49. package/src/__tests__/provider-streaming.benchmark.test.ts +773 -0
  50. package/src/__tests__/recurrence-engine-rruleset.test.ts +78 -0
  51. package/src/__tests__/recurrence-engine.test.ts +69 -0
  52. package/src/__tests__/recurrence-types.test.ts +71 -0
  53. package/src/__tests__/registry.test.ts +17 -10
  54. package/src/__tests__/relay-server.test.ts +633 -0
  55. package/src/__tests__/reminder-store.test.ts +6 -3
  56. package/src/__tests__/reminder.test.ts +43 -77
  57. package/src/__tests__/run-orchestrator-assistant-events.test.ts +222 -0
  58. package/src/__tests__/run-orchestrator.test.ts +7 -7
  59. package/src/__tests__/runtime-attachment-metadata.test.ts +19 -20
  60. package/src/__tests__/runtime-runs-http.test.ts +5 -23
  61. package/src/__tests__/runtime-runs.test.ts +11 -11
  62. package/src/__tests__/schedule-store.test.ts +482 -0
  63. package/src/__tests__/schedule-tools.test.ts +700 -0
  64. package/src/__tests__/scheduler-recurrence.test.ts +329 -0
  65. package/src/__tests__/server-history-render.test.ts +14 -13
  66. package/src/__tests__/session-error.test.ts +28 -0
  67. package/src/__tests__/session-init.benchmark.test.ts +462 -0
  68. package/src/__tests__/session-queue.test.ts +89 -16
  69. package/src/__tests__/session-runtime-assembly.test.ts +161 -0
  70. package/src/__tests__/session-surfaces-task-progress.test.ts +104 -0
  71. package/src/__tests__/signup-e2e.test.ts +2 -1
  72. package/src/__tests__/skill-projection.benchmark.test.ts +328 -0
  73. package/src/__tests__/skill-script-runner.test.ts +159 -0
  74. package/src/__tests__/speaker-identification.test.ts +52 -0
  75. package/src/__tests__/subagent-manager-notify.test.ts +42 -10
  76. package/src/__tests__/subagent-tools.test.ts +141 -41
  77. package/src/__tests__/task-compiler.test.ts +2 -1
  78. package/src/__tests__/task-runner.test.ts +2 -1
  79. package/src/__tests__/task-scheduler.test.ts +2 -1
  80. package/src/__tests__/task-tools.test.ts +49 -56
  81. package/src/__tests__/tool-audit-listener.test.ts +1 -0
  82. package/src/__tests__/tool-domain-event-publisher.test.ts +2 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +500 -0
  84. package/src/__tests__/tool-executor.test.ts +13 -17
  85. package/src/__tests__/turn-commit.test.ts +273 -2
  86. package/src/__tests__/twilio-provider.test.ts +143 -0
  87. package/src/__tests__/twilio-routes.test.ts +789 -0
  88. package/src/__tests__/twitter-auth-handler.test.ts +581 -0
  89. package/src/__tests__/view-image-tool.test.ts +217 -0
  90. package/src/__tests__/workspace-git-service.test.ts +403 -0
  91. package/src/__tests__/workspace-heartbeat-service.test.ts +141 -2
  92. package/src/agent-heartbeat/agent-heartbeat-service.ts +155 -0
  93. package/src/bundler/app-bundler.ts +35 -14
  94. package/src/calls/call-bridge.ts +95 -0
  95. package/src/calls/call-constants.ts +48 -0
  96. package/src/calls/call-domain.ts +276 -0
  97. package/src/calls/call-orchestrator.ts +390 -0
  98. package/src/calls/call-recovery.ts +207 -0
  99. package/src/calls/call-state-machine.ts +68 -0
  100. package/src/calls/call-state.ts +64 -0
  101. package/src/calls/call-store.ts +416 -0
  102. package/src/calls/relay-server.ts +335 -0
  103. package/src/calls/speaker-identification.ts +213 -0
  104. package/src/calls/twilio-config.ts +34 -0
  105. package/src/calls/twilio-provider.ts +173 -0
  106. package/src/calls/twilio-routes.ts +250 -0
  107. package/src/calls/types.ts +37 -0
  108. package/src/calls/voice-provider.ts +14 -0
  109. package/src/cli/config-commands.ts +334 -0
  110. package/src/cli/core-commands.ts +776 -0
  111. package/src/cli/doordash.ts +256 -25
  112. package/src/cli/ipc-client.ts +82 -0
  113. package/src/cli/map.ts +246 -0
  114. package/src/cli/twitter.ts +575 -0
  115. package/src/cli.ts +7 -5
  116. package/src/commands/__tests__/cc-command-registry.test.ts +319 -0
  117. package/src/commands/cc-command-registry.ts +209 -0
  118. package/src/config/bundled-skills/contacts/SKILL.md +39 -0
  119. package/src/config/bundled-skills/contacts/TOOLS.json +122 -0
  120. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +9 -0
  121. package/src/config/bundled-skills/contacts/tools/contact-search.ts +9 -0
  122. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +9 -0
  123. package/src/config/bundled-skills/document/SKILL.md +18 -0
  124. package/src/config/bundled-skills/document/TOOLS.json +53 -0
  125. package/src/config/bundled-skills/document/tools/document-create.ts +9 -0
  126. package/src/config/bundled-skills/document/tools/document-update.ts +9 -0
  127. package/src/config/bundled-skills/doordash/SKILL.md +163 -0
  128. package/src/config/bundled-skills/followups/SKILL.md +32 -0
  129. package/src/config/bundled-skills/followups/TOOLS.json +100 -0
  130. package/src/config/bundled-skills/followups/tools/followup-create.ts +9 -0
  131. package/src/config/bundled-skills/followups/tools/followup-list.ts +9 -0
  132. package/src/config/bundled-skills/followups/tools/followup-resolve.ts +9 -0
  133. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -2
  134. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +2 -24
  135. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +2 -1
  136. package/src/config/bundled-skills/playbooks/SKILL.md +31 -0
  137. package/src/config/bundled-skills/playbooks/TOOLS.json +126 -0
  138. package/src/config/bundled-skills/playbooks/tools/playbook-create.ts +9 -0
  139. package/src/config/bundled-skills/playbooks/tools/playbook-delete.ts +9 -0
  140. package/src/config/bundled-skills/playbooks/tools/playbook-list.ts +9 -0
  141. package/src/config/bundled-skills/playbooks/tools/playbook-update.ts +9 -0
  142. package/src/config/bundled-skills/reminder/SKILL.md +20 -0
  143. package/src/config/bundled-skills/reminder/TOOLS.json +67 -0
  144. package/src/config/bundled-skills/reminder/tools/reminder-cancel.ts +9 -0
  145. package/src/config/bundled-skills/reminder/tools/reminder-create.ts +9 -0
  146. package/src/config/bundled-skills/reminder/tools/reminder-list.ts +9 -0
  147. package/src/config/bundled-skills/schedule/SKILL.md +74 -0
  148. package/src/config/bundled-skills/schedule/TOOLS.json +135 -0
  149. package/src/config/bundled-skills/schedule/tools/schedule-create.ts +9 -0
  150. package/src/config/bundled-skills/schedule/tools/schedule-delete.ts +9 -0
  151. package/src/config/bundled-skills/schedule/tools/schedule-list.ts +9 -0
  152. package/src/config/bundled-skills/schedule/tools/schedule-update.ts +9 -0
  153. package/src/config/bundled-skills/subagent/SKILL.md +25 -0
  154. package/src/config/bundled-skills/subagent/TOOLS.json +107 -0
  155. package/src/config/bundled-skills/subagent/tools/subagent-abort.ts +9 -0
  156. package/src/config/bundled-skills/subagent/tools/subagent-message.ts +9 -0
  157. package/src/config/bundled-skills/subagent/tools/subagent-read.ts +9 -0
  158. package/src/config/bundled-skills/subagent/tools/subagent-spawn.ts +9 -0
  159. package/src/config/bundled-skills/subagent/tools/subagent-status.ts +9 -0
  160. package/src/config/bundled-skills/tasks/SKILL.md +28 -0
  161. package/src/config/bundled-skills/tasks/TOOLS.json +256 -0
  162. package/src/config/bundled-skills/tasks/tools/task-delete.ts +9 -0
  163. package/src/config/bundled-skills/tasks/tools/task-list-add.ts +9 -0
  164. package/src/config/bundled-skills/tasks/tools/task-list-remove.ts +9 -0
  165. package/src/config/bundled-skills/tasks/tools/task-list-show.ts +9 -0
  166. package/src/config/bundled-skills/tasks/tools/task-list-update.ts +9 -0
  167. package/src/config/bundled-skills/tasks/tools/task-list.ts +9 -0
  168. package/src/config/bundled-skills/tasks/tools/task-run.ts +9 -0
  169. package/src/config/bundled-skills/tasks/tools/task-save.ts +9 -0
  170. package/src/config/bundled-skills/twitter/SKILL.md +134 -0
  171. package/src/config/bundled-skills/watcher/SKILL.md +27 -0
  172. package/src/config/bundled-skills/watcher/TOOLS.json +147 -0
  173. package/src/config/bundled-skills/watcher/tools/watcher-create.ts +9 -0
  174. package/src/config/bundled-skills/watcher/tools/watcher-delete.ts +9 -0
  175. package/src/config/bundled-skills/watcher/tools/watcher-digest.ts +9 -0
  176. package/src/config/bundled-skills/watcher/tools/watcher-list.ts +9 -0
  177. package/src/config/bundled-skills/watcher/tools/watcher-update.ts +9 -0
  178. package/src/config/defaults.ts +44 -0
  179. package/src/config/loader.ts +4 -1
  180. package/src/config/schema.ts +218 -1
  181. package/src/config/system-prompt.ts +100 -6
  182. package/src/config/templates/IDENTITY.md +7 -0
  183. package/src/config/types.ts +5 -0
  184. package/src/contacts/contact-store.ts +4 -4
  185. package/src/daemon/assistant-attachments.ts +10 -0
  186. package/src/daemon/classifier.ts +3 -1
  187. package/src/daemon/computer-use-session.ts +3 -1
  188. package/src/daemon/date-context.ts +136 -0
  189. package/src/daemon/handlers/apps.ts +16 -1
  190. package/src/daemon/handlers/browser.ts +54 -0
  191. package/src/daemon/handlers/computer-use.ts +7 -1
  192. package/src/daemon/handlers/config.ts +192 -4
  193. package/src/daemon/handlers/diagnostics.ts +5 -1
  194. package/src/daemon/handlers/documents.ts +18 -29
  195. package/src/daemon/handlers/home-base.ts +5 -1
  196. package/src/daemon/handlers/index.ts +40 -271
  197. package/src/daemon/handlers/misc.ts +9 -1
  198. package/src/daemon/handlers/publish.ts +6 -1
  199. package/src/daemon/handlers/sessions.ts +65 -12
  200. package/src/daemon/handlers/shared.ts +36 -1
  201. package/src/daemon/handlers/signing.ts +37 -0
  202. package/src/daemon/handlers/skills.ts +20 -6
  203. package/src/daemon/handlers/subagents.ts +8 -3
  204. package/src/daemon/handlers/twitter-auth.ts +169 -0
  205. package/src/daemon/handlers/work-items.ts +495 -39
  206. package/src/daemon/ipc-contract-inventory.json +40 -4
  207. package/src/daemon/ipc-contract.ts +185 -37
  208. package/src/daemon/ipc-protocol.ts +7 -2
  209. package/src/daemon/lifecycle.ts +48 -5
  210. package/src/daemon/main.ts +10 -4
  211. package/src/daemon/ride-shotgun-handler.ts +74 -10
  212. package/src/daemon/server.ts +144 -29
  213. package/src/daemon/session-agent-loop.ts +887 -0
  214. package/src/daemon/session-attachments.ts +28 -5
  215. package/src/daemon/session-error.ts +24 -3
  216. package/src/daemon/session-lifecycle.ts +147 -0
  217. package/src/daemon/session-media-retry.ts +147 -0
  218. package/src/daemon/session-messaging.ts +145 -0
  219. package/src/daemon/session-notifiers.ts +164 -0
  220. package/src/daemon/session-process.ts +2 -2
  221. package/src/daemon/session-queue-manager.ts +1 -0
  222. package/src/daemon/session-runtime-assembly.ts +52 -0
  223. package/src/daemon/session-skill-tools.ts +124 -5
  224. package/src/daemon/session-slash.ts +3 -0
  225. package/src/daemon/session-surfaces.ts +77 -2
  226. package/src/daemon/session-tool-setup.ts +222 -2
  227. package/src/daemon/session-usage.ts +0 -2
  228. package/src/daemon/session.ts +114 -1365
  229. package/src/daemon/video-thumbnail.ts +60 -0
  230. package/src/doordash/client.ts +121 -27
  231. package/src/doordash/queries.ts +1 -2
  232. package/src/export/formatter.ts +3 -1
  233. package/src/followups/followup-store.ts +4 -2
  234. package/src/followups/types.ts +6 -0
  235. package/src/hooks/templates.ts +1 -1
  236. package/src/index.ts +32 -1151
  237. package/src/media/gemini-image-service.ts +1 -1
  238. package/src/memory/attachments-store.ts +28 -83
  239. package/src/memory/channel-delivery-store.ts +7 -21
  240. package/src/memory/clarification-resolver.ts +6 -5
  241. package/src/memory/contradiction-checker.ts +3 -2
  242. package/src/memory/conversation-key-store.ts +10 -29
  243. package/src/memory/conversation-store.ts +2 -1
  244. package/src/memory/db.ts +362 -2
  245. package/src/memory/entity-extractor.ts +6 -3
  246. package/src/memory/items-extractor.ts +5 -4
  247. package/src/memory/jobs-store.ts +3 -2
  248. package/src/memory/llm-usage-store.ts +1 -2
  249. package/src/memory/runs-store.ts +1 -2
  250. package/src/memory/schema.ts +65 -2
  251. package/src/messaging/style-analyzer.ts +3 -2
  252. package/src/messaging/thread-summarizer.ts +8 -12
  253. package/src/messaging/triage-engine.ts +4 -2
  254. package/src/providers/openrouter/client.ts +20 -0
  255. package/src/providers/registry.ts +8 -0
  256. package/src/runtime/http-server.ts +277 -25
  257. package/src/runtime/http-types.ts +0 -2
  258. package/src/runtime/routes/attachment-routes.ts +5 -6
  259. package/src/runtime/routes/call-routes.ts +140 -0
  260. package/src/runtime/routes/channel-routes.ts +12 -19
  261. package/src/runtime/routes/conversation-routes.ts +5 -9
  262. package/src/runtime/routes/run-routes.ts +4 -8
  263. package/src/runtime/run-orchestrator.ts +39 -6
  264. package/src/schedule/recurrence-engine.ts +138 -0
  265. package/src/schedule/recurrence-types.ts +67 -0
  266. package/src/schedule/schedule-store.ts +102 -57
  267. package/src/schedule/scheduler.ts +9 -6
  268. package/src/security/oauth2.ts +29 -4
  269. package/src/security/secret-allowlist.ts +46 -0
  270. package/src/skills/clawhub.ts +1 -1
  271. package/src/subagent/manager.ts +40 -8
  272. package/src/swarm/backend-claude-code.ts +64 -9
  273. package/src/swarm/worker-prompts.ts +2 -1
  274. package/src/tasks/SPEC.md +34 -28
  275. package/src/tasks/ephemeral-permissions.ts +16 -7
  276. package/src/tasks/task-compiler.ts +5 -4
  277. package/src/tasks/task-runner.ts +10 -5
  278. package/src/tasks/task-scheduler.ts +1 -1
  279. package/src/tasks/tool-sanitizer.ts +36 -0
  280. package/src/tools/assets/search.ts +4 -4
  281. package/src/tools/browser/api-map.ts +220 -0
  282. package/src/tools/browser/auto-navigate.ts +270 -0
  283. package/src/tools/browser/browser-execution.ts +2 -1
  284. package/src/tools/browser/browser-manager.ts +2 -2
  285. package/src/tools/browser/network-recorder.ts +5 -4
  286. package/src/tools/browser/x-auto-navigate.ts +207 -0
  287. package/src/tools/calls/call-end.ts +67 -0
  288. package/src/tools/calls/call-start.ts +73 -0
  289. package/src/tools/calls/call-status.ts +81 -0
  290. package/src/tools/claude-code/claude-code.ts +77 -11
  291. package/src/tools/contacts/contact-merge.ts +46 -78
  292. package/src/tools/contacts/contact-search.ts +35 -79
  293. package/src/tools/contacts/contact-upsert.ts +35 -108
  294. package/src/tools/credentials/vault.ts +21 -5
  295. package/src/tools/document/document-tool.ts +71 -144
  296. package/src/tools/executor.ts +129 -10
  297. package/src/tools/followups/followup_create.ts +46 -88
  298. package/src/tools/followups/followup_list.ts +34 -74
  299. package/src/tools/followups/followup_resolve.ts +31 -66
  300. package/src/tools/host-terminal/cli-discover.ts +2 -1
  301. package/src/tools/host-terminal/host-shell.ts +10 -0
  302. package/src/tools/memory/handlers.ts +5 -4
  303. package/src/tools/network/__tests__/web-search.test.ts +427 -0
  304. package/src/tools/network/script-proxy/__tests__/logging.test.ts +248 -0
  305. package/src/tools/network/script-proxy/__tests__/policy.test.ts +234 -0
  306. package/src/tools/network/script-proxy/__tests__/router.test.ts +76 -0
  307. package/src/tools/network/web-fetch.ts +18 -6
  308. package/src/tools/playbooks/index.ts +4 -5
  309. package/src/tools/playbooks/playbook-create.ts +3 -47
  310. package/src/tools/playbooks/playbook-delete.ts +1 -25
  311. package/src/tools/playbooks/playbook-list.ts +1 -28
  312. package/src/tools/playbooks/playbook-update.ts +3 -51
  313. package/src/tools/registry.ts +2 -4
  314. package/src/tools/reminder/reminder.ts +5 -78
  315. package/src/tools/schedule/create.ts +69 -74
  316. package/src/tools/schedule/delete.ts +21 -47
  317. package/src/tools/schedule/list.ts +55 -74
  318. package/src/tools/schedule/update.ts +77 -84
  319. package/src/tools/subagent/abort.ts +29 -58
  320. package/src/tools/subagent/message.ts +30 -63
  321. package/src/tools/subagent/read.ts +53 -84
  322. package/src/tools/subagent/spawn.ts +43 -82
  323. package/src/tools/subagent/status.ts +42 -71
  324. package/src/tools/swarm/delegate.ts +2 -1
  325. package/src/tools/tasks/index.ts +8 -6
  326. package/src/tools/tasks/task-delete.ts +69 -56
  327. package/src/tools/tasks/task-list.ts +31 -52
  328. package/src/tools/tasks/task-run.ts +74 -102
  329. package/src/tools/tasks/task-save.ts +33 -65
  330. package/src/tools/tasks/work-item-enqueue.ts +192 -134
  331. package/src/tools/tasks/work-item-list.ts +33 -78
  332. package/src/tools/tasks/work-item-remove.ts +60 -0
  333. package/src/tools/tasks/work-item-update.ts +114 -0
  334. package/src/tools/terminal/backends/native.ts +3 -1
  335. package/src/tools/tool-manifest.ts +20 -74
  336. package/src/tools/types.ts +6 -0
  337. package/src/tools/ui-surface/definitions.ts +6 -1
  338. package/src/tools/watch/screen-watch.ts +3 -1
  339. package/src/tools/watcher/create.ts +52 -98
  340. package/src/tools/watcher/delete.ts +20 -46
  341. package/src/tools/watcher/digest.ts +36 -70
  342. package/src/tools/watcher/list.ts +49 -79
  343. package/src/tools/watcher/update.ts +45 -91
  344. package/src/twitter/client.ts +690 -0
  345. package/src/twitter/session.ts +91 -0
  346. package/src/usage/types.ts +0 -1
  347. package/src/util/truncate.ts +6 -0
  348. package/src/watcher/providers/slack.ts +2 -1
  349. package/src/watcher/watcher-store.ts +3 -2
  350. package/src/work-items/work-item-store.ts +236 -2
  351. package/src/workspace/commit-message-enrichment-service.ts +284 -0
  352. package/src/workspace/commit-message-provider.ts +95 -0
  353. package/src/workspace/git-service.ts +272 -52
  354. package/src/workspace/heartbeat-service.ts +70 -13
  355. package/src/workspace/provider-commit-message-generator.ts +242 -0
  356. package/src/workspace/turn-commit.ts +100 -51
  357. package/src/tools/contacts/index.ts +0 -4
  358. package/src/tools/document/index.ts +0 -5
  359. package/src/tools/followups/index.ts +0 -3
  360. package/src/tools/subagent/index.ts +0 -5
  361. /package/src/__tests__/{memory-context-benchmark.test.ts → memory-context-benchmark.benchmark.test.ts} +0 -0
package/src/index.ts CHANGED
@@ -2,1180 +2,61 @@
2
2
 
3
3
  import { Command } from 'commander';
4
4
  import { createRequire } from 'node:module';
5
- import * as net from 'node:net';
6
5
  import { dirname, join } from 'node:path';
6
+ import { spawn } from 'node:child_process';
7
7
 
8
8
  const require = createRequire(import.meta.url);
9
9
  const { version } = require('../package.json') as { version: string };
10
+
10
11
  import {
11
- ensureDaemonRunning,
12
- startDaemon,
13
- stopDaemon,
14
- getDaemonStatus,
15
- } from './daemon/lifecycle.js';
16
- import { existsSync, statSync, readFileSync } from 'node:fs';
17
- import { execSync, spawn } from 'node:child_process';
18
- import { startCli } from './cli.js';
19
- import { getSocketPath, readSessionToken, getRootDir, getDataDir, getDbPath, getLogPath, getWorkspaceDir, getWorkspaceSkillsDir, getWorkspaceHooksDir } from './util/platform.js';
20
- import {
21
- serialize,
22
- createMessageParser,
23
- type ClientMessage,
24
- type ServerMessage,
25
- } from './daemon/ipc-protocol.js';
26
- import { IpcError } from './util/errors.js';
27
- import { getCliLogger } from './util/logger.js';
28
- import { timeAgo } from './util/time.js';
29
- import { shouldAutoStartDaemon, hasSocketOverride } from './daemon/connection-policy.js';
30
- import {
31
- loadRawConfig,
32
- saveRawConfig,
33
- getNestedValue,
34
- setNestedValue,
35
- getConfig,
36
- API_KEY_PROVIDERS,
37
- } from './config/loader.js';
38
- import {
39
- getAllRules,
40
- removeRule,
41
- clearAllRules,
42
- } from './permissions/trust-store.js';
43
- import { getSecureKey, setSecureKey, deleteSecureKey } from './security/secure-keys.js';
44
- import { getRecentInvocations } from './memory/tool-usage-store.js';
45
- import {
46
- getConversation,
47
- getMessages,
48
- listConversations,
49
- clearAll as clearAllConversations,
50
- } from './memory/conversation-store.js';
51
- import { initializeDb } from './memory/db.js';
52
- import { initQdrantClient } from './memory/qdrant-client.js';
53
- import { formatMarkdown, formatJson } from './export/formatter.js';
12
+ registerDefaultAction,
13
+ registerDaemonCommand,
14
+ registerDevCommand,
15
+ registerSessionsCommand,
16
+ registerAuditCommand,
17
+ registerDoctorCommand,
18
+ registerCompletionsCommand,
19
+ } from './cli/core-commands.js';
54
20
  import {
55
- getMemorySystemStatus,
56
- queryMemory,
57
- requestMemoryBackfill,
58
- requestMemoryCleanup,
59
- requestMemoryRebuildIndex,
60
- } from './memory/admin.js';
21
+ registerConfigCommand,
22
+ registerKeysCommand,
23
+ registerTrustCommand,
24
+ registerMemoryCommand,
25
+ } from './cli/config-commands.js';
61
26
  import { registerHooksCommand } from './hooks/cli.js';
62
27
  import { registerEmailCommand } from './cli/email.js';
63
28
  import { registerContactsCommand } from './cli/contacts.js';
64
29
  import { registerAutonomyCommand } from './cli/autonomy.js';
65
30
  import { registerDoordashCommand } from './cli/doordash.js';
66
-
67
- function sendOneMessage(
68
- msg: ClientMessage,
69
- ): Promise<ServerMessage> {
70
- return new Promise((resolve, reject) => {
71
- const socket = net.createConnection(getSocketPath());
72
- const parser = createMessageParser();
73
- let resolved = false;
74
- let authenticated = false;
75
-
76
- socket.on('connect', () => {
77
- // Authenticate first — the daemon requires a valid session token
78
- // before it will accept any other messages.
79
- const token = readSessionToken();
80
- if (!token) {
81
- resolved = true;
82
- reject(new IpcError('Session token not found — is the daemon running?'));
83
- socket.destroy();
84
- return;
85
- }
86
- socket.write(serialize({ type: 'auth', token }));
87
- });
88
-
89
- socket.on('data', (data) => {
90
- const messages = parser.feed(data.toString()) as ServerMessage[];
91
- for (const m of messages) {
92
- // Handle auth handshake
93
- if (!authenticated) {
94
- if (m.type === 'auth_result') {
95
- if ((m as { success: boolean }).success) {
96
- authenticated = true;
97
- // Now send the actual message
98
- socket.write(serialize(msg));
99
- } else {
100
- resolved = true;
101
- reject(new IpcError((m as { message?: string }).message ?? 'Authentication failed'));
102
- socket.destroy();
103
- }
104
- }
105
- continue;
106
- }
107
-
108
- // Skip push messages that aren't responses to our request
109
- if (m.type === 'daemon_status') {
110
- continue;
111
- }
112
- // On auto-auth sockets the server may send a second auth_result
113
- // in response to the client's auth message after we're already
114
- // authenticated — ignore it so it doesn't resolve as the response.
115
- if (m.type === 'auth_result') {
116
- continue;
117
- }
118
- if (m.type === 'session_info' && msg.type !== 'session_create') {
119
- continue;
120
- }
121
- resolved = true;
122
- socket.end();
123
- resolve(m);
124
- return;
125
- }
126
- });
127
-
128
- socket.on('error', (err) => {
129
- if (!resolved) reject(err);
130
- });
131
-
132
- socket.on('close', () => {
133
- if (!resolved) {
134
- reject(new IpcError('Socket closed before receiving a response'));
135
- }
136
- });
137
- });
138
- }
139
-
140
- const log = getCliLogger('cli');
31
+ import { registerTwitterCommand } from './cli/twitter.js';
32
+ import { registerMapCommand } from './cli/map.js';
141
33
 
142
34
  const program = new Command();
143
35
 
144
36
  program
145
37
  .name('vellum')
146
38
  .description('Local AI assistant')
147
- .version(version)
148
- .action(async () => {
149
- if (shouldAutoStartDaemon()) {
150
- await ensureDaemonRunning();
151
- }
152
- await startCli();
153
- });
154
-
155
- const daemon = program.command('daemon').description('Manage the daemon process');
156
-
157
- daemon
158
- .command('start')
159
- .description('Start the daemon')
160
- .action(async () => {
161
- const result = await startDaemon();
162
- if (result.alreadyRunning) {
163
- log.info(`Daemon already running (pid ${result.pid})`);
164
- } else {
165
- log.info(`Daemon started (pid ${result.pid})`);
166
- }
167
- });
168
-
169
- daemon
170
- .command('stop')
171
- .description('Stop the daemon')
172
- .action(async () => {
173
- const result = await stopDaemon();
174
- if (result.stopped) {
175
- log.info('Daemon stopped');
176
- } else if (result.reason === 'stop_failed') {
177
- log.error('Failed to stop daemon — process survived SIGKILL');
178
- process.exit(1);
179
- } else {
180
- log.info('Daemon is not running');
181
- }
182
- });
183
-
184
- daemon
185
- .command('restart')
186
- .description('Restart the daemon')
187
- .action(async () => {
188
- const stopResult = await stopDaemon();
189
- if (stopResult.stopped) {
190
- log.info('Daemon stopped');
191
- } else if (stopResult.reason === 'stop_failed') {
192
- log.error('Failed to stop daemon — process survived SIGKILL, cannot restart');
193
- process.exit(1);
194
- }
195
- const startResult = await startDaemon();
196
- log.info(`Daemon started (pid ${startResult.pid})`);
197
- });
198
-
199
- daemon
200
- .command('status')
201
- .description('Show daemon status')
202
- .action(() => {
203
- const status = getDaemonStatus();
204
- if (status.running) {
205
- log.info(`Daemon is running (pid ${status.pid})`);
206
- } else {
207
- log.info('Daemon is not running');
208
- }
209
- log.info(`Socket path: ${getSocketPath()}${hasSocketOverride() ? ' (override)' : ''}`);
210
- log.info(`Autostart: ${shouldAutoStartDaemon() ? 'enabled' : 'disabled'}`);
211
- });
212
-
213
- // --- Dev command ---
214
- program
215
- .command('dev')
216
- .description('Run the daemon in dev mode with auto-restart on file changes')
217
- .action(async () => {
218
- // Stop any existing daemon first
219
- const status = getDaemonStatus();
220
- if (status.running) {
221
- log.info('Stopping existing daemon...');
222
- const stopResult = await stopDaemon();
223
- if (!stopResult.stopped && stopResult.reason === 'stop_failed') {
224
- log.error('Failed to stop existing daemon — process survived SIGKILL');
225
- process.exit(1);
226
- }
227
- }
228
-
229
- const mainPath = `${import.meta.dirname}/daemon/main.ts`;
230
-
231
- log.info('Starting daemon in dev mode (Ctrl+C to stop)');
232
-
233
- const child = spawn('bun', ['--watch', 'run', mainPath], {
234
- stdio: 'inherit',
235
- env: {
236
- ...process.env,
237
- VELLUM_LOG_STDERR: '1',
238
- VELLUM_DEBUG: '1',
239
- },
240
- });
241
-
242
- const forward = (signal: NodeJS.Signals) => {
243
- child.kill(signal);
244
- };
245
- process.on('SIGINT', () => forward('SIGINT'));
246
- process.on('SIGTERM', () => forward('SIGTERM'));
247
-
248
- child.on('exit', (code) => {
249
- process.exit(code ?? 0);
250
- });
251
- });
252
-
253
- const sessions = program.command('sessions').description('Manage sessions');
254
-
255
- sessions
256
- .command('list')
257
- .description('List all sessions')
258
- .action(async () => {
259
- if (shouldAutoStartDaemon()) await ensureDaemonRunning();
260
- const response = await sendOneMessage({ type: 'session_list' });
261
- if (response.type === 'session_list_response') {
262
- if (response.sessions.length === 0) {
263
- log.info('No sessions');
264
- } else {
265
- for (const s of response.sessions) {
266
- log.info(` ${s.id} ${s.title} ${timeAgo(s.updatedAt)}`);
267
- }
268
- }
269
- } else if (response.type === 'error') {
270
- log.error(`Error: ${response.message}`);
271
- }
272
- });
273
-
274
- sessions
275
- .command('new [title]')
276
- .description('Create a new session')
277
- .action(async (title?: string) => {
278
- if (shouldAutoStartDaemon()) await ensureDaemonRunning();
279
- const response = await sendOneMessage({
280
- type: 'session_create',
281
- title,
282
- });
283
- if (response.type === 'session_info') {
284
- log.info(`Created session: ${response.title} (${response.sessionId})`);
285
- } else if (response.type === 'error') {
286
- log.error(`Error: ${response.message}`);
287
- }
288
- });
289
-
290
- sessions
291
- .command('export [sessionId]')
292
- .description('Export a conversation as markdown or JSON')
293
- .option('-f, --format <format>', 'Output format: md or json', 'md')
294
- .option('-o, --output <file>', 'Write to file instead of stdout')
295
- .action(async (sessionId?: string, opts?: { format: string; output?: string }) => {
296
- initializeDb();
297
- const format = opts?.format ?? 'md';
298
- if (format !== 'md' && format !== 'json') {
299
- log.error('Error: format must be "md" or "json"');
300
- process.exit(1);
301
- }
302
-
303
- // If no session ID given, pick the most recent one
304
- let id = sessionId;
305
- if (!id) {
306
- const all = listConversations(1);
307
- if (all.length === 0) {
308
- log.error('No sessions found');
309
- process.exit(1);
310
- }
311
- id = all[0].id;
312
- }
313
-
314
- // Support prefix matching for session IDs
315
- let conversation = getConversation(id);
316
- if (!conversation) {
317
- const all = listConversations(Number.MAX_SAFE_INTEGER);
318
- const match = all.find((c) => c.id.startsWith(id!));
319
- if (match) {
320
- conversation = match;
321
- } else {
322
- log.error(`Session not found: ${id}`);
323
- process.exit(1);
324
- }
325
- }
326
-
327
- const msgs = getMessages(conversation.id);
328
- const exportData = {
329
- ...conversation,
330
- messages: msgs.map((m) => ({
331
- role: m.role,
332
- content: JSON.parse(m.content),
333
- createdAt: m.createdAt,
334
- })),
335
- };
336
-
337
- const output = format === 'json'
338
- ? formatJson(exportData)
339
- : formatMarkdown(exportData);
340
-
341
- if (opts?.output) {
342
- const { writeFileSync } = await import('node:fs');
343
- writeFileSync(opts.output, output);
344
- log.info(`Exported to ${opts.output}`);
345
- } else {
346
- process.stdout.write(output);
347
- }
348
- });
349
-
350
- sessions
351
- .command('clear')
352
- .description('Clear all conversations, messages, and vector data (dev only)')
353
- .action(async () => {
354
- log.info('This will permanently delete all conversations, messages, and vector data.');
355
-
356
- const readline = await import('node:readline');
357
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
358
- const answer = await new Promise<string>((resolve) => {
359
- rl.question('Are you sure? (y/N) ', resolve);
360
- });
361
- rl.close();
362
- if (answer.toLowerCase() !== 'y') {
363
- log.info('Cancelled');
364
- return;
365
- }
366
-
367
- initializeDb();
368
- const result = clearAllConversations();
369
- log.info(`Cleared ${result.conversations} conversations, ${result.messages} messages`);
370
-
371
- // Notify a running daemon to drop its in-memory sessions so it
372
- // doesn't keep serving stale history from deleted conversation rows.
373
- try {
374
- await sendOneMessage({ type: 'sessions_clear' });
375
- } catch {
376
- // Daemon may not be running — that's fine, no sessions to invalidate.
377
- }
378
-
379
- const config = getConfig();
380
- const qdrantUrl = process.env.QDRANT_URL?.trim() || config.memory.qdrant.url;
381
- const qdrant = initQdrantClient({
382
- url: qdrantUrl,
383
- collection: config.memory.qdrant.collection,
384
- vectorSize: config.memory.qdrant.vectorSize,
385
- onDisk: config.memory.qdrant.onDisk,
386
- quantization: config.memory.qdrant.quantization,
387
- });
388
- const deleted = await qdrant.deleteCollection();
389
- if (deleted) {
390
- log.info(`Deleted Qdrant collection "${config.memory.qdrant.collection}"`);
391
- } else {
392
- log.info('Qdrant collection not found or not reachable (skipped)');
393
- }
394
-
395
- log.info('Done.');
396
- });
397
-
398
- // --- Config commands ---
399
- const config = program.command('config').description('Manage configuration');
400
-
401
- config
402
- .command('set <key> <value>')
403
- .description('Set a config value (supports dotted paths like apiKeys.anthropic)')
404
- .action((key: string, value: string) => {
405
- const raw = loadRawConfig();
406
- // Try to parse as JSON for booleans/numbers, fall back to string
407
- let parsed: unknown = value;
408
- try {
409
- parsed = JSON.parse(value);
410
- } catch {
411
- // keep as string
412
- }
413
- setNestedValue(raw, key, parsed);
414
- saveRawConfig(raw);
415
- log.info(`Set ${key} = ${JSON.stringify(parsed)}`);
416
- });
417
-
418
- config
419
- .command('get <key>')
420
- .description('Get a config value (supports dotted paths)')
421
- .action((key: string) => {
422
- const raw = loadRawConfig();
423
- const value = getNestedValue(raw, key);
424
- if (value === undefined) {
425
- log.info(`(not set)`);
426
- } else {
427
- log.info(typeof value === 'object' ? JSON.stringify(value, null, 2) : String(value));
428
- }
429
- });
430
-
431
- config
432
- .command('list')
433
- .description('List all config values')
434
- .action(() => {
435
- const raw = loadRawConfig();
436
- if (Object.keys(raw).length === 0) {
437
- log.info('No configuration set');
438
- } else {
439
- log.info(JSON.stringify(raw, null, 2));
440
- }
441
- });
442
-
443
- // --- Keys commands ---
444
- const keys = program.command('keys').description('Manage API keys in secure storage');
445
-
446
- keys
447
- .command('list')
448
- .description('List all stored API key names')
449
- .action(() => {
450
- const stored: string[] = [];
451
- for (const provider of API_KEY_PROVIDERS) {
452
- const value = getSecureKey(provider);
453
- if (value) stored.push(provider);
454
- }
455
- if (stored.length === 0) {
456
- log.info('No API keys stored');
457
- } else {
458
- for (const name of stored) {
459
- log.info(` ${name}`);
460
- }
461
- }
462
- });
463
-
464
- keys
465
- .command('set <provider> <key>')
466
- .description('Store an API key (e.g. vellum keys set anthropic sk-ant-...)')
467
- .action((provider: string, key: string) => {
468
- if (setSecureKey(provider, key)) {
469
- log.info(`Stored API key for "${provider}"`);
470
- } else {
471
- log.error(`Failed to store API key for "${provider}"`);
472
- process.exit(1);
473
- }
474
- });
475
-
476
- keys
477
- .command('delete <provider>')
478
- .description('Delete a stored API key')
479
- .action((provider: string) => {
480
- if (deleteSecureKey(provider)) {
481
- log.info(`Deleted API key for "${provider}"`);
482
- } else {
483
- log.error(`No API key found for "${provider}"`);
484
- process.exit(1);
485
- }
486
- });
487
-
488
- // --- Trust commands ---
489
- const trust = program.command('trust').description('Manage trust rules');
490
-
491
- trust
492
- .command('list')
493
- .description('List all trust rules')
494
- .action(() => {
495
- const rules = getAllRules();
496
- if (rules.length === 0) {
497
- log.info('No trust rules');
498
- return;
499
- }
500
- // Table header
501
- const idW = 8;
502
- const toolW = 12;
503
- const patternW = 30;
504
- const scopeW = 20;
505
- const decW = 6;
506
- const priW = 4;
507
- log.info(
508
- 'ID'.padEnd(idW) +
509
- 'Tool'.padEnd(toolW) +
510
- 'Pattern'.padEnd(patternW) +
511
- 'Scope'.padEnd(scopeW) +
512
- 'Dcn'.padEnd(decW) +
513
- 'Pri'.padEnd(priW) +
514
- 'Created',
515
- );
516
- log.info('-'.repeat(idW + toolW + patternW + scopeW + decW + priW + 20));
517
- for (const r of rules) {
518
- const id = r.id.slice(0, 8);
519
- const created = new Date(r.createdAt).toISOString().slice(0, 10);
520
- log.info(
521
- id.padEnd(idW) +
522
- r.tool.padEnd(toolW) +
523
- r.pattern.slice(0, patternW - 2).padEnd(patternW) +
524
- r.scope.slice(0, scopeW - 2).padEnd(scopeW) +
525
- r.decision.slice(0, decW - 1).padEnd(decW) +
526
- String(r.priority).padEnd(priW) +
527
- created,
528
- );
529
- }
530
- });
531
-
532
- trust
533
- .command('remove <id>')
534
- .description('Remove a trust rule by ID (or prefix)')
535
- .action((id: string) => {
536
- // Support prefix matching
537
- const rules = getAllRules();
538
- const match = rules.find((r) => r.id.startsWith(id));
539
- if (!match) {
540
- log.error(`No rule found matching "${id}"`);
541
- process.exit(1);
542
- }
543
- try {
544
- removeRule(match.id);
545
- } catch (err) {
546
- log.error(err instanceof Error ? err.message : String(err));
547
- process.exit(1);
548
- }
549
- log.info(`Removed rule ${match.id.slice(0, 8)} (${match.tool}: ${match.pattern})`);
550
- });
551
-
552
- trust
553
- .command('clear')
554
- .description('Remove all trust rules')
555
- .action(async () => {
556
- const rules = getAllRules();
557
- if (rules.length === 0) {
558
- log.info('No trust rules to clear');
559
- return;
560
- }
561
- // Confirmation prompt
562
- const readline = await import('node:readline');
563
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
564
- const answer = await new Promise<string>((resolve) => {
565
- rl.question(`Remove all ${rules.length} trust rules? (y/N) `, resolve);
566
- });
567
- rl.close();
568
- if (answer.toLowerCase() === 'y') {
569
- clearAllRules();
570
- log.info(`Cleared ${rules.length} trust rules`);
571
- } else {
572
- log.info('Cancelled');
573
- }
574
- });
575
-
576
- // --- Memory commands ---
577
- const memory = program.command('memory').description('Manage long-term memory indexing/retrieval');
578
-
579
- memory
580
- .command('status')
581
- .description('Show memory subsystem status')
582
- .action(() => {
583
- initializeDb();
584
- const status = getMemorySystemStatus();
585
- log.info(`Memory enabled: ${status.enabled ? 'yes' : 'no'}`);
586
- log.info(`Memory degraded: ${status.degraded ? 'yes' : 'no'}`);
587
- if (status.reason) log.info(`Reason: ${status.reason}`);
588
- if (status.provider && status.model) {
589
- log.info(`Embedding backend: ${status.provider}/${status.model}`);
590
- } else {
591
- log.info('Embedding backend: none');
592
- }
593
- log.info(`Segments: ${status.counts.segments.toLocaleString()}`);
594
- log.info(`Items: ${status.counts.items.toLocaleString()}`);
595
- log.info(`Summaries: ${status.counts.summaries.toLocaleString()}`);
596
- log.info(`Embeddings: ${status.counts.embeddings.toLocaleString()}`);
597
- log.info(`Pending conflicts: ${status.conflicts.pending.toLocaleString()}`);
598
- log.info(`Resolved conflicts: ${status.conflicts.resolved.toLocaleString()}`);
599
- if (status.conflicts.oldestPendingAgeMs !== null) {
600
- const oldestMinutes = Math.floor(status.conflicts.oldestPendingAgeMs / 60_000);
601
- log.info(`Oldest pending conflict age: ${oldestMinutes} min`);
602
- } else {
603
- log.info('Oldest pending conflict age: n/a');
604
- }
605
- log.info(`Cleanup backlog (resolved conflicts): ${status.cleanup.resolvedBacklog.toLocaleString()}`);
606
- log.info(`Cleanup backlog (superseded items): ${status.cleanup.supersededBacklog.toLocaleString()}`);
607
- log.info(`Cleanup throughput 24h (resolved conflicts): ${status.cleanup.resolvedCompleted24h.toLocaleString()}`);
608
- log.info(`Cleanup throughput 24h (superseded items): ${status.cleanup.supersededCompleted24h.toLocaleString()}`);
609
- log.info('Jobs:');
610
- for (const [key, value] of Object.entries(status.jobs)) {
611
- log.info(` ${key}: ${value}`);
612
- }
613
- });
614
-
615
- memory
616
- .command('backfill')
617
- .description('Queue a memory backfill job')
618
- .option('-f, --force', 'Restart backfill from the beginning')
619
- .action((opts: { force?: boolean }) => {
620
- initializeDb();
621
- const jobId = requestMemoryBackfill(Boolean(opts?.force));
622
- log.info(`Queued backfill job: ${jobId}`);
623
- });
624
-
625
- memory
626
- .command('cleanup')
627
- .description('Queue cleanup jobs for resolved conflicts and stale superseded items')
628
- .option('--retention-ms <ms>', 'Optional retention threshold in milliseconds')
629
- .action((opts: { retentionMs?: string }) => {
630
- initializeDb();
631
- const retentionMs = opts.retentionMs ? Number.parseInt(opts.retentionMs, 10) : undefined;
632
- const jobs = requestMemoryCleanup(Number.isFinite(retentionMs) ? retentionMs : undefined);
633
- log.info(`Queued cleanup_resolved_conflicts job: ${jobs.resolvedConflictsJobId}`);
634
- log.info(`Queued cleanup_stale_superseded_items job: ${jobs.staleSupersededItemsJobId}`);
635
- });
636
-
637
- memory
638
- .command('query <text>')
639
- .description('Run a memory recall query and print the injected memory payload')
640
- .option('-s, --session <id>', 'Optional conversation/session ID')
641
- .action(async (text: string, opts?: { session?: string }) => {
642
- initializeDb();
643
- let sessionId = opts?.session;
644
- if (!sessionId) {
645
- const latest = listConversations(1)[0];
646
- sessionId = latest?.id ?? '';
647
- }
648
- const result = await queryMemory(text, sessionId ?? '');
649
- if (result.degraded) {
650
- log.info(`Memory degraded: ${result.reason ?? 'unknown reason'}`);
651
- }
652
- log.info(`Lexical hits: ${result.lexicalHits}`);
653
- log.info(`Semantic hits: ${result.semanticHits}`);
654
- log.info(`Recency hits: ${result.recencyHits}`);
655
- log.info(`Entity hits: ${result.entityHits}`);
656
- log.info(`Injected tokens: ${result.injectedTokens}`);
657
- log.info(`Latency: ${result.latencyMs}ms`);
658
- if (result.injectedText.length > 0) {
659
- log.info('');
660
- log.info(result.injectedText);
661
- } else {
662
- log.info('No memory injected.');
663
- }
664
- });
665
-
666
- memory
667
- .command('rebuild-index')
668
- .description('Queue a memory FTS+embedding index rebuild job')
669
- .action(() => {
670
- initializeDb();
671
- const jobId = requestMemoryRebuildIndex();
672
- log.info(`Queued rebuild-index job: ${jobId}`);
673
- });
674
-
675
- // --- Audit command ---
676
- program
677
- .command('audit')
678
- .description('Show recent tool invocations')
679
- .option('-l, --limit <n>', 'Number of entries to show', '20')
680
- .action((opts: { limit: string }) => {
681
- const limit = parseInt(opts.limit, 10) || 20;
682
- const rows = getRecentInvocations(limit);
683
- if (rows.length === 0) {
684
- log.info('No tool invocations recorded');
685
- return;
686
- }
687
- const tsW = 20;
688
- const toolW = 14;
689
- const inputW = 30;
690
- const decW = 8;
691
- const riskW = 8;
692
- const durW = 8;
693
- log.info(
694
- 'Timestamp'.padEnd(tsW) +
695
- 'Tool'.padEnd(toolW) +
696
- 'Input'.padEnd(inputW) +
697
- 'Decision'.padEnd(decW) +
698
- 'Risk'.padEnd(riskW) +
699
- 'Duration',
700
- );
701
- log.info('-'.repeat(tsW + toolW + inputW + decW + riskW + durW));
702
- for (const r of rows) {
703
- const ts = new Date(r.createdAt).toISOString().slice(0, 19).replace('T', ' ');
704
- // Summarize input: take first meaningful chunk
705
- let inputSummary = '';
706
- try {
707
- const parsed = JSON.parse(r.input);
708
- if (parsed.command) inputSummary = parsed.command;
709
- else if (parsed.path) inputSummary = parsed.path;
710
- else inputSummary = r.input;
711
- } catch {
712
- inputSummary = r.input;
713
- }
714
- if (inputSummary.length > inputW - 2) {
715
- inputSummary = inputSummary.slice(0, inputW - 4) + '..';
716
- }
717
- const dur = r.durationMs < 1000 ? `${r.durationMs}ms` : `${(r.durationMs / 1000).toFixed(1)}s`;
718
- log.info(
719
- ts.padEnd(tsW) +
720
- r.toolName.padEnd(toolW) +
721
- inputSummary.padEnd(inputW) +
722
- r.decision.padEnd(decW) +
723
- r.riskLevel.padEnd(riskW) +
724
- dur,
725
- );
726
- }
727
- });
728
-
729
- // --- Doctor command ---
730
- program
731
- .command('doctor')
732
- .description('Run diagnostic checks')
733
- .action(async () => {
734
- const pass = (label: string) => log.info(` \u2713 ${label}`);
735
- const fail = (label: string, detail?: string) =>
736
- log.info(` \u2717 ${label}${detail ? ` — ${detail}` : ''}`);
737
-
738
- log.info('Vellum Doctor\n');
739
-
740
- // 0. Connection policy info
741
- const socketPath = getSocketPath();
742
- const isOverride = hasSocketOverride();
743
- const autostart = shouldAutoStartDaemon();
744
- log.info(` Socket: ${socketPath}${isOverride ? ' (override via VELLUM_DAEMON_SOCKET)' : ''}`);
745
- log.info(` Autostart: ${autostart ? 'enabled' : 'disabled'}\n`);
746
-
747
- // 1. Bun installed
748
- try {
749
- execSync('bun --version', { stdio: 'pipe' });
750
- pass('Bun is installed');
751
- } catch {
752
- fail('Bun is installed', 'bun not found in PATH');
753
- }
754
-
755
- // 2. Provider/API key configured
756
- const raw = loadRawConfig();
757
- const provider = typeof raw.provider === 'string' ? raw.provider : 'anthropic';
758
- const providerEnvVar: Record<string, string> = {
759
- anthropic: 'ANTHROPIC_API_KEY',
760
- openai: 'OPENAI_API_KEY',
761
- gemini: 'GEMINI_API_KEY',
762
- ollama: 'OLLAMA_API_KEY',
763
- fireworks: 'FIREWORKS_API_KEY',
764
- };
765
- const configKey = (raw.apiKeys as Record<string, string> | undefined)?.[provider];
766
- const envVar = providerEnvVar[provider];
767
- const envKey = envVar ? process.env[envVar] : undefined;
768
-
769
- if (provider === 'ollama') {
770
- pass('Provider configured (Ollama; API key optional)');
771
- } else if (envKey || configKey) {
772
- pass('API key configured');
773
- } else {
774
- fail(
775
- 'API key configured',
776
- envVar
777
- ? `set ${envVar} or run: vellum config set apiKeys.${provider} <key>`
778
- : `set API key for provider "${provider}"`,
779
- );
780
- }
781
-
782
- // 3. Daemon reachable
783
- try {
784
- const sock = getSocketPath();
785
- if (!existsSync(sock)) {
786
- fail('Daemon reachable', 'socket not found (is the daemon running?)');
787
- } else {
788
- await new Promise<void>((resolve, reject) => {
789
- const s = net.createConnection(sock);
790
- const timer = setTimeout(() => { s.destroy(); reject(new IpcError('timeout')); }, 2000);
791
- s.on('connect', () => { clearTimeout(timer); s.end(); resolve(); });
792
- s.on('error', (err) => { clearTimeout(timer); reject(err); });
793
- });
794
- pass('Daemon reachable');
795
- }
796
- } catch {
797
- fail('Daemon reachable', 'could not connect to daemon socket');
798
- }
799
-
800
- // 4. DB exists and readable
801
- const dbPath = getDbPath();
802
- if (existsSync(dbPath)) {
803
- try {
804
- const { Database } = await import('bun:sqlite');
805
- const db = new Database(dbPath, { readonly: true });
806
- db.query('SELECT 1').get();
807
- db.close();
808
- pass('Database exists and readable');
809
- } catch {
810
- fail('Database exists and readable', 'file exists but cannot be read');
811
- }
812
- } else {
813
- fail('Database exists and readable', `not found at ${dbPath}`);
814
- }
815
-
816
- // 5. ~/.vellum/ directory structure (workspace layout)
817
- const rootDir = getRootDir();
818
- const dataDir = getDataDir();
819
- const workspaceDir = getWorkspaceDir();
820
- const requiredDirs = [rootDir, workspaceDir, dataDir, `${dataDir}/db`, `${dataDir}/logs`, getWorkspaceSkillsDir(), getWorkspaceHooksDir(), `${rootDir}/protected`];
821
- const missing = requiredDirs.filter((d) => !existsSync(d));
822
- if (missing.length === 0) {
823
- pass('Directory structure exists');
824
- } else {
825
- fail('Directory structure exists', `missing: ${missing.join(', ')}`);
826
- }
827
-
828
- // 6. Disk space
829
- try {
830
- const output = execSync(`df -k "${rootDir}"`, { stdio: 'pipe', encoding: 'utf-8' });
831
- const lines = output.trim().split('\n');
832
- if (lines.length >= 2) {
833
- const cols = lines[1].trim().split(/\s+/);
834
- // df -k output: Filesystem 1K-blocks Used Available ...
835
- const availKB = parseInt(cols[3], 10);
836
- if (isNaN(availKB)) {
837
- fail('Disk space', 'could not parse available space');
838
- } else if (availKB < 100 * 1024) {
839
- fail('Disk space', `only ${Math.round(availKB / 1024)}MB free (< 100MB)`);
840
- } else {
841
- pass(`Disk space (${Math.round(availKB / 1024)}MB free)`);
842
- }
843
- } else {
844
- fail('Disk space', 'unexpected df output');
845
- }
846
- } catch {
847
- fail('Disk space', 'could not check disk space');
848
- }
849
-
850
- // 7. Log file size
851
- const logPath = getLogPath();
852
- if (existsSync(logPath)) {
853
- try {
854
- const logStat = statSync(logPath);
855
- const logSizeMB = logStat.size / (1024 * 1024);
856
- if (logSizeMB > 50) {
857
- fail('Log file size', `${logSizeMB.toFixed(1)}MB (> 50MB)`);
858
- } else {
859
- pass(`Log file size (${logSizeMB.toFixed(1)}MB)`);
860
- }
861
- } catch {
862
- fail('Log file size', 'could not stat log file');
863
- }
864
- } else {
865
- pass('Log file size (no log file yet)');
866
- }
867
-
868
- // 8. DB integrity check
869
- if (existsSync(dbPath)) {
870
- try {
871
- const { Database } = await import('bun:sqlite');
872
- const db = new Database(dbPath, { readonly: true });
873
- const result = db.query('PRAGMA integrity_check').get() as { integrity_check: string } | null;
874
- db.close();
875
- if (result?.integrity_check === 'ok') {
876
- pass('Database integrity check');
877
- } else {
878
- fail('Database integrity check', result?.integrity_check ?? 'unknown result');
879
- }
880
- } catch (err) {
881
- fail('Database integrity check', err instanceof Error ? err.message : 'unknown error');
882
- }
883
- } else {
884
- fail('Database integrity check', 'database file not found');
885
- }
886
-
887
- // 9. Socket permissions
888
- const sockPath = getSocketPath();
889
- if (existsSync(sockPath)) {
890
- try {
891
- const sockStat = statSync(sockPath);
892
- const mode = sockStat.mode & 0o777;
893
- if (mode === 0o600 || mode === 0o700) {
894
- pass(`Socket permissions (${mode.toString(8).padStart(4, '0')})`);
895
- } else {
896
- fail('Socket permissions', `expected 0600 or 0700, got 0${mode.toString(8)}`);
897
- }
898
- } catch {
899
- fail('Socket permissions', 'could not stat socket');
900
- }
901
- } else {
902
- pass('Socket permissions (socket not present — daemon not running)');
903
- }
904
-
905
- // 10. Trust rule syntax
906
- const trustPath = `${rootDir}/protected/trust.json`;
907
- if (existsSync(trustPath)) {
908
- try {
909
- const raw = readFileSync(trustPath, 'utf-8');
910
- const data = JSON.parse(raw);
911
- if (typeof data !== 'object' || data === null) {
912
- fail('Trust rule syntax', 'trust.json is not a JSON object');
913
- } else if (typeof data.version !== 'number') {
914
- fail('Trust rule syntax', 'missing or invalid "version" field');
915
- } else if (!Array.isArray(data.rules)) {
916
- fail('Trust rule syntax', 'missing or invalid "rules" array');
917
- } else {
918
- const invalid = data.rules.filter(
919
- (r: unknown) =>
920
- typeof r !== 'object' || r === null ||
921
- typeof (r as Record<string, unknown>).tool !== 'string' ||
922
- typeof (r as Record<string, unknown>).pattern !== 'string' ||
923
- typeof (r as Record<string, unknown>).scope !== 'string',
924
- );
925
- if (invalid.length > 0) {
926
- fail('Trust rule syntax', `${invalid.length} rule(s) have invalid structure`);
927
- } else {
928
- pass(`Trust rule syntax (${data.rules.length} rule(s))`);
929
- }
930
- }
931
- } catch (err) {
932
- fail('Trust rule syntax', err instanceof Error ? err.message : 'could not parse');
933
- }
934
- } else {
935
- pass('Trust rule syntax (no trust.json yet)');
936
- }
937
-
938
- // 11. WASM files
939
- const wasmFiles = [
940
- 'node_modules/web-tree-sitter/web-tree-sitter.wasm',
941
- 'node_modules/tree-sitter-bash/tree-sitter-bash.wasm',
942
- ];
943
- let wasmOk = true;
944
- const missingWasm: string[] = [];
945
- for (const wasm of wasmFiles) {
946
- // Resolve relative to the assistant package directory
947
- const fullPath = `${import.meta.dirname}/../${wasm}`;
948
- if (!existsSync(fullPath)) {
949
- missingWasm.push(wasm);
950
- wasmOk = false;
951
- } else {
952
- try {
953
- const wasmStat = statSync(fullPath);
954
- if (wasmStat.size === 0) {
955
- missingWasm.push(`${wasm} (empty)`);
956
- wasmOk = false;
957
- }
958
- } catch {
959
- missingWasm.push(`${wasm} (unreadable)`);
960
- wasmOk = false;
961
- }
962
- }
963
- }
964
- if (wasmOk) {
965
- pass('WASM files present and non-empty');
966
- } else {
967
- fail('WASM files', missingWasm.join(', '));
968
- }
969
-
970
- // 12. Browser runtime (Playwright + Chromium)
971
- const { checkBrowserRuntime } = await import('./tools/browser/runtime-check.js');
972
- const browserStatus = await checkBrowserRuntime();
973
- if (browserStatus.playwrightAvailable && browserStatus.chromiumInstalled) {
974
- pass('Browser runtime (Playwright + Chromium)');
975
- } else if (!browserStatus.playwrightAvailable) {
976
- fail('Browser runtime', 'playwright not available');
977
- } else {
978
- fail('Browser runtime', browserStatus.error ?? 'Chromium not installed');
979
- }
980
-
981
- // 13. Sandbox backend diagnostics
982
- const { runSandboxDiagnostics } = await import('./tools/terminal/sandbox-diagnostics.js');
983
- const sandbox = runSandboxDiagnostics();
984
- log.info(`\n Sandbox: ${sandbox.config.enabled ? 'enabled' : 'disabled'}`);
985
- log.info(` Backend: ${sandbox.config.backend}`);
986
- log.info(` Reason: ${sandbox.activeBackendReason}`);
987
- if (sandbox.config.backend === 'docker') {
988
- log.info(` Image: ${sandbox.config.dockerImage}`);
989
- }
990
- log.info('');
991
- for (const check of sandbox.checks) {
992
- if (check.ok) {
993
- pass(check.label);
994
- } else {
995
- fail(check.label, check.detail);
996
- }
997
- }
998
- });
999
-
1000
- // --- Hooks commands ---
39
+ .version(version);
40
+
41
+ registerDefaultAction(program);
42
+ registerDaemonCommand(program);
43
+ registerDevCommand(program);
44
+ registerSessionsCommand(program);
45
+ registerConfigCommand(program);
46
+ registerKeysCommand(program);
47
+ registerTrustCommand(program);
48
+ registerMemoryCommand(program);
49
+ registerAuditCommand(program);
50
+ registerDoctorCommand(program);
1001
51
  registerHooksCommand(program);
1002
-
1003
- // --- Email commands ---
1004
52
  registerEmailCommand(program);
1005
-
1006
- // --- Contacts commands ---
1007
53
  registerContactsCommand(program);
1008
-
1009
- // --- Autonomy commands ---
1010
54
  registerAutonomyCommand(program);
1011
-
1012
- // --- DoorDash commands ---
1013
55
  registerDoordashCommand(program);
56
+ registerCompletionsCommand(program);
1014
57
 
1015
- // --- Completions command ---
1016
- program
1017
- .command('completions')
1018
- .argument('<shell>', 'Shell type: bash, zsh, or fish')
1019
- .description('Generate shell completion script (e.g. vellum completions bash >> ~/.bashrc)')
1020
- .action((shell: string) => {
1021
- const subcommands: Record<string, string[]> = {
1022
- daemon: ['start', 'stop', 'restart', 'status'],
1023
- sessions: ['list', 'new', 'export', 'clear'],
1024
- config: ['set', 'get', 'list'],
1025
- keys: ['list', 'set', 'delete'],
1026
- trust: ['list', 'remove', 'clear'],
1027
- memory: ['status', 'backfill', 'query', 'rebuild-index'],
1028
- hooks: ['list', 'enable', 'disable', 'install', 'remove'],
1029
- contacts: ['list', 'get', 'merge'],
1030
- autonomy: ['get', 'set'],
1031
- };
1032
- const topLevel = [
1033
- 'daemon', 'dev', 'sessions', 'config', 'keys', 'trust', 'memory',
1034
- 'hooks', 'contacts', 'autonomy', 'audit', 'doctor', 'completions', 'help',
1035
- ];
1036
-
1037
- switch (shell) {
1038
- case 'bash':
1039
- process.stdout.write(generateBashCompletion(topLevel, subcommands));
1040
- break;
1041
- case 'zsh':
1042
- process.stdout.write(generateZshCompletion(topLevel, subcommands));
1043
- break;
1044
- case 'fish':
1045
- process.stdout.write(generateFishCompletion(topLevel, subcommands));
1046
- break;
1047
- default:
1048
- log.error(`Unknown shell: ${shell}. Supported shells: bash, zsh, fish`);
1049
- process.exit(1);
1050
- }
1051
- });
1052
-
1053
- function generateBashCompletion(
1054
- topLevel: string[],
1055
- subcommands: Record<string, string[]>,
1056
- ): string {
1057
- const subcmdCases = Object.entries(subcommands)
1058
- .map(([cmd, subs]) => ` ${cmd}) COMPREPLY=( $(compgen -W "${subs.join(' ')}" -- "$cur") ) ;;`)
1059
- .join('\n');
1060
-
1061
- return `# vellum bash completion
1062
- # Add to ~/.bashrc: eval "$(vellum completions bash)"
1063
- _vellum_completions() {
1064
- local cur prev words cword
1065
- _init_completion || return
1066
-
1067
- if [[ $cword -eq 1 ]]; then
1068
- COMPREPLY=( $(compgen -W "${topLevel.join(' ')} --help --version" -- "$cur") )
1069
- return
1070
- fi
1071
-
1072
- case "\${words[1]}" in
1073
- ${subcmdCases}
1074
- audit) COMPREPLY=( $(compgen -W "--limit -l" -- "$cur") ) ;;
1075
- completions) COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ) ;;
1076
- esac
1077
- }
1078
- complete -F _vellum_completions vellum
1079
- `;
1080
- }
1081
-
1082
- function generateZshCompletion(
1083
- topLevel: string[],
1084
- subcommands: Record<string, string[]>,
1085
- ): string {
1086
- const subcmdCases = Object.entries(subcommands)
1087
- .map(([cmd, subs]) => ` ${cmd}) compadd ${subs.join(' ')} ;;`)
1088
- .join('\n');
1089
-
1090
- return `#compdef vellum
1091
- # vellum zsh completion
1092
- # Add to ~/.zshrc: eval "$(vellum completions zsh)"
1093
- _vellum() {
1094
- local -a commands
1095
- commands=(
1096
- 'daemon:Manage the daemon process'
1097
- 'dev:Run daemon in dev mode with auto-restart'
1098
- 'sessions:Manage sessions'
1099
- 'config:Manage configuration'
1100
- 'keys:Manage API keys in secure storage'
1101
- 'trust:Manage trust rules'
1102
- 'memory:Manage long-term memory'
1103
- 'hooks:Manage hooks'
1104
- 'contacts:Manage the contact graph'
1105
- 'autonomy:View and configure autonomy tiers'
1106
- 'audit:Show recent tool invocations'
1107
- 'doctor:Run diagnostic checks'
1108
- 'completions:Generate shell completion script'
1109
- 'help:Display help'
1110
- )
1111
-
1112
- if (( CURRENT == 2 )); then
1113
- _describe 'command' commands
1114
- _arguments '--help[Show help]' '--version[Show version]'
1115
- return
1116
- fi
1117
-
1118
- case "\${words[2]}" in
1119
- ${subcmdCases}
1120
- audit) _arguments '-l[Number of entries]' '--limit[Number of entries]' ;;
1121
- completions) compadd bash zsh fish ;;
1122
- esac
1123
- }
1124
- compdef _vellum vellum
1125
- `;
1126
- }
1127
-
1128
- function generateFishCompletion(
1129
- topLevel: string[],
1130
- subcommands: Record<string, string[]>,
1131
- ): string {
1132
- let script = `# vellum fish completion
1133
- # Add to ~/.config/fish/completions/vellum.fish or eval: vellum completions fish | source
1134
- `;
1135
-
1136
- // Disable file completions
1137
- script += `complete -c vellum -f\n`;
1138
-
1139
- // Top-level commands
1140
- const descriptions: Record<string, string> = {
1141
- daemon: 'Manage the daemon process',
1142
- dev: 'Run daemon in dev mode with auto-restart',
1143
- sessions: 'Manage sessions',
1144
- config: 'Manage configuration',
1145
- keys: 'Manage API keys in secure storage',
1146
- trust: 'Manage trust rules',
1147
- memory: 'Manage long-term memory',
1148
- hooks: 'Manage hooks',
1149
- contacts: 'Manage the contact graph',
1150
- autonomy: 'View and configure autonomy tiers',
1151
- audit: 'Show recent tool invocations',
1152
- doctor: 'Run diagnostic checks',
1153
- completions: 'Generate shell completion script',
1154
- help: 'Display help',
1155
- };
1156
-
1157
- for (const cmd of topLevel) {
1158
- const desc = descriptions[cmd] ?? '';
1159
- script += `complete -c vellum -n '__fish_use_subcommand' -a '${cmd}' -d '${desc}'\n`;
1160
- }
1161
- script += `complete -c vellum -n '__fish_use_subcommand' -l help -d 'Show help'\n`;
1162
- script += `complete -c vellum -n '__fish_use_subcommand' -l version -d 'Show version'\n`;
1163
-
1164
- // Subcommands
1165
- for (const [cmd, subs] of Object.entries(subcommands)) {
1166
- for (const sub of subs) {
1167
- script += `complete -c vellum -n '__fish_seen_subcommand_from ${cmd}' -a '${sub}'\n`;
1168
- }
1169
- }
1170
-
1171
- // Audit options
1172
- script += `complete -c vellum -n '__fish_seen_subcommand_from audit' -s l -l limit -d 'Number of entries'\n`;
1173
-
1174
- // Completions shell argument
1175
- script += `complete -c vellum -n '__fish_seen_subcommand_from completions' -a 'bash zsh fish'\n`;
1176
-
1177
- return script;
1178
- }
58
+ registerTwitterCommand(program);
59
+ registerMapCommand(program);
1179
60
 
1180
61
  const knownCommands = new Set(program.commands.map(cmd => cmd.name()));
1181
62
  const firstArg = process.argv[2];