@vellumai/assistant 0.4.35 → 0.4.37

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 (239) hide show
  1. package/AGENTS.md +1 -1
  2. package/ARCHITECTURE.md +44 -49
  3. package/README.md +32 -20
  4. package/docs/architecture/keychain-broker.md +186 -0
  5. package/docs/architecture/security.md +110 -116
  6. package/docs/runbook-trusted-contacts.md +2 -2
  7. package/docs/skills.md +25 -25
  8. package/package.json +5 -2
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +11 -2
  10. package/src/__tests__/actor-token-service.test.ts +1 -0
  11. package/src/__tests__/amazon-cdp-integration.test.ts +74 -0
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +38 -9
  13. package/src/__tests__/assistant-id-boundary-guard.test.ts +29 -0
  14. package/src/__tests__/browser-fill-credential.test.ts +1 -1
  15. package/src/__tests__/bundle-scanner.test.ts +1 -1
  16. package/src/__tests__/channel-guardian.test.ts +102 -102
  17. package/src/__tests__/channel-invite-transport.test.ts +155 -256
  18. package/src/__tests__/channel-readiness-routes.test.ts +336 -0
  19. package/src/__tests__/checker.test.ts +6 -6
  20. package/src/__tests__/chrome-cdp.test.ts +350 -0
  21. package/src/__tests__/computer-use-session-lifecycle.test.ts +3 -3
  22. package/src/__tests__/computer-use-session-working-dir.test.ts +86 -52
  23. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +1 -1
  24. package/src/__tests__/config-loader-migration.test.ts +85 -0
  25. package/src/__tests__/conversation-pairing.test.ts +370 -5
  26. package/src/__tests__/credential-broker-browser-fill.test.ts +1 -10
  27. package/src/__tests__/credential-broker-server-use.test.ts +1 -10
  28. package/src/__tests__/credential-security-e2e.test.ts +7 -1
  29. package/src/__tests__/credential-security-invariants.test.ts +14 -20
  30. package/src/__tests__/credential-vault-unit.test.ts +1 -11
  31. package/src/__tests__/credential-vault.test.ts +5 -19
  32. package/src/__tests__/credentials-cli.test.ts +814 -0
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +23 -4
  34. package/src/__tests__/email-invite-adapter.test.ts +78 -0
  35. package/src/__tests__/email-service-config-fallback.test.ts +102 -0
  36. package/src/__tests__/encrypted-store.test.ts +6 -6
  37. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  38. package/src/__tests__/gateway-only-enforcement.test.ts +5 -1
  39. package/src/__tests__/guardian-actions-endpoint.test.ts +70 -12
  40. package/src/__tests__/guardian-outbound-http.test.ts +53 -47
  41. package/src/__tests__/handle-user-message-secret-resume.test.ts +23 -0
  42. package/src/__tests__/handlers-add-trust-rule-metadata.test.ts +32 -23
  43. package/src/__tests__/handlers-telegram-config.test.ts +8 -2
  44. package/src/__tests__/handlers-twitter-config.test.ts +2 -2
  45. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +108 -7
  46. package/src/__tests__/ingress-reconcile.test.ts +6 -0
  47. package/src/__tests__/intent-routing.test.ts +23 -4
  48. package/src/__tests__/invite-routes-http.test.ts +12 -0
  49. package/src/__tests__/ipc-snapshot.test.ts +8 -2
  50. package/src/__tests__/keychain-broker-client.test.ts +543 -0
  51. package/src/__tests__/llm-usage-store.test.ts +344 -0
  52. package/src/__tests__/mcp-client-auth.test.ts +2 -2
  53. package/src/__tests__/media-reuse-story.e2e.test.ts +1 -1
  54. package/src/__tests__/migration-transport.test.ts +49 -0
  55. package/src/__tests__/notification-broadcaster.test.ts +205 -5
  56. package/src/__tests__/notification-deep-link.test.ts +365 -1
  57. package/src/__tests__/oauth-connect-handler.test.ts +2 -2
  58. package/src/__tests__/onboarding-starter-tasks.test.ts +17 -4
  59. package/src/__tests__/proxy-approval-callback.test.ts +1 -1
  60. package/src/__tests__/recording-handler.test.ts +1 -1
  61. package/src/__tests__/recording-intent-handler.test.ts +6 -1
  62. package/src/__tests__/recording-state-machine.test.ts +1 -1
  63. package/src/__tests__/relay-server.test.ts +9 -1
  64. package/src/__tests__/ride-shotgun-handler.test.ts +499 -0
  65. package/src/__tests__/runtime-attachment-metadata.test.ts +160 -1
  66. package/src/__tests__/script-proxy-injection-runtime.test.ts +299 -2
  67. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +1 -1
  68. package/src/__tests__/secret-onetime-send.test.ts +8 -2
  69. package/src/__tests__/secure-keys.test.ts +175 -216
  70. package/src/__tests__/session-confirmation-signals.test.ts +1 -1
  71. package/src/__tests__/session-messaging-secret-redirect.test.ts +1 -1
  72. package/src/__tests__/session-queue.test.ts +2 -1
  73. package/src/__tests__/session-tool-setup-app-refresh.test.ts +2 -2
  74. package/src/__tests__/skill-feature-flags-integration.test.ts +29 -4
  75. package/src/__tests__/skill-feature-flags.test.ts +12 -9
  76. package/src/__tests__/skill-load-feature-flag.test.ts +26 -5
  77. package/src/__tests__/skill-projection.benchmark.test.ts +0 -1
  78. package/src/__tests__/skills.test.ts +34 -4
  79. package/src/__tests__/slack-channel-config.test.ts +2 -2
  80. package/src/__tests__/system-prompt.test.ts +26 -4
  81. package/src/__tests__/telegram-bot-username-resolution.test.ts +212 -0
  82. package/src/__tests__/telegram-invite-adapter.test.ts +164 -0
  83. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -1
  84. package/src/__tests__/tool-permission-simulate-handler.test.ts +8 -2
  85. package/src/__tests__/trusted-contact-approval-notifier.test.ts +9 -1
  86. package/src/__tests__/twitter-auth-handler.test.ts +2 -2
  87. package/src/__tests__/twitter-oauth-client.test.ts +1 -1
  88. package/src/__tests__/usage-routes.test.ts +339 -0
  89. package/src/__tests__/whatsapp-invite-adapter.test.ts +94 -0
  90. package/src/agent/loop.ts +3 -0
  91. package/src/amazon/checkout.ts +0 -1
  92. package/src/approvals/guardian-request-resolvers.ts +9 -1
  93. package/src/bundler/app-bundler.ts +28 -12
  94. package/src/bundler/bundle-scanner.ts +1 -1
  95. package/src/bundler/bundle-signer.ts +3 -3
  96. package/src/bundler/manifest.ts +1 -1
  97. package/src/bundler/signature-verifier.ts +3 -3
  98. package/src/channels/config.ts +1 -1
  99. package/src/cli/AGENTS.md +63 -0
  100. package/src/cli/__tests__/notifications.test.ts +470 -0
  101. package/src/cli/amazon.ts +344 -167
  102. package/src/cli/audit.ts +85 -0
  103. package/src/cli/autonomy.ts +369 -0
  104. package/src/cli/channels.ts +51 -0
  105. package/src/cli/completions.ts +208 -0
  106. package/src/cli/config.ts +220 -0
  107. package/src/cli/contacts.ts +471 -0
  108. package/src/cli/credentials.ts +564 -0
  109. package/src/cli/default-action.ts +14 -0
  110. package/src/cli/dev.ts +131 -0
  111. package/src/cli/doctor.ts +398 -0
  112. package/src/cli/email.ts +494 -0
  113. package/src/cli/influencer.ts +72 -0
  114. package/src/cli/integrations.ts +248 -57
  115. package/src/cli/keys.ts +114 -0
  116. package/src/cli/map.ts +46 -54
  117. package/src/cli/mcp.ts +111 -3
  118. package/src/cli/{config-commands.ts → memory.ts} +134 -245
  119. package/src/cli/notifications.ts +407 -0
  120. package/src/cli/program.ts +65 -0
  121. package/src/cli/reference.ts +48 -0
  122. package/src/cli/sequence.ts +154 -0
  123. package/src/cli/sessions.ts +262 -0
  124. package/src/cli/trust.ts +175 -0
  125. package/src/cli/twitter.ts +323 -106
  126. package/src/config/__tests__/build-cli-reference-section.test.ts +49 -0
  127. package/src/config/bundled-skills/amazon/SKILL.md +2 -2
  128. package/src/config/bundled-skills/app-builder/TOOLS.json +26 -0
  129. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +13 -0
  130. package/src/config/bundled-skills/contacts/SKILL.md +178 -10
  131. package/src/config/bundled-skills/doordash/doordash-cli.ts +23 -168
  132. package/src/config/bundled-skills/google-oauth-setup/SKILL.md +135 -34
  133. package/src/config/bundled-skills/messaging/tools/shared.ts +4 -1
  134. package/src/config/bundled-skills/twilio-setup/SKILL.md +70 -17
  135. package/src/config/bundled-tool-registry.ts +2 -0
  136. package/src/config/core-schema.ts +7 -0
  137. package/src/config/feature-flag-registry.json +16 -0
  138. package/src/config/loader.ts +26 -0
  139. package/src/config/schema.ts +4 -0
  140. package/src/config/skill-state.ts +0 -13
  141. package/src/config/system-prompt.ts +27 -0
  142. package/src/contacts/contact-store.ts +25 -0
  143. package/src/daemon/computer-use-session.ts +1 -1
  144. package/src/daemon/handlers/apps.ts +1 -0
  145. package/src/daemon/handlers/config-channels.ts +3 -3
  146. package/src/daemon/handlers/config-dispatch.ts +29 -0
  147. package/src/daemon/handlers/config-inbox.ts +4 -3
  148. package/src/daemon/handlers/config.ts +3 -43
  149. package/src/daemon/handlers/contacts.ts +34 -0
  150. package/src/daemon/handlers/index.ts +17 -3
  151. package/src/daemon/handlers/session-user-message.ts +7 -0
  152. package/src/daemon/handlers/sessions.ts +21 -2
  153. package/src/daemon/handlers/shared.ts +17 -0
  154. package/src/daemon/ipc-contract/apps.ts +2 -0
  155. package/src/daemon/ipc-contract/computer-use.ts +9 -0
  156. package/src/daemon/ipc-contract/contacts.ts +3 -3
  157. package/src/daemon/ipc-contract/inbox.ts +2 -0
  158. package/src/daemon/ipc-contract/messages.ts +4 -0
  159. package/src/daemon/ipc-contract/sessions.ts +8 -0
  160. package/src/daemon/ipc-contract-inventory.json +1 -0
  161. package/src/daemon/lifecycle.ts +0 -5
  162. package/src/daemon/ride-shotgun-handler.ts +139 -25
  163. package/src/daemon/session-agent-loop-handlers.ts +100 -0
  164. package/src/daemon/session-agent-loop.ts +72 -0
  165. package/src/daemon/session-tool-setup.ts +7 -0
  166. package/src/daemon/session.ts +23 -1
  167. package/src/daemon/tool-side-effects.ts +39 -1
  168. package/src/email/service.ts +59 -2
  169. package/src/index.ts +2 -60
  170. package/src/mcp/mcp-oauth-provider.ts +90 -8
  171. package/src/media/app-icon-generator.ts +86 -0
  172. package/src/memory/db-init.ts +11 -0
  173. package/src/memory/llm-usage-store.ts +186 -0
  174. package/src/memory/migrations/137-usage-dashboard-indexes.ts +26 -0
  175. package/src/memory/migrations/139-drop-usage-composite-indexes.ts +30 -0
  176. package/src/memory/migrations/index.ts +2 -0
  177. package/src/memory/schema-migration.ts +1 -0
  178. package/src/memory/shared-app-links-store.ts +1 -1
  179. package/src/messaging/registry.ts +27 -0
  180. package/src/notifications/README.md +79 -70
  181. package/src/notifications/broadcaster.ts +2 -1
  182. package/src/notifications/conversation-pairing.ts +147 -13
  183. package/src/notifications/copy-composer.ts +7 -3
  184. package/src/notifications/destination-resolver.ts +14 -1
  185. package/src/notifications/emit-signal.ts +3 -2
  186. package/src/notifications/signal.ts +105 -1
  187. package/src/notifications/types.ts +16 -0
  188. package/src/permissions/checker.ts +29 -3
  189. package/src/permissions/prompter.ts +11 -3
  190. package/src/runtime/access-request-helper.ts +2 -1
  191. package/src/runtime/auth/route-policy.ts +7 -1
  192. package/src/runtime/channel-invite-transport.ts +40 -63
  193. package/src/runtime/channel-invite-transports/email.ts +13 -39
  194. package/src/runtime/channel-invite-transports/slack.ts +5 -34
  195. package/src/runtime/channel-invite-transports/sms.ts +8 -29
  196. package/src/runtime/channel-invite-transports/telegram.ts +69 -28
  197. package/src/runtime/channel-invite-transports/voice.ts +0 -7
  198. package/src/runtime/channel-invite-transports/whatsapp.ts +43 -0
  199. package/src/runtime/channel-readiness-service.ts +202 -45
  200. package/src/runtime/confirmation-request-guardian-bridge.ts +2 -1
  201. package/src/runtime/guardian-outbound-actions.ts +8 -5
  202. package/src/runtime/http-server.ts +2 -0
  203. package/src/runtime/invite-instruction-generator.ts +178 -0
  204. package/src/runtime/invite-service.ts +22 -25
  205. package/src/runtime/migrations/migration-transport.ts +13 -0
  206. package/src/runtime/routes/app-routes.ts +1 -1
  207. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +8 -7
  208. package/src/runtime/routes/channel-readiness-routes.ts +30 -11
  209. package/src/runtime/routes/contact-routes.ts +54 -26
  210. package/src/runtime/routes/inbound-stages/bootstrap-intercept.ts +1 -1
  211. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +2 -1
  212. package/src/runtime/routes/inbound-stages/verification-intercept.ts +2 -1
  213. package/src/runtime/routes/integration-routes.ts +1 -1
  214. package/src/runtime/routes/invite-routes.ts +1 -1
  215. package/src/runtime/routes/secret-routes.ts +31 -7
  216. package/src/runtime/routes/twilio-routes.ts +32 -1
  217. package/src/runtime/routes/usage-routes.ts +114 -0
  218. package/src/runtime/tool-grant-request-helper.ts +2 -1
  219. package/src/security/encrypted-store.ts +9 -5
  220. package/src/security/keychain-broker-client.ts +393 -0
  221. package/src/security/secure-keys.ts +106 -321
  222. package/src/tools/apps/executors.ts +73 -0
  223. package/src/tools/browser/auto-navigate.ts +15 -6
  224. package/src/tools/browser/chrome-cdp.ts +211 -0
  225. package/src/tools/browser/network-recorder.test.ts +83 -0
  226. package/src/tools/browser/network-recorder.ts +8 -7
  227. package/src/tools/browser/x-auto-navigate.ts +12 -6
  228. package/src/tools/credentials/policy-types.ts +24 -0
  229. package/src/tools/credentials/vault.ts +22 -27
  230. package/src/tools/network/script-proxy/session-manager.ts +47 -3
  231. package/src/tools/permission-checker.ts +1 -0
  232. package/src/tools/types.ts +2 -0
  233. package/src/tools/ui-surface/definitions.ts +1 -2
  234. package/src/tools/watch/watch-state.ts +2 -0
  235. package/src/__tests__/key-migration.test.ts +0 -240
  236. package/src/__tests__/keychain.test.ts +0 -286
  237. package/src/cli/core-commands.ts +0 -899
  238. package/src/security/keychain-to-encrypted-migration.ts +0 -66
  239. package/src/security/keychain.ts +0 -490
