@vellumai/assistant 0.4.48 → 0.4.50

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 (423) hide show
  1. package/ARCHITECTURE.md +26 -35
  2. package/README.md +5 -26
  3. package/docs/architecture/integrations.md +45 -41
  4. package/docs/architecture/keychain-broker.md +3 -3
  5. package/docs/architecture/memory.md +180 -119
  6. package/docs/runbook-trusted-contacts.md +3 -8
  7. package/hook-templates/debug-prompt-logger/hook.json +1 -1
  8. package/hook-templates/debug-prompt-logger/run.sh +1 -3
  9. package/package.json +2 -2
  10. package/src/__tests__/actor-token-service.test.ts +0 -1
  11. package/src/__tests__/agent-loop.test.ts +3 -1
  12. package/src/__tests__/anthropic-provider.test.ts +249 -2
  13. package/src/__tests__/approval-cascade.test.ts +796 -0
  14. package/src/__tests__/approval-primitive.test.ts +0 -1
  15. package/src/__tests__/approval-routes-http.test.ts +4 -0
  16. package/src/__tests__/assistant-attachments.test.ts +12 -34
  17. package/src/__tests__/assistant-feature-flag-guard.test.ts +0 -23
  18. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +76 -0
  19. package/src/__tests__/assistant-feature-flags-integration.test.ts +0 -1
  20. package/src/__tests__/browser-skill-baseline-tool-payload.test.ts +2 -2
  21. package/src/__tests__/canonical-guardian-store.test.ts +95 -0
  22. package/src/__tests__/channel-guardian.test.ts +0 -2
  23. package/src/__tests__/channel-readiness-routes.test.ts +15 -6
  24. package/src/__tests__/channel-readiness-service.test.ts +10 -9
  25. package/src/__tests__/checker.test.ts +13 -20
  26. package/src/__tests__/computer-use-skill-manifest-regression.test.ts +1 -1
  27. package/src/__tests__/computer-use-tools.test.ts +2 -19
  28. package/src/__tests__/config-schema.test.ts +1 -68
  29. package/src/__tests__/config-watcher.test.ts +0 -1
  30. package/src/__tests__/confirmation-request-guardian-bridge.test.ts +0 -1
  31. package/src/__tests__/context-image-dimensions.test.ts +332 -0
  32. package/src/__tests__/context-memory-e2e.test.ts +11 -100
  33. package/src/__tests__/context-token-estimator.test.ts +196 -13
  34. package/src/__tests__/conversation-attention-store.test.ts +0 -1
  35. package/src/__tests__/conversation-attention-telegram.test.ts +0 -1
  36. package/src/__tests__/conversation-routes-guardian-reply.test.ts +152 -0
  37. package/src/__tests__/conversation-routes-slash-commands.test.ts +2 -0
  38. package/src/__tests__/credential-metadata-store.test.ts +64 -73
  39. package/src/__tests__/credential-security-e2e.test.ts +1 -0
  40. package/src/__tests__/credential-security-invariants.test.ts +13 -7
  41. package/src/__tests__/credential-vault-unit.test.ts +284 -49
  42. package/src/__tests__/credential-vault.test.ts +150 -16
  43. package/src/__tests__/credentials-cli.test.ts +71 -0
  44. package/src/__tests__/cu-unified-flow.test.ts +532 -0
  45. package/src/__tests__/date-context.test.ts +93 -77
  46. package/src/__tests__/deterministic-verification-control-plane.test.ts +64 -0
  47. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
  48. package/src/__tests__/ephemeral-permissions.test.ts +3 -3
  49. package/src/__tests__/gateway-only-guard.test.ts +0 -1
  50. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -1
  51. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +0 -1
  52. package/src/__tests__/guardian-routing-invariants.test.ts +93 -1
  53. package/src/__tests__/guardian-verification-voice-binding.test.ts +0 -1
  54. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +0 -39
  55. package/src/__tests__/heartbeat-service.test.ts +0 -1
  56. package/src/__tests__/history-repair.test.ts +245 -0
  57. package/src/__tests__/host-cu-proxy.test.ts +791 -0
  58. package/src/__tests__/host-shell-tool.test.ts +27 -15
  59. package/src/__tests__/http-user-message-parity.test.ts +2 -0
  60. package/src/__tests__/ingress-url-consistency.test.ts +14 -21
  61. package/src/__tests__/integration-status.test.ts +32 -51
  62. package/src/__tests__/intent-routing.test.ts +0 -1
  63. package/src/__tests__/invite-redemption-service.test.ts +65 -1
  64. package/src/__tests__/invite-routes-http.test.ts +10 -9
  65. package/src/__tests__/keychain-broker-client.test.ts +14 -46
  66. package/src/__tests__/memory-context-benchmark.benchmark.test.ts +56 -18
  67. package/src/__tests__/memory-lifecycle-e2e.test.ts +244 -387
  68. package/src/__tests__/memory-recall-quality.test.ts +244 -407
  69. package/src/__tests__/memory-regressions.experimental.test.ts +126 -101
  70. package/src/__tests__/memory-regressions.test.ts +477 -2841
  71. package/src/__tests__/memory-retrieval.benchmark.test.ts +33 -150
  72. package/src/__tests__/memory-upsert-concurrency.test.ts +5 -244
  73. package/src/__tests__/mime-builder.test.ts +28 -0
  74. package/src/__tests__/native-web-search.test.ts +1 -0
  75. package/src/__tests__/notification-routing-intent.test.ts +0 -1
  76. package/src/__tests__/oauth-cli.test.ts +941 -15
  77. package/src/__tests__/oauth-provider-profiles.test.ts +9 -9
  78. package/src/__tests__/oauth-scope-policy.test.ts +4 -6
  79. package/src/__tests__/oauth-store.test.ts +870 -0
  80. package/src/__tests__/onboarding-starter-tasks.test.ts +0 -1
  81. package/src/__tests__/provider-error-scenarios.test.ts +0 -1
  82. package/src/__tests__/provider-streaming.benchmark.test.ts +0 -1
  83. package/src/__tests__/public-ingress-urls.test.ts +15 -21
  84. package/src/__tests__/qdrant-collection-migration.test.ts +53 -8
  85. package/src/__tests__/recording-handler.test.ts +3 -4
  86. package/src/__tests__/registry.test.ts +2 -3
  87. package/src/__tests__/relay-server.test.ts +46 -1
  88. package/src/__tests__/runtime-events-sse.test.ts +55 -7
  89. package/src/__tests__/schedule-store.test.ts +0 -1
  90. package/src/__tests__/schedule-tools.test.ts +32 -0
  91. package/src/__tests__/scheduler-recurrence.test.ts +0 -1
  92. package/src/__tests__/scoped-approval-grants.test.ts +0 -1
  93. package/src/__tests__/scoped-grant-security-matrix.test.ts +0 -1
  94. package/src/__tests__/script-proxy-certs.test.ts +1 -1
  95. package/src/__tests__/secret-ingress-handler.test.ts +0 -1
  96. package/src/__tests__/secret-onetime-send.test.ts +1 -0
  97. package/src/__tests__/secure-keys.test.ts +7 -2
  98. package/src/__tests__/send-endpoint-busy.test.ts +24 -6
  99. package/src/__tests__/sequence-store.test.ts +0 -1
  100. package/src/__tests__/session-abort-tool-results.test.ts +1 -14
  101. package/src/__tests__/session-agent-loop-overflow.test.ts +1583 -0
  102. package/src/__tests__/session-agent-loop.test.ts +19 -15
  103. package/src/__tests__/session-confirmation-signals.test.ts +1 -15
  104. package/src/__tests__/session-error.test.ts +124 -2
  105. package/src/__tests__/session-history-web-search.test.ts +918 -0
  106. package/src/__tests__/session-init.benchmark.test.ts +4 -5
  107. package/src/__tests__/session-pre-run-repair.test.ts +1 -14
  108. package/src/__tests__/session-provider-retry-repair.test.ts +25 -28
  109. package/src/__tests__/session-queue.test.ts +37 -27
  110. package/src/__tests__/session-runtime-assembly.test.ts +54 -0
  111. package/src/__tests__/session-slash-known.test.ts +1 -15
  112. package/src/__tests__/session-slash-queue.test.ts +1 -15
  113. package/src/__tests__/session-slash-unknown.test.ts +1 -15
  114. package/src/__tests__/session-workspace-cache-state.test.ts +3 -33
  115. package/src/__tests__/session-workspace-injection.test.ts +3 -37
  116. package/src/__tests__/session-workspace-tool-tracking.test.ts +3 -37
  117. package/src/__tests__/skill-include-graph.test.ts +66 -0
  118. package/src/__tests__/skill-load-feature-flag.test.ts +0 -1
  119. package/src/__tests__/skill-load-tool.test.ts +149 -1
  120. package/src/__tests__/skill-projection-feature-flag.test.ts +0 -1
  121. package/src/__tests__/skills-install-extract.test.ts +93 -0
  122. package/src/__tests__/skills-uninstall.test.ts +1 -1
  123. package/src/__tests__/skills.test.ts +3 -3
  124. package/src/__tests__/skillssh-registry.test.ts +451 -0
  125. package/src/__tests__/slack-channel-config.test.ts +67 -3
  126. package/src/__tests__/slack-share-routes.test.ts +17 -19
  127. package/src/__tests__/system-prompt.test.ts +0 -1
  128. package/src/__tests__/telegram-invite-adapter.test.ts +18 -22
  129. package/src/__tests__/terminal-tools.test.ts +4 -3
  130. package/src/__tests__/test-support/computer-use-skill-harness.ts +3 -2
  131. package/src/__tests__/tool-approval-handler.test.ts +0 -1
  132. package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +0 -1
  133. package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
  134. package/src/__tests__/tool-executor-shell-integration.test.ts +0 -1
  135. package/src/__tests__/tool-executor.test.ts +0 -1
  136. package/src/__tests__/tool-grant-request-escalation.test.ts +0 -1
  137. package/src/__tests__/trust-store-pattern-matches.test.ts +29 -0
  138. package/src/__tests__/trust-store.test.ts +7 -13
  139. package/src/__tests__/trusted-contact-approval-notifier.test.ts +0 -1
  140. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +0 -1
  141. package/src/__tests__/twilio-routes.test.ts +0 -16
  142. package/src/__tests__/verification-control-plane-policy.test.ts +0 -1
  143. package/src/__tests__/voice-invite-redemption.test.ts +32 -1
  144. package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -1
  145. package/src/agent/ax-tree-compaction.test.ts +286 -0
  146. package/src/agent/loop.ts +104 -131
  147. package/src/approvals/AGENTS.md +1 -1
  148. package/src/approvals/guardian-request-resolvers.ts +14 -2
  149. package/src/bundler/compiler-tools.ts +66 -2
  150. package/src/calls/call-domain.ts +133 -6
  151. package/src/calls/call-store.ts +6 -0
  152. package/src/calls/relay-server.ts +52 -18
  153. package/src/calls/relay-setup-router.ts +17 -1
  154. package/src/calls/twilio-config.ts +3 -8
  155. package/src/calls/twilio-routes.ts +1 -2
  156. package/src/calls/types.ts +3 -1
  157. package/src/calls/voice-ingress-preflight.ts +1 -1
  158. package/src/cli/commands/browser-relay.ts +18 -12
  159. package/src/cli/commands/completions.ts +0 -3
  160. package/src/cli/commands/credentials.ts +101 -15
  161. package/src/cli/commands/doctor.ts +4 -3
  162. package/src/cli/commands/mcp.ts +46 -59
  163. package/src/cli/commands/memory.ts +16 -165
  164. package/src/cli/commands/oauth/apps.ts +284 -0
  165. package/src/cli/commands/oauth/connections.ts +633 -0
  166. package/src/cli/commands/oauth/index.ts +52 -0
  167. package/src/cli/commands/oauth/providers.ts +256 -0
  168. package/src/cli/commands/sessions.ts +5 -2
  169. package/src/cli/commands/skills.ts +177 -339
  170. package/src/cli/http-client.ts +0 -20
  171. package/src/cli/main-screen.tsx +2 -2
  172. package/src/cli/program.ts +6 -11
  173. package/src/cli/reference.ts +1 -3
  174. package/src/cli.ts +4 -10
  175. package/src/config/assistant-feature-flags.ts +0 -3
  176. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  177. package/src/config/bundled-skills/computer-use/SKILL.md +3 -6
  178. package/src/config/bundled-skills/computer-use/TOOLS.json +23 -5
  179. package/src/config/bundled-skills/computer-use/tools/{computer-use-request-control.ts → computer-use-observe.ts} +1 -5
  180. package/src/config/bundled-skills/google-calendar/calendar-client.ts +21 -16
  181. package/src/config/bundled-skills/messaging/tools/shared.ts +1 -4
  182. package/src/config/bundled-skills/settings/SKILL.md +1 -1
  183. package/src/config/bundled-skills/settings/TOOLS.json +2 -8
  184. package/src/config/bundled-skills/settings/tools/voice-config-update.ts +5 -33
  185. package/src/config/bundled-tool-registry.ts +2 -5
  186. package/src/config/env-registry.ts +14 -83
  187. package/src/config/env.ts +11 -50
  188. package/src/config/feature-flag-registry.json +16 -16
  189. package/src/config/loader.ts +0 -6
  190. package/src/config/schema.ts +4 -13
  191. package/src/config/schemas/memory-lifecycle.ts +0 -9
  192. package/src/config/schemas/memory-processing.ts +0 -180
  193. package/src/config/schemas/memory-retrieval.ts +32 -104
  194. package/src/config/schemas/memory.ts +0 -10
  195. package/src/config/skills.ts +21 -2
  196. package/src/config/types.ts +0 -4
  197. package/src/context/image-dimensions.ts +229 -0
  198. package/src/context/token-estimator.ts +75 -12
  199. package/src/context/window-manager.ts +53 -11
  200. package/src/daemon/assistant-attachments.ts +1 -13
  201. package/src/daemon/config-watcher.ts +61 -3
  202. package/src/daemon/daemon-control.ts +1 -1
  203. package/src/daemon/date-context.ts +114 -31
  204. package/src/daemon/handlers/config-ingress.ts +8 -33
  205. package/src/daemon/handlers/config-slack-channel.ts +49 -46
  206. package/src/daemon/handlers/config-telegram.ts +32 -16
  207. package/src/daemon/handlers/sessions.ts +27 -36
  208. package/src/daemon/handlers/shared.ts +0 -130
  209. package/src/daemon/handlers/skills.ts +20 -1
  210. package/src/daemon/history-repair.ts +72 -8
  211. package/src/daemon/host-cu-proxy.ts +430 -0
  212. package/src/daemon/lifecycle.ts +67 -71
  213. package/src/daemon/mcp-reload-service.ts +2 -2
  214. package/src/daemon/message-protocol.ts +3 -0
  215. package/src/daemon/message-types/computer-use.ts +1 -129
  216. package/src/daemon/message-types/host-cu.ts +19 -0
  217. package/src/daemon/message-types/memory.ts +4 -16
  218. package/src/daemon/message-types/messages.ts +4 -0
  219. package/src/daemon/message-types/sessions.ts +4 -0
  220. package/src/daemon/server.ts +25 -21
  221. package/src/daemon/session-agent-loop-handlers.ts +40 -0
  222. package/src/daemon/session-agent-loop.ts +334 -48
  223. package/src/daemon/session-attachments.ts +1 -2
  224. package/src/daemon/session-error.ts +89 -6
  225. package/src/daemon/session-history.ts +17 -7
  226. package/src/daemon/session-media-retry.ts +6 -2
  227. package/src/daemon/session-memory.ts +69 -149
  228. package/src/daemon/session-process.ts +10 -1
  229. package/src/daemon/session-runtime-assembly.ts +49 -19
  230. package/src/daemon/session-slash.ts +1 -1
  231. package/src/daemon/session-surfaces.ts +43 -28
  232. package/src/daemon/session-tool-setup.ts +9 -10
  233. package/src/daemon/session.ts +150 -17
  234. package/src/daemon/tool-side-effects.ts +2 -8
  235. package/src/daemon/watch-handler.ts +2 -2
  236. package/src/events/tool-metrics-listener.ts +2 -2
  237. package/src/hooks/manager.ts +1 -4
  238. package/src/inbound/public-ingress-urls.ts +7 -7
  239. package/src/instrument.ts +61 -1
  240. package/src/logfire.ts +16 -5
  241. package/src/memory/admin.ts +2 -191
  242. package/src/memory/canonical-guardian-store.ts +38 -2
  243. package/src/memory/conversation-crud.ts +0 -33
  244. package/src/memory/conversation-key-store.ts +21 -0
  245. package/src/memory/conversation-queries.ts +22 -3
  246. package/src/memory/db-init.ts +32 -0
  247. package/src/memory/embedding-backend.ts +84 -8
  248. package/src/memory/embedding-types.ts +9 -1
  249. package/src/memory/indexer.ts +7 -46
  250. package/src/memory/items-extractor.ts +274 -76
  251. package/src/memory/job-handlers/backfill.ts +2 -127
  252. package/src/memory/job-handlers/cleanup.ts +2 -16
  253. package/src/memory/job-handlers/extraction.ts +2 -138
  254. package/src/memory/job-handlers/index-maintenance.ts +1 -6
  255. package/src/memory/job-handlers/summarization.ts +3 -148
  256. package/src/memory/job-utils.ts +21 -59
  257. package/src/memory/jobs-store.ts +1 -159
  258. package/src/memory/jobs-worker.ts +9 -52
  259. package/src/memory/migrations/104-core-indexes.ts +3 -3
  260. package/src/memory/migrations/149-oauth-tables.ts +62 -0
  261. package/src/memory/migrations/150-oauth-apps-client-secret-path.ts +98 -0
  262. package/src/memory/migrations/151-oauth-providers-ping-url.ts +11 -0
  263. package/src/memory/migrations/152-memory-item-supersession.ts +44 -0
  264. package/src/memory/migrations/153-drop-entity-tables.ts +15 -0
  265. package/src/memory/migrations/154-drop-fts.ts +20 -0
  266. package/src/memory/migrations/155-drop-conflicts.ts +7 -0
  267. package/src/memory/migrations/156-call-session-invite-metadata.ts +24 -0
  268. package/src/memory/migrations/index.ts +8 -0
  269. package/src/memory/qdrant-client.ts +148 -51
  270. package/src/memory/raw-query.ts +1 -1
  271. package/src/memory/retriever.test.ts +294 -273
  272. package/src/memory/retriever.ts +421 -645
  273. package/src/memory/schema/calls.ts +2 -0
  274. package/src/memory/schema/index.ts +1 -0
  275. package/src/memory/schema/memory-core.ts +3 -48
  276. package/src/memory/schema/oauth.ts +67 -0
  277. package/src/memory/search/formatting.ts +263 -176
  278. package/src/memory/search/lexical.ts +1 -254
  279. package/src/memory/search/ranking.ts +0 -455
  280. package/src/memory/search/semantic.ts +100 -14
  281. package/src/memory/search/staleness.ts +47 -0
  282. package/src/memory/search/tier-classifier.ts +21 -0
  283. package/src/memory/search/types.ts +15 -77
  284. package/src/memory/task-memory-cleanup.ts +4 -6
  285. package/src/messaging/provider.ts +4 -4
  286. package/src/messaging/providers/gmail/client.ts +82 -2
  287. package/src/messaging/providers/gmail/mime-builder.ts +17 -7
  288. package/src/messaging/providers/gmail/people-client.ts +10 -10
  289. package/src/messaging/providers/telegram-bot/adapter.ts +17 -17
  290. package/src/messaging/providers/whatsapp/adapter.ts +11 -8
  291. package/src/messaging/registry.ts +2 -32
  292. package/src/notifications/copy-composer.ts +0 -5
  293. package/src/notifications/signal.ts +4 -5
  294. package/src/oauth/byo-connection.test.ts +133 -25
  295. package/src/oauth/byo-connection.ts +22 -6
  296. package/src/oauth/connect-orchestrator.ts +113 -57
  297. package/src/oauth/connect-types.ts +17 -23
  298. package/src/oauth/connection-resolver.ts +35 -11
  299. package/src/oauth/connection.ts +1 -1
  300. package/src/oauth/manual-token-connection.ts +104 -0
  301. package/src/oauth/oauth-store.ts +582 -0
  302. package/src/oauth/platform-connection.test.ts +29 -0
  303. package/src/oauth/platform-connection.ts +6 -5
  304. package/src/oauth/provider-behaviors.ts +124 -0
  305. package/src/oauth/scope-policy.ts +9 -2
  306. package/src/oauth/seed-providers.ts +167 -0
  307. package/src/oauth/token-persistence.ts +81 -77
  308. package/src/permissions/checker.ts +3 -3
  309. package/src/permissions/defaults.ts +1 -1
  310. package/src/permissions/prompter.ts +10 -1
  311. package/src/permissions/trust-store.ts +36 -1
  312. package/src/playbooks/playbook-compiler.ts +1 -1
  313. package/src/prompts/__tests__/build-cli-reference-section.test.ts +3 -1
  314. package/src/prompts/system-prompt.ts +46 -42
  315. package/src/providers/anthropic/client.ts +59 -20
  316. package/src/providers/retry.ts +1 -27
  317. package/src/providers/types.ts +7 -1
  318. package/src/runtime/AGENTS.md +9 -0
  319. package/src/runtime/auth/route-policy.ts +6 -6
  320. package/src/runtime/channel-reply-delivery.ts +0 -40
  321. package/src/runtime/gateway-client.ts +0 -7
  322. package/src/runtime/guardian-reply-router.ts +24 -22
  323. package/src/runtime/http-server.ts +10 -8
  324. package/src/runtime/http-types.ts +2 -2
  325. package/src/runtime/invite-redemption-service.ts +19 -1
  326. package/src/runtime/invite-service.ts +25 -0
  327. package/src/runtime/middleware/twilio-validation.ts +1 -11
  328. package/src/runtime/pending-interactions.ts +14 -12
  329. package/src/runtime/routes/brain-graph-routes.ts +10 -90
  330. package/src/runtime/routes/channel-delivery-routes.ts +0 -1
  331. package/src/runtime/routes/conversation-routes.ts +81 -19
  332. package/src/runtime/routes/events-routes.ts +21 -11
  333. package/src/runtime/routes/host-cu-routes.ts +97 -0
  334. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +21 -12
  335. package/src/runtime/routes/inbound-stages/background-dispatch.ts +12 -111
  336. package/src/runtime/routes/integrations/slack/share.ts +6 -7
  337. package/src/runtime/routes/log-export-routes.ts +126 -8
  338. package/src/runtime/routes/memory-item-routes.test.ts +754 -0
  339. package/src/runtime/routes/memory-item-routes.ts +503 -0
  340. package/src/runtime/routes/session-management-routes.ts +3 -3
  341. package/src/runtime/routes/settings-routes.ts +55 -48
  342. package/src/runtime/routes/surface-action-routes.ts +1 -1
  343. package/src/runtime/routes/trust-rules-routes.ts +14 -0
  344. package/src/runtime/routes/watch-routes.ts +128 -0
  345. package/src/runtime/routes/workspace-routes.ts +2 -1
  346. package/src/schedule/integration-status.ts +10 -9
  347. package/src/security/credential-key.ts +0 -156
  348. package/src/security/keychain-broker-client.ts +22 -10
  349. package/src/security/oauth2.ts +1 -1
  350. package/src/security/secure-keys.ts +25 -3
  351. package/src/security/token-manager.ts +137 -64
  352. package/src/skills/catalog-install.ts +414 -0
  353. package/src/skills/include-graph.ts +32 -0
  354. package/src/skills/skillssh-registry.ts +503 -0
  355. package/src/telegram/bot-username.ts +2 -3
  356. package/src/tools/assets/search.ts +5 -1
  357. package/src/tools/browser/network-recorder.ts +1 -1
  358. package/src/tools/browser/network-recording-types.ts +1 -1
  359. package/src/tools/computer-use/definitions.ts +36 -11
  360. package/src/tools/computer-use/registry.ts +5 -6
  361. package/src/tools/credentials/broker.ts +1 -2
  362. package/src/tools/credentials/metadata-store.ts +17 -121
  363. package/src/tools/credentials/vault.ts +92 -167
  364. package/src/tools/memory/definitions.ts +4 -13
  365. package/src/tools/memory/handlers.test.ts +83 -103
  366. package/src/tools/memory/handlers.ts +50 -85
  367. package/src/tools/registry.ts +2 -7
  368. package/src/tools/schedule/create.ts +8 -1
  369. package/src/tools/schedule/update.ts +8 -1
  370. package/src/tools/skills/load.ts +85 -3
  371. package/src/tools/watch/watch-state.ts +0 -12
  372. package/src/util/logger.ts +7 -41
  373. package/src/util/platform.ts +9 -28
  374. package/src/watcher/providers/google-calendar.ts +2 -1
  375. package/src/__tests__/clarification-resolver.test.ts +0 -193
  376. package/src/__tests__/computer-use-session-compaction.test.ts +0 -143
  377. package/src/__tests__/computer-use-session-lifecycle.test.ts +0 -322
  378. package/src/__tests__/computer-use-session-working-dir.test.ts +0 -166
  379. package/src/__tests__/computer-use-skill-baseline.test.ts +0 -78
  380. package/src/__tests__/computer-use-skill-endstate.test.ts +0 -105
  381. package/src/__tests__/computer-use-skill-lifecycle-cleanup.test.ts +0 -249
  382. package/src/__tests__/conflict-intent-tokenization.test.ts +0 -160
  383. package/src/__tests__/conflict-policy.test.ts +0 -269
  384. package/src/__tests__/conflict-store.test.ts +0 -372
  385. package/src/__tests__/contradiction-checker.test.ts +0 -361
  386. package/src/__tests__/entity-extractor.test.ts +0 -211
  387. package/src/__tests__/entity-search.test.ts +0 -1117
  388. package/src/__tests__/profile-compiler.test.ts +0 -392
  389. package/src/__tests__/ride-shotgun-handler.test.ts +0 -452
  390. package/src/__tests__/session-conflict-gate.test.ts +0 -1228
  391. package/src/__tests__/session-profile-injection.test.ts +0 -557
  392. package/src/cli/commands/dev.ts +0 -129
  393. package/src/cli/commands/map.ts +0 -391
  394. package/src/cli/commands/oauth.ts +0 -77
  395. package/src/config/bundled-skills/knowledge-graph/SKILL.md +0 -25
  396. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +0 -66
  397. package/src/config/bundled-skills/knowledge-graph/tools/graph-query.ts +0 -211
  398. package/src/daemon/computer-use-session.ts +0 -1026
  399. package/src/daemon/ride-shotgun-handler.ts +0 -569
  400. package/src/daemon/session-conflict-gate.ts +0 -167
  401. package/src/daemon/session-dynamic-profile.ts +0 -77
  402. package/src/memory/clarification-resolver.ts +0 -417
  403. package/src/memory/conflict-intent.ts +0 -205
  404. package/src/memory/conflict-policy.ts +0 -127
  405. package/src/memory/conflict-store.ts +0 -410
  406. package/src/memory/contradiction-checker.ts +0 -508
  407. package/src/memory/entity-extractor.ts +0 -535
  408. package/src/memory/format-recall.ts +0 -47
  409. package/src/memory/fts-reconciler.ts +0 -165
  410. package/src/memory/job-handlers/conflict.ts +0 -200
  411. package/src/memory/profile-compiler.ts +0 -195
  412. package/src/memory/recall-cache.ts +0 -117
  413. package/src/memory/search/entity.ts +0 -535
  414. package/src/memory/search/query-expansion.test.ts +0 -70
  415. package/src/memory/search/query-expansion.ts +0 -118
  416. package/src/oauth/provider-base-urls.ts +0 -21
  417. package/src/oauth/provider-profiles.ts +0 -192
  418. package/src/prompts/computer-use-prompt.ts +0 -98
  419. package/src/runtime/routes/computer-use-routes.ts +0 -641
  420. package/src/runtime/routes/mcp-routes.ts +0 -20
  421. package/src/runtime/telegram-streaming-delivery.test.ts +0 -729
  422. package/src/runtime/telegram-streaming-delivery.ts +0 -393
  423. package/src/tools/computer-use/request-computer-control.ts +0 -56
