@vellumai/assistant 0.6.5 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (443) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +15 -17
  3. package/Dockerfile +6 -4
  4. package/__tests__/permissions/gateway-threshold-reader.test.ts +283 -0
  5. package/docs/architecture/integrations.md +32 -39
  6. package/docs/architecture/memory.md +25 -30
  7. package/docs/architecture/security.md +7 -6
  8. package/docs/browser-use-architecture-phase2.md +63 -20
  9. package/docs/plugins.md +761 -0
  10. package/examples/plugins/echo/README.md +132 -0
  11. package/examples/plugins/echo/package.json +17 -0
  12. package/examples/plugins/echo/register.ts +187 -0
  13. package/node_modules/@vellumai/egress-proxy/src/types.ts +19 -0
  14. package/openapi.yaml +212 -68
  15. package/package.json +1 -1
  16. package/src/__tests__/app-compiler.test.ts +57 -0
  17. package/src/__tests__/approval-cascade.test.ts +7 -2
  18. package/src/__tests__/auto-analysis-end-to-end.test.ts +1 -0
  19. package/src/__tests__/avatar-generator.test.ts +4 -2
  20. package/src/__tests__/bundled-asset.test.ts +6 -6
  21. package/src/__tests__/catalog-cache.test.ts +69 -0
  22. package/src/__tests__/checker.test.ts +459 -171
  23. package/src/__tests__/circuit-breaker-pipeline.test.ts +406 -0
  24. package/src/__tests__/compaction-events.test.ts +501 -0
  25. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  26. package/src/__tests__/compaction-strip-metadata-clear.test.ts +181 -0
  27. package/src/__tests__/compaction-timeout-recovery.test.ts +262 -0
  28. package/src/__tests__/config-model-image-provider.test.ts +110 -0
  29. package/src/__tests__/config-schema.test.ts +22 -9
  30. package/src/__tests__/config-watcher-cleanup-throttle.test.ts +0 -4
  31. package/src/__tests__/contacts-tools.test.ts +26 -0
  32. package/src/__tests__/context-overflow-policy.test.ts +7 -7
  33. package/src/__tests__/context-window-manager.test.ts +355 -4
  34. package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
  35. package/src/__tests__/conversation-agent-loop-overflow.test.ts +26 -30
  36. package/src/__tests__/conversation-agent-loop.test.ts +30 -141
  37. package/src/__tests__/conversation-confirmation-signals.test.ts +6 -1
  38. package/src/__tests__/conversation-history-web-search.test.ts +1 -0
  39. package/src/__tests__/conversation-init.benchmark.test.ts +2 -16
  40. package/src/__tests__/conversation-pairing.test.ts +174 -10
  41. package/src/__tests__/conversation-pre-run-repair.test.ts +4 -1
  42. package/src/__tests__/conversation-process-callsite.test.ts +3 -0
  43. package/src/__tests__/conversation-provider-retry-repair.test.ts +16 -7
  44. package/src/__tests__/conversation-queue.test.ts +29 -14
  45. package/src/__tests__/conversation-routes-disk-view.test.ts +7 -6
  46. package/src/__tests__/conversation-runtime-assembly.test.ts +155 -110
  47. package/src/__tests__/conversation-runtime-workspace.test.ts +23 -38
  48. package/src/__tests__/conversation-seed-composer.test.ts +2 -2
  49. package/src/__tests__/conversation-slash-queue.test.ts +7 -2
  50. package/src/__tests__/conversation-slash-unknown.test.ts +25 -2
  51. package/src/__tests__/conversation-speed-override.test.ts +6 -1
  52. package/src/__tests__/conversation-title-service.test.ts +116 -0
  53. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +41 -2
  54. package/src/__tests__/conversation-usage.test.ts +1 -1
  55. package/src/__tests__/conversation-workspace-cache-state.test.ts +4 -1
  56. package/src/__tests__/conversation-workspace-injection.test.ts +3 -0
  57. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +4 -1
  58. package/src/__tests__/credential-health-service.test.ts +78 -9
  59. package/src/__tests__/credential-security-invariants.test.ts +2 -2
  60. package/src/__tests__/db-schedule-syntax-migration.test.ts +1 -0
  61. package/src/__tests__/empty-response-pipeline.test.ts +305 -0
  62. package/src/__tests__/extension-id-sync-guard.test.ts +3 -3
  63. package/src/__tests__/first-greeting.test.ts +247 -5
  64. package/src/__tests__/headless-browser-mode.test.ts +57 -0
  65. package/src/__tests__/history-repair-pipeline.test.ts +399 -0
  66. package/src/__tests__/host-browser-e2e-cloud.test.ts +307 -0
  67. package/src/__tests__/host-browser-e2e-self-hosted.test.ts +3 -3
  68. package/src/__tests__/host-proxy-interface.test.ts +36 -2
  69. package/src/__tests__/image-credentials.test.ts +137 -0
  70. package/src/__tests__/image-service-dispatcher.test.ts +186 -0
  71. package/src/__tests__/injector-chain.test.ts +526 -0
  72. package/src/__tests__/intent-routing.test.ts +0 -26
  73. package/src/__tests__/llm-call-pipeline.test.ts +285 -0
  74. package/src/__tests__/llm-schema.test.ts +1 -1
  75. package/src/__tests__/media-generate-image.test.ts +119 -13
  76. package/src/__tests__/memory-retrieval-pipeline.test.ts +401 -0
  77. package/src/__tests__/memory-upsert-concurrency.test.ts +1 -0
  78. package/src/__tests__/migration-import-from-url.test.ts +5 -68
  79. package/src/__tests__/model-intents.test.ts +4 -2
  80. package/src/__tests__/notification-broadcaster.test.ts +3 -3
  81. package/src/__tests__/notification-decision-strategy.test.ts +0 -11
  82. package/src/__tests__/notification-schedule-notify-dedup.test.ts +108 -0
  83. package/src/__tests__/oauth-apps-routes.test.ts +1 -1
  84. package/src/__tests__/oauth-cli.test.ts +14 -12
  85. package/src/__tests__/oauth-connect-orchestrator.test.ts +4 -13
  86. package/src/__tests__/oauth-provider-serializer.test.ts +6 -4
  87. package/src/__tests__/oauth-provider-visibility.test.ts +3 -5
  88. package/src/__tests__/oauth-providers-routes.test.ts +3 -2
  89. package/src/__tests__/oauth-store.test.ts +41 -76
  90. package/src/__tests__/onboarding-template-contract.test.ts +16 -64
  91. package/src/__tests__/openai-image-service.test.ts +368 -0
  92. package/src/__tests__/overflow-reduce-pipeline.test.ts +676 -0
  93. package/src/__tests__/permission-checker-host-gate.test.ts +0 -24
  94. package/src/__tests__/persist-onboarding-artifacts.test.ts +266 -0
  95. package/src/__tests__/persistence-pipeline.test.ts +377 -0
  96. package/src/__tests__/pipeline-runner.test.ts +565 -0
  97. package/src/__tests__/platform.test.ts +5 -2
  98. package/src/__tests__/plugin-bootstrap.test.ts +483 -0
  99. package/src/__tests__/plugin-registry.test.ts +273 -0
  100. package/src/__tests__/plugin-route-contribution.test.ts +288 -0
  101. package/src/__tests__/plugin-skill-contribution.test.ts +367 -0
  102. package/src/__tests__/plugin-tool-contribution.test.ts +286 -0
  103. package/src/__tests__/plugin-types.test.ts +320 -0
  104. package/src/__tests__/pricing.test.ts +44 -12
  105. package/src/__tests__/proxy-approval-callback.test.ts +69 -8
  106. package/src/__tests__/reaction-persistence.test.ts +1 -0
  107. package/src/__tests__/regenerate-fire-and-forget-trace.test.ts +1 -0
  108. package/src/__tests__/registry.test.ts +0 -2
  109. package/src/__tests__/schedule-routes.test.ts +131 -1
  110. package/src/__tests__/scheduler-recurrence.test.ts +14 -70
  111. package/src/__tests__/scheduler-reuse-conversation.test.ts +10 -50
  112. package/src/__tests__/secret-detection-handler.test.ts +0 -10
  113. package/src/__tests__/shell-identity.test.ts +0 -134
  114. package/src/__tests__/suggestion-routes.test.ts +103 -4
  115. package/src/__tests__/task-memory-cleanup.test.ts +1 -0
  116. package/src/__tests__/task-scheduler.test.ts +3 -15
  117. package/src/__tests__/test-preload.ts +11 -0
  118. package/src/__tests__/title-generate-pipeline.test.ts +224 -0
  119. package/src/__tests__/token-estimate-pipeline.test.ts +431 -0
  120. package/src/__tests__/tool-error-pipeline.test.ts +244 -0
  121. package/src/__tests__/tool-execute-pipeline.test.ts +431 -0
  122. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -6
  123. package/src/__tests__/tool-executor-shell-integration.test.ts +7 -10
  124. package/src/__tests__/tool-executor.test.ts +141 -0
  125. package/src/__tests__/tool-result-truncate-pipeline.test.ts +356 -0
  126. package/src/__tests__/tool-result-truncation.test.ts +0 -110
  127. package/src/__tests__/user-plugin-loader.test.ts +191 -0
  128. package/src/__tests__/workspace-migration-046-seed-conversation-starters-callsite.test.ts +185 -0
  129. package/src/__tests__/workspace-migration-049-release-notes-default-sonnet.test.ts +100 -0
  130. package/src/__tests__/workspace-migration-050-seed-main-agent-opus-callsite.test.ts +171 -0
  131. package/src/__tests__/workspace-migration-051-seed-conversation-summarization-callsite.test.ts +252 -0
  132. package/src/__tests__/workspace-migration-remove-hooks.test.ts +99 -0
  133. package/src/__tests__/workspace-policy.test.ts +21 -3
  134. package/src/agent/loop.ts +340 -102
  135. package/src/approvals/__tests__/guardian-feed-event.test.ts +304 -0
  136. package/src/approvals/guardian-request-resolvers.ts +80 -0
  137. package/src/backup/__tests__/backup-worker.test.ts +2 -13
  138. package/src/backup/backup-worker.ts +3 -15
  139. package/src/bundler/app-compiler.ts +84 -1
  140. package/src/calls/call-state.ts +2 -2
  141. package/src/channels/__tests__/types.test.ts +3 -3
  142. package/src/channels/types.ts +6 -4
  143. package/src/cli/__tests__/notifications.test.ts +87 -211
  144. package/src/cli/commands/__tests__/backup.test.ts +1 -1
  145. package/src/cli/commands/__tests__/image-generation.test.ts +255 -35
  146. package/src/cli/commands/__tests__/inference-send.test.ts +12 -0
  147. package/src/cli/commands/__tests__/tts-synthesize.test.ts +12 -0
  148. package/src/cli/commands/backup.ts +2 -2
  149. package/src/cli/commands/clients.ts +138 -0
  150. package/src/cli/commands/completions.ts +2 -9
  151. package/src/cli/commands/conversations.ts +55 -7
  152. package/src/cli/commands/image-generation.ts +33 -34
  153. package/src/cli/commands/notifications.ts +68 -103
  154. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +1 -1
  155. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
  156. package/src/cli/commands/oauth/connect.ts +2 -2
  157. package/src/cli/commands/oauth/providers.ts +176 -8
  158. package/src/cli/commands/oauth/status.ts +46 -36
  159. package/src/cli/commands/skills.ts +3 -4
  160. package/src/cli/program.ts +25 -29
  161. package/src/config/__tests__/backup-schema.test.ts +7 -2
  162. package/src/config/bundled-skills/app-builder/SKILL.md +2 -2
  163. package/src/config/bundled-skills/app-builder/references/WIDGETS.md +10 -10
  164. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +66 -87
  165. package/src/config/bundled-skills/contacts/tools/contact-search.ts +28 -51
  166. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +22 -40
  167. package/src/config/bundled-skills/image-studio/SKILL.md +2 -1
  168. package/src/config/bundled-skills/image-studio/TOOLS.json +2 -1
  169. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +23 -39
  170. package/src/config/bundled-skills/messaging/SKILL.md +3 -3
  171. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +207 -0
  172. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +12 -0
  173. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +58 -0
  174. package/src/config/bundled-skills/schedule/SKILL.md +8 -3
  175. package/src/config/bundled-skills/schedule/TOOLS.json +15 -7
  176. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +59 -0
  177. package/src/config/bundled-tool-registry.ts +0 -15
  178. package/src/config/feature-flag-registry.json +17 -1
  179. package/src/config/schema.ts +19 -0
  180. package/src/config/schemas/backup.ts +1 -1
  181. package/src/config/schemas/conversations.ts +16 -0
  182. package/src/config/schemas/llm.ts +2 -3
  183. package/src/config/schemas/security.ts +6 -6
  184. package/src/config/schemas/tts.ts +11 -0
  185. package/src/config/skill-state.ts +6 -2
  186. package/src/config/skills.ts +94 -5
  187. package/src/context/__tests__/compact-prompt.test.ts +27 -9
  188. package/src/context/prompts/compact.md +26 -12
  189. package/src/context/tool-result-truncation.ts +3 -63
  190. package/src/context/window-manager.ts +190 -16
  191. package/src/credential-health/credential-health-service.ts +19 -6
  192. package/src/daemon/__tests__/conversation-feed-event.test.ts +317 -0
  193. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +4 -12
  194. package/src/daemon/__tests__/conversation-tool-setup.test.ts +14 -15
  195. package/src/daemon/config-watcher.ts +0 -2
  196. package/src/daemon/context-overflow-policy.ts +4 -13
  197. package/src/daemon/conversation-agent-loop-handlers.ts +83 -22
  198. package/src/daemon/conversation-agent-loop.ts +984 -683
  199. package/src/daemon/conversation-history.ts +10 -19
  200. package/src/daemon/conversation-lifecycle.ts +37 -19
  201. package/src/daemon/conversation-notifiers.ts +2 -110
  202. package/src/daemon/conversation-process.ts +14 -7
  203. package/src/daemon/conversation-runtime-assembly.ts +532 -411
  204. package/src/daemon/conversation-tool-setup.ts +41 -4
  205. package/src/daemon/conversation.ts +80 -35
  206. package/src/daemon/external-plugins-bootstrap.ts +478 -0
  207. package/src/daemon/first-greeting.ts +191 -14
  208. package/src/daemon/handlers/config-model.ts +11 -0
  209. package/src/daemon/handlers/skills.ts +5 -1
  210. package/src/daemon/lifecycle.ts +33 -68
  211. package/src/daemon/message-types/computer-use.ts +2 -34
  212. package/src/daemon/message-types/conversations.ts +49 -0
  213. package/src/daemon/message-types/messages.ts +12 -0
  214. package/src/daemon/server.ts +5 -3
  215. package/src/daemon/shutdown-handlers.ts +2 -12
  216. package/src/daemon/tool-side-effects.ts +14 -56
  217. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +160 -0
  218. package/src/heartbeat/heartbeat-service.ts +24 -1
  219. package/src/home/__tests__/feed-population-integration.test.ts +312 -0
  220. package/src/home/emit-feed-event.ts +7 -0
  221. package/src/home/feed-types.ts +41 -2
  222. package/src/home/rewrite-command-preview.ts +66 -0
  223. package/src/ipc/__tests__/socket-path.test.ts +11 -50
  224. package/src/ipc/cli-client.ts +1 -1
  225. package/src/ipc/cli-server.ts +3 -3
  226. package/src/ipc/gateway-client.ts +4 -1
  227. package/src/ipc/routes/browser-context.ts +2 -0
  228. package/src/ipc/routes/browser.ts +1 -0
  229. package/src/ipc/routes/get-contact.ts +16 -0
  230. package/src/ipc/routes/index.ts +14 -0
  231. package/src/ipc/routes/list-clients.ts +31 -0
  232. package/src/ipc/routes/merge-contacts.ts +17 -0
  233. package/src/ipc/routes/notification.ts +133 -0
  234. package/src/ipc/routes/rename-conversation.ts +59 -0
  235. package/src/ipc/routes/search-contacts.ts +19 -0
  236. package/src/ipc/routes/upsert-contact.ts +25 -0
  237. package/src/ipc/socket-path.ts +14 -38
  238. package/src/media/app-icon-generator.ts +23 -46
  239. package/src/media/avatar-router.ts +26 -41
  240. package/src/media/gemini-image-service.ts +8 -41
  241. package/src/media/image-credentials.ts +73 -0
  242. package/src/media/image-service.ts +85 -0
  243. package/src/media/openai-image-service.ts +131 -0
  244. package/src/media/types.ts +46 -0
  245. package/src/memory/conversation-crud.ts +48 -18
  246. package/src/memory/conversation-queries.ts +57 -4
  247. package/src/memory/conversation-title-service.ts +25 -0
  248. package/src/memory/db-init.ts +8 -0
  249. package/src/memory/embedding-gemini.test.ts +41 -2
  250. package/src/memory/embedding-gemini.ts +6 -1
  251. package/src/memory/graph/bootstrap.test.ts +282 -0
  252. package/src/memory/graph/bootstrap.ts +8 -5
  253. package/src/memory/graph/extraction.ts +10 -2
  254. package/src/memory/graph/graph-search.test.ts +1 -0
  255. package/src/memory/graph/inspect.ts +2 -2
  256. package/src/memory/graph/retriever.ts +10 -3
  257. package/src/memory/migrations/041-approval-prompt-ts-tracker.ts +26 -0
  258. package/src/memory/migrations/149-oauth-tables.ts +1 -0
  259. package/src/memory/migrations/223-schedule-script-column.ts +11 -0
  260. package/src/memory/migrations/224-oauth-providers-managed-service-is-paid.ts +24 -0
  261. package/src/memory/migrations/225-oauth-providers-available-scopes.ts +13 -0
  262. package/src/memory/migrations/index.ts +4 -0
  263. package/src/memory/pkb/pkb-index.test.ts +1 -0
  264. package/src/memory/pkb/pkb-reconcile.test.ts +1 -0
  265. package/src/memory/pkb/pkb-search.test.ts +65 -4
  266. package/src/memory/pkb/pkb-search.ts +40 -18
  267. package/src/memory/qdrant-client.test.ts +60 -0
  268. package/src/memory/qdrant-client.ts +25 -0
  269. package/src/memory/schema/infrastructure.ts +1 -0
  270. package/src/memory/schema/oauth.ts +4 -1
  271. package/src/messaging/providers/slack/render-transcript.test.ts +77 -29
  272. package/src/messaging/providers/slack/render-transcript.ts +58 -0
  273. package/src/notifications/conversation-pairing.ts +78 -19
  274. package/src/notifications/copy-composer.ts +0 -5
  275. package/src/notifications/emit-signal.ts +1 -1
  276. package/src/notifications/signal.ts +1 -2
  277. package/src/oauth/AGENTS.md +1 -1
  278. package/src/oauth/__tests__/identity-verifier.test.ts +2 -1
  279. package/src/oauth/connect-orchestrator.ts +8 -34
  280. package/src/oauth/connect-types.ts +6 -10
  281. package/src/oauth/manual-token-connection.ts +23 -0
  282. package/src/oauth/oauth-store.ts +30 -14
  283. package/src/oauth/provider-serializer.ts +6 -1
  284. package/src/oauth/seed-providers.ts +56 -108
  285. package/src/outbound-proxy/http-forwarder.ts +9 -0
  286. package/src/permissions/approval-policy.test.ts +293 -18
  287. package/src/permissions/approval-policy.ts +110 -58
  288. package/src/permissions/arg-parser.test.ts +161 -0
  289. package/src/permissions/arg-parser.ts +141 -0
  290. package/src/permissions/bash-risk-classifier.test.ts +414 -2
  291. package/src/permissions/bash-risk-classifier.ts +303 -60
  292. package/src/permissions/checker.ts +157 -29
  293. package/src/permissions/command-registry.test.ts +239 -0
  294. package/src/permissions/command-registry.ts +234 -54
  295. package/src/permissions/defaults.ts +5 -4
  296. package/src/permissions/gateway-threshold-reader.ts +196 -0
  297. package/src/permissions/prompter.ts +4 -0
  298. package/src/permissions/risk-types.ts +61 -4
  299. package/src/permissions/schedule-risk-classifier.test.ts +129 -0
  300. package/src/permissions/schedule-risk-classifier.ts +85 -0
  301. package/src/permissions/shell-identity.ts +2 -42
  302. package/src/permissions/types.ts +2 -0
  303. package/src/permissions/workspace-policy.ts +8 -3
  304. package/src/plugins/defaults/circuit-breaker.ts +146 -0
  305. package/src/plugins/defaults/compaction.ts +145 -0
  306. package/src/plugins/defaults/empty-response.ts +126 -0
  307. package/src/plugins/defaults/history-repair.ts +85 -0
  308. package/src/plugins/defaults/index.ts +116 -0
  309. package/src/plugins/defaults/injectors.ts +491 -0
  310. package/src/plugins/defaults/llm-call.ts +82 -0
  311. package/src/plugins/defaults/memory-retrieval.ts +226 -0
  312. package/src/plugins/defaults/overflow-reduce.ts +181 -0
  313. package/src/plugins/defaults/persistence.ts +129 -0
  314. package/src/plugins/defaults/title-generate.ts +95 -0
  315. package/src/plugins/defaults/token-estimate.ts +104 -0
  316. package/src/plugins/defaults/tool-error.ts +126 -0
  317. package/src/plugins/defaults/tool-execute.ts +89 -0
  318. package/src/plugins/defaults/tool-result-truncate.ts +88 -0
  319. package/src/plugins/pipeline.ts +316 -0
  320. package/src/plugins/plugin-skill-contributions.ts +292 -0
  321. package/src/plugins/registry.ts +241 -0
  322. package/src/plugins/types.ts +1134 -0
  323. package/src/plugins/user-loader.ts +177 -0
  324. package/src/prompts/templates/BOOTSTRAP.md +27 -77
  325. package/src/providers/model-catalog.ts +52 -29
  326. package/src/providers/model-intents.ts +1 -1
  327. package/src/providers/openrouter/client.ts +5 -1
  328. package/src/providers/speech-to-text/deepgram-realtime.test.ts +61 -0
  329. package/src/providers/speech-to-text/deepgram-realtime.ts +57 -0
  330. package/src/providers/speech-to-text/xai-realtime.test.ts +72 -4
  331. package/src/providers/speech-to-text/xai-realtime.ts +39 -14
  332. package/src/runtime/AGENTS.md +25 -16
  333. package/src/runtime/__tests__/browser-extension-pair-routes.test.ts +3 -3
  334. package/src/runtime/__tests__/client-registry.test.ts +293 -0
  335. package/src/runtime/client-registry.ts +261 -0
  336. package/src/runtime/http-server.ts +77 -8
  337. package/src/runtime/http-types.ts +0 -2
  338. package/src/runtime/migrations/vbundle-builder.ts +1 -22
  339. package/src/runtime/routes/approval-prompt-ts-tracker.ts +51 -31
  340. package/src/runtime/routes/approval-routes.ts +17 -0
  341. package/src/runtime/routes/browser-extension-pair-routes.ts +27 -8
  342. package/src/runtime/routes/conversation-routes.ts +223 -116
  343. package/src/runtime/routes/inbound-message-handler.ts +88 -13
  344. package/src/runtime/routes/memory-item-routes.test.ts +1 -0
  345. package/src/runtime/routes/migration-routes.ts +0 -3
  346. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +284 -0
  347. package/src/runtime/routes/playground/__tests__/guard.test.ts +80 -0
  348. package/src/runtime/routes/playground/__tests__/inject-failures.test.ts +294 -0
  349. package/src/runtime/routes/playground/__tests__/reset-circuit.test.ts +271 -0
  350. package/src/runtime/routes/playground/__tests__/seed-conversation.test.ts +202 -0
  351. package/src/runtime/routes/playground/__tests__/seeded-conversations.test.ts +309 -0
  352. package/src/runtime/routes/playground/__tests__/state.test.ts +224 -0
  353. package/src/runtime/routes/playground/conversation-not-found.ts +29 -0
  354. package/src/runtime/routes/playground/deps.ts +56 -0
  355. package/src/runtime/routes/playground/force-compact.ts +73 -0
  356. package/src/runtime/routes/playground/guard.ts +37 -0
  357. package/src/runtime/routes/playground/index.ts +28 -0
  358. package/src/runtime/routes/playground/inject-failures.ts +159 -0
  359. package/src/runtime/routes/playground/reset-circuit.ts +115 -0
  360. package/src/runtime/routes/playground/seed-conversation.ts +139 -0
  361. package/src/runtime/routes/playground/seeded-conversations.ts +78 -0
  362. package/src/runtime/routes/playground/state.ts +78 -0
  363. package/src/runtime/routes/schedule-routes.ts +89 -8
  364. package/src/runtime/skill-route-registry.ts +75 -15
  365. package/src/schedule/run-script.ts +68 -0
  366. package/src/schedule/schedule-store.ts +7 -1
  367. package/src/schedule/scheduler.ts +48 -8
  368. package/src/skills/catalog-cache.ts +12 -5
  369. package/src/tools/browser/__tests__/browser-status.test.ts +189 -0
  370. package/src/tools/browser/browser-execution.ts +88 -19
  371. package/src/tools/browser/cdp-client/__tests__/extension-cdp-client.test.ts +230 -0
  372. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +146 -3
  373. package/src/tools/browser/cdp-client/extension-cdp-client.ts +54 -3
  374. package/src/tools/browser/cdp-client/factory.ts +15 -4
  375. package/src/tools/executor.ts +126 -74
  376. package/src/tools/network/script-proxy/session-manager.ts +37 -1
  377. package/src/tools/permission-checker.ts +98 -49
  378. package/src/tools/policy-context.ts +4 -0
  379. package/src/tools/registry.ts +140 -3
  380. package/src/tools/schedule/create.ts +23 -8
  381. package/src/tools/schedule/update.ts +3 -1
  382. package/src/tools/secret-detection-handler.ts +0 -51
  383. package/src/tools/system/avatar-generator.ts +6 -2
  384. package/src/tools/types.ts +28 -2
  385. package/src/util/platform.ts +7 -2
  386. package/src/util/pricing.ts +26 -3
  387. package/src/workspace/migrations/006-services-config.ts +2 -4
  388. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +2 -3
  389. package/src/workspace/migrations/041-backfill-google-gmail-settings-scope.ts +3 -4
  390. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +108 -0
  391. package/src/workspace/migrations/047-remove-watch-callsites.ts +54 -0
  392. package/src/workspace/migrations/048-remove-workspace-hooks.ts +81 -0
  393. package/src/workspace/migrations/049-release-notes-default-sonnet.ts +80 -0
  394. package/src/workspace/migrations/050-seed-main-agent-opus-callsite.ts +86 -0
  395. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +128 -0
  396. package/src/workspace/migrations/registry.ts +12 -0
  397. package/tsconfig.json +1 -1
  398. package/hook-templates/debug-prompt-logger/hook.json +0 -7
  399. package/hook-templates/debug-prompt-logger/run.sh +0 -66
  400. package/src/__tests__/compaction-circuit-breaker.test.ts +0 -336
  401. package/src/__tests__/context-overflow-approval.test.ts +0 -156
  402. package/src/__tests__/hooks-blocking.test.ts +0 -178
  403. package/src/__tests__/hooks-cli.test.ts +0 -182
  404. package/src/__tests__/hooks-config.test.ts +0 -108
  405. package/src/__tests__/hooks-discovery.test.ts +0 -211
  406. package/src/__tests__/hooks-integration.test.ts +0 -196
  407. package/src/__tests__/hooks-manager.test.ts +0 -226
  408. package/src/__tests__/hooks-runner.test.ts +0 -175
  409. package/src/__tests__/hooks-settings.test.ts +0 -160
  410. package/src/__tests__/hooks-templates.test.ts +0 -169
  411. package/src/__tests__/hooks-ts-runner.test.ts +0 -170
  412. package/src/__tests__/hooks-watch.test.ts +0 -112
  413. package/src/__tests__/notification-schedule-dedup.test.ts +0 -213
  414. package/src/__tests__/oauth-scope-policy.test.ts +0 -180
  415. package/src/__tests__/send-notification-tool.test.ts +0 -83
  416. package/src/cli/commands/shotgun.ts +0 -266
  417. package/src/config/bundled-skills/conversations/SKILL.md +0 -20
  418. package/src/config/bundled-skills/conversations/TOOLS.json +0 -23
  419. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +0 -88
  420. package/src/config/bundled-skills/heartbeat/SKILL.md +0 -43
  421. package/src/config/bundled-skills/notifications/SKILL.md +0 -40
  422. package/src/config/bundled-skills/notifications/TOOLS.json +0 -80
  423. package/src/config/bundled-skills/notifications/tools/send-notification.ts +0 -152
  424. package/src/config/bundled-skills/notifications/tools/shared.ts +0 -13
  425. package/src/config/bundled-skills/screen-watch/SKILL.md +0 -27
  426. package/src/config/bundled-skills/screen-watch/TOOLS.json +0 -35
  427. package/src/config/bundled-skills/screen-watch/tools/start-screen-watch.ts +0 -12
  428. package/src/config/bundled-skills/skills-catalog/SKILL.md +0 -84
  429. package/src/daemon/context-overflow-approval.ts +0 -52
  430. package/src/daemon/watch-handler.ts +0 -399
  431. package/src/hooks/cli.ts +0 -253
  432. package/src/hooks/config.ts +0 -100
  433. package/src/hooks/discovery.ts +0 -135
  434. package/src/hooks/manager.ts +0 -179
  435. package/src/hooks/runner.ts +0 -117
  436. package/src/hooks/templates.ts +0 -77
  437. package/src/hooks/types.ts +0 -75
  438. package/src/oauth/scope-policy.ts +0 -89
  439. package/src/runtime/gateway-internal-client.ts +0 -94
  440. package/src/runtime/routes/watch-routes.ts +0 -156
  441. package/src/signals/shotgun.ts +0 -203
  442. package/src/tools/watch/screen-watch.ts +0 -144
  443. package/src/tools/watch/watch-state.ts +0 -142
