@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Generates app icons using the Gemini image generation service.
2
+ * Generates app icons using the configured image-generation provider.
3
3
  *
4
4
  * Called as an async side-effect after app creation — never blocks
5
5
  * the main app_create flow. Icons are saved to the app's directory
@@ -11,25 +11,18 @@ import { join } from "node:path";
11
11
 
12
12
  import { getConfig } from "../config/loader.js";
13
13
  import { getAppDirPath } from "../memory/app-store.js";
14
- import {
15
- buildManagedBaseUrl,
16
- resolveManagedProxyContext,
17
- } from "../providers/managed-proxy/context.js";
18
- import { getProviderKeyAsync } from "../security/secure-keys.js";
19
14
  import { getLogger } from "../util/logger.js";
20
- import {
21
- generateImage,
22
- type ImageGenCredentials,
23
- mapGeminiError,
24
- } from "./gemini-image-service.js";
15
+ import { resolveImageGenCredentials } from "./image-credentials.js";
16
+ import { generateImage, mapImageGenError } from "./image-service.js";
25
17
 
26
18
  const log = getLogger("app-icon-generator");
27
19
 
28
20
  /**
29
21
  * Generate an app icon and save it to `~/.vellum/apps/{appId}/icon.png`.
30
22
  *
31
- * Uses Gemini image generation when an API key is available.
32
- * Silently no-ops if no key is configured or generation fails.
23
+ * Uses the configured image-generation provider when credentials are
24
+ * available. Silently no-ops if no credentials are configured or
25
+ * generation fails.
33
26
  */
