@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
@@ -12,7 +12,7 @@ import type {
12
12
  ToolExecutionResult,
13
13
  } from "../../../../tools/types.js";
14
14
  import { truncate } from "../../../../util/truncate.js";
15
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
15
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
16
16
 
17
17
  function upsertMemoryItem(opts: {
18
18
  kind: string;
@@ -91,76 +91,75 @@ export async function run(
91
91
 
92
92
  try {
93
93
  const provider = resolveProvider(platform);
94
- return withProviderToken(provider, async (token) => {
95
- // Search for sent messages using the platform's search
96
- const query =
97
- queryFilter ?? (provider.id === "gmail" ? "in:sent" : "from:me");
98
- const searchResult = await provider.search(token, query, {
99
- count: maxMessages,
100
- });
94
+ const conn = getProviderConnection(provider);
95
+ // Search for sent messages using the platform's search
96
+ const query =
97
+ queryFilter ?? (provider.id === "gmail" ? "in:sent" : "from:me");
98
+ const searchResult = await provider.search(conn, query, {
99
+ count: maxMessages,
100
+ });
101
101
 
102
- if (searchResult.messages.length === 0) {
103
- return err(
104
- "No sent messages found. Send some messages first, then try again.",
105
- );
106
- }
102
+ if (searchResult.messages.length === 0) {
103
+ return err(
104
+ "No sent messages found. Send some messages first, then try again.",
105
+ );
106
+ }
107
107
 
108
- const result = await extractStylePatterns(searchResult.messages);
108
+ const result = await extractStylePatterns(searchResult.messages);
109
109
 
110
- if (result.stylePatterns.length === 0) {
111
- return err("No style patterns were extracted. Try with more messages.");
112
- }
110
+ if (result.stylePatterns.length === 0) {
111
+ return err("No style patterns were extracted. Try with more messages.");
112
+ }
113
113
 
114
- const scopeId = context.memoryScopeId ?? "default";
115
- let savedCount = 0;
114
+ const scopeId = context.memoryScopeId ?? "default";
115
+ let savedCount = 0;
116
116
 
117
- for (const pattern of result.stylePatterns) {
118
- const subject = `${provider.id} writing style: ${pattern.aspect}`;
119
- const importance = clampUnitInterval(
120
- Math.min(0.85, Math.max(0.55, pattern.importance ?? 0.65)),
121
- );
122
- upsertMemoryItem({
123
- kind: "style",
124
- subject,
125
- statement: pattern.summary,
126
- importance,
127
- scopeId,
128
- });
129
- savedCount++;
130
- }
117
+ for (const pattern of result.stylePatterns) {
118
+ const subject = `${provider.id} writing style: ${pattern.aspect}`;
119
+ const importance = clampUnitInterval(
120
+ Math.min(0.85, Math.max(0.55, pattern.importance ?? 0.65)),
121
+ );
122
+ upsertMemoryItem({
123
+ kind: "style",
124
+ subject,
125
+ statement: pattern.summary,
126
+ importance,
127
+ scopeId,
128
+ });
129
+ savedCount++;
130
+ }
131
131
 
132
- for (const contact of result.contactObservations) {
133
- if (!contact.name || !contact.toneNote) continue;
134
- const subject = `${provider.id} relationship: ${contact.name}`;
135
- upsertMemoryItem({
136
- kind: "relationship",
137
- subject,
138
- statement: truncate(
139
- `${contact.name} (${contact.email}): ${contact.toneNote}`,
140
- 500,
141
- "",
142
- ),
143
- importance: 0.6,
144
- scopeId,
145
- });
146
- savedCount++;
147
- }
132
+ for (const contact of result.contactObservations) {
133
+ if (!contact.name || !contact.toneNote) continue;
134
+ const subject = `${provider.id} relationship: ${contact.name}`;
135
+ upsertMemoryItem({
136
+ kind: "relationship",
137
+ subject,
138
+ statement: truncate(
139
+ `${contact.name} (${contact.email}): ${contact.toneNote}`,
140
+ 500,
141
+ "",
142
+ ),
143
+ importance: 0.6,
144
+ scopeId,
145
+ });
146
+ savedCount++;
147
+ }
148
148
 
149
- const aspects = result.stylePatterns.map((p) => p.aspect).join(", ");
150
- const contactCount = result.contactObservations.length;
151
- const summary = [
152
- `Analyzed ${searchResult.messages.length} messages on ${provider.displayName}.`,
153
- `Extracted ${result.stylePatterns.length} style patterns (${aspects}).`,
154
- contactCount > 0
155
- ? `Noted ${contactCount} recurring contact relationship(s).`
156
- : "",
157
- `Saved ${savedCount} memory items. Future drafts will automatically reflect your writing style.`,
158
- ]
159
- .filter(Boolean)
160
- .join(" ");
149
+ const aspects = result.stylePatterns.map((p) => p.aspect).join(", ");
150
+ const contactCount = result.contactObservations.length;
151
+ const summary = [
152
+ `Analyzed ${searchResult.messages.length} messages on ${provider.displayName}.`,
153
+ `Extracted ${result.stylePatterns.length} style patterns (${aspects}).`,
154
+ contactCount > 0
155
+ ? `Noted ${contactCount} recurring contact relationship(s).`
156
+ : "",
157
+ `Saved ${savedCount} memory items. Future drafts will automatically reflect your writing style.`,
158
+ ]
159
+ .filter(Boolean)
160
+ .join(" ");
161
161
 
162
- return ok(summary);
163
- });
162
+ return ok(summary);
164
163
  } catch (e) {
165
164
  return err(e instanceof Error ? e.message : String(e));
166
165
  }
@@ -2,7 +2,7 @@ import type {
2
2
  ToolContext,
3
3
  ToolExecutionResult,
4
4
  } from "../../../../tools/types.js";
5
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
5
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
6
6
 
7
7
  export async function run(
8
8
  input: Record<string, unknown>,
@@ -30,21 +30,20 @@ export async function run(
30
30
  );
31
31
  }
32
32
 
33
- return withProviderToken(provider, async (token) => {
34
- const result = await provider.archiveByQuery!(token, query);
35
-
36
- if (result.archived === 0) {
37
- return ok("No messages matched the query. Nothing archived.");
38
- }
39
-
40
- const summary = `Archived ${result.archived} message(s) matching query: ${query}`;
41
- if (result.truncated) {
42
- return ok(
43
- `${summary}\n\nNote: this operation was capped at 5000 messages. Additional messages matching the query may remain in the inbox. Run the command again to archive more.`,
44
- );
45
- }
46
- return ok(summary);
47
- });
33
+ const conn = getProviderConnection(provider);
34
+ const result = await provider.archiveByQuery!(conn, query);
35
+
36
+ if (result.archived === 0) {
37
+ return ok("No messages matched the query. Nothing archived.");
38
+ }
39
+
40
+ const summary = `Archived ${result.archived} message(s) matching query: ${query}`;
41
+ if (result.truncated) {
42
+ return ok(
43
+ `${summary}\n\nNote: this operation was capped at 5000 messages. Additional messages matching the query may remain in the inbox. Run the command again to archive more.`,
44
+ );
45
+ }
46
+ return ok(summary);
48
47
  } catch (e) {
49
48
  return err(e instanceof Error ? e.message : String(e));
50
49
  }
@@ -2,7 +2,7 @@ import type {
2
2
  ToolContext,
3
3
  ToolExecutionResult,
4
4
  } from "../../../../tools/types.js";
5
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
5
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
6
6
 
7
7
  export async function run(
8
8
  input: Record<string, unknown>,
@@ -12,10 +12,9 @@ export async function run(
12
12
 
13
13
  try {
14
14
  const provider = resolveProvider(platform);
15
- return withProviderToken(provider, async (token) => {
16
- const info = await provider.testConnection(token);
17
- return ok(JSON.stringify(info, null, 2));
18
- });
15
+ const conn = getProviderConnection(provider);
16
+ const info = await provider.testConnection(conn);
17
+ return ok(JSON.stringify(info, null, 2));
19
18
  } catch (e) {
20
19
  return err(e instanceof Error ? e.message : String(e));
21
20
  }
@@ -2,7 +2,7 @@ import type {
2
2
  ToolContext,
3
3
  ToolExecutionResult,
4
4
  } from "../../../../tools/types.js";
5
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
5
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
6
6
 
7
7
  export async function run(
8
8
  input: Record<string, unknown>,
@@ -14,13 +14,12 @@ export async function run(
14
14
 
15
15
  try {
16
16
  const provider = resolveProvider(platform);
17
- return withProviderToken(provider, async (token) => {
18
- const conversations = await provider.listConversations(token, {
19
- types: types as Array<"channel" | "dm" | "group" | "inbox"> | undefined,
20
- limit,
21
- });
22
- return ok(JSON.stringify(conversations, null, 2));
17
+ const conn = getProviderConnection(provider);
18
+ const conversations = await provider.listConversations(conn, {
19
+ types: types as Array<"channel" | "dm" | "group" | "inbox"> | undefined,
20
+ limit,
23
21
  });
22
+ return ok(JSON.stringify(conversations, null, 2));
24
23
  } catch (e) {
25
24
  return err(e instanceof Error ? e.message : String(e));
26
25
  }
@@ -2,7 +2,7 @@ import type {
2
2
  ToolContext,
3
3
  ToolExecutionResult,
4
4
  } from "../../../../tools/types.js";
5
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
5
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
6
6
 
7
7
  export async function run(
8
8
  input: Record<string, unknown>,
@@ -23,10 +23,9 @@ export async function run(
23
23
  `${provider.displayName} does not support marking messages as read.`,
24
24
  );
25
25
  }
26
- return withProviderToken(provider, async (token) => {
27
- await provider.markRead!(token, conversationId, messageId);
28
- return ok("Marked as read.");
29
- });
26
+ const conn = getProviderConnection(provider);
27
+ await provider.markRead(conn, conversationId, messageId);
28
+ return ok("Marked as read.");
30
29
  } catch (e) {
31
30
  return err(e instanceof Error ? e.message : String(e));
32
31
  }
@@ -2,7 +2,7 @@ import type {
2
2
  ToolContext,
3
3
  ToolExecutionResult,
4
4
  } from "../../../../tools/types.js";
5
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
5
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
6
6
 
7
7
  export async function run(
8
8
  input: Record<string, unknown>,
@@ -19,20 +19,19 @@ export async function run(
19
19
 
20
20
  try {
21
21
  const provider = resolveProvider(platform);
22
- return withProviderToken(provider, async (token) => {
23
- let messages;
24
- if (threadId && provider.getThreadReplies) {
25
- messages = await provider.getThreadReplies(
26
- token,
27
- conversationId,
28
- threadId,
29
- { limit },
30
- );
31
- } else {
32
- messages = await provider.getHistory(token, conversationId, { limit });
33
- }
34
- return ok(JSON.stringify(messages, null, 2));
35
- });
22
+ const conn = getProviderConnection(provider);
23
+ let messages;
24
+ if (threadId && provider.getThreadReplies) {
25
+ messages = await provider.getThreadReplies(
26
+ conn,
27
+ conversationId,
28
+ threadId,
29
+ { limit },
30
+ );
31
+ } else {
32
+ messages = await provider.getHistory(conn, conversationId, { limit });
33
+ }
34
+ return ok(JSON.stringify(messages, null, 2));
36
35
  } catch (e) {
37
36
  return err(e instanceof Error ? e.message : String(e));
38
37
  }
@@ -2,7 +2,7 @@ import type {
2
2
  ToolContext,
3
3
  ToolExecutionResult,
4
4
  } from "../../../../tools/types.js";
5
- import { err, ok, resolveProvider, withProviderToken } from "./shared.js";
5
+ import { err, getProviderConnection, ok, resolveProvider } from "./shared.js";
6
6
 
7
7
  export async function run(
8
8
  input: Record<string, unknown>,
@@ -18,10 +18,9 @@ export async function run(
18
18
 
19
19
  try {
20
20
  const provider = resolveProvider(platform);
21
- return withProviderToken(provider, async (token) => {
22
- const result = await provider.search(token, query, { count: maxResults });
23
- return ok(JSON.stringify(result, null, 2));
24
- });
21
+ const conn = getProviderConnection(provider);
22
+ const result = await provider.search(conn, query, { count: maxResults });
23
+ return ok(JSON.stringify(result, null, 2));
25
24
  } catch (e) {
26
25
  return err(e instanceof Error ? e.message : String(e));
27
26
  }
@@ -9,6 +9,7 @@ import {
9
9
  listMessages,
10
10
  } from "../../../../messaging/providers/gmail/client.js";
11
11
  import { buildMultipartMime } from "../../../../messaging/providers/gmail/mime-builder.js";
12
+ import type { OAuthConnection } from "../../../../oauth/connection.js";
12
13
  import type {
13
14
  ToolContext,
14
15
  ToolExecutionResult,
@@ -18,10 +19,10 @@ import {
18
19
  err,
19
20
  extractEmail,
20
21
  extractHeader,
22
+ getProviderConnection,
21
23
  ok,
22
24
  parseAddressList,
23
25
  resolveProvider,
24
- withProviderToken,
25
26
  } from "./shared.js";
26
27
 
27
28
  export async function run(
@@ -51,115 +52,66 @@ export async function run(
51
52
  return err("Attachments are only supported on Gmail.");
52
53
  }
53
54
 
55
+ const conn = getProviderConnection(provider);
56
+
54
57
  // Gmail: create a draft instead of sending directly
55
58
  if (provider.id === "gmail") {
56
- return withProviderToken(provider, async (token) => {
57
- // Reply mode: thread_id provided — create a threaded draft with reply-all recipients
58
- if (threadId) {
59
- // Fetch thread messages to extract recipients and threading headers
60
- const list = await listMessages(token, `thread:${threadId}`, 10);
61
- if (!list.messages?.length) {
62
- return err("No messages found in this thread.");
63
- }
64
-
65
- const messages = await batchGetMessages(
66
- token,
67
- list.messages.map((m) => m.id),
68
- "metadata",
69
- ["From", "To", "Cc", "Message-ID", "Subject"],
70
- );
59
+ const gmailConn = conn as OAuthConnection;
60
+ // Reply mode: thread_id provided — create a threaded draft with reply-all recipients
61
+ if (threadId) {
62
+ // Fetch thread messages to extract recipients and threading headers
63
+ const list = await listMessages(gmailConn, `thread:${threadId}`, 10);
64
+ if (!list.messages?.length) {
65
+ return err("No messages found in this thread.");
66
+ }
71
67
 
72
- // Use the latest message for threading and recipient extraction
73
- const latest = messages[messages.length - 1];
74
- const latestHeaders = latest.payload?.headers ?? [];
75
-
76
- const messageIdHeader = extractHeader(latestHeaders, "Message-ID");
77
- let replySubject = extractHeader(latestHeaders, "Subject");
78
- if (replySubject && !replySubject.startsWith("Re:")) {
79
- replySubject = `Re: ${replySubject}`;
80
- }
81
-
82
- // Build reply-all recipient list, excluding the user's own email
83
- const profile = await getProfile(token);
84
- const userEmail = profile.emailAddress.toLowerCase();
85
-
86
- const allRecipients = new Set<string>();
87
- const allCc = new Set<string>();
88
-
89
- // From the latest message: From goes to To, original To/Cc go to Cc
90
- const fromAddr = extractHeader(latestHeaders, "From");
91
- const toAddrs = extractHeader(latestHeaders, "To");
92
- const ccAddrs = extractHeader(latestHeaders, "Cc");
93
-
94
- if (fromAddr) allRecipients.add(fromAddr);
95
- for (const addr of parseAddressList(toAddrs)) {
96
- allRecipients.add(addr);
97
- }
98
- for (const addr of parseAddressList(ccAddrs)) {
99
- allCc.add(addr);
100
- }
101
-
102
- // Remove user's own email from recipients using exact email comparison
103
- const filterSelf = (addr: string) => extractEmail(addr) !== userEmail;
104
- const toList = [...allRecipients].filter(filterSelf);
105
- const ccList = [...allCc].filter(filterSelf);
106
-
107
- if (toList.length === 0) {
108
- return err("Could not determine reply recipients from thread.");
109
- }
110
-
111
- // With attachments: build multipart MIME for threaded reply
112
- if (attachmentPaths?.length) {
113
- const attachments = await Promise.all(
114
- attachmentPaths.map(async (filePath) => {
115
- const data = await readFile(filePath);
116
- const filename = basename(filePath);
117
- const mimeType = guessMimeType(filePath);
118
- return { filename, mimeType, data };
119
- }),
120
- );
121
-
122
- const raw = buildMultipartMime({
123
- to: toList.join(", "),
124
- subject: replySubject,
125
- body: text,
126
- inReplyTo: messageIdHeader || undefined,
127
- cc: ccList.length > 0 ? ccList.join(", ") : undefined,
128
- attachments,
129
- });
130
- const draft = await createDraftRaw(token, raw, threadId);
131
-
132
- const filenames = attachments.map((a) => a.filename).join(", ");
133
- const recipientSummary =
134
- ccList.length > 0
135
- ? `To: ${toList.join(", ")}; Cc: ${ccList.join(", ")}`
136
- : `To: ${toList.join(", ")}`;
137
- return ok(
138
- `Gmail draft created with ${attachments.length} attachment(s): ${filenames} (Draft ID: ${draft.id}). ${recipientSummary}. Review in Gmail Drafts, then tell me to send it or send it yourself.`,
139
- );
140
- }
141
-
142
- const draft = await createDraft(
143
- token,
144
- toList.join(", "),
145
- replySubject,
146
- text,
147
- messageIdHeader || undefined,
148
- ccList.length > 0 ? ccList.join(", ") : undefined,
149
- undefined,
150
- threadId,
151
- );
68
+ const messages = await batchGetMessages(
69
+ gmailConn,
70
+ list.messages.map((m) => m.id),
71
+ "metadata",
72
+ ["From", "To", "Cc", "Message-ID", "Subject"],
73
+ );
152
74
 
153
- const recipientSummary =
154
- ccList.length > 0
155
- ? `To: ${toList.join(", ")}; Cc: ${ccList.join(", ")}`
156
- : `To: ${toList.join(", ")}`;
157
- return ok(
158
- `Gmail draft created (ID: ${draft.id}). ${recipientSummary}. Review in Gmail Drafts, then tell me to send it or send it yourself.`,
159
- );
75
+ // Use the latest message for threading and recipient extraction
76
+ const latest = messages[messages.length - 1];
77
+ const latestHeaders = latest.payload?.headers ?? [];
78
+
79
+ const messageIdHeader = extractHeader(latestHeaders, "Message-ID");
80
+ let replySubject = extractHeader(latestHeaders, "Subject");
81
+ if (replySubject && !replySubject.startsWith("Re:")) {
82
+ replySubject = `Re: ${replySubject}`;
83
+ }
84
+
85
+ // Build reply-all recipient list, excluding the user's own email
86
+ const profile = await getProfile(gmailConn);
87
+ const userEmail = profile.emailAddress.toLowerCase();
88
+
89
+ const allRecipients = new Set<string>();
90
+ const allCc = new Set<string>();
91
+
92
+ // From the latest message: From goes to To, original To/Cc go to Cc
93
+ const fromAddr = extractHeader(latestHeaders, "From");
94
+ const toAddrs = extractHeader(latestHeaders, "To");
95
+ const ccAddrs = extractHeader(latestHeaders, "Cc");
96
+
97
+ if (fromAddr) allRecipients.add(fromAddr);
98
+ for (const addr of parseAddressList(toAddrs)) {
99
+ allRecipients.add(addr);
100
+ }
101
+ for (const addr of parseAddressList(ccAddrs)) {
102
+ allCc.add(addr);
103
+ }
104
+
105
+ // Remove user's own email from recipients using exact email comparison
106
+ const filterSelf = (addr: string) => extractEmail(addr) !== userEmail;
107
+ const toList = [...allRecipients].filter(filterSelf);
108
+ const ccList = [...allCc].filter(filterSelf);
109
+
110
+ if (toList.length === 0) {
111
+ return err("Could not determine reply recipients from thread.");
160
112
  }
161
113
 
162
- // With attachments: build multipart MIME and use createDraftRaw
114
+ // With attachments: build multipart MIME for threaded reply
163
115
  if (attachmentPaths?.length) {
164
116
  const attachments = await Promise.all(
165
117
  attachmentPaths.map(async (filePath) => {
@@ -171,51 +123,99 @@ export async function run(
171
123
  );
172
124
 
173
125
  const raw = buildMultipartMime({
174
- to: conversationId,
175
- subject: subject ?? "",
126
+ to: toList.join(", "),
127
+ subject: replySubject,
176
128
  body: text,
177
- inReplyTo,
129
+ inReplyTo: messageIdHeader || undefined,
130
+ cc: ccList.length > 0 ? ccList.join(", ") : undefined,
178
131
  attachments,
179
132
  });
180
- const draft = await createDraftRaw(token, raw, threadId);
133
+ const draft = await createDraftRaw(gmailConn, raw, threadId);
181
134
 
182
135
  const filenames = attachments.map((a) => a.filename).join(", ");
136
+ const recipientSummary =
137
+ ccList.length > 0
138
+ ? `To: ${toList.join(", ")}; Cc: ${ccList.join(", ")}`
139
+ : `To: ${toList.join(", ")}`;
183
140
  return ok(
184
- `Gmail draft created with ${attachments.length} attachment(s): ${filenames} (Draft ID: ${draft.id}). Review in Gmail Drafts, then tell me to send it or send it yourself.`,
141
+ `Gmail draft created with ${attachments.length} attachment(s): ${filenames} (Draft ID: ${draft.id}). ${recipientSummary}. Review in Gmail Drafts, then tell me to send it or send it yourself.`,
185
142
  );
186
143
  }
187
144
 
188
- // Without attachments: use standard createDraft
189
145
  const draft = await createDraft(
190
- token,
191
- conversationId,
192
- subject ?? "",
146
+ gmailConn,
147
+ toList.join(", "),
148
+ replySubject,
193
149
  text,
194
- inReplyTo,
195
- undefined,
150
+ messageIdHeader || undefined,
151
+ ccList.length > 0 ? ccList.join(", ") : undefined,
196
152
  undefined,
197
153
  threadId,
198
154
  );
155
+
156
+ const recipientSummary =
157
+ ccList.length > 0
158
+ ? `To: ${toList.join(", ")}; Cc: ${ccList.join(", ")}`
159
+ : `To: ${toList.join(", ")}`;
199
160
  return ok(
200
- `Gmail draft created (ID: ${draft.id}). Review it in your Gmail Drafts, then tell me to send it or send it yourself from Gmail.`,
161
+ `Gmail draft created (ID: ${draft.id}). ${recipientSummary}. Review in Gmail Drafts, then tell me to send it or send it yourself.`,
162
+ );
163
+ }
164
+
165
+ // With attachments: build multipart MIME and use createDraftRaw
166
+ if (attachmentPaths?.length) {
167
+ const attachments = await Promise.all(
168
+ attachmentPaths.map(async (filePath) => {
169
+ const data = await readFile(filePath);
170
+ const filename = basename(filePath);
171
+ const mimeType = guessMimeType(filePath);
172
+ return { filename, mimeType, data };
173
+ }),
201
174
  );
202
- });
203
- }
204
175
 
205
- // Non-Gmail platforms
206
- return withProviderToken(provider, async (token) => {
207
- const result = await provider.sendMessage(token, conversationId, text, {
208
- subject,
176
+ const raw = buildMultipartMime({
177
+ to: conversationId,
178
+ subject: subject ?? "",
179
+ body: text,
180
+ inReplyTo,
181
+ attachments,
182
+ });
183
+ const draft = await createDraftRaw(gmailConn, raw, threadId);
184
+
185
+ const filenames = attachments.map((a) => a.filename).join(", ");
186
+ return ok(
187
+ `Gmail draft created with ${attachments.length} attachment(s): ${filenames} (Draft ID: ${draft.id}). Review in Gmail Drafts, then tell me to send it or send it yourself.`,
188
+ );
189
+ }
190
+
191
+ // Without attachments: use standard createDraft
192
+ const draft = await createDraft(
193
+ gmailConn,
194
+ conversationId,
195
+ subject ?? "",
196
+ text,
209
197
  inReplyTo,
198
+ undefined,
199
+ undefined,
210
200
  threadId,
211
- assistantId: context.assistantId,
212
- });
201
+ );
202
+ return ok(
203
+ `Gmail draft created (ID: ${draft.id}). Review it in your Gmail Drafts, then tell me to send it or send it yourself from Gmail.`,
204
+ );
205
+ }
213
206
 
214
- const threadSuffix = result.threadId
215
- ? `, "thread_id": "${result.threadId}"`
216
- : "";
217
- return ok(`Message sent (ID: ${result.id}${threadSuffix}).`);
207
+ // Non-Gmail platforms
208
+ const result = await provider.sendMessage(conn, conversationId, text, {
209
+ subject,
210
+ inReplyTo,
211
+ threadId,
212
+ assistantId: context.assistantId,
218
213
  });
214
+
215
+ const threadSuffix = result.threadId
216
+ ? `, "thread_id": "${result.threadId}"`
217
+ : "";
218
+ return ok(`Message sent (ID: ${result.id}${threadSuffix}).`);
219
219
  } catch (e) {
220
220
  return err(e instanceof Error ? e.message : String(e));
221
221
  }