@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
@@ -9,9 +9,11 @@
9
9
  import {
10
10
  batchGetMessages,
11
11
  getProfile,
12
+ listMessages,
12
13
  } from "../../messaging/providers/gmail/client.js";
13
14
  import type { GmailMessage } from "../../messaging/providers/gmail/types.js";
14
- import { withValidToken } from "../../security/token-manager.js";
15
+ import type { OAuthConnection } from "../../oauth/connection.js";
16
+ import { resolveOAuthConnection } from "../../oauth/connection-resolver.js";
15
17
  import { getLogger } from "../../util/logger.js";
16
18
  import type {
17
19
  FetchResult,
@@ -21,8 +23,6 @@ import type {
21
23
 
22
24
  const log = getLogger("watcher:gmail");
23
25
 
24
- const GMAIL_API_BASE = "https://gmail.googleapis.com/gmail/v1/users/me";
25
-
26
26
  /** Gmail History API response types */
27
27
  interface HistoryMessage {
28
28
  id: string;
@@ -71,29 +71,38 @@ function messageToItem(msg: GmailMessage): WatcherItem {
71
71
  }
72
72
 
73
73
  async function fetchHistory(
74
- token: string,
74
+ connection: OAuthConnection,
75
75
  startHistoryId: string,
76
76
  ): Promise<HistoryListResponse> {
77
- const params = new URLSearchParams({
77
+ const query: Record<string, string> = {
78
78
  startHistoryId,
79
79
  historyTypes: "messageAdded",
80
80
  maxResults: "100",
81
- });
82
- const url = `${GMAIL_API_BASE}/history?${params}`;
83
- const resp = await fetch(url, {
84
- headers: { Authorization: `Bearer ${token}` },
81
+ };
82
+
83
+ const resp = await connection.request({
84
+ method: "GET",
85
+ path: "/history",
86
+ query,
85
87
  });
86
88
 
87
- if (!resp.ok) {
88
- const body = await resp.text().catch(() => "");
89
- if (resp.status === 404) {
90
- // historyId expired — caller handles fallback
91
- throw new HistoryExpiredError(body);
92
- }
89
+ if (resp.status === 404) {
90
+ const body =
91
+ typeof resp.body === "string"
92
+ ? resp.body
93
+ : JSON.stringify(resp.body ?? "");
94
+ throw new HistoryExpiredError(body);
95
+ }
96
+
97
+ if (resp.status < 200 || resp.status >= 300) {
98
+ const body =
99
+ typeof resp.body === "string"
100
+ ? resp.body
101
+ : JSON.stringify(resp.body ?? "");
93
102
  throw new Error(`Gmail History API ${resp.status}: ${body}`);
94
103
  }
95
104
 
96
- return resp.json() as Promise<HistoryListResponse>;
105
+ return resp.body as HistoryListResponse;
97
106
  }
98
107
 
99
108
  class HistoryExpiredError extends Error {
@@ -109,13 +118,12 @@ export const gmailProvider: WatcherProvider = {
109
118
  requiredCredentialService: "integration:gmail",
110
119
 
111
120
  async getInitialWatermark(credentialService: string): Promise<string> {
112
- return withValidToken(credentialService, async (token) => {
113
- const profile = await getProfile(token);
114
- if (!profile.historyId) {
115
- throw new Error("Gmail profile did not return a historyId");
116
- }
117
- return profile.historyId;
118
- });
121
+ const connection = resolveOAuthConnection(credentialService);
122
+ const profile = await getProfile(connection);
123
+ if (!profile.historyId) {
124
+ throw new Error("Gmail profile did not return a historyId");
125
+ }
126
+ return profile.historyId;
119
127
  },
120
128
 
121
129
  async fetchNew(
@@ -124,76 +132,76 @@ export const gmailProvider: WatcherProvider = {
124
132
  _config: Record<string, unknown>,
125
133
  _watcherKey: string,
126
134
  ): Promise<FetchResult> {
127
- return withValidToken(credentialService, async (token) => {
128
- if (!watermark) {
129
- // No watermark — get initial position, return no items
130
- const profile = await getProfile(token);
131
- return { items: [], watermark: profile.historyId ?? "0" };
132
- }
135
+ const connection = resolveOAuthConnection(credentialService);
133
136
 
134
- try {
135
- const historyResp = await fetchHistory(token, watermark);
136
- const newWatermark = historyResp.historyId ?? watermark;
137
+ if (!watermark) {
138
+ // No watermark get initial position, return no items
139
+ const profile = await getProfile(connection);
140
+ return { items: [], watermark: profile.historyId ?? "0" };
141
+ }
137
142
 
138
- if (!historyResp.history || historyResp.history.length === 0) {
139
- return { items: [], watermark: newWatermark };
140
- }
143
+ try {
144
+ const historyResp = await fetchHistory(connection, watermark);
145
+ const newWatermark = historyResp.historyId ?? watermark;
141
146
 
142
- // Collect unique new message IDs
143
- const messageIds = new Set<string>();
144
- for (const record of historyResp.history) {
145
- if (record.messagesAdded) {
146
- for (const added of record.messagesAdded) {
147
- messageIds.add(added.message.id);
148
- }
149
- }
150
- }
147
+ if (!historyResp.history || historyResp.history.length === 0) {
148
+ return { items: [], watermark: newWatermark };
149
+ }
151
150
 
152
- if (messageIds.size === 0) {
153
- return { items: [], watermark: newWatermark };
151
+ // Collect unique new message IDs
152
+ const messageIds = new Set<string>();
153
+ for (const record of historyResp.history) {
154
+ if (record.messagesAdded) {
155
+ for (const added of record.messagesAdded) {
156
+ messageIds.add(added.message.id);
157
+ }
154
158
  }
159
+ }
155
160
 
156
- // Fetch metadata for new messages
157
- const messages = await batchGetMessages(
158
- token,
159
- Array.from(messageIds),
160
- "metadata",
161
- ["From", "Subject", "Date"],
162
- );
163
-
164
- // Only include INBOX messages (skip sent, drafts, etc.)
165
- const inboxMessages = messages.filter((m) =>
166
- m.labelIds?.includes("INBOX"),
167
- );
161
+ if (messageIds.size === 0) {
162
+ return { items: [], watermark: newWatermark };
163
+ }
168
164
 
169
- const items = inboxMessages.map(messageToItem);
170
- log.info(
171
- { count: items.length, watermark: newWatermark },
172
- "Gmail: fetched new messages",
165
+ // Fetch metadata for new messages
166
+ const messages = await batchGetMessages(
167
+ connection,
168
+ Array.from(messageIds),
169
+ "metadata",
170
+ ["From", "Subject", "Date"],
171
+ );
172
+
173
+ // Only include INBOX messages (skip sent, drafts, etc.)
174
+ const inboxMessages = messages.filter((m) =>
175
+ m.labelIds?.includes("INBOX"),
176
+ );
177
+
178
+ const items = inboxMessages.map(messageToItem);
179
+ log.info(
180
+ { count: items.length, watermark: newWatermark },
181
+ "Gmail: fetched new messages",
182
+ );
183
+
184
+ return { items, watermark: newWatermark };
185
+ } catch (err) {
186
+ if (err instanceof HistoryExpiredError) {
187
+ log.warn(
188
+ "Gmail historyId expired, falling back to recent unread messages",
173
189
  );
174
-
175
- return { items, watermark: newWatermark };
176
- } catch (err) {
177
- if (err instanceof HistoryExpiredError) {
178
- log.warn(
179
- "Gmail historyId expired, falling back to recent unread messages",
180
- );
181
- return fallbackFetch(token);
182
- }
183
- throw err;
190
+ return fallbackFetch(connection);
184
191
  }
185
- });
192
+ throw err;
193
+ }
186
194
  },
187
195
  };
188
196
 
189
197
  /**
190
198
  * Fallback when historyId expires: list recent unread inbox messages.
191
199
  */
192
- async function fallbackFetch(token: string): Promise<FetchResult> {
193
- const { listMessages } =
194
- await import("../../messaging/providers/gmail/client.js");
200
+ async function fallbackFetch(
201
+ connection: OAuthConnection,
202
+ ): Promise<FetchResult> {
195
203
  const listResp = await listMessages(
196
- token,
204
+ connection,
197
205
  "is:unread newer_than:1d",
198
206
  20,
199
207
  undefined,
@@ -201,12 +209,12 @@ async function fallbackFetch(token: string): Promise<FetchResult> {
201
209
  );
202
210
 
203
211
  if (!listResp.messages || listResp.messages.length === 0) {
204
- const profile = await getProfile(token);
212
+ const profile = await getProfile(connection);
205
213
  return { items: [], watermark: profile.historyId ?? "0" };
206
214
  }
207
215
 
208
216
  const messages = await batchGetMessages(
209
- token,
217
+ connection,
210
218
  listResp.messages.map((m) => m.id),
211
219
  "metadata",
212
220
  ["From", "Subject", "Date"],
@@ -215,6 +223,6 @@ async function fallbackFetch(token: string): Promise<FetchResult> {
215
223
  const items = messages.map(messageToItem);
216
224
 
217
225
  // Get fresh historyId for the new watermark
218
- const profile = await getProfile(token);
226
+ const profile = await getProfile(connection);
219
227
  return { items, watermark: profile.historyId ?? "0" };
220
228
  }
@@ -11,7 +11,9 @@ import {
11
11
  listEvents,
12
12
  } from "../../config/bundled-skills/google-calendar/calendar-client.js";
13
13
  import type { CalendarEvent } from "../../config/bundled-skills/google-calendar/types.js";
14
- import { withValidToken } from "../../security/token-manager.js";
14
+ import type { OAuthConnection } from "../../oauth/connection.js";
15
+ import { resolveOAuthConnection } from "../../oauth/connection-resolver.js";
16
+ import { GOOGLE_CALENDAR_BASE_URL } from "../../oauth/provider-base-urls.js";
15
17
  import { getLogger } from "../../util/logger.js";
16
18
  import type {
17
19
  FetchResult,
@@ -21,8 +23,6 @@ import type {
21
23
 
22
24
  const log = getLogger("watcher:google-calendar");
23
25
 
24
- const CALENDAR_API_BASE = "https://www.googleapis.com/calendar/v3";
25
-
26
26
  /** The credential service — calendar shares OAuth tokens with Gmail. */
27
27
  const CREDENTIAL_SERVICE = "integration:gmail";
28
28
 
@@ -69,7 +69,7 @@ interface SyncResponse {
69
69
  * Returns all accumulated events and the final nextSyncToken.
70
70
  */
71
71
  async function incrementalSync(
72
- token: string,
72
+ connection: OAuthConnection,
73
73
  syncToken: string,
74
74
  ): Promise<SyncResponse> {
75
75
  let allItems: CalendarEvent[] = [];
@@ -77,27 +77,32 @@ async function incrementalSync(
77
77
  let nextSyncToken: string | undefined;
78
78
 
79
79
  do {
80
- const params = new URLSearchParams({ syncToken });
81
- if (pageToken) params.set("pageToken", pageToken);
82
- const url = `${CALENDAR_API_BASE}/calendars/primary/events?${params}`;
83
- const resp = await fetch(url, {
84
- headers: { Authorization: `Bearer ${token}` },
80
+ const query: Record<string, string> = { syncToken };
81
+ if (pageToken) query.pageToken = pageToken;
82
+
83
+ const resp = await connection.request({
84
+ method: "GET",
85
+ path: "/calendars/primary/events",
86
+ query,
87
+ baseUrl: GOOGLE_CALENDAR_BASE_URL,
85
88
  });
86
89
 
87
- if (!resp.ok) {
88
- const body = await resp.text().catch(() => "");
90
+ if (resp.status < 200 || resp.status >= 300) {
91
+ const bodyStr =
92
+ typeof resp.body === "string"
93
+ ? resp.body
94
+ : JSON.stringify(resp.body ?? "");
89
95
  if (resp.status === 410) {
90
- throw new SyncTokenExpiredError(body);
96
+ throw new SyncTokenExpiredError(bodyStr);
91
97
  }
92
- // Throw CalendarApiError so withValidToken can detect 401s via the status property
93
98
  throw new CalendarApiError(
94
99
  resp.status,
95
- resp.statusText,
96
- `Calendar Sync API ${resp.status}: ${body}`,
100
+ "",
101
+ `Calendar Sync API ${resp.status}: ${bodyStr}`,
97
102
  );
98
103
  }
99
104
 
100
- const page = (await resp.json()) as SyncResponse;
105
+ const page = resp.body as SyncResponse;
101
106
  if (page.items) allItems = allItems.concat(page.items);
102
107
  pageToken = page.nextPageToken;
103
108
  nextSyncToken = page.nextSyncToken;
@@ -119,16 +124,48 @@ export const googleCalendarProvider: WatcherProvider = {
119
124
  requiredCredentialService: CREDENTIAL_SERVICE,
120
125
 
121
126
  async getInitialWatermark(credentialService: string): Promise<string> {
122
- return withValidToken(credentialService, async (token) => {
123
- // Do a full sync with a narrow window to get the initial syncToken.
124
- // The API may paginate even for small result sets, so follow nextPageToken
125
- // until we reach the final page that carries the nextSyncToken.
127
+ const connection = resolveOAuthConnection(credentialService);
128
+
129
+ // Do a full sync with a narrow window to get the initial syncToken.
130
+ // The API may paginate even for small result sets, so follow nextPageToken
131
+ // until we reach the final page that carries the nextSyncToken.
132
+ const now = new Date().toISOString();
133
+ let pageToken: string | undefined;
134
+ let syncToken: string | undefined;
135
+
136
+ do {
137
+ const result = await listEvents(connection, "primary", {
138
+ timeMin: now,
139
+ maxResults: 250,
140
+ singleEvents: true,
141
+ pageToken,
142
+ });
143
+ syncToken = result.nextSyncToken;
144
+ pageToken = result.nextPageToken;
145
+ } while (pageToken && !syncToken);
146
+
147
+ if (!syncToken) {
148
+ throw new Error("Calendar API did not return a syncToken");
149
+ }
150
+ return syncToken;
151
+ },
152
+
153
+ async fetchNew(
154
+ credentialService: string,
155
+ watermark: string | null,
156
+ _config: Record<string, unknown>,
157
+ _watcherKey: string,
158
+ ): Promise<FetchResult> {
159
+ const connection = resolveOAuthConnection(credentialService);
160
+
161
+ if (!watermark) {
162
+ // No watermark — paginate through to get the initial syncToken, return no items
126
163
  const now = new Date().toISOString();
127
164
  let pageToken: string | undefined;
128
165
  let syncToken: string | undefined;
129
166
 
130
167
  do {
131
- const result = await listEvents(token, "primary", {
168
+ const result = await listEvents(connection, "primary", {
132
169
  timeMin: now,
133
170
  maxResults: 250,
134
171
  singleEvents: true,
@@ -138,82 +175,52 @@ export const googleCalendarProvider: WatcherProvider = {
138
175
  pageToken = result.nextPageToken;
139
176
  } while (pageToken && !syncToken);
140
177
 
141
- if (!syncToken) {
142
- throw new Error("Calendar API did not return a syncToken");
178
+ return { items: [], watermark: syncToken ?? "" };
179
+ }
180
+
181
+ try {
182
+ const syncResp = await incrementalSync(connection, watermark);
183
+ const newWatermark = syncResp.nextSyncToken ?? watermark;
184
+
185
+ if (!syncResp.items || syncResp.items.length === 0) {
186
+ return { items: [], watermark: newWatermark };
143
187
  }
144
- return syncToken;
145
- });
146
- },
147
188
 
148
- async fetchNew(
149
- credentialService: string,
150
- watermark: string | null,
151
- _config: Record<string, unknown>,
152
- _watcherKey: string,
153
- ): Promise<FetchResult> {
154
- return withValidToken(credentialService, async (token) => {
155
- if (!watermark) {
156
- // No watermark — paginate through to get the initial syncToken, return no items
157
- const now = new Date().toISOString();
158
- let pageToken: string | undefined;
159
- let syncToken: string | undefined;
160
-
161
- do {
162
- const result = await listEvents(token, "primary", {
163
- timeMin: now,
164
- maxResults: 250,
165
- singleEvents: true,
166
- pageToken,
167
- });
168
- syncToken = result.nextSyncToken;
169
- pageToken = result.nextPageToken;
170
- } while (pageToken && !syncToken);
171
-
172
- return { items: [], watermark: syncToken ?? "" };
189
+ // Convert events to watcher items, distinguishing new vs updated
190
+ const items: WatcherItem[] = [];
191
+ for (const event of syncResp.items) {
192
+ if (event.status === "cancelled") continue;
193
+
194
+ const eventType =
195
+ event.created === event.updated
196
+ ? "new_calendar_event"
197
+ : "updated_calendar_event";
198
+ items.push(eventToItem(event, eventType));
173
199
  }
174
200
 
175
- try {
176
- const syncResp = await incrementalSync(token, watermark);
177
- const newWatermark = syncResp.nextSyncToken ?? watermark;
178
-
179
- if (!syncResp.items || syncResp.items.length === 0) {
180
- return { items: [], watermark: newWatermark };
181
- }
182
-
183
- // Convert events to watcher items, distinguishing new vs updated
184
- const items: WatcherItem[] = [];
185
- for (const event of syncResp.items) {
186
- if (event.status === "cancelled") continue;
187
-
188
- const eventType =
189
- event.created === event.updated
190
- ? "new_calendar_event"
191
- : "updated_calendar_event";
192
- items.push(eventToItem(event, eventType));
193
- }
194
-
195
- log.info(
196
- { count: items.length, watermark: newWatermark },
197
- "Calendar: fetched event changes",
198
- );
199
- return { items, watermark: newWatermark };
200
- } catch (err) {
201
- if (err instanceof SyncTokenExpiredError) {
202
- log.warn("Calendar syncToken expired, falling back to recent events");
203
- return fallbackFetch(token);
204
- }
205
- throw err;
201
+ log.info(
202
+ { count: items.length, watermark: newWatermark },
203
+ "Calendar: fetched event changes",
204
+ );
205
+ return { items, watermark: newWatermark };
206
+ } catch (err) {
207
+ if (err instanceof SyncTokenExpiredError) {
208
+ log.warn("Calendar syncToken expired, falling back to recent events");
209
+ return fallbackFetch(connection);
206
210
  }
207
- });
211
+ throw err;
212
+ }
208
213
  },
209
214
  };
210
215
 
211
216
  /**
212
217
  * Fallback when syncToken expires: list upcoming events from today.
213
218
  */
214
- async function fallbackFetch(token: string): Promise<FetchResult> {
219
+ async function fallbackFetch(
220
+ connection: OAuthConnection,
221
+ ): Promise<FetchResult> {
215
222
  const now = new Date().toISOString();
216
- const result = await listEvents(token, "primary", {
223
+ const result = await listEvents(connection, "primary", {
217
224
  timeMin: now,
218
225
  maxResults: 25,
219
226
  singleEvents: true,
@@ -229,7 +236,7 @@ async function fallbackFetch(token: string): Promise<FetchResult> {
229
236
  let syncToken: string | undefined;
230
237
 
231
238
  do {
232
- const syncResult = await listEvents(token, "primary", {
239
+ const syncResult = await listEvents(connection, "primary", {
233
240
  timeMin: now,
234
241
  maxResults: 250,
235
242
  singleEvents: true,