@vellumai/assistant 0.7.3 → 0.8.1

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 (778) hide show
  1. package/AGENTS.md +11 -0
  2. package/ARCHITECTURE.md +29 -28
  3. package/Dockerfile +6 -4
  4. package/README.md +2 -2
  5. package/__tests__/permissions/gateway-threshold-reader.test.ts +236 -9
  6. package/bun.lock +3 -0
  7. package/docker-entrypoint.sh +16 -0
  8. package/eslint-rules/__tests__/cli-no-daemon-internals.test.ts +420 -0
  9. package/eslint-rules/cli-no-daemon-internals.js +283 -0
  10. package/eslint.config.mjs +12 -0
  11. package/knip.json +3 -1
  12. package/node_modules/@vellumai/ipc-server-utils/bun.lock +24 -0
  13. package/node_modules/@vellumai/ipc-server-utils/package.json +18 -0
  14. package/node_modules/@vellumai/ipc-server-utils/src/index.ts +6 -0
  15. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.test.ts +430 -0
  16. package/node_modules/@vellumai/ipc-server-utils/src/socket-watchdog.ts +221 -0
  17. package/node_modules/@vellumai/ipc-server-utils/tsconfig.json +20 -0
  18. package/node_modules/@vellumai/skill-host-contracts/src/client.ts +10 -1
  19. package/openapi.yaml +4126 -959
  20. package/package.json +5 -1
  21. package/scripts/generate-openapi.ts +52 -4
  22. package/scripts/sync-llm-catalog.ts +165 -0
  23. package/scripts/sync-web-search-catalog.ts +107 -0
  24. package/src/__tests__/actor-trust-resolver-address-fallback.test.ts +169 -0
  25. package/src/__tests__/agent-loop-override-profile.test.ts +26 -1
  26. package/src/__tests__/annotate-risk-options.test.ts +291 -0
  27. package/src/__tests__/anthropic-provider.test.ts +92 -2
  28. package/src/__tests__/app-control-flow.test.ts +7 -0
  29. package/src/__tests__/approval-cascade.test.ts +8 -16
  30. package/src/__tests__/approval-routes-http.test.ts +6 -0
  31. package/src/__tests__/assistant-events-sse-shed.test.ts +232 -0
  32. package/src/__tests__/auto-analysis-end-to-end.test.ts +12 -25
  33. package/src/__tests__/avatar-identity-sync.test.ts +87 -0
  34. package/src/__tests__/background-workers-disk-pressure.test.ts +11 -22
  35. package/src/__tests__/btw-routes.test.ts +1 -0
  36. package/src/__tests__/call-constants.test.ts +10 -1
  37. package/src/__tests__/call-controller.test.ts +127 -0
  38. package/src/__tests__/call-site-routing-provider.test.ts +172 -45
  39. package/src/__tests__/cancel-resolves-conversation-key.test.ts +44 -3
  40. package/src/__tests__/channel-policy.test.ts +12 -0
  41. package/src/__tests__/checker.test.ts +89 -0
  42. package/src/__tests__/cli-memory-v2-reembed-skills.test.ts +88 -30
  43. package/src/__tests__/compact-event-conversation-id-guard.test.ts +33 -5
  44. package/src/__tests__/compaction-strip-metadata-clear.test.ts +26 -1
  45. package/src/__tests__/config-loader-backfill.test.ts +526 -102
  46. package/src/__tests__/config-loader-corrupt.test.ts +68 -0
  47. package/src/__tests__/config-loader-platform-defaults.test.ts +345 -8
  48. package/src/__tests__/config-schema-cmd.test.ts +63 -29
  49. package/src/__tests__/config-schema.test.ts +14 -3
  50. package/src/__tests__/config-set-platform-guard.test.ts +75 -152
  51. package/src/__tests__/config-set-route.test.ts +198 -0
  52. package/src/__tests__/config-watcher.test.ts +6 -0
  53. package/src/__tests__/contacts-tools.test.ts +51 -199
  54. package/src/__tests__/context-search-agent-protocol.test.ts +21 -2
  55. package/src/__tests__/context-search-agent-runner.test.ts +22 -138
  56. package/src/__tests__/context-search-conversations-source.test.ts +42 -16
  57. package/src/__tests__/context-search-fanout.test.ts +20 -157
  58. package/src/__tests__/context-search-memory-source.test.ts +3 -26
  59. package/src/__tests__/context-search-memory-v2-source.test.ts +3 -3
  60. package/src/__tests__/context-search-types.test.ts +7 -2
  61. package/src/__tests__/context-window-manager.test.ts +389 -1
  62. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -6
  63. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  64. package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -1
  65. package/src/__tests__/conversation-agent-loop.test.ts +3 -3
  66. package/src/__tests__/conversation-confirmation-signals.test.ts +5 -13
  67. package/src/__tests__/conversation-crud-inference-profile.test.ts +100 -0
  68. package/src/__tests__/conversation-error.test.ts +38 -0
  69. package/src/__tests__/conversation-fork-crud.test.ts +241 -1
  70. package/src/__tests__/conversation-inference-profile-route.test.ts +14 -14
  71. package/src/__tests__/conversation-init.benchmark.test.ts +2 -1
  72. package/src/__tests__/conversation-lifecycle.test.ts +124 -0
  73. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +100 -1
  74. package/src/__tests__/conversation-process-callsite.test.ts +22 -7
  75. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -6
  76. package/src/__tests__/conversation-runtime-assembly.test.ts +19 -10
  77. package/src/__tests__/conversation-slash-commands.test.ts +194 -2
  78. package/src/__tests__/conversation-slash-unknown.test.ts +1 -6
  79. package/src/__tests__/conversation-surfaces-action-delivery.test.ts +170 -9
  80. package/src/__tests__/conversation-surfaces-app-control.test.ts +323 -3
  81. package/src/__tests__/conversation-surfaces-data-persist.test.ts +73 -1
  82. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +59 -0
  83. package/src/__tests__/conversation-workspace-injection.test.ts +1 -7
  84. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -7
  85. package/src/__tests__/credential-security-invariants.test.ts +5 -6
  86. package/src/__tests__/daemon-credential-client.test.ts +56 -1
  87. package/src/__tests__/db-activation-state-fk-cascade.test.ts +132 -0
  88. package/src/__tests__/db-conversation-inference-profile-migration.test.ts +37 -0
  89. package/src/__tests__/db-memory-graph-event-date-repair.test.ts +43 -20
  90. package/src/__tests__/db-proxy-transaction.test.ts +206 -0
  91. package/src/__tests__/external-plugin-loader.test.ts +458 -0
  92. package/src/__tests__/filing-service.test.ts +25 -22
  93. package/src/__tests__/fixtures/mock-chrome-extension.ts +5 -0
  94. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  95. package/src/__tests__/graph-extraction-event-date.test.ts +34 -0
  96. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +10 -34
  97. package/src/__tests__/heartbeat-disk-pressure.test.ts +21 -8
  98. package/src/__tests__/heartbeat-service.test.ts +50 -233
  99. package/src/__tests__/history-repair.test.ts +89 -0
  100. package/src/__tests__/host-app-control-proxy.test.ts +109 -1
  101. package/src/__tests__/host-app-control-routes.test.ts +247 -1
  102. package/src/__tests__/host-browser-proxy.test.ts +416 -20
  103. package/src/__tests__/host-browser-routes.test.ts +325 -33
  104. package/src/__tests__/host-proxy-preactivation.test.ts +211 -0
  105. package/src/__tests__/inference-no-mode-boot-e2e.test.ts +246 -0
  106. package/src/__tests__/inference-profile-reaper.test.ts +154 -0
  107. package/src/__tests__/inference-profile-session-handler.test.ts +398 -0
  108. package/src/__tests__/inference-profile-session-ipc.test.ts +236 -0
  109. package/src/__tests__/injector-chain.test.ts +24 -16
  110. package/src/__tests__/injector-pkb-v2-silenced.test.ts +10 -7
  111. package/src/__tests__/inline-skill-load-permissions.test.ts +6 -1
  112. package/src/__tests__/install-skill-routing.test.ts +2 -2
  113. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +169 -67
  114. package/src/__tests__/llm-callsite-catalog.test.ts +20 -1
  115. package/src/__tests__/llm-catalog-parity.test.ts +146 -0
  116. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +188 -0
  117. package/src/__tests__/llm-request-log-source-factory.test.ts +124 -0
  118. package/src/__tests__/llm-resolver.test.ts +46 -0
  119. package/src/__tests__/managed-profile-guard.test.ts +131 -2
  120. package/src/__tests__/mcp-auth-routes.test.ts +1 -0
  121. package/src/__tests__/mcp-cli.test.ts +182 -220
  122. package/src/__tests__/mcp-health-check.test.ts +56 -27
  123. package/src/__tests__/memory-jobs-worker-lanes.test.ts +18 -11
  124. package/src/__tests__/message-complete-display-id.test.ts +175 -0
  125. package/src/__tests__/notification-decision-fallback.test.ts +91 -0
  126. package/src/__tests__/notification-decision-strategy.test.ts +22 -0
  127. package/src/__tests__/notification-platform-adapter.test.ts +229 -0
  128. package/src/__tests__/oauth-cli.test.ts +38 -1888
  129. package/src/__tests__/oauth-commands-routes.test.ts +711 -0
  130. package/src/__tests__/oauth-connect-routes.test.ts +174 -11
  131. package/src/__tests__/oauth-providers-routes.test.ts +14 -10
  132. package/src/__tests__/openai-responses-cutover-guard.test.ts +33 -12
  133. package/src/__tests__/openai-responses-provider.test.ts +17 -0
  134. package/src/__tests__/plugin-bootstrap.test.ts +31 -2
  135. package/src/__tests__/plugin-route-contribution.test.ts +31 -3
  136. package/src/__tests__/plugin-tool-contribution.test.ts +31 -3
  137. package/src/__tests__/plugin-types.test.ts +13 -11
  138. package/src/__tests__/process-message-background-slack.test.ts +46 -0
  139. package/src/__tests__/profile-entry-status.test.ts +43 -0
  140. package/src/__tests__/provider-managed-proxy-integration.test.ts +12 -4
  141. package/src/__tests__/provider-registry-ollama.test.ts +12 -4
  142. package/src/__tests__/provider-send-message-override-profile.test.ts +10 -4
  143. package/src/__tests__/relay-server.test.ts +164 -2
  144. package/src/__tests__/retry-thinking-tool-choice.test.ts +15 -0
  145. package/src/__tests__/schedule-retry.test.ts +56 -4
  146. package/src/__tests__/schedule-routes.test.ts +104 -0
  147. package/src/__tests__/scheduler-disk-pressure.test.ts +0 -4
  148. package/src/__tests__/scheduler-recurrence.test.ts +87 -34
  149. package/src/__tests__/scheduler-reuse-conversation.test.ts +161 -5
  150. package/src/__tests__/scheduler-wake.test.ts +0 -63
  151. package/src/__tests__/secret-allowlist.test.ts +1 -0
  152. package/src/__tests__/secret-prompt-log-hygiene.test.ts +7 -5
  153. package/src/__tests__/secret-prompter-channel-fallback.test.ts +7 -5
  154. package/src/__tests__/secret-response-routing.test.ts +7 -5
  155. package/src/__tests__/secret-routes-managed-proxy.test.ts +12 -4
  156. package/src/__tests__/server-history-render.test.ts +82 -0
  157. package/src/__tests__/shell-credential-ref.test.ts +95 -3
  158. package/src/__tests__/shell-tool-proxy-mode.test.ts +14 -0
  159. package/src/__tests__/skill-include-graph.test.ts +31 -0
  160. package/src/__tests__/skill-load-feature-flag.test.ts +1 -0
  161. package/src/__tests__/skill-load-tool.test.ts +42 -16
  162. package/src/__tests__/skills.test.ts +39 -0
  163. package/src/__tests__/subagent-call-site-routing.test.ts +78 -16
  164. package/src/__tests__/suggestion-routes.test.ts +3 -3
  165. package/src/__tests__/sync-message-contract.test.ts +63 -0
  166. package/src/__tests__/task-scheduler.test.ts +88 -23
  167. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -42
  168. package/src/__tests__/tool-executor.test.ts +155 -0
  169. package/src/__tests__/update-bulletin-job.test.ts +96 -193
  170. package/src/__tests__/usage-cli.test.ts +11 -73
  171. package/src/__tests__/user-plugin-loader.test.ts +145 -0
  172. package/src/__tests__/vercel-config.test.ts +168 -0
  173. package/src/__tests__/voice-session-bridge.test.ts +3 -0
  174. package/src/__tests__/web-search-catalog-parity.test.ts +86 -0
  175. package/src/__tests__/web-search.test.ts +303 -2
  176. package/src/__tests__/workspace-migration-039-drop-legacy-llm-keys.test.ts +1 -21
  177. package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +58 -0
  178. package/src/__tests__/workspace-migration-069-seed-onboarding-threads.test.ts +153 -0
  179. package/src/__tests__/workspace-migration-071-remove-safe-storage-release-note.test.ts +206 -0
  180. package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +191 -0
  181. package/src/__tests__/workspace-migration-076-drop-services-inference-mode.test.ts +211 -0
  182. package/src/__tests__/workspace-migration-077-seed-memory-router-callsite.test.ts +174 -0
  183. package/src/__tests__/workspace-migration-079-home-feed-notification-only.test.ts +323 -0
  184. package/src/__tests__/workspace-migration-080-restrict-vercel-api-token-metadata.test.ts +299 -0
  185. package/src/__tests__/workspace-migration-081-backfill-bash-allowed-tools.test.ts +410 -0
  186. package/src/__tests__/workspace-migration-082-backfill-managed-profile-labels.test.ts +268 -0
  187. package/src/__tests__/workspace-migration-safe-storage-limits-release.test.ts +15 -27
  188. package/src/__tests__/workspace-migration-unify-llm-callsite-configs.test.ts +3 -3
  189. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +115 -0
  190. package/src/acp/__tests__/helpers/which-stub.ts +4 -2
  191. package/src/acp/resolve-agent.test.ts +25 -0
  192. package/src/acp/resolve-agent.ts +13 -2
  193. package/src/acp/session-manager.ts +14 -0
  194. package/src/agent/loop.ts +11 -0
  195. package/src/approvals/guardian-decision-primitive.ts +0 -13
  196. package/src/approvals/guardian-request-resolvers.ts +19 -102
  197. package/src/calls/call-constants.ts +5 -8
  198. package/src/calls/call-controller.ts +130 -67
  199. package/src/calls/relay-server.ts +42 -1
  200. package/src/calls/relay-setup-router.ts +36 -0
  201. package/src/calls/types.ts +1 -0
  202. package/src/calls/voice-session-bridge.ts +24 -5
  203. package/src/channels/config.ts +14 -1
  204. package/src/channels/types.ts +1 -0
  205. package/src/cli/AGENTS.md +164 -4
  206. package/src/cli/__tests__/notifications.test.ts +54 -0
  207. package/src/cli/commands/__tests__/avatar.test.ts +540 -0
  208. package/src/cli/commands/__tests__/backup.test.ts +236 -776
  209. package/src/cli/commands/__tests__/cache.test.ts +1 -1
  210. package/src/cli/commands/__tests__/changelog.test.ts +593 -0
  211. package/src/cli/commands/__tests__/channel-verification-sessions.test.ts +503 -0
  212. package/src/cli/commands/__tests__/conversations-import.test.ts +515 -0
  213. package/src/cli/commands/__tests__/domain-register.test.ts +140 -167
  214. package/src/cli/commands/__tests__/domain-status.test.ts +137 -76
  215. package/src/cli/commands/__tests__/email-attachment.test.ts +314 -337
  216. package/src/cli/commands/__tests__/email-core.test.ts +579 -0
  217. package/src/cli/commands/__tests__/image-generation.test.ts +87 -824
  218. package/src/cli/commands/__tests__/inference-send.test.ts +30 -266
  219. package/src/cli/commands/__tests__/inference-session.test.ts +423 -0
  220. package/src/cli/commands/__tests__/memory-v2.test.ts +81 -110
  221. package/src/cli/commands/__tests__/skills.test.ts +563 -0
  222. package/src/cli/commands/__tests__/status.test.ts +249 -0
  223. package/src/cli/commands/__tests__/stt.test.ts +320 -0
  224. package/src/cli/commands/__tests__/tts-synthesize.test.ts +4 -603
  225. package/src/cli/commands/__tests__/tts.test.ts +321 -0
  226. package/src/cli/commands/__tests__/webhooks.test.ts +86 -511
  227. package/src/cli/commands/attachment.ts +8 -3
  228. package/src/cli/commands/audit.ts +95 -64
  229. package/src/cli/commands/auth.ts +61 -58
  230. package/src/cli/commands/avatar.ts +276 -390
  231. package/src/cli/commands/backup.ts +409 -505
  232. package/src/cli/commands/bash.ts +9 -5
  233. package/src/cli/commands/browser.ts +28 -9
  234. package/src/cli/commands/cache.ts +9 -4
  235. package/src/cli/commands/changelog.ts +414 -0
  236. package/src/cli/commands/channel-verification-sessions.ts +238 -317
  237. package/src/cli/commands/clients.ts +8 -3
  238. package/src/cli/commands/completions.ts +9 -9
  239. package/src/cli/commands/config.ts +102 -72
  240. package/src/cli/commands/contacts.ts +575 -696
  241. package/src/cli/commands/conversations-defer.ts +17 -69
  242. package/src/cli/commands/conversations-import.ts +90 -253
  243. package/src/cli/commands/conversations.ts +346 -436
  244. package/src/cli/commands/credential-execution.ts +9 -6
  245. package/src/cli/commands/credentials.ts +456 -736
  246. package/src/cli/commands/domain.ts +128 -206
  247. package/src/cli/commands/email.ts +606 -794
  248. package/src/cli/commands/gateway.ts +8 -1
  249. package/src/cli/commands/image-generation.ts +157 -205
  250. package/src/cli/commands/inference-providers.ts +352 -0
  251. package/src/cli/commands/inference-session.ts +415 -0
  252. package/src/cli/commands/inference.ts +87 -65
  253. package/src/cli/commands/keys.ts +8 -3
  254. package/src/cli/commands/mcp.ts +103 -287
  255. package/src/cli/commands/memory-v2.ts +163 -517
  256. package/src/cli/commands/notifications.ts +33 -7
  257. package/src/cli/commands/oauth/apps.ts +292 -261
  258. package/src/cli/commands/oauth/connect.ts +182 -345
  259. package/src/cli/commands/oauth/disconnect.ts +16 -215
  260. package/src/cli/commands/oauth/index.ts +49 -45
  261. package/src/cli/commands/oauth/mode.ts +43 -199
  262. package/src/cli/commands/oauth/ping.ts +17 -125
  263. package/src/cli/commands/oauth/providers.ts +732 -921
  264. package/src/cli/commands/oauth/request.ts +60 -350
  265. package/src/cli/commands/oauth/shared.ts +11 -121
  266. package/src/cli/commands/oauth/status.ts +31 -121
  267. package/src/cli/commands/oauth/token.ts +13 -55
  268. package/src/cli/commands/pending.ts +19 -10
  269. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +133 -183
  270. package/src/cli/commands/platform/__tests__/connect.test.ts +66 -181
  271. package/src/cli/commands/platform/__tests__/disconnect.test.ts +71 -227
  272. package/src/cli/commands/platform/__tests__/status.test.ts +169 -287
  273. package/src/cli/commands/platform/connect.ts +16 -80
  274. package/src/cli/commands/platform/disconnect.ts +14 -112
  275. package/src/cli/commands/platform/index.ts +177 -246
  276. package/src/cli/commands/routes.ts +153 -336
  277. package/src/cli/commands/sequence.ts +316 -360
  278. package/src/cli/commands/skills.ts +449 -671
  279. package/src/cli/commands/status.ts +58 -37
  280. package/src/cli/commands/stt.ts +94 -262
  281. package/src/cli/commands/task.ts +14 -40
  282. package/src/cli/commands/trust.ts +8 -3
  283. package/src/cli/commands/tts.ts +162 -167
  284. package/src/cli/commands/ui.ts +35 -42
  285. package/src/cli/commands/usage.ts +188 -126
  286. package/src/cli/commands/watchers.ts +8 -3
  287. package/src/cli/commands/webhooks.ts +99 -193
  288. package/src/cli/lib/__tests__/register-command.test.ts +85 -0
  289. package/src/cli/lib/daemon-credential-client.ts +4 -5
  290. package/src/cli/lib/nested-value.ts +44 -0
  291. package/src/cli/lib/open-browser.ts +36 -0
  292. package/src/cli/lib/register-command.ts +19 -0
  293. package/src/cli/lib/time-ago.ts +34 -0
  294. package/src/cli/program.ts +2 -4
  295. package/src/cli/utils/__tests__/conversation-id.test.ts +66 -0
  296. package/src/cli/utils/__tests__/parse-duration.test.ts +49 -0
  297. package/src/cli/utils/conversation-id.ts +30 -0
  298. package/src/cli/utils/parse-duration.ts +41 -0
  299. package/src/config/acp-defaults.test.ts +5 -1
  300. package/src/config/acp-defaults.ts +11 -4
  301. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  302. package/src/config/bundled-skills/app-builder/SKILL.md +1 -3
  303. package/src/config/bundled-skills/app-control/TOOLS.json +32 -0
  304. package/src/config/bundled-skills/contacts/SKILL.md +12 -45
  305. package/src/config/bundled-skills/contacts/TOOLS.json +0 -57
  306. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +0 -12
  307. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +0 -58
  308. package/src/config/bundled-tool-registry.ts +0 -2
  309. package/src/config/feature-flag-registry.json +17 -17
  310. package/src/config/llm-resolver.ts +16 -1
  311. package/src/config/loader.ts +148 -33
  312. package/src/config/raw-config-utils.ts +2 -30
  313. package/src/config/schema.ts +4 -0
  314. package/src/config/schemas/__tests__/memory-v2.test.ts +49 -0
  315. package/src/config/schemas/call-site-catalog.ts +29 -7
  316. package/src/config/schemas/llm-request-logs.ts +57 -0
  317. package/src/config/schemas/llm.ts +52 -2
  318. package/src/config/schemas/memory-retrospective.ts +48 -0
  319. package/src/config/schemas/memory-v2.ts +33 -2
  320. package/src/config/schemas/memory.ts +4 -0
  321. package/src/config/schemas/services.ts +15 -12
  322. package/src/config/seed-inference-profiles.ts +195 -134
  323. package/src/contacts/contact-store.ts +0 -61
  324. package/src/context/window-manager.ts +191 -5
  325. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +111 -0
  326. package/src/daemon/__tests__/conversation-tool-setup.test.ts +109 -4
  327. package/src/daemon/__tests__/daemon-skill-host.test.ts +10 -4
  328. package/src/daemon/approval-generators.ts +23 -29
  329. package/src/daemon/config-watcher.ts +2 -0
  330. package/src/daemon/conversation-agent-loop-handlers.ts +56 -0
  331. package/src/daemon/conversation-agent-loop.ts +140 -107
  332. package/src/daemon/conversation-error.ts +21 -0
  333. package/src/daemon/conversation-lifecycle.ts +68 -13
  334. package/src/daemon/conversation-process.ts +36 -19
  335. package/src/daemon/conversation-runtime-assembly.ts +14 -5
  336. package/src/daemon/conversation-slash.ts +175 -23
  337. package/src/daemon/conversation-store.ts +17 -10
  338. package/src/daemon/conversation-surfaces.ts +92 -26
  339. package/src/daemon/conversation-tool-setup.ts +33 -19
  340. package/src/daemon/conversation.ts +49 -10
  341. package/src/daemon/external-plugins-bootstrap.ts +18 -8
  342. package/src/daemon/guardian-action-generators.ts +7 -22
  343. package/src/daemon/handlers/config-model.ts +8 -126
  344. package/src/daemon/handlers/config-slack-channel.ts +10 -7
  345. package/src/daemon/handlers/config-vercel.ts +3 -1
  346. package/src/daemon/handlers/shared.ts +26 -0
  347. package/src/daemon/handlers/skills.ts +84 -5
  348. package/src/daemon/history-repair.ts +33 -6
  349. package/src/daemon/host-app-control-proxy.ts +44 -19
  350. package/src/daemon/host-bash-proxy.ts +85 -158
  351. package/src/daemon/host-browser-proxy.ts +97 -36
  352. package/src/daemon/host-cu-proxy.ts +1 -1
  353. package/src/daemon/host-file-proxy.ts +1 -1
  354. package/src/daemon/host-proxy-base.ts +13 -1
  355. package/src/daemon/host-proxy-preactivation.ts +25 -1
  356. package/src/daemon/host-transfer-proxy.ts +2 -2
  357. package/src/daemon/identity-helpers.ts +19 -0
  358. package/src/daemon/lifecycle.ts +128 -114
  359. package/src/daemon/meet-host-supervisor.ts +15 -15
  360. package/src/daemon/memory-v2-startup.ts +62 -14
  361. package/src/daemon/message-protocol.ts +6 -0
  362. package/src/daemon/message-types/bookmarks.ts +18 -0
  363. package/src/daemon/message-types/conversations.ts +12 -9
  364. package/src/daemon/message-types/messages.ts +28 -2
  365. package/src/daemon/message-types/sync.ts +60 -0
  366. package/src/daemon/pkb-reminder-builder.test.ts +54 -13
  367. package/src/daemon/pkb-reminder-builder.ts +21 -7
  368. package/src/daemon/process-message.ts +56 -23
  369. package/src/daemon/server.ts +23 -18
  370. package/src/daemon/shutdown-handlers.ts +0 -2
  371. package/src/daemon/tool-setup-types.ts +9 -0
  372. package/src/daemon/tool-side-effects.ts +6 -4
  373. package/src/daemon/wake-target-adapter.ts +11 -0
  374. package/src/documents/document-store.ts +35 -1
  375. package/src/export/transcript-formatter.ts +61 -2
  376. package/src/filing/filing-service.ts +42 -56
  377. package/src/heartbeat/__tests__/heartbeat-service.test.ts +359 -0
  378. package/src/heartbeat/heartbeat-run-store.ts +2 -1
  379. package/src/heartbeat/heartbeat-service.ts +149 -128
  380. package/src/home/__tests__/feed-types.test.ts +63 -131
  381. package/src/home/__tests__/feed-writer.test.ts +77 -278
  382. package/src/home/__tests__/post-connect-feed.test.ts +9 -12
  383. package/src/home/feed-types.ts +19 -73
  384. package/src/home/feed-writer.ts +25 -156
  385. package/src/home/post-connect-feed.ts +1 -3
  386. package/src/ipc/__tests__/cli-ipc.test.ts +2 -0
  387. package/src/ipc/__tests__/email-ipc.test.ts +506 -0
  388. package/src/ipc/__tests__/exit-helper.test.ts +104 -0
  389. package/src/ipc/__tests__/streaming-client.test.ts +237 -0
  390. package/src/ipc/__tests__/streaming-framing.test.ts +142 -0
  391. package/src/ipc/assistant-server.ts +148 -42
  392. package/src/ipc/cli-client.ts +370 -50
  393. package/src/ipc/routes/db-proxy-transaction.ts +151 -0
  394. package/src/ipc/skill-routes/__tests__/events-ipc.test.ts +60 -0
  395. package/src/ipc/skill-routes/events.ts +30 -3
  396. package/src/ipc/skill-server.ts +99 -42
  397. package/src/live-voice/__tests__/live-voice-session-manager.test.ts +46 -0
  398. package/src/live-voice/__tests__/runtime-websocket-shell.test.ts +1 -0
  399. package/src/live-voice/live-voice-session-manager.ts +11 -4
  400. package/src/live-voice/live-voice-session.ts +14 -6
  401. package/src/memory/__tests__/bookmark-crud.test.ts +258 -0
  402. package/src/memory/__tests__/bookmark-schema.test.ts +181 -0
  403. package/src/memory/__tests__/conversation-types.test.ts +36 -0
  404. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +130 -0
  405. package/src/memory/__tests__/jobs-worker-v2-schedule.test.ts +10 -57
  406. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +177 -0
  407. package/src/memory/__tests__/memory-retrospective-job.test.ts +328 -0
  408. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +213 -0
  409. package/src/memory/__tests__/memory-retrospective-trigger-check.test.ts +90 -0
  410. package/src/memory/__tests__/memory-v2-activation-log-store.test.ts +69 -0
  411. package/src/memory/__tests__/memory-v2-concept-frequency.test.ts +3 -0
  412. package/src/memory/bookmark-crud.ts +179 -0
  413. package/src/memory/context-search/__tests__/agent-runner-redaction.test.ts +31 -9
  414. package/src/memory/context-search/agent-protocol.ts +5 -1
  415. package/src/memory/context-search/agent-runner.ts +60 -85
  416. package/src/memory/context-search/limits.ts +1 -4
  417. package/src/memory/context-search/search.ts +23 -113
  418. package/src/memory/context-search/sources/conversations.ts +18 -6
  419. package/src/memory/context-search/sources/memory-v2.ts +40 -31
  420. package/src/memory/context-search/sources/memory.ts +9 -2
  421. package/src/memory/context-search/sources/workspace.ts +13 -10
  422. package/src/memory/context-search/types.ts +1 -1
  423. package/src/memory/conversation-bootstrap.ts +11 -0
  424. package/src/memory/conversation-crud.ts +312 -10
  425. package/src/memory/conversation-queries.ts +9 -5
  426. package/src/memory/conversation-title-service.ts +1 -0
  427. package/src/memory/conversation-types.ts +16 -0
  428. package/src/memory/db-init.ts +14 -0
  429. package/src/memory/embedding-backend.ts +2 -1
  430. package/src/memory/embedding-runtime-manager.ts +1 -2
  431. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +104 -61
  432. package/src/memory/graph/__tests__/handle-remember-v2.test.ts +11 -26
  433. package/src/memory/graph/__tests__/remember-description.test.ts +55 -0
  434. package/src/memory/graph/conversation-graph-memory.ts +108 -14
  435. package/src/memory/graph/extraction.ts +4 -0
  436. package/src/memory/graph/graph-memory-state-store.ts +16 -3
  437. package/src/memory/graph/graph-search.test.ts +6 -5
  438. package/src/memory/graph/graph-search.ts +3 -4
  439. package/src/memory/graph/retriever.test.ts +12 -7
  440. package/src/memory/graph/retriever.ts +4 -5
  441. package/src/memory/graph/tool-handlers.ts +20 -11
  442. package/src/memory/graph/tools.ts +48 -9
  443. package/src/memory/indexer.ts +18 -2
  444. package/src/memory/jobs/__tests__/embed-concept-page.test.ts +120 -6
  445. package/src/memory/jobs/embed-concept-page.ts +261 -89
  446. package/src/memory/jobs-store.ts +51 -1
  447. package/src/memory/jobs-worker.ts +60 -7
  448. package/src/memory/llm-request-log-source-clickhouse.ts +317 -0
  449. package/src/memory/llm-request-log-source-local.ts +26 -0
  450. package/src/memory/llm-request-log-source.ts +97 -0
  451. package/src/memory/llm-request-log-store.ts +1 -1
  452. package/src/memory/memory-retrospective-constants.ts +13 -0
  453. package/src/memory/memory-retrospective-enqueue.ts +114 -0
  454. package/src/memory/memory-retrospective-job.ts +351 -0
  455. package/src/memory/memory-retrospective-startup-cleanup.ts +108 -0
  456. package/src/memory/memory-retrospective-state.ts +162 -0
  457. package/src/memory/memory-retrospective-trigger-check.ts +91 -0
  458. package/src/memory/memory-v2-activation-log-store.ts +49 -5
  459. package/src/memory/memory-v2-concept-frequency.ts +4 -0
  460. package/src/memory/message-content.ts +38 -1
  461. package/src/memory/migrations/227-add-conversation-inference-profile.ts +6 -1
  462. package/src/memory/migrations/228-rename-inference-profile-snake-case.ts +20 -7
  463. package/src/memory/migrations/229-delete-private-conversations.test.ts +70 -1
  464. package/src/memory/migrations/229-delete-private-conversations.ts +12 -0
  465. package/src/memory/migrations/231-repair-memory-graph-event-dates.ts +16 -2
  466. package/src/memory/migrations/240-conversation-inference-profile-session.ts +25 -0
  467. package/src/memory/migrations/241-activation-state-fk-cascade.ts +50 -0
  468. package/src/memory/migrations/242-message-bookmarks.ts +38 -0
  469. package/src/memory/migrations/243-provider-connections.ts +68 -0
  470. package/src/memory/migrations/244-provider-connection-status-label.ts +23 -0
  471. package/src/memory/migrations/245-memory-retrospective-state.ts +36 -0
  472. package/src/memory/migrations/246-backfill-provider-connection-label.ts +81 -0
  473. package/src/memory/migrations/__tests__/244-provider-connection-status-label.test.ts +84 -0
  474. package/src/memory/migrations/__tests__/245-memory-retrospective-state.test.ts +125 -0
  475. package/src/memory/migrations/__tests__/246-backfill-provider-connection-label.test.ts +192 -0
  476. package/src/memory/migrations/index.ts +7 -0
  477. package/src/memory/pkb/pkb-search.test.ts +6 -5
  478. package/src/memory/pkb/pkb-search.ts +4 -5
  479. package/src/memory/published-pages-store.ts +16 -0
  480. package/src/memory/qdrant-client.ts +3 -0
  481. package/src/memory/schema/bookmarks.ts +38 -0
  482. package/src/memory/schema/conversations.ts +2 -0
  483. package/src/memory/schema/index.ts +2 -0
  484. package/src/memory/schema/inference.ts +29 -0
  485. package/src/memory/schema/memory-core.ts +9 -0
  486. package/src/memory/search/semantic.ts +5 -9
  487. package/src/memory/v2/__tests__/__snapshots__/prompts-router.test.ts.snap +27 -0
  488. package/src/memory/v2/__tests__/activation-store.test.ts +5 -5
  489. package/src/memory/v2/__tests__/activation.test.ts +46 -9
  490. package/src/memory/v2/__tests__/backfill-jobs.test.ts +38 -21
  491. package/src/memory/v2/__tests__/consolidation-job.test.ts +140 -163
  492. package/src/memory/v2/__tests__/edge-index.test.ts +1 -1
  493. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +111 -0
  494. package/src/memory/v2/__tests__/injection.test.ts +768 -33
  495. package/src/memory/v2/__tests__/migration.test.ts +7 -3
  496. package/src/memory/v2/__tests__/page-index.test.ts +277 -0
  497. package/src/memory/v2/__tests__/page-store.test.ts +14 -1
  498. package/src/memory/v2/__tests__/prompts-router.test.ts +257 -0
  499. package/src/memory/v2/__tests__/qdrant.test.ts +382 -9
  500. package/src/memory/v2/__tests__/reranker.test.ts +4 -4
  501. package/src/memory/v2/__tests__/router.test.ts +516 -0
  502. package/src/memory/v2/__tests__/sim.test.ts +163 -8
  503. package/src/memory/v2/__tests__/skill-store.test.ts +58 -3
  504. package/src/memory/v2/__tests__/static-context.test.ts +8 -35
  505. package/src/memory/v2/__tests__/sweep-job.test.ts +114 -33
  506. package/src/memory/v2/activation-store.ts +34 -5
  507. package/src/memory/v2/activation.ts +40 -27
  508. package/src/memory/v2/backfill-jobs.ts +17 -84
  509. package/src/memory/v2/consolidation-job.ts +92 -86
  510. package/src/memory/v2/frontmatter-sweep.ts +91 -0
  511. package/src/memory/v2/injection.ts +466 -115
  512. package/src/memory/v2/migration.ts +117 -20
  513. package/src/memory/v2/page-index.ts +191 -0
  514. package/src/memory/v2/page-store.ts +42 -0
  515. package/src/memory/v2/prompts/consolidation.ts +14 -7
  516. package/src/memory/v2/prompts/router.ts +192 -0
  517. package/src/memory/v2/qdrant.ts +307 -133
  518. package/src/memory/v2/reranker.ts +14 -7
  519. package/src/memory/v2/router.ts +322 -0
  520. package/src/memory/v2/sim.ts +88 -34
  521. package/src/memory/v2/skill-store.ts +118 -29
  522. package/src/memory/v2/static-context.ts +20 -17
  523. package/src/memory/v2/sweep-job.ts +127 -102
  524. package/src/memory/v2/types.ts +16 -5
  525. package/src/memory/validation.ts +13 -0
  526. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +182 -0
  527. package/src/notifications/__tests__/home-feed-side-effect.test.ts +199 -0
  528. package/src/notifications/__tests__/signal-registry.test.ts +17 -0
  529. package/src/notifications/adapters/platform.ts +171 -0
  530. package/src/notifications/conversation-pairing.ts +2 -2
  531. package/src/notifications/copy-composer.ts +61 -12
  532. package/src/notifications/decision-engine.ts +46 -0
  533. package/src/notifications/destination-resolver.ts +21 -0
  534. package/src/notifications/emit-signal.ts +28 -1
  535. package/src/notifications/home-feed-side-effect.ts +111 -0
  536. package/src/notifications/signal.ts +5 -0
  537. package/src/permissions/checker.ts +12 -0
  538. package/src/permissions/gateway-threshold-reader.ts +116 -8
  539. package/src/permissions/ipc-risk-types.ts +2 -0
  540. package/src/permissions/prompter.ts +86 -96
  541. package/src/permissions/secret-prompter.ts +31 -31
  542. package/src/plugin-api/index.ts +13 -0
  543. package/src/plugin-api/package.json +12 -0
  544. package/src/plugin-api/types.ts +62 -0
  545. package/src/plugins/defaults/injectors.ts +20 -5
  546. package/src/plugins/external-plugin-loader.ts +294 -0
  547. package/src/plugins/types.ts +46 -30
  548. package/src/plugins/user-loader.ts +64 -41
  549. package/src/proactive-artifact/job.test.ts +63 -8
  550. package/src/proactive-artifact/job.ts +20 -2
  551. package/src/proactive-artifact/message-copy.ts +18 -1
  552. package/src/proactive-artifact/trigger-state.test.ts +9 -0
  553. package/src/proactive-artifact/trigger-state.ts +4 -0
  554. package/src/prompts/__tests__/system-prompt.test.ts +105 -0
  555. package/src/prompts/system-prompt.ts +22 -1
  556. package/src/prompts/templates/SOUL.md +13 -28
  557. package/src/prompts/update-bulletin-job.ts +61 -73
  558. package/src/providers/__tests__/dispatch-connection-routing.test.ts +279 -0
  559. package/src/providers/__tests__/inference.test.ts +288 -0
  560. package/src/providers/__tests__/provider-env-vars.test.ts +6 -0
  561. package/src/providers/__tests__/provider-secret-catalog.test.ts +6 -0
  562. package/src/providers/__tests__/retry-callsite.test.ts +14 -32
  563. package/src/providers/__tests__/satellite-connection-routing.test.ts +510 -0
  564. package/src/providers/__tests__/search-provider-catalog.test.ts +80 -0
  565. package/src/providers/anthropic/client.ts +95 -26
  566. package/src/providers/call-site-routing.ts +94 -16
  567. package/src/providers/connection-resolution.ts +163 -0
  568. package/src/providers/inference/__tests__/connections-status-label.test.ts +250 -0
  569. package/src/providers/inference/adapter-factory.ts +173 -0
  570. package/src/providers/inference/auth.ts +112 -0
  571. package/src/providers/inference/backfill.ts +196 -0
  572. package/src/providers/inference/connections.ts +356 -0
  573. package/src/providers/inference/resolve-auth.ts +65 -0
  574. package/src/providers/model-catalog.ts +104 -6
  575. package/src/providers/openai/responses-provider.ts +4 -2
  576. package/src/providers/provider-env-vars.ts +17 -7
  577. package/src/providers/provider-secret-catalog.ts +49 -30
  578. package/src/providers/provider-send-message.ts +41 -20
  579. package/src/providers/registry.ts +143 -159
  580. package/src/providers/retry.ts +18 -10
  581. package/src/providers/search-provider-catalog.ts +121 -0
  582. package/src/runtime/AGENTS.md +18 -5
  583. package/src/runtime/__tests__/background-job-runner.test.ts +357 -0
  584. package/src/runtime/__tests__/pre-first-message-gate.test.ts +82 -0
  585. package/src/runtime/actor-trust-resolver.ts +32 -10
  586. package/src/runtime/agent-wake.ts +35 -6
  587. package/src/runtime/assistant-event-hub.ts +3 -85
  588. package/src/runtime/auth/route-policy.ts +304 -8
  589. package/src/runtime/auth/same-actor.ts +2 -0
  590. package/src/runtime/background-job-runner.ts +339 -0
  591. package/src/runtime/btw-sidechain.ts +1 -0
  592. package/src/runtime/channel-approvals.ts +3 -2
  593. package/src/runtime/guardian-reply-router.ts +0 -10
  594. package/src/runtime/http-router.ts +36 -1
  595. package/src/runtime/http-server.ts +31 -5
  596. package/src/runtime/http-types.ts +2 -0
  597. package/src/runtime/middleware/__tests__/request-logger.test.ts +162 -0
  598. package/src/runtime/middleware/request-logger.ts +62 -1
  599. package/src/runtime/pending-interactions.ts +19 -15
  600. package/src/runtime/pre-first-message-gate.ts +83 -0
  601. package/src/runtime/routes/__tests__/backup-routes.test.ts +8 -1
  602. package/src/runtime/routes/__tests__/bookmark-routes.test.ts +251 -0
  603. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +142 -0
  604. package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +315 -0
  605. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +189 -0
  606. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +15 -136
  607. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +736 -0
  608. package/src/runtime/routes/__tests__/memory-v2-routes.test.ts +147 -0
  609. package/src/runtime/routes/__tests__/stt-routes.test.ts +5 -1
  610. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +384 -0
  611. package/src/runtime/routes/__tests__/tts-routes.test.ts +6 -2
  612. package/src/runtime/routes/acp-routes.ts +10 -8
  613. package/src/runtime/routes/app-management-routes.ts +228 -3
  614. package/src/runtime/routes/approval-routes.ts +7 -21
  615. package/src/runtime/routes/audit-routes.ts +43 -0
  616. package/src/runtime/routes/auth-routes.ts +72 -0
  617. package/src/runtime/routes/avatar-routes.ts +273 -20
  618. package/src/runtime/routes/backup-routes.ts +406 -2
  619. package/src/runtime/routes/bookmark-routes.ts +154 -0
  620. package/src/runtime/routes/channel-verification-routes.ts +2 -1
  621. package/src/runtime/routes/consolidation-routes.ts +8 -9
  622. package/src/runtime/routes/contact-routes.ts +0 -160
  623. package/src/runtime/routes/conversation-cli-routes.ts +192 -0
  624. package/src/runtime/routes/conversation-management-routes.ts +30 -43
  625. package/src/runtime/routes/conversation-query-routes.ts +373 -82
  626. package/src/runtime/routes/conversation-routes.ts +31 -10
  627. package/src/runtime/routes/conversations-import-routes.ts +229 -0
  628. package/src/runtime/routes/credential-routes.ts +540 -0
  629. package/src/runtime/routes/debug-bash-routes.ts +2 -0
  630. package/src/runtime/routes/debug-routes.ts +2 -2
  631. package/src/runtime/routes/document-pdf-renderer.ts +5 -1
  632. package/src/runtime/routes/domain-routes.ts +167 -0
  633. package/src/runtime/routes/email-routes.ts +603 -0
  634. package/src/runtime/routes/errors.ts +2 -2
  635. package/src/runtime/routes/events-routes.ts +192 -0
  636. package/src/runtime/routes/filing-routes.ts +2 -3
  637. package/src/runtime/routes/home-feed-routes.ts +6 -78
  638. package/src/runtime/routes/host-app-control-routes.ts +44 -2
  639. package/src/runtime/routes/host-browser-routes.ts +103 -22
  640. package/src/runtime/routes/http-adapter.ts +2 -0
  641. package/src/runtime/routes/identity-routes.ts +5 -0
  642. package/src/runtime/routes/image-generation-routes.ts +99 -0
  643. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +137 -1
  644. package/src/runtime/routes/inbound-stages/background-dispatch.ts +87 -7
  645. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.test.ts +156 -0
  646. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +22 -7
  647. package/src/runtime/routes/index.ts +36 -0
  648. package/src/runtime/routes/inference-profile-session-handler.ts +312 -0
  649. package/src/runtime/routes/inference-profile-session-reaper.ts +98 -0
  650. package/src/runtime/routes/inference-profile-session-routes.ts +146 -0
  651. package/src/runtime/routes/inference-provider-connection-routes.ts +317 -0
  652. package/src/runtime/routes/inference-send-routes.ts +115 -0
  653. package/src/runtime/routes/integrations/twilio.ts +1 -0
  654. package/src/runtime/routes/mcp-auth-routes.ts +283 -9
  655. package/src/runtime/routes/memory-item-routes.test.ts +3 -9
  656. package/src/runtime/routes/memory-item-routes.ts +5 -6
  657. package/src/runtime/routes/memory-v2-routes.ts +105 -404
  658. package/src/runtime/routes/notification-routes.ts +2 -0
  659. package/src/runtime/routes/oauth-apps.ts +112 -7
  660. package/src/runtime/routes/oauth-commands-routes.ts +1007 -0
  661. package/src/runtime/routes/oauth-connect-routes.ts +67 -5
  662. package/src/runtime/routes/oauth-providers.ts +298 -8
  663. package/src/runtime/routes/platform-routes.ts +336 -0
  664. package/src/runtime/routes/playground/inject-failures.ts +2 -1
  665. package/src/runtime/routes/playground/reset-circuit.ts +2 -1
  666. package/src/runtime/routes/playground/state.ts +2 -1
  667. package/src/runtime/routes/publish-routes.ts +221 -0
  668. package/src/runtime/routes/schedule-routes.ts +82 -0
  669. package/src/runtime/routes/sequence-routes.ts +291 -0
  670. package/src/runtime/routes/settings-routes.ts +2 -10
  671. package/src/runtime/routes/skills-routes.ts +31 -1
  672. package/src/runtime/routes/stt-routes.ts +240 -3
  673. package/src/runtime/routes/surface-action-routes.ts +43 -7
  674. package/src/runtime/routes/tts-routes.ts +67 -0
  675. package/src/runtime/routes/types.ts +32 -0
  676. package/src/runtime/routes/user-routes-cli.ts +243 -0
  677. package/src/runtime/routes/webhook-routes.ts +165 -0
  678. package/src/runtime/sync/resource-sync-events.ts +25 -0
  679. package/src/runtime/sync/sync-publisher.test.ts +105 -0
  680. package/src/runtime/sync/sync-publisher.ts +21 -0
  681. package/src/schedule/scheduler.ts +200 -123
  682. package/src/security/__tests__/provider-key-env-fallback.test.ts +12 -6
  683. package/src/security/secret-patterns.ts +3 -0
  684. package/src/sequence/engine.ts +38 -40
  685. package/src/skills/include-graph.ts +35 -13
  686. package/src/subagent/manager.ts +20 -15
  687. package/src/tools/browser/__tests__/browser-execution-acquire.test.ts +206 -0
  688. package/src/tools/browser/browser-execution.ts +15 -4
  689. package/src/tools/browser/cdp-client/__tests__/factory.test.ts +174 -0
  690. package/src/tools/browser/cdp-client/cdp-inspect/__tests__/ws-transport.test.ts +16 -13
  691. package/src/tools/browser/cdp-client/extension-cdp-client.ts +24 -1
  692. package/src/tools/browser/cdp-client/factory.ts +66 -5
  693. package/src/tools/browser/runtime-check.ts +77 -0
  694. package/src/tools/document/document-tool.ts +20 -0
  695. package/src/tools/executor.ts +18 -2
  696. package/src/tools/memory/register.test.ts +10 -8
  697. package/src/tools/memory/register.ts +9 -1
  698. package/src/tools/network/__tests__/web-search.test.ts +156 -0
  699. package/src/tools/network/web-search.ts +280 -37
  700. package/src/tools/permission-checker.ts +28 -5
  701. package/src/tools/skills/load.ts +24 -20
  702. package/src/tools/subagent/spawn.ts +3 -3
  703. package/src/tools/terminal/shell.ts +44 -0
  704. package/src/tools/tool-name-aliases.ts +19 -0
  705. package/src/tools/types.ts +19 -1
  706. package/src/usage/attribution.ts +3 -2
  707. package/src/util/pricing.ts +86 -160
  708. package/src/watcher/__tests__/engine.test.ts +301 -0
  709. package/src/watcher/constants.ts +7 -0
  710. package/src/watcher/engine.ts +90 -90
  711. package/src/workspace/migrations/046-seed-conversation-starters-callsite.ts +6 -9
  712. package/src/workspace/migrations/054-seed-recall-callsite.ts +10 -1
  713. package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +28 -4
  714. package/src/workspace/migrations/067-release-notes-safe-storage-limits.ts +4 -62
  715. package/src/workspace/migrations/069-seed-onboarding-threads.ts +34 -0
  716. package/src/workspace/migrations/070-memory-v2-summary-schema-rebuild.ts +31 -0
  717. package/src/workspace/migrations/071-remove-safe-storage-release-note.ts +111 -0
  718. package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +104 -0
  719. package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +93 -0
  720. package/src/workspace/migrations/074-drop-deprecated-secret-detection-keys.ts +117 -0
  721. package/src/workspace/migrations/075-memory-v2-bm25-b-default-reembed.ts +61 -0
  722. package/src/workspace/migrations/076-drop-services-inference-mode.ts +62 -0
  723. package/src/workspace/migrations/077-seed-memory-router-callsite.ts +89 -0
  724. package/src/workspace/migrations/078-release-notes-tavily-web-search.ts +66 -0
  725. package/src/workspace/migrations/079-home-feed-notification-only.ts +197 -0
  726. package/src/workspace/migrations/080-restrict-vercel-api-token-metadata.ts +182 -0
  727. package/src/workspace/migrations/081-backfill-bash-allowed-tools-for-injection-credentials.ts +160 -0
  728. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +154 -0
  729. package/src/workspace/migrations/registry.ts +28 -0
  730. package/src/workspace/migrations/runner.ts +13 -2
  731. package/src/workspace/migrations/types.ts +13 -3
  732. package/src/workspace/provider-commit-message-generator.ts +3 -2
  733. package/src/__tests__/context-search-pkb-source.test.ts +0 -492
  734. package/src/__tests__/credentials-cli.test.ts +0 -1225
  735. package/src/__tests__/memory-admin-recall.test.ts +0 -213
  736. package/src/approvals/__tests__/guardian-feed-event.test.ts +0 -303
  737. package/src/cli/commands/__tests__/email-download.test.ts +0 -260
  738. package/src/cli/commands/__tests__/email-list.test.ts +0 -216
  739. package/src/cli/commands/__tests__/email-register.test.ts +0 -186
  740. package/src/cli/commands/__tests__/email-send.test.ts +0 -416
  741. package/src/cli/commands/__tests__/email-status.test.ts +0 -185
  742. package/src/cli/commands/__tests__/email-unregister.test.ts +0 -168
  743. package/src/cli/commands/__tests__/routes.test.ts +0 -562
  744. package/src/cli/commands/__tests__/stt-transcribe.test.ts +0 -454
  745. package/src/cli/commands/autonomy.ts +0 -365
  746. package/src/cli/commands/memory.ts +0 -424
  747. package/src/cli/commands/oauth/__tests__/connect.test.ts +0 -1201
  748. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +0 -686
  749. package/src/cli/commands/oauth/__tests__/mode.test.ts +0 -632
  750. package/src/cli/commands/oauth/__tests__/ping.test.ts +0 -631
  751. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +0 -573
  752. package/src/cli/commands/oauth/__tests__/providers-register.test.ts +0 -330
  753. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +0 -521
  754. package/src/cli/commands/oauth/__tests__/status.test.ts +0 -551
  755. package/src/cli/commands/oauth/__tests__/token.test.ts +0 -420
  756. package/src/cli/lib/daemon-avatar-client.ts +0 -37
  757. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +0 -87
  758. package/src/config/bundled-skills/messaging/tools/__tests__/messaging-feed-events.test.ts +0 -207
  759. package/src/daemon/__tests__/conversation-feed-event.test.ts +0 -304
  760. package/src/heartbeat/__tests__/heartbeat-feed-event.test.ts +0 -233
  761. package/src/home/__tests__/assistant-feed-authoring.test.ts +0 -156
  762. package/src/home/__tests__/emit-feed-event.test.ts +0 -169
  763. package/src/home/__tests__/feed-population-integration.test.ts +0 -312
  764. package/src/home/__tests__/feed-scheduler.test.ts +0 -222
  765. package/src/home/__tests__/phase5-exit-criteria.test.ts +0 -229
  766. package/src/home/__tests__/platform-gmail-digest.test.ts +0 -222
  767. package/src/home/__tests__/rollup-producer.test.ts +0 -507
  768. package/src/home/assistant-feed-authoring.ts +0 -135
  769. package/src/home/emit-feed-event.ts +0 -169
  770. package/src/home/feed-scheduler.ts +0 -281
  771. package/src/home/platform-gmail-digest.ts +0 -163
  772. package/src/home/rewrite-command-preview.ts +0 -66
  773. package/src/home/rewrite-feed-title.ts +0 -58
  774. package/src/home/rollup-producer.ts +0 -426
  775. package/src/memory/admin.ts +0 -326
  776. package/src/memory/context-search/sources/pkb.ts +0 -477
  777. package/src/memory/graph/compaction.ts +0 -299
  778. /package/src/cli/{commands → lib}/cache-fs.ts +0 -0
