surface-cli 0.1.1 → 0.3.0

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 (41) hide show
  1. package/README.md +42 -15
  2. package/dist/cli.js +236 -21
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config.js +2 -2
  5. package/dist/config.js.map +1 -1
  6. package/dist/contracts/account.js +8 -0
  7. package/dist/contracts/account.js.map +1 -1
  8. package/dist/e2e/gmail-v1.js +31 -0
  9. package/dist/e2e/gmail-v1.js.map +1 -1
  10. package/dist/e2e/outlook-v1.js +11 -0
  11. package/dist/e2e/outlook-v1.js.map +1 -1
  12. package/dist/lib/remote-auth.js +18 -2
  13. package/dist/lib/remote-auth.js.map +1 -1
  14. package/dist/lib/stored-mail.js +69 -0
  15. package/dist/lib/stored-mail.js.map +1 -0
  16. package/dist/paths.js +2 -0
  17. package/dist/paths.js.map +1 -1
  18. package/dist/providers/gmail/adapter.js +336 -53
  19. package/dist/providers/gmail/adapter.js.map +1 -1
  20. package/dist/providers/gmail/api.js +68 -0
  21. package/dist/providers/gmail/api.js.map +1 -1
  22. package/dist/providers/gmail/normalize.js.map +1 -1
  23. package/dist/providers/gmail/oauth.js +4 -0
  24. package/dist/providers/gmail/oauth.js.map +1 -1
  25. package/dist/providers/outlook/adapter.js +185 -97
  26. package/dist/providers/outlook/adapter.js.map +1 -1
  27. package/dist/providers/outlook/extract.js +7 -0
  28. package/dist/providers/outlook/extract.js.map +1 -1
  29. package/dist/providers/shared/inline-attachments.js +17 -0
  30. package/dist/providers/shared/inline-attachments.js.map +1 -0
  31. package/dist/refs.js +3 -0
  32. package/dist/refs.js.map +1 -1
  33. package/dist/session-daemon.js +218 -0
  34. package/dist/session-daemon.js.map +1 -0
  35. package/dist/session.js +283 -0
  36. package/dist/session.js.map +1 -0
  37. package/dist/state/database.js +542 -8
  38. package/dist/state/database.js.map +1 -1
  39. package/dist/summarizer.js +259 -76
  40. package/dist/summarizer.js.map +1 -1
  41. package/package.json +1 -1
@@ -1,11 +1,12 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
- import { SurfaceError, notImplemented } from "../../lib/errors.js";
4
+ import { SurfaceError } from "../../lib/errors.js";
5
5
  import { assertWriteAllowed } from "../../lib/write-safety.js";
6
6
  import { makeAttachmentId, makeMessageRef, makeThreadRef } from "../../refs.js";
7
- import { summarizeThread } from "../../summarizer.js";
8
- import { createGmailDraft, downloadGmailAttachmentBytes, getGmailThread, listGmailThreads, modifyGmailMessage, modifyGmailThread, sendGmailRawMessage, } from "./api.js";
7
+ import { summarizeAndPersistThreads } from "../../summarizer.js";
8
+ import { annotateBodyWithInlineAttachments } from "../shared/inline-attachments.js";
9
+ import { createGmailDraft, downloadGmailAttachmentBytes, getGoogleCalendarEvent, getGmailThread, listGmailThreads, listGoogleCalendarEventsByIcalUid, modifyGmailMessage, modifyGmailThread, patchGoogleCalendarEvent, sendGmailRawMessage, } from "./api.js";
9
10
  import { clearGmailAuthState, gmailAuthStatus, runGmailLogin } from "./oauth.js";
10
11
  import { decodeBase64UrlBytes, decodePartData, headerDateToIso, headerIndex, internalDateToIso, isCalendarPart, iterParts, normalizeGmailBody, parseCalendarInvite, parseMailbox, parseMailboxes, } from "./normalize.js";
