@vellumai/assistant 0.5.10 → 0.5.12

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 (395) hide show
  1. package/AGENTS.md +8 -0
  2. package/ARCHITECTURE.md +43 -43
  3. package/Dockerfile +3 -0
  4. package/docs/architecture/integrations.md +37 -42
  5. package/docs/architecture/memory.md +7 -12
  6. package/docs/credential-execution-service.md +9 -9
  7. package/docs/skills.md +1 -1
  8. package/node_modules/@vellumai/ces-contracts/src/__tests__/grants.test.ts +7 -7
  9. package/node_modules/@vellumai/ces-contracts/src/handles.ts +5 -4
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +3 -3
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7208 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/catalog-cache.test.ts +164 -0
  23. package/src/__tests__/catalog-search.test.ts +61 -0
  24. package/src/__tests__/channel-approvals.test.ts +7 -7
  25. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  26. package/src/__tests__/cli-command-risk-guard.test.ts +181 -6
  27. package/src/__tests__/config-schema.test.ts +10 -2
  28. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  29. package/src/__tests__/conversation-delete-schedule-cleanup.test.ts +396 -0
  30. package/src/__tests__/conversation-error.test.ts +3 -2
  31. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  32. package/src/__tests__/conversation-title-service.test.ts +2 -15
  33. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  34. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  35. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  36. package/src/__tests__/credential-security-invariants.test.ts +12 -18
  37. package/src/__tests__/credential-vault-unit.test.ts +32 -34
  38. package/src/__tests__/credential-vault.test.ts +25 -33
  39. package/src/__tests__/credentials-cli.test.ts +3 -3
  40. package/src/__tests__/daemon-credential-client.test.ts +2 -2
  41. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  42. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  43. package/src/__tests__/heartbeat-service.test.ts +35 -0
  44. package/src/__tests__/host-bash-proxy.test.ts +79 -0
  45. package/src/__tests__/host-cu-proxy.test.ts +90 -0
  46. package/src/__tests__/host-file-proxy.test.ts +89 -0
  47. package/src/__tests__/host-shell-tool.test.ts +1 -1
  48. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  49. package/src/__tests__/integration-status.test.ts +5 -5
  50. package/src/__tests__/list-messages-attachments.test.ts +171 -0
  51. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  52. package/src/__tests__/log-export-workspace.test.ts +1 -1
  53. package/src/__tests__/mcp-abort-signal.test.ts +205 -0
  54. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  55. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  56. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  57. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  58. package/src/__tests__/memory-regressions.test.ts +53 -42
  59. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  60. package/src/__tests__/messaging-send-tool.test.ts +5 -5
  61. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  62. package/src/__tests__/notification-telegram-adapter.test.ts +125 -0
  63. package/src/__tests__/oauth-cli.test.ts +203 -649
  64. package/src/__tests__/oauth-provider-profiles.test.ts +55 -20
  65. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  66. package/src/__tests__/onboarding-template-contract.test.ts +2 -2
  67. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  68. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  69. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  70. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  71. package/src/__tests__/secret-ingress.test.ts +283 -0
  72. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  73. package/src/__tests__/secret-routes-managed-proxy.test.ts +78 -0
  74. package/src/__tests__/secure-keys-managed-failover.test.ts +73 -0
  75. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  76. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  77. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  78. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  79. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  80. package/src/__tests__/skill-memory.test.ts +2 -4
  81. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  82. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  83. package/src/__tests__/skills-uninstall.test.ts +2 -2
  84. package/src/__tests__/skills.test.ts +16 -2
  85. package/src/__tests__/slack-channel-config.test.ts +1 -1
  86. package/src/__tests__/slack-messaging-token-resolution.test.ts +22 -24
  87. package/src/__tests__/slack-share-routes.test.ts +5 -5
  88. package/src/__tests__/slack-skill.test.ts +5 -69
  89. package/src/__tests__/system-prompt.test.ts +39 -0
  90. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  91. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  92. package/src/__tests__/workspace-migration-backfill-installation-id.test.ts +5 -4
  93. package/src/acp/client-handler.ts +113 -31
  94. package/src/acp/session-manager.ts +29 -27
  95. package/src/approvals/guardian-request-resolvers.ts +1 -1
  96. package/src/cli/AGENTS.md +113 -0
  97. package/src/cli/commands/autonomy.ts +3 -5
  98. package/src/cli/commands/browser-relay.ts +2 -17
  99. package/src/cli/commands/contacts.ts +6 -4
  100. package/src/cli/commands/conversations.ts +13 -1
  101. package/src/cli/commands/credential-execution.ts +17 -3
  102. package/src/cli/commands/credentials.ts +2 -8
  103. package/src/cli/commands/memory.ts +2 -3
  104. package/src/cli/commands/oauth/__tests__/connect.test.ts +706 -0
  105. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +686 -0
  106. package/src/cli/commands/oauth/__tests__/mode.test.ts +625 -0
  107. package/src/cli/commands/oauth/__tests__/ping.test.ts +631 -0
  108. package/src/cli/commands/oauth/__tests__/providers-delete.test.ts +574 -0
  109. package/src/cli/commands/oauth/__tests__/providers-update.test.ts +416 -0
  110. package/src/cli/commands/oauth/__tests__/status.test.ts +551 -0
  111. package/src/cli/commands/oauth/__tests__/token.test.ts +420 -0
  112. package/src/cli/commands/oauth/apps.ts +87 -50
  113. package/src/cli/commands/oauth/connect.ts +405 -0
  114. package/src/cli/commands/oauth/disconnect.ts +285 -0
  115. package/src/cli/commands/oauth/index.ts +62 -20
  116. package/src/cli/commands/oauth/mode.ts +251 -0
  117. package/src/cli/commands/oauth/ping.ts +196 -0
  118. package/src/cli/commands/oauth/providers.ts +589 -55
  119. package/src/cli/commands/oauth/request.ts +564 -0
  120. package/src/cli/commands/oauth/shared.ts +114 -0
  121. package/src/cli/commands/oauth/status.ts +191 -0
  122. package/src/cli/commands/oauth/token.ts +150 -0
  123. package/src/cli/commands/platform/connect.ts +104 -0
  124. package/src/cli/commands/platform/disconnect.ts +118 -0
  125. package/src/cli/commands/platform/index.ts +252 -0
  126. package/src/cli/commands/sequence.ts +5 -4
  127. package/src/cli/commands/shotgun.ts +16 -0
  128. package/src/cli/commands/skills.ts +173 -41
  129. package/src/cli/commands/usage.ts +5 -11
  130. package/src/cli/lib/daemon-credential-client.ts +22 -38
  131. package/src/cli/program.ts +1 -1
  132. package/src/cli.ts +82 -17
  133. package/src/config/assistant-feature-flags.ts +77 -18
  134. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  135. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  136. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +1 -1
  137. package/src/config/bundled-skills/conversations/SKILL.md +20 -0
  138. package/src/config/bundled-skills/conversations/TOOLS.json +23 -0
  139. package/src/config/bundled-skills/conversations/tools/rename-conversation.ts +66 -0
  140. package/src/config/bundled-skills/gmail/SKILL.md +13 -13
  141. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +3 -3
  142. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +2 -2
  143. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +1 -1
  144. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +1 -1
  145. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +1 -1
  146. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +1 -1
  147. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +2 -2
  148. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +1 -1
  149. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +1 -1
  150. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +1 -1
  151. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +1 -1
  152. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +1 -1
  153. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +1 -1
  154. package/src/config/bundled-skills/google-calendar/SKILL.md +10 -4
  155. package/src/config/bundled-skills/google-calendar/tools/shared.ts +1 -1
  156. package/src/config/bundled-skills/messaging/SKILL.md +19 -42
  157. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  158. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  159. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +5 -2
  160. package/src/config/bundled-skills/messaging/tools/shared.ts +5 -6
  161. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  162. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  163. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  164. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  165. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  166. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  167. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  168. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  169. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  170. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  171. package/src/config/bundled-tool-registry.ts +7 -19
  172. package/src/config/env.ts +5 -1
  173. package/src/config/feature-flag-registry.json +58 -42
  174. package/src/config/loader.ts +4 -0
  175. package/src/config/schemas/platform.ts +0 -8
  176. package/src/config/schemas/security.ts +9 -1
  177. package/src/config/schemas/services.ts +1 -1
  178. package/src/config/skill-state.ts +1 -3
  179. package/src/config/skills.ts +2 -4
  180. package/src/credential-execution/client.ts +1 -1
  181. package/src/credential-execution/feature-gates.ts +9 -16
  182. package/src/credential-execution/process-manager.ts +12 -0
  183. package/src/daemon/config-watcher.ts +4 -0
  184. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  185. package/src/daemon/conversation-agent-loop.ts +51 -2
  186. package/src/daemon/conversation-error.ts +36 -6
  187. package/src/daemon/conversation-memory.ts +0 -1
  188. package/src/daemon/conversation-messaging.ts +9 -0
  189. package/src/daemon/conversation-runtime-assembly.ts +33 -0
  190. package/src/daemon/conversation-surfaces.ts +120 -14
  191. package/src/daemon/conversation.ts +5 -0
  192. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  193. package/src/daemon/handlers/conversations.ts +41 -33
  194. package/src/daemon/handlers/skills.ts +148 -3
  195. package/src/daemon/host-bash-proxy.ts +16 -0
  196. package/src/daemon/host-cu-proxy.ts +16 -0
  197. package/src/daemon/host-file-proxy.ts +16 -0
  198. package/src/daemon/lifecycle.ts +73 -3
  199. package/src/daemon/message-types/acp.ts +0 -15
  200. package/src/daemon/message-types/conversations.ts +1 -0
  201. package/src/daemon/message-types/guardian-actions.ts +2 -0
  202. package/src/daemon/message-types/host-bash.ts +6 -1
  203. package/src/daemon/message-types/host-cu.ts +6 -1
  204. package/src/daemon/message-types/host-file.ts +6 -1
  205. package/src/daemon/message-types/integrations.ts +0 -1
  206. package/src/daemon/message-types/memory.ts +0 -1
  207. package/src/daemon/message-types/messages.ts +9 -1
  208. package/src/daemon/message-types/schedules.ts +9 -0
  209. package/src/daemon/server.ts +48 -9
  210. package/src/email/feature-gate.ts +3 -3
  211. package/src/heartbeat/heartbeat-service.ts +48 -0
  212. package/src/hooks/cli.ts +74 -0
  213. package/src/inbound/platform-callback-registration.ts +68 -19
  214. package/src/mcp/client.ts +6 -1
  215. package/src/mcp/manager.ts +2 -1
  216. package/src/mcp/mcp-oauth-provider.ts +3 -3
  217. package/src/memory/app-store.ts +3 -3
  218. package/src/memory/conversation-crud.ts +213 -0
  219. package/src/memory/conversation-key-store.ts +26 -0
  220. package/src/memory/conversation-title-service.ts +7 -17
  221. package/src/memory/db-init.ts +24 -0
  222. package/src/memory/embedding-local.ts +47 -2
  223. package/src/memory/indexer.ts +13 -10
  224. package/src/memory/items-extractor.ts +12 -4
  225. package/src/memory/job-utils.ts +5 -0
  226. package/src/memory/jobs-store.ts +10 -2
  227. package/src/memory/journal-memory.ts +6 -2
  228. package/src/memory/llm-request-log-store.ts +88 -21
  229. package/src/memory/memory-recall-log-store.ts +128 -0
  230. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  231. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  232. package/src/memory/migrations/196-messages-conversation-created-at-index.ts +9 -0
  233. package/src/memory/migrations/196-strip-integration-prefix-from-provider-keys.ts +186 -0
  234. package/src/memory/migrations/197-oauth-providers-behavior-columns.ts +29 -0
  235. package/src/memory/migrations/198-drop-setup-skill-id-column.ts +11 -0
  236. package/src/memory/migrations/index.ts +6 -0
  237. package/src/memory/migrations/registry.ts +8 -0
  238. package/src/memory/retriever.test.ts +4 -5
  239. package/src/memory/schema/infrastructure.ts +31 -0
  240. package/src/memory/schema/oauth.ts +14 -0
  241. package/src/messaging/provider.ts +13 -12
  242. package/src/messaging/providers/gmail/adapter.ts +44 -35
  243. package/src/messaging/providers/slack/adapter.ts +63 -33
  244. package/src/messaging/providers/telegram-bot/adapter.ts +7 -9
  245. package/src/messaging/providers/whatsapp/adapter.ts +6 -8
  246. package/src/notifications/adapters/telegram.ts +78 -2
  247. package/src/oauth/__tests__/identity-verifier.test.ts +464 -0
  248. package/src/oauth/byo-connection.test.ts +22 -24
  249. package/src/oauth/connect-orchestrator.ts +79 -64
  250. package/src/oauth/connect-types.ts +7 -65
  251. package/src/oauth/connection-resolver.test.ts +13 -13
  252. package/src/oauth/connection-resolver.ts +3 -4
  253. package/src/oauth/identity-verifier.ts +177 -0
  254. package/src/oauth/manual-token-connection.ts +5 -5
  255. package/src/oauth/oauth-store.ts +251 -5
  256. package/src/oauth/platform-connection.test.ts +56 -6
  257. package/src/oauth/platform-connection.ts +8 -1
  258. package/src/oauth/seed-providers.ts +256 -34
  259. package/src/permissions/checker.ts +129 -3
  260. package/src/permissions/trust-client.ts +2 -2
  261. package/src/platform/client.ts +2 -2
  262. package/src/prompts/journal-context.ts +6 -1
  263. package/src/prompts/system-prompt.ts +43 -9
  264. package/src/prompts/templates/BOOTSTRAP.md +16 -5
  265. package/src/providers/anthropic/client.ts +139 -28
  266. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  267. package/src/runtime/auth/route-policy.ts +0 -1
  268. package/src/runtime/btw-sidechain.ts +7 -1
  269. package/src/runtime/channel-approvals.ts +2 -2
  270. package/src/runtime/channel-readiness-service.ts +30 -7
  271. package/src/runtime/guardian-action-service.ts +7 -2
  272. package/src/runtime/http-router.ts +31 -0
  273. package/src/runtime/http-server.ts +26 -7
  274. package/src/runtime/http-types.ts +9 -0
  275. package/src/runtime/pending-interactions.ts +21 -3
  276. package/src/runtime/routes/acp-routes.ts +46 -28
  277. package/src/runtime/routes/app-management-routes.ts +123 -0
  278. package/src/runtime/routes/app-routes.ts +31 -0
  279. package/src/runtime/routes/approval-routes.ts +108 -3
  280. package/src/runtime/routes/attachment-routes.ts +45 -0
  281. package/src/runtime/routes/avatar-routes.ts +16 -0
  282. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  283. package/src/runtime/routes/btw-routes.ts +20 -0
  284. package/src/runtime/routes/call-routes.ts +81 -0
  285. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  286. package/src/runtime/routes/channel-routes.ts +18 -0
  287. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  288. package/src/runtime/routes/contact-routes.ts +77 -0
  289. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  290. package/src/runtime/routes/conversation-management-routes.ts +125 -0
  291. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  292. package/src/runtime/routes/conversation-routes.ts +191 -39
  293. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  294. package/src/runtime/routes/debug-routes.ts +23 -0
  295. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  296. package/src/runtime/routes/documents-routes.ts +42 -0
  297. package/src/runtime/routes/events-routes.ts +10 -0
  298. package/src/runtime/routes/global-search-routes.ts +35 -0
  299. package/src/runtime/routes/guardian-action-routes.ts +61 -3
  300. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  301. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  302. package/src/runtime/routes/host-bash-routes.ts +16 -1
  303. package/src/runtime/routes/host-cu-routes.ts +23 -1
  304. package/src/runtime/routes/host-file-routes.ts +18 -1
  305. package/src/runtime/routes/identity-routes.ts +35 -0
  306. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  307. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -8
  308. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  309. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  310. package/src/runtime/routes/integrations/slack/share.ts +1 -1
  311. package/src/runtime/routes/integrations/twilio.ts +32 -22
  312. package/src/runtime/routes/invite-routes.ts +83 -0
  313. package/src/runtime/routes/log-export-routes.ts +14 -0
  314. package/src/runtime/routes/memory-item-routes.ts +99 -1
  315. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  316. package/src/runtime/routes/migration-routes.ts +40 -0
  317. package/src/runtime/routes/notification-routes.ts +20 -0
  318. package/src/runtime/routes/oauth-apps.ts +13 -4
  319. package/src/runtime/routes/pairing-routes.ts +15 -0
  320. package/src/runtime/routes/recording-routes.ts +72 -0
  321. package/src/runtime/routes/schedule-routes.ts +77 -5
  322. package/src/runtime/routes/secret-routes.ts +99 -14
  323. package/src/runtime/routes/settings-routes.ts +102 -19
  324. package/src/runtime/routes/skills-routes.ts +141 -18
  325. package/src/runtime/routes/subagents-routes.ts +38 -3
  326. package/src/runtime/routes/surface-action-routes.ts +66 -24
  327. package/src/runtime/routes/surface-content-routes.ts +20 -0
  328. package/src/runtime/routes/telemetry-routes.ts +12 -0
  329. package/src/runtime/routes/trace-event-routes.ts +25 -0
  330. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  331. package/src/runtime/routes/tts-routes.ts +15 -4
  332. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  333. package/src/runtime/routes/usage-routes.ts +59 -0
  334. package/src/runtime/routes/watch-routes.ts +28 -0
  335. package/src/runtime/routes/work-items-routes.ts +59 -0
  336. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  337. package/src/runtime/routes/workspace-routes.ts +102 -0
  338. package/src/schedule/integration-status.ts +2 -2
  339. package/src/schedule/scheduler.ts +7 -1
  340. package/src/security/AGENTS.md +7 -0
  341. package/src/security/ces-rpc-credential-backend.ts +19 -16
  342. package/src/security/credential-backend.ts +1 -1
  343. package/src/security/encrypted-store.ts +3 -3
  344. package/src/security/oauth-completion-page.ts +153 -0
  345. package/src/security/oauth2.ts +58 -17
  346. package/src/security/secret-ingress.ts +174 -0
  347. package/src/security/secret-patterns.ts +133 -0
  348. package/src/security/secret-scanner.ts +28 -117
  349. package/src/security/secure-keys.ts +207 -7
  350. package/src/security/token-manager.ts +3 -6
  351. package/src/signals/bash.ts +6 -1
  352. package/src/signals/confirm.ts +12 -8
  353. package/src/signals/user-message.ts +18 -3
  354. package/src/skills/catalog-cache.ts +44 -0
  355. package/src/skills/catalog-search.ts +18 -0
  356. package/src/skills/skill-memory.ts +1 -2
  357. package/src/tasks/task-runner.ts +7 -1
  358. package/src/tools/credentials/broker.ts +1 -1
  359. package/src/tools/credentials/metadata-store.ts +1 -1
  360. package/src/tools/credentials/post-connect-hooks.ts +1 -1
  361. package/src/tools/credentials/vault.ts +36 -48
  362. package/src/tools/host-terminal/host-shell.ts +16 -3
  363. package/src/tools/mcp/mcp-tool-factory.ts +2 -1
  364. package/src/tools/memory/definitions.ts +1 -1
  365. package/src/tools/memory/handlers.test.ts +2 -4
  366. package/src/tools/skills/load.ts +1 -1
  367. package/src/tools/skills/sandbox-runner.ts +16 -3
  368. package/src/tools/terminal/safe-env.ts +7 -0
  369. package/src/tools/terminal/shell.ts +16 -3
  370. package/src/tools/tool-manifest.ts +1 -1
  371. package/src/util/log-redact.ts +9 -34
  372. package/src/util/logger.ts +11 -1
  373. package/src/util/sentry-log-stream.ts +51 -0
  374. package/src/watcher/providers/github.ts +2 -2
  375. package/src/watcher/providers/gmail.ts +1 -1
  376. package/src/watcher/providers/google-calendar.ts +1 -1
  377. package/src/watcher/providers/linear.ts +2 -2
  378. package/src/workspace/migrations/011-backfill-installation-id.ts +5 -3
  379. package/src/workspace/migrations/020-rename-oauth-skill-dirs.ts +119 -0
  380. package/src/workspace/migrations/registry.ts +2 -0
  381. package/docs/architecture/keychain-broker.md +0 -68
  382. package/src/cli/commands/oauth/connections.ts +0 -734
  383. package/src/cli/commands/oauth/platform.ts +0 -525
  384. package/src/cli/commands/platform.ts +0 -176
  385. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  386. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  387. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  388. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  389. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  390. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  391. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  392. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  393. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  394. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  395. package/src/oauth/provider-behaviors.ts +0 -634