@@ -1,366 +1,32 @@
1
- import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
1
+ /**
2
+ * CLI plumbing tests for `assistant/src/cli/commands/oauth/`.
3
+ *
4
+ * The oauth CLI commands are thin wrappers around `cliIpcCall(...)`; daemon
5
+ * route handlers in `runtime/routes/oauth-commands-routes.ts` execute the
6
+ * actual work. Tests here focus on argument handling and helpers that live
7
+ * entirely in the CLI process.
8
+ *
9
+ * What lives here today:
10
+ * - `requirePlatformConnection` helper — gates managed-mode operations on
11
+ * a connected platform; used by multiple oauth subcommands.
12
+ *
13
+ * Daemon-side coverage of the IPC endpoints lives in
14
+ * `oauth-commands-routes.test.ts`. Underlying store and token-refresh logic
15
+ * is covered by `oauth-store.test.ts` and `credential-vault.test.ts`.
16
+ *
17
+ * Follow-up opportunities for CLI-layer coverage:
18
+ * - `exitFromIpcResult` exit-code mapping
19
+ * - `shouldOutputJson` / `writeOutput` output formatting
20
+ * - `oauth token` shell-lockdown guard (`VELLUM_UNTRUSTED_SHELL=1`)
21
+ * - per-subcommand argument parsing & help text
22
+ */
23
+
24
+ import { describe, expect, mock, test } from "bun:test";
2
25
 