34
27
  export async function generateAppIcon(
35
28
  appId: string,
@@ -37,34 +30,15 @@ export async function generateAppIcon(
37
30
  appDescription?: string,
38
31
  ): Promise<void> {
39
32
  const config = getConfig();
40
- const imageGenMode = config.services["image-generation"].mode;
41
-
42
- // Resolve credentials strictly based on mode — no cross-mode fallbacks
43
- let credentials: ImageGenCredentials | undefined;
44
-
45
- if (imageGenMode === "managed") {
46
- const managedBaseUrl = await buildManagedBaseUrl("gemini");
47
- if (managedBaseUrl) {
48
- const ctx = await resolveManagedProxyContext();
49
- credentials = {
50
- type: "managed-proxy",
51
- assistantApiKey: ctx.assistantApiKey,
52
- baseUrl: managedBaseUrl,
53
- };
54
- }
55
- } else {
56
- const apiKey = await getProviderKeyAsync("gemini");
57
- if (apiKey) {
58
- credentials = { type: "direct", apiKey };
59
- }
60
- }
61
-
33
+ const svc = config.services["image-generation"];
34
+ const { credentials, errorHint } = await resolveImageGenCredentials({
35
+ provider: svc.provider,
36
+ mode: svc.mode,
37
+ });
62
38
  if (!credentials) {
63
- const reason =
64
- imageGenMode === "managed"
65
- ? "Managed proxy is not available"
66
- : "No Gemini API key configured";
67
- log.debug(`${reason} — skipping app icon generation`);
39
+ log.debug(
40
+ `${errorHint ?? "Image generation is not configured"} — skipping app icon generation`,
41
+ );
68
42
  return;
69
43
  }
70
44
 
@@ -90,16 +64,19 @@ export async function generateAppIcon(
90
64
  "- Modern aesthetic similar to Apple's design language";
91
65
 
92
66
  try {
93
- log.info({ appId, appName }, "Generating app icon via Gemini");
67
+ log.info({ appId, appName, provider: svc.provider }, "Generating app icon");
94
68
 
95
- const result = await generateImage(credentials, {
69
+ const result = await generateImage(svc.provider, credentials, {
96
70
  prompt,
97
71
  mode: "generate",
98
- model: config.services["image-generation"].model,
72
+ model: svc.model,
99
73
  });
100
74
 
101
75
  if (result.images.length === 0) {
102
- log.warn({ appId }, "Gemini returned no image for app icon");
76
+ log.warn(
77
+ { appId, provider: svc.provider },
78
+ "Provider returned no image for app icon",
79
+ );
103
80
  return;
104
81
  }
105
82
 
@@ -111,9 +88,9 @@ export async function generateAppIcon(
111
88
 
112
89
  log.info({ appId, iconPath }, "App icon saved");
113
90
  } catch (error) {
114
- const message = mapGeminiError(error);
91
+ const message = mapImageGenError(svc.provider, error);
115
92
  log.warn(
116
- { appId, error: message },
93
+ { appId, provider: svc.provider, error: message },
117
94
  "App icon generation failed — skipping",
118
95
  );
119
96
  }
@@ -1,60 +1,45 @@
1
1
  import { getConfig } from "../config/loader.js";
2
- import {
3
- buildManagedBaseUrl,
4
- resolveManagedProxyContext,
5
- } from "../providers/managed-proxy/context.js";
6
- import { getProviderKeyAsync } from "../security/secure-keys.js";
7
2
  import { ConfigError, ProviderError } from "../util/errors.js";
8
- import {
9
- generateImage,
10
- type ImageGenCredentials,
11
- } from "./gemini-image-service.js";
3
+ import { resolveImageGenCredentials } from "./image-credentials.js";
4
+ import { generateImage, mapImageGenError } from "./image-service.js";
12
5
 
13
6
  export async function generateAvatar(
14
7
  prompt: string,
15
8
  ): Promise<{ imageBase64: string; mimeType: string }> {
16
9
  const config = getConfig();
17
- const imageGenMode = config.services["image-generation"].mode;
10
+ const svc = config.services["image-generation"];
18
11
 
19
- // Resolve credentials strictly based on mode — no cross-mode fallbacks
20
- let credentials: ImageGenCredentials | undefined;
21
-
22
- if (imageGenMode === "managed") {
23
- const managedBaseUrl = await buildManagedBaseUrl("gemini");
24
- if (managedBaseUrl) {
25
- const ctx = await resolveManagedProxyContext();
26
- credentials = {
27
- type: "managed-proxy",
28
- assistantApiKey: ctx.assistantApiKey,
29
- baseUrl: managedBaseUrl,
30
- };
31
- }
32
- } else {
33
- const geminiKey = await getProviderKeyAsync("gemini");
34
- if (geminiKey) {
35
- credentials = { type: "direct", apiKey: geminiKey };
36
- }
37
- }
12
+ const { credentials, errorHint } = await resolveImageGenCredentials({
13
+ provider: svc.provider,
14
+ mode: svc.mode,
15
+ });
38
16
 
39
17
  if (!credentials) {
40
- const hint =
41
- imageGenMode === "managed"
42
- ? "Managed proxy is not available. Please log in to Vellum or switch to Your Own mode."
43
- : "Gemini API key is not configured. Please set your Gemini API key in Settings > Models & Services.";
44
- throw new ConfigError(hint);
18
+ throw new ConfigError(errorHint ?? "Image generation is not configured.");
45
19
  }
46
20
 
47
- const result = await generateImage(credentials, {
48
- prompt,
49
- mode: "generate",
50
- model: config.services["image-generation"].model,
51
- });
21
+ let result;
22
+ try {
23
+ result = await generateImage(svc.provider, credentials, {
24
+ prompt,
25
+ mode: "generate",
26
+ model: svc.model,
27
+ });
28
+ } catch (error) {
29
+ // Re-throw with a provider-aware, user-friendly message so callers
30
+ // (e.g. avatar-generator) don't need provider context to surface a
31
+ // useful error.
32
+ throw new ProviderError(
33
+ mapImageGenError(svc.provider, error),
34
+ svc.provider,
35
+ );
36
+ }
52
37
 
53
38
  const image = result.images[0];
54
39
  if (!image) {
55
40
  throw new ProviderError(
56
- "Gemini image generation returned no images.",
57
- "gemini",
41
+ "Image generation returned no images.",
42
+ svc.provider,
58
43
  );
59
44
  }
60
45
 
@@ -1,45 +1,13 @@
1
1
  import { ApiError, GoogleGenAI } from "@google/genai";
2
2
 
3
- // --- Request / Response types ---
4
-
5
- interface ImageGenerationRequest {
6
- prompt: string;
7
- mode: "generate" | "edit";
8
- /** Base64-encoded source images for edit mode */
9
- sourceImages?: Array<{ mimeType: string; dataBase64: string }>;
10
- /** Model override; defaults to 'gemini-3.1-flash-image-preview' */
11
- model?: string;
12
- /** Number of output variants (1-4, default 1) */
13
- variants?: number;
14
- }
15
-
16
- /** Credentials for direct Gemini API access. */
17
- interface DirectCredentials {
18
- type: "direct";
19
- apiKey: string;
20
- }
21
-
22
- /** Credentials for managed proxy access (platform translates to Vertex AI). */
23
- interface ManagedProxyCredentials {
24
- type: "managed-proxy";
25
- assistantApiKey: string;
26
- baseUrl: string;
27
- }
28
-
29
- export type ImageGenCredentials = DirectCredentials | ManagedProxyCredentials;
30
-
31
- interface GeneratedImage {
32
- mimeType: string;
33
- dataBase64: string;
34
- /** Short title derived from the model's text response, if available. */
35
- title?: string;
36
- }
37
-
38
- interface ImageGenerationResult {
39
- images: GeneratedImage[];
40
- text?: string;
41
- resolvedModel: string;
42
- }
3
+ import {
4
+ type GeneratedImage,
5
+ type ImageGenCredentials,
6
+ type ImageGenerationRequest,
7
+ type ImageGenerationResult,
8
+ type ManagedProxyCredentials,
9
+ MAX_VARIANTS,
10
+ } from "./types.js";
43
11
 
44
12
  // --- Constants ---
45
13
 
@@ -48,7 +16,6 @@ const ALLOWED_MODELS = new Set([
48
16
  "gemini-3.1-flash-image-preview",
49
17
  "gemini-3-pro-image-preview",
50
18
  ]);
51
- const MAX_VARIANTS = 4;
52
19
 
53
20
  // --- Error mapping ---
54
21
 
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Shared credential resolver for image-generation call sites (image-studio
3
+ * tool, CLI `image-generation` command, app-icon generator).
4
+ *
5
+ * Each call site picks between the managed-proxy path (routes through the
6
+ * platform) and the "your own" path (direct provider API key). This module
7
+ * resolves either path and returns a provider-aware error hint when
8
+ * credentials are unavailable.
9
+ */
10
+
11
+ import { MANAGED_PROVIDER_META } from "../providers/managed-proxy/constants.js";
12
+ import { resolveManagedProxyContext } from "../providers/managed-proxy/context.js";
13
+ import { getProviderKeyAsync } from "../security/secure-keys.js";
14
+ import type { ImageGenCredentials, ImageGenProvider } from "./types.js";
15
+
16
+ /**
17
+ * Resolve credentials for an image-generation request.
18
+ *
19
+ * - `mode === "managed"`: returns managed-proxy credentials when the
20
+ * platform URL and assistant API key are both configured, otherwise
21
+ * returns a hint telling the user to log in or switch modes.
22
+ * - `mode === "your-own"`: returns direct credentials when the provider
23
+ * API key is present in secure storage (or the env-var fallback),
24
+ * otherwise returns a provider-aware hint pointing at Settings.
25
+ */
26
+ export async function resolveImageGenCredentials(opts: {
27
+ provider: ImageGenProvider;
28
+ mode: "managed" | "your-own";
29
+ }): Promise<{ credentials?: ImageGenCredentials; errorHint?: string }> {
30
+ const { provider, mode } = opts;
31
+
32
+ if (mode === "managed") {
33
+ // Resolve platform URL + assistant API key from a single snapshot so
34
+ // baseUrl and assistantApiKey can't diverge if the credential is cleared
35
+ // between lookups.
36
+ const meta = MANAGED_PROVIDER_META[provider];
37
+ const ctx = await resolveManagedProxyContext();
38
+ if (
39
+ !meta?.managed ||
40
+ !meta.proxyPath ||
41
+ !ctx.enabled ||
42
+ !ctx.assistantApiKey
43
+ ) {
44
+ return {
45
+ errorHint:
46
+ "Managed proxy is not available. Please log in to Vellum or switch to Your Own mode.",
47
+ };
48
+ }
49
+ return {
50
+ credentials: {
51
+ type: "managed-proxy",
52
+ assistantApiKey: ctx.assistantApiKey,
53
+ baseUrl: `${ctx.platformBaseUrl}${meta.proxyPath}`,
54
+ },
55
+ };
56
+ }
57
+
58
+ // mode === "your-own"
59
+ const apiKey = await getProviderKeyAsync(provider);
60
+ if (apiKey) {
61
+ return { credentials: { type: "direct", apiKey } };
62
+ }
63
+ return { errorHint: providerKeyHint(provider) };
64
+ }
65
+
66
+ function providerKeyHint(provider: ImageGenProvider): string {
67
+ switch (provider) {
68
+ case "gemini":
69
+ return "No Gemini API key configured. Please set your Gemini API key in Settings > Models & Services.";
70
+ case "openai":
71
+ return "No OpenAI API key configured. Please set your OpenAI API key in Settings > Models & Services.";
72
+ }
73
+ }
@@ -0,0 +1,85 @@
1
+ import {
2
+ generateImage as generateImageGemini,
3
+ mapGeminiError,
4
+ } from "./gemini-image-service.js";
5
+ import { generateImageOpenAI, mapOpenAIError } from "./openai-image-service.js";
6
+ import type {
7
+ ImageGenCredentials,
8
+ ImageGenerationRequest,
9
+ ImageGenerationResult,
10
+ ImageGenProvider,
11
+ } from "./types.js";
12
+
13
+ /**
14
+ * Dispatch image generation to the provider-specific implementation.
15
+ */
16
+ export function generateImage(
17
+ provider: ImageGenProvider,
18
+ credentials: ImageGenCredentials,
19
+ request: ImageGenerationRequest,
20
+ ): Promise<ImageGenerationResult> {
21
+ switch (provider) {
22
+ case "openai":
23
+ return generateImageOpenAI(credentials, request);
24
+ case "gemini":
25
+ return generateImageGemini(credentials, request);
26
+ default: {
27
+ const _exhaustive: never = provider;
28
+ throw new Error(`Unknown image generation provider: ${_exhaustive}`);
29
+ }
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Dispatch error mapping to the provider-specific implementation.
35
+ */
36
+ export function mapImageGenError(
37
+ provider: ImageGenProvider,
38
+ error: unknown,
39
+ ): string {
40
+ switch (provider) {
41
+ case "openai":
42
+ return mapOpenAIError(error);
43
+ case "gemini":
44
+ return mapGeminiError(error);
45
+ default: {
46
+ const _exhaustive: never = provider;
47
+ throw new Error(`Unknown image generation provider: ${_exhaustive}`);
48
+ }
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Derive the owning provider from an explicit model ID.
54
+ *
55
+ * When a caller (LLM tool invocation, CLI `--model` flag) passes an explicit
56
+ * `model` argument, the request should dispatch to the provider that owns
57
+ * that model — not to the user's configured Settings provider. Without this,
58
+ * asking for `gpt-image-2` while `services["image-generation"].provider` is
59
+ * `gemini` silently falls back to the Gemini default model.
60
+ *
61
+ * Model prefix mapping:
62
+ * - `gpt-*` or `dall-e-*` → `openai`
63
+ * - `gemini-*` → `gemini`
64
+ * - anything else (or `undefined`) → the provided `fallback`
65
+ */
66
+ export function providerForModel(
67
+ model: unknown,
68
+ fallback: ImageGenProvider,
69
+ ): ImageGenProvider {
70
+ // `model` originates from an LLM tool call's `input.model`, which is a
71
+ // `Record<string, unknown>` without runtime validation — so a non-string
72
+ // value (e.g. `{"model": 123}`) must not crash `.startsWith`.
73
+ if (typeof model !== "string" || !model) return fallback;
74
+ if (model.startsWith("gpt-") || model.startsWith("dall-e-")) return "openai";
75
+ if (model.startsWith("gemini-")) return "gemini";
76
+ return fallback;
77
+ }
78
+
79
+ export type {
80
+ GeneratedImage,
81
+ ImageGenCredentials,
82
+ ImageGenerationRequest,
83
+ ImageGenerationResult,
84
+ ImageGenProvider,
85
+ } from "./types.js";
@@ -0,0 +1,131 @@
1
+ import OpenAI from "openai";
2
+ import { toFile } from "openai/uploads";
3
+
4
+ import {
5
+ type GeneratedImage,
6
+ type ImageGenCredentials,
7
+ type ImageGenerationRequest,
8
+ type ImageGenerationResult,
9
+ MAX_VARIANTS,
10
+ } from "./types.js";
11
+
12
+ // --- Constants ---
13
+
14
+ const DEFAULT_MODEL = "gpt-image-2";
15
+ const ALLOWED_MODELS = new Set(["gpt-image-2"]);
16
+
17
+ // --- Error mapping ---
18
+
19
+ /**
20
+ * Map an error raised by the OpenAI Images API to a user-friendly string.
21
+ * Mirrors the status-code branches of `mapGeminiError` in
22
+ * `./gemini-image-service.ts`.
23
+ */
24
+ export function mapOpenAIError(error: unknown): string {
25
+ if (error instanceof OpenAI.APIError) {
26
+ const status = error.status;
27
+ if (status === 400) {
28
+ return "The image request was invalid. Please check your prompt and try again.";
29
+ }
30
+ if (status === 401 || status === 403) {
31
+ return "Authentication failed. Please check your OpenAI API key.";
32
+ }
33
+ if (status === 429) {
34
+ return "Rate limit exceeded. Please wait a moment and try again.";
35
+ }
36
+ if (status !== undefined && status >= 500) {
37
+ return "The OpenAI service is temporarily unavailable. Please try again later.";
38
+ }
39
+ return `OpenAI API error (status ${status}). Please try again.`;
40
+ }
41
+ if (error instanceof Error) {
42
+ return `Image generation failed: ${error.message}`;
43
+ }
44
+ return "An unexpected error occurred during image generation.";
45
+ }
46
+
47
+ // --- Title derivation ---
48
+
49
+ /**
50
+ * Derive a short filename-safe title from the first 6 words of the prompt.
51
+ * Uses the same sanitization regex as `extractTitle` in
52
+ * `./gemini-image-service.ts`.
53
+ */
54
+ function deriveTitleFromPrompt(prompt: string): string | undefined {
55
+ const firstWords = prompt.trim().split(/\s+/).slice(0, 6).join(" ");
56
+ if (!firstWords) return undefined;
57
+ const sanitized = firstWords
58
+ .replace(/[^\w\s-]/g, "")
59
+ .replace(/\s+/g, "-")
60
+ .toLowerCase()
61
+ .slice(0, 60);
62
+ return sanitized.length > 0 ? sanitized : undefined;
63
+ }
64
+
65
+ // --- Core function ---
66
+
67
+ /**
68
+ * Generate or edit an image via the OpenAI Images API (`gpt-image-2`).
69
+ *
70
+ * The OpenAI Images API does not return commentary text, so the returned
71
+ * `text` field is always `undefined`. A title is derived from the prompt
72
+ * instead and attached to every returned image.
73
+ */
74
+ export async function generateImageOpenAI(
75
+ credentials: ImageGenCredentials,
76
+ request: ImageGenerationRequest,
77
+ ): Promise<ImageGenerationResult> {
78
+ const model =
79
+ request.model && ALLOWED_MODELS.has(request.model)
80
+ ? request.model
81
+ : DEFAULT_MODEL;
82
+
83
+ const variants = Math.max(1, Math.min(request.variants ?? 1, MAX_VARIANTS));
84
+
85
+ const client =
86
+ credentials.type === "managed-proxy"
87
+ ? new OpenAI({
88
+ apiKey: credentials.assistantApiKey,
89
+ baseURL: credentials.baseUrl,
90
+ })
91
+ : new OpenAI({ apiKey: credentials.apiKey });
92
+
93
+ const title = deriveTitleFromPrompt(request.prompt);
94
+
95
+ let response: { data?: Array<{ b64_json?: string }> };
96
+
97
+ if (request.mode === "edit" && request.sourceImages) {
98
+ const files = await Promise.all(
99
+ request.sourceImages.map((img) =>
100
+ toFile(Buffer.from(img.dataBase64, "base64"), "input.png", {
101
+ type: img.mimeType,
102
+ }),
103
+ ),
104
+ );
105
+ response = (await client.images.edit({
106
+ model,
107
+ prompt: request.prompt,
108
+ image: files,
109
+ n: variants,
110
+ })) as { data?: Array<{ b64_json?: string }> };
111
+ } else {
112
+ response = (await client.images.generate({
113
+ model,
114
+ prompt: request.prompt,
115
+ n: variants,
116
+ })) as { data?: Array<{ b64_json?: string }> };
117
+ }
118
+
119
+ const images: GeneratedImage[] = [];
120
+ for (const entry of response.data ?? []) {
121
+ if (!entry.b64_json) continue;
122
+ const img: GeneratedImage = {
123
+ mimeType: "image/png",
124
+ dataBase64: entry.b64_json,
125
+ };
126
+ if (title) img.title = title;
127
+ images.push(img);
128
+ }
129
+
130
+ return { images, text: undefined, resolvedModel: model };
131
+ }
@@ -0,0 +1,46 @@
1
+ export type ImageGenProvider = "gemini" | "openai";
2
+
3
+ export interface DirectCredentials {
4
+ type: "direct";
5
+ apiKey: string;
6
+ }
7
+ export interface ManagedProxyCredentials {
8
+ type: "managed-proxy";
9
+ assistantApiKey: string;
10
+ baseUrl: string;
11
+ }
12
+ export type ImageGenCredentials = DirectCredentials | ManagedProxyCredentials;
13
+
14
+ export interface ImageGenerationRequest {
15
+ prompt: string;
16
+ mode: "generate" | "edit";
17
+ sourceImages?: Array<{ mimeType: string; dataBase64: string }>;
18
+ model?: string;
19
+ variants?: number;
20
+ }
21
+
22
+ export interface GeneratedImage {
23
+ mimeType: string;
24
+ dataBase64: string;
25
+ title?: string;
26
+ }
27
+ export interface ImageGenerationResult {
28
+ images: GeneratedImage[];
29
+ text?: string;
30
+ resolvedModel: string;
31
+ }
32
+
33
+ export const MAX_VARIANTS = 4;
34
+
35
+ /**
36
+ * Derive the image-generation provider from a model identifier by prefix.
37
+ * Canonical mapping shared between the runtime model setter
38
+ * (`setImageGenModel`) and workspace migration 006-services-config so the
39
+ * two cannot drift. Unknown models fall through to "gemini".
40
+ */
41
+ export function providerForImageModelPrefix(model: string): ImageGenProvider {
42
+ if (model.startsWith("gpt") || model.startsWith("dall-e")) {
43
+ return "openai";
44
+ }
45
+ return "gemini";
46
+ }