@@ -11,6 +11,7 @@
11
11
  * - Edit mode with --source (source images passed through)
12
12
  * - --variants passed through
13
13
  * - --model override
14
+ * - Provider dispatch (gemini vs openai)
14
15
  */
15
16
 
16
17
  import { existsSync, readFileSync } from "node:fs";
@@ -29,15 +30,12 @@ let mockConfig = {
29
30
  services: {
30
31
  "image-generation": {
31
32
  mode: "your-own" as "managed" | "your-own",
32
- provider: "gemini" as const,
33
+ provider: "gemini" as "gemini" | "openai",
33
34
  model: "gemini-3.1-flash-image-preview",
34
35
  },
35
36
  },
36
37
  };
37
38
 
38
- /** Result returned by buildManagedBaseUrl() */
39
- let mockManagedBaseUrl: string | undefined = undefined;
40
-
41
39
  /** Context returned by resolveManagedProxyContext() */
42
40
  let mockManagedProxyContext = {
43
41
  enabled: false,
@@ -45,8 +43,8 @@ let mockManagedProxyContext = {
45
43
  assistantApiKey: "",
46
44
  };
47
45
 
48
- /** Key returned by getProviderKeyAsync() */
49
- let mockProviderKey: string | undefined = undefined;
46
+ /** Key returned by getProviderKeyAsync(). Keyed by provider. */
47
+ let mockProviderKeys: Record<string, string | undefined> = {};
50
48
 
51
49
  /** Result returned by generateImage() */
52
50
  let mockGenerateResult: {
@@ -68,6 +66,7 @@ let mockGenerateError: Error | undefined = undefined;
68
66
 
69
67
  /** Captured generateImage call args */
70
68
  let lastGenerateCall: {
69
+ provider: unknown;
71
70
  credentials: unknown;
72
71
  request: unknown;
73
72
  } | null = null;
@@ -82,24 +81,35 @@ mock.module("../../../config/loader.js", () => ({
82
81
  }));
83
82
 
84
83
  mock.module("../../../providers/managed-proxy/context.js", () => ({
85
- buildManagedBaseUrl: async () => mockManagedBaseUrl,
86
84
  resolveManagedProxyContext: async () => mockManagedProxyContext,
87
85
  }));
88
86
 
89
87
  mock.module("../../../security/secure-keys.js", () => ({
90
- getProviderKeyAsync: async () => mockProviderKey,
88
+ getProviderKeyAsync: async (provider: string) => mockProviderKeys[provider],
91
89
  }));
92
90
 
93
- mock.module("../../../media/gemini-image-service.js", () => ({
94
- generateImage: async (credentials: unknown, request: unknown) => {
95
- lastGenerateCall = { credentials, request };
91
+ mock.module("../../../media/image-service.js", () => ({
92
+ generateImage: async (
93
+ provider: unknown,
94
+ credentials: unknown,
95
+ request: unknown,
96
+ ) => {
97
+ lastGenerateCall = { provider, credentials, request };
96
98
  if (mockGenerateError) throw mockGenerateError;
97
99
  return mockGenerateResult;
98
100
  },
99
- mapGeminiError: (error: unknown) => {
100
- if (error instanceof Error) return `Mapped error: ${error.message}`;
101
+ mapImageGenError: (provider: unknown, error: unknown) => {
102
+ if (error instanceof Error)
103
+ return `Mapped error (${String(provider)}): ${error.message}`;
101
104
  return "An unexpected error occurred during image generation.";
102
105
  },
106
+ providerForModel: (model: string | undefined, fallback: string) => {
107
+ if (!model) return fallback;
108
+ if (model.startsWith("gpt-") || model.startsWith("dall-e-"))
109
+ return "openai";
110
+ if (model.startsWith("gemini-")) return "gemini";
111
+ return fallback;
112
+ },
103
113
  }));
104
114
 
105
115
  mock.module("../../../util/logger.js", () => ({
@@ -188,13 +198,12 @@ beforeEach(() => {
188
198
  },
189
199
  },
190
200
  };
191
- mockManagedBaseUrl = undefined;
192
201
  mockManagedProxyContext = {
193
202
  enabled: false,
194
203
  platformBaseUrl: "",
195
204
  assistantApiKey: "",
196
205
  };
197
- mockProviderKey = undefined;
206
+ mockProviderKeys = {};
198
207
  mockGenerateResult = {
199
208
  images: [
200
209
  {
@@ -221,6 +230,10 @@ describe("help text", () => {
221
230
  expect(stdout).toContain("your-own");
222
231
  expect(stdout).toContain("Examples:");
223
232
  expect(stdout).toContain("generate");
233
+ // Both providers mentioned and gpt-image-2 listed as a supported model.
234
+ expect(stdout).toContain("Gemini");
235
+ expect(stdout).toContain("OpenAI");
236
+ expect(stdout).toContain("gpt-image-2");
224
237
  });
225
238
 
226
239
  test("image-generation generate --help renders argument docs and examples", async () => {
@@ -245,7 +258,7 @@ describe("help text", () => {
245
258
 
246
259
  describe("required arguments", () => {
247
260
  test("generate requires --prompt", async () => {
248
- mockProviderKey = "test-key";
261
+ mockProviderKeys.gemini = "test-key";
249
262
  const { exitCode } = await runCommand(["image-generation", "generate"]);
250
263
  expect(exitCode).not.toBe(0);
251
264
  });
@@ -258,7 +271,7 @@ describe("required arguments", () => {
258
271
  describe("credential errors", () => {
259
272
  test("exits with code 1 when no credentials in your-own mode", async () => {
260
273
  mockConfig.services["image-generation"].mode = "your-own";
261
- mockProviderKey = undefined;
274
+ mockProviderKeys.gemini = undefined;
262
275
 
263
276
  const { exitCode } = await runCommand([
264
277
  "image-generation",
@@ -271,7 +284,6 @@ describe("credential errors", () => {
271
284
 
272
285
  test("exits with code 1 when no credentials in managed mode", async () => {
273
286
  mockConfig.services["image-generation"].mode = "managed";
274
- mockManagedBaseUrl = undefined;
275
287
 
276
288
  const { exitCode } = await runCommand([
277
289
  "image-generation",
@@ -282,9 +294,10 @@ describe("credential errors", () => {
282
294
  expect(exitCode).toBe(1);
283
295
  });
284
296
 
285
- test("--json outputs error when no credentials in your-own mode", async () => {
297
+ test("--json outputs error when no credentials in your-own mode (gemini)", async () => {
286
298
  mockConfig.services["image-generation"].mode = "your-own";
287
- mockProviderKey = undefined;
299
+ mockConfig.services["image-generation"].provider = "gemini";
300
+ mockProviderKeys.gemini = undefined;
288
301
 
289
302
  const { exitCode, stdout } = await runCommand([
290
303
  "image-generation",
@@ -297,11 +310,34 @@ describe("credential errors", () => {
297
310
  const parsed = JSON.parse(stdout.trim());
298
311
  expect(parsed.ok).toBe(false);
299
312
  expect(parsed.error).toContain("Gemini API key");
313
+ // CLI prepends the actionable `assistant keys set` command with the
314
+ // resolved provider — the shared hint is UI-neutral ("Settings") so
315
+ // without this prefix CLI users get no copy-pasteable next step.
316
+ expect(parsed.error).toContain("Run: assistant keys set gemini <key>");
317
+ });
318
+
319
+ test("--json outputs OpenAI-specific hint when provider=openai and no key set", async () => {
320
+ mockConfig.services["image-generation"].mode = "your-own";
321
+ mockConfig.services["image-generation"].provider = "openai";
322
+ mockConfig.services["image-generation"].model = "gpt-image-2";
323
+ mockProviderKeys.openai = undefined;
324
+
325
+ const { exitCode, stdout } = await runCommand([
326
+ "image-generation",
327
+ "generate",
328
+ "--prompt",
329
+ "A sunset",
330
+ "--json",
331
+ ]);
332
+ expect(exitCode).toBe(1);
333
+ const parsed = JSON.parse(stdout.trim());
334
+ expect(parsed.ok).toBe(false);
335
+ expect(parsed.error).toContain("OpenAI API key");
336
+ expect(parsed.error).toContain("Run: assistant keys set openai <key>");
300
337
  });
301
338
 
302
339
  test("--json outputs error when no credentials in managed mode", async () => {
303
340
  mockConfig.services["image-generation"].mode = "managed";
304
- mockManagedBaseUrl = undefined;
305
341
 
306
342
  const { exitCode, stdout } = await runCommand([
307
343
  "image-generation",
@@ -313,7 +349,39 @@ describe("credential errors", () => {
313
349
  expect(exitCode).toBe(1);
314
350
  const parsed = JSON.parse(stdout.trim());
315
351
  expect(parsed.ok).toBe(false);
352
+ // Base hint from image-credentials.ts is preserved.
316
353
  expect(parsed.error).toContain("Managed proxy");
354
+ // CLI augments the hint with CLI-specific recovery guidance so users
355
+ // know how to resolve the problem from the CLI (the shared hint is
356
+ // tool-flavored and only mentions the Vellum app).
357
+ expect(parsed.error).toContain("assistant auth login");
358
+ expect(parsed.error).toContain(
359
+ "services.image-generation.mode to 'your-own'",
360
+ );
361
+ });
362
+
363
+ test("your-own mode hint is NOT augmented with CLI auth guidance", async () => {
364
+ // The CLI-specific `assistant auth login` guidance only makes sense when
365
+ // the user is trying to use managed mode. In your-own mode, the shared
366
+ // hint (pointing at Settings > Models & Services for the API key) is
367
+ // already actionable from the CLI perspective (the key is in secure
368
+ // storage, the user just hasn't set it). Augmenting here would confuse
369
+ // the user into thinking they need to authenticate to Vellum.
370
+ mockConfig.services["image-generation"].mode = "your-own";
371
+ mockConfig.services["image-generation"].provider = "gemini";
372
+ mockProviderKeys.gemini = undefined;
373
+
374
+ const { exitCode, stdout } = await runCommand([
375
+ "image-generation",
376
+ "generate",
377
+ "--prompt",
378
+ "A sunset",
379
+ "--json",
380
+ ]);
381
+ expect(exitCode).toBe(1);
382
+ const parsed = JSON.parse(stdout.trim());
383
+ expect(parsed.ok).toBe(false);
384
+ expect(parsed.error).not.toContain("assistant auth login");
317
385
  });
318
386
  });
319
387
 
@@ -324,7 +392,7 @@ describe("credential errors", () => {
324
392
  describe("credential resolution", () => {
325
393
  test("your-own mode uses getProviderKeyAsync for direct credentials", async () => {
326
394
  mockConfig.services["image-generation"].mode = "your-own";
327
- mockProviderKey = "test-gemini-key";
395
+ mockProviderKeys.gemini = "test-gemini-key";
328
396
 
329
397
  await runCommand([
330
398
  "image-generation",
@@ -344,9 +412,8 @@ describe("credential resolution", () => {
344
412
  expect(creds.apiKey).toBe("test-gemini-key");
345
413
  });
346
414
 
347
- test("managed mode uses buildManagedBaseUrl + resolveManagedProxyContext", async () => {
415
+ test("managed mode uses resolveManagedProxyContext", async () => {
348
416
  mockConfig.services["image-generation"].mode = "managed";
349
- mockManagedBaseUrl = "https://platform.example.com/proxy/gemini";
350
417
  mockManagedProxyContext = {
351
418
  enabled: true,
352
419
  platformBaseUrl: "https://platform.example.com",
@@ -370,7 +437,160 @@ describe("credential resolution", () => {
370
437
  };
371
438
  expect(creds.type).toBe("managed-proxy");
372
439
  expect(creds.assistantApiKey).toBe("managed-api-key");
373
- expect(creds.baseUrl).toBe("https://platform.example.com/proxy/gemini");
440
+ expect(creds.baseUrl).toBe(
441
+ "https://platform.example.com/v1/runtime-proxy/gemini",
442
+ );
443
+ });
444
+ });
445
+
446
+ // ---------------------------------------------------------------------------
447
+ // Provider dispatch
448
+ // ---------------------------------------------------------------------------
449
+
450
+ describe("provider dispatch", () => {
451
+ test("provider=gemini is forwarded to the dispatcher", async () => {
452
+ mockConfig.services["image-generation"].mode = "your-own";
453
+ mockConfig.services["image-generation"].provider = "gemini";
454
+ mockProviderKeys.gemini = "test-gemini-key";
455
+ const outDir = join(os.tmpdir(), `img-dispatch-gemini-${Date.now()}`);
456
+
457
+ const { exitCode } = await runCommand([
458
+ "image-generation",
459
+ "generate",
460
+ "--prompt",
461
+ "Test",
462
+ "--output-dir",
463
+ outDir,
464
+ ]);
465
+
466
+ expect(exitCode).toBe(0);
467
+ expect(lastGenerateCall).toBeDefined();
468
+ expect(lastGenerateCall!.provider).toBe("gemini");
469
+ });
470
+
471
+ test("provider=openai with --model gpt-image-2 is forwarded to the dispatcher", async () => {
472
+ mockConfig.services["image-generation"].mode = "your-own";
473
+ mockConfig.services["image-generation"].provider = "openai";
474
+ mockConfig.services["image-generation"].model = "gpt-image-2";
475
+ mockProviderKeys.openai = "test-openai-key";
476
+ mockGenerateResult = {
477
+ images: [
478
+ {
479
+ mimeType: "image/png",
480
+ dataBase64: Buffer.from("fake-png-data").toString("base64"),
481
+ },
482
+ ],
483
+ resolvedModel: "gpt-image-2",
484
+ };
485
+ const outDir = join(os.tmpdir(), `img-dispatch-openai-${Date.now()}`);
486
+
487
+ const { exitCode } = await runCommand([
488
+ "image-generation",
489
+ "generate",
490
+ "--prompt",
491
+ "Test",
492
+ "--model",
493
+ "gpt-image-2",
494
+ "--output-dir",
495
+ outDir,
496
+ ]);
497
+
498
+ expect(exitCode).toBe(0);
499
+ expect(lastGenerateCall).toBeDefined();
500
+ expect(lastGenerateCall!.provider).toBe("openai");
501
+ const req = lastGenerateCall!.request as { model: string };
502
+ expect(req.model).toBe("gpt-image-2");
503
+ const creds = lastGenerateCall!.credentials as {
504
+ type: string;
505
+ apiKey: string;
506
+ };
507
+ expect(creds.type).toBe("direct");
508
+ expect(creds.apiKey).toBe("test-openai-key");
509
+ });
510
+
511
+ test("cross-provider override: config=gemini + --model gpt-image-2 dispatches to openai", async () => {
512
+ // Config still points at gemini (the user's Settings default), but the
513
+ // CLI caller explicitly picks gpt-image-2. The command must dispatch to
514
+ // OpenAI and resolve OpenAI credentials, not fall back to Gemini's
515
+ // default model.
516
+ mockConfig.services["image-generation"].mode = "your-own";
517
+ mockConfig.services["image-generation"].provider = "gemini";
518
+ mockConfig.services["image-generation"].model =
519
+ "gemini-3.1-flash-image-preview";
520
+ mockProviderKeys.openai = "test-openai-key";
521
+ mockGenerateResult = {
522
+ images: [
523
+ {
524
+ mimeType: "image/png",
525
+ dataBase64: Buffer.from("fake-png-data").toString("base64"),
526
+ },
527
+ ],
528
+ resolvedModel: "gpt-image-2",
529
+ };
530
+ const outDir = join(os.tmpdir(), `img-cross-openai-${Date.now()}`);
531
+
532
+ const { exitCode } = await runCommand([
533
+ "image-generation",
534
+ "generate",
535
+ "--prompt",
536
+ "Test",
537
+ "--model",
538
+ "gpt-image-2",
539
+ "--output-dir",
540
+ outDir,
541
+ ]);
542
+
543
+ expect(exitCode).toBe(0);
544
+ expect(lastGenerateCall).toBeDefined();
545
+ expect(lastGenerateCall!.provider).toBe("openai");
546
+ const req = lastGenerateCall!.request as { model: string };
547
+ expect(req.model).toBe("gpt-image-2");
548
+ const creds = lastGenerateCall!.credentials as {
549
+ type: string;
550
+ apiKey: string;
551
+ };
552
+ expect(creds.type).toBe("direct");
553
+ expect(creds.apiKey).toBe("test-openai-key");
554
+ });
555
+
556
+ test("cross-provider override: config=openai + --model gemini-3-pro-image-preview dispatches to gemini", async () => {
557
+ mockConfig.services["image-generation"].mode = "your-own";
558
+ mockConfig.services["image-generation"].provider = "openai";
559
+ mockConfig.services["image-generation"].model = "gpt-image-2";
560
+ mockProviderKeys.gemini = "test-gemini-key";
561
+ mockGenerateResult = {
562
+ images: [
563
+ {
564
+ mimeType: "image/png",
565
+ dataBase64: Buffer.from("fake-png-data").toString("base64"),
566
+ },
567
+ ],
568
+ resolvedModel: "gemini-3-pro-image-preview",
569
+ };
570
+ const outDir = join(os.tmpdir(), `img-cross-gemini-${Date.now()}`);
571
+
572
+ const { exitCode } = await runCommand([
573
+ "image-generation",
574
+ "generate",
575
+ "--prompt",
576
+ "Test",
577
+ "--model",
578
+ "gemini-3-pro-image-preview",
579
+ "--output-dir",
580
+ outDir,
581
+ ]);
582
+
583
+ expect(exitCode).toBe(0);
584
+ expect(lastGenerateCall).toBeDefined();
585
+ expect(lastGenerateCall!.provider).toBe("gemini");
586
+ const req = lastGenerateCall!.request as { model: string };
587
+ expect(req.model).toBe("gemini-3-pro-image-preview");
588
+ const creds = lastGenerateCall!.credentials as {
589
+ type: string;
590
+ apiKey: string;
591
+ };
592
+ expect(creds.type).toBe("direct");
593
+ expect(creds.apiKey).toBe("test-gemini-key");
374
594
  });
375
595
  });
376
596
 
@@ -380,7 +600,7 @@ describe("credential resolution", () => {
380
600
 
381
601
  describe("generate mode", () => {
382
602
  test("generates image and prints file path to stdout", async () => {
383
- mockProviderKey = "test-key";
603
+ mockProviderKeys.gemini = "test-key";
384
604
  const outDir = join(os.tmpdir(), `img-gen-test-${Date.now()}`);
385
605
 
386
606
  const { exitCode, stdout } = await runCommand([
@@ -407,7 +627,7 @@ describe("generate mode", () => {
407
627
  });
408
628
 
409
629
  test("--json produces structured output with paths, MIME types, and sizes", async () => {
410
- mockProviderKey = "test-key";
630
+ mockProviderKeys.gemini = "test-key";
411
631
  mockGenerateResult = {
412
632
  images: [
413
633
  {
@@ -449,7 +669,7 @@ describe("generate mode", () => {
449
669
 
450
670
  describe("edit mode", () => {
451
671
  test("exits with code 1 when --mode edit is used without --source", async () => {
452
- mockProviderKey = "test-key";
672
+ mockProviderKeys.gemini = "test-key";
453
673
 
454
674
  const { exitCode, stdout } = await runCommand([
455
675
  "image-generation",
@@ -470,7 +690,7 @@ describe("edit mode", () => {
470
690
  });
471
691
 
472
692
  test("passes source images to generateImage in edit mode", async () => {
473
- mockProviderKey = "test-key";
693
+ mockProviderKeys.gemini = "test-key";
474
694
 
475
695
  // Use a real temp file for the source image
476
696
  const sourceDir = join(os.tmpdir(), `img-src-test-${Date.now()}`);
@@ -515,7 +735,7 @@ describe("edit mode", () => {
515
735
 
516
736
  describe("variants", () => {
517
737
  test("non-numeric --variants defaults to 1", async () => {
518
- mockProviderKey = "test-key";
738
+ mockProviderKeys.gemini = "test-key";
519
739
  const outDir = join(os.tmpdir(), `img-nan-variants-${Date.now()}`);
520
740
 
521
741
  const { exitCode } = await runCommand([
@@ -536,7 +756,7 @@ describe("variants", () => {
536
756
  });
537
757
 
538
758
  test("--variants is passed through to generateImage", async () => {
539
- mockProviderKey = "test-key";
759
+ mockProviderKeys.gemini = "test-key";
540
760
  mockGenerateResult = {
541
761
  images: [
542
762
  {
@@ -587,7 +807,7 @@ describe("variants", () => {
587
807
 
588
808
  describe("model override", () => {
589
809
  test("--model overrides config model", async () => {
590
- mockProviderKey = "test-key";
810
+ mockProviderKeys.gemini = "test-key";
591
811
  const outDir = join(os.tmpdir(), `img-model-test-${Date.now()}`);
592
812
 
593
813
  await runCommand([
@@ -607,7 +827,7 @@ describe("model override", () => {
607
827
  });
608
828
 
609
829
  test("falls back to config model when --model is not provided", async () => {
610
- mockProviderKey = "test-key";
830
+ mockProviderKeys.gemini = "test-key";
611
831
  mockConfig.services["image-generation"].model =
612
832
  "gemini-3.1-flash-image-preview";
613
833
  const outDir = join(os.tmpdir(), `img-model-fallback-${Date.now()}`);
@@ -633,7 +853,7 @@ describe("model override", () => {
633
853
 
634
854
  describe("error handling", () => {
635
855
  test("maps generateImage error and exits with code 1", async () => {
636
- mockProviderKey = "test-key";
856
+ mockProviderKeys.gemini = "test-key";
637
857
  mockGenerateError = new Error("API rate limit exceeded");
638
858
 
639
859
  const { exitCode } = await runCommand([
@@ -647,7 +867,7 @@ describe("error handling", () => {
647
867
  });
648
868
 
649
869
  test("--json outputs mapped error on generateImage failure", async () => {
650
- mockProviderKey = "test-key";
870
+ mockProviderKeys.gemini = "test-key";
651
871
  mockGenerateError = new Error("Connection timeout");
652
872
 
653
873
  const { exitCode, stdout } = await runCommand([
@@ -138,6 +138,14 @@ async function runCommand(
138
138
  const stdoutChunks: string[] = [];
139
139
  const stderrChunks: string[] = [];
140
140
 
141
+ // Mock isTTY to undefined so the stdin fallback path is reachable even
142
+ // when tests run from an interactive terminal (where isTTY === true).
143
+ const originalIsTTY = process.stdin.isTTY;
144
+ Object.defineProperty(process.stdin, "isTTY", {
145
+ value: undefined,
146
+ configurable: true,
147
+ });
148
+
141
149
  process.stdout.write = ((chunk: unknown) => {
142
150
  stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
143
151
  return true;
@@ -164,6 +172,10 @@ async function runCommand(
164
172
  } finally {
165
173
  process.stdout.write = originalStdoutWrite;
166
174
  process.stderr.write = originalStderrWrite;
175
+ Object.defineProperty(process.stdin, "isTTY", {
176
+ value: originalIsTTY,
177
+ configurable: true,
178
+ });
167
179
  }
168
180
 
169
181
  const exitCode = process.exitCode ?? 0;
@@ -128,6 +128,14 @@ async function runCommand(
128
128
  const stdoutChunks: string[] = [];
129
129
  const stderrChunks: string[] = [];
130
130
 
131
+ // Mock isTTY to undefined so the stdin fallback path is reachable even
132
+ // when tests run from an interactive terminal (where isTTY === true).
133
+ const originalIsTTY = process.stdin.isTTY;
134
+ Object.defineProperty(process.stdin, "isTTY", {
135
+ value: undefined,
136
+ configurable: true,
137
+ });
138
+
131
139
  process.stdout.write = ((chunk: unknown) => {
132
140
  stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
133
141
  return true;
@@ -161,6 +169,10 @@ async function runCommand(
161
169
  process.stdout.write = originalStdoutWrite;
162
170
  process.stderr.write = originalStderrWrite;
163
171
  process.exit = originalExit;
172
+ Object.defineProperty(process.stdin, "isTTY", {
173
+ value: originalIsTTY,
174
+ configurable: true,
175
+ });
164
176
  }
165
177
 
166
178
  const exitCode = process.exitCode ?? 0;
@@ -699,7 +699,7 @@ plaintext — plaintext only makes sense when the user owns physical access to
699
699
  the medium (e.g. an external SSD).
700
700
 
701
701
  Examples:
702
- $ vellum backup enable --interval 6 --retention 7
702
+ $ vellum backup enable --interval 6 --retention 3
703
703
  $ vellum backup destinations add /Volumes/BackupSSD/vellum --plaintext
704
704
  $ vellum backup status
705
705
  $ vellum backup list
@@ -717,7 +717,7 @@ Examples:
717
717
  )
718
718
  .option(
719
719
  "--retention <n>",
720
- "Snapshots to retain per destination (1-100). Defaults to 7.",
720
+ "Snapshots to retain per destination (1-100). Defaults to 3.",
721
721
  )
722
722
  .option(
723
723
  "--no-offsite",