@vellumai/assistant 0.4.45 → 0.4.48

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 (236) hide show
  1. package/ARCHITECTURE.md +6 -6
  2. package/docs/architecture/memory.md +1 -1
  3. package/docs/architecture/scheduling.md +2 -3
  4. package/docs/architecture/security.md +5 -5
  5. package/docs/trusted-contact-access.md +5 -6
  6. package/package.json +4 -1
  7. package/src/__tests__/avatar-e2e.test.ts +18 -219
  8. package/src/__tests__/avatar-generator.test.ts +5 -57
  9. package/src/__tests__/browser-fill-credential.test.ts +5 -2
  10. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
  11. package/src/__tests__/channel-readiness-routes.test.ts +20 -19
  12. package/src/__tests__/cli.test.ts +23 -0
  13. package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
  14. package/src/__tests__/credential-broker-server-use.test.ts +22 -21
  15. package/src/__tests__/credential-broker.test.ts +2 -1
  16. package/src/__tests__/credential-metadata-store.test.ts +240 -18
  17. package/src/__tests__/credential-resolve.test.ts +5 -4
  18. package/src/__tests__/credential-security-e2e.test.ts +8 -8
  19. package/src/__tests__/credential-security-invariants.test.ts +104 -7
  20. package/src/__tests__/credential-vault-unit.test.ts +22 -20
  21. package/src/__tests__/credential-vault.test.ts +284 -12
  22. package/src/__tests__/credentials-cli.test.ts +11 -6
  23. package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
  24. package/src/__tests__/gemini-image-service.test.ts +75 -45
  25. package/src/__tests__/gemini-provider.test.ts +9 -6
  26. package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
  27. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
  28. package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
  29. package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
  30. package/src/__tests__/guardian-grant-minting.test.ts +35 -0
  31. package/src/__tests__/integration-status.test.ts +53 -21
  32. package/src/__tests__/managed-proxy-context.test.ts +5 -3
  33. package/src/__tests__/media-generate-image.test.ts +63 -2
  34. package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
  35. package/src/__tests__/messaging-send-tool.test.ts +4 -6
  36. package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
  37. package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
  38. package/src/__tests__/schedule-store.test.ts +1 -1
  39. package/src/__tests__/schema-transforms.test.ts +226 -0
  40. package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
  41. package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
  42. package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
  43. package/src/__tests__/secret-onetime-send.test.ts +5 -3
  44. package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
  45. package/src/__tests__/skills-uninstall.test.ts +2 -2
  46. package/src/__tests__/skills.test.ts +0 -9
  47. package/src/__tests__/slack-channel-config.test.ts +9 -8
  48. package/src/__tests__/slack-share-routes.test.ts +11 -6
  49. package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
  50. package/src/__tests__/twilio-config.test.ts +2 -1
  51. package/src/__tests__/twilio-provider.test.ts +4 -2
  52. package/src/__tests__/twilio-routes.test.ts +5 -4
  53. package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
  54. package/src/approvals/AGENTS.md +1 -1
  55. package/src/calls/call-domain.ts +7 -4
  56. package/src/calls/twilio-config.ts +2 -1
  57. package/src/calls/twilio-provider.ts +2 -1
  58. package/src/calls/twilio-rest.ts +2 -2
  59. package/src/cli/commands/browser-relay.ts +40 -15
  60. package/src/cli/commands/credentials.ts +9 -8
  61. package/src/cli/commands/oauth.ts +1 -1
  62. package/src/cli.ts +3 -2
  63. package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
  64. package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
  65. package/src/config/bundled-skills/gmail/SKILL.md +4 -4
  66. package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
  67. package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
  68. package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
  69. package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
  70. package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
  71. package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
  72. package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
  73. package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
  74. package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
  75. package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
  76. package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
  77. package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
  78. package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
  79. package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
  80. package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
  81. package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
  82. package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
  83. package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
  84. package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
  85. package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
  86. package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
  87. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
  88. package/src/config/bundled-skills/messaging/SKILL.md +6 -6
  89. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
  90. package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
  91. package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
  92. package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
  93. package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
  94. package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
  95. package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
  96. package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
  97. package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
  98. package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
  99. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  100. package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
  101. package/src/config/bundled-skills/schedule/SKILL.md +1 -1
  102. package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
  103. package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
  104. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
  105. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
  106. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
  107. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
  108. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
  109. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
  110. package/src/config/loader.ts +6 -0
  111. package/src/daemon/computer-use-session.ts +7 -1
  112. package/src/daemon/guardian-action-generators.ts +4 -5
  113. package/src/daemon/handlers/config-slack-channel.ts +37 -20
  114. package/src/daemon/handlers/config-telegram.ts +33 -20
  115. package/src/daemon/lifecycle.ts +9 -1
  116. package/src/daemon/message-types/integrations.ts +1 -0
  117. package/src/daemon/ride-shotgun-handler.ts +3 -1
  118. package/src/daemon/session-messaging.ts +3 -1
  119. package/src/daemon/session-tool-setup.ts +18 -2
  120. package/src/daemon/session.ts +1 -1
  121. package/src/email/providers/index.ts +2 -1
  122. package/src/instrument.ts +15 -1
  123. package/src/media/app-icon-generator.ts +30 -4
  124. package/src/media/avatar-router.ts +28 -62
  125. package/src/media/gemini-image-service.ts +28 -2
  126. package/src/memory/canonical-guardian-store.ts +1 -1
  127. package/src/memory/guardian-action-store.ts +1 -1
  128. package/src/memory/schema/guardian.ts +1 -1
  129. package/src/messaging/provider.ts +16 -10
  130. package/src/messaging/providers/gmail/adapter.ts +40 -23
  131. package/src/messaging/providers/gmail/client.ts +203 -122
  132. package/src/messaging/providers/gmail/people-client.ts +26 -18
  133. package/src/messaging/providers/slack/adapter.ts +29 -19
  134. package/src/messaging/providers/slack/client.ts +265 -78
  135. package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
  136. package/src/messaging/providers/whatsapp/adapter.ts +6 -3
  137. package/src/messaging/registry.ts +2 -1
  138. package/src/oauth/byo-connection.test.ts +436 -0
  139. package/src/oauth/byo-connection.ts +112 -0
  140. package/src/oauth/connect-orchestrator.ts +27 -0
  141. package/src/oauth/connection-resolver.ts +34 -0
  142. package/src/oauth/connection.ts +38 -0
  143. package/src/oauth/platform-connection.test.ts +163 -0
  144. package/src/oauth/platform-connection.ts +110 -0
  145. package/src/oauth/provider-base-urls.ts +21 -0
  146. package/src/oauth/provider-profiles.ts +1 -1
  147. package/src/oauth/token-persistence.ts +20 -20
  148. package/src/permissions/checker.ts +6 -1
  149. package/src/prompts/system-prompt.ts +52 -15
  150. package/src/prompts/templates/BOOTSTRAP.md +1 -1
  151. package/src/providers/gemini/client.ts +15 -6
  152. package/src/providers/managed-proxy/constants.ts +2 -2
  153. package/src/providers/managed-proxy/context.ts +5 -1
  154. package/src/providers/ratelimit.ts +17 -0
  155. package/src/providers/registry.ts +2 -2
  156. package/src/runtime/AGENTS.md +18 -1
  157. package/src/runtime/auth/route-policy.ts +1 -0
  158. package/src/runtime/channel-invite-transports/telegram.ts +2 -1
  159. package/src/runtime/channel-readiness-service.ts +168 -195
  160. package/src/runtime/channel-readiness-types.ts +4 -0
  161. package/src/runtime/guardian-action-conversation-turn.ts +1 -3
  162. package/src/runtime/guardian-action-followup-executor.ts +1 -2
  163. package/src/runtime/guardian-action-message-composer.ts +3 -23
  164. package/src/runtime/http-server.ts +9 -4
  165. package/src/runtime/http-types.ts +0 -1
  166. package/src/runtime/middleware/rate-limiter.ts +74 -20
  167. package/src/runtime/middleware/twilio-validation.ts +1 -3
  168. package/src/runtime/routes/channel-readiness-routes.ts +2 -0
  169. package/src/runtime/routes/diagnostics-routes.ts +11 -9
  170. package/src/runtime/routes/guardian-approval-interception.ts +20 -5
  171. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
  172. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
  173. package/src/runtime/routes/integrations/slack/share.ts +3 -2
  174. package/src/runtime/routes/integrations/twilio.ts +6 -5
  175. package/src/runtime/routes/secret-routes.ts +3 -2
  176. package/src/runtime/routes/settings-routes.ts +75 -17
  177. package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
  178. package/src/runtime/telegram-streaming-delivery.ts +11 -1
  179. package/src/schedule/integration-status.ts +5 -4
  180. package/src/security/credential-key.ts +170 -0
  181. package/src/security/token-manager.ts +36 -7
  182. package/src/tools/apps/definitions.ts +0 -5
  183. package/src/tools/assets/materialize.ts +0 -5
  184. package/src/tools/assets/search.ts +0 -5
  185. package/src/tools/browser/headless-browser.ts +1 -67
  186. package/src/tools/claude-code/claude-code.ts +0 -5
  187. package/src/tools/computer-use/request-computer-control.ts +0 -5
  188. package/src/tools/credentials/broker.ts +6 -4
  189. package/src/tools/credentials/metadata-store.ts +72 -20
  190. package/src/tools/credentials/resolve.ts +2 -1
  191. package/src/tools/credentials/vault.ts +77 -16
  192. package/src/tools/filesystem/edit.ts +1 -6
  193. package/src/tools/filesystem/read.ts +0 -5
  194. package/src/tools/filesystem/write.ts +1 -6
  195. package/src/tools/host-filesystem/edit.ts +1 -6
  196. package/src/tools/host-filesystem/read.ts +1 -6
  197. package/src/tools/host-filesystem/write.ts +1 -6
  198. package/src/tools/mcp/mcp-tool-factory.ts +18 -1
  199. package/src/tools/memory/definitions.ts +0 -5
  200. package/src/tools/network/web-fetch.ts +0 -5
  201. package/src/tools/network/web-search.ts +0 -5
  202. package/src/tools/schema-transforms.ts +99 -0
  203. package/src/tools/skills/load.ts +0 -5
  204. package/src/tools/swarm/delegate.ts +0 -5
  205. package/src/tools/system/avatar-generator.ts +3 -44
  206. package/src/tools/ui-surface/definitions.ts +0 -15
  207. package/src/tools/watch/screen-watch.ts +0 -5
  208. package/src/version.ts +10 -0
  209. package/src/watcher/providers/github.ts +51 -52
  210. package/src/watcher/providers/gmail.ts +88 -80
  211. package/src/watcher/providers/google-calendar.ts +93 -86
  212. package/src/watcher/providers/linear.ts +87 -93
  213. package/src/__tests__/avatar-router.test.ts +0 -149
  214. package/src/__tests__/managed-avatar-client.test.ts +0 -337
  215. package/src/config/bundled-skills/doordash/SKILL.md +0 -170
  216. package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
  217. package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
  218. package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
  219. package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
  220. package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
  221. package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
  222. package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
  223. package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
  224. package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
  225. package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
  226. package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
  227. package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
  228. package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
  229. package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
  230. package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
  231. package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
  232. package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
  233. package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
  234. package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
  235. package/src/media/avatar-types.ts +0 -53
  236. package/src/media/managed-avatar-client.ts +0 -225