@@ -2,6 +2,8 @@ import { beforeEach, describe, expect, mock, test } from "bun:test";
2
2
 
3
3
  import { Command } from "commander";
4
4
 
5
+ import { credentialKey } from "../security/credential-key.js";
6
+
5
7
  // ---------------------------------------------------------------------------
6
8
  // Mock state
7
9
  // ---------------------------------------------------------------------------
@@ -11,6 +13,64 @@ let mockWithValidToken: <T>(
11
13
  cb: (token: string) => Promise<T>,
12
14
  ) => Promise<T>;
13
15
 
16
+ // Disconnect mock state
17
+ let mockListProviders: () => Array<Record<string, unknown>> = () => [];
18
+ let secureKeyStore = new Map<string, string>();
19
+ let metadataStore: Array<{
20
+ credentialId: string;
21
+ service: string;
22
+ field: string;
23
+ allowedTools: string[];
24
+ allowedDomains: string[];
25
+ createdAt: number;
26
+ updatedAt: number;
27
+ }> = [];
28
+ let disconnectOAuthProviderCalls: string[] = [];
29
+ let disconnectOAuthProviderResult: "disconnected" | "not-found" | "error" =
30
+ "not-found";
31
+ let idCounter = 0;
32
+
33
+ // App upsert mock state
34
+ let mockUpsertAppCalls: Array<{
35
+ provider: string;
36
+ clientId: string;
37
+ clientSecretOpts?: {
38
+ clientSecretValue?: string;
39
+ clientSecretCredentialPath?: string;
40
+ };
41
+ }> = [];
42
+ let mockUpsertAppResult: Record<string, unknown> = {
43
+ id: "app-upsert-1",
44
+ providerKey: "integration:test",
45
+ clientId: "test-client-id",
46
+ createdAt: 1700000000000,
47
+ updatedAt: 1700000000000,
48
+ };
49
+
50
+ // Connect mock state
51
+ let mockOrchestrateOAuthConnect: (
52
+ opts: Record<string, unknown>,
53
+ ) => Promise<Record<string, unknown>>;
54
+ let mockGetAppByProviderAndClientId: (
55
+ providerKey: string,
56
+ clientId: string,
57
+ ) => Record<string, unknown> | undefined = () => undefined;
58
+ let mockGetMostRecentAppByProvider: (
59
+ providerKey: string,
60
+ ) => Record<string, unknown> | undefined = () => undefined;
61
+ let mockGetProvider: (
62
+ providerKey: string,
63
+ ) => Record<string, unknown> | undefined = () => undefined;
64
+ let mockGetProviderBehavior: (
65
+ providerKey: string,
66
+ ) => Record<string, unknown> | undefined = () => undefined;
67
+ let mockGetSecureKey: (account: string) => string | undefined = () => undefined;
68
+
69
+ function nextUUID(): string {
70
+ idCounter += 1;
71
+ return `00000000-0000-0000-0000-${String(idCounter).padStart(12, "0")}`;
72
+ }
73
+
14
74
  // ---------------------------------------------------------------------------