@@ -118,12 +118,57 @@ export function registerTwitterCommand(program: Command): void {
118
118
  )
119
119
  .option("--json", "Machine-readable JSON output");
120
120
 
121
+ tw.addHelpText(
122
+ "after",
123
+ `
124
+ Twitter (X) uses a dual-path architecture for interacting with the platform:
125
+
126
+ 1. OAuth (official API) — uses an authenticated Twitter OAuth application for
127
+ posting and replying. Requires a connected OAuth credential.
128
+ 2. Browser session (Ride Shotgun) — uses cookies captured from a real Chrome
129
+ session to call Twitter's internal GraphQL API. Supports all read operations
130
+ and posting as a fallback.
131
+
132
+ The strategy system controls which path is used for operations that support both:
133
+ oauth — always use the OAuth API; fail if unavailable
134
+ browser — always use the browser session; fail if unavailable
135
+ auto — try OAuth first, fall back to browser session (default)
136
+
137
+ Session management:
138
+ - "login" imports cookies from a Ride Shotgun recording file
139
+ - "refresh" launches Chrome with CDP, navigates to x.com/login, and runs a
140
+ Ride Shotgun learn session to capture fresh cookies automatically
141
+ - "status" shows whether browser session and OAuth are active
142
+ - "logout" clears the saved browser session cookies
143
+
144
+ Examples:
145
+ $ vellum x status
146
+ $ vellum x post "Hello world"
147
+ $ vellum x timeline elonmusk --count 10
148
+ $ vellum x search "from:vaborsh AI agents" --product Latest
149
+ $ vellum x strategy set oauth`,
150
+ );
151
+
121
152
  // =========================================================================