3
26
  import { Command } from "commander";
4
27
 
5
- // ---------------------------------------------------------------------------
6
- // Mock state
7
- // ---------------------------------------------------------------------------
8
-
9
- let mockWithValidToken: <T>(
10
- service: string,
11
- cb: (token: string) => Promise<T>,
12
- ) => Promise<T>;
13
-
14
- let mockListProviders: () => Array<Record<string, unknown>> = () => [];
15
- const secureKeyStore = new Map<string, string>();
16
- const metadataStore: Array<{
17
- credentialId: string;
18
- service: string;
19
- field: string;
20
- allowedTools: string[];
21
- allowedDomains: string[];
22
- createdAt: number;
23
- updatedAt: number;
24
- }> = [];
25
- const disconnectOAuthProviderCalls: string[] = [];
26
- const disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
27
- "not-found";
28
-
29
- // In-memory provider store used by registerProvider/updateProvider/getProvider
30
- // mocks below. Tests that exercise the providers register/update/get commands
31
- // can read and write through this map directly.
32
- const mockProviderStore = new Map<string, Record<string, unknown>>();
33
-
34
- // App upsert mock state
35
- let mockUpsertAppCalls: Array<{
36
- provider: string;
37
- clientId: string;
38
- clientSecretOpts?: {
39
- clientSecretValue?: string;
40
- clientSecretCredentialPath?: string;
41
- };
42
- }> = [];
43
- let mockUpsertAppResult: Record<string, unknown> = {
44
- id: "app-upsert-1",
45
- provider: "test",
46
- clientId: "test-client-id",
47
- createdAt: 1700000000000,
48
- updatedAt: 1700000000000,
49
- };
50
- let mockUpsertAppImpl:
51
- | ((
52
- provider: string,
53
- clientId: string,
54
- clientSecretOpts?: {
55
- clientSecretValue?: string;
56
- clientSecretCredentialPath?: string;
57
- },
58
- ) => Promise<Record<string, unknown>>)
59
- | undefined;
60
-
61
- // Transitive mock state (connect-orchestrator, etc.)
62
- let mockOrchestrateOAuthConnect: (
63
- opts: Record<string, unknown>,
64
- ) => Promise<Record<string, unknown>>;
65
- let mockGetAppByProviderAndClientId: (
66
- provider: string,
67
- clientId: string,
68
- ) => Record<string, unknown> | undefined = () => undefined;
69
- let mockGetMostRecentAppByProvider: (
70
- provider: string,
71
- ) => Record<string, unknown> | undefined = () => undefined;
72
- let mockGetProvider: (
73
- provider: string,
74
- ) => Record<string, unknown> | undefined = () => undefined;
75
- let mockGetSecureKey: (account: string) => string | undefined = () => undefined;
76
- let mockResolveOAuthConnection: (
77
- provider: string,
78
- options?: Record<string, unknown>,
79
- ) => Promise<{
80
- request: (req: Record<string, unknown>) => Promise<{
81
- status: number;
82
- headers: Record<string, string>;
83
- body: unknown;
84
- }>;
85
- withToken: <T>(fn: (token: string) => Promise<T>) => Promise<T>;
86
- id: string;
87
- provider: string;
88
- accountInfo: string | null;
89
- }> = async () => {
90
- throw new Error("resolveOAuthConnection not configured in test");
91
- };
92
- let mockGetCredentialMetadata: (
93
- service: string,
94
- field: string,
95
- ) => Record<string, unknown> | undefined = () => undefined;
96
- let mockPlatformClientCreate: () => Promise<Record<
97
- string,
98
- unknown
99
- > | null> = async () => null;
100
-
101
- // ---------------------------------------------------------------------------
102
- // Mock token-manager
103
- // ---------------------------------------------------------------------------
104
-
105
- mock.module("../security/token-manager.js", () => ({
106
- withValidToken: <T>(
107
- service: string,
108
- cb: (token: string) => Promise<T>,
109
- ): Promise<T> => mockWithValidToken(service, cb),
110
- // Stubs for any transitive imports that reference other exports:
111
- TokenExpiredError: class TokenExpiredError extends Error {
112
- constructor(
113
- public readonly service: string,
114
- message?: string,
115
- ) {
116
- super(message ?? `Token expired for "${service}".`);
117
- this.name = "TokenExpiredError";
118
- }
119
- },
120
- }));
121
-
122
- // ---------------------------------------------------------------------------
123
- // Mock oauth-store
124
- // ---------------------------------------------------------------------------
125
-
126
- mock.module("../oauth/oauth-store.js", () => ({
127
- disconnectOAuthProvider: async (
128
- provider: string,
129
- ): Promise<"disconnected" | "not-found" | "error"> => {
130
- disconnectOAuthProviderCalls.push(provider);
131
- return disconnectOAuthProviderResult;
132
- },
133
- getConnection: () => undefined,
134
- getConnectionByProvider: () => undefined,
135
- listConnections: () => [],
136
- deleteConnection: () => false,
137
- // Stubs required by apps.ts and providers.ts (transitively loaded via oauth/index.ts)
138
- upsertApp: async (
139
- provider: string,
140
- clientId: string,
141
- clientSecretOpts?: {
142
- clientSecretValue?: string;
143
- clientSecretCredentialPath?: string;
144
- },
145
- ) => {
146
- if (mockUpsertAppImpl) {
147
- return mockUpsertAppImpl(provider, clientId, clientSecretOpts);
148
- }
149
- mockUpsertAppCalls.push({ provider, clientId, clientSecretOpts });
150
- return mockUpsertAppResult;
151
- },
152
- getApp: () => undefined,
153
- getAppByProviderAndClientId: (provider: string, clientId: string) =>
154
- mockGetAppByProviderAndClientId(provider, clientId),
155
- getMostRecentAppByProvider: (provider: string) =>
156
- mockGetMostRecentAppByProvider(provider),
157
- listApps: () => [],
158
- deleteApp: async () => false,
159
- getProvider: (provider: string) => {
160
- // If the test has plugged in a custom mockGetProvider, prefer that.
161
- const custom = mockGetProvider(provider);
162
- if (custom !== undefined) return custom;
163
- return mockProviderStore.get(provider);
164
- },
165
- listProviders: () => mockListProviders(),
166
- registerProvider: (params: Record<string, unknown>) => {
167
- const now = Date.now();
168
- const row: Record<string, unknown> = {
169
- provider: params.provider,
170
- authorizeUrl: params.authorizeUrl,
171
- tokenExchangeUrl: params.tokenExchangeUrl,
172
- refreshUrl: (params.refreshUrl as string | undefined) ?? null,
173
- tokenEndpointAuthMethod:
174
- (params.tokenEndpointAuthMethod as string | undefined) ||
175
- "client_secret_post",
176
- tokenExchangeBodyFormat:
177
- (params.tokenExchangeBodyFormat as string | undefined) ?? "form",
178
- userinfoUrl: params.userinfoUrl ?? null,
179
- baseUrl: params.baseUrl ?? null,
180
- defaultScopes: JSON.stringify(params.defaultScopes ?? []),
181
- availableScopes: params.availableScopes
182
- ? JSON.stringify(params.availableScopes)
183
- : null,
184
- scopeSeparator: (params.scopeSeparator as string | undefined) ?? " ",
185
- authorizeParams: params.authorizeParams
186
- ? JSON.stringify(params.authorizeParams)
187
- : null,
188
- pingUrl: params.pingUrl ?? null,
189
- pingMethod: params.pingMethod ?? null,
190
- pingHeaders: params.pingHeaders
191
- ? JSON.stringify(params.pingHeaders)
192
- : null,
193
- pingBody:
194
- params.pingBody !== undefined ? JSON.stringify(params.pingBody) : null,
195
- revokeUrl: (params.revokeUrl as string | undefined) ?? null,
196
- revokeBodyTemplate: params.revokeBodyTemplate
197
- ? JSON.stringify(params.revokeBodyTemplate)
198
- : null,
199
- managedServiceConfigKey: params.managedServiceConfigKey ?? null,
200
- displayLabel: params.displayLabel ?? null,
201
- description: params.description ?? null,
202
- dashboardUrl: params.dashboardUrl ?? null,
203
- logoUrl: params.logoUrl ?? null,
204
- clientIdPlaceholder: params.clientIdPlaceholder ?? null,
205
- requiresClientSecret: params.requiresClientSecret ?? 1,
206
- loopbackPort: params.loopbackPort ?? null,
207
- injectionTemplates: params.injectionTemplates
208
- ? JSON.stringify(params.injectionTemplates)
209
- : null,
210
- appType: params.appType ?? null,
211
- setupNotes: params.setupNotes ? JSON.stringify(params.setupNotes) : null,
212
- identityUrl: params.identityUrl ?? null,
213
- identityMethod: params.identityMethod ?? null,
214
- identityHeaders: params.identityHeaders
215
- ? JSON.stringify(params.identityHeaders)
216
- : null,
217
- identityBody:
218
- params.identityBody !== undefined
219
- ? JSON.stringify(params.identityBody)
220
- : null,
221
- identityResponsePaths: params.identityResponsePaths
222
- ? JSON.stringify(params.identityResponsePaths)
223
- : null,
224
- identityFormat: params.identityFormat ?? null,
225
- identityOkField: params.identityOkField ?? null,
226
- featureFlag: params.featureFlag ?? null,
227
- createdAt: now,
228
- updatedAt: now,
229
- };
230
- mockProviderStore.set(params.provider as string, row);
231
- return row;
232
- },
233
- updateProvider: (provider: string, params: Record<string, unknown>) => {
234
- const existing = mockProviderStore.get(provider);
235
- if (!existing) return undefined;
236
- const updated: Record<string, unknown> = { ...existing };
237
- if (params.scopeSeparator !== undefined) {
238
- updated.scopeSeparator = params.scopeSeparator;
239
- }
240
- if (params.authorizeUrl !== undefined) {
241
- updated.authorizeUrl = params.authorizeUrl;
242
- }
243
- if (params.tokenExchangeUrl !== undefined) {
244
- updated.tokenExchangeUrl = params.tokenExchangeUrl;
245
- }
246
- if (params.refreshUrl !== undefined) {
247
- updated.refreshUrl = params.refreshUrl;
248
- }
249
- if (params.revokeUrl !== undefined) {
250
- updated.revokeUrl = params.revokeUrl;
251
- }
252
- if (params.revokeBodyTemplate !== undefined) {
253
- updated.revokeBodyTemplate =
254
- params.revokeBodyTemplate === null
255
- ? null
256
- : JSON.stringify(params.revokeBodyTemplate);
257
- }
258
- if (params.defaultScopes !== undefined) {
259
- updated.defaultScopes = JSON.stringify(params.defaultScopes);
260
- }
261
- if (params.displayLabel !== undefined) {
262
- updated.displayLabel = params.displayLabel;
263
- }
264
- if (params.logoUrl !== undefined) {
265
- updated.logoUrl = params.logoUrl;
266
- }
267
- updated.updatedAt = Date.now();
268
- mockProviderStore.set(provider, updated);
269
- return updated;
270
- },
271
- deleteProvider: () => false,
272
- seedProviders: () => {},
273
- getActiveConnection: () => undefined,
274
- listActiveConnectionsByProvider: () => [],
275
- createConnection: () => ({}),
276
- isProviderConnected: () => false,
277
- updateConnection: () => ({}),
278
- }));
279
-
280
- // Stub out transitive dependencies that token-manager would normally pull in.
281
- // All named exports must be present to avoid SyntaxError when bun's module
282
- // mock is shared across test files in the same run.
283
- mock.module("../security/secure-keys.js", () => ({
284
- getSecureKeyAsync: async (account: string) => mockGetSecureKey(account),
285
- getSecureKeyResultAsync: async (account: string) => ({
286
- value: mockGetSecureKey(account),
287
- unreachable: false,
288
- }),
289
- setSecureKeyAsync: async () => true,
290
- deleteSecureKeyAsync: async (account: string) => {
291
- if (secureKeyStore.has(account)) {
292
- secureKeyStore.delete(account);
293
- return "deleted" as const;
294
- }
295
- return "not-found" as const;
296
- },
297
- bulkSetSecureKeysAsync: async () => true,
298
- getProviderKeyAsync: async () => undefined,
299
- getMaskedProviderKey: async () => undefined,
300
- listSecureKeysAsync: async () => ({
301
- accounts: [...secureKeyStore.keys()],
302
- unreachable: false,
303
- }),
304
- getActiveBackendName: () => "mock",
305
- setCesClient: () => {},
306
- onCesClientChanged: () => () => {},
307
- setCesReconnect: () => {},
308
- _resetBackend: () => {},
309
- }));
310
-
311
- mock.module("../tools/credentials/metadata-store.js", () => ({
312
- assertMetadataWritable: () => {},
313
- getCredentialMetadata: (service: string, field: string) =>
314
- mockGetCredentialMetadata(service, field),
315
- getCredentialMetadataById: () => undefined,
316
- upsertCredentialMetadata: () => ({}),
317
- listCredentialMetadata: () => [],
318
- deleteCredentialMetadata: (service: string, field: string): boolean => {
319
- const idx = metadataStore.findIndex(
320
- (c) => c.service === service && c.field === field,
321
- );
322
- if (idx === -1) return false;
323
- metadataStore.splice(idx, 1);
324
- return true;
325
- },
326
- _setMetadataPath: () => {},
327
- }));
328
-
329
- // ---------------------------------------------------------------------------
330
- // Mock connect-orchestrator
331
- // ---------------------------------------------------------------------------
332
-
333
- mock.module("../oauth/connect-orchestrator.js", () => ({
334
- orchestrateOAuthConnect: (opts: Record<string, unknown>) =>
335
- mockOrchestrateOAuthConnect(opts),
336
- }));
337
-
338
- mock.module("../oauth/seed-providers.js", () => ({
339
- SEEDED_PROVIDER_KEYS: new Set([
340
- "google",
341
- "slack",
342
- "github",
343
- "notion",
344
- "twitter",
345
- "linear",
346
- ]),
347
- seedOAuthProviders: () => {},
348
- }));
349
-
350
- // ---------------------------------------------------------------------------
351
- // Mock connection-resolver (needed by request.ts)
352
- // ---------------------------------------------------------------------------
353
-
354
- mock.module("../oauth/connection-resolver.js", () => ({
355
- resolveOAuthConnection: (
356
- provider: string,
357
- options?: Record<string, unknown>,
358
- ) => mockResolveOAuthConnection(provider, options),
359
- }));
360
-
361
- // ---------------------------------------------------------------------------
362
- // Mock platform/client (needed by request.ts)
363
- // ---------------------------------------------------------------------------
28
+ let mockPlatformClientCreate: () => Promise<Record<string, unknown> | null> =
29
+ async () => null;
364
30
 
