@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.
- package/ARCHITECTURE.md +6 -6
- package/docs/architecture/memory.md +1 -1
- package/docs/architecture/scheduling.md +2 -3
- package/docs/architecture/security.md +5 -5
- package/docs/trusted-contact-access.md +5 -6
- package/package.json +4 -1
- package/src/__tests__/avatar-e2e.test.ts +18 -219
- package/src/__tests__/avatar-generator.test.ts +5 -57
- package/src/__tests__/browser-fill-credential.test.ts +5 -2
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +2 -1
- package/src/__tests__/channel-readiness-routes.test.ts +20 -19
- package/src/__tests__/cli.test.ts +23 -0
- package/src/__tests__/credential-broker-browser-fill.test.ts +23 -22
- package/src/__tests__/credential-broker-server-use.test.ts +22 -21
- package/src/__tests__/credential-broker.test.ts +2 -1
- package/src/__tests__/credential-metadata-store.test.ts +240 -18
- package/src/__tests__/credential-resolve.test.ts +5 -4
- package/src/__tests__/credential-security-e2e.test.ts +8 -8
- package/src/__tests__/credential-security-invariants.test.ts +104 -7
- package/src/__tests__/credential-vault-unit.test.ts +22 -20
- package/src/__tests__/credential-vault.test.ts +284 -12
- package/src/__tests__/credentials-cli.test.ts +11 -6
- package/src/__tests__/gateway-only-enforcement.test.ts +4 -2
- package/src/__tests__/gemini-image-service.test.ts +75 -45
- package/src/__tests__/gemini-provider.test.ts +9 -6
- package/src/__tests__/guardian-action-conversation-turn.test.ts +1 -33
- package/src/__tests__/guardian-action-copy-generator.test.ts +0 -20
- package/src/__tests__/guardian-action-followup-executor.test.ts +1 -28
- package/src/__tests__/guardian-action-followup-store.test.ts +1 -1
- package/src/__tests__/guardian-grant-minting.test.ts +35 -0
- package/src/__tests__/integration-status.test.ts +53 -21
- package/src/__tests__/managed-proxy-context.test.ts +5 -3
- package/src/__tests__/media-generate-image.test.ts +63 -2
- package/src/__tests__/media-reuse-story.e2e.test.ts +7 -3
- package/src/__tests__/messaging-send-tool.test.ts +4 -6
- package/src/__tests__/provider-fail-open-selection.test.ts +3 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +70 -6
- package/src/__tests__/schedule-store.test.ts +1 -1
- package/src/__tests__/schema-transforms.test.ts +226 -0
- package/src/__tests__/script-proxy-injection-runtime.test.ts +23 -13
- package/src/__tests__/script-proxy-policy-runtime.test.ts +1 -1
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -1
- package/src/__tests__/secret-onetime-send.test.ts +5 -3
- package/src/__tests__/session-messaging-secret-redirect.test.ts +5 -4
- package/src/__tests__/skills-uninstall.test.ts +2 -2
- package/src/__tests__/skills.test.ts +0 -9
- package/src/__tests__/slack-channel-config.test.ts +9 -8
- package/src/__tests__/slack-share-routes.test.ts +11 -6
- package/src/__tests__/telegram-bot-username-resolution.test.ts +3 -0
- package/src/__tests__/twilio-config.test.ts +2 -1
- package/src/__tests__/twilio-provider.test.ts +4 -2
- package/src/__tests__/twilio-routes.test.ts +5 -4
- package/src/__tests__/verification-control-plane-policy.test.ts +1 -1
- package/src/approvals/AGENTS.md +1 -1
- package/src/calls/call-domain.ts +7 -4
- package/src/calls/twilio-config.ts +2 -1
- package/src/calls/twilio-provider.ts +2 -1
- package/src/calls/twilio-rest.ts +2 -2
- package/src/cli/commands/browser-relay.ts +40 -15
- package/src/cli/commands/credentials.ts +9 -8
- package/src/cli/commands/oauth.ts +1 -1
- package/src/cli.ts +3 -2
- package/src/config/bundled-skills/claude-code/TOOLS.json +0 -4
- package/src/config/bundled-skills/contacts/tools/google-contacts.ts +29 -32
- package/src/config/bundled-skills/gmail/SKILL.md +4 -4
- package/src/config/bundled-skills/gmail/tools/gmail-archive.ts +54 -61
- package/src/config/bundled-skills/gmail/tools/gmail-attachments.ts +25 -28
- package/src/config/bundled-skills/gmail/tools/gmail-draft.ts +14 -17
- package/src/config/bundled-skills/gmail/tools/gmail-filters.ts +39 -44
- package/src/config/bundled-skills/gmail/tools/gmail-follow-up.ts +61 -58
- package/src/config/bundled-skills/gmail/tools/gmail-forward.ts +50 -49
- package/src/config/bundled-skills/gmail/tools/gmail-label.ts +11 -13
- package/src/config/bundled-skills/gmail/tools/gmail-outreach-scan.ts +148 -146
- package/src/config/bundled-skills/gmail/tools/gmail-send-draft.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +175 -173
- package/src/config/bundled-skills/gmail/tools/gmail-trash.ts +4 -7
- package/src/config/bundled-skills/gmail/tools/gmail-unsubscribe.ts +71 -76
- package/src/config/bundled-skills/gmail/tools/gmail-vacation.ts +32 -38
- package/src/config/bundled-skills/google-calendar/SKILL.md +2 -2
- package/src/config/bundled-skills/google-calendar/calendar-client.ts +70 -29
- package/src/config/bundled-skills/google-calendar/tools/calendar-check-availability.ts +9 -10
- package/src/config/bundled-skills/google-calendar/tools/calendar-create-event.ts +5 -6
- package/src/config/bundled-skills/google-calendar/tools/calendar-get-event.ts +4 -5
- package/src/config/bundled-skills/google-calendar/tools/calendar-list-events.ts +14 -15
- package/src/config/bundled-skills/google-calendar/tools/calendar-rsvp.ts +37 -37
- package/src/config/bundled-skills/google-calendar/tools/shared.ts +4 -9
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +24 -3
- package/src/config/bundled-skills/messaging/SKILL.md +6 -6
- package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +62 -63
- package/src/config/bundled-skills/messaging/tools/messaging-archive-by-sender.ts +15 -16
- package/src/config/bundled-skills/messaging/tools/messaging-auth-test.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-list-conversations.ts +6 -7
- package/src/config/bundled-skills/messaging/tools/messaging-mark-read.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-read.ts +14 -15
- package/src/config/bundled-skills/messaging/tools/messaging-search.ts +4 -5
- package/src/config/bundled-skills/messaging/tools/messaging-send.ts +128 -128
- package/src/config/bundled-skills/messaging/tools/messaging-sender-digest.ts +33 -34
- package/src/config/bundled-skills/messaging/tools/shared.ts +11 -11
- package/src/config/bundled-skills/notifications/SKILL.md +1 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +5 -5
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/skill-management/SKILL.md +1 -1
- package/src/config/bundled-skills/slack/tools/shared.ts +4 -10
- package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +15 -16
- package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +4 -5
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +95 -92
- package/src/config/loader.ts +6 -0
- package/src/daemon/computer-use-session.ts +7 -1
- package/src/daemon/guardian-action-generators.ts +4 -5
- package/src/daemon/handlers/config-slack-channel.ts +37 -20
- package/src/daemon/handlers/config-telegram.ts +33 -20
- package/src/daemon/lifecycle.ts +9 -1
- package/src/daemon/message-types/integrations.ts +1 -0
- package/src/daemon/ride-shotgun-handler.ts +3 -1
- package/src/daemon/session-messaging.ts +3 -1
- package/src/daemon/session-tool-setup.ts +18 -2
- package/src/daemon/session.ts +1 -1
- package/src/email/providers/index.ts +2 -1
- package/src/instrument.ts +15 -1
- package/src/media/app-icon-generator.ts +30 -4
- package/src/media/avatar-router.ts +28 -62
- package/src/media/gemini-image-service.ts +28 -2
- package/src/memory/canonical-guardian-store.ts +1 -1
- package/src/memory/guardian-action-store.ts +1 -1
- package/src/memory/schema/guardian.ts +1 -1
- package/src/messaging/provider.ts +16 -10
- package/src/messaging/providers/gmail/adapter.ts +40 -23
- package/src/messaging/providers/gmail/client.ts +203 -122
- package/src/messaging/providers/gmail/people-client.ts +26 -18
- package/src/messaging/providers/slack/adapter.ts +29 -19
- package/src/messaging/providers/slack/client.ts +265 -78
- package/src/messaging/providers/telegram-bot/adapter.ts +5 -4
- package/src/messaging/providers/whatsapp/adapter.ts +6 -3
- package/src/messaging/registry.ts +2 -1
- package/src/oauth/byo-connection.test.ts +436 -0
- package/src/oauth/byo-connection.ts +112 -0
- package/src/oauth/connect-orchestrator.ts +27 -0
- package/src/oauth/connection-resolver.ts +34 -0
- package/src/oauth/connection.ts +38 -0
- package/src/oauth/platform-connection.test.ts +163 -0
- package/src/oauth/platform-connection.ts +110 -0
- package/src/oauth/provider-base-urls.ts +21 -0
- package/src/oauth/provider-profiles.ts +1 -1
- package/src/oauth/token-persistence.ts +20 -20
- package/src/permissions/checker.ts +6 -1
- package/src/prompts/system-prompt.ts +52 -15
- package/src/prompts/templates/BOOTSTRAP.md +1 -1
- package/src/providers/gemini/client.ts +15 -6
- package/src/providers/managed-proxy/constants.ts +2 -2
- package/src/providers/managed-proxy/context.ts +5 -1
- package/src/providers/ratelimit.ts +17 -0
- package/src/providers/registry.ts +2 -2
- package/src/runtime/AGENTS.md +18 -1
- package/src/runtime/auth/route-policy.ts +1 -0
- package/src/runtime/channel-invite-transports/telegram.ts +2 -1
- package/src/runtime/channel-readiness-service.ts +168 -195
- package/src/runtime/channel-readiness-types.ts +4 -0
- package/src/runtime/guardian-action-conversation-turn.ts +1 -3
- package/src/runtime/guardian-action-followup-executor.ts +1 -2
- package/src/runtime/guardian-action-message-composer.ts +3 -23
- package/src/runtime/http-server.ts +9 -4
- package/src/runtime/http-types.ts +0 -1
- package/src/runtime/middleware/rate-limiter.ts +74 -20
- package/src/runtime/middleware/twilio-validation.ts +1 -3
- package/src/runtime/routes/channel-readiness-routes.ts +2 -0
- package/src/runtime/routes/diagnostics-routes.ts +11 -9
- package/src/runtime/routes/guardian-approval-interception.ts +20 -5
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +71 -25
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +12 -5
- package/src/runtime/routes/integrations/slack/share.ts +3 -2
- package/src/runtime/routes/integrations/twilio.ts +6 -5
- package/src/runtime/routes/secret-routes.ts +3 -2
- package/src/runtime/routes/settings-routes.ts +75 -17
- package/src/runtime/telegram-streaming-delivery.test.ts +132 -0
- package/src/runtime/telegram-streaming-delivery.ts +11 -1
- package/src/schedule/integration-status.ts +5 -4
- package/src/security/credential-key.ts +170 -0
- package/src/security/token-manager.ts +36 -7
- package/src/tools/apps/definitions.ts +0 -5
- package/src/tools/assets/materialize.ts +0 -5
- package/src/tools/assets/search.ts +0 -5
- package/src/tools/browser/headless-browser.ts +1 -67
- package/src/tools/claude-code/claude-code.ts +0 -5
- package/src/tools/computer-use/request-computer-control.ts +0 -5
- package/src/tools/credentials/broker.ts +6 -4
- package/src/tools/credentials/metadata-store.ts +72 -20
- package/src/tools/credentials/resolve.ts +2 -1
- package/src/tools/credentials/vault.ts +77 -16
- package/src/tools/filesystem/edit.ts +1 -6
- package/src/tools/filesystem/read.ts +0 -5
- package/src/tools/filesystem/write.ts +1 -6
- package/src/tools/host-filesystem/edit.ts +1 -6
- package/src/tools/host-filesystem/read.ts +1 -6
- package/src/tools/host-filesystem/write.ts +1 -6
- package/src/tools/mcp/mcp-tool-factory.ts +18 -1
- package/src/tools/memory/definitions.ts +0 -5
- package/src/tools/network/web-fetch.ts +0 -5
- package/src/tools/network/web-search.ts +0 -5
- package/src/tools/schema-transforms.ts +99 -0
- package/src/tools/skills/load.ts +0 -5
- package/src/tools/swarm/delegate.ts +0 -5
- package/src/tools/system/avatar-generator.ts +3 -44
- package/src/tools/ui-surface/definitions.ts +0 -15
- package/src/tools/watch/screen-watch.ts +0 -5
- package/src/version.ts +10 -0
- package/src/watcher/providers/github.ts +51 -52
- package/src/watcher/providers/gmail.ts +88 -80
- package/src/watcher/providers/google-calendar.ts +93 -86
- package/src/watcher/providers/linear.ts +87 -93
- package/src/__tests__/avatar-router.test.ts +0 -149
- package/src/__tests__/managed-avatar-client.test.ts +0 -337
- package/src/config/bundled-skills/doordash/SKILL.md +0 -170
- package/src/config/bundled-skills/doordash/__tests__/doordash-client.test.ts +0 -205
- package/src/config/bundled-skills/doordash/__tests__/doordash-session.test.ts +0 -74
- package/src/config/bundled-skills/doordash/doordash-cli.ts +0 -1081
- package/src/config/bundled-skills/doordash/doordash-entry.ts +0 -22
- package/src/config/bundled-skills/doordash/lib/cart-queries.ts +0 -787
- package/src/config/bundled-skills/doordash/lib/client.ts +0 -1069
- package/src/config/bundled-skills/doordash/lib/order-queries.ts +0 -85
- package/src/config/bundled-skills/doordash/lib/queries.ts +0 -28
- package/src/config/bundled-skills/doordash/lib/query-extractor.ts +0 -94
- package/src/config/bundled-skills/doordash/lib/search-queries.ts +0 -203
- package/src/config/bundled-skills/doordash/lib/session.ts +0 -96
- package/src/config/bundled-skills/doordash/lib/shared/errors.ts +0 -61
- package/src/config/bundled-skills/doordash/lib/shared/network-recorder.ts +0 -380
- package/src/config/bundled-skills/doordash/lib/shared/platform.ts +0 -55
- package/src/config/bundled-skills/doordash/lib/shared/recording-store.ts +0 -43
- package/src/config/bundled-skills/doordash/lib/shared/recording-types.ts +0 -49
- package/src/config/bundled-skills/doordash/lib/shared/truncate.ts +0 -6
- package/src/config/bundled-skills/doordash/lib/store-queries.ts +0 -246
- package/src/config/bundled-skills/doordash/lib/types.ts +0 -367
- package/src/media/avatar-types.ts +0 -53
- 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 {
|
|
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
|
-
|
|
74
|
+
connection: OAuthConnection,
|
|
75
75
|
startHistoryId: string,
|
|
76
76
|
): Promise<HistoryListResponse> {
|
|
77
|
-
const
|
|
77
|
+
const query: Record<string, string> = {
|
|
78
78
|
startHistoryId,
|
|
79
79
|
historyTypes: "messageAdded",
|
|
80
80
|
maxResults: "100",
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const resp = await
|
|
84
|
-
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const resp = await connection.request({
|
|
84
|
+
method: "GET",
|
|
85
|
+
path: "/history",
|
|
86
|
+
query,
|
|
85
87
|
});
|
|
86
88
|
|
|
87
|
-
if (
|
|
88
|
-
const body =
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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.
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
143
|
+
try {
|
|
144
|
+
const historyResp = await fetchHistory(connection, watermark);
|
|
145
|
+
const newWatermark = historyResp.historyId ?? watermark;
|
|
141
146
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
153
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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(
|
|
193
|
-
|
|
194
|
-
|
|
200
|
+
async function fallbackFetch(
|
|
201
|
+
connection: OAuthConnection,
|
|
202
|
+
): Promise<FetchResult> {
|
|
195
203
|
const listResp = await listMessages(
|
|
196
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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 {
|
|
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
|
-
|
|
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
|
|
81
|
-
if (pageToken)
|
|
82
|
-
|
|
83
|
-
const resp = await
|
|
84
|
-
|
|
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 (
|
|
88
|
-
const
|
|
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(
|
|
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
|
-
|
|
96
|
-
`Calendar Sync API ${resp.status}: ${
|
|
100
|
+
"",
|
|
101
|
+
`Calendar Sync API ${resp.status}: ${bodyStr}`,
|
|
97
102
|
);
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
const page =
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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(
|
|
219
|
+
async function fallbackFetch(
|
|
220
|
+
connection: OAuthConnection,
|
|
221
|
+
): Promise<FetchResult> {
|
|
215
222
|
const now = new Date().toISOString();
|
|
216
|
-
const result = await listEvents(
|
|
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(
|
|
239
|
+
const syncResult = await listEvents(connection, "primary", {
|
|
233
240
|
timeMin: now,
|
|
234
241
|
maxResults: 250,
|
|
235
242
|
singleEvents: true,
|