15
75
  // Mock token-manager
16
76
  // ---------------------------------------------------------------------------
@@ -32,19 +92,108 @@ mock.module("../security/token-manager.js", () => ({
32
92
  },
33
93
  }));
34
94
 
95
+ // ---------------------------------------------------------------------------
96
+ // Mock oauth-store (stateful for disconnect tests)
97
+ // ---------------------------------------------------------------------------
98
+
99
+ mock.module("../oauth/oauth-store.js", () => ({
100
+ disconnectOAuthProvider: async (
101
+ providerKey: string,
102
+ ): Promise<"disconnected" | "not-found" | "error"> => {
103
+ disconnectOAuthProviderCalls.push(providerKey);
104
+ return disconnectOAuthProviderResult;
105
+ },
106
+ getConnection: () => undefined,
107
+ getConnectionByProvider: () => undefined,
108
+ listConnections: () => [],
109
+ deleteConnection: () => false,
110
+ // Stubs required by apps.ts and providers.ts (transitively loaded via oauth/index.ts)
111
+ upsertApp: async (
112
+ provider: string,
113
+ clientId: string,
114
+ clientSecretOpts?: {
115
+ clientSecretValue?: string;
116
+ clientSecretCredentialPath?: string;
117
+ },
118
+ ) => {
119
+ mockUpsertAppCalls.push({ provider, clientId, clientSecretOpts });
120
+ return mockUpsertAppResult;
121
+ },
122
+ getApp: () => undefined,
123
+ getAppByProviderAndClientId: (providerKey: string, clientId: string) =>
124
+ mockGetAppByProviderAndClientId(providerKey, clientId),
125
+ getMostRecentAppByProvider: (providerKey: string) =>
126
+ mockGetMostRecentAppByProvider(providerKey),
127
+ listApps: () => [],
128
+ deleteApp: async () => false,
129
+ getProvider: (providerKey: string) => mockGetProvider(providerKey),
130
+ listProviders: () => mockListProviders(),
131
+ registerProvider: () => ({}),
132
+ seedProviders: () => {},
133
+ createConnection: () => ({}),
134
+ isProviderConnected: () => false,
135
+ updateConnection: () => ({}),
136
+ }));
137
+
35
138
  // Stub out transitive dependencies that token-manager would normally pull in