365
31
  mock.module("../platform/client.js", () => ({
366
32
  VellumPlatformClient: {
@@ -368,18 +34,13 @@ mock.module("../platform/client.js", () => ({
368
34
  },
369
35
  }));
370
36
 
371
- // ---------------------------------------------------------------------------
372
- // Mock config/loader (needed by isManagedMode in shared.ts)
373
- // ---------------------------------------------------------------------------
374
-
375
- let mockGetConfig: () => Record<string, unknown> = () => ({
376
- services: {},
377
- });
378
-
37
+ // Some shared helpers in oauth/shared.ts touch getConfig() — stub it so the
38
+ // import resolves cleanly even though the requirePlatformConnection path
39
+ // never reads service configuration.
379
40
  mock.module("../config/loader.js", () => ({
380
- getConfig: () => mockGetConfig(),
381
- getConfigReadOnly: () => mockGetConfig(),
382
- loadConfig: () => mockGetConfig(),
41
+ getConfig: () => ({ services: {} }),
42
+ getConfigReadOnly: () => ({ services: {} }),
43
+ loadConfig: () => ({ services: {} }),
383
44
  invalidateConfigCache: () => {},
384
45
  loadRawConfig: () => ({}),
385
46
  saveRawConfig: () => {},
@@ -389,917 +50,17 @@ mock.module("../config/loader.js", () => ({
389
50
  getNestedValue: () => undefined,
390
51
  setNestedValue: () => {},
391
52
  _appendQuarantineBulletin: () => {},
392
- API_KEY_PROVIDERS: [
393
- "anthropic",
394
- "openai",
395
- "gemini",
396
- "ollama",
397
- "fireworks",
398
- "openrouter",
399
- "brave",
400
- "perplexity",
401
- ],
53
+ API_KEY_PROVIDERS: ["anthropic", "openai", "gemini"],
402
54
  }));
403
55
 
404
56
  mock.module("../util/logger.js", () => ({
405
- getLogger: () => ({
406
- info: () => {},
407
- warn: () => {},
408
- error: () => {},
409
- debug: () => {},
410
- }),
411
- getCliLogger: () => ({
412
- info: () => {},
413
- warn: () => {},
414
- error: () => {},
415
- debug: () => {},
416
- }),
57
+ getLogger: () => ({ info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }),
58
+ getCliLogger: () => ({ info: () => {}, warn: () => {}, error: () => {}, debug: () => {} }),
417
59
  }));
418
60
 
419
- // ---------------------------------------------------------------------------
420
- // Import the module under test (after mocks are registered)
421
- // ---------------------------------------------------------------------------
422
-
423
- const { registerOAuthCommand } = await import("../cli/commands/oauth/index.js");
424
- const { requirePlatformClient, requirePlatformConnection } =
425
- await import("../cli/commands/oauth/shared.js");
426
-
427
- // ---------------------------------------------------------------------------
428
- // Test helper
429
- // ---------------------------------------------------------------------------
430
-
431
- async function runCli(
432
- args: string[],
433
- ): Promise<{ exitCode: number; stdout: string }> {
434
- const originalStdoutWrite = process.stdout.write.bind(process.stdout);
435
- const originalStderrWrite = process.stderr.write.bind(process.stderr);
436
- const stdoutChunks: string[] = [];
437
-
438
- process.stdout.write = ((chunk: unknown) => {
439
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
440
- return true;
441
- }) as typeof process.stdout.write;
442
-
443
- process.stderr.write = (() => true) as typeof process.stderr.write;
444
-
445
- process.exitCode = 0;
446
-
447
- try {
448
- const program = new Command();
449
- program.exitOverride();
450
- program.configureOutput({
451
- writeErr: () => {},
452
- writeOut: (str: string) => stdoutChunks.push(str),
453
- });
454
- registerOAuthCommand(program);
455
- await program.parseAsync(["node", "assistant", "oauth", ...args]);
456
- } catch {
457
- if (process.exitCode === 0) process.exitCode = 1;
458
- } finally {
459
- process.stdout.write = originalStdoutWrite;
460
- process.stderr.write = originalStderrWrite;
461
- }
462
-
463
- const exitCode = process.exitCode ?? 0;
464
- process.exitCode = 0;
465
-
466
- return {
467
- exitCode,
468
- stdout: stdoutChunks.join(""),
469
- };
470
- }
471
-
472
- // ---------------------------------------------------------------------------
473
- // Tests
474
- // ---------------------------------------------------------------------------
475
-
476
- describe("assistant oauth token <provider-key>", () => {
477
- beforeEach(() => {
478
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
479
- });
480
-
481
- test("prints bare token in human mode", async () => {
482
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
483
- expect(exitCode).toBe(0);
484
- expect(stdout).toBe("mock-access-token-xyz\n");
485
- });
486
-
487
- test("prints JSON in --json mode", async () => {
488
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
489
- expect(exitCode).toBe(0);
490
- const parsed = JSON.parse(stdout);
491
- expect(parsed).toEqual({ ok: true, token: "mock-access-token-xyz" });
492
- });
493
-
494
- test("passes provider key directly to withValidToken", async () => {
495
- let capturedService: string | undefined;
496
- mockWithValidToken = async (service, cb) => {
497
- capturedService = service;
498
- return cb("tok");
499
- };
500
-
501
- await runCli(["token", "twitter"]);
502
- expect(capturedService).toBe("twitter");
503
- });
504
-
505
- test("works with other provider keys", async () => {
506
- let capturedService: string | undefined;
507
- mockWithValidToken = async (service, cb) => {
508
- capturedService = service;
509
- return cb("gmail-token");
510
- };
511
-
512
- const { exitCode, stdout } = await runCli(["token", "google"]);
513
- expect(exitCode).toBe(0);
514
- expect(stdout).toBe("gmail-token\n");
515
- expect(capturedService).toBe("google");
516
- });
517
-
518
- test("exits 1 when no token exists", async () => {
519
- mockWithValidToken = async () => {
520
- throw new Error(
521
- 'No access token found for "twitter". Authorization required.',
522
- );
523
- };
524
-
525
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
526
- expect(exitCode).toBe(1);
527
- const parsed = JSON.parse(stdout);
528
- expect(parsed.ok).toBe(false);
529
- expect(parsed.error).toContain("No access token found");
530
- });
531
-
532
- test("exits 1 when refresh fails", async () => {
533
- mockWithValidToken = async () => {
534
- throw new Error('Token refresh failed for "twitter": invalid_grant.');
535
- };
536
-
537
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
538
- expect(exitCode).toBe(1);
539
- const parsed = JSON.parse(stdout);
540
- expect(parsed.ok).toBe(false);
541
- expect(parsed.error).toContain("Token refresh failed");
542
- });
543
-
544
- test("returns refreshed token transparently", async () => {
545
- // Simulate withValidToken refreshing and returning a new token
546
- mockWithValidToken = async (_service, cb) => cb("refreshed-new-token");
547
-
548
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
549
- expect(exitCode).toBe(0);
550
- expect(stdout).toBe("refreshed-new-token\n");
551
- });
552
-
553
- test("missing provider-key argument exits non-zero", async () => {
554
- const { exitCode } = await runCli(["token"]);
555
- expect(exitCode).not.toBe(0);
556
- });
557
- });
558
-
559
- // ---------------------------------------------------------------------------
560
- // providers list
561
- // ---------------------------------------------------------------------------
562
-
563
- describe("assistant oauth providers list", () => {
564
- const fakeProviders = [
565
- {
566
- provider: "google",
567
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
568
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
569
- defaultScopes: "[]",
570
- availableScopes: null,
571
- authorizeParams: null,
572
- managedServiceConfigKey: "google-oauth",
573
- createdAt: "2025-01-01T00:00:00.000Z",
574
- updatedAt: "2025-01-01T00:00:00.000Z",
575
- },
576
- {
577
- provider: "google-calendar",
578
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
579
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
580
- defaultScopes: "[]",
581
- availableScopes: null,
582
- authorizeParams: null,
583
- managedServiceConfigKey: "google-calendar-oauth",
584
- createdAt: "2025-01-01T00:00:00.000Z",
585
- updatedAt: "2025-01-01T00:00:00.000Z",
586
- },
587
- {
588
- provider: "slack",
589
- authorizeUrl: "https://slack.com/oauth/v2/authorize",
590
- tokenExchangeUrl: "https://slack.com/api/oauth.v2.access",
591
- defaultScopes: "[]",
592
- availableScopes: null,
593
- authorizeParams: null,
594
- managedServiceConfigKey: null,
595
- createdAt: "2025-01-01T00:00:00.000Z",
596
- updatedAt: "2025-01-01T00:00:00.000Z",
597
- },
598
- {
599
- provider: "twitter",
600
- authorizeUrl: "https://twitter.com/i/oauth2/authorize",
601
- tokenExchangeUrl: "https://api.twitter.com/2/oauth2/token",
602
- defaultScopes: "[]",
603
- availableScopes: null,
604
- authorizeParams: null,
605
- managedServiceConfigKey: null,
606
- createdAt: "2025-01-01T00:00:00.000Z",
607
- updatedAt: "2025-01-01T00:00:00.000Z",
608
- },
609
- ];
610
-
611
- beforeEach(() => {
612
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
613
- mockListProviders = () => fakeProviders;
614
- });
615
-
616
- test("returns all providers when no --provider-key is given", async () => {
617
- const { exitCode, stdout } = await runCli(["providers", "list", "--json"]);
618
- expect(exitCode).toBe(0);
619
- const parsed = JSON.parse(stdout);
620
- expect(parsed).toHaveLength(4);
621
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
622
- expect(keys).toContain("google");
623
- expect(keys).toContain("google-calendar");
624
- expect(keys).toContain("slack");
625
- expect(keys).toContain("twitter");
626
- });
627
-
628
- test("filters by single --provider-key value", async () => {
629
- const { exitCode, stdout } = await runCli([
630
- "providers",
631
- "list",
632
- "--provider-key",
633
- "slack",
634
- "--json",
635
- ]);
636
- expect(exitCode).toBe(0);
637
- const parsed = JSON.parse(stdout);
638
- expect(parsed).toHaveLength(1);
639
- expect(parsed[0].providerKey).toBe("slack");
640
- });
641
-
642
- test("filters by comma-separated OR values", async () => {
643
- const { exitCode, stdout } = await runCli([
644
- "providers",
645
- "list",
646
- "--provider-key",
647
- "slack,google",
648
- "--json",
649
- ]);
650
- expect(exitCode).toBe(0);
651
- const parsed = JSON.parse(stdout);
652
- expect(parsed).toHaveLength(3);
653
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
654
- expect(keys).toContain("google");
655
- expect(keys).toContain("google-calendar");
656
- expect(keys).toContain("slack");
657
- });
658
-
659
- test("returns empty array when comma-separated filter has no matches", async () => {
660
- const { exitCode, stdout } = await runCli([
661
- "providers",
662
- "list",
663
- "--provider-key",
664
- "notion,linear",
665
- "--json",
666
- ]);
667
- expect(exitCode).toBe(0);
668
- const parsed = JSON.parse(stdout);
669
- expect(parsed).toHaveLength(0);
670
- });
671
-
672
- test("trims whitespace around commas in --provider-key", async () => {
673
- const { exitCode, stdout } = await runCli([
674
- "providers",
675
- "list",
676
- "--provider-key",
677
- "slack, google",
678
- "--json",
679
- ]);
680
- expect(exitCode).toBe(0);
681
- const parsed = JSON.parse(stdout);
682
- expect(parsed).toHaveLength(3);
683
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
684
- expect(keys).toContain("google");
685
- expect(keys).toContain("google-calendar");
686
- expect(keys).toContain("slack");
687
- });
688
-
689
- test("ignores empty segments from extra commas in --provider-key", async () => {
690
- const { exitCode, stdout } = await runCli([
691
- "providers",
692
- "list",
693
- "--provider-key",
694
- "slack,,google",
695
- "--json",
696
- ]);
697
- expect(exitCode).toBe(0);
698
- const parsed = JSON.parse(stdout);
699
- expect(parsed).toHaveLength(3);
700
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
701
- expect(keys).toContain("google");
702
- expect(keys).toContain("google-calendar");
703
- expect(keys).toContain("slack");
704
- });
705
-
706
- test("--supports-managed returns only providers with managedServiceConfigKey set", async () => {
707
- const { exitCode, stdout } = await runCli([
708
- "providers",
709
- "list",
710
- "--supports-managed",
711
- "--json",
712
- ]);
713
- expect(exitCode).toBe(0);
714
- const parsed = JSON.parse(stdout);
715
- expect(parsed).toHaveLength(2);
716
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
717
- expect(keys).toContain("google");
718
- expect(keys).toContain("google-calendar");
719
- expect(keys).not.toContain("slack");
720
- expect(keys).not.toContain("twitter");
721
- });
722
-
723
- test("--supports-managed combined with --provider-key applies both filters (AND)", async () => {
724
- const { exitCode, stdout } = await runCli([
725
- "providers",
726
- "list",
727
- "--supports-managed",
728
- "--provider-key",
729
- "google",
730
- "--json",
731
- ]);
732
- expect(exitCode).toBe(0);
733
- const parsed = JSON.parse(stdout);
734
- // Both google and google-calendar match --provider-key "google" AND have
735
- // managedServiceConfigKey set, so both are returned.
736
- expect(parsed).toHaveLength(2);
737
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
738
- expect(keys).toContain("google");
739
- expect(keys).toContain("google-calendar");
740
- });
741
-
742
- test("without --supports-managed all providers are returned (existing behavior)", async () => {
743
- const { exitCode, stdout } = await runCli(["providers", "list", "--json"]);
744
- expect(exitCode).toBe(0);
745
- const parsed = JSON.parse(stdout);
746
- expect(parsed).toHaveLength(4);
747
- const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
748
- expect(keys).toContain("google");
749
- expect(keys).toContain("google-calendar");
750
- expect(keys).toContain("slack");
751
- expect(keys).toContain("twitter");
752
- });
753
- });
754
-
755
- // ---------------------------------------------------------------------------
756
- // apps upsert --client-secret-credential-path
757
- // ---------------------------------------------------------------------------
758
-
759
- describe("assistant oauth apps upsert --client-secret-credential-path", () => {
760
- beforeEach(() => {
761
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
762
- mockUpsertAppCalls = [];
763
- mockUpsertAppResult = {
764
- id: "app-upsert-1",
765
- provider: "google",
766
- clientId: "abc123",
767
- createdAt: 1700000000000,
768
- updatedAt: 1700000000000,
769
- };
770
- mockOrchestrateOAuthConnect = async () => ({
771
- success: true,
772
- deferred: false,
773
- grantedScopes: [],
774
- });
775
- mockGetAppByProviderAndClientId = () => undefined;
776
- mockGetMostRecentAppByProvider = () => undefined;
777
- mockGetProvider = () => undefined;
778
- mockGetSecureKey = () => undefined;
779
- mockGetCredentialMetadata = () => undefined;
780
- mockUpsertAppImpl = undefined;
781
- });
782
-
783
- test("upsert with --client-secret-credential-path passes path to upsertApp", async () => {
784
- // "custom/path" has no colon and no credential/ or oauth_app/ prefix.
785
- // resolveCredentialPath passes it through unchanged since it doesn't
786
- // match the service:field shorthand pattern.
787
- const { exitCode, stdout } = await runCli([
788
- "apps",
789
- "upsert",
790
- "--provider",
791
- "google",
792
- "--client-id",
793
- "abc123",
794
- "--client-secret-credential-path",
795
- "custom/path",
796
- "--json",
797
- ]);
798
- expect(exitCode).toBe(0);
799
- expect(mockUpsertAppCalls).toHaveLength(1);
800
- expect(mockUpsertAppCalls[0]).toEqual({
801
- provider: "google",
802
- clientId: "abc123",
803
- clientSecretOpts: { clientSecretCredentialPath: "custom/path" },
804
- });
805
- const parsed = JSON.parse(stdout);
806
- expect(parsed.id).toBe("app-upsert-1");
807
- });
808
-
809
- test("upsert with both --client-secret and --client-secret-credential-path returns error", async () => {
810
- const { exitCode, stdout } = await runCli([
811
- "apps",
812
- "upsert",
813
- "--provider",
814
- "google",
815
- "--client-id",
816
- "abc123",
817
- "--client-secret",
818
- "s3cret",
819
- "--client-secret-credential-path",
820
- "custom/path",
821
- "--json",
822
- ]);
823
- expect(exitCode).toBe(1);
824
- const parsed = JSON.parse(stdout);
825
- expect(parsed.ok).toBe(false);
826
- expect(parsed.error).toContain(
827
- "Cannot provide both --client-secret and --client-secret-credential-path",
828
- );
829
- // upsertApp should NOT have been called
830
- expect(mockUpsertAppCalls).toHaveLength(0);
831
- });
832
-
833
- test("upsert with --client-secret passes clientSecretValue to upsertApp", async () => {
834
- const { exitCode } = await runCli([
835
- "apps",
836
- "upsert",
837
- "--provider",
838
- "google",
839
- "--client-id",
840
- "abc123",
841
- "--client-secret",
842
- "s3cret",
843
- "--json",
844
- ]);
845
- expect(exitCode).toBe(0);
846
- expect(mockUpsertAppCalls).toHaveLength(1);
847
- expect(mockUpsertAppCalls[0]).toEqual({
848
- provider: "google",
849
- clientId: "abc123",
850
- clientSecretOpts: { clientSecretValue: "s3cret" },
851
- });
852
- });
853
-
854
- test("upsert without any secret option passes undefined", async () => {
855
- const { exitCode } = await runCli([
856
- "apps",
857
- "upsert",
858
- "--provider",
859
- "google",
860
- "--client-id",
861
- "abc123",
862
- "--json",
863
- ]);
864
- expect(exitCode).toBe(0);
865
- expect(mockUpsertAppCalls).toHaveLength(1);
866
- expect(mockUpsertAppCalls[0]).toEqual({
867
- provider: "google",
868
- clientId: "abc123",
869
- clientSecretOpts: undefined,
870
- });
871
- });
872
-
873
- test("upsert resolves service:field shorthand to full credential path", async () => {
874
- // The service:field shorthand is resolved to credential/{service}/{field}
875
- const { exitCode, stdout } = await runCli([
876
- "apps",
877
- "upsert",
878
- "--provider",
879
- "google",
880
- "--client-id",
881
- "abc",
882
- "--client-secret-credential-path",
883
- "google:client_secret",
884
- "--json",
885
- ]);
886
- expect(exitCode).toBe(0);
887
- expect(mockUpsertAppCalls).toHaveLength(1);
888
- expect(mockUpsertAppCalls[0]).toEqual({
889
- provider: "google",
890
- clientId: "abc",
891
- clientSecretOpts: {
892
- clientSecretCredentialPath: "credential/google/client_secret",
893
- },
894
- });
895
- const parsed = JSON.parse(stdout);
896
- expect(parsed.id).toBe("app-upsert-1");
897
- });
898
-
899
- test("upsert resolves slack:client_secret shorthand to full credential path", async () => {
900
- const { exitCode, stdout } = await runCli([
901
- "apps",
902
- "upsert",
903
- "--provider",
904
- "slack",
905
- "--client-id",
906
- "slack-abc",
907
- "--client-secret-credential-path",
908
- "slack:client_secret",
909
- "--json",
910
- ]);
911
- expect(exitCode).toBe(0);
912
- expect(mockUpsertAppCalls).toHaveLength(1);
913
- expect(mockUpsertAppCalls[0]).toEqual({
914
- provider: "slack",
915
- clientId: "slack-abc",
916
- clientSecretOpts: {
917
- clientSecretCredentialPath: "credential/slack/client_secret",
918
- },
919
- });
920
- const parsed = JSON.parse(stdout);
921
- expect(parsed.id).toBe("app-upsert-1");
922
- });
923
-
924
- test("upsert passes prefixed credential path through unchanged", async () => {
925
- const { exitCode, stdout } = await runCli([
926
- "apps",
927
- "upsert",
928
- "--provider",
929
- "google",
930
- "--client-id",
931
- "abc",
932
- "--client-secret-credential-path",
933
- "credential/google/client_secret",
934
- "--json",
935
- ]);
936
- expect(exitCode).toBe(0);
937
- expect(mockUpsertAppCalls).toHaveLength(1);
938
- // Already-prefixed path should be passed through as-is
939
- expect(mockUpsertAppCalls[0]).toEqual({
940
- provider: "google",
941
- clientId: "abc",
942
- clientSecretOpts: {
943
- clientSecretCredentialPath: "credential/google/client_secret",
944
- },
945
- });
946
- const parsed = JSON.parse(stdout);
947
- expect(parsed.id).toBe("app-upsert-1");
948
- });
949
-
950
- test("upsert passes oauth_app/ prefixed credential path through unchanged", async () => {
951
- const { exitCode, stdout } = await runCli([
952
- "apps",
953
- "upsert",
954
- "--provider",
955
- "google",
956
- "--client-id",
957
- "abc",
958
- "--client-secret-credential-path",
959
- "oauth_app/some-id/client_secret",
960
- "--json",
961
- ]);
962
- expect(exitCode).toBe(0);
963
- expect(mockUpsertAppCalls).toHaveLength(1);
964
- // oauth_app/ prefixed path should be passed through as-is
965
- expect(mockUpsertAppCalls[0]).toEqual({
966
- provider: "google",
967
- clientId: "abc",
968
- clientSecretOpts: {
969
- clientSecretCredentialPath: "oauth_app/some-id/client_secret",
970
- },
971
- });
972
- const parsed = JSON.parse(stdout);
973
- expect(parsed.id).toBe("app-upsert-1");
974
- });
975
-
976
- test("upsert with invalid credential path returns error when no secret found", async () => {
977
- // Override upsertApp to throw when given an unresolvable credential path
978
- mockUpsertAppImpl = async (_provider, _clientId, clientSecretOpts) => {
979
- throw new Error(
980
- `No secret found at credential path: ${clientSecretOpts?.clientSecretCredentialPath}`,
981
- );
982
- };
983
-
984
- const { exitCode, stdout } = await runCli([
985
- "apps",
986
- "upsert",
987
- "--provider",
988
- "google",
989
- "--client-id",
990
- "abc",
991
- "--client-secret-credential-path",
992
- "bogus:nonexistent:path",
993
- "--json",
994
- ]);
995
- expect(exitCode).toBe(1);
996
- const parsed = JSON.parse(stdout);
997
- expect(parsed.ok).toBe(false);
998
- expect(parsed.error).toContain("No secret found");
999
- });
1000
- });
1001
-
1002
- // ---------------------------------------------------------------------------
1003
- // ping
1004
- // ---------------------------------------------------------------------------
1005
-
1006
- describe("assistant oauth ping <provider-key>", () => {
1007
- beforeEach(() => {
1008
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1009
- // Reset resolveOAuthConnection to default (unconfigured)
1010
- mockResolveOAuthConnection = async () => {
1011
- throw new Error("resolveOAuthConnection not configured in test");
1012
- };
1013
- });
1014
-
1015
- test("returns ok when ping endpoint returns 200", async () => {
1016
- mockGetProvider = () => ({
1017
- provider: "google",
1018
- pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1019
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1020
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1021
- defaultScopes: "[]",
1022
- availableScopes: null,
1023
- authorizeParams: null,
1024
- createdAt: Date.now(),
1025
- updatedAt: Date.now(),
1026
- });
1027
- mockResolveOAuthConnection = async () => ({
1028
- id: "conn-1",
1029
- provider: "google",
1030
- accountInfo: null,
1031
- request: async () => ({ status: 200, headers: {}, body: {} }),
1032
- withToken: async (fn) => fn("mock-access-token-xyz"),
1033
- });
1034
- const { exitCode, stdout } = await runCli(["ping", "google", "--json"]);
1035
- expect(exitCode).toBe(0);
1036
- const parsed = JSON.parse(stdout);
1037
- expect(parsed.ok).toBe(true);
1038
- expect(parsed.status).toBe(200);
1039
- });
1040
-
1041
- test("exits 1 when provider not found", async () => {
1042
- mockGetProvider = () => undefined;
1043
- const { exitCode, stdout } = await runCli(["ping", "unknown", "--json"]);
1044
- expect(exitCode).toBe(1);
1045
- const parsed = JSON.parse(stdout);
1046
- expect(parsed.ok).toBe(false);
1047
- expect(parsed.error).toContain("Unknown provider");
1048
- });
1049
-
1050
- test("exits 1 when no ping URL configured", async () => {
1051
- mockGetProvider = () => ({
1052
- provider: "telegram",
1053
- pingUrl: null,
1054
- authorizeUrl: "urn:manual-token",
1055
- tokenExchangeUrl: "urn:manual-token",
1056
- defaultScopes: "[]",
1057
- availableScopes: null,
1058
- authorizeParams: null,
1059
- createdAt: Date.now(),
1060
- updatedAt: Date.now(),
1061
- });
1062
- const { exitCode, stdout } = await runCli(["ping", "telegram", "--json"]);
1063
- expect(exitCode).toBe(1);
1064
- const parsed = JSON.parse(stdout);
1065
- expect(parsed.ok).toBe(false);
1066
- expect(parsed.error).toContain("No ping URL configured");
1067
- });
1068
-
1069
- test("exits 1 when ping endpoint returns non-2xx", async () => {
1070
- mockGetProvider = () => ({
1071
- provider: "google",
1072
- pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1073
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1074
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1075
- defaultScopes: "[]",
1076
- availableScopes: null,
1077
- authorizeParams: null,
1078
- createdAt: Date.now(),
1079
- updatedAt: Date.now(),
1080
- });
1081
- mockResolveOAuthConnection = async () => ({
1082
- id: "conn-1",
1083
- provider: "google",
1084
- accountInfo: null,
1085
- request: async () => ({ status: 403, headers: {}, body: "Forbidden" }),
1086
- withToken: async (fn) => fn("mock-access-token-xyz"),
1087
- });
1088
- const { exitCode, stdout } = await runCli(["ping", "google", "--json"]);
1089
- expect(exitCode).toBe(1);
1090
- const parsed = JSON.parse(stdout);
1091
- expect(parsed.ok).toBe(false);
1092
- expect(parsed.status).toBe(403);
1093
- });
1094
-
1095
- test("exits 1 when no connection can be resolved", async () => {
1096
- mockGetProvider = () => ({
1097
- provider: "google",
1098
- pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1099
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1100
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1101
- defaultScopes: "[]",
1102
- availableScopes: null,
1103
- authorizeParams: null,
1104
- createdAt: Date.now(),
1105
- updatedAt: Date.now(),
1106
- });
1107
- mockResolveOAuthConnection = async () => {
1108
- throw new Error('No access token found for "google".');
1109
- };
1110
- const { exitCode, stdout } = await runCli(["ping", "google", "--json"]);
1111
- expect(exitCode).toBe(1);
1112
- const parsed = JSON.parse(stdout);
1113
- expect(parsed.ok).toBe(false);
1114
- expect(parsed.error).toContain("No access token");
1115
- });
1116
- });
1117
-
1118
- // ---------------------------------------------------------------------------
1119
- // oauth connect — managed mode 401/403 error messages
1120
- // ---------------------------------------------------------------------------
1121
-
1122
- describe("assistant oauth connect managed mode — platform 401/403 errors", () => {
1123
- /**
1124
- * Helper: create a mock platform client whose `fetch` always returns the
1125
- * given status code and body text.
1126
- */
1127
- function makeMockPlatformClient(status: number, body = "") {
1128
- return {
1129
- platformAssistantId: "asst-test-123",
1130
- fetch: async () =>
1131
- new Response(body, { status, statusText: `HTTP ${status}` }),
1132
- };
1133
- }
1134
-
1135
- beforeEach(() => {
1136
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1137
- mockOrchestrateOAuthConnect = async () => ({
1138
- success: true,
1139
- deferred: false,
1140
- grantedScopes: [],
1141
- });
1142
- mockGetAppByProviderAndClientId = () => undefined;
1143
- mockGetMostRecentAppByProvider = () => undefined;
1144
- mockGetSecureKey = () => undefined;
1145
- mockGetCredentialMetadata = () => undefined;
1146
-
1147
- // Set up managed mode: provider has managedServiceConfigKey, config
1148
- // returns the matching service with mode "managed".
1149
- mockGetProvider = () => ({
1150
- provider: "google",
1151
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1152
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1153
- defaultScopes: "[]",
1154
- availableScopes: null,
1155
- authorizeParams: null,
1156
- managedServiceConfigKey: "google-oauth",
1157
- createdAt: Date.now(),
1158
- updatedAt: Date.now(),
1159
- });
1160
- mockGetConfig = () => ({
1161
- services: {
1162
- "google-oauth": { mode: "managed" },
1163
- },
1164
- });
1165
- });
1166
-
1167
- afterEach(() => {
1168
- mockPlatformClientCreate = async () => null;
1169
- mockGetConfig = () => ({ services: {} });
1170
- });
1171
-
1172
- test("401 response includes 'vellum platform connect' suggestion", async () => {
1173
- mockPlatformClientCreate = async () =>
1174
- makeMockPlatformClient(401, "Unauthorized");
1175
- const { exitCode, stdout } = await runCli([
1176
- "connect",
1177
- "google",
1178
- "--no-browser",
1179
- "--json",
1180
- ]);
1181
- expect(exitCode).toBe(1);
1182
- const parsed = JSON.parse(stdout);
1183
- expect(parsed.ok).toBe(false);
1184
- expect(parsed.error).toContain("Platform returned HTTP 401");
1185
- expect(parsed.error).toContain("Unauthorized");
1186
- expect(parsed.error).toContain("vellum platform connect");
1187
- });
1188
-
1189
- test("403 response includes 'vellum platform connect' suggestion", async () => {
1190
- mockPlatformClientCreate = async () =>
1191
- makeMockPlatformClient(403, "Forbidden");
1192
- const { exitCode, stdout } = await runCli([
1193
- "connect",
1194
- "google",
1195
- "--no-browser",
1196
- "--json",
1197
- ]);
1198
- expect(exitCode).toBe(1);
1199
- const parsed = JSON.parse(stdout);
1200
- expect(parsed.ok).toBe(false);
1201
- expect(parsed.error).toContain("Platform returned HTTP 403");
1202
- expect(parsed.error).toContain("Forbidden");
1203
- expect(parsed.error).toContain("vellum platform connect");
1204
- });
1205
-
1206
- test("500 response does NOT include 'vellum platform connect' suggestion", async () => {
1207
- mockPlatformClientCreate = async () =>
1208
- makeMockPlatformClient(500, "Internal Server Error");
1209
- const { exitCode, stdout } = await runCli([
1210
- "connect",
1211
- "google",
1212
- "--no-browser",
1213
- "--json",
1214
- ]);
1215
- expect(exitCode).toBe(1);
1216
- const parsed = JSON.parse(stdout);
1217
- expect(parsed.ok).toBe(false);
1218
- expect(parsed.error).toContain("Platform returned HTTP 500");
1219
- expect(parsed.error).not.toContain("vellum platform connect");
1220
- });
1221
- });
1222
-
1223
- // ---------------------------------------------------------------------------
1224
- // requirePlatformClient — improved error messages
1225
- // ---------------------------------------------------------------------------
1226
-
1227
- describe("requirePlatformClient", () => {
1228
- test("returns error mentioning 'vellum platform connect' when not connected", async () => {
1229
- mockPlatformClientCreate = async () => null;
1230
- const stdoutChunks: string[] = [];
1231
- const originalWrite = process.stdout.write.bind(process.stdout);
1232
- process.stdout.write = ((chunk: unknown) => {
1233
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
1234
- return true;
1235
- }) as typeof process.stdout.write;
1236
- process.exitCode = 0;
1237
-
1238
- try {
1239
- const cmd = new Command();
1240
- cmd.option("--json");
1241
- cmd.parse(["node", "test", "--json"]);
1242
- const result = await requirePlatformClient(cmd);
1243
- expect(result).toBeNull();
1244
- expect(process.exitCode).toBe(1);
1245
- const output = stdoutChunks.join("");
1246
- const parsed = JSON.parse(output);
1247
- expect(parsed.ok).toBe(false);
1248
- expect(parsed.error).toContain("vellum platform connect");
1249
- expect(parsed.error).toContain("Not connected");
1250
- } finally {
1251
- process.stdout.write = originalWrite;
1252
- process.exitCode = 0;
1253
- }
1254
- });
1255
-
1256
- test("returns distinct error when connected but missing assistant ID", async () => {
1257
- mockPlatformClientCreate = async () => ({
1258
- platformAssistantId: "",
1259
- fetch: async () => new Response(),
1260
- });
1261
- const stdoutChunks: string[] = [];
1262
- const originalWrite = process.stdout.write.bind(process.stdout);
1263
- process.stdout.write = ((chunk: unknown) => {
1264
- stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
1265
- return true;
1266
- }) as typeof process.stdout.write;
1267
- process.exitCode = 0;
1268
-
1269
- try {
1270
- const cmd = new Command();
1271
- cmd.option("--json");
1272
- cmd.parse(["node", "test", "--json"]);
1273
- const result = await requirePlatformClient(cmd);
1274
- expect(result).toBeNull();
1275
- expect(process.exitCode).toBe(1);
1276
- const output = stdoutChunks.join("");
1277
- const parsed = JSON.parse(output);
1278
- expect(parsed.ok).toBe(false);
1279
- expect(parsed.error).toContain("no assistant ID is configured");
1280
- expect(parsed.error).toContain("registered on the platform");
1281
- } finally {
1282
- process.stdout.write = originalWrite;
1283
- process.exitCode = 0;
1284
- }
1285
- });
1286
-
1287
- test("returns client when connected with assistant ID", async () => {
1288
- mockPlatformClientCreate = async () => ({
1289
- platformAssistantId: "asst-123",
1290
- fetch: async () => new Response(),
1291
- });
1292
- process.exitCode = 0;
1293
-
1294
- const cmd = new Command();
1295
- cmd.option("--json");
1296
- cmd.parse(["node", "test", "--json"]);
1297
- const result = await requirePlatformClient(cmd);
1298
- expect(result).not.toBeNull();
1299
- expect(result!.platformAssistantId).toBe("asst-123");
1300
- expect(process.exitCode).toBe(0);
1301
- });
1302
- });
61
+ const { requirePlatformConnection } = await import(
62
+ "../cli/commands/oauth/shared.js"
63
+ );
1303
64
 
1304
65
  // ---------------------------------------------------------------------------
1305
66
  // requirePlatformConnection
@@ -1364,614 +125,3 @@ describe("requirePlatformConnection", () => {
1364
125
  expect(process.exitCode).toBe(0);
1365
126
  });
1366
127
  });
1367
-
1368
- // ---------------------------------------------------------------------------
1369
- // oauth mode — platform connection guard
1370
- // ---------------------------------------------------------------------------
1371
-
1372
- describe("assistant oauth mode", () => {
1373
- beforeEach(() => {
1374
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1375
- mockGetProvider = () => ({
1376
- provider: "google",
1377
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1378
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1379
- defaultScopes: "[]",
1380
- availableScopes: null,
1381
- authorizeParams: null,
1382
- managedServiceConfigKey: "google-oauth",
1383
- createdAt: Date.now(),
1384
- updatedAt: Date.now(),
1385
- });
1386
- mockGetConfig = () => ({
1387
- services: {
1388
- "google-oauth": { mode: "your-own" },
1389
- },
1390
- });
1391
- });
1392
-
1393
- afterEach(() => {
1394
- mockPlatformClientCreate = async () => null;
1395
- mockGetConfig = () => ({ services: {} });
1396
- mockGetProvider = () => undefined;
1397
- });
1398
-
1399
- test("oauth mode <provider> --set managed fails when not connected to platform", async () => {
1400
- mockPlatformClientCreate = async () => null;
1401
- const { exitCode, stdout } = await runCli([
1402
- "mode",
1403
- "google",
1404
- "--set",
1405
- "managed",
1406
- "--json",
1407
- ]);
1408
- expect(exitCode).toBe(1);
1409
- const parsed = JSON.parse(stdout);
1410
- expect(parsed.ok).toBe(false);
1411
- expect(parsed.error).toContain("vellum platform connect");
1412
- });
1413
-
1414
- test("oauth mode <provider> --set your-own succeeds without platform connection", async () => {
1415
- mockPlatformClientCreate = async () => null;
1416
- const { exitCode, stdout } = await runCli([
1417
- "mode",
1418
- "google",
1419
- "--set",
1420
- "your-own",
1421
- "--json",
1422
- ]);
1423
- // Setting to "your-own" doesn't need platform — it's a local-only operation
1424
- expect(exitCode).toBe(0);
1425
- const parsed = JSON.parse(stdout);
1426
- expect(parsed.ok).toBe(true);
1427
- expect(parsed.mode).toBe("your-own");
1428
- });
1429
-
1430
- test("oauth mode <provider> (read) succeeds without platform connection", async () => {
1431
- mockPlatformClientCreate = async () => null;
1432
- const { exitCode, stdout } = await runCli(["mode", "google", "--json"]);
1433
- expect(exitCode).toBe(0);
1434
- const parsed = JSON.parse(stdout);
1435
- expect(parsed.ok).toBe(true);
1436
- expect(parsed.provider).toBe("google");
1437
- expect(parsed.mode).toBe("your-own");
1438
- });
1439
- });
1440
-
1441
- // ---------------------------------------------------------------------------
1442
- // providers register / update / get — --scope-separator wiring
1443
- // ---------------------------------------------------------------------------
1444
-
1445
- describe("assistant oauth providers --scope-separator", () => {
1446
- beforeEach(() => {
1447
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1448
- mockProviderStore.clear();
1449
- // Default getProvider falls through to mockProviderStore via the
1450
- // oauth-store mock module. Tests in this describe block don't need
1451
- // a per-test mockGetProvider override.
1452
- mockGetProvider = () => undefined;
1453
- mockGetConfig = () => ({ services: {} });
1454
- });
1455
-
1456
- afterEach(() => {
1457
- mockProviderStore.clear();
1458
- mockGetProvider = () => undefined;
1459
- });
1460
-
1461
- test("providers register --scope-separator , stores ',' on the provider row", async () => {
1462
- const { exitCode } = await runCli([
1463
- "providers",
1464
- "register",
1465
- "--provider-key",
1466
- "custom-linear",
1467
- "--auth-url",
1468
- "https://linear.app/oauth/authorize",
1469
- "--token-url",
1470
- "https://api.linear.app/oauth/token",
1471
- "--scopes",
1472
- "read,write",
1473
- "--scope-separator",
1474
- ",",
1475
- "--json",
1476
- ]);
1477
- expect(exitCode).toBe(0);
1478
- const stored = mockProviderStore.get("custom-linear");
1479
- expect(stored).toBeDefined();
1480
- expect(stored?.scopeSeparator).toBe(",");
1481
- });
1482
-
1483
- test("providers register without --scope-separator stores the default ' '", async () => {
1484
- const { exitCode } = await runCli([
1485
- "providers",
1486
- "register",
1487
- "--provider-key",
1488
- "custom-default-sep",
1489
- "--auth-url",
1490
- "https://example.com/oauth/authorize",
1491
- "--token-url",
1492
- "https://example.com/oauth/token",
1493
- "--scopes",
1494
- "read,write",
1495
- "--json",
1496
- ]);
1497
- expect(exitCode).toBe(0);
1498
- const stored = mockProviderStore.get("custom-default-sep");
1499
- expect(stored).toBeDefined();
1500
- expect(stored?.scopeSeparator).toBe(" ");
1501
- });
1502
-
1503
- test("providers update --scope-separator , updates an existing custom provider", async () => {
1504
- // Seed the store with an existing custom provider that uses the default
1505
- // " " separator.
1506
- await runCli([
1507
- "providers",
1508
- "register",
1509
- "--provider-key",
1510
- "custom-update-target",
1511
- "--auth-url",
1512
- "https://example.com/oauth/authorize",
1513
- "--token-url",
1514
- "https://example.com/oauth/token",
1515
- "--scopes",
1516
- "read",
1517
- "--json",
1518
- ]);
1519
- expect(mockProviderStore.get("custom-update-target")?.scopeSeparator).toBe(
1520
- " ",
1521
- );
1522
-
1523
- const { exitCode } = await runCli([
1524
- "providers",
1525
- "update",
1526
- "custom-update-target",
1527
- "--scope-separator",
1528
- ",",
1529
- "--json",
1530
- ]);
1531
- expect(exitCode).toBe(0);
1532
- expect(mockProviderStore.get("custom-update-target")?.scopeSeparator).toBe(
1533
- ",",
1534
- );
1535
- });
1536
-
1537
- test("providers get <key> --json includes scopeSeparator from the serialized output", async () => {
1538
- // Seed the store with a custom provider that uses ',' as the separator.
1539
- await runCli([
1540
- "providers",
1541
- "register",
1542
- "--provider-key",
1543
- "custom-get-target",
1544
- "--auth-url",
1545
- "https://example.com/oauth/authorize",
1546
- "--token-url",
1547
- "https://example.com/oauth/token",
1548
- "--scopes",
1549
- "read,write",
1550
- "--scope-separator",
1551
- ",",
1552
- "--json",
1553
- ]);
1554
-
1555
- const { exitCode, stdout } = await runCli([
1556
- "providers",
1557
- "get",
1558
- "custom-get-target",
1559
- "--json",
1560
- ]);
1561
- expect(exitCode).toBe(0);
1562
- const parsed = JSON.parse(stdout);
1563
- expect(parsed.scopeSeparator).toBe(",");
1564
- });
1565
- });
1566
-
1567
- // ---------------------------------------------------------------------------
1568
- // providers register / update / get — --refresh-url wiring
1569
- // ---------------------------------------------------------------------------
1570
-
1571
- describe("assistant oauth providers --refresh-url", () => {
1572
- beforeEach(() => {
1573
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1574
- mockProviderStore.clear();
1575
- // Default getProvider falls through to mockProviderStore via the
1576
- // oauth-store mock module.
1577
- mockGetProvider = () => undefined;
1578
- mockGetConfig = () => ({ services: {} });
1579
- });
1580
-
1581
- afterEach(() => {
1582
- mockProviderStore.clear();
1583
- mockGetProvider = () => undefined;
1584
- });
1585
-
1586
- test("providers register --refresh-url stores the URL on the provider row", async () => {
1587
- const { exitCode } = await runCli([
1588
- "providers",
1589
- "register",
1590
- "--provider-key",
1591
- "custom-refresh-url",
1592
- "--auth-url",
1593
- "https://example.com/oauth/authorize",
1594
- "--token-url",
1595
- "https://example.com/oauth/token",
1596
- "--refresh-url",
1597
- "https://refresh.example.com/token",
1598
- "--json",
1599
- ]);
1600
- expect(exitCode).toBe(0);
1601
- const stored = mockProviderStore.get("custom-refresh-url");
1602
- expect(stored).toBeDefined();
1603
- expect(stored?.refreshUrl).toBe("https://refresh.example.com/token");
1604
- });
1605
-
1606
- test("providers register without --refresh-url stores null", async () => {
1607
- const { exitCode } = await runCli([
1608
- "providers",
1609
- "register",
1610
- "--provider-key",
1611
- "custom-no-refresh-url",
1612
- "--auth-url",
1613
- "https://example.com/oauth/authorize",
1614
- "--token-url",
1615
- "https://example.com/oauth/token",
1616
- "--json",
1617
- ]);
1618
- expect(exitCode).toBe(0);
1619
- const stored = mockProviderStore.get("custom-no-refresh-url");
1620
- expect(stored).toBeDefined();
1621
- expect(stored?.refreshUrl).toBeNull();
1622
- });
1623
-
1624
- test("providers update --refresh-url updates an existing custom provider", async () => {
1625
- // Seed the store with an existing custom provider that has no refresh URL.
1626
- await runCli([
1627
- "providers",
1628
- "register",
1629
- "--provider-key",
1630
- "custom-update-refresh",
1631
- "--auth-url",
1632
- "https://example.com/oauth/authorize",
1633
- "--token-url",
1634
- "https://example.com/oauth/token",
1635
- "--json",
1636
- ]);
1637
- expect(
1638
- mockProviderStore.get("custom-update-refresh")?.refreshUrl,
1639
- ).toBeNull();
1640
-
1641
- const { exitCode } = await runCli([
1642
- "providers",
1643
- "update",
1644
- "custom-update-refresh",
1645
- "--refresh-url",
1646
- "https://new-refresh.example.com/token",
1647
- "--json",
1648
- ]);
1649
- expect(exitCode).toBe(0);
1650
- expect(mockProviderStore.get("custom-update-refresh")?.refreshUrl).toBe(
1651
- "https://new-refresh.example.com/token",
1652
- );
1653
- });
1654
-
1655
- test("providers get <key> --json includes refreshUrl from the serialized output", async () => {
1656
- // Seed the store with a custom provider that has a refresh URL set.
1657
- await runCli([
1658
- "providers",
1659
- "register",
1660
- "--provider-key",
1661
- "custom-get-refresh",
1662
- "--auth-url",
1663
- "https://example.com/oauth/authorize",
1664
- "--token-url",
1665
- "https://example.com/oauth/token",
1666
- "--refresh-url",
1667
- "https://refresh.example.com/token",
1668
- "--json",
1669
- ]);
1670
-
1671
- const { exitCode, stdout } = await runCli([
1672
- "providers",
1673
- "get",
1674
- "custom-get-refresh",
1675
- "--json",
1676
- ]);
1677
- expect(exitCode).toBe(0);
1678
- const parsed = JSON.parse(stdout);
1679
- expect(parsed.refreshUrl).toBe("https://refresh.example.com/token");
1680
- });
1681
- });
1682
-
1683
- // ---------------------------------------------------------------------------
1684
- // providers register / update / get — --revoke-url and --revoke-body-template wiring
1685
- // ---------------------------------------------------------------------------
1686
-
1687
- describe("assistant oauth providers --revoke-url and --revoke-body-template", () => {
1688
- beforeEach(() => {
1689
- mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
1690
- mockProviderStore.clear();
1691
- mockGetProvider = () => undefined;
1692
- mockGetConfig = () => ({ services: {} });
1693
- });
1694
-
1695
- afterEach(() => {
1696
- mockProviderStore.clear();
1697
- mockGetProvider = () => undefined;
1698
- });
1699
-
1700
- test("providers register --revoke-url and --revoke-body-template stores both fields", async () => {
1701
- const { exitCode } = await runCli([
1702
- "providers",
1703
- "register",
1704
- "--provider-key",
1705
- "custom-revoke-roundtrip",
1706
- "--auth-url",
1707
- "https://example.com/oauth/authorize",
1708
- "--token-url",
1709
- "https://example.com/oauth/token",
1710
- "--revoke-url",
1711
- "https://revoke.example.com",
1712
- "--revoke-body-template",
1713
- '{"token":"{access_token}"}',
1714
- "--json",
1715
- ]);
1716
- expect(exitCode).toBe(0);
1717
- const stored = mockProviderStore.get("custom-revoke-roundtrip");
1718
- expect(stored).toBeDefined();
1719
- expect(stored?.revokeUrl).toBe("https://revoke.example.com");
1720
- // revokeBodyTemplate is JSON-stringified at the storage layer.
1721
- expect(JSON.parse(stored?.revokeBodyTemplate as string)).toEqual({
1722
- token: "{access_token}",
1723
- });
1724
- });
1725
-
1726
- test("providers register without --revoke-url or --revoke-body-template stores both as null", async () => {
1727
- const { exitCode } = await runCli([
1728
- "providers",
1729
- "register",
1730
- "--provider-key",
1731
- "custom-no-revoke",
1732
- "--auth-url",
1733
- "https://example.com/oauth/authorize",
1734
- "--token-url",
1735
- "https://example.com/oauth/token",
1736
- "--json",
1737
- ]);
1738
- expect(exitCode).toBe(0);
1739
- const stored = mockProviderStore.get("custom-no-revoke");
1740
- expect(stored).toBeDefined();
1741
- expect(stored?.revokeUrl).toBeNull();
1742
- expect(stored?.revokeBodyTemplate).toBeNull();
1743
- });
1744
-
1745
- test("providers register with malformed --revoke-body-template fails with a JSON parse error", async () => {
1746
- const { exitCode, stdout } = await runCli([
1747
- "providers",
1748
- "register",
1749
- "--provider-key",
1750
- "custom-bad-revoke",
1751
- "--auth-url",
1752
- "https://example.com/oauth/authorize",
1753
- "--token-url",
1754
- "https://example.com/oauth/token",
1755
- "--revoke-body-template",
1756
- "not-json{",
1757
- "--json",
1758
- ]);
1759
- expect(exitCode).toBe(1);
1760
- const parsed = JSON.parse(stdout);
1761
- expect(parsed.ok).toBe(false);
1762
- // The error message comes from JSON.parse — match permissively.
1763
- expect(parsed.error).toMatch(/JSON|Unexpected|parse/i);
1764
- // Provider should not have been written.
1765
- expect(mockProviderStore.get("custom-bad-revoke")).toBeUndefined();
1766
- });
1767
-
1768
- test("providers update --revoke-url updates only the URL", async () => {
1769
- // Seed an existing custom provider.
1770
- await runCli([
1771
- "providers",
1772
- "register",
1773
- "--provider-key",
1774
- "custom-update-revoke-url",
1775
- "--auth-url",
1776
- "https://example.com/oauth/authorize",
1777
- "--token-url",
1778
- "https://example.com/oauth/token",
1779
- "--json",
1780
- ]);
1781
- expect(
1782
- mockProviderStore.get("custom-update-revoke-url")?.revokeUrl,
1783
- ).toBeNull();
1784
-
1785
- const { exitCode } = await runCli([
1786
- "providers",
1787
- "update",
1788
- "custom-update-revoke-url",
1789
- "--revoke-url",
1790
- "https://new-revoke.example.com",
1791
- "--json",
1792
- ]);
1793
- expect(exitCode).toBe(0);
1794
- expect(mockProviderStore.get("custom-update-revoke-url")?.revokeUrl).toBe(
1795
- "https://new-revoke.example.com",
1796
- );
1797
- // revokeBodyTemplate should still be null (unchanged).
1798
- expect(
1799
- mockProviderStore.get("custom-update-revoke-url")?.revokeBodyTemplate,
1800
- ).toBeNull();
1801
- });
1802
-
1803
- test("providers update --revoke-url with empty string clears the stored URL to null", async () => {
1804
- // Seed an existing custom provider that already has a non-null revokeUrl.
1805
- await runCli([
1806
- "providers",
1807
- "register",
1808
- "--provider-key",
1809
- "custom-clear-revoke-url",
1810
- "--auth-url",
1811
- "https://example.com/oauth/authorize",
1812
- "--token-url",
1813
- "https://example.com/oauth/token",
1814
- "--revoke-url",
1815
- "https://revoke.example.com",
1816
- "--json",
1817
- ]);
1818
- expect(mockProviderStore.get("custom-clear-revoke-url")?.revokeUrl).toBe(
1819
- "https://revoke.example.com",
1820
- );
1821
-
1822
- // Pass an empty string to --revoke-url — the help text promises this
1823
- // clears the field, so the stored value should end up as null (not "").
1824
- const { exitCode, stdout } = await runCli([
1825
- "providers",
1826
- "update",
1827
- "custom-clear-revoke-url",
1828
- "--revoke-url",
1829
- "",
1830
- "--json",
1831
- ]);
1832
- expect(exitCode).toBe(0);
1833
- expect(
1834
- mockProviderStore.get("custom-clear-revoke-url")?.revokeUrl,
1835
- ).toBeNull();
1836
-
1837
- // The serialized --json output should also reflect null, not "".
1838
- const parsed = JSON.parse(stdout);
1839
- expect(parsed.revokeUrl).toBeNull();
1840
- });
1841
-
1842
- test("providers update --revoke-body-template updates only the template (JSON round-trip)", async () => {
1843
- await runCli([
1844
- "providers",
1845
- "register",
1846
- "--provider-key",
1847
- "custom-update-revoke-body",
1848
- "--auth-url",
1849
- "https://example.com/oauth/authorize",
1850
- "--token-url",
1851
- "https://example.com/oauth/token",
1852
- "--json",
1853
- ]);
1854
-
1855
- const { exitCode } = await runCli([
1856
- "providers",
1857
- "update",
1858
- "custom-update-revoke-body",
1859
- "--revoke-body-template",
1860
- '{"token":"{access_token}","client_id":"{client_id}"}',
1861
- "--json",
1862
- ]);
1863
- expect(exitCode).toBe(0);
1864
- const stored = mockProviderStore.get("custom-update-revoke-body");
1865
- expect(stored).toBeDefined();
1866
- expect(JSON.parse(stored?.revokeBodyTemplate as string)).toEqual({
1867
- token: "{access_token}",
1868
- client_id: "{client_id}",
1869
- });
1870
- // revokeUrl should still be null (unchanged).
1871
- expect(stored?.revokeUrl).toBeNull();
1872
- });
1873
-
1874
- test("providers update --revoke-body-template with empty string clears the stored template to null", async () => {
1875
- // Seed an existing custom provider that already has a non-null
1876
- // revokeBodyTemplate set via register.
1877
- await runCli([
1878
- "providers",
1879
- "register",
1880
- "--provider-key",
1881
- "custom-clear-revoke-body",
1882
- "--auth-url",
1883
- "https://example.com/oauth/authorize",
1884
- "--token-url",
1885
- "https://example.com/oauth/token",
1886
- "--revoke-url",
1887
- "https://revoke.example.com",
1888
- "--revoke-body-template",
1889
- '{"token":"{access_token}"}',
1890
- "--json",
1891
- ]);
1892
- const initial = mockProviderStore.get("custom-clear-revoke-body");
1893
- expect(initial?.revokeBodyTemplate).toBeDefined();
1894
- expect(JSON.parse(initial?.revokeBodyTemplate as string)).toEqual({
1895
- token: "{access_token}",
1896
- });
1897
-
1898
- // Pass an empty string to --revoke-body-template — the help text promises
1899
- // this clears the field, so the stored value should end up as null
1900
- // (not the string "null" or a JSON.parse crash).
1901
- const { exitCode, stdout } = await runCli([
1902
- "providers",
1903
- "update",
1904
- "custom-clear-revoke-body",
1905
- "--revoke-body-template",
1906
- "",
1907
- "--json",
1908
- ]);
1909
- expect(exitCode).toBe(0);
1910
- expect(
1911
- mockProviderStore.get("custom-clear-revoke-body")?.revokeBodyTemplate,
1912
- ).toBeNull();
1913
-
1914
- // The serialized --json output should also reflect null.
1915
- const parsed = JSON.parse(stdout);
1916
- expect(parsed.revokeBodyTemplate).toBeNull();
1917
- });
1918
-
1919
- test("providers get google --json includes both fields populated from PR 1's seed data", async () => {
1920
- // Simulate the seeded "google" provider row by overriding mockGetProvider
1921
- // to return a row matching what PR 1's seed inserts.
1922
- const now = Date.now();
1923
- mockGetProvider = (provider: string) => {
1924
- if (provider !== "google") return undefined;
1925
- return {
1926
- provider: "google",
1927
- authorizeUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1928
- tokenExchangeUrl: "https://oauth2.googleapis.com/token",
1929
- refreshUrl: null,
1930
- tokenEndpointAuthMethod: "client_secret_post",
1931
- userinfoUrl: null,
1932
- baseUrl: null,
1933
- defaultScopes: "[]",
1934
- availableScopes: null,
1935
- scopeSeparator: " ",
1936
- authorizeParams: null,
1937
- pingUrl: null,
1938
- pingMethod: null,
1939
- pingHeaders: null,
1940
- pingBody: null,
1941
- revokeUrl: "https://oauth2.googleapis.com/revoke",
1942
- revokeBodyTemplate: JSON.stringify({ token: "{access_token}" }),
1943
- managedServiceConfigKey: null,
1944
- displayLabel: "Google",
1945
- description: null,
1946
- dashboardUrl: null,
1947
- clientIdPlaceholder: null,
1948
- requiresClientSecret: 1,
1949
- loopbackPort: null,
1950
- injectionTemplates: null,
1951
- appType: null,
1952
- setupNotes: null,
1953
- identityUrl: null,
1954
- identityMethod: null,
1955
- identityHeaders: null,
1956
- identityBody: null,
1957
- identityResponsePaths: null,
1958
- identityFormat: null,
1959
- identityOkField: null,
1960
- featureFlag: null,
1961
- createdAt: now,
1962
- updatedAt: now,
1963
- };
1964
- };
1965
-
1966
- const { exitCode, stdout } = await runCli([
1967
- "providers",
1968
- "get",
1969
- "google",
1970
- "--json",
1971
- ]);
1972
- expect(exitCode).toBe(0);
1973
- const parsed = JSON.parse(stdout);
1974
- expect(parsed.revokeUrl).toBe("https://oauth2.googleapis.com/revoke");
1975
- expect(parsed.revokeBodyTemplate).toEqual({ token: "{access_token}" });
1976
- });
1977
- });