@@ -0,0 +1,405 @@
1
+ import { createServer, type Server } from "node:http";
2
+
3
+ import type { Command } from "commander";
4
+
5
+ import { orchestrateOAuthConnect } from "../../../oauth/connect-orchestrator.js";
6
+ import {
7
+ getAppByProviderAndClientId,
8
+ getMostRecentAppByProvider,
9
+ getProvider,
10
+ } from "../../../oauth/oauth-store.js";
11
+ import { renderOAuthCompletionPage } from "../../../security/oauth-completion-page.js";
12
+ import { openInBrowser } from "../../../util/browser.js";
13
+ import { getSecureKeyViaDaemon } from "../../lib/daemon-credential-client.js";
14
+ import { getCliLogger } from "../../logger.js";
15
+ import { shouldOutputJson, writeOutput } from "../../output.js";
16
+ import {
17
+ fetchActiveConnections,
18
+ isManagedMode,
19
+ requirePlatformClient,
20
+ } from "./shared.js";
21
+
22
+ const log = getCliLogger("cli");
23
+
24
+ /**
25
+ * Start a temporary loopback server to serve a nice completion page after the
26
+ * platform redirects the user's browser following a managed OAuth flow.
27
+ * Returns the base URL and a cleanup function.
28
+ */
29
+ function startManagedRedirectServer(provider: string): Promise<{
30
+ redirectUrl: string;
31
+ cleanup: () => void;
32
+ }> {
33
+ return new Promise((resolve, reject) => {
34
+ const server: Server = createServer((req, res) => {
35
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
36
+ const error = url.searchParams.get("error");
37
+ const errorDesc = url.searchParams.get("error_description");
38
+ const providerHint = url.searchParams.get("provider") ?? provider;
39
+
40
+ if (error) {
41
+ const message = errorDesc ?? error;
42
+ res.writeHead(200, { "Content-Type": "text/html" });
43
+ res.end(renderOAuthCompletionPage(message, false, providerHint));
44
+ } else {
45
+ res.writeHead(200, { "Content-Type": "text/html" });
46
+ res.end(
47
+ renderOAuthCompletionPage(
48
+ "You can close this tab and return to your assistant.",
49
+ true,
50
+ providerHint,
51
+ ),
52
+ );
53
+ }
54
+ });
55
+
56
+ server.listen(0, "localhost", () => {
57
+ const addr = server.address() as { port: number };
58
+ const redirectUrl = `http://localhost:${addr.port}/oauth/complete`;
59
+ resolve({
60
+ redirectUrl,
61
+ cleanup: () => server.close(),
62
+ });
63
+ });
64
+
65
+ server.on("error", (err) => {
66
+ reject(new Error(`Failed to start redirect server: ${err.message}`));
67
+ });
68
+ });
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Command registration
73
+ // ---------------------------------------------------------------------------
74
+
75
+ export function registerConnectCommand(oauth: Command): void {
76
+ oauth
77
+ .command("connect <provider>")
78
+ .description(
79
+ "Initiate an OAuth authorization flow for a specified provider",
80
+ )
81
+ .option("--scopes <scopes...>", "Scopes to request for the authorization")
82
+ .option(
83
+ "--open-browser",
84
+ "Open the auth URL in the browser and wait for completion",
85
+ )
86
+ .option("--client-id <id>", "BYO app client ID disambiguation")
87
+ .addHelpText(
88
+ "after",
89
+ `
90
+ Arguments:
91
+ provider Provider name (e.g. google, slack, notion).
92
+ Run 'assistant oauth providers list' to see available providers.
93
+
94
+ In managed mode, --scopes must be in the provider's allowed set (use full
95
+ scope URLs). In BYO mode, --scopes are appended to the provider's defaults.
96
+ The --open-browser flag polls for a platform connection (managed) or starts
97
+ a local callback server (BYO).
98
+
99
+ Examples:
100
+ $ assistant oauth connect google
101
+ $ assistant oauth connect google --open-browser
102
+ $ assistant oauth connect google --scopes https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events
103
+ $ assistant oauth connect google --client-id abc123 --open-browser`,
104
+ )
105
+ .action(
106
+ async (
107
+ provider: string,
108
+ opts: {
109
+ scopes?: string[];
110
+ openBrowser?: boolean;
111
+ clientId?: string;
112
+ },
113
+ cmd: Command,
114
+ ) => {
115
+ const jsonMode = shouldOutputJson(cmd);
116
+
117
+ // Helper: write an error and set exit code
118
+ const writeError = (error: string): void => {
119
+ writeOutput(cmd, { ok: false, error });
120
+ process.exitCode = 1;
121
+ };
122
+
123
+ try {
124
+ // ---------------------------------------------------------------
125
+ // 1. Validate provider exists
126
+ // ---------------------------------------------------------------
127
+ const providerRow = getProvider(provider);
128
+ if (!providerRow) {
129
+ writeError(
130
+ `Unknown provider "${provider}". ` +
131
+ `Run 'assistant oauth providers list' to see available providers.`,
132
+ );
133
+ return;
134
+ }
135
+
136
+ // ---------------------------------------------------------------
137
+ // 3. Detect mode
138
+ // ---------------------------------------------------------------
139
+ const managed = isManagedMode(provider);
140
+
141
+ if (managed) {
142
+ // =============================================================
143
+ // MANAGED PATH
144
+ // =============================================================
145
+
146
+ // Warn about --client-id being ignored in managed mode
147
+ if (opts.clientId) {
148
+ log.info(
149
+ `Warning: --client-id is ignored for platform-managed providers. The platform manages OAuth apps for "${provider}".`,
150
+ );
151
+ }
152
+
153
+ const client = await requirePlatformClient(cmd);
154
+ if (!client) return;
155
+
156
+ // Call the platform's OAuth start endpoint
157
+ const startPath = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/${encodeURIComponent(provider)}/start/`;
158
+
159
+ const body: Record<string, unknown> = {};
160
+ if (opts.scopes && opts.scopes.length > 0) {
161
+ body.requested_scopes = opts.scopes;
162
+ }
163
+
164
+ // When opening the browser, start a local server to show a nice
165
+ // completion page instead of redirecting to the platform website.
166
+ let redirectServer:
167
+ | { redirectUrl: string; cleanup: () => void }
168
+ | undefined;
169
+ if (opts.openBrowser) {
170
+ try {
171
+ redirectServer = await startManagedRedirectServer(provider);
172
+ body.redirect_after_connect = redirectServer.redirectUrl;
173
+ } catch {
174
+ // Non-fatal — fall back to platform default redirect
175
+ }
176
+ }
177
+
178
+ try {
179
+ const response = await client.fetch(startPath, {
180
+ method: "POST",
181
+ headers: { "Content-Type": "application/json" },
182
+ body: JSON.stringify(body),
183
+ });
184
+
185
+ if (!response.ok) {
186
+ const errorText = await response.text().catch(() => "");
187
+ writeError(
188
+ `Platform returned HTTP ${response.status}${errorText ? `: ${errorText}` : ""}`,
189
+ );
190
+ return;
191
+ }
192
+
193
+ const result = (await response.json()) as {
194
+ connect_url?: string;
195
+ };
196
+
197
+ if (!result.connect_url) {
198
+ writeError(
199
+ "Platform did not return a connect URL — the OAuth flow could not be started",
200
+ );
201
+ return;
202
+ }
203
+
204
+ if (opts.openBrowser) {
205
+ // Snapshot existing connection IDs before opening browser
206
+ const snapshotEntries = await fetchActiveConnections(
207
+ client,
208
+ provider,
209
+ cmd,
210
+ );
211
+ if (!snapshotEntries) {
212
+ // fetchActiveConnections already wrote the error output
213
+ return;
214
+ }
215
+ const snapshotIds = new Set(snapshotEntries.map((e) => e.id));
216
+
217
+ openInBrowser(result.connect_url);
218
+
219
+ if (!jsonMode) {
220
+ log.info(
221
+ `Opening browser to connect ${provider}. Waiting for authorization...`,
222
+ );
223
+ }
224
+
225
+ // Poll for a new connection every 2s for up to 5 minutes
226
+ const pollIntervalMs = 2000;
227
+ const timeoutMs = 5 * 60 * 1000;
228
+ const deadline = Date.now() + timeoutMs;
229
+ let newConnection: {
230
+ id: string;
231
+ account_label?: string;
232
+ scopes_granted?: string[];
233
+ } | null = null;
234
+
235
+ while (Date.now() < deadline) {
236
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
237
+
238
+ const currentEntries = await fetchActiveConnections(
239
+ client,
240
+ provider,
241
+ cmd,
242
+ { silent: true },
243
+ );
244
+ if (!currentEntries) continue;
245
+
246
+ const found = currentEntries.find(
247
+ (e) => !snapshotIds.has(e.id),
248
+ );
249
+ if (found) {
250
+ newConnection = found;
251
+ break;
252
+ }
253
+ }
254
+
255
+ if (newConnection) {
256
+ // Success — new connection found
257
+ if (jsonMode) {
258
+ writeOutput(cmd, {
259
+ ok: true,
260
+ provider: provider,
261
+ connectionId: newConnection.id,
262
+ accountLabel: newConnection.account_label ?? null,
263
+ scopesGranted: newConnection.scopes_granted ?? [],
264
+ });
265
+ } else {
266
+ const label = newConnection.account_label
267
+ ? ` as ${newConnection.account_label}`
268
+ : "";
269
+ process.stdout.write(`Connected to ${provider}${label}\n`);
270
+ }
271
+ } else {
272
+ // Timeout — authorization may still be in progress
273
+ if (jsonMode) {
274
+ writeOutput(cmd, {
275
+ ok: true,
276
+ deferred: true,
277
+ provider: provider,
278
+ connectUrl: result.connect_url,
279
+ message:
280
+ "Authorization may still be in progress. Check with 'assistant oauth status <provider>'.",
281
+ });
282
+ } else {
283
+ process.stdout.write(
284
+ `Timed out waiting for authorization. It may still be in progress.\n` +
285
+ `Check with: assistant oauth status ${provider}\n`,
286
+ );
287
+ }
288
+ }
289
+ } else {
290
+ // No --open-browser: output the connect URL
291
+ if (jsonMode) {
292
+ writeOutput(cmd, {
293
+ ok: true,
294
+ deferred: true,
295
+ connectUrl: result.connect_url,
296
+ provider: provider,
297
+ });
298
+ } else {
299
+ process.stdout.write(result.connect_url + "\n");
300
+ }
301
+ }
302
+ } finally {
303
+ redirectServer?.cleanup();
304
+ }
305
+ } else {
306
+ // =============================================================
307
+ // BYO PATH
308
+ // =============================================================
309
+
310
+ // a. Resolve client credentials from the DB
311
+ const dbApp = opts.clientId
312
+ ? getAppByProviderAndClientId(provider, opts.clientId)
313
+ : getMostRecentAppByProvider(provider);
314
+
315
+ let clientId = opts.clientId;
316
+ let clientSecret: string | undefined;
317
+
318
+ if (dbApp) {
319
+ if (!clientId) clientId = dbApp.clientId;
320
+ const storedSecret = await getSecureKeyViaDaemon(
321
+ dbApp.clientSecretCredentialPath,
322
+ );
323
+ if (storedSecret) clientSecret = storedSecret;
324
+ } else if (opts.clientId) {
325
+ // --client-id was explicitly provided but no matching app exists
326
+ writeError(
327
+ `No registered app found for "${provider}" with client ID "${opts.clientId}". ` +
328
+ `Register one with 'assistant oauth apps upsert'.`,
329
+ );
330
+ return;
331
+ }
332
+
333
+ // c. Validate client_id exists
334
+ if (!clientId) {
335
+ writeError(
336
+ `No client_id found for "${provider}". ` +
337
+ `Register one with 'assistant oauth apps upsert'.`,
338
+ );
339
+ return;
340
+ }
341
+
342
+ // d. Check if client_secret is required but missing
343
+ if (clientSecret === undefined) {
344
+ const requiresSecret = !!providerRow?.requiresClientSecret;
345
+
346
+ if (requiresSecret) {
347
+ writeError(
348
+ `client_secret is required for ${provider} but not found. ` +
349
+ `Store it with 'assistant oauth apps upsert --client-secret'.`,
350
+ );
351
+ return;
352
+ }
353
+ }
354
+
355
+ // e. Call the orchestrator
356
+ const result = await orchestrateOAuthConnect({
357
+ service: provider,
358
+ clientId,
359
+ clientSecret,
360
+ isInteractive: !!opts.openBrowser,
361
+ openUrl: opts.openBrowser ? openInBrowser : undefined,
362
+ ...(opts.scopes ? { requestedScopes: opts.scopes } : {}),
363
+ });
364
+
365
+ // f. Handle results
366
+ if (!result.success) {
367
+ writeError(result.error ?? "OAuth connect failed");
368
+ return;
369
+ }
370
+
371
+ if (result.deferred) {
372
+ if (jsonMode) {
373
+ writeOutput(cmd, {
374
+ ok: true,
375
+ deferred: true,
376
+ authUrl: result.authUrl,
377
+ service: result.service,
378
+ });
379
+ } else {
380
+ process.stdout.write(
381
+ `\nAuthorize with ${provider}:\n\n${result.authUrl}\n\nThe connection will complete automatically once you authorize.\n`,
382
+ );
383
+ }
384
+ return;
385
+ }
386
+
387
+ // Interactive mode completed
388
+ if (jsonMode) {
389
+ writeOutput(cmd, {
390
+ ok: true,
391
+ grantedScopes: result.grantedScopes,
392
+ accountInfo: result.accountInfo,
393
+ });
394
+ } else {
395
+ const msg = `Connected to ${provider}${result.accountInfo ? ` as ${result.accountInfo}` : ""}`;
396
+ process.stdout.write(msg + "\n");
397
+ }
398
+ }
399
+ } catch (err) {
400
+ const message = err instanceof Error ? err.message : String(err);
401
+ writeError(message);
402
+ }
403
+ },
404
+ );
405
+ }
@@ -0,0 +1,285 @@
1
+ import type { Command } from "commander";
2
+
3
+ import {
4
+ disconnectOAuthProvider,
5
+ getActiveConnection,
6
+ getConnection,
7
+ getProvider,
8
+ listActiveConnectionsByProvider,
9
+ } from "../../../oauth/oauth-store.js";
10
+ import { getCliLogger } from "../../logger.js";
11
+ import { shouldOutputJson, writeOutput } from "../../output.js";
12
+ import {
13
+ fetchActiveConnections,
14
+ isManagedMode,
15
+ requirePlatformClient,
16
+ } from "./shared.js";
17
+
18
+ const log = getCliLogger("cli");
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Command registration
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export function registerDisconnectCommand(oauth: Command): void {
25
+ oauth
26
+ .command("disconnect <provider>")
27
+ .description(
28
+ "Disconnect an OAuth provider and remove associated credentials",
29
+ )
30
+ .option(
31
+ "--account <identifier>",
32
+ "Account identifier to disconnect (e.g. email address)",
33
+ )
34
+ .option("--connection-id <id>", "Exact connection ID to disconnect")
35
+ .addHelpText(
36
+ "after",
37
+ `
38
+ Arguments:
39
+ provider Provider name (e.g. google, slack, notion).
40
+ Run 'assistant oauth providers list' to see available providers.
41
+
42
+ At most one of --account or --connection-id may be specified. Use the values
43
+ shown by 'assistant oauth status <provider>' to find the right identifier.
44
+
45
+ When a provider has multiple active connections and neither flag is given,
46
+ the command errors with a list of connections and a hint to disambiguate.
47
+
48
+ Examples:
49
+ $ assistant oauth disconnect google
50
+ $ assistant oauth disconnect google --account user@gmail.com
51
+ $ assistant oauth disconnect google --connection-id conn_abc123`,
52
+ )
53
+ .action(
54
+ async (
55
+ provider: string,
56
+ opts: { account?: string; connectionId?: string },
57
+ cmd: Command,
58
+ ) => {
59
+ const jsonMode = shouldOutputJson(cmd);
60
+
61
+ // Helper: write an error and set exit code
62
+ const writeError = (
63
+ error: string,
64
+ extra?: Record<string, unknown>,
65
+ ): void => {
66
+ writeOutput(cmd, { ok: false, error, ...extra });
67
+ process.exitCode = 1;
68
+ };
69
+
70
+ try {
71
+ // -------------------------------------------------------------------
72
+ // 1. Validate provider
73
+ // -------------------------------------------------------------------
74
+ const providerRow = getProvider(provider);
75
+ if (!providerRow) {
76
+ writeError(
77
+ `Unknown provider "${provider}".\n\n` +
78
+ `Run 'assistant oauth providers list' to see available providers.\n` +
79
+ `If this is a custom provider, register it first with 'assistant oauth providers register --help'.`,
80
+ );
81
+ return;
82
+ }
83
+
84
+ // -------------------------------------------------------------------
85
+ // 2. Validate mutual exclusivity
86
+ // -------------------------------------------------------------------
87
+ if (opts.account && opts.connectionId) {
88
+ writeError(
89
+ `Cannot specify both --account and --connection-id. Use one or the other.\n\n` +
90
+ `Run 'assistant oauth status ${provider}' to see connected accounts and IDs.`,
91
+ );
92
+ return;
93
+ }
94
+
95
+ // -------------------------------------------------------------------
96
+ // 3. Detect mode
97
+ // -------------------------------------------------------------------
98
+ const managed = isManagedMode(provider);
99
+
100
+ if (managed) {
101
+ // -----------------------------------------------------------------
102
+ // Managed path
103
+ // -----------------------------------------------------------------
104
+ const client = await requirePlatformClient(cmd);
105
+ if (!client) return;
106
+
107
+ const entries = await fetchActiveConnections(client, provider, cmd);
108
+ if (!entries) return;
109
+
110
+ let connectionId: string | undefined;
111
+ let accountLabel: string | undefined;
112
+
113
+ if (opts.account) {
114
+ // Filter connections by account_label matching the account value
115
+ const matching = entries.filter(
116
+ (c) => c.account_label === opts.account,
117
+ );
118
+ if (matching.length === 0) {
119
+ writeError(
120
+ `No active connection found for "${provider}" with account "${opts.account}".\n\n` +
121
+ `Run 'assistant oauth status ${provider}' to see connected accounts.`,
122
+ );
123
+ return;
124
+ }
125
+ connectionId = matching[0].id;
126
+ accountLabel = matching[0].account_label;
127
+ } else if (opts.connectionId) {
128
+ // Verify the supplied ID belongs to this provider
129
+ const match = entries.find((c) => c.id === opts.connectionId);
130
+ if (!match) {
131
+ writeError(
132
+ `Connection "${opts.connectionId}" is not an active ${provider} connection.\n\n` +
133
+ `Run 'assistant oauth status ${provider}' to see active connections.`,
134
+ );
135
+ return;
136
+ }
137
+ connectionId = match.id;
138
+ accountLabel = match.account_label;
139
+ } else {
140
+ // Neither specified — auto-resolve
141
+ if (entries.length === 0) {
142
+ writeError(
143
+ `No active connections found for "${provider}".\n\n` +
144
+ `Run 'assistant oauth status ${provider}' to check connection status.`,
145
+ );
146
+ return;
147
+ }
148
+
149
+ if (entries.length > 1) {
150
+ const connectionList = entries.map((c) => ({
151
+ id: c.id,
152
+ account: c.account_label ?? null,
153
+ }));
154
+ writeError(
155
+ `Multiple active connections for "${provider}". ` +
156
+ `Specify which one to disconnect with --account or --connection-id.\n\n` +
157
+ `Run 'assistant oauth status ${provider}' to see connected accounts and IDs.`,
158
+ { connections: connectionList },
159
+ );
160
+ return;
161
+ }
162
+
163
+ connectionId = entries[0].id;
164
+ accountLabel = entries[0].account_label;
165
+ }
166
+
167
+ // Call platform /disconnect/ endpoint
168
+ const disconnectPath = `/v1/assistants/${encodeURIComponent(client.platformAssistantId)}/oauth/connections/${encodeURIComponent(connectionId)}/disconnect/`;
169
+ const disconnectResponse = await client.fetch(disconnectPath, {
170
+ method: "POST",
171
+ headers: { "Content-Type": "application/json" },
172
+ });
173
+
174
+ if (!disconnectResponse.ok) {
175
+ const errorText = await disconnectResponse.text().catch(() => "");
176
+ writeError(
177
+ `Platform returned HTTP ${disconnectResponse.status}${errorText ? `: ${errorText}` : ""}`,
178
+ );
179
+ return;
180
+ }
181
+
182
+ const result: Record<string, unknown> = {
183
+ ok: true,
184
+ provider: provider,
185
+ connectionId,
186
+ };
187
+ if (accountLabel) result.account = accountLabel;
188
+ writeOutput(cmd, result);
189
+
190
+ if (!jsonMode) {
191
+ log.info(`Disconnected ${provider} connection ${connectionId}`);
192
+ }
193
+ } else {
194
+ // -----------------------------------------------------------------
195
+ // BYO path
196
+ // -----------------------------------------------------------------
197
+ let connectionId: string | undefined;
198
+ let accountLabel: string | undefined;
199
+
200
+ if (opts.account) {
201
+ const conn = getActiveConnection(provider, {
202
+ account: opts.account,
203
+ });
204
+ if (!conn) {
205
+ writeError(
206
+ `No active connection found for "${provider}" with account "${opts.account}".\n\n` +
207
+ `Run 'assistant oauth status ${provider}' to see connected accounts.`,
208
+ );
209
+ return;
210
+ }
211
+ connectionId = conn.id;
212
+ accountLabel = conn.accountInfo ?? undefined;
213
+ } else if (opts.connectionId) {
214
+ const conn = getConnection(opts.connectionId);
215
+ if (!conn || conn.providerKey !== provider) {
216
+ writeError(
217
+ `Connection "${opts.connectionId}" is not an active ${provider} connection.\n\n` +
218
+ `Run 'assistant oauth status ${provider}' to see active connections.`,
219
+ );
220
+ return;
221
+ }
222
+ connectionId = conn.id;
223
+ accountLabel = conn.accountInfo ?? undefined;
224
+ } else {
225
+ // Neither specified — auto-resolve
226
+ const active = listActiveConnectionsByProvider(provider);
227
+
228
+ if (active.length === 0) {
229
+ writeError(
230
+ `No active connections found for "${provider}".\n\n` +
231
+ `Run 'assistant oauth status ${provider}' to check connection status.`,
232
+ );
233
+ return;
234
+ }
235
+
236
+ if (active.length > 1) {
237
+ const connectionList = active.map((c) => ({
238
+ id: c.id,
239
+ account: c.accountInfo ?? null,
240
+ }));
241
+ writeError(
242
+ `Multiple active connections for "${provider}". ` +
243
+ `Specify which one to disconnect with --account or --connection-id.\n\n` +
244
+ `Run 'assistant oauth status ${provider}' to see connected accounts and IDs.`,
245
+ { connections: connectionList },
246
+ );
247
+ return;
248
+ }
249
+
250
+ connectionId = active[0].id;
251
+ accountLabel = active[0].accountInfo ?? undefined;
252
+ }
253
+
254
+ // Disconnect the OAuth connection (tokens + connection row)
255
+ const oauthResult = await disconnectOAuthProvider(
256
+ provider,
257
+ undefined,
258
+ connectionId,
259
+ );
260
+ if (oauthResult === "error") {
261
+ writeError(
262
+ `Failed to disconnect OAuth provider "${provider}" — please try again.`,
263
+ );
264
+ return;
265
+ }
266
+
267
+ const result: Record<string, unknown> = {
268
+ ok: true,
269
+ provider: provider,
270
+ connectionId,
271
+ };
272
+ if (accountLabel) result.account = accountLabel;
273
+ writeOutput(cmd, result);
274
+
275
+ if (!jsonMode) {
276
+ log.info(`Disconnected ${provider} connection ${connectionId}`);
277
+ }
278
+ }
279
+ } catch (err) {
280
+ const message = err instanceof Error ? err.message : String(err);
281
+ writeError(message);
282
+ }
283
+ },
284
+ );
285
+ }