36
139
  mock.module("../security/secure-keys.js", () => ({
37
- getSecureKey: () => undefined,
140
+ getSecureKey: (account: string) => mockGetSecureKey(account),
38
141
  setSecureKey: () => true,
39
142
  getSecureKeyAsync: async () => undefined,
40
143
  setSecureKeyAsync: async () => true,
41
- deleteSecureKey: () => "not-found",
144
+ deleteSecureKey: (account: string) => {
145
+ if (secureKeyStore.has(account)) {
146
+ secureKeyStore.delete(account);
147
+ return "deleted" as const;
148
+ }
149
+ return "not-found" as const;
150
+ },
151
+ deleteSecureKeyAsync: async (account: string) => {
152
+ if (secureKeyStore.has(account)) {
153
+ secureKeyStore.delete(account);
154
+ return "deleted" as const;
155
+ }
156
+ return "not-found" as const;
157
+ },
158
+ listSecureKeys: () => [...secureKeyStore.keys()],
159
+ getBackendType: () => "encrypted",
160
+ isDowngradedFromKeychain: () => false,
161
+ _resetBackend: () => {},
162
+ _setBackend: () => {},
42
163
  }));
43
164
 
44
165
  mock.module("../tools/credentials/metadata-store.js", () => ({
166
+ assertMetadataWritable: () => {},
45
167
  getCredentialMetadata: () => undefined,
46
168
  upsertCredentialMetadata: () => ({}),
47
169
  listCredentialMetadata: () => [],
170
+ deleteCredentialMetadata: (service: string, field: string): boolean => {
171
+ const idx = metadataStore.findIndex(
172
+ (c) => c.service === service && c.field === field,
173
+ );
174
+ if (idx === -1) return false;
175
+ metadataStore.splice(idx, 1);
176
+ return true;
177
+ },
178
+ }));
179
+
180
+ // ---------------------------------------------------------------------------
181
+ // Mock connect-orchestrator
182
+ // ---------------------------------------------------------------------------
183
+
184
+ mock.module("../oauth/connect-orchestrator.js", () => ({
185
+ orchestrateOAuthConnect: (opts: Record<string, unknown>) =>
186
+ mockOrchestrateOAuthConnect(opts),
187
+ }));
188
+
189
+ // ---------------------------------------------------------------------------
190
+ // Mock provider-behaviors
191
+ // ---------------------------------------------------------------------------
192
+
193
+ mock.module("../oauth/provider-behaviors.js", () => ({
194
+ resolveService: (service: string) => service,
195
+ getProviderBehavior: (providerKey: string) =>
196
+ mockGetProviderBehavior(providerKey),
48
197
  }));