122
153
  // login — import session from a recording
123
154
  // =========================================================================
124
155
  tw.command("login")
125
156
  .description("Import a Twitter session from a Ride Shotgun recording")
126
157
  .requiredOption("--recording <path>", "Path to the recording JSON file")
158
+ .addHelpText(
159
+ "after",
160
+ `
161
+ Imports cookies from a Ride Shotgun recording file to establish a browser
162
+ session. The recording file is a JSON file produced by a Ride Shotgun learn
163
+ session that contains captured cookies for x.com.
164
+
165
+ After import, all browser-path commands (timeline, search, bookmarks, etc.)
166
+ will use these cookies for authentication.
167
+
168
+ Examples:
169
+ $ vellum x login --recording /tmp/ride-shotgun/recording-abc123.json
170
+ $ vellum x login --recording ~/recordings/twitter-session.json`,
171
+ )
127
172
  .action(async (opts: { recording: string }, cmd: Command) => {
128
173
  await run(cmd, async () => {
129
174
  const session = importFromRecording(opts.recording);
@@ -140,6 +185,16 @@ export function registerTwitterCommand(program: Command): void {
140
185
  // =========================================================================
141
186
  tw.command("logout")
142
187
  .description("Clear the saved Twitter session")
188
+ .addHelpText(
189
+ "after",
190
+ `
191
+ Deletes all saved browser session cookies. After logout, browser-path commands
192
+ will fail until a new session is imported via "login" or captured via "refresh".
193
+ OAuth credentials are not affected.
194
+
195
+ Examples:
196
+ $ vellum x logout`,
197
+ )
143
198
  .action((_opts: unknown, cmd: Command) => {
144
199
  clearSession();
145
200
  output({ ok: true, message: "Session cleared" }, getJson(cmd));
@@ -155,6 +210,24 @@ export function registerTwitterCommand(program: Command): void {
155
210
  "NOTE: Chrome will restart with debugging enabled; your tabs will be restored.",
156
211
  )
157
212
  .option("--duration <seconds>", "Recording duration in seconds", "180")
213
+ .addHelpText(
214
+ "after",
215
+ `
216
+ Restarts Chrome with CDP (Chrome DevTools Protocol) enabled, navigates to
217
+ x.com/login, and runs a Ride Shotgun learn session to capture fresh cookies.
218
+ Sign in when Chrome opens — the session will be recorded automatically.
219
+
220
+ The --duration flag sets how long (in seconds) the recording runs before
221
+ stopping. Default is 180 seconds (3 minutes). After the recording completes,
222
+ cookies are imported automatically and Chrome is minimized.
223
+
224
+ Requires the assistant to be running (Ride Shotgun runs via the assistant).
225
+
226
+ Examples:
227
+ $ vellum x refresh
228
+ $ vellum x refresh --duration 120
229
+ $ vellum x refresh --duration 300`,
230
+ )
158
231
  .action(async (opts: { duration: string }, cmd: Command) => {
159
232
  const json = getJson(cmd);
160
233
  const duration = parseInt(opts.duration, 10);
@@ -166,7 +239,7 @@ export function registerTwitterCommand(program: Command): void {
166
239
 
167
240
  // Hide Chrome after capturing session
168
241
  try {
169
- await minimizeChromeWindow();
242
+ await minimizeChromeWindow(); // uses default CDP port
170
243
  } catch {
171
244
  /* best-effort */
172
245
  }
@@ -201,6 +274,22 @@ export function registerTwitterCommand(program: Command): void {
201
274
  // =========================================================================
202
275
  tw.command("status")
203
276
  .description("Check Twitter session, OAuth, and strategy status")
277
+ .addHelpText(
278
+ "after",
279
+ `
280
+ Shows the current state of both authentication paths:
281
+
282
+ Browser session — whether cookies are loaded, cookie count, import timestamp,
283
+ and the recording ID they came from.
284
+ OAuth — whether an OAuth credential is connected, the linked account, the
285
+ current strategy setting, and whether a strategy has been explicitly configured.
286
+
287
+ If the assistant is not running, OAuth fields will be reported as undefined.
288
+
289
+ Examples:
290
+ $ vellum x status
291
+ $ vellum x status --json`,
292
+ )
204
293
  .action(async (_opts: unknown, cmd: Command) => {
205
294
  const session = loadSession();
206
295
  const browserInfo: Record<string, unknown> = session
@@ -258,6 +347,26 @@ export function registerTwitterCommand(program: Command): void {
258
347
  .description(
259
348
  "Get or set the Twitter operation strategy (oauth, browser, auto)",
260
349
  )
350
+ .addHelpText(
351
+ "after",
352
+ `
353
+ The strategy controls which authentication path is used for operations that
354
+ support both OAuth and browser session:
355
+
356
+ oauth — always use the official Twitter OAuth API. Fails if no OAuth
357
+ credential is connected. Best for reliable posting.
358
+ browser — always use the browser session (captured cookies). Fails if no
359
+ session is loaded. Required for read-only endpoints not available
360
+ via OAuth (bookmarks, notifications, search).
361
+ auto — try OAuth first, fall back to browser session. This is the default.
362
+
363
+ Run without a subcommand to display the current strategy. Use "set" to change it.
364
+
365
+ Examples:
366
+ $ vellum x strategy
367
+ $ vellum x strategy set oauth
368
+ $ vellum x strategy set auto`,
369
+ )
261
370
  .action(async (_opts: unknown, cmd: Command) => {
262
371
  const json = getJson(cmd);
263
372
  try {
@@ -279,6 +388,21 @@ export function registerTwitterCommand(program: Command): void {
279
388
  .command("set")
280
389
  .description("Set the Twitter operation strategy")
281
390
  .argument("<value>", "Strategy value: oauth, browser, or auto")
391
+ .addHelpText(
392
+ "after",
393
+ `
394
+ Arguments:
395
+ value Strategy to use: "oauth", "browser", or "auto"
396
+
397
+ Sets the preferred strategy for Twitter operations that support dual-path
398
+ routing. The setting is persisted by the assistant and applies to all subsequent
399
+ operations until changed.
400
+
401
+ Examples:
402
+ $ vellum x strategy set oauth
403
+ $ vellum x strategy set browser
404
+ $ vellum x strategy set auto`,
405
+ )
282
406
  .action(async (value: string, _opts: unknown, cmd: Command) => {
283
407
  const json = getJson(cmd);
284
408
  try {
@@ -311,6 +435,20 @@ export function registerTwitterCommand(program: Command): void {
311
435
  tw.command("post")
312
436
  .description("Post a tweet")
313
437
  .argument("<text>", "Tweet text")
438
+ .addHelpText(
439
+ "after",
440
+ `
441
+ Arguments:
442
+ text The tweet text to post (max 280 characters)
443
+
444
+ Posts a new tweet using the routed dual-path system. The path used (oauth or
445
+ browser) depends on the current strategy setting. The response includes the
446
+ tweet ID, URL, and which path was used.
447
+
448
+ Examples:
449
+ $ vellum x post "Hello world"
450
+ $ vellum x post "Check out this thread on AI agents" --json`,
451
+ )
314
452
  .action(async (text: string, _opts: unknown, cmd: Command) => {
315
453
  await run(cmd, async () => {
316
454
  const { result, pathUsed } = await routedPostTweet(text);
@@ -330,6 +468,21 @@ export function registerTwitterCommand(program: Command): void {
330
468
  .description("Reply to a tweet")
331
469
  .argument("<tweetUrl>", "Tweet URL or tweet ID")
332
470
  .argument("<text>", "Reply text")
471
+ .addHelpText(
472
+ "after",
473
+ `
474
+ Arguments:
475
+ tweetUrl Full tweet URL (e.g. https://x.com/user/status/123456) or a bare tweet ID
476
+ text The reply text to post (max 280 characters)
477
+
478
+ Posts a reply to the specified tweet. Accepts either a full tweet URL or a bare
479
+ numeric tweet ID. The tweet ID is extracted from the last numeric segment of the
480
+ URL. Uses the routed dual-path system based on the current strategy.
481
+
482
+ Examples:
483
+ $ vellum x reply https://x.com/elonmusk/status/1234567890 "Great point!"
484
+ $ vellum x reply 1234567890 "Interesting thread"`,
485
+ )
333
486
  .action(
334
487
  async (tweetUrl: string, text: string, _opts: unknown, cmd: Command) => {
335
488
  await run(cmd, async () => {
@@ -359,6 +512,21 @@ export function registerTwitterCommand(program: Command): void {
359
512
  .description("Fetch a user's recent tweets")
360
513
  .argument("<screenName>", "Twitter screen name (without @)")
361
514
  .option("--count <n>", "Number of tweets to fetch", "20")
515
+ .addHelpText(
516
+ "after",
517
+ `
518
+ Arguments:
519
+ screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
520
+
521
+ Fetches a user's recent tweets via the browser session. Resolves the screen name
522
+ to a user ID first, then retrieves their tweet timeline. The --count flag controls
523
+ how many tweets to return (default: 20).
524
+
525
+ Examples:
526
+ $ vellum x timeline elonmusk
527
+ $ vellum x timeline vaborsh --count 50
528
+ $ vellum x timeline openai --count 10 --json`,
529
+ )
362
530
  .action(
363
531
  async (screenName: string, opts: { count: string }, cmd: Command) => {
364
532
  await run(cmd, async () => {
@@ -378,6 +546,22 @@ export function registerTwitterCommand(program: Command): void {
378
546
  tw.command("tweet")
379
547
  .description("Fetch a tweet and its reply thread")
380
548
  .argument("<tweetIdOrUrl>", "Tweet ID or URL")
549
+ .addHelpText(
550
+ "after",
551
+ `
552
+ Arguments:
553
+ tweetIdOrUrl A bare tweet ID (e.g. 1234567890) or a full tweet URL
554
+ (e.g. https://x.com/user/status/1234567890)
555
+
556
+ Fetches a single tweet and its reply thread via the browser session. The tweet
557
+ ID is extracted from the last numeric segment of the input. Returns an array of
558
+ tweets representing the conversation thread.
559
+
560
+ Examples:
561
+ $ vellum x tweet 1234567890
562
+ $ vellum x tweet https://x.com/elonmusk/status/1234567890
563
+ $ vellum x tweet https://x.com/openai/status/9876543210 --json`,
564
+ )
381
565
  .action(async (tweetIdOrUrl: string, _opts: unknown, cmd: Command) => {
382
566
  await run(cmd, async () => {
383
567
  const idMatch = tweetIdOrUrl.match(/(\d+)\s*$/);
@@ -395,6 +579,26 @@ export function registerTwitterCommand(program: Command): void {
395
579
  .description("Search tweets")
396
580
  .argument("<query>", "Search query")
397
581
  .option("--product <type>", "Top, Latest, People, or Media", "Top")
582
+ .addHelpText(
583
+ "after",
584
+ `
585
+ Arguments:
586
+ query Twitter search query string. Supports Twitter search operators
587
+ (e.g. "from:user", "to:user", "min_faves:100", quoted phrases)
588
+
589
+ The --product flag selects the search result type:
590
+ Top — most relevant tweets (default)
591
+ Latest — most recent tweets, reverse chronological
592
+ People — user accounts matching the query
593
+ Media — tweets containing images or video
594
+
595
+ Uses the browser session path. Requires an active browser session.
596
+
597
+ Examples:
598
+ $ vellum x search "AI agents"
599
+ $ vellum x search "from:elonmusk SpaceX" --product Latest
600
+ $ vellum x search "machine learning" --product Media --json`,
601
+ )
398
602
  .action(async (query: string, opts: { product: string }, cmd: Command) => {
399
603
  await run(cmd, async () => {
400
604
  const tweets = await searchTweets(
@@ -411,6 +615,20 @@ export function registerTwitterCommand(program: Command): void {
411
615
  tw.command("bookmarks")
412
616
  .description("Fetch your bookmarks")
413
617
  .option("--count <n>", "Number of bookmarks", "20")
618
+ .addHelpText(
619
+ "after",
620
+ `
621
+ Fetches the authenticated user's bookmarked tweets via the browser session.
622
+ The --count flag controls how many bookmarks to return (default: 20).
623
+
624
+ Requires an active browser session. Bookmarks are private and only available
625
+ for the logged-in account.
626
+
627
+ Examples:
628
+ $ vellum x bookmarks
629
+ $ vellum x bookmarks --count 50
630
+ $ vellum x bookmarks --json`,
631
+ )
414
632
  .action(async (opts: { count: string }, cmd: Command) => {
415
633
  await run(cmd, async () => {
416
634
  const tweets = await getBookmarks(parseInt(opts.count, 10));
@@ -424,6 +642,19 @@ export function registerTwitterCommand(program: Command): void {
424
642
  tw.command("home")
425
643
  .description("Fetch your home timeline")
426
644
  .option("--count <n>", "Number of tweets", "20")
645
+ .addHelpText(
646
+ "after",
647
+ `
648
+ Fetches the authenticated user's home timeline (the "For You" feed) via the
649
+ browser session. The --count flag controls how many tweets to return (default: 20).
650
+
651
+ Requires an active browser session.
652
+
653
+ Examples:
654
+ $ vellum x home
655
+ $ vellum x home --count 50
656
+ $ vellum x home --json`,
657
+ )
427
658
  .action(async (opts: { count: string }, cmd: Command) => {
428
659
  await run(cmd, async () => {
429
660
  const tweets = await getHomeTimeline(parseInt(opts.count, 10));
@@ -437,6 +668,20 @@ export function registerTwitterCommand(program: Command): void {
437
668
  tw.command("notifications")
438
669
  .description("Fetch your notifications")
439
670
  .option("--count <n>", "Number of notifications", "20")
671
+ .addHelpText(
672
+ "after",
673
+ `
674
+ Fetches the authenticated user's Twitter notifications (mentions, likes,
675
+ retweets, follows, etc.) via the browser session. The --count flag controls
676
+ how many notifications to return (default: 20).
677
+
678
+ Requires an active browser session.
679
+
680
+ Examples:
681
+ $ vellum x notifications
682
+ $ vellum x notifications --count 50
683
+ $ vellum x notifications --json`,
684
+ )
440
685
  .action(async (opts: { count: string }, cmd: Command) => {
441
686
  await run(cmd, async () => {
442
687
  const notifications = await getNotifications(parseInt(opts.count, 10));
@@ -451,6 +696,21 @@ export function registerTwitterCommand(program: Command): void {
451
696
  .description("Fetch a user's liked tweets")
452
697
  .argument("<screenName>", "Twitter screen name (without @)")
453
698
  .option("--count <n>", "Number of likes", "20")
699
+ .addHelpText(
700
+ "after",
701
+ `
702
+ Arguments:
703
+ screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
704
+
705
+ Fetches tweets liked by the specified user via the browser session. Resolves the
706
+ screen name to a user ID first. The --count flag controls how many liked tweets
707
+ to return (default: 20).
708
+
709
+ Examples:
710
+ $ vellum x likes elonmusk
711
+ $ vellum x likes vaborsh --count 50
712
+ $ vellum x likes openai --json`,
713
+ )
454
714
  .action(
455
715
  async (screenName: string, opts: { count: string }, cmd: Command) => {
456
716
  await run(cmd, async () => {
@@ -467,6 +727,19 @@ export function registerTwitterCommand(program: Command): void {
467
727
  tw.command("followers")
468
728
  .description("Fetch a user's followers")
469
729
  .argument("<screenName>", "Twitter screen name (without @)")
730
+ .addHelpText(
731
+ "after",
732
+ `
733
+ Arguments:
734
+ screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
735
+
736
+ Fetches the list of accounts following the specified user via the browser session.
737
+ Resolves the screen name to a user ID first.
738
+
739
+ Examples:
740
+ $ vellum x followers elonmusk
741
+ $ vellum x followers vaborsh --json`,
742
+ )
470
743
  .action(async (screenName: string, _opts: unknown, cmd: Command) => {
471
744
  await run(cmd, async () => {
472
745
  const cleanName = screenName.replace(/^@/, "");
@@ -483,6 +756,21 @@ export function registerTwitterCommand(program: Command): void {
483
756
  .description("Fetch who a user follows")
484
757
  .argument("<screenName>", "Twitter screen name (without @)")
485
758
  .option("--count <n>", "Number of following", "20")
759
+ .addHelpText(
760
+ "after",
761
+ `
762
+ Arguments:
763
+ screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
764
+
765
+ Fetches the list of accounts the specified user follows via the browser session.
766
+ Resolves the screen name to a user ID first. The --count flag controls how many
767
+ results to return (default: 20).
768
+
769
+ Examples:
770
+ $ vellum x following elonmusk
771
+ $ vellum x following vaborsh --count 100
772
+ $ vellum x following openai --json`,
773
+ )
486
774
  .action(
487
775
  async (screenName: string, opts: { count: string }, cmd: Command) => {
488
776
  await run(cmd, async () => {
@@ -503,6 +791,21 @@ export function registerTwitterCommand(program: Command): void {
503
791
  .description("Fetch a user's media tweets")
504
792
  .argument("<screenName>", "Twitter screen name (without @)")
505
793
  .option("--count <n>", "Number of media tweets", "20")
794
+ .addHelpText(
795
+ "after",
796
+ `
797
+ Arguments:
798
+ screenName Twitter screen name without the @ prefix (e.g. "elonmusk", not "@elonmusk")
799
+
800
+ Fetches tweets containing images or video from the specified user via the browser
801
+ session. Resolves the screen name to a user ID first. The --count flag controls
802
+ how many media tweets to return (default: 20).
803
+
804
+ Examples:
805
+ $ vellum x media elonmusk
806
+ $ vellum x media nasa --count 50
807
+ $ vellum x media openai --json`,
808
+ )
506
809
  .action(
507
810
  async (screenName: string, opts: { count: string }, cmd: Command) => {
508
811
  await run(cmd, async () => {
@@ -613,108 +916,13 @@ function sendDaemonMessage(
613
916
  }
614
917
 
615
918
  // ---------------------------------------------------------------------------
616
- // Chrome CDP restart helper
919
+ // Chrome CDP helpers (shared)
617
920
  // ---------------------------------------------------------------------------
618
921
 
619
- import { spawn as spawnChild } from "node:child_process";
620
- import { homedir } from "node:os";
621
- import { join as pathJoin } from "node:path";
622
-
623
- const CDP_BASE = "http://localhost:9222";
624
- const CHROME_DATA_DIR = pathJoin(
625
- homedir(),
626
- "Library/Application Support/Google/Chrome-CDP",
627
- );
628
-
629
- async function isCdpReady(): Promise<boolean> {
630
- try {
631
- const res = await fetch(`${CDP_BASE}/json/version`);
632
- return res.ok;
633
- } catch {
634
- return false;
635
- }
636
- }
637
-
638
- async function ensureChromeWithCDP(): Promise<void> {
639
- // Already running with CDP?
640
- if (await isCdpReady()) return;
641
-
642
- // Launch a separate Chrome instance with CDP flags alongside any existing Chrome.
643
- // Using a dedicated --user-data-dir allows coexistence without killing the user's browser.
644
- const chromeApp =
645
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome";
646
- spawnChild(
647
- chromeApp,
648
- [
649
- `--remote-debugging-port=9222`,
650
- `--force-renderer-accessibility`,
651
- `--user-data-dir=${CHROME_DATA_DIR}`,
652
- "https://x.com/login",
653
- ],
654
- {
655
- detached: true,
656
- stdio: "ignore",
657
- },
658
- ).unref();
659
-
660
- // Wait for CDP to be ready
661
- for (let i = 0; i < 30; i++) {
662
- await new Promise((r) => setTimeout(r, 500));
663
- if (await isCdpReady()) return;
664
- }
665
- throw new Error("Chrome started but CDP endpoint not responding after 15s");
666
- }
667
-
668
- async function minimizeChromeWindow(): Promise<void> {
669
- const res = await fetch(`${CDP_BASE}/json/list`);
670
- const targets = (await res.json()) as Array<{
671
- type: string;
672
- webSocketDebuggerUrl: string;
673
- }>;
674
- const pageTarget = targets.find((t) => t.type === "page");
675
- if (!pageTarget) return;
676
-
677
- const ws = new WebSocket(pageTarget.webSocketDebuggerUrl);
678
-
679
- await new Promise<void>((resolve, reject) => {
680
- const timeout = setTimeout(() => {
681
- ws.close();
682
- reject(new Error("CDP minimize timed out"));
683
- }, 5000);
684
-
685
- ws.addEventListener("open", () => {
686
- ws.send(JSON.stringify({ id: 1, method: "Browser.getWindowForTarget" }));
687
- });
688
-
689
- ws.addEventListener("message", (event) => {
690
- const msg = JSON.parse(String(event.data)) as {
691
- id: number;
692
- result?: { windowId: number };
693
- };
694
- if (msg.id === 1 && msg.result) {
695
- ws.send(
696
- JSON.stringify({
697
- id: 2,
698
- method: "Browser.setWindowBounds",
699
- params: {
700
- windowId: msg.result.windowId,
701
- bounds: { windowState: "minimized" },
702
- },
703
- }),
704
- );
705
- } else if (msg.id === 2) {
706
- clearTimeout(timeout);
707
- ws.close();
708
- resolve();
709
- }
710
- });
711
-
712
- ws.addEventListener("error", (err) => {
713
- clearTimeout(timeout);
714
- reject(err);
715
- });
716
- });
717
- }
922
+ import {
923
+ ensureChromeWithCdp,
924
+ minimizeChromeWindow,
925
+ } from "../tools/browser/chrome-cdp.js";
718
926
 
719
927
  // ---------------------------------------------------------------------------
720
928
  // Ride Shotgun learn session helper
@@ -725,9 +933,9 @@ interface LearnResult {
725
933
  recordingPath?: string;
726
934
  }
727
935
 
728
- async function navigateToX(): Promise<void> {
936
+ async function navigateToX(cdpBase: string): Promise<void> {
729
937
  try {
730
- const res = await fetch(`${CDP_BASE}/json/list`);
938
+ const res = await fetch(`${cdpBase}/json/list`);
731
939
  if (!res.ok) return;
732
940
  const targets = (await res.json()) as Array<{
733
941
  id: string;
@@ -737,7 +945,7 @@ async function navigateToX(): Promise<void> {
737
945
  const tab = targets.find((t) => t.type === "page");
738
946
  if (!tab) return;
739
947
  await fetch(
740
- `${CDP_BASE}/json/navigate?url=${encodeURIComponent(
948
+ `${cdpBase}/json/navigate?url=${encodeURIComponent(
741
949
  "https://x.com/login",
742
950
  )}&id=${tab.id}`,
743
951
  { method: "PUT" },
@@ -750,8 +958,10 @@ async function navigateToX(): Promise<void> {
750
958
  async function startLearnSession(
751
959
  durationSeconds: number,
752
960
  ): Promise<LearnResult> {
753
- await ensureChromeWithCDP();
754
- await navigateToX();
961
+ const cdpSession = await ensureChromeWithCdp({
962
+ startUrl: "https://x.com/login",
963
+ });
964
+ await navigateToX(cdpSession.baseUrl);
755
965
 
756
966
  return new Promise((resolve, reject) => {
757
967
  const socketPath = getSocketPath();
@@ -813,6 +1023,13 @@ async function startLearnSession(
813
1023
  continue;
814
1024
  }
815
1025
 
1026
+ if (m.type === "ride_shotgun_error") {
1027
+ clearTimeout(timeoutHandle);
1028
+ socket.destroy();
1029
+ reject(new Error((m as { message: string }).message));
1030
+ continue;
1031
+ }
1032
+
816
1033
  if (m.type === "ride_shotgun_result") {
817
1034
  clearTimeout(timeoutHandle);
818
1035
  socket.destroy();
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Tests for buildCliReferenceSection — verifies the CLI reference section
3
+ * included in the system prompt has the expected structure and caching behaviour.
4
+ */
5
+
6
+ import { beforeEach, describe, expect, test } from "bun:test";
7
+
8
+ import {
9
+ _resetCliHelpCache,
10
+ buildCliReferenceSection,
11
+ } from "../system-prompt.js";
12
+
13
+ describe("buildCliReferenceSection", () => {
14
+ beforeEach(() => {
15
+ _resetCliHelpCache();
16
+ });
17
+
18
+ test("includes the Assistant CLI heading", () => {
19
+ const result = buildCliReferenceSection();
20
+ expect(result).toContain("## Assistant CLI");
21
+ });
22
+
23
+ test("includes CLI help text with command listings", () => {
24
+ const result = buildCliReferenceSection();
25
+ // The reference is a side-effect-free snapshot of the top-level CLI help.
26
+ expect(result).toContain("Usage:");
27
+ expect(result).toContain("Commands:");
28
+ });
29
+
30
+ test("mentions bash as the way to invoke the CLI", () => {
31
+ const result = buildCliReferenceSection();
32
+ expect(result).toContain("available via `bash`");
33
+ });
34
+
35
+ test("result is cached — calling twice returns the same string", () => {
36
+ const first = buildCliReferenceSection();
37
+ const second = buildCliReferenceSection();
38
+ expect(first).toBe(second);
39
+ });
40
+
41
+ test("cache is reset by _resetCliHelpCache", () => {
42
+ const first = buildCliReferenceSection();
43
+ _resetCliHelpCache();
44
+ const second = buildCliReferenceSection();
45
+ // Content should be identical even after reset (same CLI program),
46
+ // but they should be independently computed strings.
47
+ expect(first).toEqual(second);
48
+ });
49
+ });
@@ -2,7 +2,7 @@
2
2
  name: "Amazon"
3
3
  description: "Shop on Amazon and Amazon Fresh using the built-in CLI integration"
4
4
  user-invocable: true
5
- metadata: {"vellum": {"emoji": "\uD83D\uDCE6"}}
5
+ metadata: { "vellum": { "emoji": "\uD83D\uDCE6" } }
6
6
  ---
7
7
 
8
8
  You can shop on Amazon (and Amazon Fresh for groceries) for the user using the `vellum amazon` CLI.
@@ -92,7 +92,7 @@ vellum amazon fresh select-slot --slot-id <id> --json
92
92
 
93
93
  vellum amazon payment-methods --json
94
94
  vellum amazon checkout --json
95
- vellum amazon order place [--payment-method-id <id>] [--slot-id <id>] --json
95
+ vellum amazon order place [--payment-method-id <id>] --json
96
96
  ```
97
97
 
98
98
  ## Example Interactions
@@ -326,6 +326,32 @@
326
326
  },
327
327
  "executor": "tools/app-file-write.ts",
328
328
  "execution_target": "host"
329
+ },
330
+ {
331
+ "name": "app_generate_icon",
332
+ "description": "Generate or regenerate an AI-designed icon for an app. Uses Gemini to create a professional app icon. Call this when the user wants a new or different icon for their app.",
333
+ "category": "apps",
334
+ "risk": "low",
335
+ "input_schema": {
336
+ "type": "object",
337
+ "properties": {
338
+ "app_id": {
339
+ "type": "string",
340
+ "description": "The ID of the app to generate an icon for"
341
+ },
342
+ "description": {
343
+ "type": "string",
344
+ "description": "Optional description to guide icon generation (e.g. 'a blue calendar with a checkmark'). If omitted, the app name and description are used."
345
+ },
346
+ "reason": {
347
+ "type": "string",
348
+ "description": "Brief non-technical explanation of why this tool is being called"
349
+ }
350
+ },
351
+ "required": ["app_id"]
352
+ },
353
+ "executor": "tools/app-generate-icon.ts",
354
+ "execution_target": "host"
329
355
  }
330
356
  ]
331
357
  }