@@ -3,8 +3,7 @@ import {
3
3
  searchContacts,
4
4
  } from "../../../../messaging/providers/gmail/people-client.js";
5
5
  import type { Person } from "../../../../messaging/providers/gmail/people-types.js";
6
- import { getMessagingProvider } from "../../../../messaging/registry.js";
7
- import { withValidToken } from "../../../../security/token-manager.js";
6
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
8
7
  import type {
9
8
  ToolContext,
10
9
  ToolExecutionResult,
@@ -44,43 +43,41 @@ export async function run(
44
43
  }
45
44
 
46
45
  try {
47
- const provider = getMessagingProvider("gmail");
48
- return await withValidToken(provider.credentialService, async (token) => {
49
- switch (action) {
50
- case "list": {
51
- const pageSize = (input.page_size as number) ?? 50;
52
- const pageToken = input.page_token as string | undefined;
46
+ const connection = resolveOAuthConnection("integration:gmail");
47
+ switch (action) {
48
+ case "list": {
49
+ const pageSize = (input.page_size as number) ?? 50;
50
+ const pageToken = input.page_token as string | undefined;
53
51
 
54
- const resp = await listContacts(token, pageSize, pageToken);
55
- const contacts = (resp.connections ?? []).map(formatContact);
52
+ const resp = await listContacts(connection, pageSize, pageToken);
53
+ const contacts = (resp.connections ?? []).map(formatContact);
56
54
 
57
- const result: Record<string, unknown> = {
58
- contacts,
59
- total: resp.totalPeople ?? contacts.length,
60
- };
61
- if (resp.nextPageToken) result.nextPageToken = resp.nextPageToken;
55
+ const result: Record<string, unknown> = {
56
+ contacts,
57
+ total: resp.totalPeople ?? contacts.length,
58
+ };
59
+ if (resp.nextPageToken) result.nextPageToken = resp.nextPageToken;
62
60
 
63
- return ok(JSON.stringify(result, null, 2));
64
- }
65
-
66
- case "search": {
67
- const query = input.query as string;
68
- if (!query) return err("query is required for search action.");
61
+ return ok(JSON.stringify(result, null, 2));
62
+ }
69
63
 
70
- const resp = await searchContacts(token, query);
71
- const contacts = (resp.results ?? []).map((r) =>
72
- formatContact(r.person),
73
- );
64
+ case "search": {
65
+ const query = input.query as string;
66
+ if (!query) return err("query is required for search action.");
74
67
 
75
- return ok(
76
- JSON.stringify({ contacts, total: contacts.length }, null, 2),
77
- );
78
- }
68
+ const resp = await searchContacts(connection, query);
69
+ const contacts = (resp.results ?? []).map((r) =>
70
+ formatContact(r.person),
71
+ );
79
72
 
80
- default:
81
- return err(`Unknown action "${action}". Use list or search.`);
73
+ return ok(
74
+ JSON.stringify({ contacts, total: contacts.length }, null, 2),
75
+ );
82
76
  }
83
- });
77
+
78
+ default:
79
+ return err(`Unknown action "${action}". Use list or search.`);
80
+ }
84
81
  } catch (e) {
85
82
  return err(e instanceof Error ? e.message : String(e));
86
83
  }
@@ -30,11 +30,11 @@ Do not offer AgentMail as an option or mention it unless the user specifically a
30
30
  ### Gmail
31
31
 
32
32
  1. **Try connecting directly first.** Call `credential_store` with `action: "oauth2_connect"` and `service: "gmail"`. The tool auto-fills Google's OAuth endpoints and looks up any previously stored client credentials — so this single call may be all that's needed.
33
- 2. **If it fails because no client_id is found:** The user needs to create Google Cloud OAuth credentials first. Load the **google-oauth-setup** skill (which depends on **public-ingress** for the redirect URI):
34
- - Call `skill_load` with `skill: "google-oauth-setup"` to load the dependency skill.
33
+ 2. **If it fails because no client_id is found:** The user needs to create Google Cloud OAuth credentials first. Load the **google-oauth-applescript** skill:
34
+ - Call `skill_load` with `skill: "google-oauth-applescript"` to load the dependency skill.
35
35
  - Tell the user Gmail isn't connected yet and briefly explain what the setup involves, then use `ui_show` with `surface_type: "confirmation"` to ask for permission to start:
36
36
  - **message:** "Ready to set up Gmail?"
37
- - **detail:** "I'll open a browser where you sign in to Google, then automate everything else — creating a project, enabling APIs, and connecting your account. Takes 2-3 minutes and you can watch in the browser preview panel."
37
+ - **detail:** "I'll open a few pages in your browser and walk you through setting up Google Cloud credentials — creating a project, enabling APIs, and connecting your account. Takes about 5 minutes."
38
38
  - **confirmLabel:** "Get Started"
39
39
  - **cancelLabel:** "Not Now"
40
40
  - If the user confirms, briefly acknowledge (e.g., "Setting up Gmail now...") and proceed with the setup guide. If they decline, acknowledge and let them know they can set it up later.
@@ -45,7 +45,7 @@ Do not offer AgentMail as an option or mention it unless the user specifically a
45
45
  When a Gmail tool fails with a token or authorization error:
46
46
 
47
47
  1. **Try to reconnect silently.** Call `credential_store` with `action: "oauth2_connect"` and `service: "gmail"`. This often resolves expired tokens automatically.
48
- 2. **If reconnection fails, go straight to setup.** Don't present options, ask which route the user prefers, or explain what went wrong technically. Just tell the user briefly (e.g., "Gmail needs to be reconnected — let me set that up") and immediately follow the connection setup flow for Gmail (e.g., install and load **google-oauth-setup**). The user came to you to get something done, not to troubleshoot OAuth — make it seamless.
48
+ 2. **If reconnection fails, go straight to setup.** Don't present options, ask which route the user prefers, or explain what went wrong technically. Just tell the user briefly (e.g., "Gmail needs to be reconnected — let me set that up") and immediately follow the connection setup flow for Gmail (e.g., install and load **google-oauth-applescript**). The user came to you to get something done, not to troubleshoot OAuth — make it seamless.
49
49
  3. **Never try alternative approaches.** Don't use bash, curl, browser automation, or any workaround. If the Gmail tools can't do it, the reconnection flow is the answer.
50
50
  4. **Never expose error details.** The user doesn't need to see error messages about tokens, OAuth, or API failures. Translate errors into plain language.
51
51
 
@@ -3,8 +3,7 @@ import {
3
3
  listMessages,
4
4
  modifyMessage,
5
5
  } from "../../../../messaging/providers/gmail/client.js";
6
- import { getMessagingProvider } from "../../../../messaging/registry.js";
7
- import { withValidToken } from "../../../../security/token-manager.js";
6
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
8
7
  import type {
9
8
  ToolContext,
10
9
  ToolExecutionResult,
@@ -35,49 +34,47 @@ export async function run(
35
34
  }
36
35
 
37
36
  try {
38
- const provider = getMessagingProvider("gmail");
39
- return await withValidToken(provider.credentialService, async (token) => {
40
- const allMessageIds: string[] = [];
41
- let pageToken: string | undefined;
42
- let truncated = false;
37
+ const connection = resolveOAuthConnection("integration:gmail");
38
+ const allMessageIds: string[] = [];
39
+ let pageToken: string | undefined;
40
+ let truncated = false;
43
41
 
44
- while (allMessageIds.length < MAX_MESSAGES) {
45
- const listResp = await listMessages(
46
- token,
47
- query,
48
- Math.min(500, MAX_MESSAGES - allMessageIds.length),
49
- pageToken,
50
- );
51
- const ids = (listResp.messages ?? []).map((m) => m.id);
52
- if (ids.length === 0) break;
53
- allMessageIds.push(...ids);
54
- pageToken = listResp.nextPageToken ?? undefined;
55
- if (!pageToken) break;
56
- }
42
+ while (allMessageIds.length < MAX_MESSAGES) {
43
+ const listResp = await listMessages(
44
+ connection,
45
+ query,
46
+ Math.min(500, MAX_MESSAGES - allMessageIds.length),
47
+ pageToken,
48
+ );
49
+ const ids = (listResp.messages ?? []).map((m) => m.id);
50
+ if (ids.length === 0) break;
51
+ allMessageIds.push(...ids);
52
+ pageToken = listResp.nextPageToken ?? undefined;
53
+ if (!pageToken) break;
54
+ }
57
55
 
58
- if (allMessageIds.length >= MAX_MESSAGES && pageToken) {
59
- truncated = true;
60
- }
56
+ if (allMessageIds.length >= MAX_MESSAGES && pageToken) {
57
+ truncated = true;
58
+ }
61
59
 
62
- if (allMessageIds.length === 0) {
63
- return ok("No messages matched the query. Nothing archived.");
64
- }
60
+ if (allMessageIds.length === 0) {
61
+ return ok("No messages matched the query. Nothing archived.");
62
+ }
65
63
 
66
- for (let i = 0; i < allMessageIds.length; i += BATCH_MODIFY_LIMIT) {
67
- const chunk = allMessageIds.slice(i, i + BATCH_MODIFY_LIMIT);
68
- await batchModifyMessages(token, chunk, {
69
- removeLabelIds: ["INBOX"],
70
- });
71
- }
64
+ for (let i = 0; i < allMessageIds.length; i += BATCH_MODIFY_LIMIT) {
65
+ const chunk = allMessageIds.slice(i, i + BATCH_MODIFY_LIMIT);
66
+ await batchModifyMessages(connection, chunk, {
67
+ removeLabelIds: ["INBOX"],
68
+ });
69
+ }
72
70
 
73
- const summary = `Archived ${allMessageIds.length} message(s) matching query: ${query}`;
74
- if (truncated) {
75
- return ok(
76
- `${summary}\n\nNote: this operation was capped at ${MAX_MESSAGES} messages. Additional messages matching the query may remain in the inbox. Run the command again to archive more.`,
77
- );
78
- }
79
- return ok(summary);
80
- });
71
+ const summary = `Archived ${allMessageIds.length} message(s) matching query: ${query}`;
72
+ if (truncated) {
73
+ return ok(
74
+ `${summary}\n\nNote: this operation was capped at ${MAX_MESSAGES} messages. Additional messages matching the query may remain in the inbox. Run the command again to archive more.`,
75
+ );
76
+ }
77
+ return ok(summary);
81
78
  } catch (e) {
82
79
  return err(e instanceof Error ? e.message : String(e));
83
80
  }
@@ -106,11 +103,9 @@ export async function run(
106
103
  } else if (messageId) {
107
104
  // Single message path
108
105
  try {
109
- const provider = getMessagingProvider("gmail");
110
- return await withValidToken(provider.credentialService, async (token) => {
111
- await modifyMessage(token, messageId, { removeLabelIds: ["INBOX"] });
112
- return ok("Message archived.");
113
- });
106
+ const connection = resolveOAuthConnection("integration:gmail");
107
+ await modifyMessage(connection, messageId, { removeLabelIds: ["INBOX"] });
108
+ return ok("Message archived.");
114
109
  } catch (e) {
115
110
  return err(e instanceof Error ? e.message : String(e));
116
111
  }
@@ -126,23 +121,21 @@ export async function run(
126
121
  }
127
122
 
128
123
  try {
129
- const provider = getMessagingProvider("gmail");
130
- return await withValidToken(provider.credentialService, async (token) => {
131
- if (messageIds.length === 1) {
132
- await modifyMessage(token, messageIds[0], {
133
- removeLabelIds: ["INBOX"],
134
- });
135
- return ok("Message archived.");
136
- }
124
+ const connection = resolveOAuthConnection("integration:gmail");
125
+ if (messageIds.length === 1) {
126
+ await modifyMessage(connection, messageIds[0], {
127
+ removeLabelIds: ["INBOX"],
128
+ });
129
+ return ok("Message archived.");
130
+ }
137
131
 
138
- for (let i = 0; i < messageIds.length; i += BATCH_MODIFY_LIMIT) {
139
- const chunk = messageIds.slice(i, i + BATCH_MODIFY_LIMIT);
140
- await batchModifyMessages(token, chunk, {
141
- removeLabelIds: ["INBOX"],
142
- });
143
- }
144
- return ok(`Archived ${messageIds.length} message(s).`);
145
- });
132
+ for (let i = 0; i < messageIds.length; i += BATCH_MODIFY_LIMIT) {
133
+ const chunk = messageIds.slice(i, i + BATCH_MODIFY_LIMIT);
134
+ await batchModifyMessages(connection, chunk, {
135
+ removeLabelIds: ["INBOX"],
136
+ });
137
+ }
138
+ return ok(`Archived ${messageIds.length} message(s).`);
146
139
  } catch (e) {
147
140
  return err(e instanceof Error ? e.message : String(e));
148
141
  }
@@ -6,8 +6,7 @@ import {
6
6
  getMessage,
7
7
  } from "../../../../messaging/providers/gmail/client.js";
8
8
  import type { GmailMessagePart } from "../../../../messaging/providers/gmail/types.js";
9
- import { getMessagingProvider } from "../../../../messaging/registry.js";
10
- import { withValidToken } from "../../../../security/token-manager.js";
9
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
11
10
  import type {
12
11
  ToolContext,
13
12
  ToolExecutionResult,
@@ -57,17 +56,15 @@ export async function run(
57
56
 
58
57
  if (action === "list") {
59
58
  try {
60
- const provider = getMessagingProvider("gmail");
61
- return await withValidToken(provider.credentialService, async (token) => {
62
- const message = await getMessage(token, messageId, "full");
63
- const attachments = collectAttachments(message.payload?.parts);
59
+ const connection = resolveOAuthConnection("integration:gmail");
60
+ const message = await getMessage(connection, messageId, "full");
61
+ const attachments = collectAttachments(message.payload?.parts);
64
62
 
65
- if (attachments.length === 0) {
66
- return ok("No attachments found on this message.");
67
- }
63
+ if (attachments.length === 0) {
64
+ return ok("No attachments found on this message.");
65
+ }
68
66
 
69
- return ok(JSON.stringify(attachments, null, 2));
70
- });
67
+ return ok(JSON.stringify(attachments, null, 2));
71
68
  } catch (e) {
72
69
  return err(e instanceof Error ? e.message : String(e));
73
70
  }
@@ -81,26 +78,26 @@ export async function run(
81
78
  if (!filename) return err("filename is required for download.");
82
79
 
83
80
  try {
84
- const provider = getMessagingProvider("gmail");
85
- return await withValidToken(provider.credentialService, async (token) => {
86
- const attachment = await getAttachment(token, messageId, attachmentId);
81
+ const connection = resolveOAuthConnection("integration:gmail");
82
+ const attachment = await getAttachment(
83
+ connection,
84
+ messageId,
85
+ attachmentId,
86
+ );
87
87
 
88
- // Gmail returns base64url; convert to standard base64 then to Buffer
89
- const base64 = attachment.data.replace(/-/g, "+").replace(/_/g, "/");
90
- const buffer = Buffer.from(base64, "base64");
88
+ // Gmail returns base64url; convert to standard base64 then to Buffer
89
+ const base64 = attachment.data.replace(/-/g, "+").replace(/_/g, "/");
90
+ const buffer = Buffer.from(base64, "base64");
91
91
 
92
- const outputDir = context.workingDir ?? process.cwd();
93
- // Sanitize filename: strip path separators to prevent traversal attacks from crafted MIME filenames
94
- const safeName = basename(filename).replace(/\.\./g, "_");
95
- const outputPath = resolve(outputDir, safeName);
96
- if (!outputPath.startsWith(outputDir))
97
- return err("Invalid filename: path traversal detected.");
98
- await writeFile(outputPath, buffer);
92
+ const outputDir = context.workingDir ?? process.cwd();
93
+ // Sanitize filename: strip path separators to prevent traversal attacks from crafted MIME filenames
94
+ const safeName = basename(filename).replace(/\.\./g, "_");
95
+ const outputPath = resolve(outputDir, safeName);
96
+ if (!outputPath.startsWith(outputDir))
97
+ return err("Invalid filename: path traversal detected.");
98
+ await writeFile(outputPath, buffer);
99
99
 
100
- return ok(
101
- `Attachment saved to ${outputPath} (${buffer.length} bytes).`,
102
- );
103
- });
100
+ return ok(`Attachment saved to ${outputPath} (${buffer.length} bytes).`);
104
101
  } catch (e) {
105
102
  return err(e instanceof Error ? e.message : String(e));
106
103
  }
@@ -1,6 +1,5 @@
1
1
  import { createDraft } from "../../../../messaging/providers/gmail/client.js";
2
- import { getMessagingProvider } from "../../../../messaging/registry.js";
3
- import { withValidToken } from "../../../../security/token-manager.js";
2
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
4
3
  import type {
5
4
  ToolContext,
6
5
  ToolExecutionResult,
@@ -23,21 +22,19 @@ export async function run(
23
22
  if (!body) return err("body is required.");
24
23
 
25
24
  try {
26
- const provider = getMessagingProvider("gmail");
27
- return await withValidToken(provider.credentialService, async (token) => {
28
- const draft = await createDraft(
29
- token,
30
- to,
31
- subject,
32
- body,
33
- inReplyTo,
34
- cc,
35
- bcc,
36
- );
37
- return ok(
38
- `Draft created (ID: ${draft.id}). It will appear in your Gmail Drafts.`,
39
- );
40
- });
25
+ const connection = resolveOAuthConnection("integration:gmail");
26
+ const draft = await createDraft(
27
+ connection,
28
+ to,
29
+ subject,
30
+ body,
31
+ inReplyTo,
32
+ cc,
33
+ bcc,
34
+ );
35
+ return ok(
36
+ `Draft created (ID: ${draft.id}). It will appear in your Gmail Drafts.`,
37
+ );
41
38
  } catch (e) {
42
39
  return err(e instanceof Error ? e.message : String(e));
43
40
  }
@@ -7,8 +7,7 @@ import type {
7
7
  GmailFilterAction,
8
8
  GmailFilterCriteria,
9
9
  } from "../../../../messaging/providers/gmail/types.js";
10
- import { getMessagingProvider } from "../../../../messaging/registry.js";
11
- import { withValidToken } from "../../../../security/token-manager.js";
10
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
12
11
  import type {
13
12
  ToolContext,
14
13
  ToolExecutionResult,
@@ -26,57 +25,53 @@ export async function run(
26
25
  }
27
26
 
28
27
  try {
29
- const provider = getMessagingProvider("gmail");
30
- return await withValidToken(provider.credentialService, async (token) => {
31
- switch (action) {
32
- case "list": {
33
- const filters = await listFilters(token);
34
- if (filters.length === 0) {
35
- return ok("No filters configured.");
36
- }
37
- return ok(JSON.stringify(filters, null, 2));
28
+ const connection = resolveOAuthConnection("integration:gmail");
29
+ switch (action) {
30
+ case "list": {
31
+ const filters = await listFilters(connection);
32
+ if (filters.length === 0) {
33
+ return ok("No filters configured.");
38
34
  }
35
+ return ok(JSON.stringify(filters, null, 2));
36
+ }
39
37
 
40
- case "create": {
41
- const criteria: GmailFilterCriteria = {};
42
- if (input.from) criteria.from = input.from as string;
43
- if (input.to) criteria.to = input.to as string;
44
- if (input.subject) criteria.subject = input.subject as string;
45
- if (input.query) criteria.query = input.query as string;
46
- if (input.has_attachment !== undefined)
47
- criteria.hasAttachment = input.has_attachment as boolean;
48
-
49
- const filterAction: GmailFilterAction = {};
50
- if (input.add_label_ids)
51
- filterAction.addLabelIds = input.add_label_ids as string[];
52
- if (input.remove_label_ids)
53
- filterAction.removeLabelIds = input.remove_label_ids as string[];
54
- if (input.forward) filterAction.forward = input.forward as string;
38
+ case "create": {
39
+ const criteria: GmailFilterCriteria = {};
40
+ if (input.from) criteria.from = input.from as string;
41
+ if (input.to) criteria.to = input.to as string;
42
+ if (input.subject) criteria.subject = input.subject as string;
43
+ if (input.query) criteria.query = input.query as string;
44
+ if (input.has_attachment !== undefined)
45
+ criteria.hasAttachment = input.has_attachment as boolean;
55
46
 
56
- if (Object.keys(criteria).length === 0) {
57
- return err(
58
- "At least one filter criteria is required (from, to, subject, query, or has_attachment).",
59
- );
60
- }
47
+ const filterAction: GmailFilterAction = {};
48
+ if (input.add_label_ids)
49
+ filterAction.addLabelIds = input.add_label_ids as string[];
50
+ if (input.remove_label_ids)
51
+ filterAction.removeLabelIds = input.remove_label_ids as string[];
52
+ if (input.forward) filterAction.forward = input.forward as string;
61
53
 
62
- const filter = await createFilter(token, criteria, filterAction);
63
- return ok(`Filter created (ID: ${filter.id}).`);
54
+ if (Object.keys(criteria).length === 0) {
55
+ return err(
56
+ "At least one filter criteria is required (from, to, subject, query, or has_attachment).",
57
+ );
64
58
  }
65
59
 
66
- case "delete": {
67
- const filterId = input.filter_id as string;
68
- if (!filterId) return err("filter_id is required for delete action.");
60
+ const filter = await createFilter(connection, criteria, filterAction);
61
+ return ok(`Filter created (ID: ${filter.id}).`);
62
+ }
69
63
 
70
- await deleteFilter(token, filterId);
71
- return ok("Filter deleted.");
72
- }
64
+ case "delete": {
65
+ const filterId = input.filter_id as string;
66
+ if (!filterId) return err("filter_id is required for delete action.");
73
67
 
74
- default:
75
- return err(
76
- `Unknown action "${action}". Use list, create, or delete.`,
77
- );
68
+ await deleteFilter(connection, filterId);
69
+ return ok("Filter deleted.");
78
70
  }
79
- });
71
+
72
+ default:
73
+ return err(`Unknown action "${action}". Use list, create, or delete.`);
74
+ }
80
75
  } catch (e) {
81
76
  return err(e instanceof Error ? e.message : String(e));
82
77
  }
@@ -5,8 +5,8 @@ import {
5
5
  listMessages,
6
6
  modifyMessage,
7
7
  } from "../../../../messaging/providers/gmail/client.js";
8
- import { getMessagingProvider } from "../../../../messaging/registry.js";
9
- import { withValidToken } from "../../../../security/token-manager.js";
8
+ import type { OAuthConnection } from "../../../../oauth/connection.js";
9
+ import { resolveOAuthConnection } from "../../../../oauth/connection-resolver.js";
10
10
  import type {
11
11
  ToolContext,
12
12
  ToolExecutionResult,
@@ -15,12 +15,14 @@ import { err, ok } from "./shared.js";
15
15
 
16
16
  const FOLLOW_UP_LABEL_NAME = "Follow-up";
17
17
 
18
- async function getOrCreateFollowUpLabel(token: string): Promise<string> {
19
- const labels = await listLabels(token);
18
+ async function getOrCreateFollowUpLabel(
19
+ connection: OAuthConnection,
20
+ ): Promise<string> {
21
+ const labels = await listLabels(connection);
20
22
  const existing = labels.find((l) => l.name === FOLLOW_UP_LABEL_NAME);
21
23
  if (existing) return existing.id;
22
24
 
23
- const created = await createLabel(token, FOLLOW_UP_LABEL_NAME);
25
+ const created = await createLabel(connection, FOLLOW_UP_LABEL_NAME);
24
26
  return created.id;
25
27
  }
26
28
 
@@ -35,67 +37,68 @@ export async function run(
35
37
  }
36
38
 
37
39
  try {
38
- const provider = getMessagingProvider("gmail");
39
- return await withValidToken(provider.credentialService, async (token) => {
40
- switch (action) {
41
- case "track": {
42
- const messageId = input.message_id as string;
43
- if (!messageId)
44
- return err("message_id is required for track action.");
40
+ const connection = resolveOAuthConnection("integration:gmail");
41
+ switch (action) {
42
+ case "track": {
43
+ const messageId = input.message_id as string;
44
+ if (!messageId) return err("message_id is required for track action.");
45
45
 
46
- const labelId = await getOrCreateFollowUpLabel(token);
47
- await modifyMessage(token, messageId, { addLabelIds: [labelId] });
48
- return ok("Message marked for follow-up.");
49
- }
50
-
51
- case "list": {
52
- const labelId = await getOrCreateFollowUpLabel(token);
53
- const listResp = await listMessages(token, undefined, 50, undefined, [
54
- labelId,
55
- ]);
56
- const messageIds = (listResp.messages ?? []).map((m) => m.id);
57
-
58
- if (messageIds.length === 0) {
59
- return ok("No messages are currently tracked for follow-up.");
60
- }
46
+ const labelId = await getOrCreateFollowUpLabel(connection);
47
+ await modifyMessage(connection, messageId, { addLabelIds: [labelId] });
48
+ return ok("Message marked for follow-up.");
49
+ }
61
50
 
62
- const messages = await batchGetMessages(
63
- token,
64
- messageIds,
65
- "metadata",
66
- ["From", "Subject", "Date"],
67
- );
68
- const items = messages.map((m) => {
69
- const headers = m.payload?.headers ?? [];
70
- const from =
71
- headers.find((h) => h.name.toLowerCase() === "from")?.value ?? "";
72
- const subject =
73
- headers.find((h) => h.name.toLowerCase() === "subject")?.value ??
74
- "";
75
- const date =
76
- headers.find((h) => h.name.toLowerCase() === "date")?.value ?? "";
77
- return { id: m.id, threadId: m.threadId, from, subject, date };
78
- });
51
+ case "list": {
52
+ const labelId = await getOrCreateFollowUpLabel(connection);
53
+ const listResp = await listMessages(
54
+ connection,
55
+ undefined,
56
+ 50,
57
+ undefined,
58
+ [labelId],
59
+ );
60
+ const messageIds = (listResp.messages ?? []).map((m) => m.id);
79
61
 
80
- return ok(JSON.stringify(items, null, 2));
62
+ if (messageIds.length === 0) {
63
+ return ok("No messages are currently tracked for follow-up.");
81
64
  }
82
65
 
83
- case "untrack": {
84
- const messageId = input.message_id as string;
85
- if (!messageId)
86
- return err("message_id is required for untrack action.");
66
+ const messages = await batchGetMessages(
67
+ connection,
68
+ messageIds,
69
+ "metadata",
70
+ ["From", "Subject", "Date"],
71
+ );
72
+ const items = messages.map((m) => {
73
+ const headers = m.payload?.headers ?? [];
74
+ const from =
75
+ headers.find((h) => h.name.toLowerCase() === "from")?.value ?? "";
76
+ const subject =
77
+ headers.find((h) => h.name.toLowerCase() === "subject")?.value ??
78
+ "";
79
+ const date =
80
+ headers.find((h) => h.name.toLowerCase() === "date")?.value ?? "";
81
+ return { id: m.id, threadId: m.threadId, from, subject, date };
82
+ });
87
83
 
88
- const labelId = await getOrCreateFollowUpLabel(token);
89
- await modifyMessage(token, messageId, { removeLabelIds: [labelId] });
90
- return ok("Follow-up tracking removed from message.");
91
- }
84
+ return ok(JSON.stringify(items, null, 2));
85
+ }
92
86
 
93
- default:
94
- return err(
95
- `Unknown action "${action}". Use track, list, or untrack.`,
96
- );
87
+ case "untrack": {
88
+ const messageId = input.message_id as string;
89
+ if (!messageId)
90
+ return err("message_id is required for untrack action.");
91
+
92
+ const labelId = await getOrCreateFollowUpLabel(connection);
93
+ await modifyMessage(connection, messageId, {
94
+ removeLabelIds: [labelId],
95
+ });
96
+ return ok("Follow-up tracking removed from message.");
97
97
  }
98
- });
98
+
99
+ default:
100
+ return err(`Unknown action "${action}". Use track, list, or untrack.`);
101
+ }
99
102
  } catch (e) {
100
103
  return err(e instanceof Error ? e.message : String(e));
101
104
  }