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,575 @@
1
+ /**
2
+ * CLI command group: `vellum twitter`
3
+ *
4
+ * Post tweets and manage Twitter sessions via the command line.
5
+ * All commands output JSON to stdout. Use --json for machine-readable output.
6
+ */
7
+
8
+ import * as net from 'node:net';
9
+ import { Command } from 'commander';
10
+ import {
11
+ loadSession,
12
+ importFromRecording,
13
+ clearSession,
14
+ } from '../twitter/session.js';
15
+ import {
16
+ postTweet,
17
+ getUserByScreenName,
18
+ getUserTweets,
19
+ getTweetDetail,
20
+ searchTweets,
21
+ getBookmarks,
22
+ getHomeTimeline,
23
+ getNotifications,
24
+ getLikes,
25
+ getFollowers,
26
+ getFollowing,
27
+ getUserMedia,
28
+ SessionExpiredError,
29
+ } from '../twitter/client.js';
30
+ import { getSocketPath, readSessionToken } from '../util/platform.js';
31
+ import {
32
+ serialize,
33
+ createMessageParser,
34
+ } from '../daemon/ipc-protocol.js';
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Helpers
38
+ // ---------------------------------------------------------------------------
39
+
40
+ function output(data: unknown, json: boolean): void {
41
+ process.stdout.write(
42
+ json ? JSON.stringify(data) + '\n' : JSON.stringify(data, null, 2) + '\n',
43
+ );
44
+ }
45
+
46
+ function outputError(message: string, code = 1): void {
47
+ output({ ok: false, error: message }, true);
48
+ process.exitCode = code;
49
+ }
50
+
51
+ function getJson(cmd: Command): boolean {
52
+ let c: Command | null = cmd;
53
+ while (c) {
54
+ if ((c.opts() as { json?: boolean }).json) return true;
55
+ c = c.parent;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ const SESSION_EXPIRED_MSG =
61
+ 'Your Twitter session has expired. Please sign in to Twitter in Chrome — ' +
62
+ 'run `vellum twitter refresh` to capture your session automatically.';
63
+
64
+ async function run(cmd: Command, fn: () => Promise<unknown>): Promise<void> {
65
+ try {
66
+ const result = await fn();
67
+ output(
68
+ { ok: true, ...(result as Record<string, unknown>) },
69
+ getJson(cmd),
70
+ );
71
+ } catch (err) {
72
+ if (err instanceof SessionExpiredError) {
73
+ output(
74
+ { ok: false, error: 'session_expired', message: SESSION_EXPIRED_MSG },
75
+ getJson(cmd),
76
+ );
77
+ process.exitCode = 1;
78
+ return;
79
+ }
80
+ outputError(err instanceof Error ? err.message : String(err));
81
+ }
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Command registration
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export function registerTwitterCommand(program: Command): void {
89
+ const tw = program
90
+ .command('x')
91
+ .alias('twitter')
92
+ .description(
93
+ 'Post on X and manage sessions. Requires a session imported from a Ride Shotgun recording.',
94
+ )
95
+ .option('--json', 'Machine-readable JSON output');
96
+
97
+ // =========================================================================
98
+ // login — import session from a recording
99
+ // =========================================================================
100
+ tw.command('login')
101
+ .description('Import a Twitter session from a Ride Shotgun recording')
102
+ .requiredOption(
103
+ '--recording <path>',
104
+ 'Path to the recording JSON file',
105
+ )
106
+ .action(async (opts: { recording: string }, cmd: Command) => {
107
+ await run(cmd, async () => {
108
+ const session = importFromRecording(opts.recording);
109
+ return {
110
+ message: 'Session imported successfully',
111
+ cookieCount: session.cookies.length,
112
+ recordingId: session.recordingId,
113
+ };
114
+ });
115
+ });
116
+
117
+ // =========================================================================
118
+ // logout — clear saved session
119
+ // =========================================================================
120
+ tw.command('logout')
121
+ .description('Clear the saved Twitter session')
122
+ .action((_opts: unknown, cmd: Command) => {
123
+ clearSession();
124
+ output({ ok: true, message: 'Session cleared' }, getJson(cmd));
125
+ });
126
+
127
+ // =========================================================================
128
+ // refresh — start Ride Shotgun learn to capture fresh cookies
129
+ // =========================================================================
130
+ tw.command('refresh')
131
+ .description(
132
+ 'Start a Ride Shotgun learn session to capture fresh Twitter cookies. ' +
133
+ 'Opens x.com in Chrome — sign in when prompted. ' +
134
+ 'NOTE: Chrome will restart with debugging enabled; your tabs will be restored.',
135
+ )
136
+ .option('--duration <seconds>', 'Recording duration in seconds', '180')
137
+ .action(async (opts: { duration: string }, cmd: Command) => {
138
+ const json = getJson(cmd);
139
+ const duration = parseInt(opts.duration, 10);
140
+
141
+ try {
142
+ const result = await startLearnSession(duration);
143
+ if (result.recordingPath) {
144
+ const session = importFromRecording(result.recordingPath);
145
+
146
+ // Hide Chrome after capturing session
147
+ try { await minimizeChromeWindow(); } catch { /* best-effort */ }
148
+
149
+ output(
150
+ {
151
+ ok: true,
152
+ message: 'Session refreshed successfully',
153
+ cookieCount: session.cookies.length,
154
+ recordingId: result.recordingId,
155
+ },
156
+ json,
157
+ );
158
+ } else {
159
+ output(
160
+ {
161
+ ok: false,
162
+ error: 'Recording completed but no recording path returned',
163
+ recordingId: result.recordingId,
164
+ },
165
+ json,
166
+ );
167
+ process.exitCode = 1;
168
+ }
169
+ } catch (err) {
170
+ outputError(err instanceof Error ? err.message : String(err));
171
+ }
172
+ });
173
+
174
+ // =========================================================================
175
+ // status — check session status
176
+ // =========================================================================
177
+ tw.command('status')
178
+ .description('Check if a Twitter session is active')
179
+ .action((_opts: unknown, cmd: Command) => {
180
+ const session = loadSession();
181
+ if (session) {
182
+ output(
183
+ {
184
+ ok: true,
185
+ loggedIn: true,
186
+ cookieCount: session.cookies.length,
187
+ importedAt: session.importedAt,
188
+ recordingId: session.recordingId,
189
+ },
190
+ getJson(cmd),
191
+ );
192
+ } else {
193
+ output({ ok: true, loggedIn: false }, getJson(cmd));
194
+ }
195
+ });
196
+
197
+ // =========================================================================
198
+ // post — post a tweet
199
+ // =========================================================================
200
+ tw.command('post')
201
+ .description('Post a tweet')
202
+ .argument('<text>', 'Tweet text')
203
+ .action(async (text: string, _opts: unknown, cmd: Command) => {
204
+ await run(cmd, async () => {
205
+ const result = await postTweet(text);
206
+ return {
207
+ tweetId: result.tweetId,
208
+ text: result.text,
209
+ url: result.url,
210
+ };
211
+ });
212
+ });
213
+
214
+ // =========================================================================
215
+ // reply — reply to a tweet
216
+ // =========================================================================
217
+ tw.command('reply')
218
+ .description('Reply to a tweet')
219
+ .argument('<tweetUrl>', 'Tweet URL or tweet ID')
220
+ .argument('<text>', 'Reply text')
221
+ .action(async (tweetUrl: string, text: string, _opts: unknown, cmd: Command) => {
222
+ await run(cmd, async () => {
223
+ // Extract tweet ID: either a bare numeric ID or the last numeric segment of a URL
224
+ const idMatch = tweetUrl.match(/(\d+)\s*$/);
225
+ if (!idMatch) {
226
+ throw new Error(`Could not extract tweet ID from: ${tweetUrl}`);
227
+ }
228
+ const inReplyToTweetId = idMatch[1];
229
+ const result = await postTweet(text, { inReplyToTweetId });
230
+ return {
231
+ tweetId: result.tweetId,
232
+ text: result.text,
233
+ url: result.url,
234
+ inReplyToTweetId,
235
+ };
236
+ });
237
+ });
238
+ // =========================================================================
239
+ // timeline — fetch a user's recent tweets
240
+ // =========================================================================
241
+ tw.command('timeline')
242
+ .description("Fetch a user's recent tweets")
243
+ .argument('<screenName>', 'Twitter screen name (without @)')
244
+ .option('--count <n>', 'Number of tweets to fetch', '20')
245
+ .action(async (screenName: string, opts: { count: string }, cmd: Command) => {
246
+ await run(cmd, async () => {
247
+ const user = await getUserByScreenName(screenName.replace(/^@/, ''));
248
+ const tweets = await getUserTweets(user.userId, parseInt(opts.count, 10));
249
+ return { user, tweets };
250
+ });
251
+ });
252
+
253
+ // =========================================================================
254
+ // tweet — fetch a single tweet and its replies
255
+ // =========================================================================
256
+ tw.command('tweet')
257
+ .description('Fetch a tweet and its reply thread')
258
+ .argument('<tweetIdOrUrl>', 'Tweet ID or URL')
259
+ .action(async (tweetIdOrUrl: string, _opts: unknown, cmd: Command) => {
260
+ await run(cmd, async () => {
261
+ const idMatch = tweetIdOrUrl.match(/(\d+)\s*$/);
262
+ if (!idMatch) throw new Error(`Could not extract tweet ID from: ${tweetIdOrUrl}`);
263
+ const tweets = await getTweetDetail(idMatch[1]);
264
+ return { tweets };
265
+ });
266
+ });
267
+
268
+ // =========================================================================
269
+ // search — search tweets
270
+ // =========================================================================
271
+ tw.command('search')
272
+ .description('Search tweets')
273
+ .argument('<query>', 'Search query')
274
+ .option('--product <type>', 'Top, Latest, People, or Media', 'Top')
275
+ .action(async (query: string, opts: { product: string }, cmd: Command) => {
276
+ await run(cmd, async () => {
277
+ const tweets = await searchTweets(
278
+ query,
279
+ opts.product as 'Top' | 'Latest' | 'People' | 'Media',
280
+ );
281
+ return { query, tweets };
282
+ });
283
+ });
284
+
285
+ // =========================================================================
286
+ // bookmarks — fetch bookmarks
287
+ // =========================================================================
288
+ tw.command('bookmarks')
289
+ .description('Fetch your bookmarks')
290
+ .option('--count <n>', 'Number of bookmarks', '20')
291
+ .action(async (opts: { count: string }, cmd: Command) => {
292
+ await run(cmd, async () => {
293
+ const tweets = await getBookmarks(parseInt(opts.count, 10));
294
+ return { tweets };
295
+ });
296
+ });
297
+
298
+ // =========================================================================
299
+ // home — fetch home timeline
300
+ // =========================================================================
301
+ tw.command('home')
302
+ .description('Fetch your home timeline')
303
+ .option('--count <n>', 'Number of tweets', '20')
304
+ .action(async (opts: { count: string }, cmd: Command) => {
305
+ await run(cmd, async () => {
306
+ const tweets = await getHomeTimeline(parseInt(opts.count, 10));
307
+ return { tweets };
308
+ });
309
+ });
310
+
311
+ // =========================================================================
312
+ // notifications — fetch notifications
313
+ // =========================================================================
314
+ tw.command('notifications')
315
+ .description('Fetch your notifications')
316
+ .option('--count <n>', 'Number of notifications', '20')
317
+ .action(async (opts: { count: string }, cmd: Command) => {
318
+ await run(cmd, async () => {
319
+ const notifications = await getNotifications(parseInt(opts.count, 10));
320
+ return { notifications };
321
+ });
322
+ });
323
+
324
+ // =========================================================================
325
+ // likes — fetch a user's liked tweets
326
+ // =========================================================================
327
+ tw.command('likes')
328
+ .description("Fetch a user's liked tweets")
329
+ .argument('<screenName>', 'Twitter screen name (without @)')
330
+ .option('--count <n>', 'Number of likes', '20')
331
+ .action(async (screenName: string, opts: { count: string }, cmd: Command) => {
332
+ await run(cmd, async () => {
333
+ const user = await getUserByScreenName(screenName.replace(/^@/, ''));
334
+ const tweets = await getLikes(user.userId, parseInt(opts.count, 10));
335
+ return { user, tweets };
336
+ });
337
+ });
338
+
339
+ // =========================================================================
340
+ // followers — fetch a user's followers
341
+ // =========================================================================
342
+ tw.command('followers')
343
+ .description("Fetch a user's followers")
344
+ .argument('<screenName>', 'Twitter screen name (without @)')
345
+ .action(async (screenName: string, _opts: unknown, cmd: Command) => {
346
+ await run(cmd, async () => {
347
+ const cleanName = screenName.replace(/^@/, '');
348
+ const user = await getUserByScreenName(cleanName);
349
+ const followers = await getFollowers(user.userId, cleanName);
350
+ return { user, followers };
351
+ });
352
+ });
353
+
354
+ // =========================================================================
355
+ // following — fetch who a user follows
356
+ // =========================================================================
357
+ tw.command('following')
358
+ .description("Fetch who a user follows")
359
+ .argument('<screenName>', 'Twitter screen name (without @)')
360
+ .option('--count <n>', 'Number of following', '20')
361
+ .action(async (screenName: string, opts: { count: string }, cmd: Command) => {
362
+ await run(cmd, async () => {
363
+ const user = await getUserByScreenName(screenName.replace(/^@/, ''));
364
+ const following = await getFollowing(user.userId, parseInt(opts.count, 10));
365
+ return { user, following };
366
+ });
367
+ });
368
+
369
+ // =========================================================================
370
+ // media — fetch a user's media tweets
371
+ // =========================================================================
372
+ tw.command('media')
373
+ .description("Fetch a user's media tweets")
374
+ .argument('<screenName>', 'Twitter screen name (without @)')
375
+ .option('--count <n>', 'Number of media tweets', '20')
376
+ .action(async (screenName: string, opts: { count: string }, cmd: Command) => {
377
+ await run(cmd, async () => {
378
+ const user = await getUserByScreenName(screenName.replace(/^@/, ''));
379
+ const tweets = await getUserMedia(user.userId, parseInt(opts.count, 10));
380
+ return { user, tweets };
381
+ });
382
+ });
383
+ }
384
+
385
+ // ---------------------------------------------------------------------------
386
+ // Chrome CDP restart helper
387
+ // ---------------------------------------------------------------------------
388
+
389
+ import { spawn as spawnChild } from 'node:child_process';
390
+ import { homedir } from 'node:os';
391
+ import { join as pathJoin } from 'node:path';
392
+
393
+ const CDP_BASE = 'http://localhost:9222';
394
+ const CHROME_DATA_DIR = pathJoin(
395
+ homedir(),
396
+ 'Library/Application Support/Google/Chrome-CDP',
397
+ );
398
+
399
+ async function isCdpReady(): Promise<boolean> {
400
+ try {
401
+ const res = await fetch(`${CDP_BASE}/json/version`);
402
+ return res.ok;
403
+ } catch {
404
+ return false;
405
+ }
406
+ }
407
+
408
+ async function ensureChromeWithCDP(): Promise<void> {
409
+ // Already running with CDP?
410
+ if (await isCdpReady()) return;
411
+
412
+ // Launch a separate Chrome instance with CDP flags alongside any existing Chrome.
413
+ // Using a dedicated --user-data-dir allows coexistence without killing the user's browser.
414
+ const chromeApp =
415
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome';
416
+ spawnChild(chromeApp, [
417
+ `--remote-debugging-port=9222`,
418
+ `--force-renderer-accessibility`,
419
+ `--user-data-dir=${CHROME_DATA_DIR}`,
420
+ 'https://x.com/login',
421
+ ], {
422
+ detached: true,
423
+ stdio: 'ignore',
424
+ }).unref();
425
+
426
+ // Wait for CDP to be ready
427
+ for (let i = 0; i < 30; i++) {
428
+ await new Promise(r => setTimeout(r, 500));
429
+ if (await isCdpReady()) return;
430
+ }
431
+ throw new Error('Chrome started but CDP endpoint not responding after 15s');
432
+ }
433
+
434
+ async function minimizeChromeWindow(): Promise<void> {
435
+ const res = await fetch(`${CDP_BASE}/json/list`);
436
+ const targets = (await res.json()) as Array<{ type: string; webSocketDebuggerUrl: string }>;
437
+ const pageTarget = targets.find(t => t.type === 'page');
438
+ if (!pageTarget) return;
439
+
440
+ const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
441
+
442
+ await new Promise<void>((resolve, reject) => {
443
+ const timeout = setTimeout(() => {
444
+ ws.close();
445
+ reject(new Error('CDP minimize timed out'));
446
+ }, 5000);
447
+
448
+ ws.addEventListener('open', () => {
449
+ ws.send(JSON.stringify({ id: 1, method: 'Browser.getWindowForTarget' }));
450
+ });
451
+
452
+ ws.addEventListener('message', (event) => {
453
+ const msg = JSON.parse(String(event.data)) as { id: number; result?: { windowId: number } };
454
+ if (msg.id === 1 && msg.result) {
455
+ ws.send(JSON.stringify({
456
+ id: 2,
457
+ method: 'Browser.setWindowBounds',
458
+ params: { windowId: msg.result.windowId, bounds: { windowState: 'minimized' } },
459
+ }));
460
+ } else if (msg.id === 2) {
461
+ clearTimeout(timeout);
462
+ ws.close();
463
+ resolve();
464
+ }
465
+ });
466
+
467
+ ws.addEventListener('error', (err) => {
468
+ clearTimeout(timeout);
469
+ reject(err);
470
+ });
471
+ });
472
+ }
473
+
474
+ // ---------------------------------------------------------------------------
475
+ // Ride Shotgun learn session helper
476
+ // ---------------------------------------------------------------------------
477
+
478
+ interface LearnResult {
479
+ recordingId?: string;
480
+ recordingPath?: string;
481
+ }
482
+
483
+ async function navigateToX(): Promise<void> {
484
+ try {
485
+ const res = await fetch(`${CDP_BASE}/json/list`);
486
+ if (!res.ok) return;
487
+ const targets = (await res.json()) as Array<{ id: string; type: string; url: string }>;
488
+ const tab = targets.find(t => t.type === 'page');
489
+ if (!tab) return;
490
+ await fetch(`${CDP_BASE}/json/navigate?url=${encodeURIComponent('https://x.com/login')}&id=${tab.id}`, { method: 'PUT' });
491
+ } catch {
492
+ // best-effort
493
+ }
494
+ }
495
+
496
+ async function startLearnSession(durationSeconds: number): Promise<LearnResult> {
497
+ await ensureChromeWithCDP();
498
+ await navigateToX();
499
+
500
+ return new Promise((resolve, reject) => {
501
+ const socketPath = getSocketPath();
502
+ const sessionToken = readSessionToken();
503
+ const socket = net.createConnection(socketPath);
504
+ const parser = createMessageParser();
505
+
506
+ socket.on('error', (err) => {
507
+ reject(new Error(`Cannot connect to daemon: ${err.message}. Is the daemon running?`));
508
+ });
509
+
510
+ const timeoutHandle = setTimeout(() => {
511
+ socket.destroy();
512
+ reject(new Error(`Learn session timed out after ${durationSeconds + 30}s`));
513
+ }, (durationSeconds + 30) * 1000);
514
+ timeoutHandle.unref();
515
+
516
+ let authenticated = !sessionToken;
517
+
518
+ const sendStartCommand = () => {
519
+ socket.write(
520
+ serialize({
521
+ type: 'ride_shotgun_start',
522
+ durationSeconds,
523
+ intervalSeconds: 5,
524
+ mode: 'learn',
525
+ targetDomain: 'x.com',
526
+ } as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
527
+ );
528
+ };
529
+
530
+ socket.on('data', (chunk) => {
531
+ const messages = parser.feed(chunk.toString('utf-8'));
532
+ for (const msg of messages) {
533
+ const m = msg as unknown as Record<string, unknown>;
534
+
535
+ if (!authenticated && m.type === 'auth_result') {
536
+ if ((m as { success: boolean }).success) {
537
+ authenticated = true;
538
+ sendStartCommand();
539
+ } else {
540
+ clearTimeout(timeoutHandle);
541
+ socket.destroy();
542
+ reject(new Error('Daemon authentication failed'));
543
+ }
544
+ continue;
545
+ }
546
+
547
+ if (m.type === 'auth_result') {
548
+ continue;
549
+ }
550
+
551
+ if (m.type === 'ride_shotgun_result') {
552
+ clearTimeout(timeoutHandle);
553
+ socket.destroy();
554
+ resolve({
555
+ recordingId: m.recordingId as string | undefined,
556
+ recordingPath: m.recordingPath as string | undefined,
557
+ });
558
+ }
559
+ }
560
+ });
561
+
562
+ socket.on('connect', () => {
563
+ if (sessionToken) {
564
+ socket.write(
565
+ serialize({
566
+ type: 'auth',
567
+ token: sessionToken,
568
+ } as unknown as import('../daemon/ipc-protocol.js').ClientMessage),
569
+ );
570
+ } else {
571
+ sendStartCommand();
572
+ }
573
+ });
574
+ });
575
+ }
package/src/cli.ts CHANGED
@@ -13,12 +13,14 @@ import {
13
13
  } from './daemon/ipc-protocol.js';
14
14
  import { formatDiff, formatNewFileDiff } from './util/diff.js';
15
15
  import { Spinner } from './util/spinner.js';
16
+ import { truncate } from './util/truncate.js';
16
17
  import { copyToClipboard, extractLastCodeBlock, formatSessionForExport } from './util/clipboard.js';
17
18
  import { timeAgo } from './util/time.js';
18
19
  import { ensureDaemonRunning } from './daemon/lifecycle.js';
19
20
  import { shouldAutoStartDaemon } from './daemon/connection-policy.js';
20
21
  import { renderMainScreen, updateStatusText, updateDaemonText, type MainScreenLayout } from './cli/main-screen.jsx';
21
22
 
23
+ const SHORT_HASH_LENGTH = 8;
22
24
  const HEARTBEAT_INTERVAL_MS = 30_000;
23
25
  const HEARTBEAT_TIMEOUT_MS = 10_000;
24
26
  const RECONNECT_BASE_DELAY_MS = 1_000;
@@ -51,7 +53,7 @@ export function formatPrincipalTag(req: Pick<ConfirmationRequest, 'principalKind
51
53
  const name = req.principalId ?? req.principalKind;
52
54
  // Show a shortened version hash when available (first 8 hex chars after any scheme prefix)
53
55
  const versionSuffix = req.principalVersion
54
- ? `@${req.principalVersion.replace(/^[^:]+:/, '').slice(0, 8)}`
56
+ ? `@${req.principalVersion.replace(/^[^:]+:/, '').slice(0, SHORT_HASH_LENGTH)}`
55
57
  : '';
56
58
  const target = req.executionTarget ? ` \u2192 ${req.executionTarget}` : '';
57
59
  return `[${req.principalKind}: ${name}${versionSuffix}${target}]`;
@@ -179,7 +181,7 @@ export async function startCli(): Promise<void> {
179
181
  if (req.toolName === 'browser_press_key') {
180
182
  return `press "${req.input.key ?? ''}"`;
181
183
  }
182
- return `${req.toolName}: ${JSON.stringify(req.input).slice(0, 80)}`;
184
+ return `${req.toolName}: ${truncate(JSON.stringify(req.input), 80)}`;
183
185
  }
184
186
 
185
187
  function renderConfirmationPrompt(req: ConfirmationRequest): void {
@@ -331,7 +333,7 @@ export async function startCli(): Promise<void> {
331
333
  for (let i = 0; i < sessions.length; i++) {
332
334
  const s = sessions[i];
333
335
  const ago = timeAgo(s.updatedAt);
334
- const title = s.title.length > 50 ? s.title.slice(0, 47) + '...' : s.title;
336
+ const title = truncate(s.title, 50);
335
337
  const padding = ' '.repeat(Math.max(1, 55 - title.length));
336
338
  process.stdout.write(` [${i + 1}] ${title}${padding}${ago}\n`);
337
339
  }
@@ -499,7 +501,7 @@ export async function startCli(): Promise<void> {
499
501
  }
500
502
  process.stdout.write('\n');
501
503
  } else {
502
- process.stdout.write(`\n[Tool: ${msg.result.slice(0, 200)}]\n`);
504
+ process.stdout.write(`\n[Tool: ${truncate(msg.result, 200)}]\n`);
503
505
  }
504
506
  toolStreaming = false;
505
507
  if (msg.diff) {
@@ -585,7 +587,7 @@ export async function startCli(): Promise<void> {
585
587
  } else {
586
588
  for (const m of msg.messages) {
587
589
  const label = m.role === 'user' ? 'you' : 'assistant';
588
- const preview = m.text.length > 120 ? m.text.slice(0, 117) + '...' : m.text;
590
+ const preview = truncate(m.text, 120);
589
591
  process.stdout.write(` ${label}> ${preview.replace(/\n/g, ' ')}\n`);
590
592
  }
591
593
  }