@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
@@ -0,0 +1,285 @@
1
+ /**
2
+ * Unit tests for the `llmCall` pipeline wrapping (PR 15).
3
+ *
4
+ * Exercises the three behaviors the plan calls out:
5
+ *
6
+ * 1. The default `llmCall` pipeline delegates to `provider.sendMessage(...)`
7
+ * and returns its response unchanged.
8
+ * 2. A spy middleware registered for `llmCall` observes the full argument
9
+ * payload before the provider is called.
10
+ * 3. A short-circuit middleware synthesizes a `ProviderResponse` and prevents
11
+ * the real `provider.sendMessage` from running.
12
+ */
13
+
14
+ import { afterAll, beforeEach, describe, expect, test } from "bun:test";
15
+
16
+ import type { TrustContext } from "../daemon/conversation-runtime-assembly.js";
17
+ import { defaultLlmCallPlugin } from "../plugins/defaults/llm-call.js";
18
+ import { DEFAULT_TIMEOUTS, runPipeline } from "../plugins/pipeline.js";
19
+ import {
20
+ getMiddlewaresFor,
21
+ registerPlugin,
22
+ resetPluginRegistryForTests,
23
+ } from "../plugins/registry.js";
24
+ import type {
25
+ LLMCallArgs,
26
+ LLMCallResult,
27
+ Middleware,
28
+ Plugin,
29
+ TurnContext,
30
+ } from "../plugins/types.js";
31
+ import type {
32
+ Message,
33
+ Provider,
34
+ ProviderResponse,
35
+ ToolDefinition,
36
+ } from "../providers/types.js";
37
+
38
+ // ─── Fixtures ───────────────────────────────────────────────────────────────
39
+
40
+ const trust: TrustContext = {
41
+ sourceChannel: "vellum",
42
+ trustClass: "guardian",
43
+ };
44
+
45
+ function makeCtx(overrides: Partial<TurnContext> = {}): TurnContext {
46
+ return {
47
+ requestId: "req-test",
48
+ conversationId: "conv-test",
49
+ turnIndex: 0,
50
+ trust,
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function makeResponse(
56
+ overrides: Partial<ProviderResponse> = {},
57
+ ): ProviderResponse {
58
+ return {
59
+ content: [{ type: "text", text: "hello from provider" }],
60
+ model: "fake-model",
61
+ usage: {
62
+ inputTokens: 10,
63
+ outputTokens: 5,
64
+ },
65
+ stopReason: "end_turn",
66
+ ...overrides,
67
+ };
68
+ }
69
+
70
+ type FakeProviderCall = {
71
+ messages: Message[];
72
+ tools?: ToolDefinition[];
73
+ systemPrompt?: string;
74
+ };
75
+
76
+ function makeFakeProvider(
77
+ response: ProviderResponse = makeResponse(),
78
+ ): Provider & { calls: FakeProviderCall[] } {
79
+ const calls: FakeProviderCall[] = [];
80
+ return {
81
+ name: "fake-provider",
82
+ calls,
83
+ async sendMessage(messages, tools, systemPrompt, _options) {
84
+ calls.push({ messages, tools, systemPrompt });
85
+ return response;
86
+ },
87
+ };
88
+ }
89
+
90
+ function makeArgs(
91
+ provider: Provider,
92
+ overrides: Partial<LLMCallArgs> = {},
93
+ ): LLMCallArgs {
94
+ return {
95
+ provider,
96
+ messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],
97
+ tools: undefined,
98
+ systemPrompt: "you are a helpful assistant",
99
+ options: { config: {} },
100
+ ...overrides,
101
+ };
102
+ }
103
+
104
+ // The terminal passed into `runPipeline` matches the one in `agent/loop.ts`:
105
+ // it delegates straight to `args.provider.sendMessage(...)` with no
106
+ // transformation. Keeping it identical here means the test exercises the
107
+ // exact call shape the real loop uses.
108
+ const terminal = (args: LLMCallArgs): Promise<LLMCallResult> =>
109
+ args.provider.sendMessage(
110
+ args.messages,
111
+ args.tools,
112
+ args.systemPrompt,
113
+ args.options,
114
+ );
115
+
116
+ // ─── Tests ──────────────────────────────────────────────────────────────────
117
+
118
+ describe("llmCall pipeline", () => {
119
+ beforeEach(() => {
120
+ resetPluginRegistryForTests();
121
+ });
122
+
123
+ // Clear the registry on the way out too so later test files in the same
124
+ // `bun test` run don't inherit `llmCall` middleware from our final test.
125
+ // Bun runs files sequentially within a process; `beforeEach` only clears
126
+ // at the start of each case, leaving whatever the final test registered
127
+ // in place for the next file.
128
+ afterAll(() => {
129
+ resetPluginRegistryForTests();
130
+ });
131
+
132
+ test("default pipeline invokes provider.sendMessage and returns its response", async () => {
133
+ registerPlugin(defaultLlmCallPlugin);
134
+
135
+ const expected = makeResponse({ model: "expected-model" });
136
+ const provider = makeFakeProvider(expected);
137
+ const args = makeArgs(provider);
138
+
139
+ const result = await runPipeline<LLMCallArgs, LLMCallResult>(
140
+ "llmCall",
141
+ getMiddlewaresFor("llmCall"),
142
+ terminal,
143
+ args,
144
+ makeCtx(),
145
+ DEFAULT_TIMEOUTS.llmCall,
146
+ );
147
+
148
+ expect(result).toBe(expected);
149
+ expect(provider.calls).toHaveLength(1);
150
+ expect(provider.calls[0]!.messages).toBe(args.messages);
151
+ expect(provider.calls[0]!.systemPrompt).toBe("you are a helpful assistant");
152
+ });
153
+
154
+ test("spy middleware records the full invocation arguments", async () => {
155
+ const observed: LLMCallArgs[] = [];
156
+ const spyPlugin: Plugin = {
157
+ manifest: {
158
+ name: "spy-llm",
159
+ version: "0.0.1",
160
+ requires: { pluginRuntime: "v1" },
161
+ },
162
+ middleware: {
163
+ llmCall: async (args, next, _ctx) => {
164
+ observed.push(args);
165
+ return next(args);
166
+ },
167
+ },
168
+ };
169
+
170
+ registerPlugin(spyPlugin);
171
+ registerPlugin(defaultLlmCallPlugin);
172
+
173
+ const provider = makeFakeProvider();
174
+ const tools: ToolDefinition[] = [
175
+ {
176
+ name: "echo",
177
+ description: "echoes its input",
178
+ input_schema: { type: "object" },
179
+ },
180
+ ];
181
+ const args = makeArgs(provider, { tools });
182
+
183
+ await runPipeline<LLMCallArgs, LLMCallResult>(
184
+ "llmCall",
185
+ getMiddlewaresFor("llmCall"),
186
+ terminal,
187
+ args,
188
+ makeCtx(),
189
+ DEFAULT_TIMEOUTS.llmCall,
190
+ );
191
+
192
+ expect(observed).toHaveLength(1);
193
+ expect(observed[0]!.provider).toBe(provider);
194
+ expect(observed[0]!.messages).toBe(args.messages);
195
+ expect(observed[0]!.tools).toBe(tools);
196
+ expect(observed[0]!.systemPrompt).toBe("you are a helpful assistant");
197
+ expect(provider.calls).toHaveLength(1);
198
+ });
199
+
200
+ test("default registered first does not shadow later-registered user middleware", async () => {
201
+ // Regression for a shadowing bug where `defaultLlmCallPlugin` called
202
+ // `provider.sendMessage` directly instead of `next(args)`. Because the
203
+ // default registers at module load (before `bootstrapPlugins()` loads
204
+ // user plugins), it sat at the outermost layer in production — any
205
+ // user-registered `llmCall` middleware would have been silently skipped.
206
+ // This test locks in the fix by registering the default FIRST (matching
207
+ // production ordering) and asserting a user-registered spy still runs.
208
+ const observed: LLMCallArgs[] = [];
209
+ const spyPlugin: Plugin = {
210
+ manifest: {
211
+ name: "spy-llm-after-default",
212
+ version: "0.0.1",
213
+ requires: { pluginRuntime: "v1" },
214
+ },
215
+ middleware: {
216
+ llmCall: async (args, next, _ctx) => {
217
+ observed.push(args);
218
+ return next(args);
219
+ },
220
+ },
221
+ };
222
+
223
+ registerPlugin(defaultLlmCallPlugin);
224
+ registerPlugin(spyPlugin);
225
+
226
+ const provider = makeFakeProvider();
227
+ const args = makeArgs(provider);
228
+
229
+ await runPipeline<LLMCallArgs, LLMCallResult>(
230
+ "llmCall",
231
+ getMiddlewaresFor("llmCall"),
232
+ terminal,
233
+ args,
234
+ makeCtx(),
235
+ DEFAULT_TIMEOUTS.llmCall,
236
+ );
237
+
238
+ expect(observed).toHaveLength(1);
239
+ expect(observed[0]!.provider).toBe(provider);
240
+ expect(provider.calls).toHaveLength(1);
241
+ });
242
+
243
+ test("short-circuit middleware prevents the real provider call", async () => {
244
+ const synthetic = makeResponse({
245
+ model: "synthetic-model",
246
+ content: [{ type: "text", text: "synthesized" }],
247
+ });
248
+
249
+ const shortCircuit: Middleware<LLMCallArgs, LLMCallResult> = async (
250
+ _args,
251
+ _next,
252
+ _ctx,
253
+ ) => synthetic;
254
+
255
+ const shortCircuitPlugin: Plugin = {
256
+ manifest: {
257
+ name: "short-circuit-llm",
258
+ version: "0.0.1",
259
+ requires: { pluginRuntime: "v1" },
260
+ },
261
+ middleware: { llmCall: shortCircuit },
262
+ };
263
+
264
+ registerPlugin(shortCircuitPlugin);
265
+ registerPlugin(defaultLlmCallPlugin);
266
+
267
+ const provider = makeFakeProvider();
268
+ const args = makeArgs(provider);
269
+
270
+ const result = await runPipeline<LLMCallArgs, LLMCallResult>(
271
+ "llmCall",
272
+ getMiddlewaresFor("llmCall"),
273
+ terminal,
274
+ args,
275
+ makeCtx(),
276
+ DEFAULT_TIMEOUTS.llmCall,
277
+ );
278
+
279
+ expect(result).toBe(synthetic);
280
+ // The short-circuit middleware never calls `next`, so the terminal and
281
+ // every downstream middleware (including the default) are skipped and
282
+ // the provider is never contacted.
283
+ expect(provider.calls).toHaveLength(0);
284
+ });
285
+ });
@@ -70,7 +70,7 @@ describe("LLMSchema", () => {
70
70
  const parsed = LLMSchema.parse({});
71
71
  expect(parsed.default).toEqual({
72
72
  provider: "anthropic",
73
- model: "claude-opus-4-7",
73
+ model: "claude-sonnet-4-6",
74
74
  maxTokens: 64000,
75
75
  effort: "max",
76
76
  speed: "standard",
@@ -10,14 +10,17 @@ import type { ToolContext } from "../tools/types.js";
10
10
  // Mock dependencies for the tool wrapper
11
11
  // ---------------------------------------------------------------------------
12
12
 
13
- let mockApiKey: string | undefined = "test-gemini-key";
13
+ let mockGeminiKey: string | undefined = "test-gemini-key";
14
+ let mockOpenAIKey: string | undefined = "test-openai-key";
14
15
  let mockImageGenMode: "your-own" | "managed" = "your-own";
16
+ let mockImageGenProvider: "gemini" | "openai" = "gemini";
15
17
  let mockGenerateResult = {
16
18
  images: [{ mimeType: "image/png", dataBase64: "generated-data" }],
17
19
  text: "A beautiful image",
18
20
  resolvedModel: "gemini-3.1-flash-image-preview",
19
21
  };
20
22
  let mockGenerateError: Error | null = null;
23
+ let lastGenerateProvider: unknown = null;
21
24
  let lastGenerateCredentials: unknown = null;
22
25
 
23
26
  mock.module("../config/loader.js", () => ({
@@ -31,7 +34,7 @@ mock.module("../config/loader.js", () => ({
31
34
  },
32
35
  "image-generation": {
33
36
  mode: mockImageGenMode,
34
- provider: "gemini",
37
+ provider: mockImageGenProvider,
35
38
  model: "gemini-3.1-flash-image-preview",
36
39
  },
37
40
  "web-search": { mode: "your-own", provider: "inference-provider-native" },
@@ -41,27 +44,33 @@ mock.module("../config/loader.js", () => ({
41
44
 
42
45
  mock.module("../security/secure-keys.js", () => ({
43
46
  getSecureKeyAsync: async (account: string) => {
44
- if (account === "gemini") return mockApiKey;
47
+ if (account === "gemini") return mockGeminiKey;
48
+ if (account === "openai") return mockOpenAIKey;
45
49
  return undefined;
46
50
  },
47
51
  getProviderKeyAsync: async (provider: string) => {
48
- if (provider === "gemini") return mockApiKey;
52
+ if (provider === "gemini") return mockGeminiKey;
53
+ if (provider === "openai") return mockOpenAIKey;
49
54
  return undefined;
50
55
  },
51
56
  }));
52
57
 
53
- mock.module("../media/gemini-image-service.js", () => ({
58
+ mock.module("../media/image-service.js", () => ({
54
59
  generateImage: async (
60
+ provider: unknown,
55
61
  credentials: unknown,
56
62
  _request: Record<string, unknown>,
57
63
  ) => {
64
+ lastGenerateProvider = provider;
58
65
  lastGenerateCredentials = credentials;
59
66
  if (mockGenerateError) throw mockGenerateError;
60
67
  return mockGenerateResult;
61
68
  },
62
- mapGeminiError: (error: unknown) => {
63
- if (error instanceof Error) return `Mock error: ${error.message}`;
64
- return "Mock unknown error";
69
+ mapImageGenError: (provider: unknown, error: unknown) => {
70
+ const providerLabel = provider === "openai" ? "OpenAI" : "Gemini";
71
+ if (error instanceof Error)
72
+ return `Mock ${providerLabel} error: ${error.message}`;
73
+ return `Mock ${providerLabel} unknown error`;
65
74
  },
66
75
  }));
67
76
 
@@ -97,14 +106,17 @@ const CONFIG_DIR = join(
97
106
  // ---------------------------------------------------------------------------
98
107
 
99
108
  beforeEach(() => {
100
- mockApiKey = "test-gemini-key";
109
+ mockGeminiKey = "test-gemini-key";
110
+ mockOpenAIKey = "test-openai-key";
101
111
  mockImageGenMode = "your-own";
112
+ mockImageGenProvider = "gemini";
102
113
  mockGenerateResult = {
103
114
  images: [{ mimeType: "image/png", dataBase64: "generated-data" }],
104
115
  text: "A beautiful image",
105
116
  resolvedModel: "gemini-3.1-flash-image-preview",
106
117
  };
107
118
  mockGenerateError = null;
119
+ lastGenerateProvider = null;
108
120
  lastGenerateCredentials = null;
109
121
  mockManagedBaseUrl = undefined;
110
122
  mockManagedProxyContext = {
@@ -127,7 +139,7 @@ describe("image-studio skill script wrapper", () => {
127
139
  });
128
140
 
129
141
  test("returns error when no API key and no managed proxy", async () => {
130
- mockApiKey = undefined;
142
+ mockGeminiKey = undefined;
131
143
 
132
144
  const result = await run({ prompt: "a cat" }, fakeContext);
133
145
 
@@ -148,6 +160,7 @@ describe("image-studio skill script wrapper", () => {
148
160
 
149
161
  expect(result.isError).toBe(false);
150
162
  expect(result.content).toContain("Generated 1 image");
163
+ expect(lastGenerateProvider).toBe("gemini");
151
164
  expect(lastGenerateCredentials).toEqual({
152
165
  type: "managed-proxy",
153
166
  assistantApiKey: "managed-key-123",
@@ -157,7 +170,7 @@ describe("image-studio skill script wrapper", () => {
157
170
 
158
171
  test("managed mode returns error when managed proxy is unavailable", async () => {
159
172
  mockImageGenMode = "managed";
160
- mockApiKey = "direct-key"; // should be ignored in managed mode
173
+ mockGeminiKey = "direct-key"; // should be ignored in managed mode
161
174
  mockManagedBaseUrl = undefined;
162
175
 
163
176
  const result = await run({ prompt: "a cat" }, fakeContext);
@@ -168,7 +181,7 @@ describe("image-studio skill script wrapper", () => {
168
181
 
169
182
  test("your-own mode uses direct API key", async () => {
170
183
  mockImageGenMode = "your-own";
171
- mockApiKey = "direct-key";
184
+ mockGeminiKey = "direct-key";
172
185
  mockManagedBaseUrl = "https://platform.example.com/v1/runtime-proxy/gemini";
173
186
  mockManagedProxyContext = {
174
187
  enabled: true,
@@ -178,12 +191,94 @@ describe("image-studio skill script wrapper", () => {
178
191
 
179
192
  await run({ prompt: "a cat" }, fakeContext);
180
193
 
194
+ expect(lastGenerateProvider).toBe("gemini");
181
195
  expect(lastGenerateCredentials).toEqual({
182
196
  type: "direct",
183
197
  apiKey: "direct-key",
184
198
  });
185
199
  });
186
200
 
201
+ test("openai provider dispatches to OpenAI with its key", async () => {
202
+ mockImageGenProvider = "openai";
203
+ mockOpenAIKey = "openai-direct-key";
204
+
205
+ const result = await run({ prompt: "a robot" }, fakeContext);
206
+
207
+ expect(result.isError).toBe(false);
208
+ expect(lastGenerateProvider).toBe("openai");
209
+ expect(lastGenerateCredentials).toEqual({
210
+ type: "direct",
211
+ apiKey: "openai-direct-key",
212
+ });
213
+ });
214
+
215
+ test("openai provider returns OpenAI-specific error hint when no key", async () => {
216
+ mockImageGenProvider = "openai";
217
+ mockOpenAIKey = undefined;
218
+
219
+ const result = await run({ prompt: "a robot" }, fakeContext);
220
+
221
+ expect(result.isError).toBe(true);
222
+ expect(result.content).toContain("OpenAI");
223
+ expect(result.content).not.toContain("No Gemini API key");
224
+ });
225
+
226
+ test("explicit model override routes to owning provider (gemini config → openai call)", async () => {
227
+ // Config says the user's default provider is gemini, but the LLM
228
+ // explicitly requests a gpt-* model. The tool must dispatch to OpenAI
229
+ // and resolve OpenAI credentials, not fall back to Gemini's default.
230
+ mockImageGenProvider = "gemini";
231
+ mockOpenAIKey = "openai-direct-key";
232
+
233
+ const result = await run(
234
+ { prompt: "a robot", model: "gpt-image-2" },
235
+ fakeContext,
236
+ );
237
+
238
+ expect(result.isError).toBe(false);
239
+ expect(lastGenerateProvider).toBe("openai");
240
+ expect(lastGenerateCredentials).toEqual({
241
+ type: "direct",
242
+ apiKey: "openai-direct-key",
243
+ });
244
+ });
245
+
246
+ test("explicit model override routes to owning provider (openai config → gemini call)", async () => {
247
+ // The inverse: config says openai but the LLM asks for a gemini-* model.
248
+ mockImageGenProvider = "openai";
249
+ mockGeminiKey = "gemini-direct-key";
250
+
251
+ const result = await run(
252
+ { prompt: "a cat", model: "gemini-3-pro-image-preview" },
253
+ fakeContext,
254
+ );
255
+
256
+ expect(result.isError).toBe(false);
257
+ expect(lastGenerateProvider).toBe("gemini");
258
+ expect(lastGenerateCredentials).toEqual({
259
+ type: "direct",
260
+ apiKey: "gemini-direct-key",
261
+ });
262
+ });
263
+
264
+ test("cross-provider override surfaces owning provider's credential error", async () => {
265
+ // Config: gemini (with a gemini key). LLM asks for gpt-image-2 but the
266
+ // OpenAI key is missing. The error hint must reference OpenAI, not
267
+ // Gemini, because the dispatch target is OpenAI.
268
+ mockImageGenProvider = "gemini";
269
+ mockGeminiKey = "test-gemini-key";
270
+ mockOpenAIKey = undefined;
271
+
272
+ const result = await run(
273
+ { prompt: "a robot", model: "gpt-image-2" },
274
+ fakeContext,
275
+ );
276
+
277
+ expect(result.isError).toBe(true);
278
+ expect(result.content).toContain("OpenAI");
279
+ expect(result.content).not.toContain("No Gemini API key");
280
+ });
281
+
187
282
  test("returns generated image with contentBlocks", async () => {
188
283
  const result = await run({ prompt: "a sunset" }, fakeContext);
189
284
 
@@ -225,7 +320,17 @@ describe("image-studio skill script wrapper", () => {
225
320
  const result = await run({ prompt: "a cat" }, fakeContext);
226
321
 
227
322
  expect(result.isError).toBe(true);
228
- expect(result.content).toContain("Mock error: API failure");
323
+ expect(result.content).toContain("Mock Gemini error: API failure");
324
+ });
325
+
326
+ test("openai generation error uses OpenAI-specific mapping", async () => {
327
+ mockImageGenProvider = "openai";
328
+ mockGenerateError = new Error("openai failure");
329
+
330
+ const result = await run({ prompt: "a cat" }, fakeContext);
331
+
332
+ expect(result.isError).toBe(true);
333
+ expect(result.content).toContain("Mock OpenAI error: openai failure");
229
334
  });
230
335
 
231
336
  test("reads source images from file paths on disk", async () => {
@@ -330,6 +435,7 @@ describe("image-studio TOOLS.json manifest", () => {
330
435
  expect(props.model.enum).toEqual([
331
436
  "gemini-3.1-flash-image-preview",
332
437
  "gemini-3-pro-image-preview",
438
+ "gpt-image-2",
333
439
  ]);
334
440
  expect(props.variants.type).toBe("number");
335
441
  });