11
12
  function sourceInfo(account) {
@@ -76,6 +77,88 @@ function gmailMailbox(labels) {
76
77
  }
77
78
  return "archive";
78
79
  }
80
+ function quoteGmailSearchValue(value) {
81
+ const normalized = value.trim();
82
+ if (!normalized) {
83
+ return '""';
84
+ }
85
+ return /[\s"]/u.test(normalized) ? `"${normalized.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : normalized;
86
+ }
87
+ function gmailMailboxSearchOperator(mailbox) {
88
+ const normalized = mailbox.trim().toLowerCase();
89
+ switch (normalized) {
90
+ case "drafts":
91
+ return "in:drafts";
92
+ case "sent":
93
+ return "in:sent";
94
+ case "trash":
95
+ return "in:trash";
96
+ case "spam":
97
+ return "in:spam";
98
+ case "inbox":
99
+ return "in:inbox";
100
+ case "archive":
101
+ return "in:archive";
102
+ default:
103
+ return `in:${quoteGmailSearchValue(normalized)}`;
104
+ }
105
+ }
106
+ function gmailLabelSearchOperator(label) {
107
+ const normalized = normalizeLabel(label);
108
+ switch (normalized) {
109
+ case "unread":
110
+ return "is:unread";
111
+ case "read":
112
+ return "is:read";
113
+ case "starred":
114
+ return "is:starred";
115
+ case "important":
116
+ return "label:important";
117
+ case "inbox":
118
+ case "sent":
119
+ case "drafts":
120
+ case "trash":
121
+ case "spam":
122
+ case "archive":
123
+ return gmailMailboxSearchOperator(normalized);
124
+ default:
125
+ return `label:${quoteGmailSearchValue(normalized)}`;
126
+ }
127
+ }
128
+ function buildGmailSearchQuery(query) {
129
+ const parts = [];
130
+ if (query.text?.trim()) {
131
+ parts.push(query.text.trim());
132
+ }
133
+ if (query.from?.trim()) {
134
+ parts.push(`from:${quoteGmailSearchValue(query.from)}`);
135
+ }
136
+ if (query.subject?.trim()) {
137
+ parts.push(`subject:${quoteGmailSearchValue(query.subject)}`);
138
+ }
139
+ if (query.mailbox?.trim()) {
140
+ parts.push(gmailMailboxSearchOperator(query.mailbox));
141
+ }
142
+ for (const label of query.labels ?? []) {
143
+ if (!label.trim()) {
144
+ continue;
145
+ }
146
+ parts.push(gmailLabelSearchOperator(label));
147
+ }
148
+ return parts.length > 0 ? parts.join(" ").trim() : undefined;
149
+ }
150
+ function threadMatchesStructuredFilters(thread, query) {
151
+ if (query.mailbox?.trim() && thread.envelope.mailbox.trim().toLowerCase() !== query.mailbox.trim().toLowerCase()) {
152
+ return false;
153
+ }
154
+ if ((query.labels?.length ?? 0) > 0) {
155
+ const available = new Set(thread.envelope.labels.map((label) => label.trim().toLowerCase()));
156
+ if ((query.labels ?? []).some((label) => !available.has(label.trim().toLowerCase()))) {
157
+ return false;
158
+ }
159
+ }
160
+ return true;
161
+ }
79
162
  function partSizeBytes(part) {
80
163
  if (typeof part.body?.size === "number") {
81
164
  return part.body.size;
@@ -152,6 +235,126 @@ function mapCalendarPartstat(value) {
152
235
  return null;
153
236
  }
154
237
  }
238
+ function mapGoogleCalendarResponseStatus(value) {
239
+ const normalized = (value ?? "").trim().toLowerCase();
240
+ switch (normalized) {
241
+ case "accepted":
242
+ return "accept";
243
+ case "declined":
244
+ return "decline";
245
+ case "tentative":
246
+ return "tentative";
247
+ case "needsaction":
248
+ return "needs_response";
249
+ default:
250
+ return null;
251
+ }
252
+ }
253
+ function googleCalendarResponseStatusForRsvp(response) {
254
+ switch (response) {
255
+ case "accept":
256
+ return "accepted";
257
+ case "decline":
258
+ return "declined";
259
+ case "tentative":
260
+ return "tentative";
261
+ }
262
+ }
263
+ function googleCalendarEventStart(event) {
264
+ return event.start?.dateTime ?? event.start?.date ?? null;
265
+ }
266
+ function normalizeComparableDatePrefix(value) {
267
+ if (!value) {
268
+ return null;
269
+ }
270
+ const match = value.match(/^(\d{4}-\d{2}-\d{2})/);
271
+ return match?.[1] ?? null;
272
+ }
273
+ function chooseGoogleCalendarEvent(events, meetingStart) {
274
+ if (events.length === 0) {
275
+ return null;
276
+ }
277
+ const targetDate = normalizeComparableDatePrefix(meetingStart);
278
+ if (targetDate) {
279
+ const match = events.find((event) => normalizeComparableDatePrefix(googleCalendarEventStart(event)) === targetDate);
280
+ if (match) {
281
+ return match;
282
+ }
283
+ }
284
+ return events[0] ?? null;
285
+ }
286
+ function pickCalendarAttendee(event, attendeeEmail, fallbackEmail) {
287
+ const normalizedTarget = (attendeeEmail ?? fallbackEmail).trim().toLowerCase();
288
+ const normalizedFallback = fallbackEmail.trim().toLowerCase();
289
+ const attendees = event.attendees ?? [];
290
+ const chosen = attendees.find((attendee) => attendee.self === true)
291
+ ?? attendees.find((attendee) => (attendee.email ?? "").trim().toLowerCase() === normalizedTarget)
292
+ ?? attendees.find((attendee) => (attendee.email ?? "").trim().toLowerCase() === normalizedFallback);
293
+ if (!chosen?.email) {
294
+ return normalizedTarget
295
+ ? { email: normalizedTarget, response_status: null }
296
+ : null;
297
+ }
298
+ return {
299
+ email: chosen.email,
300
+ response_status: mapGoogleCalendarResponseStatus(chosen.responseStatus),
301
+ };
302
+ }
303
+ async function hydrateGmailInviteMetadataFromCalendar(account, context, metadata) {
304
+ if (!metadata.calendar_uid) {
305
+ return metadata;
306
+ }
307
+ try {
308
+ const events = await listGoogleCalendarEventsByIcalUid(account, context, "primary", metadata.calendar_uid);
309
+ const event = chooseGoogleCalendarEvent(events, metadata.meeting_start);
310
+ if (!event) {
311
+ return metadata;
312
+ }
313
+ const attendee = pickCalendarAttendee(event, metadata.attendee_email, account.email);
314
+ return {
315
+ ...metadata,
316
+ attendee_email: attendee?.email ?? metadata.attendee_email,
317
+ invite: {
318
+ ...metadata.invite,
319
+ rsvp_supported: true,
320
+ response_status: attendee?.response_status ?? metadata.invite.response_status,
321
+ available_rsvp_responses: ["accept", "decline", "tentative"],
322
+ },
323
+ };
324
+ }
325
+ catch (error) {
326
+ if (error instanceof SurfaceError && (error.code === "reauth_required" || error.code === "transport_error")) {
327
+ return metadata;
328
+ }
329
+ throw error;
330
+ }
331
+ }
332
+ async function extractGmailInviteMetadata(account, context, message, participants) {
333
+ const calendarText = await extractCalendarText(account, context, message);
334
+ if (!calendarText) {
335
+ return null;
336
+ }
337
+ const parsedInvite = parseCalendarInvite(calendarText, {
338
+ mailboxEmail: account.email,
339
+ recipientEmails: [...participants.to, ...participants.cc].map((mailbox) => mailbox.email),
340
+ });
341
+ const meeting = parsedInvite.meeting;
342
+ if (!meeting) {
343
+ return null;
344
+ }
345
+ const metadata = {
346
+ invite: {
347
+ is_invite: true,
348
+ rsvp_supported: Boolean(meeting.uid),
349
+ response_status: mapCalendarPartstat(meeting.response_type),
350
+ available_rsvp_responses: meeting.uid ? ["accept", "decline", "tentative"] : [],
351
+ },
352
+ calendar_uid: meeting.uid ?? null,
353
+ attendee_email: meeting.attendee?.email ?? null,
354
+ meeting_start: meeting.start ?? null,
355
+ };
356
+ return hydrateGmailInviteMetadataFromCalendar(account, context, metadata);
357
+ }
155
358
  async function normalizeGmailMessage(account, context, message) {
156
359
  const indexedHeaders = headerIndex(message.payload?.headers);
157
360
  const subject = indexedHeaders.subject ?? "";
@@ -163,22 +366,9 @@ async function normalizeGmailMessage(account, context, message) {
163
366
  const unread = (message.labelIds ?? []).includes("UNREAD");
164
367
  const body = normalizeGmailBody(message.payload, message.snippet ?? "");
165
368
  const attachments = extractAttachmentRecords(message);
166
- let invite;
167
- const calendarText = await extractCalendarText(account, context, message);
168
- if (calendarText) {
169
- const parsedInvite = parseCalendarInvite(calendarText, {
170
- mailboxEmail: account.email,
171
- recipientEmails: [...to, ...cc].map((mailbox) => mailbox.email),
172
- });
173
- if (parsedInvite.meeting) {
174
- invite = {
175
- is_invite: true,
176
- rsvp_supported: false,
177
- response_status: mapCalendarPartstat(parsedInvite.meeting.response_type),
178
- available_rsvp_responses: [],
179
- };
180
- }
181
- }
369
+ const bodyText = annotateBodyWithInlineAttachments(body.text, attachments);
370
+ const inviteMetadata = await extractGmailInviteMetadata(account, context, message, { to, cc });
371
+ const invite = inviteMetadata?.invite;
182
372
  return {
183
373
  message_ref: "",
184
374
  envelope: {
@@ -190,12 +380,12 @@ async function normalizeGmailMessage(account, context, message) {
190
380
  unread,
191
381
  ...(subject ? { subject } : {}),
192
382
  },
193
- snippet: message.snippet ?? body.text.slice(0, 240),
383
+ snippet: message.snippet ?? bodyText.slice(0, 240),
194
384
  body: {
195
- text: body.text,
385
+ text: bodyText,
196
386
  truncated: false,
197
387
  cached: true,
198
- cached_bytes: Buffer.byteLength(body.text, "utf8"),
388
+ cached_bytes: Buffer.byteLength(bodyText, "utf8"),
199
389
  },
200
390
  attachments,
201
391
  ...(invite ? { invite } : {}),
@@ -208,6 +398,9 @@ async function normalizeGmailMessage(account, context, message) {
208
398
  locator: {
209
399
  thread_id: message.threadId ?? null,
210
400
  message_id: message.id ?? null,
401
+ calendar_uid: inviteMetadata?.calendar_uid ?? null,
402
+ attendee_email: inviteMetadata?.attendee_email ?? null,
403
+ meeting_start: inviteMetadata?.meeting_start ?? null,
211
404
  },
212
405
  },
213
406
  };
@@ -368,6 +561,9 @@ function parseMessageLocator(locatorJson) {
368
561
  return {
369
562
  thread_id: typeof parsed.thread_id === "string" && parsed.thread_id ? parsed.thread_id : null,
370
563
  message_id: typeof parsed.message_id === "string" && parsed.message_id ? parsed.message_id : null,
564
+ calendar_uid: typeof parsed.calendar_uid === "string" && parsed.calendar_uid ? parsed.calendar_uid : null,
565
+ attendee_email: typeof parsed.attendee_email === "string" && parsed.attendee_email ? parsed.attendee_email : null,
566
+ meeting_start: typeof parsed.meeting_start === "string" && parsed.meeting_start ? parsed.meeting_start : null,
371
567
  };
372
568
  }
373
569
  function latestStoredThreadMessage(threadRef, context) {
@@ -411,6 +607,18 @@ function buildMarkMessagesEnvelope(account, command, updated) {
411
607
  updated,
412
608
  };
413
609
  }
610
+ function buildRsvpEnvelope(account, messageRef, threadRef, response, invite) {
611
+ return {
612
+ schema_version: "1",
613
+ command: "rsvp",
614
+ account: account.name,
615
+ message_ref: messageRef,
616
+ thread_ref: threadRef,
617
+ source: sourceInfo(account),
618
+ response,
619
+ invite: invite ?? null,
620
+ };
621
+ }
414
622
  function buildReadEnvelope(account, messageRef, threadRef, parsed, attachments, cacheStatus) {
415
623
  return {
416
624
  schema_version: "1",
@@ -431,19 +639,6 @@ function buildReadEnvelope(account, messageRef, threadRef, parsed, attachments,
431
639
  },
432
640
  };
433
641
  }
434
- async function maybeSummarizeThreads(threads, context) {
435
- if (context.config.summarizerBackend === "none") {
436
- return threads;
437
- }
438
- const summarized = [];
439
- for (const thread of threads) {
440
- summarized.push({
441
- ...thread,
442
- summary: await summarizeThread(thread, context.config),
443
- });
444
- }
445
- return summarized;
446
- }
447
642
  async function persistThreads(account, context, threads) {
448
643
  return context.db.transaction(() => {
449
644
  const persistedThreads = [];
@@ -557,9 +752,6 @@ async function persistThreads(account, context, threads) {
557
752
  messageRefs.push(resolvedMessageRef);
558
753
  }
559
754
  context.db.replaceThreadMessages(resolvedThreadRef, messageRefs);
560
- if (thread.summary) {
561
- context.db.upsertSummary(resolvedThreadRef, thread.summary);
562
- }
563
755
  persistedThreads.push({
564
756
  ...thread,
565
757
  thread_ref: resolvedThreadRef,
@@ -572,8 +764,8 @@ async function persistThreads(account, context, threads) {
572
764
  async function fetchAndPersistGmailThread(account, context, threadId) {
573
765
  const thread = await getGmailThread(account, context, threadId);
574
766
  const normalized = await normalizeGmailThread(account, context, thread);
575
- const withSummary = (await maybeSummarizeThreads([normalized], context))[0];
576
- await persistThreads(account, context, [withSummary]);
767
+ const persisted = await persistThreads(account, context, [normalized]);
768
+ await summarizeAndPersistThreads(persisted, context.config, context.db, context.db.getAccountIdentity(account));
577
769
  }
578
770
  async function refreshStoredMessage(account, messageRef, context) {
579
771
  const locatorRow = context.db.findProviderLocator("message", messageRef);
@@ -654,6 +846,72 @@ async function resolveGmailMessageContext(account, messageRef, context) {
654
846
  headers: headerIndex(message.payload?.headers),
655
847
  };
656
848
  }
849
+ async function resolveGmailRsvpTarget(account, messageRef, context) {
850
+ const target = await resolveGmailMessageContext(account, messageRef, context);
851
+ const inviteMetadata = await extractGmailInviteMetadata(account, context, target.message, {
852
+ to: parseMailboxes(target.headers.to),
853
+ cc: parseMailboxes(target.headers.cc),
854
+ });
855
+ if (!inviteMetadata?.calendar_uid) {
856
+ throw new SurfaceError("unsupported", `Message '${messageRef}' is not a Gmail calendar invite with a resolvable UID.`, {
857
+ account: account.name,
858
+ messageRef,
859
+ });
860
+ }
861
+ if (!inviteMetadata.invite.rsvp_supported) {
862
+ throw new SurfaceError("unsupported", `Message '${messageRef}' is not a Gmail invite Surface can RSVP to.`, {
863
+ account: account.name,
864
+ messageRef,
865
+ });
866
+ }
867
+ const attendeeEmail = inviteMetadata.attendee_email?.trim().toLowerCase()
868
+ || account.email.trim().toLowerCase();
869
+ if (!attendeeEmail) {
870
+ throw new SurfaceError("unsupported", `Message '${messageRef}' does not expose an attendee email for RSVP.`, {
871
+ account: account.name,
872
+ messageRef,
873
+ });
874
+ }
875
+ return {
876
+ stored: target.stored,
877
+ message: target.message,
878
+ threadRef: target.stored.thread_ref,
879
+ calendarUid: inviteMetadata.calendar_uid,
880
+ attendeeEmail,
881
+ meetingStart: inviteMetadata.meeting_start,
882
+ invite: inviteMetadata.invite,
883
+ };
884
+ }
885
+ async function performGmailRsvp(account, context, target, response) {
886
+ const events = await listGoogleCalendarEventsByIcalUid(account, context, "primary", target.calendarUid);
887
+ const event = chooseGoogleCalendarEvent(events, target.meetingStart);
888
+ if (!event?.id) {
889
+ throw new SurfaceError("not_found", `Google Calendar could not find an event matching invite UID '${target.calendarUid}'.`, { account: account.name });
890
+ }
891
+ const attendee = pickCalendarAttendee(event, target.attendeeEmail, account.email);
892
+ if (!attendee?.email) {
893
+ throw new SurfaceError("unsupported", `Google Calendar event '${event.id}' does not expose an attendee that Surface can RSVP as.`, { account: account.name });
894
+ }
895
+ const patched = await patchGoogleCalendarEvent(account, context, "primary", event.id, {
896
+ attendeesOmitted: true,
897
+ attendees: [
898
+ {
899
+ email: attendee.email,
900
+ responseStatus: googleCalendarResponseStatusForRsvp(response),
901
+ },
902
+ ],
903
+ });
904
+ const refreshed = patched.id
905
+ ? await getGoogleCalendarEvent(account, context, "primary", patched.id)
906
+ : patched;
907
+ const refreshedAttendee = pickCalendarAttendee(refreshed, attendee.email, account.email);
908
+ return {
909
+ is_invite: true,
910
+ rsvp_supported: true,
911
+ response_status: refreshedAttendee?.response_status ?? mapGoogleCalendarResponseStatus(googleCalendarResponseStatusForRsvp(response)),
912
+ available_rsvp_responses: ["accept", "decline", "tentative"],
913
+ };
914
+ }
657
915
  async function refreshRefsFromGmailResponse(account, context, response) {
658
916
  const threadId = response.threadId ?? null;
659
917
  if (threadId) {
@@ -697,8 +955,8 @@ async function fetchGmailThreads(account, context, options) {
697
955
  .filter((threadId) => Boolean(threadId))
698
956
  .map((threadId) => getGmailThread(account, context, threadId)));
699
957
  const normalized = await Promise.all(hydrated.map((thread) => normalizeGmailThread(account, context, thread)));
700
- const summarized = await maybeSummarizeThreads(normalized, context);
701
- return persistThreads(account, context, summarized);
958
+ const persisted = await persistThreads(account, context, normalized);
959
+ return summarizeAndPersistThreads(persisted, context.config, context.db, context.db.getAccountIdentity(account));
702
960
  }
703
961
  async function sendOrDraftGmailMessage(account, context, payload) {
704
962
  if (payload.draft) {
@@ -725,13 +983,8 @@ export class GmailApiAdapter {
725
983
  transport = "gmail-api";
726
984
  async login(account, context) {
727
985
  const result = await runGmailLogin(account, context);
728
- if (result.authenticatedEmail && result.authenticatedEmail !== account.email) {
729
- context.db.upsertAccount({
730
- name: account.name,
731
- provider: account.provider,
732
- transport: account.transport,
733
- email: result.authenticatedEmail,
734
- });
986
+ if (result.authenticatedEmail) {
987
+ context.db.updateAccountIdentityFromProvider(account, result.authenticatedEmail);
735
988
  }
736
989
  return {
737
990
  status: "authenticated",
@@ -749,16 +1002,21 @@ export class GmailApiAdapter {
749
1002
  }
750
1003
  async authStatus(account, context) {
751
1004
  const status = await gmailAuthStatus(account, context);
1005
+ if (status.authenticatedEmail) {
1006
+ context.db.updateAccountIdentityFromProvider(account, status.authenticatedEmail);
1007
+ }
752
1008
  return status.authenticated
753
1009
  ? { status: "authenticated", detail: status.detail }
754
1010
  : { status: "unauthenticated", detail: status.detail };
755
1011
  }
756
1012
  async search(account, query, context) {
757
- return fetchGmailThreads(account, context, {
1013
+ const queryText = buildGmailSearchQuery(query);
1014
+ const threads = await fetchGmailThreads(account, context, {
758
1015
  kind: "search",
759
- queryText: query.text,
1016
+ ...(queryText ? { queryText } : {}),
760
1017
  limit: query.limit,
761
1018
  });
1019
+ return threads.filter((thread) => threadMatchesStructuredFilters(thread, query)).slice(0, query.limit);
762
1020
  }
763
1021
  async fetchUnread(account, query, context) {
764
1022
  return fetchGmailThreads(account, context, {
@@ -766,6 +1024,23 @@ export class GmailApiAdapter {
766
1024
  limit: query.limit,
767
1025
  });
768
1026
  }
1027
+ async refreshThread(account, threadRef, context) {
1028
+ const locatorRow = context.db.findProviderLocator("thread", threadRef);
1029
+ if (!locatorRow) {
1030
+ throw new SurfaceError("cache_miss", `No provider locator exists for thread '${threadRef}'.`, {
1031
+ account: account.name,
1032
+ threadRef,
1033
+ });
1034
+ }
1035
+ const locator = JSON.parse(locatorRow.locator_json);
1036
+ if (!locator.thread_id) {
1037
+ throw new SurfaceError("transport_error", `Thread '${threadRef}' is missing a Gmail thread id.`, {
1038
+ account: account.name,
1039
+ threadRef,
1040
+ });
1041
+ }
1042
+ await fetchAndPersistGmailThread(account, context, locator.thread_id);
1043
+ }
769
1044
  async readMessage(account, messageRef, refresh, context) {
770
1045
  const stored = context.db.getStoredMessage(messageRef);
771
1046
  if (!stored) {
@@ -884,8 +1159,16 @@ export class GmailApiAdapter {
884
1159
  },
885
1160
  };
886
1161
  }
887
- async rsvp(account, _messageRef, _response) {
888
- notImplemented("Gmail RSVP is deferred pending explicit Google Calendar integration.", account.name);
1162
+ async rsvp(account, messageRef, response, context) {
1163
+ const target = await resolveGmailRsvpTarget(account, messageRef, context);
1164
+ const invite = await performGmailRsvp(account, context, target, response);
1165
+ context.db.updateInviteForThread(target.threadRef, {
1166
+ is_invite: true,
1167
+ rsvp_supported: invite.rsvp_supported,
1168
+ response_status: invite.response_status,
1169
+ available_rsvp_responses: invite.available_rsvp_responses,
1170
+ });
1171
+ return buildRsvpEnvelope(account, messageRef, target.threadRef, response, invite);
889
1172
  }
890
1173
  async sendMessage(account, input, context) {
891
1174
  const recipients = {