49
198
 
50
199
  mock.module("../util/logger.js", () => ({
@@ -66,7 +215,7 @@ mock.module("../util/logger.js", () => ({
66
215
  // Import the module under test (after mocks are registered)
67
216
  // ---------------------------------------------------------------------------
68
217
 
69
- const { registerOAuthCommand } = await import("../cli/commands/oauth.js");
218
+ const { registerOAuthCommand } = await import("../cli/commands/oauth/index.js");
70
219
 
71
220
  // ---------------------------------------------------------------------------
72
221
  // Test helper
@@ -117,43 +266,61 @@ async function runCli(
117
266
  // Tests
118
267
  // ---------------------------------------------------------------------------
119
268
 
120
- describe("assistant oauth token", () => {
269
+ describe("assistant oauth connections token <provider-key>", () => {
121
270
  beforeEach(() => {
122
271
  mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
272
+ secureKeyStore = new Map();
273
+ metadataStore = [];
274
+ disconnectOAuthProviderCalls = [];
275
+ disconnectOAuthProviderResult = "not-found";
276
+ idCounter = 0;
123
277
  });
124
278
 
125
279
  test("prints bare token in human mode", async () => {
126
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
280
+ const { exitCode, stdout } = await runCli([
281
+ "connections",
282
+ "token",
283
+ "integration:twitter",
284
+ ]);
127
285
  expect(exitCode).toBe(0);
128
286
  expect(stdout).toBe("mock-access-token-xyz\n");
129
287
  });
130
288
 
131
289
  test("prints JSON in --json mode", async () => {
132
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
290
+ const { exitCode, stdout } = await runCli([
291
+ "connections",
292
+ "token",
293
+ "integration:twitter",
294
+ "--json",
295
+ ]);
133
296
  expect(exitCode).toBe(0);
134
297
  const parsed = JSON.parse(stdout);
135
298
  expect(parsed).toEqual({ ok: true, token: "mock-access-token-xyz" });
136
299
  });
137
300
 
138
- test("qualifies service name with integration: prefix", async () => {
301
+ test("passes provider key directly to withValidToken", async () => {
139
302
  let capturedService: string | undefined;
140
303
  mockWithValidToken = async (service, cb) => {
141
304
  capturedService = service;
142
305
  return cb("tok");
143
306
  };
144
307
 
145
- await runCli(["token", "twitter"]);
308
+ await runCli(["connections", "token", "integration:twitter"]);
146
309
  expect(capturedService).toBe("integration:twitter");
147
310
  });
148
311
 
149
- test("works with other service names", async () => {
312
+ test("works with other provider keys", async () => {
150
313
  let capturedService: string | undefined;
151
314
  mockWithValidToken = async (service, cb) => {
152
315
  capturedService = service;
153
316
  return cb("gmail-token");
154
317
  };
155
318
 
156
- const { exitCode, stdout } = await runCli(["token", "gmail"]);
319
+ const { exitCode, stdout } = await runCli([
320
+ "connections",
321
+ "token",
322
+ "integration:gmail",
323
+ ]);
157
324
  expect(exitCode).toBe(0);
158
325
  expect(stdout).toBe("gmail-token\n");
159
326
  expect(capturedService).toBe("integration:gmail");
@@ -166,7 +333,12 @@ describe("assistant oauth token", () => {
166
333
  );
167
334
  };
168
335
 
169
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
336
+ const { exitCode, stdout } = await runCli([
337
+ "connections",
338
+ "token",
339
+ "integration:twitter",
340
+ "--json",
341
+ ]);
170
342
  expect(exitCode).toBe(1);
171
343
  const parsed = JSON.parse(stdout);
172
344
  expect(parsed.ok).toBe(false);
@@ -180,7 +352,12 @@ describe("assistant oauth token", () => {
180
352
  );
181
353
  };
182
354
 
183
- const { exitCode, stdout } = await runCli(["token", "twitter", "--json"]);
355
+ const { exitCode, stdout } = await runCli([
356
+ "connections",
357
+ "token",
358
+ "integration:twitter",
359
+ "--json",
360
+ ]);
184
361
  expect(exitCode).toBe(1);
185
362
  const parsed = JSON.parse(stdout);
186
363
  expect(parsed.ok).toBe(false);
@@ -191,13 +368,762 @@ describe("assistant oauth token", () => {
191
368
  // Simulate withValidToken refreshing and returning a new token
192
369
  mockWithValidToken = async (_service, cb) => cb("refreshed-new-token");
193
370
 
194
- const { exitCode, stdout } = await runCli(["token", "twitter"]);
371
+ const { exitCode, stdout } = await runCli([
372
+ "connections",
373
+ "token",
374
+ "integration:twitter",
375
+ ]);
195
376
  expect(exitCode).toBe(0);
196
377
  expect(stdout).toBe("refreshed-new-token\n");
197
378
  });
198
379
 
199
- test("missing service argument exits non-zero", async () => {
200
- const { exitCode } = await runCli(["token"]);
380
+ test("missing provider-key argument exits non-zero", async () => {
381
+ const { exitCode } = await runCli(["connections", "token"]);
201
382
  expect(exitCode).not.toBe(0);
202
383
  });
203
384
  });
385
+
386
+ // ---------------------------------------------------------------------------
387
+ // disconnect
388
+ // ---------------------------------------------------------------------------
389
+
390
+ describe("assistant oauth connections disconnect <provider-key>", () => {
391
+ beforeEach(() => {
392
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
393
+ secureKeyStore = new Map();
394
+ metadataStore = [];
395
+ disconnectOAuthProviderCalls = [];
396
+ disconnectOAuthProviderResult = "not-found";
397
+ idCounter = 0;
398
+ });
399
+
400
+ test("succeeds when an OAuth connection exists", async () => {
401
+ disconnectOAuthProviderResult = "disconnected";
402
+
403
+ const result = await runCli([
404
+ "connections",
405
+ "disconnect",
406
+ "integration:gmail",
407
+ "--json",
408
+ ]);
409
+ expect(result.exitCode).toBe(0);
410
+ const parsed = JSON.parse(result.stdout);
411
+ expect(parsed.ok).toBe(true);
412
+ expect(parsed.service).toBe("integration:gmail");
413
+
414
+ // disconnectOAuthProvider should have been called with the full provider key
415
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
416
+ });
417
+
418
+ test("reports not-found when nothing exists", async () => {
419
+ const result = await runCli([
420
+ "connections",
421
+ "disconnect",
422
+ "integration:gmail",
423
+ "--json",
424
+ ]);
425
+ expect(result.exitCode).toBe(1);
426
+ const parsed = JSON.parse(result.stdout);
427
+ expect(parsed.ok).toBe(false);
428
+ expect(parsed.error).toContain("No OAuth connection or credentials");
429
+ expect(parsed.error).toContain("integration:gmail");
430
+ });
431
+
432
+ test("cleans up legacy credential keys if present", async () => {
433
+ // Seed legacy credential keys (no OAuth connection)
434
+ const legacyFields = [
435
+ "access_token",
436
+ "refresh_token",
437
+ "client_id",
438
+ "client_secret",
439
+ ];
440
+ for (const field of legacyFields) {
441
+ secureKeyStore.set(
442
+ credentialKey("integration:gmail", field),
443
+ `legacy_${field}_value`,
444
+ );
445
+ metadataStore.push({
446
+ credentialId: nextUUID(),
447
+ service: "integration:gmail",
448
+ field,
449
+ allowedTools: [],
450
+ allowedDomains: [],
451
+ createdAt: Date.now(),
452
+ updatedAt: Date.now(),
453
+ });
454
+ }
455
+
456
+ const result = await runCli([
457
+ "connections",
458
+ "disconnect",
459
+ "integration:gmail",
460
+ "--json",
461
+ ]);
462
+ expect(result.exitCode).toBe(0);
463
+ const parsed = JSON.parse(result.stdout);
464
+ expect(parsed.ok).toBe(true);
465
+ expect(parsed.service).toBe("integration:gmail");
466
+
467
+ // All legacy keys should be removed
468
+ for (const field of legacyFields) {
469
+ expect(
470
+ secureKeyStore.has(credentialKey("integration:gmail", field)),
471
+ ).toBe(false);
472
+ expect(
473
+ metadataStore.find(
474
+ (m) => m.service === "integration:gmail" && m.field === field,
475
+ ),
476
+ ).toBeUndefined();
477
+ }
478
+ });
479
+
480
+ test("cleans up both OAuth connection and legacy keys when both exist", async () => {
481
+ // Seed OAuth connection
482
+ disconnectOAuthProviderResult = "disconnected";
483
+
484
+ // Seed a legacy credential key
485
+ secureKeyStore.set(
486
+ credentialKey("integration:gmail", "access_token"),
487
+ "legacy_token",
488
+ );
489
+ metadataStore.push({
490
+ credentialId: nextUUID(),
491
+ service: "integration:gmail",
492
+ field: "access_token",
493
+ allowedTools: [],
494
+ allowedDomains: [],
495
+ createdAt: Date.now(),
496
+ updatedAt: Date.now(),
497
+ });
498
+
499
+ const result = await runCli([
500
+ "connections",
501
+ "disconnect",
502
+ "integration:gmail",
503
+ "--json",
504
+ ]);
505
+ expect(result.exitCode).toBe(0);
506
+ const parsed = JSON.parse(result.stdout);
507
+ expect(parsed.ok).toBe(true);
508
+
509
+ // Both should be cleaned up
510
+ expect(disconnectOAuthProviderCalls).toEqual(["integration:gmail"]);
511
+ expect(
512
+ secureKeyStore.has(credentialKey("integration:gmail", "access_token")),
513
+ ).toBe(false);
514
+ });
515
+ });
516
+
517
+ // ---------------------------------------------------------------------------
518
+ // providers list
519
+ // ---------------------------------------------------------------------------
520
+
521
+ describe("assistant oauth providers list", () => {
522
+ const fakeProviders = [
523
+ {
524
+ providerKey: "integration:gmail",
525
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
526
+ tokenUrl: "https://oauth2.googleapis.com/token",
527
+ defaultScopes: "[]",
528
+ scopePolicy: "{}",
529
+ extraParams: null,
530
+ createdAt: "2025-01-01T00:00:00.000Z",
531
+ updatedAt: "2025-01-01T00:00:00.000Z",
532
+ },
533
+ {
534
+ providerKey: "integration:google-calendar",
535
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
536
+ tokenUrl: "https://oauth2.googleapis.com/token",
537
+ defaultScopes: "[]",
538
+ scopePolicy: "{}",
539
+ extraParams: null,
540
+ createdAt: "2025-01-01T00:00:00.000Z",
541
+ updatedAt: "2025-01-01T00:00:00.000Z",
542
+ },
543
+ {
544
+ providerKey: "integration:slack",
545
+ authUrl: "https://slack.com/oauth/v2/authorize",
546
+ tokenUrl: "https://slack.com/api/oauth.v2.access",
547
+ defaultScopes: "[]",
548
+ scopePolicy: "{}",
549
+ extraParams: null,
550
+ createdAt: "2025-01-01T00:00:00.000Z",
551
+ updatedAt: "2025-01-01T00:00:00.000Z",
552
+ },
553
+ {
554
+ providerKey: "integration:twitter",
555
+ authUrl: "https://twitter.com/i/oauth2/authorize",
556
+ tokenUrl: "https://api.twitter.com/2/oauth2/token",
557
+ defaultScopes: "[]",
558
+ scopePolicy: "{}",
559
+ extraParams: null,
560
+ createdAt: "2025-01-01T00:00:00.000Z",
561
+ updatedAt: "2025-01-01T00:00:00.000Z",
562
+ },
563
+ ];
564
+
565
+ beforeEach(() => {
566
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
567
+ mockListProviders = () => fakeProviders;
568
+ secureKeyStore = new Map();
569
+ metadataStore = [];
570
+ disconnectOAuthProviderCalls = [];
571
+ disconnectOAuthProviderResult = "not-found";
572
+ idCounter = 0;
573
+ });
574
+
575
+ test("returns all providers when no --provider-key is given", async () => {
576
+ const { exitCode, stdout } = await runCli(["providers", "list", "--json"]);
577
+ expect(exitCode).toBe(0);
578
+ const parsed = JSON.parse(stdout);
579
+ expect(parsed).toHaveLength(4);
580
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
581
+ expect(keys).toContain("integration:gmail");
582
+ expect(keys).toContain("integration:google-calendar");
583
+ expect(keys).toContain("integration:slack");
584
+ expect(keys).toContain("integration:twitter");
585
+ });
586
+
587
+ test("filters by single --provider-key value", async () => {
588
+ const { exitCode, stdout } = await runCli([
589
+ "providers",
590
+ "list",
591
+ "--provider-key",
592
+ "gmail",
593
+ "--json",
594
+ ]);
595
+ expect(exitCode).toBe(0);
596
+ const parsed = JSON.parse(stdout);
597
+ expect(parsed).toHaveLength(1);
598
+ expect(parsed[0].providerKey).toBe("integration:gmail");
599
+ });
600
+
601
+ test("filters by comma-separated OR values", async () => {
602
+ const { exitCode, stdout } = await runCli([
603
+ "providers",
604
+ "list",
605
+ "--provider-key",
606
+ "gmail,google",
607
+ "--json",
608
+ ]);
609
+ expect(exitCode).toBe(0);
610
+ const parsed = JSON.parse(stdout);
611
+ expect(parsed).toHaveLength(2);
612
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
613
+ expect(keys).toContain("integration:gmail");
614
+ expect(keys).toContain("integration:google-calendar");
615
+ });
616
+
617
+ test("returns empty array when comma-separated filter has no matches", async () => {
618
+ const { exitCode, stdout } = await runCli([
619
+ "providers",
620
+ "list",
621
+ "--provider-key",
622
+ "notion,linear",
623
+ "--json",
624
+ ]);
625
+ expect(exitCode).toBe(0);
626
+ const parsed = JSON.parse(stdout);
627
+ expect(parsed).toHaveLength(0);
628
+ });
629
+
630
+ test("trims whitespace around commas in --provider-key", async () => {
631
+ const { exitCode, stdout } = await runCli([
632
+ "providers",
633
+ "list",
634
+ "--provider-key",
635
+ "gmail, google",
636
+ "--json",
637
+ ]);
638
+ expect(exitCode).toBe(0);
639
+ const parsed = JSON.parse(stdout);
640
+ expect(parsed).toHaveLength(2);
641
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
642
+ expect(keys).toContain("integration:gmail");
643
+ expect(keys).toContain("integration:google-calendar");
644
+ });
645
+
646
+ test("ignores empty segments from extra commas in --provider-key", async () => {
647
+ const { exitCode, stdout } = await runCli([
648
+ "providers",
649
+ "list",
650
+ "--provider-key",
651
+ "gmail,,google",
652
+ "--json",
653
+ ]);
654
+ expect(exitCode).toBe(0);
655
+ const parsed = JSON.parse(stdout);
656
+ expect(parsed).toHaveLength(2);
657
+ const keys = parsed.map((p: { providerKey: string }) => p.providerKey);
658
+ expect(keys).toContain("integration:gmail");
659
+ expect(keys).toContain("integration:google-calendar");
660
+ });
661
+ });
662
+
663
+ // ---------------------------------------------------------------------------
664
+ // connect
665
+ // ---------------------------------------------------------------------------
666
+
667
+ describe("assistant oauth connections connect <provider-key>", () => {
668
+ beforeEach(() => {
669
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
670
+ secureKeyStore = new Map();
671
+ metadataStore = [];
672
+ disconnectOAuthProviderCalls = [];
673
+ disconnectOAuthProviderResult = "not-found";
674
+ idCounter = 0;
675
+ mockOrchestrateOAuthConnect = async () => ({
676
+ success: true,
677
+ deferred: false,
678
+ grantedScopes: [],
679
+ });
680
+ mockGetAppByProviderAndClientId = () => undefined;
681
+ mockGetMostRecentAppByProvider = () => undefined;
682
+ mockGetProvider = () => undefined;
683
+ mockGetProviderBehavior = () => undefined;
684
+ mockGetSecureKey = () => undefined;
685
+ });
686
+
687
+ test("completes interactive flow and prints success (human mode)", async () => {
688
+ mockOrchestrateOAuthConnect = async () => ({
689
+ success: true,
690
+ deferred: false,
691
+ grantedScopes: ["read"],
692
+ accountInfo: "user@example.com",
693
+ });
694
+
695
+ const { exitCode, stdout } = await runCli([
696
+ "connections",
697
+ "connect",
698
+ "integration:gmail",
699
+ "--client-id",
700
+ "test-id",
701
+ ]);
702
+ expect(exitCode).toBe(0);
703
+ expect(stdout).toContain("Connected");
704
+ });
705
+
706
+ test("completes interactive flow and returns JSON with --json flag", async () => {
707
+ mockOrchestrateOAuthConnect = async () => ({
708
+ success: true,
709
+ deferred: false,
710
+ grantedScopes: ["read"],
711
+ accountInfo: "user@example.com",
712
+ });
713
+
714
+ const { exitCode, stdout } = await runCli([
715
+ "connections",
716
+ "connect",
717
+ "integration:gmail",
718
+ "--client-id",
719
+ "test-id",
720
+ "--json",
721
+ ]);
722
+ expect(exitCode).toBe(0);
723
+ const parsed = JSON.parse(stdout);
724
+ expect(parsed).toEqual({
725
+ ok: true,
726
+ grantedScopes: ["read"],
727
+ accountInfo: "user@example.com",
728
+ });
729
+ });
730
+
731
+ test("returns auth URL in default (non-interactive) mode (JSON)", async () => {
732
+ mockOrchestrateOAuthConnect = async () => ({
733
+ success: true,
734
+ deferred: true,
735
+ authUrl: "https://example.com/auth",
736
+ state: "abc",
737
+ service: "integration:gmail",
738
+ });
739
+
740
+ const { exitCode, stdout } = await runCli([
741
+ "connections",
742
+ "connect",
743
+ "integration:gmail",
744
+ "--client-id",
745
+ "test-id",
746
+ "--json",
747
+ ]);
748
+ expect(exitCode).toBe(0);
749
+ const parsed = JSON.parse(stdout);
750
+ expect(parsed.ok).toBe(true);
751
+ expect(parsed.deferred).toBe(true);
752
+ expect(parsed.authUrl).toBe("https://example.com/auth");
753
+ });
754
+
755
+ test("fails when no client_id available", async () => {
756
+ mockGetMostRecentAppByProvider = () => undefined;
757
+
758
+ const { exitCode, stdout } = await runCli([
759
+ "connections",
760
+ "connect",
761
+ "integration:gmail",
762
+ "--json",
763
+ ]);
764
+ expect(exitCode).toBe(1);
765
+ const parsed = JSON.parse(stdout);
766
+ expect(parsed.ok).toBe(false);
767
+ expect(parsed.error).toContain("client_id");
768
+ });
769
+
770
+ test("resolves client_id from DB when not provided", async () => {
771
+ mockGetMostRecentAppByProvider = () => ({
772
+ id: "app-1",
773
+ clientId: "db-client-id",
774
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
775
+ providerKey: "integration:gmail",
776
+ createdAt: 0,
777
+ updatedAt: 0,
778
+ });
779
+
780
+ let capturedClientId: string | undefined;
781
+ mockOrchestrateOAuthConnect = async (opts) => {
782
+ capturedClientId = opts.clientId as string;
783
+ return {
784
+ success: true,
785
+ deferred: false,
786
+ grantedScopes: [],
787
+ };
788
+ };
789
+
790
+ await runCli(["connections", "connect", "integration:gmail"]);
791
+ expect(capturedClientId).toBe("db-client-id");
792
+ });
793
+
794
+ test("resolves client_secret from secure store when not provided", async () => {
795
+ mockGetMostRecentAppByProvider = () => ({
796
+ id: "app-1",
797
+ clientId: "db-client-id",
798
+ clientSecretCredentialPath: "oauth_app/app-1/client_secret",
799
+ providerKey: "integration:gmail",
800
+ createdAt: 0,
801
+ updatedAt: 0,
802
+ });
803
+
804
+ mockGetSecureKey = (account: string) =>
805
+ account === "oauth_app/app-1/client_secret" ? "db-secret" : undefined;
806
+
807
+ let capturedOpts: Record<string, unknown> | undefined;
808
+ mockOrchestrateOAuthConnect = async (opts) => {
809
+ capturedOpts = opts;
810
+ return {
811
+ success: true,
812
+ deferred: false,
813
+ grantedScopes: [],
814
+ };
815
+ };
816
+
817
+ await runCli(["connections", "connect", "integration:gmail"]);
818
+ expect(capturedOpts).toBeDefined();
819
+ expect(capturedOpts!.clientId).toBe("db-client-id");
820
+ expect(capturedOpts!.clientSecret).toBe("db-secret");
821
+ });
822
+
823
+ test("outputs error from orchestrator", async () => {
824
+ mockOrchestrateOAuthConnect = async () => ({
825
+ success: false,
826
+ error: "Something went wrong",
827
+ });
828
+
829
+ const { exitCode, stdout } = await runCli([
830
+ "connections",
831
+ "connect",
832
+ "integration:gmail",
833
+ "--client-id",
834
+ "x",
835
+ "--json",
836
+ ]);
837
+ expect(exitCode).toBe(1);
838
+ const parsed = JSON.parse(stdout);
839
+ expect(parsed.ok).toBe(false);
840
+ expect(parsed.error).toBe("Something went wrong");
841
+ });
842
+
843
+ test("fails when client_secret is required but missing", async () => {
844
+ mockGetProviderBehavior = () => ({
845
+ setup: {
846
+ requiresClientSecret: true,
847
+ displayName: "Test",
848
+ dashboardUrl: "https://example.com",
849
+ appType: "app",
850
+ },
851
+ });
852
+
853
+ const { exitCode, stdout } = await runCli([
854
+ "connections",
855
+ "connect",
856
+ "integration:gmail",
857
+ "--client-id",
858
+ "test-id",
859
+ "--json",
860
+ ]);
861
+ expect(exitCode).toBe(1);
862
+ const parsed = JSON.parse(stdout);
863
+ expect(parsed.ok).toBe(false);
864
+ expect(parsed.error).toContain("client_secret");
865
+ expect(parsed.error).toContain("apps upsert");
866
+ });
867
+ });
868
+
869
+ // ---------------------------------------------------------------------------
870
+ // apps upsert --client-secret-credential-path
871
+ // ---------------------------------------------------------------------------
872
+
873
+ describe("assistant oauth apps upsert --client-secret-credential-path", () => {
874
+ beforeEach(() => {
875
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
876
+ secureKeyStore = new Map();
877
+ metadataStore = [];
878
+ disconnectOAuthProviderCalls = [];
879
+ disconnectOAuthProviderResult = "not-found";
880
+ idCounter = 0;
881
+ mockUpsertAppCalls = [];
882
+ mockUpsertAppResult = {
883
+ id: "app-upsert-1",
884
+ providerKey: "integration:gmail",
885
+ clientId: "abc123",
886
+ createdAt: 1700000000000,
887
+ updatedAt: 1700000000000,
888
+ };
889
+ mockOrchestrateOAuthConnect = async () => ({
890
+ success: true,
891
+ deferred: false,
892
+ grantedScopes: [],
893
+ });
894
+ mockGetAppByProviderAndClientId = () => undefined;
895
+ mockGetMostRecentAppByProvider = () => undefined;
896
+ mockGetProvider = () => undefined;
897
+ mockGetProviderBehavior = () => undefined;
898
+ mockGetSecureKey = () => undefined;
899
+ });
900
+
901
+ test("upsert with --client-secret-credential-path passes path to upsertApp", async () => {
902
+ const { exitCode, stdout } = await runCli([
903
+ "apps",
904
+ "upsert",
905
+ "--provider",
906
+ "integration:gmail",
907
+ "--client-id",
908
+ "abc123",
909
+ "--client-secret-credential-path",
910
+ "custom/path",
911
+ "--json",
912
+ ]);
913
+ expect(exitCode).toBe(0);
914
+ expect(mockUpsertAppCalls).toHaveLength(1);
915
+ expect(mockUpsertAppCalls[0]).toEqual({
916
+ provider: "integration:gmail",
917
+ clientId: "abc123",
918
+ clientSecretOpts: { clientSecretCredentialPath: "custom/path" },
919
+ });
920
+ const parsed = JSON.parse(stdout);
921
+ expect(parsed.id).toBe("app-upsert-1");
922
+ });
923
+
924
+ test("upsert with both --client-secret and --client-secret-credential-path returns error", async () => {
925
+ const { exitCode, stdout } = await runCli([
926
+ "apps",
927
+ "upsert",
928
+ "--provider",
929
+ "integration:gmail",
930
+ "--client-id",
931
+ "abc123",
932
+ "--client-secret",
933
+ "s3cret",
934
+ "--client-secret-credential-path",
935
+ "custom/path",
936
+ "--json",
937
+ ]);
938
+ expect(exitCode).toBe(1);
939
+ const parsed = JSON.parse(stdout);
940
+ expect(parsed.ok).toBe(false);
941
+ expect(parsed.error).toContain(
942
+ "Cannot provide both --client-secret and --client-secret-credential-path",
943
+ );
944
+ // upsertApp should NOT have been called
945
+ expect(mockUpsertAppCalls).toHaveLength(0);
946
+ });
947
+
948
+ test("upsert with --client-secret passes clientSecretValue to upsertApp", async () => {
949
+ const { exitCode } = await runCli([
950
+ "apps",
951
+ "upsert",
952
+ "--provider",
953
+ "integration:gmail",
954
+ "--client-id",
955
+ "abc123",
956
+ "--client-secret",
957
+ "s3cret",
958
+ "--json",
959
+ ]);
960
+ expect(exitCode).toBe(0);
961
+ expect(mockUpsertAppCalls).toHaveLength(1);
962
+ expect(mockUpsertAppCalls[0]).toEqual({
963
+ provider: "integration:gmail",
964
+ clientId: "abc123",
965
+ clientSecretOpts: { clientSecretValue: "s3cret" },
966
+ });
967
+ });
968
+
969
+ test("upsert without any secret option passes undefined", async () => {
970
+ const { exitCode } = await runCli([
971
+ "apps",
972
+ "upsert",
973
+ "--provider",
974
+ "integration:gmail",
975
+ "--client-id",
976
+ "abc123",
977
+ "--json",
978
+ ]);
979
+ expect(exitCode).toBe(0);
980
+ expect(mockUpsertAppCalls).toHaveLength(1);
981
+ expect(mockUpsertAppCalls[0]).toEqual({
982
+ provider: "integration:gmail",
983
+ clientId: "abc123",
984
+ clientSecretOpts: undefined,
985
+ });
986
+ });
987
+ });
988
+
989
+ // ---------------------------------------------------------------------------
990
+ // ping
991
+ // ---------------------------------------------------------------------------
992
+
993
+ describe("assistant oauth connections ping <provider-key>", () => {
994
+ beforeEach(() => {
995
+ mockWithValidToken = async (_service, cb) => cb("mock-access-token-xyz");
996
+ secureKeyStore = new Map();
997
+ metadataStore = [];
998
+ disconnectOAuthProviderCalls = [];
999
+ disconnectOAuthProviderResult = "not-found";
1000
+ idCounter = 0;
1001
+ });
1002
+
1003
+ test("returns ok when ping endpoint returns 200", async () => {
1004
+ mockGetProvider = () => ({
1005
+ providerKey: "integration:gmail",
1006
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1007
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1008
+ tokenUrl: "https://oauth2.googleapis.com/token",
1009
+ defaultScopes: "[]",
1010
+ scopePolicy: "{}",
1011
+ extraParams: null,
1012
+ createdAt: Date.now(),
1013
+ updatedAt: Date.now(),
1014
+ });
1015
+ const originalFetch = globalThis.fetch;
1016
+ globalThis.fetch = (async () =>
1017
+ new Response("{}", { status: 200 })) as unknown as typeof fetch;
1018
+ try {
1019
+ const { exitCode, stdout } = await runCli([
1020
+ "connections",
1021
+ "ping",
1022
+ "integration:gmail",
1023
+ "--json",
1024
+ ]);
1025
+ expect(exitCode).toBe(0);
1026
+ const parsed = JSON.parse(stdout);
1027
+ expect(parsed.ok).toBe(true);
1028
+ expect(parsed.status).toBe(200);
1029
+ } finally {
1030
+ globalThis.fetch = originalFetch;
1031
+ }
1032
+ });
1033
+
1034
+ test("exits 1 when provider not found", async () => {
1035
+ mockGetProvider = () => undefined;
1036
+ const { exitCode, stdout } = await runCli([
1037
+ "connections",
1038
+ "ping",
1039
+ "integration:unknown",
1040
+ "--json",
1041
+ ]);
1042
+ expect(exitCode).toBe(1);
1043
+ const parsed = JSON.parse(stdout);
1044
+ expect(parsed.ok).toBe(false);
1045
+ expect(parsed.error).toContain("Provider not found");
1046
+ });
1047
+
1048
+ test("exits 1 when no ping URL configured", async () => {
1049
+ mockGetProvider = () => ({
1050
+ providerKey: "telegram",
1051
+ pingUrl: null,
1052
+ authUrl: "urn:manual-token",
1053
+ tokenUrl: "urn:manual-token",
1054
+ defaultScopes: "[]",
1055
+ scopePolicy: "{}",
1056
+ extraParams: null,
1057
+ createdAt: Date.now(),
1058
+ updatedAt: Date.now(),
1059
+ });
1060
+ const { exitCode, stdout } = await runCli([
1061
+ "connections",
1062
+ "ping",
1063
+ "telegram",
1064
+ "--json",
1065
+ ]);
1066
+ expect(exitCode).toBe(1);
1067
+ const parsed = JSON.parse(stdout);
1068
+ expect(parsed.ok).toBe(false);
1069
+ expect(parsed.error).toContain("No ping URL configured");
1070
+ });
1071
+
1072
+ test("exits 1 when ping endpoint returns non-2xx", async () => {
1073
+ mockGetProvider = () => ({
1074
+ providerKey: "integration:gmail",
1075
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1076
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1077
+ tokenUrl: "https://oauth2.googleapis.com/token",
1078
+ defaultScopes: "[]",
1079
+ scopePolicy: "{}",
1080
+ extraParams: null,
1081
+ createdAt: Date.now(),
1082
+ updatedAt: Date.now(),
1083
+ });
1084
+ const originalFetch = globalThis.fetch;
1085
+ globalThis.fetch = (async () =>
1086
+ new Response("Unauthorized", { status: 401 })) as unknown as typeof fetch;
1087
+ try {
1088
+ const { exitCode, stdout } = await runCli([
1089
+ "connections",
1090
+ "ping",
1091
+ "integration:gmail",
1092
+ "--json",
1093
+ ]);
1094
+ expect(exitCode).toBe(1);
1095
+ const parsed = JSON.parse(stdout);
1096
+ expect(parsed.ok).toBe(false);
1097
+ expect(parsed.status).toBe(401);
1098
+ } finally {
1099
+ globalThis.fetch = originalFetch;
1100
+ }
1101
+ });
1102
+
1103
+ test("exits 1 when no token exists", async () => {
1104
+ mockWithValidToken = async () => {
1105
+ throw new Error('No access token found for "integration:gmail".');
1106
+ };
1107
+ mockGetProvider = () => ({
1108
+ providerKey: "integration:gmail",
1109
+ pingUrl: "https://www.googleapis.com/oauth2/v2/userinfo",
1110
+ authUrl: "https://accounts.google.com/o/oauth2/v2/auth",
1111
+ tokenUrl: "https://oauth2.googleapis.com/token",
1112
+ defaultScopes: "[]",
1113
+ scopePolicy: "{}",
1114
+ extraParams: null,
1115
+ createdAt: Date.now(),
1116
+ updatedAt: Date.now(),
1117
+ });
1118
+ const { exitCode, stdout } = await runCli([
1119
+ "connections",
1120
+ "ping",
1121
+ "integration:gmail",
1122
+ "--json",
1123
+ ]);
1124
+ expect(exitCode).toBe(1);
1125
+ const parsed = JSON.parse(stdout);
1126
+ expect(parsed.ok).toBe(false);
1127
+ expect(parsed.error).toContain("No access token");
1128
+ });
1129
+ });