@mulmoclaude/core 0.20.2 → 0.21.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.
@@ -326,6 +326,9 @@ This is independent of claude.ai Google connectors. Each failure names its missi
326
326
  - **Multiple client secrets** — ask the user to keep exactly one `client_secret_*.json` in
327
327
  `~/.secrets/`; the stored refresh token pairs with one OAuth client, so duplicates are refused
328
328
  rather than guessed at.
329
- - **HTTP 403 from a Google API** — the API is not enabled for the user's Cloud project. Ask them
330
- to enable it in the Cloud Console (APIs & Services → Library — e.g. "Google Calendar API"),
331
- then retry. No re-link needed.
329
+ - **HTTP 403 from a Google API** — the API is not enabled for the user's Cloud project. The error
330
+ names the API; ask them to enable that one in the Cloud Console (APIs & Services → Library —
331
+ "Google Calendar API", "Google Tasks API", or "Google Drive API"), then retry. No re-link needed.
332
+ - **Drive shows nothing / "I can't find the user's file"** — not an error. The app holds the
333
+ `drive.file` scope, so it can only ever see files IT created; the user's wider Drive is
334
+ invisible by design. Say so plainly instead of implying an empty Drive.
@@ -0,0 +1,18 @@
1
+ export declare const GOOGLE_API_TIMEOUT_MS: number;
2
+ export declare const DEFAULT_LIST_MAX_RESULTS = 10;
3
+ export declare const MAX_LIST_RESULTS = 50;
4
+ /** 403 usually means the API is not enabled for the user's Cloud project —
5
+ * name the API so the agent's recovery guidance can be specific. */
6
+ export declare const googleApiError: (apiLabel: string, status: number, body: string) => Error;
7
+ export interface GoogleRequestInit {
8
+ method?: string;
9
+ body?: string;
10
+ /** Overrides the default JSON content type (multipart upload, …). */
11
+ contentType?: string;
12
+ /** Response is not JSON (Drive media download) — return the raw text. */
13
+ expectText?: boolean;
14
+ }
15
+ export declare function googleRequest(apiLabel: string, accessToken: string, url: string, init?: GoogleRequestInit): Promise<unknown>;
16
+ export declare const stringField: (record: Record<string, unknown>, key: string) => string;
17
+ export declare const asRecord: (value: unknown) => Record<string, unknown>;
18
+ export declare const itemsOf: (value: unknown) => unknown[];
@@ -1,5 +1,3 @@
1
- export declare const DEFAULT_LIST_MAX_RESULTS = 10;
2
- export declare const MAX_LIST_RESULTS = 50;
3
1
  export interface CalendarEventInput {
4
2
  summary: string;
5
3
  startDateTime: string;
@@ -19,6 +17,8 @@ export interface CalendarEventSummary {
19
17
  status: string;
20
18
  }
21
19
  export declare const toEventSummary: (value: unknown) => CalendarEventSummary;
20
+ /** Kept as a named export for the existing unit tests / callers; the shared
21
+ * helper now carries the wording. */
22
22
  export declare const calendarApiError: (status: number, body: string) => Error;
23
23
  export declare function createCalendarEvent(accessToken: string, input: CalendarEventInput): Promise<CalendarEventSummary>;
24
24
  export declare function listCalendarEvents(accessToken: string, input?: ListEventsInput): Promise<CalendarEventSummary[]>;
@@ -0,0 +1,31 @@
1
+ export interface DriveFileSummary {
2
+ id: string;
3
+ name: string;
4
+ mimeType: string;
5
+ webViewLink: string;
6
+ modifiedTime: string;
7
+ }
8
+ export interface ListDriveFilesInput {
9
+ maxResults?: number;
10
+ }
11
+ export interface CreateDriveFileInput {
12
+ name: string;
13
+ content: string;
14
+ mimeType?: string;
15
+ }
16
+ export interface ReadDriveFileInput {
17
+ fileId: string;
18
+ }
19
+ export declare const toDriveFileSummary: (value: unknown) => DriveFileSummary;
20
+ export declare const isTextMimeType: (mimeType: string) => boolean;
21
+ export declare function listDriveFiles(accessToken: string, input?: ListDriveFilesInput): Promise<DriveFileSummary[]>;
22
+ export declare const assertSafeMimeType: (mimeType: string) => string;
23
+ export declare const buildMultipartBody: (metadata: Record<string, string>, content: string, mimeType: string, boundary: string) => string;
24
+ /** A boundary that appears nowhere in the parts it delimits. */
25
+ export declare const pickBoundary: (parts: string[], generate?: () => string) => string;
26
+ export declare function createDriveFile(accessToken: string, input: CreateDriveFileInput): Promise<DriveFileSummary>;
27
+ export declare function readDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<{
28
+ file: DriveFileSummary;
29
+ content: string;
30
+ }>;
31
+ export declare function deleteDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<void>;
@@ -394,50 +394,67 @@ var createGoogleAuthFlow = (authorize) => {
394
394
  };
395
395
  var googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);
396
396
  //#endregion
397
- //#region src/google/calendar.ts
398
- var CALENDAR_EVENTS_URL = "https://www.googleapis.com/calendar/v3/calendars/primary/events";
399
- var CALENDAR_TIMEOUT_MS = 30 * ONE_SECOND_MS;
397
+ //#region src/google/apiClient.ts
398
+ var GOOGLE_API_TIMEOUT_MS = 30 * ONE_SECOND_MS;
400
399
  var ERROR_BODY_MAX_CHARS = 300;
401
400
  var HTTP_FORBIDDEN = 403;
402
401
  var DEFAULT_LIST_MAX_RESULTS = 10;
403
402
  var MAX_LIST_RESULTS = 50;
404
- var eventTime = (value) => {
405
- if (!isRecord(value)) return "";
406
- if (typeof value.dateTime === "string") return value.dateTime;
407
- if (typeof value.date === "string") return value.date;
408
- return "";
409
- };
410
- var toEventSummary = (value) => {
411
- const record = isRecord(value) ? value : {};
412
- return {
413
- id: typeof record.id === "string" ? record.id : "",
414
- summary: typeof record.summary === "string" ? record.summary : "",
415
- start: eventTime(record.start),
416
- end: eventTime(record.end),
417
- htmlLink: typeof record.htmlLink === "string" ? record.htmlLink : "",
418
- status: typeof record.status === "string" ? record.status : ""
419
- };
420
- };
421
- var calendarApiError = (status, body) => {
422
- const hint = status === HTTP_FORBIDDEN ? " (is the Google Calendar API enabled for the Cloud project?)" : "";
403
+ /** 403 usually means the API is not enabled for the user's Cloud project —
404
+ * name the API so the agent's recovery guidance can be specific. */
405
+ var googleApiError = (apiLabel, status, body) => {
406
+ const hint = status === HTTP_FORBIDDEN ? ` (is the ${apiLabel} enabled for the Cloud project?)` : "";
423
407
  const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : "";
424
- return /* @__PURE__ */ new Error(`Google Calendar API: HTTP ${status}${hint}${detail}`);
408
+ return /* @__PURE__ */ new Error(`${apiLabel}: HTTP ${status}${hint}${detail}`);
425
409
  };
426
- var calendarRequest = async (accessToken, url, init = {}) => {
410
+ async function googleRequest(apiLabel, accessToken, url, init = {}) {
411
+ const { contentType = "application/json", expectText = false, ...rest } = init;
427
412
  const response = await fetchWithTimeout(url, {
428
- ...init,
429
- timeoutMs: CALENDAR_TIMEOUT_MS,
413
+ ...rest,
414
+ timeoutMs: GOOGLE_API_TIMEOUT_MS,
430
415
  headers: {
431
416
  Authorization: `Bearer ${accessToken}`,
432
- "Content-Type": "application/json"
417
+ "Content-Type": contentType
433
418
  }
434
419
  });
435
420
  if (!response.ok) {
436
421
  const body = await response.text().catch((err) => errorMessage(err));
437
- throw calendarApiError(response.status, body);
422
+ throw googleApiError(apiLabel, response.status, body);
438
423
  }
424
+ if (expectText) return await response.text();
425
+ if (response.status === 204) return {};
439
426
  return await response.json();
427
+ }
428
+ var stringField = (record, key) => typeof record[key] === "string" ? record[key] : "";
429
+ var asRecord = (value) => isRecord(value) ? value : {};
430
+ var itemsOf = (value) => {
431
+ const record = asRecord(value);
432
+ return Array.isArray(record.items) ? record.items : [];
440
433
  };
434
+ //#endregion
435
+ //#region src/google/calendar.ts
436
+ var CALENDAR_EVENTS_URL = "https://www.googleapis.com/calendar/v3/calendars/primary/events";
437
+ var CALENDAR_API_LABEL = "Google Calendar API";
438
+ var eventTime = (value) => {
439
+ if (!isRecord(value)) return "";
440
+ if (typeof value.dateTime === "string") return value.dateTime;
441
+ if (typeof value.date === "string") return value.date;
442
+ return "";
443
+ };
444
+ var toEventSummary = (value) => {
445
+ const record = asRecord(value);
446
+ return {
447
+ id: stringField(record, "id"),
448
+ summary: stringField(record, "summary"),
449
+ start: eventTime(record.start),
450
+ end: eventTime(record.end),
451
+ htmlLink: stringField(record, "htmlLink"),
452
+ status: stringField(record, "status")
453
+ };
454
+ };
455
+ /** Kept as a named export for the existing unit tests / callers; the shared
456
+ * helper now carries the wording. */
457
+ var calendarApiError = (status, body) => googleApiError(CALENDAR_API_LABEL, status, body);
441
458
  async function createCalendarEvent(accessToken, input) {
442
459
  const body = {
443
460
  summary: input.summary,
@@ -445,19 +462,167 @@ async function createCalendarEvent(accessToken, input) {
445
462
  start: { dateTime: input.startDateTime },
446
463
  end: { dateTime: input.endDateTime }
447
464
  };
448
- return toEventSummary(await calendarRequest(accessToken, CALENDAR_EVENTS_URL, {
465
+ return toEventSummary(await googleRequest(CALENDAR_API_LABEL, accessToken, CALENDAR_EVENTS_URL, {
449
466
  method: "POST",
450
467
  body: JSON.stringify(body)
451
468
  }));
452
469
  }
453
470
  async function listCalendarEvents(accessToken, input = {}) {
454
- const listed = await calendarRequest(accessToken, `${CALENDAR_EVENTS_URL}?${new URLSearchParams({
471
+ const record = asRecord(await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_EVENTS_URL}?${new URLSearchParams({
455
472
  timeMin: input.timeMin ?? (/* @__PURE__ */ new Date()).toISOString(),
456
473
  maxResults: String(input.maxResults ?? 10),
457
474
  singleEvents: "true",
458
475
  orderBy: "startTime"
459
- }).toString()}`);
460
- return (isRecord(listed) && Array.isArray(listed.items) ? listed.items : []).map(toEventSummary);
476
+ }).toString()}`));
477
+ return (Array.isArray(record.items) ? record.items : []).map(toEventSummary);
478
+ }
479
+ //#endregion
480
+ //#region src/google/tasks.ts
481
+ var TASKS_BASE_URL = "https://tasks.googleapis.com/tasks/v1";
482
+ var TASKS_API_LABEL = "Google Tasks API";
483
+ var DEFAULT_TASK_LIST_ID = "@default";
484
+ var TASK_STATUS_COMPLETED = "completed";
485
+ var MAX_TASK_LISTS = 50;
486
+ var toTaskListSummary = (value) => {
487
+ const record = asRecord(value);
488
+ return {
489
+ id: stringField(record, "id"),
490
+ title: stringField(record, "title")
491
+ };
492
+ };
493
+ var toTaskSummary = (value) => {
494
+ const record = asRecord(value);
495
+ return {
496
+ id: stringField(record, "id"),
497
+ title: stringField(record, "title"),
498
+ status: stringField(record, "status"),
499
+ due: stringField(record, "due"),
500
+ notes: stringField(record, "notes")
501
+ };
502
+ };
503
+ var tasksUrl = (taskListId, suffix = "") => `${TASKS_BASE_URL}/lists/${encodeURIComponent(taskListId ?? DEFAULT_TASK_LIST_ID)}/tasks${suffix}`;
504
+ async function listTaskLists(accessToken) {
505
+ return itemsOf(await googleRequest(TASKS_API_LABEL, accessToken, `${TASKS_BASE_URL}/users/@me/lists?maxResults=${MAX_TASK_LISTS}`)).map(toTaskListSummary);
506
+ }
507
+ async function listTasks(accessToken, input = {}) {
508
+ const params = new URLSearchParams({
509
+ maxResults: String(input.maxResults ?? 10),
510
+ showCompleted: String(input.showCompleted ?? false)
511
+ });
512
+ return itemsOf(await googleRequest(TASKS_API_LABEL, accessToken, `${tasksUrl(input.taskListId)}?${params.toString()}`)).map(toTaskSummary);
513
+ }
514
+ async function createTask(accessToken, input) {
515
+ const body = {
516
+ title: input.title,
517
+ notes: input.notes,
518
+ due: input.due
519
+ };
520
+ return toTaskSummary(await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId), {
521
+ method: "POST",
522
+ body: JSON.stringify(body)
523
+ }));
524
+ }
525
+ async function completeTask(accessToken, input) {
526
+ return toTaskSummary(await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`), {
527
+ method: "PATCH",
528
+ body: JSON.stringify({ status: TASK_STATUS_COMPLETED })
529
+ }));
530
+ }
531
+ async function deleteTask(accessToken, input) {
532
+ await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`), { method: "DELETE" });
533
+ }
534
+ //#endregion
535
+ //#region src/google/driveFile.ts
536
+ var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
537
+ var DRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3/files";
538
+ var DRIVE_API_LABEL = "Google Drive API";
539
+ var DEFAULT_MIME_TYPE = "text/plain";
540
+ var FILE_FIELDS = "id,name,mimeType,webViewLink,modifiedTime";
541
+ var MAX_READ_CHARS = 1e5;
542
+ var TEXT_MIME_PREFIXES = [
543
+ "text/",
544
+ "application/json",
545
+ "application/xml",
546
+ "application/javascript"
547
+ ];
548
+ var toDriveFileSummary = (value) => {
549
+ const record = asRecord(value);
550
+ return {
551
+ id: stringField(record, "id"),
552
+ name: stringField(record, "name"),
553
+ mimeType: stringField(record, "mimeType"),
554
+ webViewLink: stringField(record, "webViewLink"),
555
+ modifiedTime: stringField(record, "modifiedTime")
556
+ };
557
+ };
558
+ var isTextMimeType = (mimeType) => TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));
559
+ async function listDriveFiles(accessToken, input = {}) {
560
+ const record = asRecord(await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}?${new URLSearchParams({
561
+ pageSize: String(input.maxResults ?? 10),
562
+ fields: `files(${FILE_FIELDS})`,
563
+ orderBy: "modifiedTime desc"
564
+ }).toString()}`));
565
+ return (Array.isArray(record.files) ? record.files : []).map(toDriveFileSummary);
566
+ }
567
+ var BOUNDARY_BYTES = 16;
568
+ /** Fresh per request: a fixed boundary appearing inside file content would
569
+ * split the payload at the wrong place, so Drive would store a truncated or
570
+ * mangled file. Random 128 bits makes an accidental — or crafted — collision
571
+ * infeasible; `newMultipartBoundary` is re-derived until it is absent from
572
+ * the body it must delimit. */
573
+ var newMultipartBoundary = () => `mulmo-drive-${(0, node_crypto.randomBytes)(BOUNDARY_BYTES).toString("hex")}`;
574
+ var MIME_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
575
+ var assertSafeMimeType = (mimeType) => {
576
+ if (!MIME_TYPE_RE.test(mimeType)) throw new Error(`Google Drive API: invalid mimeType '${mimeType}' — expected a plain type/subtype such as text/plain`);
577
+ return mimeType;
578
+ };
579
+ var buildMultipartBody = (metadata, content, mimeType, boundary) => [
580
+ `--${boundary}`,
581
+ "Content-Type: application/json; charset=UTF-8",
582
+ "",
583
+ JSON.stringify(metadata),
584
+ `--${boundary}`,
585
+ `Content-Type: ${mimeType}`,
586
+ "",
587
+ content,
588
+ `--${boundary}--`,
589
+ ""
590
+ ].join("\r\n");
591
+ /** A boundary that appears nowhere in the parts it delimits. */
592
+ var pickBoundary = (parts, generate = newMultipartBoundary) => {
593
+ const collides = (candidate) => parts.some((part) => part.includes(candidate));
594
+ let boundary = generate();
595
+ while (collides(boundary)) boundary = generate();
596
+ return boundary;
597
+ };
598
+ async function createDriveFile(accessToken, input) {
599
+ const mimeType = assertSafeMimeType(input.mimeType ?? DEFAULT_MIME_TYPE);
600
+ const metadata = {
601
+ name: input.name,
602
+ mimeType
603
+ };
604
+ const boundary = pickBoundary([input.content, JSON.stringify(metadata)]);
605
+ return toDriveFileSummary(await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_UPLOAD_URL}?${new URLSearchParams({
606
+ uploadType: "multipart",
607
+ fields: FILE_FIELDS
608
+ }).toString()}`, {
609
+ method: "POST",
610
+ contentType: `multipart/related; boundary=${boundary}`,
611
+ body: buildMultipartBody(metadata, input.content, mimeType, boundary)
612
+ }));
613
+ }
614
+ async function readDriveFile(accessToken, input) {
615
+ const fileId = encodeURIComponent(input.fileId);
616
+ const file = toDriveFileSummary(await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?fields=${FILE_FIELDS}`));
617
+ if (!isTextMimeType(file.mimeType)) throw new Error(`Google Drive API: '${file.name}' is ${file.mimeType || "a binary file"} — only text files can be read as content`);
618
+ const raw = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?alt=media`, { expectText: true });
619
+ return {
620
+ file,
621
+ content: typeof raw === "string" ? raw.slice(0, MAX_READ_CHARS) : ""
622
+ };
623
+ }
624
+ async function deleteDriveFile(accessToken, input) {
625
+ await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
461
626
  }
462
627
  //#endregion
463
628
  exports.DEFAULT_LIST_MAX_RESULTS = DEFAULT_LIST_MAX_RESULTS;
@@ -466,27 +631,44 @@ exports.GOOGLE_DRIVE_FILE_SCOPE = GOOGLE_DRIVE_FILE_SCOPE;
466
631
  exports.GOOGLE_SCOPES = GOOGLE_SCOPES;
467
632
  exports.GOOGLE_TASKS_SCOPE = GOOGLE_TASKS_SCOPE;
468
633
  exports.MAX_LIST_RESULTS = MAX_LIST_RESULTS;
634
+ exports.assertSafeMimeType = assertSafeMimeType;
469
635
  exports.authorizeGoogle = authorizeGoogle;
636
+ exports.buildMultipartBody = buildMultipartBody;
470
637
  exports.calendarApiError = calendarApiError;
471
638
  exports.clientSecretPresence = clientSecretPresence;
639
+ exports.completeTask = completeTask;
472
640
  exports.configureGoogleHost = configureGoogleHost;
473
641
  exports.createCalendarEvent = createCalendarEvent;
642
+ exports.createDriveFile = createDriveFile;
474
643
  exports.createGoogleAuthFlow = createGoogleAuthFlow;
644
+ exports.createTask = createTask;
645
+ exports.deleteDriveFile = deleteDriveFile;
475
646
  exports.deleteGoogleTokens = deleteGoogleTokens;
647
+ exports.deleteTask = deleteTask;
476
648
  exports.findClientSecretPath = findClientSecretPath;
477
649
  exports.getGoogleAccessToken = getGoogleAccessToken;
650
+ exports.googleApiError = googleApiError;
478
651
  exports.googleAuthFlow = googleAuthFlow;
479
652
  exports.googleConfigDir = googleConfigDir;
480
653
  exports.googleSecretsDir = googleSecretsDir;
481
654
  exports.googleTokenPath = googleTokenPath;
482
655
  exports.isIsoDateTimeWithOffset = isIsoDateTimeWithOffset;
656
+ exports.isTextMimeType = isTextMimeType;
483
657
  exports.legacyGoogleTokenPath = legacyGoogleTokenPath;
484
658
  exports.listCalendarEvents = listCalendarEvents;
659
+ exports.listDriveFiles = listDriveFiles;
660
+ exports.listTaskLists = listTaskLists;
661
+ exports.listTasks = listTasks;
485
662
  exports.loadClientSecret = loadClientSecret;
486
663
  exports.loadGoogleTokens = loadGoogleTokens;
487
664
  exports.mergeGoogleTokens = mergeGoogleTokens;
665
+ exports.pickBoundary = pickBoundary;
666
+ exports.readDriveFile = readDriveFile;
488
667
  exports.saveGoogleTokens = saveGoogleTokens;
668
+ exports.toDriveFileSummary = toDriveFileSummary;
489
669
  exports.toEventSummary = toEventSummary;
670
+ exports.toTaskListSummary = toTaskListSummary;
671
+ exports.toTaskSummary = toTaskSummary;
490
672
  exports.unlinkGoogle = unlinkGoogle;
491
673
  exports.waitForAuthCode = waitForAuthCode;
492
674
 
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":[],"sources":["../../src/google/host.ts","../../src/google/datetime.ts","../../src/google/paths.ts","../../src/google/util.ts","../../src/google/clientSecret.ts","../../src/google/fsJson.ts","../../src/google/tokenStore.ts","../../src/google/fetch.ts","../../src/google/auth.ts","../../src/google/authFlow.ts","../../src/google/calendar.ts"],"sourcesContent":["// Host binding for the Google engine. The engine logs through the host's\n// logger, but a package-level import of the host logger would be an uphill\n// dependency — so the host injects it once at startup (same pattern as\n// `collection/server/host.ts`). The default is silent so the engine works\n// unconfigured in unit tests.\n\nexport interface GoogleLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nconst silentLogger: GoogleLogger = {\n error: () => undefined,\n warn: () => undefined,\n info: () => undefined,\n debug: () => undefined,\n};\n\nlet hostLog: GoogleLogger = silentLogger;\n\nexport function configureGoogleHost(binding: { log: GoogleLogger }): void {\n hostLog = binding.log;\n}\n\nexport const log: GoogleLogger = {\n error: (prefix, message, data) => hostLog.error(prefix, message, data),\n warn: (prefix, message, data) => hostLog.warn(prefix, message, data),\n info: (prefix, message, data) => hostLog.info(prefix, message, data),\n debug: (prefix, message, data) => hostLog.debug(prefix, message, data),\n};\n","// Strict RFC3339 date-time validation shared by every surface that feeds\n// Calendar `dateTime`/`timeMin` values (agent tool args, remote-host command\n// params). Calendar rejects date-only / offset-less values with an opaque\n// 400, so callers validate here and return an actionable message instead.\n\n// Fractional seconds are normalized away first — an optional `(\\.\\d+)?`\n// group inside the main pattern trips security/detect-unsafe-regex.\nconst ISO_DATE_TIME_WITH_OFFSET_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:Z|[+-](\\d{2}):(\\d{2}))$/;\nconst FRACTIONAL_SECONDS_RE = /\\.\\d+(?=Z|[+-])/;\n\nconst MAX_HOUR = 23;\nconst MAX_MINUTE = 59;\nconst MAX_SECOND = 59;\n\n// JS Date normalizes overflowed components (2026-02-31 parses as\n// 2026-03-03), so `new Date(value)` alone cannot reject impossible dates —\n// a UTC round-trip of the raw components can.\nconst isRealCalendarDate = (year: number, month: number, day: number): boolean => {\n const roundTrip = new Date(Date.UTC(year, month - 1, day));\n return roundTrip.getUTCFullYear() === year && roundTrip.getUTCMonth() === month - 1 && roundTrip.getUTCDate() === day;\n};\n\nexport const isIsoDateTimeWithOffset = (value: string): boolean => {\n const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE, \"\"));\n if (!match) return false;\n const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = match.slice(1, 7).map(Number);\n const timeInRange = hour <= MAX_HOUR && minute <= MAX_MINUTE && second <= MAX_SECOND;\n // RFC3339 bounds the offset to 00-23:59; groups 7/8 are undefined for \"Z\".\n const offsetInRange = match[7] === undefined || (Number(match[7]) <= MAX_HOUR && Number(match[8]) <= MAX_MINUTE);\n return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;\n};\n","// Google OAuth material lives OUTSIDE the workspace: the client secret is\n// machine-only (never synced) and the refresh token must survive workspace\n// resets — same reasoning as the gcloud / gh CLI model. The dir is the\n// host-NEUTRAL `~/.config/mulmo` because this engine is shared by both\n// MulmoClaude and MulmoTerminal, which deliberately share one grant per\n// machine. The `home` parameter exists so tests can thread a fake home.\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function googleConfigDir(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmo\");\n}\n\n/** Pre-0.20.1 token dir (mulmoclaude-branded); reads migrate away from it. */\nexport function legacyGoogleTokenPath(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmoclaude\", \"google-token.json\");\n}\n\nexport function googleTokenPath(home?: string): string {\n return join(googleConfigDir(home), \"google-token.json\");\n}\n\nexport function googleSecretsDir(home?: string): string {\n return join(home ?? homedir(), \".secrets\");\n}\n","// Small helpers ported from the host (`server/utils/{errors,text,time,types}.ts`)\n// so the Google engine carries no dependency on host utils — same convention\n// as `collection/registry/server/fetch.ts`.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport function errorMessage(err: unknown, fallback = \"unknown error\"): string {\n if (err instanceof Error) return err.message || fallback;\n const text = String(err);\n return text === \"\" || text === \"[object Object]\" ? fallback : text;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Clip a string to at most `max` chars; the ellipsis is included in the\n * budget so output never exceeds `max`. */\nexport function truncate(text: string, max: number, ellipsis = \"…\"): string {\n if (max <= 0) return \"\";\n if (text.length <= max) return text;\n return `${text.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}`;\n}\n","// Loads the Google OAuth desktop-app client credentials the user downloaded\n// from the Cloud Console into `~/.secrets/client_secret_*.json`. The file is\n// discovered by prefix so the user doesn't have to rename Google's long\n// default filename.\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { isRecord } from \"./util.js\";\nimport { googleSecretsDir } from \"./paths.js\";\n\nexport interface InstalledClientSecret {\n client_id: string;\n client_secret: string;\n}\n\nconst isInstalledClientSecret = (value: unknown): value is { installed: InstalledClientSecret } => {\n if (!isRecord(value) || !isRecord(value.installed)) return false;\n return typeof value.installed.client_id === \"string\" && typeof value.installed.client_secret === \"string\";\n};\n\nconst isClientSecretFileName = (name: string): boolean => name.startsWith(\"client_secret_\") && name.endsWith(\".json\");\n\nasync function listClientSecretFiles(home?: string): Promise<string[]> {\n const entries = await readdir(googleSecretsDir(home)).catch((): string[] => []);\n return entries.filter(isClientSecretFileName).sort();\n}\n\n/** \"ambiguous\" (2+ files) is distinct from \"missing\" — the fixes differ\n * (remove duplicates vs download credentials), so the UI must not conflate\n * them. */\nexport type ClientSecretPresence = \"found\" | \"missing\" | \"ambiguous\";\n\nexport async function clientSecretPresence(home?: string): Promise<ClientSecretPresence> {\n const matches = await listClientSecretFiles(home);\n if (matches.length === 0) return \"missing\";\n return matches.length === 1 ? \"found\" : \"ambiguous\";\n}\n\nexport async function findClientSecretPath(home?: string): Promise<string> {\n const dir = googleSecretsDir(home);\n const matches = await listClientSecretFiles(home);\n const [first, ...rest] = matches;\n if (!first) {\n throw new Error(\n `no client_secret_*.json found in ${dir} — download the OAuth desktop-app credentials JSON from the Google Cloud Console and place it there (mode 600)`,\n );\n }\n if (rest.length > 0) {\n // The stored refresh token is bound to one client_id; silently picking one\n // of several files could pair the token with the wrong client\n // (invalid_grant), so ambiguity is an error the user must resolve.\n throw new Error(`multiple client_secret_*.json files found in ${dir} (${matches.join(\", \")}) — keep exactly one`);\n }\n return join(dir, first);\n}\n\nexport async function loadClientSecret(home?: string): Promise<InstalledClientSecret> {\n const filePath = await findClientSecretPath(home);\n const raw = await readFile(filePath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (!isInstalledClientSecret(parsed)) {\n throw new Error(`${filePath} is not a desktop-app OAuth client secret (missing \"installed\" with client_id / client_secret)`);\n }\n return { client_id: parsed.installed.client_id, client_secret: parsed.installed.client_secret };\n}\n","// Minimal JSON file I/O for the token store. Unlike the host's\n// `writeJsonAtomic` (and core's collection `atomic.ts`), this one supports a\n// file mode — the token file must be 0600.\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\n\nexport async function readJsonOrNull<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsp.readFile(filePath, \"utf-8\");\n const parsed: T = JSON.parse(raw);\n return parsed;\n } catch {\n return null;\n }\n}\n\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300];\n\nconst hasErrnoCode = (err: unknown): err is { code: string } =>\n typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename\n// trips EPERM/EBUSY/EACCES. The retry is gated to Windows because POSIX EPERM\n// means a real permission problem — retrying would just delay the throw.\n// Ported from the host's server/utils/files/atomic.ts.\nconst isTransientRenameError = (err: unknown): boolean =>\n IS_WINDOWS && hasErrnoCode(err) && (err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\");\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsp.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n await fsp.rename(fromPath, toPath);\n}\n\n/** tmp-write + rename so readers never see a half-written file; `mode`\n * applies to the tmp file and survives the rename. */\nexport async function writeJsonAtomicWithMode(filePath: string, data: unknown, mode: number): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: \"utf-8\", mode });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","// Persistence for the Google OAuth tokens (refresh + access) at\n// `~/.config/mulmo/google-token.json`, mode 600. Google omits\n// `refresh_token` from refresh responses, so merges must preserve the one we\n// already hold — losing it forces the user through the browser consent again.\nimport { constants as fsConstants, copyFile, mkdir, rm, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Credentials } from \"google-auth-library\";\nimport { readJsonOrNull, writeJsonAtomicWithMode } from \"./fsJson.js\";\nimport { googleTokenPath, legacyGoogleTokenPath } from \"./paths.js\";\n\nconst TOKEN_FILE_MODE = 0o600;\n\nexport function mergeGoogleTokens(existing: Credentials | null, incoming: Credentials): Credentials {\n const merged = { ...existing, ...incoming };\n if (!incoming.refresh_token && existing?.refresh_token) merged.refresh_token = existing.refresh_token;\n return merged;\n}\n\nconst fileExists = async (filePath: string): Promise<boolean> =>\n await stat(filePath).then(\n () => true,\n () => false,\n );\n\n// Tokens written before 0.20.1 live under the mulmoclaude-branded dir; move\n// them once. COPYFILE_EXCL makes the create atomic-and-non-clobbering — an\n// exists+rename sequence could overwrite a token a concurrent process wrote\n// to the new path in between (TOCTOU). The legacy file is deleted only after\n// a successful copy; on EEXIST (new path won a race, or both files already\n// exist) it is left for any older install still reading it. copyFile\n// preserves the 600 mode.\nasync function migrateLegacyTokenFile(home?: string): Promise<void> {\n const current = googleTokenPath(home);\n const legacy = legacyGoogleTokenPath(home);\n if (!(await fileExists(legacy))) return;\n await mkdir(path.dirname(current), { recursive: true });\n try {\n await copyFile(legacy, current, fsConstants.COPYFILE_EXCL);\n } catch {\n return;\n }\n await rm(legacy, { force: true });\n}\n\nexport async function loadGoogleTokens(home?: string): Promise<Credentials | null> {\n await migrateLegacyTokenFile(home).catch(() => undefined);\n const current = await readJsonOrNull<Credentials>(googleTokenPath(home));\n if (current) return current;\n // Migration is best-effort — a valid legacy token must still count as\n // linked even when the move failed (permissions, read-only fs, …).\n return await readJsonOrNull<Credentials>(legacyGoogleTokenPath(home));\n}\n\nexport async function saveGoogleTokens(incoming: Credentials, home?: string): Promise<Credentials> {\n const merged = mergeGoogleTokens(await loadGoogleTokens(home), incoming);\n await writeJsonAtomicWithMode(googleTokenPath(home), merged, TOKEN_FILE_MODE);\n return merged;\n}\n\nexport async function deleteGoogleTokens(home?: string): Promise<void> {\n await rm(googleTokenPath(home), { force: true });\n}\n","// `fetch` with a finite timeout, ported from the host (`server/utils/fetch.ts`)\n// so the Google engine carries no dependency on host utils (same convention as\n// `collection/registry/server/fetch.ts`).\n\nimport { ONE_SECOND_MS } from \"./util.js\";\n\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/** `fetch` with a finite timeout. Rejects with a `TimeoutError` once\n * `timeoutMs` elapses. Composes with a caller-supplied `signal` so external\n * cancellation still works. */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n","// Google OAuth for the host machine, independent of Firebase Auth (which\n// discards refresh tokens). Entry points:\n// - authorizeGoogle(): one-shot loopback + PKCE browser consent flow\n// (desktop-app clients may redirect to any 127.0.0.1 port), storing the\n// refresh token locally via tokenStore.\n// - getGoogleAccessToken(): mints a fresh access token from the stored\n// refresh token; the OAuth2Client \"tokens\" event persists rotations.\n// - unlinkGoogle(): best-effort revoke at Google + local token delete.\nimport { randomBytes } from \"node:crypto\";\nimport http from \"node:http\";\nimport { CodeChallengeMethod, OAuth2Client, type Credentials } from \"google-auth-library\";\nimport { log } from \"./host.js\";\nimport { errorMessage, ONE_MINUTE_MS, ONE_SECOND_MS } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { loadClientSecret, type InstalledClientSecret } from \"./clientSecret.js\";\nimport { deleteGoogleTokens, loadGoogleTokens, saveGoogleTokens } from \"./tokenStore.js\";\n\nexport const GOOGLE_CALENDAR_SCOPE = \"https://www.googleapis.com/auth/calendar.events\";\nexport const GOOGLE_TASKS_SCOPE = \"https://www.googleapis.com/auth/tasks\";\nexport const GOOGLE_DRIVE_FILE_SCOPE = \"https://www.googleapis.com/auth/drive.file\";\n/** Requested at consent as one set — matches the scopes registered on the\n * OAuth consent screen, so a single re-link covers every supported API\n * (Calendar now; Tasks / Drive tools ride the same grant later). */\nexport const GOOGLE_SCOPES = [GOOGLE_CALENDAR_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE];\nconst CALLBACK_PATH = \"/oauth2callback\";\nconst AUTH_TIMEOUT_MS = 5 * ONE_MINUTE_MS;\nconst STATE_BYTES = 16;\n\nexport interface AuthorizeGoogleOptions {\n home?: string;\n /** Called with the consent URL; open it in a browser (and/or print it). */\n onAuthUrl?: (url: string) => void;\n timeoutMs?: number;\n}\n\nconst createClient = (secret: InstalledClientSecret, redirectUri?: string): OAuth2Client =>\n new OAuth2Client({ clientId: secret.client_id, clientSecret: secret.client_secret, redirectUri });\n\nconst persistRotatedTokens = (client: OAuth2Client, home?: string): void => {\n client.on(\"tokens\", (tokens) => {\n saveGoogleTokens(tokens, home).catch((err: unknown) => {\n log.error(\"google\", \"failed to persist rotated tokens\", { error: String(err) });\n });\n });\n};\n\nexport async function getGoogleAccessToken(home?: string): Promise<string> {\n const saved = await loadGoogleTokens(home);\n if (!saved?.refresh_token) {\n // Host-neutral wording — this engine ships to multiple hosts whose link\n // flows differ (#2128); each host's own help carries the specific steps.\n throw new Error(\"Google account not linked on this host — ask the user to link their Google account in this app's settings, then retry\");\n }\n const client = createClient(await loadClientSecret(home));\n client.setCredentials(saved);\n persistRotatedTokens(client, home);\n const { token } = await client.getAccessToken();\n if (!token) {\n throw new Error(\"could not obtain a Google access token — the grant may have been revoked; re-link the account\");\n }\n return token;\n}\n\nconst REVOKE_URL = \"https://oauth2.googleapis.com/revoke\";\n\n/** The revoke POST, injectable for tests. */\nexport type RevokeFetch = typeof fetchWithTimeout;\n\n/** Revoke the grant at Google (best-effort) and delete the local token file.\n * Revoke failures are logged but never block the local delete — Google may\n * already consider the token invalid, and keeping the file would leave the\n * user unable to unlink. */\nexport async function unlinkGoogle(home?: string, revokeFetch: RevokeFetch = fetchWithTimeout): Promise<void> {\n const saved = await loadGoogleTokens(home);\n const token = saved?.refresh_token ?? saved?.access_token;\n if (token) {\n try {\n const response = await revokeFetch(REVOKE_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({ token }).toString(),\n });\n if (!response.ok) log.warn(\"google\", \"token revoke returned non-ok\", { status: response.status });\n } catch (err) {\n log.warn(\"google\", \"token revoke failed, deleting local tokens anyway\", { error: errorMessage(err) });\n }\n }\n await deleteGoogleTokens(home);\n}\n\nconst startLoopbackServer = (): Promise<{ server: http.Server; port: number }> =>\n new Promise((resolve, reject) => {\n const server = http.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n reject(new Error(\"loopback server has no port\"));\n return;\n }\n resolve({ server, port: address.port });\n });\n });\n\n// State is validated before error/code — a callback that can't prove it\n// belongs to this flow must not influence it (its `error` text would\n// otherwise reach the terminal attacker-controlled).\nconst authCodeFromCallback = (url: URL, expectedState: string): string => {\n if (url.searchParams.get(\"state\") !== expectedState) throw new Error(\"OAuth state mismatch — possible CSRF, aborting\");\n const error = url.searchParams.get(\"error\");\n if (error) throw new Error(`Google authorization failed: ${error}`);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"authorization callback carried no code\");\n return code;\n};\n\nconst respondHtml = (res: http.ServerResponse, status: number, message: string): void => {\n res.writeHead(status, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(`<html><body><h3>${message}</h3></body></html>`);\n};\n\nexport const waitForAuthCode = (server: http.Server, expectedState: string, timeoutMs: number): Promise<string> =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`authorization timed out after ${timeoutMs / ONE_SECOND_MS}s`)), timeoutMs);\n server.on(\"request\", (req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n if (url.pathname !== CALLBACK_PATH) {\n res.writeHead(404);\n res.end();\n return;\n }\n // A wrong-state request is not our callback (drive-by localhost probe\n // or stale tab) — answer it but keep waiting for the real redirect, so\n // an unauthenticated request can't abort the pending flow.\n if (url.searchParams.get(\"state\") !== expectedState) {\n respondHtml(res, 400, \"Invalid authorization callback. You can close this tab.\");\n return;\n }\n clearTimeout(timer);\n try {\n const code = authCodeFromCallback(url, expectedState);\n respondHtml(res, 200, \"Authorization complete — you can close this tab.\");\n resolve(code);\n } catch (err) {\n // Static text only — the failure detail echoes query-string content,\n // which must not be reflected into HTML. The CLI prints the detail.\n respondHtml(res, 400, \"Authorization failed — see the terminal for details. You can close this tab.\");\n reject(err);\n }\n });\n });\n\n// `access_type: offline` + `prompt: consent` force Google to return a refresh\n// token on every run (repeat consents otherwise omit it).\nconst buildConsentUrl = (client: OAuth2Client, codeChallenge: string, state: string): string =>\n client.generateAuthUrl({\n access_type: \"offline\",\n prompt: \"consent\",\n scope: GOOGLE_SCOPES,\n code_challenge_method: CodeChallengeMethod.S256,\n code_challenge: codeChallenge,\n state,\n });\n\nexport async function authorizeGoogle(opts: AuthorizeGoogleOptions = {}): Promise<Credentials> {\n const secret = await loadClientSecret(opts.home);\n const { server, port } = await startLoopbackServer();\n try {\n const client = createClient(secret, `http://127.0.0.1:${port}${CALLBACK_PATH}`);\n const { codeVerifier, codeChallenge } = await client.generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n const state = randomBytes(STATE_BYTES).toString(\"hex\");\n opts.onAuthUrl?.(buildConsentUrl(client, codeChallenge, state));\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n const { tokens } = await client.getToken({ code, codeVerifier });\n if (!tokens.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return await saveGoogleTokens(tokens, opts.home);\n } finally {\n server.close();\n }\n}\n","// In-flight manager for the settings-UI OAuth flow. authorizeGoogle()\n// resolves only after the user finishes the browser consent, so the HTTP\n// layer starts it in the background, returns the consent URL immediately,\n// and reports progress via status polling. One flow at a time — the guard\n// is the in-flight start promise itself (set synchronously before any\n// await), so concurrent authorize requests share one flow instead of\n// spawning parallel loopback listeners.\nimport { log } from \"./host.js\";\nimport { errorMessage } from \"./util.js\";\nimport { authorizeGoogle } from \"./auth.js\";\n\nexport interface GoogleAuthFlowStatus {\n pending: boolean;\n lastError: string | null;\n}\n\nexport interface GoogleAuthFlow {\n start: () => Promise<{ authUrl: string }>;\n status: () => GoogleAuthFlowStatus;\n}\n\nexport const createGoogleAuthFlow = (authorize: typeof authorizeGoogle): GoogleAuthFlow => {\n let inFlightStart: Promise<{ authUrl: string }> | null = null;\n let flowRunning = false;\n let lastError: string | null = null;\n\n const launchFlow = (): Promise<{ authUrl: string }> =>\n new Promise((resolve, reject) => {\n flowRunning = true;\n authorize({\n onAuthUrl: (url) => resolve({ authUrl: url }),\n })\n .then(() => log.info(\"google\", \"authorize flow completed\"))\n .catch((err: unknown) => {\n lastError = errorMessage(err);\n log.warn(\"google\", \"authorize flow failed\", { error: lastError });\n // No-op when the URL already resolved; covers pre-URL failures\n // (missing client secret, port bind error).\n reject(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n flowRunning = false;\n inFlightStart = null;\n });\n });\n\n const start = (): Promise<{ authUrl: string }> => {\n if (inFlightStart) return inFlightStart;\n lastError = null;\n inFlightStart = launchFlow();\n return inFlightStart;\n };\n\n const status = (): GoogleAuthFlowStatus => ({ pending: flowRunning, lastError });\n\n return { start, status };\n};\n\nexport const googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);\n","// Google Calendar v3 REST calls against the user's primary calendar. Plain\n// fetch instead of the `googleapis` SDK — two endpoints don't justify the\n// dependency (see plans/done/feat-google-oauth-calendar.md).\nimport { errorMessage, isRecord, ONE_SECOND_MS, truncate } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\n\nconst CALENDAR_EVENTS_URL = \"https://www.googleapis.com/calendar/v3/calendars/primary/events\";\nconst CALENDAR_TIMEOUT_MS = 30 * ONE_SECOND_MS;\nconst ERROR_BODY_MAX_CHARS = 300;\nconst HTTP_FORBIDDEN = 403;\nexport const DEFAULT_LIST_MAX_RESULTS = 10;\nexport const MAX_LIST_RESULTS = 50;\n\nexport interface CalendarEventInput {\n summary: string;\n startDateTime: string;\n endDateTime: string;\n description?: string;\n}\n\nexport interface ListEventsInput {\n timeMin?: string;\n maxResults?: number;\n}\n\nexport interface CalendarEventSummary {\n id: string;\n summary: string;\n start: string;\n end: string;\n htmlLink: string;\n status: string;\n}\n\n// All-day events carry `date`, timed events carry `dateTime`.\nconst eventTime = (value: unknown): string => {\n if (!isRecord(value)) return \"\";\n if (typeof value.dateTime === \"string\") return value.dateTime;\n if (typeof value.date === \"string\") return value.date;\n return \"\";\n};\n\nexport const toEventSummary = (value: unknown): CalendarEventSummary => {\n const record: Record<string, unknown> = isRecord(value) ? value : {};\n return {\n id: typeof record.id === \"string\" ? record.id : \"\",\n summary: typeof record.summary === \"string\" ? record.summary : \"\",\n start: eventTime(record.start),\n end: eventTime(record.end),\n htmlLink: typeof record.htmlLink === \"string\" ? record.htmlLink : \"\",\n status: typeof record.status === \"string\" ? record.status : \"\",\n };\n};\n\nexport const calendarApiError = (status: number, body: string): Error => {\n const hint = status === HTTP_FORBIDDEN ? \" (is the Google Calendar API enabled for the Cloud project?)\" : \"\";\n const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : \"\";\n return new Error(`Google Calendar API: HTTP ${status}${hint}${detail}`);\n};\n\nconst calendarRequest = async (accessToken: string, url: string, init: { method?: string; body?: string } = {}): Promise<unknown> => {\n const response = await fetchWithTimeout(url, {\n ...init,\n timeoutMs: CALENDAR_TIMEOUT_MS,\n headers: { Authorization: `Bearer ${accessToken}`, \"Content-Type\": \"application/json\" },\n });\n if (!response.ok) {\n const body = await response.text().catch((err: unknown) => errorMessage(err));\n throw calendarApiError(response.status, body);\n }\n return await response.json();\n};\n\nexport async function createCalendarEvent(accessToken: string, input: CalendarEventInput): Promise<CalendarEventSummary> {\n const body = {\n summary: input.summary,\n description: input.description,\n start: { dateTime: input.startDateTime },\n end: { dateTime: input.endDateTime },\n };\n const created = await calendarRequest(accessToken, CALENDAR_EVENTS_URL, { method: \"POST\", body: JSON.stringify(body) });\n return toEventSummary(created);\n}\n\nexport async function listCalendarEvents(accessToken: string, input: ListEventsInput = {}): Promise<CalendarEventSummary[]> {\n const params = new URLSearchParams({\n timeMin: input.timeMin ?? new Date().toISOString(),\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n singleEvents: \"true\",\n orderBy: \"startTime\",\n });\n const listed = await calendarRequest(accessToken, `${CALENDAR_EVENTS_URL}?${params.toString()}`);\n const items = isRecord(listed) && Array.isArray(listed.items) ? listed.items : [];\n return items.map(toEventSummary);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../src/google/host.ts","../../src/google/datetime.ts","../../src/google/paths.ts","../../src/google/util.ts","../../src/google/clientSecret.ts","../../src/google/fsJson.ts","../../src/google/tokenStore.ts","../../src/google/fetch.ts","../../src/google/auth.ts","../../src/google/authFlow.ts","../../src/google/apiClient.ts","../../src/google/calendar.ts","../../src/google/tasks.ts","../../src/google/driveFile.ts"],"sourcesContent":["// Host binding for the Google engine. The engine logs through the host's\n// logger, but a package-level import of the host logger would be an uphill\n// dependency — so the host injects it once at startup (same pattern as\n// `collection/server/host.ts`). The default is silent so the engine works\n// unconfigured in unit tests.\n\nexport interface GoogleLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nconst silentLogger: GoogleLogger = {\n error: () => undefined,\n warn: () => undefined,\n info: () => undefined,\n debug: () => undefined,\n};\n\nlet hostLog: GoogleLogger = silentLogger;\n\nexport function configureGoogleHost(binding: { log: GoogleLogger }): void {\n hostLog = binding.log;\n}\n\nexport const log: GoogleLogger = {\n error: (prefix, message, data) => hostLog.error(prefix, message, data),\n warn: (prefix, message, data) => hostLog.warn(prefix, message, data),\n info: (prefix, message, data) => hostLog.info(prefix, message, data),\n debug: (prefix, message, data) => hostLog.debug(prefix, message, data),\n};\n","// Strict RFC3339 date-time validation shared by every surface that feeds\n// Calendar `dateTime`/`timeMin` values (agent tool args, remote-host command\n// params). Calendar rejects date-only / offset-less values with an opaque\n// 400, so callers validate here and return an actionable message instead.\n\n// Fractional seconds are normalized away first — an optional `(\\.\\d+)?`\n// group inside the main pattern trips security/detect-unsafe-regex.\nconst ISO_DATE_TIME_WITH_OFFSET_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:Z|[+-](\\d{2}):(\\d{2}))$/;\nconst FRACTIONAL_SECONDS_RE = /\\.\\d+(?=Z|[+-])/;\n\nconst MAX_HOUR = 23;\nconst MAX_MINUTE = 59;\nconst MAX_SECOND = 59;\n\n// JS Date normalizes overflowed components (2026-02-31 parses as\n// 2026-03-03), so `new Date(value)` alone cannot reject impossible dates —\n// a UTC round-trip of the raw components can.\nconst isRealCalendarDate = (year: number, month: number, day: number): boolean => {\n const roundTrip = new Date(Date.UTC(year, month - 1, day));\n return roundTrip.getUTCFullYear() === year && roundTrip.getUTCMonth() === month - 1 && roundTrip.getUTCDate() === day;\n};\n\nexport const isIsoDateTimeWithOffset = (value: string): boolean => {\n const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE, \"\"));\n if (!match) return false;\n const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = match.slice(1, 7).map(Number);\n const timeInRange = hour <= MAX_HOUR && minute <= MAX_MINUTE && second <= MAX_SECOND;\n // RFC3339 bounds the offset to 00-23:59; groups 7/8 are undefined for \"Z\".\n const offsetInRange = match[7] === undefined || (Number(match[7]) <= MAX_HOUR && Number(match[8]) <= MAX_MINUTE);\n return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;\n};\n","// Google OAuth material lives OUTSIDE the workspace: the client secret is\n// machine-only (never synced) and the refresh token must survive workspace\n// resets — same reasoning as the gcloud / gh CLI model. The dir is the\n// host-NEUTRAL `~/.config/mulmo` because this engine is shared by both\n// MulmoClaude and MulmoTerminal, which deliberately share one grant per\n// machine. The `home` parameter exists so tests can thread a fake home.\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function googleConfigDir(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmo\");\n}\n\n/** Pre-0.20.1 token dir (mulmoclaude-branded); reads migrate away from it. */\nexport function legacyGoogleTokenPath(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmoclaude\", \"google-token.json\");\n}\n\nexport function googleTokenPath(home?: string): string {\n return join(googleConfigDir(home), \"google-token.json\");\n}\n\nexport function googleSecretsDir(home?: string): string {\n return join(home ?? homedir(), \".secrets\");\n}\n","// Small helpers ported from the host (`server/utils/{errors,text,time,types}.ts`)\n// so the Google engine carries no dependency on host utils — same convention\n// as `collection/registry/server/fetch.ts`.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport function errorMessage(err: unknown, fallback = \"unknown error\"): string {\n if (err instanceof Error) return err.message || fallback;\n const text = String(err);\n return text === \"\" || text === \"[object Object]\" ? fallback : text;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Clip a string to at most `max` chars; the ellipsis is included in the\n * budget so output never exceeds `max`. */\nexport function truncate(text: string, max: number, ellipsis = \"…\"): string {\n if (max <= 0) return \"\";\n if (text.length <= max) return text;\n return `${text.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}`;\n}\n","// Loads the Google OAuth desktop-app client credentials the user downloaded\n// from the Cloud Console into `~/.secrets/client_secret_*.json`. The file is\n// discovered by prefix so the user doesn't have to rename Google's long\n// default filename.\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { isRecord } from \"./util.js\";\nimport { googleSecretsDir } from \"./paths.js\";\n\nexport interface InstalledClientSecret {\n client_id: string;\n client_secret: string;\n}\n\nconst isInstalledClientSecret = (value: unknown): value is { installed: InstalledClientSecret } => {\n if (!isRecord(value) || !isRecord(value.installed)) return false;\n return typeof value.installed.client_id === \"string\" && typeof value.installed.client_secret === \"string\";\n};\n\nconst isClientSecretFileName = (name: string): boolean => name.startsWith(\"client_secret_\") && name.endsWith(\".json\");\n\nasync function listClientSecretFiles(home?: string): Promise<string[]> {\n const entries = await readdir(googleSecretsDir(home)).catch((): string[] => []);\n return entries.filter(isClientSecretFileName).sort();\n}\n\n/** \"ambiguous\" (2+ files) is distinct from \"missing\" — the fixes differ\n * (remove duplicates vs download credentials), so the UI must not conflate\n * them. */\nexport type ClientSecretPresence = \"found\" | \"missing\" | \"ambiguous\";\n\nexport async function clientSecretPresence(home?: string): Promise<ClientSecretPresence> {\n const matches = await listClientSecretFiles(home);\n if (matches.length === 0) return \"missing\";\n return matches.length === 1 ? \"found\" : \"ambiguous\";\n}\n\nexport async function findClientSecretPath(home?: string): Promise<string> {\n const dir = googleSecretsDir(home);\n const matches = await listClientSecretFiles(home);\n const [first, ...rest] = matches;\n if (!first) {\n throw new Error(\n `no client_secret_*.json found in ${dir} — download the OAuth desktop-app credentials JSON from the Google Cloud Console and place it there (mode 600)`,\n );\n }\n if (rest.length > 0) {\n // The stored refresh token is bound to one client_id; silently picking one\n // of several files could pair the token with the wrong client\n // (invalid_grant), so ambiguity is an error the user must resolve.\n throw new Error(`multiple client_secret_*.json files found in ${dir} (${matches.join(\", \")}) — keep exactly one`);\n }\n return join(dir, first);\n}\n\nexport async function loadClientSecret(home?: string): Promise<InstalledClientSecret> {\n const filePath = await findClientSecretPath(home);\n const raw = await readFile(filePath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (!isInstalledClientSecret(parsed)) {\n throw new Error(`${filePath} is not a desktop-app OAuth client secret (missing \"installed\" with client_id / client_secret)`);\n }\n return { client_id: parsed.installed.client_id, client_secret: parsed.installed.client_secret };\n}\n","// Minimal JSON file I/O for the token store. Unlike the host's\n// `writeJsonAtomic` (and core's collection `atomic.ts`), this one supports a\n// file mode — the token file must be 0600.\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\n\nexport async function readJsonOrNull<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsp.readFile(filePath, \"utf-8\");\n const parsed: T = JSON.parse(raw);\n return parsed;\n } catch {\n return null;\n }\n}\n\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300];\n\nconst hasErrnoCode = (err: unknown): err is { code: string } =>\n typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename\n// trips EPERM/EBUSY/EACCES. The retry is gated to Windows because POSIX EPERM\n// means a real permission problem — retrying would just delay the throw.\n// Ported from the host's server/utils/files/atomic.ts.\nconst isTransientRenameError = (err: unknown): boolean =>\n IS_WINDOWS && hasErrnoCode(err) && (err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\");\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsp.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n await fsp.rename(fromPath, toPath);\n}\n\n/** tmp-write + rename so readers never see a half-written file; `mode`\n * applies to the tmp file and survives the rename. */\nexport async function writeJsonAtomicWithMode(filePath: string, data: unknown, mode: number): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: \"utf-8\", mode });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","// Persistence for the Google OAuth tokens (refresh + access) at\n// `~/.config/mulmo/google-token.json`, mode 600. Google omits\n// `refresh_token` from refresh responses, so merges must preserve the one we\n// already hold — losing it forces the user through the browser consent again.\nimport { constants as fsConstants, copyFile, mkdir, rm, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Credentials } from \"google-auth-library\";\nimport { readJsonOrNull, writeJsonAtomicWithMode } from \"./fsJson.js\";\nimport { googleTokenPath, legacyGoogleTokenPath } from \"./paths.js\";\n\nconst TOKEN_FILE_MODE = 0o600;\n\nexport function mergeGoogleTokens(existing: Credentials | null, incoming: Credentials): Credentials {\n const merged = { ...existing, ...incoming };\n if (!incoming.refresh_token && existing?.refresh_token) merged.refresh_token = existing.refresh_token;\n return merged;\n}\n\nconst fileExists = async (filePath: string): Promise<boolean> =>\n await stat(filePath).then(\n () => true,\n () => false,\n );\n\n// Tokens written before 0.20.1 live under the mulmoclaude-branded dir; move\n// them once. COPYFILE_EXCL makes the create atomic-and-non-clobbering — an\n// exists+rename sequence could overwrite a token a concurrent process wrote\n// to the new path in between (TOCTOU). The legacy file is deleted only after\n// a successful copy; on EEXIST (new path won a race, or both files already\n// exist) it is left for any older install still reading it. copyFile\n// preserves the 600 mode.\nasync function migrateLegacyTokenFile(home?: string): Promise<void> {\n const current = googleTokenPath(home);\n const legacy = legacyGoogleTokenPath(home);\n if (!(await fileExists(legacy))) return;\n await mkdir(path.dirname(current), { recursive: true });\n try {\n await copyFile(legacy, current, fsConstants.COPYFILE_EXCL);\n } catch {\n return;\n }\n await rm(legacy, { force: true });\n}\n\nexport async function loadGoogleTokens(home?: string): Promise<Credentials | null> {\n await migrateLegacyTokenFile(home).catch(() => undefined);\n const current = await readJsonOrNull<Credentials>(googleTokenPath(home));\n if (current) return current;\n // Migration is best-effort — a valid legacy token must still count as\n // linked even when the move failed (permissions, read-only fs, …).\n return await readJsonOrNull<Credentials>(legacyGoogleTokenPath(home));\n}\n\nexport async function saveGoogleTokens(incoming: Credentials, home?: string): Promise<Credentials> {\n const merged = mergeGoogleTokens(await loadGoogleTokens(home), incoming);\n await writeJsonAtomicWithMode(googleTokenPath(home), merged, TOKEN_FILE_MODE);\n return merged;\n}\n\nexport async function deleteGoogleTokens(home?: string): Promise<void> {\n await rm(googleTokenPath(home), { force: true });\n}\n","// `fetch` with a finite timeout, ported from the host (`server/utils/fetch.ts`)\n// so the Google engine carries no dependency on host utils (same convention as\n// `collection/registry/server/fetch.ts`).\n\nimport { ONE_SECOND_MS } from \"./util.js\";\n\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/** `fetch` with a finite timeout. Rejects with a `TimeoutError` once\n * `timeoutMs` elapses. Composes with a caller-supplied `signal` so external\n * cancellation still works. */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n","// Google OAuth for the host machine, independent of Firebase Auth (which\n// discards refresh tokens). Entry points:\n// - authorizeGoogle(): one-shot loopback + PKCE browser consent flow\n// (desktop-app clients may redirect to any 127.0.0.1 port), storing the\n// refresh token locally via tokenStore.\n// - getGoogleAccessToken(): mints a fresh access token from the stored\n// refresh token; the OAuth2Client \"tokens\" event persists rotations.\n// - unlinkGoogle(): best-effort revoke at Google + local token delete.\nimport { randomBytes } from \"node:crypto\";\nimport http from \"node:http\";\nimport { CodeChallengeMethod, OAuth2Client, type Credentials } from \"google-auth-library\";\nimport { log } from \"./host.js\";\nimport { errorMessage, ONE_MINUTE_MS, ONE_SECOND_MS } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { loadClientSecret, type InstalledClientSecret } from \"./clientSecret.js\";\nimport { deleteGoogleTokens, loadGoogleTokens, saveGoogleTokens } from \"./tokenStore.js\";\n\nexport const GOOGLE_CALENDAR_SCOPE = \"https://www.googleapis.com/auth/calendar.events\";\nexport const GOOGLE_TASKS_SCOPE = \"https://www.googleapis.com/auth/tasks\";\nexport const GOOGLE_DRIVE_FILE_SCOPE = \"https://www.googleapis.com/auth/drive.file\";\n/** Requested at consent as one set — matches the scopes registered on the\n * OAuth consent screen, so a single re-link covers every supported API\n * (Calendar now; Tasks / Drive tools ride the same grant later). */\nexport const GOOGLE_SCOPES = [GOOGLE_CALENDAR_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE];\nconst CALLBACK_PATH = \"/oauth2callback\";\nconst AUTH_TIMEOUT_MS = 5 * ONE_MINUTE_MS;\nconst STATE_BYTES = 16;\n\nexport interface AuthorizeGoogleOptions {\n home?: string;\n /** Called with the consent URL; open it in a browser (and/or print it). */\n onAuthUrl?: (url: string) => void;\n timeoutMs?: number;\n}\n\nconst createClient = (secret: InstalledClientSecret, redirectUri?: string): OAuth2Client =>\n new OAuth2Client({ clientId: secret.client_id, clientSecret: secret.client_secret, redirectUri });\n\nconst persistRotatedTokens = (client: OAuth2Client, home?: string): void => {\n client.on(\"tokens\", (tokens) => {\n saveGoogleTokens(tokens, home).catch((err: unknown) => {\n log.error(\"google\", \"failed to persist rotated tokens\", { error: String(err) });\n });\n });\n};\n\nexport async function getGoogleAccessToken(home?: string): Promise<string> {\n const saved = await loadGoogleTokens(home);\n if (!saved?.refresh_token) {\n // Host-neutral wording — this engine ships to multiple hosts whose link\n // flows differ (#2128); each host's own help carries the specific steps.\n throw new Error(\"Google account not linked on this host — ask the user to link their Google account in this app's settings, then retry\");\n }\n const client = createClient(await loadClientSecret(home));\n client.setCredentials(saved);\n persistRotatedTokens(client, home);\n const { token } = await client.getAccessToken();\n if (!token) {\n throw new Error(\"could not obtain a Google access token — the grant may have been revoked; re-link the account\");\n }\n return token;\n}\n\nconst REVOKE_URL = \"https://oauth2.googleapis.com/revoke\";\n\n/** The revoke POST, injectable for tests. */\nexport type RevokeFetch = typeof fetchWithTimeout;\n\n/** Revoke the grant at Google (best-effort) and delete the local token file.\n * Revoke failures are logged but never block the local delete — Google may\n * already consider the token invalid, and keeping the file would leave the\n * user unable to unlink. */\nexport async function unlinkGoogle(home?: string, revokeFetch: RevokeFetch = fetchWithTimeout): Promise<void> {\n const saved = await loadGoogleTokens(home);\n const token = saved?.refresh_token ?? saved?.access_token;\n if (token) {\n try {\n const response = await revokeFetch(REVOKE_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({ token }).toString(),\n });\n if (!response.ok) log.warn(\"google\", \"token revoke returned non-ok\", { status: response.status });\n } catch (err) {\n log.warn(\"google\", \"token revoke failed, deleting local tokens anyway\", { error: errorMessage(err) });\n }\n }\n await deleteGoogleTokens(home);\n}\n\nconst startLoopbackServer = (): Promise<{ server: http.Server; port: number }> =>\n new Promise((resolve, reject) => {\n const server = http.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n reject(new Error(\"loopback server has no port\"));\n return;\n }\n resolve({ server, port: address.port });\n });\n });\n\n// State is validated before error/code — a callback that can't prove it\n// belongs to this flow must not influence it (its `error` text would\n// otherwise reach the terminal attacker-controlled).\nconst authCodeFromCallback = (url: URL, expectedState: string): string => {\n if (url.searchParams.get(\"state\") !== expectedState) throw new Error(\"OAuth state mismatch — possible CSRF, aborting\");\n const error = url.searchParams.get(\"error\");\n if (error) throw new Error(`Google authorization failed: ${error}`);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"authorization callback carried no code\");\n return code;\n};\n\nconst respondHtml = (res: http.ServerResponse, status: number, message: string): void => {\n res.writeHead(status, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(`<html><body><h3>${message}</h3></body></html>`);\n};\n\nexport const waitForAuthCode = (server: http.Server, expectedState: string, timeoutMs: number): Promise<string> =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`authorization timed out after ${timeoutMs / ONE_SECOND_MS}s`)), timeoutMs);\n server.on(\"request\", (req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n if (url.pathname !== CALLBACK_PATH) {\n res.writeHead(404);\n res.end();\n return;\n }\n // A wrong-state request is not our callback (drive-by localhost probe\n // or stale tab) — answer it but keep waiting for the real redirect, so\n // an unauthenticated request can't abort the pending flow.\n if (url.searchParams.get(\"state\") !== expectedState) {\n respondHtml(res, 400, \"Invalid authorization callback. You can close this tab.\");\n return;\n }\n clearTimeout(timer);\n try {\n const code = authCodeFromCallback(url, expectedState);\n respondHtml(res, 200, \"Authorization complete — you can close this tab.\");\n resolve(code);\n } catch (err) {\n // Static text only — the failure detail echoes query-string content,\n // which must not be reflected into HTML. The CLI prints the detail.\n respondHtml(res, 400, \"Authorization failed — see the terminal for details. You can close this tab.\");\n reject(err);\n }\n });\n });\n\n// `access_type: offline` + `prompt: consent` force Google to return a refresh\n// token on every run (repeat consents otherwise omit it).\nconst buildConsentUrl = (client: OAuth2Client, codeChallenge: string, state: string): string =>\n client.generateAuthUrl({\n access_type: \"offline\",\n prompt: \"consent\",\n scope: GOOGLE_SCOPES,\n code_challenge_method: CodeChallengeMethod.S256,\n code_challenge: codeChallenge,\n state,\n });\n\nexport async function authorizeGoogle(opts: AuthorizeGoogleOptions = {}): Promise<Credentials> {\n const secret = await loadClientSecret(opts.home);\n const { server, port } = await startLoopbackServer();\n try {\n const client = createClient(secret, `http://127.0.0.1:${port}${CALLBACK_PATH}`);\n const { codeVerifier, codeChallenge } = await client.generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n const state = randomBytes(STATE_BYTES).toString(\"hex\");\n opts.onAuthUrl?.(buildConsentUrl(client, codeChallenge, state));\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n const { tokens } = await client.getToken({ code, codeVerifier });\n if (!tokens.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return await saveGoogleTokens(tokens, opts.home);\n } finally {\n server.close();\n }\n}\n","// In-flight manager for the settings-UI OAuth flow. authorizeGoogle()\n// resolves only after the user finishes the browser consent, so the HTTP\n// layer starts it in the background, returns the consent URL immediately,\n// and reports progress via status polling. One flow at a time — the guard\n// is the in-flight start promise itself (set synchronously before any\n// await), so concurrent authorize requests share one flow instead of\n// spawning parallel loopback listeners.\nimport { log } from \"./host.js\";\nimport { errorMessage } from \"./util.js\";\nimport { authorizeGoogle } from \"./auth.js\";\n\nexport interface GoogleAuthFlowStatus {\n pending: boolean;\n lastError: string | null;\n}\n\nexport interface GoogleAuthFlow {\n start: () => Promise<{ authUrl: string }>;\n status: () => GoogleAuthFlowStatus;\n}\n\nexport const createGoogleAuthFlow = (authorize: typeof authorizeGoogle): GoogleAuthFlow => {\n let inFlightStart: Promise<{ authUrl: string }> | null = null;\n let flowRunning = false;\n let lastError: string | null = null;\n\n const launchFlow = (): Promise<{ authUrl: string }> =>\n new Promise((resolve, reject) => {\n flowRunning = true;\n authorize({\n onAuthUrl: (url) => resolve({ authUrl: url }),\n })\n .then(() => log.info(\"google\", \"authorize flow completed\"))\n .catch((err: unknown) => {\n lastError = errorMessage(err);\n log.warn(\"google\", \"authorize flow failed\", { error: lastError });\n // No-op when the URL already resolved; covers pre-URL failures\n // (missing client secret, port bind error).\n reject(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n flowRunning = false;\n inFlightStart = null;\n });\n });\n\n const start = (): Promise<{ authUrl: string }> => {\n if (inFlightStart) return inFlightStart;\n lastError = null;\n inFlightStart = launchFlow();\n return inFlightStart;\n };\n\n const status = (): GoogleAuthFlowStatus => ({ pending: flowRunning, lastError });\n\n return { start, status };\n};\n\nexport const googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);\n","// Shared REST plumbing for the Google APIs this engine wraps (Calendar,\n// Tasks, Drive). Plain fetch instead of the `googleapis` SDK — a handful of\n// endpoints don't justify the dependency (see\n// plans/done/feat-google-oauth-calendar.md).\nimport { errorMessage, isRecord, ONE_SECOND_MS, truncate } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\n\nexport const GOOGLE_API_TIMEOUT_MS = 30 * ONE_SECOND_MS;\nconst ERROR_BODY_MAX_CHARS = 300;\nconst HTTP_FORBIDDEN = 403;\nexport const DEFAULT_LIST_MAX_RESULTS = 10;\nexport const MAX_LIST_RESULTS = 50;\n\n/** 403 usually means the API is not enabled for the user's Cloud project —\n * name the API so the agent's recovery guidance can be specific. */\nexport const googleApiError = (apiLabel: string, status: number, body: string): Error => {\n const hint = status === HTTP_FORBIDDEN ? ` (is the ${apiLabel} enabled for the Cloud project?)` : \"\";\n const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : \"\";\n return new Error(`${apiLabel}: HTTP ${status}${hint}${detail}`);\n};\n\nexport interface GoogleRequestInit {\n method?: string;\n body?: string;\n /** Overrides the default JSON content type (multipart upload, …). */\n contentType?: string;\n /** Response is not JSON (Drive media download) — return the raw text. */\n expectText?: boolean;\n}\n\nexport async function googleRequest(apiLabel: string, accessToken: string, url: string, init: GoogleRequestInit = {}): Promise<unknown> {\n const { contentType = \"application/json\", expectText = false, ...rest } = init;\n const response = await fetchWithTimeout(url, {\n ...rest,\n timeoutMs: GOOGLE_API_TIMEOUT_MS,\n headers: { Authorization: `Bearer ${accessToken}`, \"Content-Type\": contentType },\n });\n if (!response.ok) {\n const body = await response.text().catch((err: unknown) => errorMessage(err));\n throw googleApiError(apiLabel, response.status, body);\n }\n if (expectText) return await response.text();\n // 204 (Drive delete, …) has no body to parse.\n if (response.status === 204) return {};\n return await response.json();\n}\n\nexport const stringField = (record: Record<string, unknown>, key: string): string => (typeof record[key] === \"string\" ? record[key] : \"\");\n\nexport const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});\n\nexport const itemsOf = (value: unknown): unknown[] => {\n const record = asRecord(value);\n return Array.isArray(record.items) ? record.items : [];\n};\n","// Google Calendar v3 REST calls against the user's primary calendar.\nimport { asRecord, googleApiError, googleRequest, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\nimport { isRecord } from \"./util.js\";\n\nconst CALENDAR_EVENTS_URL = \"https://www.googleapis.com/calendar/v3/calendars/primary/events\";\nconst CALENDAR_API_LABEL = \"Google Calendar API\";\n\nexport interface CalendarEventInput {\n summary: string;\n startDateTime: string;\n endDateTime: string;\n description?: string;\n}\n\nexport interface ListEventsInput {\n timeMin?: string;\n maxResults?: number;\n}\n\nexport interface CalendarEventSummary {\n id: string;\n summary: string;\n start: string;\n end: string;\n htmlLink: string;\n status: string;\n}\n\n// All-day events carry `date`, timed events carry `dateTime`.\nconst eventTime = (value: unknown): string => {\n if (!isRecord(value)) return \"\";\n if (typeof value.dateTime === \"string\") return value.dateTime;\n if (typeof value.date === \"string\") return value.date;\n return \"\";\n};\n\nexport const toEventSummary = (value: unknown): CalendarEventSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n summary: stringField(record, \"summary\"),\n start: eventTime(record.start),\n end: eventTime(record.end),\n htmlLink: stringField(record, \"htmlLink\"),\n status: stringField(record, \"status\"),\n };\n};\n\n/** Kept as a named export for the existing unit tests / callers; the shared\n * helper now carries the wording. */\nexport const calendarApiError = (status: number, body: string): Error => googleApiError(CALENDAR_API_LABEL, status, body);\n\nexport async function createCalendarEvent(accessToken: string, input: CalendarEventInput): Promise<CalendarEventSummary> {\n const body = {\n summary: input.summary,\n description: input.description,\n start: { dateTime: input.startDateTime },\n end: { dateTime: input.endDateTime },\n };\n const created = await googleRequest(CALENDAR_API_LABEL, accessToken, CALENDAR_EVENTS_URL, { method: \"POST\", body: JSON.stringify(body) });\n return toEventSummary(created);\n}\n\nexport async function listCalendarEvents(accessToken: string, input: ListEventsInput = {}): Promise<CalendarEventSummary[]> {\n const params = new URLSearchParams({\n timeMin: input.timeMin ?? new Date().toISOString(),\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n singleEvents: \"true\",\n orderBy: \"startTime\",\n });\n const listed = await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_EVENTS_URL}?${params.toString()}`);\n const record = asRecord(listed);\n const items = Array.isArray(record.items) ? record.items : [];\n return items.map(toEventSummary);\n}\n","// Google Tasks v1 REST calls. `@default` is Google's alias for the user's\n// default task list, so callers can stay list-agnostic.\nimport { asRecord, googleRequest, itemsOf, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst TASKS_BASE_URL = \"https://tasks.googleapis.com/tasks/v1\";\nconst TASKS_API_LABEL = \"Google Tasks API\";\nconst DEFAULT_TASK_LIST_ID = \"@default\";\nconst TASK_STATUS_COMPLETED = \"completed\";\nconst MAX_TASK_LISTS = 50;\n\nexport interface TaskListSummary {\n id: string;\n title: string;\n}\n\nexport interface TaskSummary {\n id: string;\n title: string;\n status: string;\n due: string;\n notes: string;\n}\n\nexport interface ListTasksInput {\n taskListId?: string;\n maxResults?: number;\n showCompleted?: boolean;\n}\n\nexport interface CreateTaskInput {\n title: string;\n notes?: string;\n /** RFC3339. Google stores a DATE only — the time part is recorded but\n * ignored by the UI, so callers should not promise time-of-day fidelity. */\n due?: string;\n taskListId?: string;\n}\n\nexport interface CompleteTaskInput {\n taskId: string;\n taskListId?: string;\n}\n\nexport const toTaskListSummary = (value: unknown): TaskListSummary => {\n const record = asRecord(value);\n return { id: stringField(record, \"id\"), title: stringField(record, \"title\") };\n};\n\nexport const toTaskSummary = (value: unknown): TaskSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n title: stringField(record, \"title\"),\n status: stringField(record, \"status\"),\n due: stringField(record, \"due\"),\n notes: stringField(record, \"notes\"),\n };\n};\n\nconst tasksUrl = (taskListId: string | undefined, suffix = \"\"): string =>\n `${TASKS_BASE_URL}/lists/${encodeURIComponent(taskListId ?? DEFAULT_TASK_LIST_ID)}/tasks${suffix}`;\n\nexport async function listTaskLists(accessToken: string): Promise<TaskListSummary[]> {\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${TASKS_BASE_URL}/users/@me/lists?maxResults=${MAX_TASK_LISTS}`);\n return itemsOf(listed).map(toTaskListSummary);\n}\n\nexport async function listTasks(accessToken: string, input: ListTasksInput = {}): Promise<TaskSummary[]> {\n const params = new URLSearchParams({\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n showCompleted: String(input.showCompleted ?? false),\n });\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${tasksUrl(input.taskListId)}?${params.toString()}`);\n return itemsOf(listed).map(toTaskSummary);\n}\n\nexport async function createTask(accessToken: string, input: CreateTaskInput): Promise<TaskSummary> {\n const body = { title: input.title, notes: input.notes, due: input.due };\n const created = await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId), { method: \"POST\", body: JSON.stringify(body) });\n return toTaskSummary(created);\n}\n\nexport async function completeTask(accessToken: string, input: CompleteTaskInput): Promise<TaskSummary> {\n // PATCH keeps the rest of the task intact — a PUT would need the full body\n // and would silently drop fields the caller never read.\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n const updated = await googleRequest(TASKS_API_LABEL, accessToken, url, {\n method: \"PATCH\",\n body: JSON.stringify({ status: TASK_STATUS_COMPLETED }),\n });\n return toTaskSummary(updated);\n}\n\nexport async function deleteTask(accessToken: string, input: CompleteTaskInput): Promise<void> {\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n await googleRequest(TASKS_API_LABEL, accessToken, url, { method: \"DELETE\" });\n}\n","// Google Drive v3 REST calls under the `drive.file` scope — the app only\n// ever sees files IT created, never the user's wider Drive. That narrow\n// scope is why this is non-sensitive and needs no Google verification\n// review; keep every call inside it.\nimport { randomBytes } from \"node:crypto\";\nimport { asRecord, googleRequest, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst DRIVE_FILES_URL = \"https://www.googleapis.com/drive/v3/files\";\nconst DRIVE_UPLOAD_URL = \"https://www.googleapis.com/upload/drive/v3/files\";\nconst DRIVE_API_LABEL = \"Google Drive API\";\nconst DEFAULT_MIME_TYPE = \"text/plain\";\nconst FILE_FIELDS = \"id,name,mimeType,webViewLink,modifiedTime\";\n// Reading a huge blob into a tool result would blow the context window; the\n// tool surface is for app-created text documents, not media.\nconst MAX_READ_CHARS = 100_000;\nconst TEXT_MIME_PREFIXES = [\"text/\", \"application/json\", \"application/xml\", \"application/javascript\"];\n\nexport interface DriveFileSummary {\n id: string;\n name: string;\n mimeType: string;\n webViewLink: string;\n modifiedTime: string;\n}\n\nexport interface ListDriveFilesInput {\n maxResults?: number;\n}\n\nexport interface CreateDriveFileInput {\n name: string;\n content: string;\n mimeType?: string;\n}\n\nexport interface ReadDriveFileInput {\n fileId: string;\n}\n\nexport const toDriveFileSummary = (value: unknown): DriveFileSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n name: stringField(record, \"name\"),\n mimeType: stringField(record, \"mimeType\"),\n webViewLink: stringField(record, \"webViewLink\"),\n modifiedTime: stringField(record, \"modifiedTime\"),\n };\n};\n\nexport const isTextMimeType = (mimeType: string): boolean => TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));\n\nexport async function listDriveFiles(accessToken: string, input: ListDriveFilesInput = {}): Promise<DriveFileSummary[]> {\n const params = new URLSearchParams({\n pageSize: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n fields: `files(${FILE_FIELDS})`,\n orderBy: \"modifiedTime desc\",\n });\n const listed = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}?${params.toString()}`);\n const record = asRecord(listed);\n const files = Array.isArray(record.files) ? record.files : [];\n return files.map(toDriveFileSummary);\n}\n\n// Multipart upload: metadata part + media part in one request. Built by hand\n// because the `googleapis` SDK is the only alternative and this is the sole\n// multipart call in the engine.\nconst BOUNDARY_BYTES = 16;\n\n/** Fresh per request: a fixed boundary appearing inside file content would\n * split the payload at the wrong place, so Drive would store a truncated or\n * mangled file. Random 128 bits makes an accidental — or crafted — collision\n * infeasible; `newMultipartBoundary` is re-derived until it is absent from\n * the body it must delimit. */\nconst newMultipartBoundary = (): string => `mulmo-drive-${randomBytes(BOUNDARY_BYTES).toString(\"hex\")}`;\n\n// RFC 2045 token + parameters: no CR/LF (header injection into the part\n// headers) and no quotes/semicolons that would let a crafted value forge\n// extra headers or parameters.\nconst MIME_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\\/[A-Za-z0-9!#$&^_.+-]+$/;\n\nexport const assertSafeMimeType = (mimeType: string): string => {\n if (!MIME_TYPE_RE.test(mimeType)) throw new Error(`Google Drive API: invalid mimeType '${mimeType}' — expected a plain type/subtype such as text/plain`);\n return mimeType;\n};\n\nexport const buildMultipartBody = (metadata: Record<string, string>, content: string, mimeType: string, boundary: string): string =>\n [\n `--${boundary}`,\n \"Content-Type: application/json; charset=UTF-8\",\n \"\",\n JSON.stringify(metadata),\n `--${boundary}`,\n `Content-Type: ${mimeType}`,\n \"\",\n content,\n `--${boundary}--`,\n \"\",\n ].join(\"\\r\\n\");\n\n/** A boundary that appears nowhere in the parts it delimits. */\nexport const pickBoundary = (parts: string[], generate: () => string = newMultipartBoundary): string => {\n const collides = (candidate: string): boolean => parts.some((part) => part.includes(candidate));\n let boundary = generate();\n while (collides(boundary)) boundary = generate();\n return boundary;\n};\n\nexport async function createDriveFile(accessToken: string, input: CreateDriveFileInput): Promise<DriveFileSummary> {\n const mimeType = assertSafeMimeType(input.mimeType ?? DEFAULT_MIME_TYPE);\n const metadata = { name: input.name, mimeType };\n const boundary = pickBoundary([input.content, JSON.stringify(metadata)]);\n const params = new URLSearchParams({ uploadType: \"multipart\", fields: FILE_FIELDS });\n const created = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_UPLOAD_URL}?${params.toString()}`, {\n method: \"POST\",\n contentType: `multipart/related; boundary=${boundary}`,\n body: buildMultipartBody(metadata, input.content, mimeType, boundary),\n });\n return toDriveFileSummary(created);\n}\n\nexport async function readDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<{ file: DriveFileSummary; content: string }> {\n const fileId = encodeURIComponent(input.fileId);\n const metadata = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?fields=${FILE_FIELDS}`);\n const file = toDriveFileSummary(metadata);\n if (!isTextMimeType(file.mimeType)) {\n throw new Error(`Google Drive API: '${file.name}' is ${file.mimeType || \"a binary file\"} — only text files can be read as content`);\n }\n const raw = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?alt=media`, { expectText: true });\n const content = typeof raw === \"string\" ? raw.slice(0, MAX_READ_CHARS) : \"\";\n return { file, content };\n}\n\nexport async function deleteDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<void> {\n await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: \"DELETE\" });\n}\n"],"mappings":";;;;;;;;;;;;AAoBA,IAAI,UAAwB;CAN1B,aAAa,KAAA;CACb,YAAY,KAAA;CACZ,YAAY,KAAA;CACZ,aAAa,KAAA;AAGa;AAE5B,SAAgB,oBAAoB,SAAsC;CACxE,UAAU,QAAQ;AACpB;AAEA,IAAa,MAAoB;CAC/B,QAAQ,QAAQ,SAAS,SAAS,QAAQ,MAAM,QAAQ,SAAS,IAAI;CACrE,OAAO,QAAQ,SAAS,SAAS,QAAQ,KAAK,QAAQ,SAAS,IAAI;CACnE,OAAO,QAAQ,SAAS,SAAS,QAAQ,KAAK,QAAQ,SAAS,IAAI;CACnE,QAAQ,QAAQ,SAAS,SAAS,QAAQ,MAAM,QAAQ,SAAS,IAAI;AACvE;;;ACxBA,IAAM,+BAA+B;AACrC,IAAM,wBAAwB;AAE9B,IAAM,WAAW;AACjB,IAAM,aAAa;AACnB,IAAM,aAAa;AAKnB,IAAM,sBAAsB,MAAc,OAAe,QAAyB;CAChF,MAAM,YAAY,IAAI,KAAK,KAAK,IAAI,MAAM,QAAQ,GAAG,GAAG,CAAC;CACzD,OAAO,UAAU,eAAe,MAAM,QAAQ,UAAU,YAAY,MAAM,QAAQ,KAAK,UAAU,WAAW,MAAM;AACpH;AAEA,IAAa,2BAA2B,UAA2B;CACjE,MAAM,QAAQ,6BAA6B,KAAK,MAAM,QAAQ,uBAAuB,EAAE,CAAC;CACxF,IAAI,CAAC,OAAO,OAAO;CACnB,MAAM,CAAC,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,KAAK,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,IAAI,MAAM;CACrG,MAAM,cAAc,QAAQ,YAAY,UAAU,cAAc,UAAU;CAE1E,MAAM,gBAAgB,MAAM,OAAO,KAAA,KAAc,OAAO,MAAM,EAAE,KAAK,YAAY,OAAO,MAAM,EAAE,KAAK;CACrG,OAAO,mBAAmB,MAAM,OAAO,GAAG,KAAK,eAAe;AAChE;;;ACrBA,SAAgB,gBAAgB,MAAuB;CACrD,QAAA,GAAA,UAAA,KAAA,CAAY,SAAA,GAAA,QAAA,QAAA,CAAgB,GAAG,WAAW,OAAO;AACnD;;AAGA,SAAgB,sBAAsB,MAAuB;CAC3D,QAAA,GAAA,UAAA,KAAA,CAAY,SAAA,GAAA,QAAA,QAAA,CAAgB,GAAG,WAAW,eAAe,mBAAmB;AAC9E;AAEA,SAAgB,gBAAgB,MAAuB;CACrD,QAAA,GAAA,UAAA,KAAA,CAAY,gBAAgB,IAAI,GAAG,mBAAmB;AACxD;AAEA,SAAgB,iBAAiB,MAAuB;CACtD,QAAA,GAAA,UAAA,KAAA,CAAY,SAAA,GAAA,QAAA,QAAA,CAAgB,GAAG,UAAU;AAC3C;;;ACpBA,IAAa,gBAAgB;AAC7B,IAAa,gBAAgB;AAE7B,SAAgB,aAAa,KAAc,WAAW,iBAAyB;CAC7E,IAAI,eAAe,OAAO,OAAO,IAAI,WAAW;CAChD,MAAM,OAAO,OAAO,GAAG;CACvB,OAAO,SAAS,MAAM,SAAS,oBAAoB,WAAW;AAChE;AAEA,SAAgB,SAAS,OAAkD;CACzE,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,CAAC,MAAM,QAAQ,KAAK;AAC5E;;;AAIA,SAAgB,SAAS,MAAc,KAAa,WAAW,KAAa;CAC1E,IAAI,OAAO,GAAG,OAAO;CACrB,IAAI,KAAK,UAAU,KAAK,OAAO;CAC/B,OAAO,GAAG,KAAK,MAAM,GAAG,KAAK,IAAI,GAAG,MAAM,SAAS,MAAM,CAAC,IAAI;AAChE;;;ACTA,IAAM,2BAA2B,UAAkE;CACjG,IAAI,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,MAAM,SAAS,GAAG,OAAO;CAC3D,OAAO,OAAO,MAAM,UAAU,cAAc,YAAY,OAAO,MAAM,UAAU,kBAAkB;AACnG;AAEA,IAAM,0BAA0B,SAA0B,KAAK,WAAW,gBAAgB,KAAK,KAAK,SAAS,OAAO;AAEpH,eAAe,sBAAsB,MAAkC;CAErE,QAAO,OAAA,GAAA,iBAAA,QAAA,CADuB,iBAAiB,IAAI,CAAC,CAAC,CAAC,YAAsB,CAAC,CAAC,EAAA,CAC/D,OAAO,sBAAsB,CAAC,CAAC,KAAK;AACrD;AAOA,eAAsB,qBAAqB,MAA8C;CACvF,MAAM,UAAU,MAAM,sBAAsB,IAAI;CAChD,IAAI,QAAQ,WAAW,GAAG,OAAO;CACjC,OAAO,QAAQ,WAAW,IAAI,UAAU;AAC1C;AAEA,eAAsB,qBAAqB,MAAgC;CACzE,MAAM,MAAM,iBAAiB,IAAI;CACjC,MAAM,UAAU,MAAM,sBAAsB,IAAI;CAChD,MAAM,CAAC,OAAO,GAAG,QAAQ;CACzB,IAAI,CAAC,OACH,MAAM,IAAI,MACR,oCAAoC,IAAI,+GAC1C;CAEF,IAAI,KAAK,SAAS,GAIhB,MAAM,IAAI,MAAM,gDAAgD,IAAI,IAAI,QAAQ,KAAK,IAAI,EAAE,qBAAqB;CAElH,QAAA,GAAA,UAAA,KAAA,CAAY,KAAK,KAAK;AACxB;AAEA,eAAsB,iBAAiB,MAA+C;CACpF,MAAM,WAAW,MAAM,qBAAqB,IAAI;CAChD,MAAM,MAAM,OAAA,GAAA,iBAAA,SAAA,CAAe,UAAU,OAAO;CAC5C,MAAM,SAAkB,KAAK,MAAM,GAAG;CACtC,IAAI,CAAC,wBAAwB,MAAM,GACjC,MAAM,IAAI,MAAM,GAAG,SAAS,+FAA+F;CAE7H,OAAO;EAAE,WAAW,OAAO,UAAU;EAAW,eAAe,OAAO,UAAU;CAAc;AAChG;;;ACzDA,eAAsB,eAAkB,UAAqC;CAC3E,IAAI;EACF,MAAM,MAAM,MAAM,QAAA,SAAI,SAAS,UAAU,OAAO;EAEhD,OADkB,KAAK,MAAM,GACtB;CACT,QAAQ;EACN,OAAO;CACT;AACF;AAEA,IAAM,aAAa,QAAQ,aAAa;AACxC,IAAM,yBAAyB;CAAC;CAAI;CAAK;AAAG;AAE5C,IAAM,gBAAgB,QACpB,OAAO,QAAQ,YAAY,QAAQ,QAAQ,UAAU,OAAO,OAAQ,IAA0B,SAAS;AAMzG,IAAM,0BAA0B,QAC9B,cAAc,aAAa,GAAG,MAAM,IAAI,SAAS,WAAW,IAAI,SAAS,WAAW,IAAI,SAAS;AAEnG,eAAe,uBAAuB,UAAkB,QAA+B;CACrF,KAAK,MAAM,WAAW,wBACpB,IAAI;EACF,MAAM,QAAA,SAAI,OAAO,UAAU,MAAM;EACjC;CACF,SAAS,KAAK;EACZ,IAAI,CAAC,uBAAuB,GAAG,GAAG,MAAM;EACxC,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,OAAO,CAAC;CAC7D;CAEF,MAAM,QAAA,SAAI,OAAO,UAAU,MAAM;AACnC;;;AAIA,eAAsB,wBAAwB,UAAkB,MAAe,MAA6B;CAC1G,MAAM,MAAM,GAAG,SAAS;CACxB,MAAM,QAAA,SAAI,MAAM,UAAA,QAAK,QAAQ,QAAQ,GAAG,EAAE,WAAW,KAAK,CAAC;CAC3D,IAAI;EACF,MAAM,QAAA,SAAI,UAAU,KAAK,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG;GAAE,UAAU;GAAS;EAAK,CAAC;EACnF,MAAM,uBAAuB,KAAK,QAAQ;CAC5C,SAAS,KAAK;EACZ,MAAM,QAAA,SAAI,OAAO,GAAG,CAAC,CAAC,YAAY,KAAA,CAAS;EAC3C,MAAM;CACR;AACF;;;AC5CA,IAAM,kBAAkB;AAExB,SAAgB,kBAAkB,UAA8B,UAAoC;CAClG,MAAM,SAAS;EAAE,GAAG;EAAU,GAAG;CAAS;CAC1C,IAAI,CAAC,SAAS,iBAAiB,UAAU,eAAe,OAAO,gBAAgB,SAAS;CACxF,OAAO;AACT;AAEA,IAAM,aAAa,OAAO,aACxB,OAAA,GAAA,iBAAA,KAAA,CAAW,QAAQ,CAAC,CAAC,WACb,YACA,KACR;AASF,eAAe,uBAAuB,MAA8B;CAClE,MAAM,UAAU,gBAAgB,IAAI;CACpC,MAAM,SAAS,sBAAsB,IAAI;CACzC,IAAI,CAAE,MAAM,WAAW,MAAM,GAAI;CACjC,OAAA,GAAA,iBAAA,MAAA,CAAY,UAAA,QAAK,QAAQ,OAAO,GAAG,EAAE,WAAW,KAAK,CAAC;CACtD,IAAI;EACF,OAAA,GAAA,iBAAA,SAAA,CAAe,QAAQ,SAAS,iBAAA,UAAY,aAAa;CAC3D,QAAQ;EACN;CACF;CACA,OAAA,GAAA,iBAAA,GAAA,CAAS,QAAQ,EAAE,OAAO,KAAK,CAAC;AAClC;AAEA,eAAsB,iBAAiB,MAA4C;CACjF,MAAM,uBAAuB,IAAI,CAAC,CAAC,YAAY,KAAA,CAAS;CACxD,MAAM,UAAU,MAAM,eAA4B,gBAAgB,IAAI,CAAC;CACvE,IAAI,SAAS,OAAO;CAGpB,OAAO,MAAM,eAA4B,sBAAsB,IAAI,CAAC;AACtE;AAEA,eAAsB,iBAAiB,UAAuB,MAAqC;CACjG,MAAM,SAAS,kBAAkB,MAAM,iBAAiB,IAAI,GAAG,QAAQ;CACvE,MAAM,wBAAwB,gBAAgB,IAAI,GAAG,QAAQ,eAAe;CAC5E,OAAO;AACT;AAEA,eAAsB,mBAAmB,MAA8B;CACrE,OAAA,GAAA,iBAAA,GAAA,CAAS,gBAAgB,IAAI,GAAG,EAAE,OAAO,KAAK,CAAC;AACjD;;;ACvDA,IAAa,2BAA2B,KAAK;;;;AAS7C,eAAsB,iBAAiB,KAAmB,OAA6B,CAAC,GAAsB;CAC5G,MAAM,EAAE,YAAY,0BAA0B,QAAQ,cAAc,GAAG,SAAS;CAEhF,IAAI,cAAc,SAChB,MAAM,aAAa,UAAU,IAAI,aAAa,WAAW,YAAY;CAGvE,MAAM,aAAa,IAAI,gBAAgB;CACvC,MAAM,QAAQ,iBAAiB;EAC7B,WAAW,MAAM,IAAI,aAAa,yBAAyB,UAAU,KAAK,cAAc,CAAC;CAC3F,GAAG,SAAS;CAEZ,MAAM,oBAAoB,qBAAqB,cAAc,UAAU;CAEvE,IAAI;EACF,OAAO,MAAM,MAAM,KAAK;GAAE,GAAG;GAAM,QAAQ,WAAW;EAAO,CAAC;CAChE,UAAU;EACR,aAAa,KAAK;EAClB,oBAAoB;CACtB;AACF;AAEA,SAAS,qBAAqB,UAA0C,YAAkD;CACxH,IAAI,CAAC,UAAU,OAAO;CACtB,MAAM,gBAAgB,WAAW,MAAM,SAAS,MAAM;CACtD,SAAS,iBAAiB,SAAS,SAAS,EAAE,MAAM,KAAK,CAAC;CAC1D,aAAa,SAAS,oBAAoB,SAAS,OAAO;AAC5D;;;ACzBA,IAAa,wBAAwB;AACrC,IAAa,qBAAqB;AAClC,IAAa,0BAA0B;;;;AAIvC,IAAa,gBAAgB;CAAC;CAAuB;CAAoB;AAAuB;AAChG,IAAM,gBAAgB;AACtB,IAAM,kBAAkB,IAAI;AAC5B,IAAM,cAAc;AASpB,IAAM,gBAAgB,QAA+B,gBACnD,IAAI,oBAAA,aAAa;CAAE,UAAU,OAAO;CAAW,cAAc,OAAO;CAAe;AAAY,CAAC;AAElG,IAAM,wBAAwB,QAAsB,SAAwB;CAC1E,OAAO,GAAG,WAAW,WAAW;EAC9B,iBAAiB,QAAQ,IAAI,CAAC,CAAC,OAAO,QAAiB;GACrD,IAAI,MAAM,UAAU,oCAAoC,EAAE,OAAO,OAAO,GAAG,EAAE,CAAC;EAChF,CAAC;CACH,CAAC;AACH;AAEA,eAAsB,qBAAqB,MAAgC;CACzE,MAAM,QAAQ,MAAM,iBAAiB,IAAI;CACzC,IAAI,CAAC,OAAO,eAGV,MAAM,IAAI,MAAM,uHAAuH;CAEzI,MAAM,SAAS,aAAa,MAAM,iBAAiB,IAAI,CAAC;CACxD,OAAO,eAAe,KAAK;CAC3B,qBAAqB,QAAQ,IAAI;CACjC,MAAM,EAAE,UAAU,MAAM,OAAO,eAAe;CAC9C,IAAI,CAAC,OACH,MAAM,IAAI,MAAM,+FAA+F;CAEjH,OAAO;AACT;AAEA,IAAM,aAAa;;;;;AASnB,eAAsB,aAAa,MAAe,cAA2B,kBAAiC;CAC5G,MAAM,QAAQ,MAAM,iBAAiB,IAAI;CACzC,MAAM,QAAQ,OAAO,iBAAiB,OAAO;CAC7C,IAAI,OACF,IAAI;EACF,MAAM,WAAW,MAAM,YAAY,YAAY;GAC7C,QAAQ;GACR,SAAS,EAAE,gBAAgB,oCAAoC;GAC/D,MAAM,IAAI,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS;EAChD,CAAC;EACD,IAAI,CAAC,SAAS,IAAI,IAAI,KAAK,UAAU,gCAAgC,EAAE,QAAQ,SAAS,OAAO,CAAC;CAClG,SAAS,KAAK;EACZ,IAAI,KAAK,UAAU,qDAAqD,EAAE,OAAO,aAAa,GAAG,EAAE,CAAC;CACtG;CAEF,MAAM,mBAAmB,IAAI;AAC/B;AAEA,IAAM,4BACJ,IAAI,SAAS,SAAS,WAAW;CAC/B,MAAM,SAAS,UAAA,QAAK,aAAa;CACjC,OAAO,KAAK,SAAS,MAAM;CAC3B,OAAO,OAAO,GAAG,mBAAmB;EAClC,MAAM,UAAU,OAAO,QAAQ;EAC/B,IAAI,YAAY,QAAQ,OAAO,YAAY,UAAU;GACnD,uBAAO,IAAI,MAAM,6BAA6B,CAAC;GAC/C;EACF;EACA,QAAQ;GAAE;GAAQ,MAAM,QAAQ;EAAK,CAAC;CACxC,CAAC;AACH,CAAC;AAKH,IAAM,wBAAwB,KAAU,kBAAkC;CACxE,IAAI,IAAI,aAAa,IAAI,OAAO,MAAM,eAAe,MAAM,IAAI,MAAM,gDAAgD;CACrH,MAAM,QAAQ,IAAI,aAAa,IAAI,OAAO;CAC1C,IAAI,OAAO,MAAM,IAAI,MAAM,gCAAgC,OAAO;CAClE,MAAM,OAAO,IAAI,aAAa,IAAI,MAAM;CACxC,IAAI,CAAC,MAAM,MAAM,IAAI,MAAM,wCAAwC;CACnE,OAAO;AACT;AAEA,IAAM,eAAe,KAA0B,QAAgB,YAA0B;CACvF,IAAI,UAAU,QAAQ,EAAE,gBAAgB,2BAA2B,CAAC;CACpE,IAAI,IAAI,mBAAmB,QAAQ,oBAAoB;AACzD;AAEA,IAAa,mBAAmB,QAAqB,eAAuB,cAC1E,IAAI,SAAS,SAAS,WAAW;CAC/B,MAAM,QAAQ,iBAAiB,uBAAO,IAAI,MAAM,iCAAiC,YAAY,cAAc,EAAE,CAAC,GAAG,SAAS;CAC1H,OAAO,GAAG,YAAY,KAAK,QAAQ;EACjC,MAAM,MAAM,IAAI,IAAI,IAAI,OAAO,KAAK,kBAAkB;EACtD,IAAI,IAAI,aAAa,eAAe;GAClC,IAAI,UAAU,GAAG;GACjB,IAAI,IAAI;GACR;EACF;EAIA,IAAI,IAAI,aAAa,IAAI,OAAO,MAAM,eAAe;GACnD,YAAY,KAAK,KAAK,yDAAyD;GAC/E;EACF;EACA,aAAa,KAAK;EAClB,IAAI;GACF,MAAM,OAAO,qBAAqB,KAAK,aAAa;GACpD,YAAY,KAAK,KAAK,kDAAkD;GACxE,QAAQ,IAAI;EACd,SAAS,KAAK;GAGZ,YAAY,KAAK,KAAK,8EAA8E;GACpG,OAAO,GAAG;EACZ;CACF,CAAC;AACH,CAAC;AAIH,IAAM,mBAAmB,QAAsB,eAAuB,UACpE,OAAO,gBAAgB;CACrB,aAAa;CACb,QAAQ;CACR,OAAO;CACP,uBAAuB,oBAAA,oBAAoB;CAC3C,gBAAgB;CAChB;AACF,CAAC;AAEH,eAAsB,gBAAgB,OAA+B,CAAC,GAAyB;CAC7F,MAAM,SAAS,MAAM,iBAAiB,KAAK,IAAI;CAC/C,MAAM,EAAE,QAAQ,SAAS,MAAM,oBAAoB;CACnD,IAAI;EACF,MAAM,SAAS,aAAa,QAAQ,oBAAoB,OAAO,eAAe;EAC9E,MAAM,EAAE,cAAc,kBAAkB,MAAM,OAAO,0BAA0B;EAC/E,IAAI,CAAC,eAAe,MAAM,IAAI,MAAM,wCAAwC;EAC5E,MAAM,SAAA,GAAA,YAAA,YAAA,CAAoB,WAAW,CAAC,CAAC,SAAS,KAAK;EACrD,KAAK,YAAY,gBAAgB,QAAQ,eAAe,KAAK,CAAC;EAC9D,MAAM,OAAO,MAAM,gBAAgB,QAAQ,OAAO,KAAK,aAAa,eAAe;EACnF,MAAM,EAAE,WAAW,MAAM,OAAO,SAAS;GAAE;GAAM;EAAa,CAAC;EAC/D,IAAI,CAAC,OAAO,eACV,MAAM,IAAI,MAAM,qHAAqH;EAEvI,OAAO,MAAM,iBAAiB,QAAQ,KAAK,IAAI;CACjD,UAAU;EACR,OAAO,MAAM;CACf;AACF;;;ACjKA,IAAa,wBAAwB,cAAsD;CACzF,IAAI,gBAAqD;CACzD,IAAI,cAAc;CAClB,IAAI,YAA2B;CAE/B,MAAM,mBACJ,IAAI,SAAS,SAAS,WAAW;EAC/B,cAAc;EACd,UAAU,EACR,YAAY,QAAQ,QAAQ,EAAE,SAAS,IAAI,CAAC,EAC9C,CAAC,CAAC,CACC,WAAW,IAAI,KAAK,UAAU,0BAA0B,CAAC,CAAC,CAC1D,OAAO,QAAiB;GACvB,YAAY,aAAa,GAAG;GAC5B,IAAI,KAAK,UAAU,yBAAyB,EAAE,OAAO,UAAU,CAAC;GAGhE,OAAO,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC,CAAC;EAC5D,CAAC,CAAC,CACD,cAAc;GACb,cAAc;GACd,gBAAgB;EAClB,CAAC;CACL,CAAC;CAEH,MAAM,cAA4C;EAChD,IAAI,eAAe,OAAO;EAC1B,YAAY;EACZ,gBAAgB,WAAW;EAC3B,OAAO;CACT;CAEA,MAAM,gBAAsC;EAAE,SAAS;EAAa;CAAU;CAE9E,OAAO;EAAE;EAAO;CAAO;AACzB;AAEA,IAAa,iBAAiB,qBAAqB,eAAe;;;ACnDlE,IAAa,wBAAwB,KAAK;AAC1C,IAAM,uBAAuB;AAC7B,IAAM,iBAAiB;AACvB,IAAa,2BAA2B;AACxC,IAAa,mBAAmB;;;AAIhC,IAAa,kBAAkB,UAAkB,QAAgB,SAAwB;CACvF,MAAM,OAAO,WAAW,iBAAiB,YAAY,SAAS,oCAAoC;CAClG,MAAM,SAAS,OAAO,MAAM,SAAS,MAAM,oBAAoB,MAAM;CACrE,uBAAO,IAAI,MAAM,GAAG,SAAS,SAAS,SAAS,OAAO,QAAQ;AAChE;AAWA,eAAsB,cAAc,UAAkB,aAAqB,KAAa,OAA0B,CAAC,GAAqB;CACtI,MAAM,EAAE,cAAc,oBAAoB,aAAa,OAAO,GAAG,SAAS;CAC1E,MAAM,WAAW,MAAM,iBAAiB,KAAK;EAC3C,GAAG;EACH,WAAW;EACX,SAAS;GAAE,eAAe,UAAU;GAAe,gBAAgB;EAAY;CACjF,CAAC;CACD,IAAI,CAAC,SAAS,IAAI;EAChB,MAAM,OAAO,MAAM,SAAS,KAAK,CAAC,CAAC,OAAO,QAAiB,aAAa,GAAG,CAAC;EAC5E,MAAM,eAAe,UAAU,SAAS,QAAQ,IAAI;CACtD;CACA,IAAI,YAAY,OAAO,MAAM,SAAS,KAAK;CAE3C,IAAI,SAAS,WAAW,KAAK,OAAO,CAAC;CACrC,OAAO,MAAM,SAAS,KAAK;AAC7B;AAEA,IAAa,eAAe,QAAiC,QAAyB,OAAO,OAAO,SAAS,WAAW,OAAO,OAAO;AAEtI,IAAa,YAAY,UAA6C,SAAS,KAAK,IAAI,QAAQ,CAAC;AAEjG,IAAa,WAAW,UAA8B;CACpD,MAAM,SAAS,SAAS,KAAK;CAC7B,OAAO,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,QAAQ,CAAC;AACvD;;;AClDA,IAAM,sBAAsB;AAC5B,IAAM,qBAAqB;AAwB3B,IAAM,aAAa,UAA2B;CAC5C,IAAI,CAAC,SAAS,KAAK,GAAG,OAAO;CAC7B,IAAI,OAAO,MAAM,aAAa,UAAU,OAAO,MAAM;CACrD,IAAI,OAAO,MAAM,SAAS,UAAU,OAAO,MAAM;CACjD,OAAO;AACT;AAEA,IAAa,kBAAkB,UAAyC;CACtE,MAAM,SAAS,SAAS,KAAK;CAC7B,OAAO;EACL,IAAI,YAAY,QAAQ,IAAI;EAC5B,SAAS,YAAY,QAAQ,SAAS;EACtC,OAAO,UAAU,OAAO,KAAK;EAC7B,KAAK,UAAU,OAAO,GAAG;EACzB,UAAU,YAAY,QAAQ,UAAU;EACxC,QAAQ,YAAY,QAAQ,QAAQ;CACtC;AACF;;;AAIA,IAAa,oBAAoB,QAAgB,SAAwB,eAAe,oBAAoB,QAAQ,IAAI;AAExH,eAAsB,oBAAoB,aAAqB,OAA0D;CACvH,MAAM,OAAO;EACX,SAAS,MAAM;EACf,aAAa,MAAM;EACnB,OAAO,EAAE,UAAU,MAAM,cAAc;EACvC,KAAK,EAAE,UAAU,MAAM,YAAY;CACrC;CAEA,OAAO,eAAe,MADA,cAAc,oBAAoB,aAAa,qBAAqB;EAAE,QAAQ;EAAQ,MAAM,KAAK,UAAU,IAAI;CAAE,CAAC,CAC3G;AAC/B;AAEA,eAAsB,mBAAmB,aAAqB,QAAyB,CAAC,GAAoC;CAQ1H,MAAM,SAAS,SAAS,MADH,cAAc,oBAAoB,aAAa,GAAG,oBAAoB,GAAG,IAN3E,gBAAgB;EACjC,SAAS,MAAM,4BAAW,IAAI,KAAK,EAAA,CAAE,YAAY;EACjD,YAAY,OAAO,MAAM,cAAA,EAAsC;EAC/D,cAAc;EACd,SAAS;CACX,CAC8F,CAAA,CAAO,SAAS,GAAG,CACnF;CAE9B,QADc,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,QAAQ,CAAC,EAAA,CAC/C,IAAI,cAAc;AACjC;;;ACtEA,IAAM,iBAAiB;AACvB,IAAM,kBAAkB;AACxB,IAAM,uBAAuB;AAC7B,IAAM,wBAAwB;AAC9B,IAAM,iBAAiB;AAmCvB,IAAa,qBAAqB,UAAoC;CACpE,MAAM,SAAS,SAAS,KAAK;CAC7B,OAAO;EAAE,IAAI,YAAY,QAAQ,IAAI;EAAG,OAAO,YAAY,QAAQ,OAAO;CAAE;AAC9E;AAEA,IAAa,iBAAiB,UAAgC;CAC5D,MAAM,SAAS,SAAS,KAAK;CAC7B,OAAO;EACL,IAAI,YAAY,QAAQ,IAAI;EAC5B,OAAO,YAAY,QAAQ,OAAO;EAClC,QAAQ,YAAY,QAAQ,QAAQ;EACpC,KAAK,YAAY,QAAQ,KAAK;EAC9B,OAAO,YAAY,QAAQ,OAAO;CACpC;AACF;AAEA,IAAM,YAAY,YAAgC,SAAS,OACzD,GAAG,eAAe,SAAS,mBAAmB,cAAc,oBAAoB,EAAE,QAAQ;AAE5F,eAAsB,cAAc,aAAiD;CAEnF,OAAO,QAAQ,MADM,cAAc,iBAAiB,aAAa,GAAG,eAAe,8BAA8B,gBAAgB,CAC5G,CAAC,CAAC,IAAI,iBAAiB;AAC9C;AAEA,eAAsB,UAAU,aAAqB,QAAwB,CAAC,GAA2B;CACvG,MAAM,SAAS,IAAI,gBAAgB;EACjC,YAAY,OAAO,MAAM,cAAA,EAAsC;EAC/D,eAAe,OAAO,MAAM,iBAAiB,KAAK;CACpD,CAAC;CAED,OAAO,QAAQ,MADM,cAAc,iBAAiB,aAAa,GAAG,SAAS,MAAM,UAAU,EAAE,GAAG,OAAO,SAAS,GAAG,CAChG,CAAC,CAAC,IAAI,aAAa;AAC1C;AAEA,eAAsB,WAAW,aAAqB,OAA8C;CAClG,MAAM,OAAO;EAAE,OAAO,MAAM;EAAO,OAAO,MAAM;EAAO,KAAK,MAAM;CAAI;CAEtE,OAAO,cAAc,MADC,cAAc,iBAAiB,aAAa,SAAS,MAAM,UAAU,GAAG;EAAE,QAAQ;EAAQ,MAAM,KAAK,UAAU,IAAI;CAAE,CAAC,CAChH;AAC9B;AAEA,eAAsB,aAAa,aAAqB,OAAgD;CAQtG,OAAO,cAAc,MAJC,cAAc,iBAAiB,aADzC,SAAS,MAAM,YAAY,IAAI,mBAAmB,MAAM,MAAM,GACR,GAAK;EACrE,QAAQ;EACR,MAAM,KAAK,UAAU,EAAE,QAAQ,sBAAsB,CAAC;CACxD,CAAC,CAC2B;AAC9B;AAEA,eAAsB,WAAW,aAAqB,OAAyC;CAE7F,MAAM,cAAc,iBAAiB,aADzB,SAAS,MAAM,YAAY,IAAI,mBAAmB,MAAM,MAAM,GACxB,GAAK,EAAE,QAAQ,SAAS,CAAC;AAC7E;;;ACzFA,IAAM,kBAAkB;AACxB,IAAM,mBAAmB;AACzB,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAC1B,IAAM,cAAc;AAGpB,IAAM,iBAAiB;AACvB,IAAM,qBAAqB;CAAC;CAAS;CAAoB;CAAmB;AAAwB;AAwBpG,IAAa,sBAAsB,UAAqC;CACtE,MAAM,SAAS,SAAS,KAAK;CAC7B,OAAO;EACL,IAAI,YAAY,QAAQ,IAAI;EAC5B,MAAM,YAAY,QAAQ,MAAM;EAChC,UAAU,YAAY,QAAQ,UAAU;EACxC,aAAa,YAAY,QAAQ,aAAa;EAC9C,cAAc,YAAY,QAAQ,cAAc;CAClD;AACF;AAEA,IAAa,kBAAkB,aAA8B,mBAAmB,MAAM,WAAW,SAAS,WAAW,MAAM,CAAC;AAE5H,eAAsB,eAAe,aAAqB,QAA6B,CAAC,GAAgC;CAOtH,MAAM,SAAS,SAAS,MADH,cAAc,iBAAiB,aAAa,GAAG,gBAAgB,GAAG,IALpE,gBAAgB;EACjC,UAAU,OAAO,MAAM,cAAA,EAAsC;EAC7D,QAAQ,SAAS,YAAY;EAC7B,SAAS;CACX,CACuF,CAAA,CAAO,SAAS,GAAG,CAC5E;CAE9B,QADc,MAAM,QAAQ,OAAO,KAAK,IAAI,OAAO,QAAQ,CAAC,EAAA,CAC/C,IAAI,kBAAkB;AACrC;AAKA,IAAM,iBAAiB;;;;;;AAOvB,IAAM,6BAAqC,gBAAA,GAAA,YAAA,YAAA,CAA2B,cAAc,CAAC,CAAC,SAAS,KAAK;AAKpG,IAAM,eAAe;AAErB,IAAa,sBAAsB,aAA6B;CAC9D,IAAI,CAAC,aAAa,KAAK,QAAQ,GAAG,MAAM,IAAI,MAAM,uCAAuC,SAAS,qDAAqD;CACvJ,OAAO;AACT;AAEA,IAAa,sBAAsB,UAAkC,SAAiB,UAAkB,aACtG;CACE,KAAK;CACL;CACA;CACA,KAAK,UAAU,QAAQ;CACvB,KAAK;CACL,iBAAiB;CACjB;CACA;CACA,KAAK,SAAS;CACd;AACF,CAAC,CAAC,KAAK,MAAM;;AAGf,IAAa,gBAAgB,OAAiB,WAAyB,yBAAiC;CACtG,MAAM,YAAY,cAA+B,MAAM,MAAM,SAAS,KAAK,SAAS,SAAS,CAAC;CAC9F,IAAI,WAAW,SAAS;CACxB,OAAO,SAAS,QAAQ,GAAG,WAAW,SAAS;CAC/C,OAAO;AACT;AAEA,eAAsB,gBAAgB,aAAqB,OAAwD;CACjH,MAAM,WAAW,mBAAmB,MAAM,YAAY,iBAAiB;CACvE,MAAM,WAAW;EAAE,MAAM,MAAM;EAAM;CAAS;CAC9C,MAAM,WAAW,aAAa,CAAC,MAAM,SAAS,KAAK,UAAU,QAAQ,CAAC,CAAC;CAOvE,OAAO,mBAAmB,MALJ,cAAc,iBAAiB,aAAa,GAAG,iBAAiB,GAAG,IADtE,gBAAgB;EAAE,YAAY;EAAa,QAAQ;CAAY,CACO,CAAA,CAAO,SAAS,KAAK;EAC5G,QAAQ;EACR,aAAa,+BAA+B;EAC5C,MAAM,mBAAmB,UAAU,MAAM,SAAS,UAAU,QAAQ;CACtE,CAAC,CACgC;AACnC;AAEA,eAAsB,cAAc,aAAqB,OAAiF;CACxI,MAAM,SAAS,mBAAmB,MAAM,MAAM;CAE9C,MAAM,OAAO,mBAAmB,MADT,cAAc,iBAAiB,aAAa,GAAG,gBAAgB,GAAG,OAAO,UAAU,aAAa,CAC/E;CACxC,IAAI,CAAC,eAAe,KAAK,QAAQ,GAC/B,MAAM,IAAI,MAAM,sBAAsB,KAAK,KAAK,OAAO,KAAK,YAAY,gBAAgB,0CAA0C;CAEpI,MAAM,MAAM,MAAM,cAAc,iBAAiB,aAAa,GAAG,gBAAgB,GAAG,OAAO,aAAa,EAAE,YAAY,KAAK,CAAC;CAE5H,OAAO;EAAE;EAAM,SADC,OAAO,QAAQ,WAAW,IAAI,MAAM,GAAG,cAAc,IAAI;CAClD;AACzB;AAEA,eAAsB,gBAAgB,aAAqB,OAA0C;CACnG,MAAM,cAAc,iBAAiB,aAAa,GAAG,gBAAgB,GAAG,mBAAmB,MAAM,MAAM,KAAK,EAAE,QAAQ,SAAS,CAAC;AAClI"}
@@ -5,4 +5,7 @@ export { clientSecretPresence, findClientSecretPath, loadClientSecret, type Clie
5
5
  export { deleteGoogleTokens, loadGoogleTokens, mergeGoogleTokens, saveGoogleTokens } from './tokenStore.js';
6
6
  export { authorizeGoogle, getGoogleAccessToken, unlinkGoogle, waitForAuthCode, GOOGLE_CALENDAR_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, type AuthorizeGoogleOptions, type RevokeFetch, } from './auth.js';
7
7
  export { createGoogleAuthFlow, googleAuthFlow, type GoogleAuthFlow, type GoogleAuthFlowStatus } from './authFlow.js';
8
- export { calendarApiError, createCalendarEvent, listCalendarEvents, toEventSummary, DEFAULT_LIST_MAX_RESULTS, MAX_LIST_RESULTS, type CalendarEventInput, type CalendarEventSummary, type ListEventsInput, } from './calendar.js';
8
+ export { googleApiError, DEFAULT_LIST_MAX_RESULTS, MAX_LIST_RESULTS } from './apiClient.js';
9
+ export { calendarApiError, createCalendarEvent, listCalendarEvents, toEventSummary, type CalendarEventInput, type CalendarEventSummary, type ListEventsInput, } from './calendar.js';
10
+ export { completeTask, createTask, deleteTask, listTaskLists, listTasks, toTaskListSummary, toTaskSummary, type CompleteTaskInput, type CreateTaskInput, type ListTasksInput, type TaskListSummary, type TaskSummary, } from './tasks.js';
11
+ export { assertSafeMimeType, buildMultipartBody, createDriveFile, deleteDriveFile, isTextMimeType, listDriveFiles, pickBoundary, readDriveFile, toDriveFileSummary, type CreateDriveFileInput, type DriveFileSummary, type ListDriveFilesInput, type ReadDriveFileInput, } from './driveFile.js';
@@ -390,50 +390,67 @@ var createGoogleAuthFlow = (authorize) => {
390
390
  };
391
391
  var googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);
392
392
  //#endregion
393
- //#region src/google/calendar.ts
394
- var CALENDAR_EVENTS_URL = "https://www.googleapis.com/calendar/v3/calendars/primary/events";
395
- var CALENDAR_TIMEOUT_MS = 30 * ONE_SECOND_MS;
393
+ //#region src/google/apiClient.ts
394
+ var GOOGLE_API_TIMEOUT_MS = 30 * ONE_SECOND_MS;
396
395
  var ERROR_BODY_MAX_CHARS = 300;
397
396
  var HTTP_FORBIDDEN = 403;
398
397
  var DEFAULT_LIST_MAX_RESULTS = 10;
399
398
  var MAX_LIST_RESULTS = 50;
400
- var eventTime = (value) => {
401
- if (!isRecord(value)) return "";
402
- if (typeof value.dateTime === "string") return value.dateTime;
403
- if (typeof value.date === "string") return value.date;
404
- return "";
405
- };
406
- var toEventSummary = (value) => {
407
- const record = isRecord(value) ? value : {};
408
- return {
409
- id: typeof record.id === "string" ? record.id : "",
410
- summary: typeof record.summary === "string" ? record.summary : "",
411
- start: eventTime(record.start),
412
- end: eventTime(record.end),
413
- htmlLink: typeof record.htmlLink === "string" ? record.htmlLink : "",
414
- status: typeof record.status === "string" ? record.status : ""
415
- };
416
- };
417
- var calendarApiError = (status, body) => {
418
- const hint = status === HTTP_FORBIDDEN ? " (is the Google Calendar API enabled for the Cloud project?)" : "";
399
+ /** 403 usually means the API is not enabled for the user's Cloud project —
400
+ * name the API so the agent's recovery guidance can be specific. */
401
+ var googleApiError = (apiLabel, status, body) => {
402
+ const hint = status === HTTP_FORBIDDEN ? ` (is the ${apiLabel} enabled for the Cloud project?)` : "";
419
403
  const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : "";
420
- return /* @__PURE__ */ new Error(`Google Calendar API: HTTP ${status}${hint}${detail}`);
404
+ return /* @__PURE__ */ new Error(`${apiLabel}: HTTP ${status}${hint}${detail}`);
421
405
  };
422
- var calendarRequest = async (accessToken, url, init = {}) => {
406
+ async function googleRequest(apiLabel, accessToken, url, init = {}) {
407
+ const { contentType = "application/json", expectText = false, ...rest } = init;
423
408
  const response = await fetchWithTimeout(url, {
424
- ...init,
425
- timeoutMs: CALENDAR_TIMEOUT_MS,
409
+ ...rest,
410
+ timeoutMs: GOOGLE_API_TIMEOUT_MS,
426
411
  headers: {
427
412
  Authorization: `Bearer ${accessToken}`,
428
- "Content-Type": "application/json"
413
+ "Content-Type": contentType
429
414
  }
430
415
  });
431
416
  if (!response.ok) {
432
417
  const body = await response.text().catch((err) => errorMessage(err));
433
- throw calendarApiError(response.status, body);
418
+ throw googleApiError(apiLabel, response.status, body);
434
419
  }
420
+ if (expectText) return await response.text();
421
+ if (response.status === 204) return {};
435
422
  return await response.json();
423
+ }
424
+ var stringField = (record, key) => typeof record[key] === "string" ? record[key] : "";
425
+ var asRecord = (value) => isRecord(value) ? value : {};
426
+ var itemsOf = (value) => {
427
+ const record = asRecord(value);
428
+ return Array.isArray(record.items) ? record.items : [];
436
429
  };
430
+ //#endregion
431
+ //#region src/google/calendar.ts
432
+ var CALENDAR_EVENTS_URL = "https://www.googleapis.com/calendar/v3/calendars/primary/events";
433
+ var CALENDAR_API_LABEL = "Google Calendar API";
434
+ var eventTime = (value) => {
435
+ if (!isRecord(value)) return "";
436
+ if (typeof value.dateTime === "string") return value.dateTime;
437
+ if (typeof value.date === "string") return value.date;
438
+ return "";
439
+ };
440
+ var toEventSummary = (value) => {
441
+ const record = asRecord(value);
442
+ return {
443
+ id: stringField(record, "id"),
444
+ summary: stringField(record, "summary"),
445
+ start: eventTime(record.start),
446
+ end: eventTime(record.end),
447
+ htmlLink: stringField(record, "htmlLink"),
448
+ status: stringField(record, "status")
449
+ };
450
+ };
451
+ /** Kept as a named export for the existing unit tests / callers; the shared
452
+ * helper now carries the wording. */
453
+ var calendarApiError = (status, body) => googleApiError(CALENDAR_API_LABEL, status, body);
437
454
  async function createCalendarEvent(accessToken, input) {
438
455
  const body = {
439
456
  summary: input.summary,
@@ -441,21 +458,169 @@ async function createCalendarEvent(accessToken, input) {
441
458
  start: { dateTime: input.startDateTime },
442
459
  end: { dateTime: input.endDateTime }
443
460
  };
444
- return toEventSummary(await calendarRequest(accessToken, CALENDAR_EVENTS_URL, {
461
+ return toEventSummary(await googleRequest(CALENDAR_API_LABEL, accessToken, CALENDAR_EVENTS_URL, {
445
462
  method: "POST",
446
463
  body: JSON.stringify(body)
447
464
  }));
448
465
  }
449
466
  async function listCalendarEvents(accessToken, input = {}) {
450
- const listed = await calendarRequest(accessToken, `${CALENDAR_EVENTS_URL}?${new URLSearchParams({
467
+ const record = asRecord(await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_EVENTS_URL}?${new URLSearchParams({
451
468
  timeMin: input.timeMin ?? (/* @__PURE__ */ new Date()).toISOString(),
452
469
  maxResults: String(input.maxResults ?? 10),
453
470
  singleEvents: "true",
454
471
  orderBy: "startTime"
455
- }).toString()}`);
456
- return (isRecord(listed) && Array.isArray(listed.items) ? listed.items : []).map(toEventSummary);
472
+ }).toString()}`));
473
+ return (Array.isArray(record.items) ? record.items : []).map(toEventSummary);
474
+ }
475
+ //#endregion
476
+ //#region src/google/tasks.ts
477
+ var TASKS_BASE_URL = "https://tasks.googleapis.com/tasks/v1";
478
+ var TASKS_API_LABEL = "Google Tasks API";
479
+ var DEFAULT_TASK_LIST_ID = "@default";
480
+ var TASK_STATUS_COMPLETED = "completed";
481
+ var MAX_TASK_LISTS = 50;
482
+ var toTaskListSummary = (value) => {
483
+ const record = asRecord(value);
484
+ return {
485
+ id: stringField(record, "id"),
486
+ title: stringField(record, "title")
487
+ };
488
+ };
489
+ var toTaskSummary = (value) => {
490
+ const record = asRecord(value);
491
+ return {
492
+ id: stringField(record, "id"),
493
+ title: stringField(record, "title"),
494
+ status: stringField(record, "status"),
495
+ due: stringField(record, "due"),
496
+ notes: stringField(record, "notes")
497
+ };
498
+ };
499
+ var tasksUrl = (taskListId, suffix = "") => `${TASKS_BASE_URL}/lists/${encodeURIComponent(taskListId ?? DEFAULT_TASK_LIST_ID)}/tasks${suffix}`;
500
+ async function listTaskLists(accessToken) {
501
+ return itemsOf(await googleRequest(TASKS_API_LABEL, accessToken, `${TASKS_BASE_URL}/users/@me/lists?maxResults=${MAX_TASK_LISTS}`)).map(toTaskListSummary);
502
+ }
503
+ async function listTasks(accessToken, input = {}) {
504
+ const params = new URLSearchParams({
505
+ maxResults: String(input.maxResults ?? 10),
506
+ showCompleted: String(input.showCompleted ?? false)
507
+ });
508
+ return itemsOf(await googleRequest(TASKS_API_LABEL, accessToken, `${tasksUrl(input.taskListId)}?${params.toString()}`)).map(toTaskSummary);
509
+ }
510
+ async function createTask(accessToken, input) {
511
+ const body = {
512
+ title: input.title,
513
+ notes: input.notes,
514
+ due: input.due
515
+ };
516
+ return toTaskSummary(await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId), {
517
+ method: "POST",
518
+ body: JSON.stringify(body)
519
+ }));
520
+ }
521
+ async function completeTask(accessToken, input) {
522
+ return toTaskSummary(await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`), {
523
+ method: "PATCH",
524
+ body: JSON.stringify({ status: TASK_STATUS_COMPLETED })
525
+ }));
526
+ }
527
+ async function deleteTask(accessToken, input) {
528
+ await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`), { method: "DELETE" });
529
+ }
530
+ //#endregion
531
+ //#region src/google/driveFile.ts
532
+ var DRIVE_FILES_URL = "https://www.googleapis.com/drive/v3/files";
533
+ var DRIVE_UPLOAD_URL = "https://www.googleapis.com/upload/drive/v3/files";
534
+ var DRIVE_API_LABEL = "Google Drive API";
535
+ var DEFAULT_MIME_TYPE = "text/plain";
536
+ var FILE_FIELDS = "id,name,mimeType,webViewLink,modifiedTime";
537
+ var MAX_READ_CHARS = 1e5;
538
+ var TEXT_MIME_PREFIXES = [
539
+ "text/",
540
+ "application/json",
541
+ "application/xml",
542
+ "application/javascript"
543
+ ];
544
+ var toDriveFileSummary = (value) => {
545
+ const record = asRecord(value);
546
+ return {
547
+ id: stringField(record, "id"),
548
+ name: stringField(record, "name"),
549
+ mimeType: stringField(record, "mimeType"),
550
+ webViewLink: stringField(record, "webViewLink"),
551
+ modifiedTime: stringField(record, "modifiedTime")
552
+ };
553
+ };
554
+ var isTextMimeType = (mimeType) => TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));
555
+ async function listDriveFiles(accessToken, input = {}) {
556
+ const record = asRecord(await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}?${new URLSearchParams({
557
+ pageSize: String(input.maxResults ?? 10),
558
+ fields: `files(${FILE_FIELDS})`,
559
+ orderBy: "modifiedTime desc"
560
+ }).toString()}`));
561
+ return (Array.isArray(record.files) ? record.files : []).map(toDriveFileSummary);
562
+ }
563
+ var BOUNDARY_BYTES = 16;
564
+ /** Fresh per request: a fixed boundary appearing inside file content would
565
+ * split the payload at the wrong place, so Drive would store a truncated or
566
+ * mangled file. Random 128 bits makes an accidental — or crafted — collision
567
+ * infeasible; `newMultipartBoundary` is re-derived until it is absent from
568
+ * the body it must delimit. */
569
+ var newMultipartBoundary = () => `mulmo-drive-${randomBytes(BOUNDARY_BYTES).toString("hex")}`;
570
+ var MIME_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+$/;
571
+ var assertSafeMimeType = (mimeType) => {
572
+ if (!MIME_TYPE_RE.test(mimeType)) throw new Error(`Google Drive API: invalid mimeType '${mimeType}' — expected a plain type/subtype such as text/plain`);
573
+ return mimeType;
574
+ };
575
+ var buildMultipartBody = (metadata, content, mimeType, boundary) => [
576
+ `--${boundary}`,
577
+ "Content-Type: application/json; charset=UTF-8",
578
+ "",
579
+ JSON.stringify(metadata),
580
+ `--${boundary}`,
581
+ `Content-Type: ${mimeType}`,
582
+ "",
583
+ content,
584
+ `--${boundary}--`,
585
+ ""
586
+ ].join("\r\n");
587
+ /** A boundary that appears nowhere in the parts it delimits. */
588
+ var pickBoundary = (parts, generate = newMultipartBoundary) => {
589
+ const collides = (candidate) => parts.some((part) => part.includes(candidate));
590
+ let boundary = generate();
591
+ while (collides(boundary)) boundary = generate();
592
+ return boundary;
593
+ };
594
+ async function createDriveFile(accessToken, input) {
595
+ const mimeType = assertSafeMimeType(input.mimeType ?? DEFAULT_MIME_TYPE);
596
+ const metadata = {
597
+ name: input.name,
598
+ mimeType
599
+ };
600
+ const boundary = pickBoundary([input.content, JSON.stringify(metadata)]);
601
+ return toDriveFileSummary(await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_UPLOAD_URL}?${new URLSearchParams({
602
+ uploadType: "multipart",
603
+ fields: FILE_FIELDS
604
+ }).toString()}`, {
605
+ method: "POST",
606
+ contentType: `multipart/related; boundary=${boundary}`,
607
+ body: buildMultipartBody(metadata, input.content, mimeType, boundary)
608
+ }));
609
+ }
610
+ async function readDriveFile(accessToken, input) {
611
+ const fileId = encodeURIComponent(input.fileId);
612
+ const file = toDriveFileSummary(await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?fields=${FILE_FIELDS}`));
613
+ if (!isTextMimeType(file.mimeType)) throw new Error(`Google Drive API: '${file.name}' is ${file.mimeType || "a binary file"} — only text files can be read as content`);
614
+ const raw = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?alt=media`, { expectText: true });
615
+ return {
616
+ file,
617
+ content: typeof raw === "string" ? raw.slice(0, MAX_READ_CHARS) : ""
618
+ };
619
+ }
620
+ async function deleteDriveFile(accessToken, input) {
621
+ await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: "DELETE" });
457
622
  }
458
623
  //#endregion
459
- export { DEFAULT_LIST_MAX_RESULTS, GOOGLE_CALENDAR_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, MAX_LIST_RESULTS, authorizeGoogle, calendarApiError, clientSecretPresence, configureGoogleHost, createCalendarEvent, createGoogleAuthFlow, deleteGoogleTokens, findClientSecretPath, getGoogleAccessToken, googleAuthFlow, googleConfigDir, googleSecretsDir, googleTokenPath, isIsoDateTimeWithOffset, legacyGoogleTokenPath, listCalendarEvents, loadClientSecret, loadGoogleTokens, mergeGoogleTokens, saveGoogleTokens, toEventSummary, unlinkGoogle, waitForAuthCode };
624
+ export { DEFAULT_LIST_MAX_RESULTS, GOOGLE_CALENDAR_SCOPE, GOOGLE_DRIVE_FILE_SCOPE, GOOGLE_SCOPES, GOOGLE_TASKS_SCOPE, MAX_LIST_RESULTS, assertSafeMimeType, authorizeGoogle, buildMultipartBody, calendarApiError, clientSecretPresence, completeTask, configureGoogleHost, createCalendarEvent, createDriveFile, createGoogleAuthFlow, createTask, deleteDriveFile, deleteGoogleTokens, deleteTask, findClientSecretPath, getGoogleAccessToken, googleApiError, googleAuthFlow, googleConfigDir, googleSecretsDir, googleTokenPath, isIsoDateTimeWithOffset, isTextMimeType, legacyGoogleTokenPath, listCalendarEvents, listDriveFiles, listTaskLists, listTasks, loadClientSecret, loadGoogleTokens, mergeGoogleTokens, pickBoundary, readDriveFile, saveGoogleTokens, toDriveFileSummary, toEventSummary, toTaskListSummary, toTaskSummary, unlinkGoogle, waitForAuthCode };
460
625
 
461
626
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/google/host.ts","../../src/google/datetime.ts","../../src/google/paths.ts","../../src/google/util.ts","../../src/google/clientSecret.ts","../../src/google/fsJson.ts","../../src/google/tokenStore.ts","../../src/google/fetch.ts","../../src/google/auth.ts","../../src/google/authFlow.ts","../../src/google/calendar.ts"],"sourcesContent":["// Host binding for the Google engine. The engine logs through the host's\n// logger, but a package-level import of the host logger would be an uphill\n// dependency — so the host injects it once at startup (same pattern as\n// `collection/server/host.ts`). The default is silent so the engine works\n// unconfigured in unit tests.\n\nexport interface GoogleLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nconst silentLogger: GoogleLogger = {\n error: () => undefined,\n warn: () => undefined,\n info: () => undefined,\n debug: () => undefined,\n};\n\nlet hostLog: GoogleLogger = silentLogger;\n\nexport function configureGoogleHost(binding: { log: GoogleLogger }): void {\n hostLog = binding.log;\n}\n\nexport const log: GoogleLogger = {\n error: (prefix, message, data) => hostLog.error(prefix, message, data),\n warn: (prefix, message, data) => hostLog.warn(prefix, message, data),\n info: (prefix, message, data) => hostLog.info(prefix, message, data),\n debug: (prefix, message, data) => hostLog.debug(prefix, message, data),\n};\n","// Strict RFC3339 date-time validation shared by every surface that feeds\n// Calendar `dateTime`/`timeMin` values (agent tool args, remote-host command\n// params). Calendar rejects date-only / offset-less values with an opaque\n// 400, so callers validate here and return an actionable message instead.\n\n// Fractional seconds are normalized away first — an optional `(\\.\\d+)?`\n// group inside the main pattern trips security/detect-unsafe-regex.\nconst ISO_DATE_TIME_WITH_OFFSET_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:Z|[+-](\\d{2}):(\\d{2}))$/;\nconst FRACTIONAL_SECONDS_RE = /\\.\\d+(?=Z|[+-])/;\n\nconst MAX_HOUR = 23;\nconst MAX_MINUTE = 59;\nconst MAX_SECOND = 59;\n\n// JS Date normalizes overflowed components (2026-02-31 parses as\n// 2026-03-03), so `new Date(value)` alone cannot reject impossible dates —\n// a UTC round-trip of the raw components can.\nconst isRealCalendarDate = (year: number, month: number, day: number): boolean => {\n const roundTrip = new Date(Date.UTC(year, month - 1, day));\n return roundTrip.getUTCFullYear() === year && roundTrip.getUTCMonth() === month - 1 && roundTrip.getUTCDate() === day;\n};\n\nexport const isIsoDateTimeWithOffset = (value: string): boolean => {\n const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE, \"\"));\n if (!match) return false;\n const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = match.slice(1, 7).map(Number);\n const timeInRange = hour <= MAX_HOUR && minute <= MAX_MINUTE && second <= MAX_SECOND;\n // RFC3339 bounds the offset to 00-23:59; groups 7/8 are undefined for \"Z\".\n const offsetInRange = match[7] === undefined || (Number(match[7]) <= MAX_HOUR && Number(match[8]) <= MAX_MINUTE);\n return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;\n};\n","// Google OAuth material lives OUTSIDE the workspace: the client secret is\n// machine-only (never synced) and the refresh token must survive workspace\n// resets — same reasoning as the gcloud / gh CLI model. The dir is the\n// host-NEUTRAL `~/.config/mulmo` because this engine is shared by both\n// MulmoClaude and MulmoTerminal, which deliberately share one grant per\n// machine. The `home` parameter exists so tests can thread a fake home.\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function googleConfigDir(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmo\");\n}\n\n/** Pre-0.20.1 token dir (mulmoclaude-branded); reads migrate away from it. */\nexport function legacyGoogleTokenPath(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmoclaude\", \"google-token.json\");\n}\n\nexport function googleTokenPath(home?: string): string {\n return join(googleConfigDir(home), \"google-token.json\");\n}\n\nexport function googleSecretsDir(home?: string): string {\n return join(home ?? homedir(), \".secrets\");\n}\n","// Small helpers ported from the host (`server/utils/{errors,text,time,types}.ts`)\n// so the Google engine carries no dependency on host utils — same convention\n// as `collection/registry/server/fetch.ts`.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport function errorMessage(err: unknown, fallback = \"unknown error\"): string {\n if (err instanceof Error) return err.message || fallback;\n const text = String(err);\n return text === \"\" || text === \"[object Object]\" ? fallback : text;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Clip a string to at most `max` chars; the ellipsis is included in the\n * budget so output never exceeds `max`. */\nexport function truncate(text: string, max: number, ellipsis = \"…\"): string {\n if (max <= 0) return \"\";\n if (text.length <= max) return text;\n return `${text.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}`;\n}\n","// Loads the Google OAuth desktop-app client credentials the user downloaded\n// from the Cloud Console into `~/.secrets/client_secret_*.json`. The file is\n// discovered by prefix so the user doesn't have to rename Google's long\n// default filename.\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { isRecord } from \"./util.js\";\nimport { googleSecretsDir } from \"./paths.js\";\n\nexport interface InstalledClientSecret {\n client_id: string;\n client_secret: string;\n}\n\nconst isInstalledClientSecret = (value: unknown): value is { installed: InstalledClientSecret } => {\n if (!isRecord(value) || !isRecord(value.installed)) return false;\n return typeof value.installed.client_id === \"string\" && typeof value.installed.client_secret === \"string\";\n};\n\nconst isClientSecretFileName = (name: string): boolean => name.startsWith(\"client_secret_\") && name.endsWith(\".json\");\n\nasync function listClientSecretFiles(home?: string): Promise<string[]> {\n const entries = await readdir(googleSecretsDir(home)).catch((): string[] => []);\n return entries.filter(isClientSecretFileName).sort();\n}\n\n/** \"ambiguous\" (2+ files) is distinct from \"missing\" — the fixes differ\n * (remove duplicates vs download credentials), so the UI must not conflate\n * them. */\nexport type ClientSecretPresence = \"found\" | \"missing\" | \"ambiguous\";\n\nexport async function clientSecretPresence(home?: string): Promise<ClientSecretPresence> {\n const matches = await listClientSecretFiles(home);\n if (matches.length === 0) return \"missing\";\n return matches.length === 1 ? \"found\" : \"ambiguous\";\n}\n\nexport async function findClientSecretPath(home?: string): Promise<string> {\n const dir = googleSecretsDir(home);\n const matches = await listClientSecretFiles(home);\n const [first, ...rest] = matches;\n if (!first) {\n throw new Error(\n `no client_secret_*.json found in ${dir} — download the OAuth desktop-app credentials JSON from the Google Cloud Console and place it there (mode 600)`,\n );\n }\n if (rest.length > 0) {\n // The stored refresh token is bound to one client_id; silently picking one\n // of several files could pair the token with the wrong client\n // (invalid_grant), so ambiguity is an error the user must resolve.\n throw new Error(`multiple client_secret_*.json files found in ${dir} (${matches.join(\", \")}) — keep exactly one`);\n }\n return join(dir, first);\n}\n\nexport async function loadClientSecret(home?: string): Promise<InstalledClientSecret> {\n const filePath = await findClientSecretPath(home);\n const raw = await readFile(filePath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (!isInstalledClientSecret(parsed)) {\n throw new Error(`${filePath} is not a desktop-app OAuth client secret (missing \"installed\" with client_id / client_secret)`);\n }\n return { client_id: parsed.installed.client_id, client_secret: parsed.installed.client_secret };\n}\n","// Minimal JSON file I/O for the token store. Unlike the host's\n// `writeJsonAtomic` (and core's collection `atomic.ts`), this one supports a\n// file mode — the token file must be 0600.\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\n\nexport async function readJsonOrNull<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsp.readFile(filePath, \"utf-8\");\n const parsed: T = JSON.parse(raw);\n return parsed;\n } catch {\n return null;\n }\n}\n\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300];\n\nconst hasErrnoCode = (err: unknown): err is { code: string } =>\n typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename\n// trips EPERM/EBUSY/EACCES. The retry is gated to Windows because POSIX EPERM\n// means a real permission problem — retrying would just delay the throw.\n// Ported from the host's server/utils/files/atomic.ts.\nconst isTransientRenameError = (err: unknown): boolean =>\n IS_WINDOWS && hasErrnoCode(err) && (err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\");\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsp.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n await fsp.rename(fromPath, toPath);\n}\n\n/** tmp-write + rename so readers never see a half-written file; `mode`\n * applies to the tmp file and survives the rename. */\nexport async function writeJsonAtomicWithMode(filePath: string, data: unknown, mode: number): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: \"utf-8\", mode });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","// Persistence for the Google OAuth tokens (refresh + access) at\n// `~/.config/mulmo/google-token.json`, mode 600. Google omits\n// `refresh_token` from refresh responses, so merges must preserve the one we\n// already hold — losing it forces the user through the browser consent again.\nimport { constants as fsConstants, copyFile, mkdir, rm, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Credentials } from \"google-auth-library\";\nimport { readJsonOrNull, writeJsonAtomicWithMode } from \"./fsJson.js\";\nimport { googleTokenPath, legacyGoogleTokenPath } from \"./paths.js\";\n\nconst TOKEN_FILE_MODE = 0o600;\n\nexport function mergeGoogleTokens(existing: Credentials | null, incoming: Credentials): Credentials {\n const merged = { ...existing, ...incoming };\n if (!incoming.refresh_token && existing?.refresh_token) merged.refresh_token = existing.refresh_token;\n return merged;\n}\n\nconst fileExists = async (filePath: string): Promise<boolean> =>\n await stat(filePath).then(\n () => true,\n () => false,\n );\n\n// Tokens written before 0.20.1 live under the mulmoclaude-branded dir; move\n// them once. COPYFILE_EXCL makes the create atomic-and-non-clobbering — an\n// exists+rename sequence could overwrite a token a concurrent process wrote\n// to the new path in between (TOCTOU). The legacy file is deleted only after\n// a successful copy; on EEXIST (new path won a race, or both files already\n// exist) it is left for any older install still reading it. copyFile\n// preserves the 600 mode.\nasync function migrateLegacyTokenFile(home?: string): Promise<void> {\n const current = googleTokenPath(home);\n const legacy = legacyGoogleTokenPath(home);\n if (!(await fileExists(legacy))) return;\n await mkdir(path.dirname(current), { recursive: true });\n try {\n await copyFile(legacy, current, fsConstants.COPYFILE_EXCL);\n } catch {\n return;\n }\n await rm(legacy, { force: true });\n}\n\nexport async function loadGoogleTokens(home?: string): Promise<Credentials | null> {\n await migrateLegacyTokenFile(home).catch(() => undefined);\n const current = await readJsonOrNull<Credentials>(googleTokenPath(home));\n if (current) return current;\n // Migration is best-effort — a valid legacy token must still count as\n // linked even when the move failed (permissions, read-only fs, …).\n return await readJsonOrNull<Credentials>(legacyGoogleTokenPath(home));\n}\n\nexport async function saveGoogleTokens(incoming: Credentials, home?: string): Promise<Credentials> {\n const merged = mergeGoogleTokens(await loadGoogleTokens(home), incoming);\n await writeJsonAtomicWithMode(googleTokenPath(home), merged, TOKEN_FILE_MODE);\n return merged;\n}\n\nexport async function deleteGoogleTokens(home?: string): Promise<void> {\n await rm(googleTokenPath(home), { force: true });\n}\n","// `fetch` with a finite timeout, ported from the host (`server/utils/fetch.ts`)\n// so the Google engine carries no dependency on host utils (same convention as\n// `collection/registry/server/fetch.ts`).\n\nimport { ONE_SECOND_MS } from \"./util.js\";\n\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/** `fetch` with a finite timeout. Rejects with a `TimeoutError` once\n * `timeoutMs` elapses. Composes with a caller-supplied `signal` so external\n * cancellation still works. */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n","// Google OAuth for the host machine, independent of Firebase Auth (which\n// discards refresh tokens). Entry points:\n// - authorizeGoogle(): one-shot loopback + PKCE browser consent flow\n// (desktop-app clients may redirect to any 127.0.0.1 port), storing the\n// refresh token locally via tokenStore.\n// - getGoogleAccessToken(): mints a fresh access token from the stored\n// refresh token; the OAuth2Client \"tokens\" event persists rotations.\n// - unlinkGoogle(): best-effort revoke at Google + local token delete.\nimport { randomBytes } from \"node:crypto\";\nimport http from \"node:http\";\nimport { CodeChallengeMethod, OAuth2Client, type Credentials } from \"google-auth-library\";\nimport { log } from \"./host.js\";\nimport { errorMessage, ONE_MINUTE_MS, ONE_SECOND_MS } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { loadClientSecret, type InstalledClientSecret } from \"./clientSecret.js\";\nimport { deleteGoogleTokens, loadGoogleTokens, saveGoogleTokens } from \"./tokenStore.js\";\n\nexport const GOOGLE_CALENDAR_SCOPE = \"https://www.googleapis.com/auth/calendar.events\";\nexport const GOOGLE_TASKS_SCOPE = \"https://www.googleapis.com/auth/tasks\";\nexport const GOOGLE_DRIVE_FILE_SCOPE = \"https://www.googleapis.com/auth/drive.file\";\n/** Requested at consent as one set — matches the scopes registered on the\n * OAuth consent screen, so a single re-link covers every supported API\n * (Calendar now; Tasks / Drive tools ride the same grant later). */\nexport const GOOGLE_SCOPES = [GOOGLE_CALENDAR_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE];\nconst CALLBACK_PATH = \"/oauth2callback\";\nconst AUTH_TIMEOUT_MS = 5 * ONE_MINUTE_MS;\nconst STATE_BYTES = 16;\n\nexport interface AuthorizeGoogleOptions {\n home?: string;\n /** Called with the consent URL; open it in a browser (and/or print it). */\n onAuthUrl?: (url: string) => void;\n timeoutMs?: number;\n}\n\nconst createClient = (secret: InstalledClientSecret, redirectUri?: string): OAuth2Client =>\n new OAuth2Client({ clientId: secret.client_id, clientSecret: secret.client_secret, redirectUri });\n\nconst persistRotatedTokens = (client: OAuth2Client, home?: string): void => {\n client.on(\"tokens\", (tokens) => {\n saveGoogleTokens(tokens, home).catch((err: unknown) => {\n log.error(\"google\", \"failed to persist rotated tokens\", { error: String(err) });\n });\n });\n};\n\nexport async function getGoogleAccessToken(home?: string): Promise<string> {\n const saved = await loadGoogleTokens(home);\n if (!saved?.refresh_token) {\n // Host-neutral wording — this engine ships to multiple hosts whose link\n // flows differ (#2128); each host's own help carries the specific steps.\n throw new Error(\"Google account not linked on this host — ask the user to link their Google account in this app's settings, then retry\");\n }\n const client = createClient(await loadClientSecret(home));\n client.setCredentials(saved);\n persistRotatedTokens(client, home);\n const { token } = await client.getAccessToken();\n if (!token) {\n throw new Error(\"could not obtain a Google access token — the grant may have been revoked; re-link the account\");\n }\n return token;\n}\n\nconst REVOKE_URL = \"https://oauth2.googleapis.com/revoke\";\n\n/** The revoke POST, injectable for tests. */\nexport type RevokeFetch = typeof fetchWithTimeout;\n\n/** Revoke the grant at Google (best-effort) and delete the local token file.\n * Revoke failures are logged but never block the local delete — Google may\n * already consider the token invalid, and keeping the file would leave the\n * user unable to unlink. */\nexport async function unlinkGoogle(home?: string, revokeFetch: RevokeFetch = fetchWithTimeout): Promise<void> {\n const saved = await loadGoogleTokens(home);\n const token = saved?.refresh_token ?? saved?.access_token;\n if (token) {\n try {\n const response = await revokeFetch(REVOKE_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({ token }).toString(),\n });\n if (!response.ok) log.warn(\"google\", \"token revoke returned non-ok\", { status: response.status });\n } catch (err) {\n log.warn(\"google\", \"token revoke failed, deleting local tokens anyway\", { error: errorMessage(err) });\n }\n }\n await deleteGoogleTokens(home);\n}\n\nconst startLoopbackServer = (): Promise<{ server: http.Server; port: number }> =>\n new Promise((resolve, reject) => {\n const server = http.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n reject(new Error(\"loopback server has no port\"));\n return;\n }\n resolve({ server, port: address.port });\n });\n });\n\n// State is validated before error/code — a callback that can't prove it\n// belongs to this flow must not influence it (its `error` text would\n// otherwise reach the terminal attacker-controlled).\nconst authCodeFromCallback = (url: URL, expectedState: string): string => {\n if (url.searchParams.get(\"state\") !== expectedState) throw new Error(\"OAuth state mismatch — possible CSRF, aborting\");\n const error = url.searchParams.get(\"error\");\n if (error) throw new Error(`Google authorization failed: ${error}`);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"authorization callback carried no code\");\n return code;\n};\n\nconst respondHtml = (res: http.ServerResponse, status: number, message: string): void => {\n res.writeHead(status, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(`<html><body><h3>${message}</h3></body></html>`);\n};\n\nexport const waitForAuthCode = (server: http.Server, expectedState: string, timeoutMs: number): Promise<string> =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`authorization timed out after ${timeoutMs / ONE_SECOND_MS}s`)), timeoutMs);\n server.on(\"request\", (req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n if (url.pathname !== CALLBACK_PATH) {\n res.writeHead(404);\n res.end();\n return;\n }\n // A wrong-state request is not our callback (drive-by localhost probe\n // or stale tab) — answer it but keep waiting for the real redirect, so\n // an unauthenticated request can't abort the pending flow.\n if (url.searchParams.get(\"state\") !== expectedState) {\n respondHtml(res, 400, \"Invalid authorization callback. You can close this tab.\");\n return;\n }\n clearTimeout(timer);\n try {\n const code = authCodeFromCallback(url, expectedState);\n respondHtml(res, 200, \"Authorization complete — you can close this tab.\");\n resolve(code);\n } catch (err) {\n // Static text only — the failure detail echoes query-string content,\n // which must not be reflected into HTML. The CLI prints the detail.\n respondHtml(res, 400, \"Authorization failed — see the terminal for details. You can close this tab.\");\n reject(err);\n }\n });\n });\n\n// `access_type: offline` + `prompt: consent` force Google to return a refresh\n// token on every run (repeat consents otherwise omit it).\nconst buildConsentUrl = (client: OAuth2Client, codeChallenge: string, state: string): string =>\n client.generateAuthUrl({\n access_type: \"offline\",\n prompt: \"consent\",\n scope: GOOGLE_SCOPES,\n code_challenge_method: CodeChallengeMethod.S256,\n code_challenge: codeChallenge,\n state,\n });\n\nexport async function authorizeGoogle(opts: AuthorizeGoogleOptions = {}): Promise<Credentials> {\n const secret = await loadClientSecret(opts.home);\n const { server, port } = await startLoopbackServer();\n try {\n const client = createClient(secret, `http://127.0.0.1:${port}${CALLBACK_PATH}`);\n const { codeVerifier, codeChallenge } = await client.generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n const state = randomBytes(STATE_BYTES).toString(\"hex\");\n opts.onAuthUrl?.(buildConsentUrl(client, codeChallenge, state));\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n const { tokens } = await client.getToken({ code, codeVerifier });\n if (!tokens.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return await saveGoogleTokens(tokens, opts.home);\n } finally {\n server.close();\n }\n}\n","// In-flight manager for the settings-UI OAuth flow. authorizeGoogle()\n// resolves only after the user finishes the browser consent, so the HTTP\n// layer starts it in the background, returns the consent URL immediately,\n// and reports progress via status polling. One flow at a time — the guard\n// is the in-flight start promise itself (set synchronously before any\n// await), so concurrent authorize requests share one flow instead of\n// spawning parallel loopback listeners.\nimport { log } from \"./host.js\";\nimport { errorMessage } from \"./util.js\";\nimport { authorizeGoogle } from \"./auth.js\";\n\nexport interface GoogleAuthFlowStatus {\n pending: boolean;\n lastError: string | null;\n}\n\nexport interface GoogleAuthFlow {\n start: () => Promise<{ authUrl: string }>;\n status: () => GoogleAuthFlowStatus;\n}\n\nexport const createGoogleAuthFlow = (authorize: typeof authorizeGoogle): GoogleAuthFlow => {\n let inFlightStart: Promise<{ authUrl: string }> | null = null;\n let flowRunning = false;\n let lastError: string | null = null;\n\n const launchFlow = (): Promise<{ authUrl: string }> =>\n new Promise((resolve, reject) => {\n flowRunning = true;\n authorize({\n onAuthUrl: (url) => resolve({ authUrl: url }),\n })\n .then(() => log.info(\"google\", \"authorize flow completed\"))\n .catch((err: unknown) => {\n lastError = errorMessage(err);\n log.warn(\"google\", \"authorize flow failed\", { error: lastError });\n // No-op when the URL already resolved; covers pre-URL failures\n // (missing client secret, port bind error).\n reject(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n flowRunning = false;\n inFlightStart = null;\n });\n });\n\n const start = (): Promise<{ authUrl: string }> => {\n if (inFlightStart) return inFlightStart;\n lastError = null;\n inFlightStart = launchFlow();\n return inFlightStart;\n };\n\n const status = (): GoogleAuthFlowStatus => ({ pending: flowRunning, lastError });\n\n return { start, status };\n};\n\nexport const googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);\n","// Google Calendar v3 REST calls against the user's primary calendar. Plain\n// fetch instead of the `googleapis` SDK — two endpoints don't justify the\n// dependency (see plans/done/feat-google-oauth-calendar.md).\nimport { errorMessage, isRecord, ONE_SECOND_MS, truncate } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\n\nconst CALENDAR_EVENTS_URL = \"https://www.googleapis.com/calendar/v3/calendars/primary/events\";\nconst CALENDAR_TIMEOUT_MS = 30 * ONE_SECOND_MS;\nconst ERROR_BODY_MAX_CHARS = 300;\nconst HTTP_FORBIDDEN = 403;\nexport const DEFAULT_LIST_MAX_RESULTS = 10;\nexport const MAX_LIST_RESULTS = 50;\n\nexport interface CalendarEventInput {\n summary: string;\n startDateTime: string;\n endDateTime: string;\n description?: string;\n}\n\nexport interface ListEventsInput {\n timeMin?: string;\n maxResults?: number;\n}\n\nexport interface CalendarEventSummary {\n id: string;\n summary: string;\n start: string;\n end: string;\n htmlLink: string;\n status: string;\n}\n\n// All-day events carry `date`, timed events carry `dateTime`.\nconst eventTime = (value: unknown): string => {\n if (!isRecord(value)) return \"\";\n if (typeof value.dateTime === \"string\") return value.dateTime;\n if (typeof value.date === \"string\") return value.date;\n return \"\";\n};\n\nexport const toEventSummary = (value: unknown): CalendarEventSummary => {\n const record: Record<string, unknown> = isRecord(value) ? value : {};\n return {\n id: typeof record.id === \"string\" ? record.id : \"\",\n summary: typeof record.summary === \"string\" ? record.summary : \"\",\n start: eventTime(record.start),\n end: eventTime(record.end),\n htmlLink: typeof record.htmlLink === \"string\" ? record.htmlLink : \"\",\n status: typeof record.status === \"string\" ? record.status : \"\",\n };\n};\n\nexport const calendarApiError = (status: number, body: string): Error => {\n const hint = status === HTTP_FORBIDDEN ? \" (is the Google Calendar API enabled for the Cloud project?)\" : \"\";\n const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : \"\";\n return new Error(`Google Calendar API: HTTP ${status}${hint}${detail}`);\n};\n\nconst calendarRequest = async (accessToken: string, url: string, init: { method?: string; body?: string } = {}): Promise<unknown> => {\n const response = await fetchWithTimeout(url, {\n ...init,\n timeoutMs: CALENDAR_TIMEOUT_MS,\n headers: { Authorization: `Bearer ${accessToken}`, \"Content-Type\": \"application/json\" },\n });\n if (!response.ok) {\n const body = await response.text().catch((err: unknown) => errorMessage(err));\n throw calendarApiError(response.status, body);\n }\n return await response.json();\n};\n\nexport async function createCalendarEvent(accessToken: string, input: CalendarEventInput): Promise<CalendarEventSummary> {\n const body = {\n summary: input.summary,\n description: input.description,\n start: { dateTime: input.startDateTime },\n end: { dateTime: input.endDateTime },\n };\n const created = await calendarRequest(accessToken, CALENDAR_EVENTS_URL, { method: \"POST\", body: JSON.stringify(body) });\n return toEventSummary(created);\n}\n\nexport async function listCalendarEvents(accessToken: string, input: ListEventsInput = {}): Promise<CalendarEventSummary[]> {\n const params = new URLSearchParams({\n timeMin: input.timeMin ?? new Date().toISOString(),\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n singleEvents: \"true\",\n orderBy: \"startTime\",\n });\n const listed = await calendarRequest(accessToken, `${CALENDAR_EVENTS_URL}?${params.toString()}`);\n const items = isRecord(listed) && Array.isArray(listed.items) ? listed.items : [];\n return items.map(toEventSummary);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/google/host.ts","../../src/google/datetime.ts","../../src/google/paths.ts","../../src/google/util.ts","../../src/google/clientSecret.ts","../../src/google/fsJson.ts","../../src/google/tokenStore.ts","../../src/google/fetch.ts","../../src/google/auth.ts","../../src/google/authFlow.ts","../../src/google/apiClient.ts","../../src/google/calendar.ts","../../src/google/tasks.ts","../../src/google/driveFile.ts"],"sourcesContent":["// Host binding for the Google engine. The engine logs through the host's\n// logger, but a package-level import of the host logger would be an uphill\n// dependency — so the host injects it once at startup (same pattern as\n// `collection/server/host.ts`). The default is silent so the engine works\n// unconfigured in unit tests.\n\nexport interface GoogleLogger {\n error: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n warn: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n info: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n debug: (prefix: string, message: string, data?: Record<string, unknown>) => void;\n}\n\nconst silentLogger: GoogleLogger = {\n error: () => undefined,\n warn: () => undefined,\n info: () => undefined,\n debug: () => undefined,\n};\n\nlet hostLog: GoogleLogger = silentLogger;\n\nexport function configureGoogleHost(binding: { log: GoogleLogger }): void {\n hostLog = binding.log;\n}\n\nexport const log: GoogleLogger = {\n error: (prefix, message, data) => hostLog.error(prefix, message, data),\n warn: (prefix, message, data) => hostLog.warn(prefix, message, data),\n info: (prefix, message, data) => hostLog.info(prefix, message, data),\n debug: (prefix, message, data) => hostLog.debug(prefix, message, data),\n};\n","// Strict RFC3339 date-time validation shared by every surface that feeds\n// Calendar `dateTime`/`timeMin` values (agent tool args, remote-host command\n// params). Calendar rejects date-only / offset-less values with an opaque\n// 400, so callers validate here and return an actionable message instead.\n\n// Fractional seconds are normalized away first — an optional `(\\.\\d+)?`\n// group inside the main pattern trips security/detect-unsafe-regex.\nconst ISO_DATE_TIME_WITH_OFFSET_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(?:Z|[+-](\\d{2}):(\\d{2}))$/;\nconst FRACTIONAL_SECONDS_RE = /\\.\\d+(?=Z|[+-])/;\n\nconst MAX_HOUR = 23;\nconst MAX_MINUTE = 59;\nconst MAX_SECOND = 59;\n\n// JS Date normalizes overflowed components (2026-02-31 parses as\n// 2026-03-03), so `new Date(value)` alone cannot reject impossible dates —\n// a UTC round-trip of the raw components can.\nconst isRealCalendarDate = (year: number, month: number, day: number): boolean => {\n const roundTrip = new Date(Date.UTC(year, month - 1, day));\n return roundTrip.getUTCFullYear() === year && roundTrip.getUTCMonth() === month - 1 && roundTrip.getUTCDate() === day;\n};\n\nexport const isIsoDateTimeWithOffset = (value: string): boolean => {\n const match = ISO_DATE_TIME_WITH_OFFSET_RE.exec(value.replace(FRACTIONAL_SECONDS_RE, \"\"));\n if (!match) return false;\n const [year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0] = match.slice(1, 7).map(Number);\n const timeInRange = hour <= MAX_HOUR && minute <= MAX_MINUTE && second <= MAX_SECOND;\n // RFC3339 bounds the offset to 00-23:59; groups 7/8 are undefined for \"Z\".\n const offsetInRange = match[7] === undefined || (Number(match[7]) <= MAX_HOUR && Number(match[8]) <= MAX_MINUTE);\n return isRealCalendarDate(year, month, day) && timeInRange && offsetInRange;\n};\n","// Google OAuth material lives OUTSIDE the workspace: the client secret is\n// machine-only (never synced) and the refresh token must survive workspace\n// resets — same reasoning as the gcloud / gh CLI model. The dir is the\n// host-NEUTRAL `~/.config/mulmo` because this engine is shared by both\n// MulmoClaude and MulmoTerminal, which deliberately share one grant per\n// machine. The `home` parameter exists so tests can thread a fake home.\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\n\nexport function googleConfigDir(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmo\");\n}\n\n/** Pre-0.20.1 token dir (mulmoclaude-branded); reads migrate away from it. */\nexport function legacyGoogleTokenPath(home?: string): string {\n return join(home ?? homedir(), \".config\", \"mulmoclaude\", \"google-token.json\");\n}\n\nexport function googleTokenPath(home?: string): string {\n return join(googleConfigDir(home), \"google-token.json\");\n}\n\nexport function googleSecretsDir(home?: string): string {\n return join(home ?? homedir(), \".secrets\");\n}\n","// Small helpers ported from the host (`server/utils/{errors,text,time,types}.ts`)\n// so the Google engine carries no dependency on host utils — same convention\n// as `collection/registry/server/fetch.ts`.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport function errorMessage(err: unknown, fallback = \"unknown error\"): string {\n if (err instanceof Error) return err.message || fallback;\n const text = String(err);\n return text === \"\" || text === \"[object Object]\" ? fallback : text;\n}\n\nexport function isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\n/** Clip a string to at most `max` chars; the ellipsis is included in the\n * budget so output never exceeds `max`. */\nexport function truncate(text: string, max: number, ellipsis = \"…\"): string {\n if (max <= 0) return \"\";\n if (text.length <= max) return text;\n return `${text.slice(0, Math.max(0, max - ellipsis.length))}${ellipsis}`;\n}\n","// Loads the Google OAuth desktop-app client credentials the user downloaded\n// from the Cloud Console into `~/.secrets/client_secret_*.json`. The file is\n// discovered by prefix so the user doesn't have to rename Google's long\n// default filename.\nimport { readdir, readFile } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { isRecord } from \"./util.js\";\nimport { googleSecretsDir } from \"./paths.js\";\n\nexport interface InstalledClientSecret {\n client_id: string;\n client_secret: string;\n}\n\nconst isInstalledClientSecret = (value: unknown): value is { installed: InstalledClientSecret } => {\n if (!isRecord(value) || !isRecord(value.installed)) return false;\n return typeof value.installed.client_id === \"string\" && typeof value.installed.client_secret === \"string\";\n};\n\nconst isClientSecretFileName = (name: string): boolean => name.startsWith(\"client_secret_\") && name.endsWith(\".json\");\n\nasync function listClientSecretFiles(home?: string): Promise<string[]> {\n const entries = await readdir(googleSecretsDir(home)).catch((): string[] => []);\n return entries.filter(isClientSecretFileName).sort();\n}\n\n/** \"ambiguous\" (2+ files) is distinct from \"missing\" — the fixes differ\n * (remove duplicates vs download credentials), so the UI must not conflate\n * them. */\nexport type ClientSecretPresence = \"found\" | \"missing\" | \"ambiguous\";\n\nexport async function clientSecretPresence(home?: string): Promise<ClientSecretPresence> {\n const matches = await listClientSecretFiles(home);\n if (matches.length === 0) return \"missing\";\n return matches.length === 1 ? \"found\" : \"ambiguous\";\n}\n\nexport async function findClientSecretPath(home?: string): Promise<string> {\n const dir = googleSecretsDir(home);\n const matches = await listClientSecretFiles(home);\n const [first, ...rest] = matches;\n if (!first) {\n throw new Error(\n `no client_secret_*.json found in ${dir} — download the OAuth desktop-app credentials JSON from the Google Cloud Console and place it there (mode 600)`,\n );\n }\n if (rest.length > 0) {\n // The stored refresh token is bound to one client_id; silently picking one\n // of several files could pair the token with the wrong client\n // (invalid_grant), so ambiguity is an error the user must resolve.\n throw new Error(`multiple client_secret_*.json files found in ${dir} (${matches.join(\", \")}) — keep exactly one`);\n }\n return join(dir, first);\n}\n\nexport async function loadClientSecret(home?: string): Promise<InstalledClientSecret> {\n const filePath = await findClientSecretPath(home);\n const raw = await readFile(filePath, \"utf-8\");\n const parsed: unknown = JSON.parse(raw);\n if (!isInstalledClientSecret(parsed)) {\n throw new Error(`${filePath} is not a desktop-app OAuth client secret (missing \"installed\" with client_id / client_secret)`);\n }\n return { client_id: parsed.installed.client_id, client_secret: parsed.installed.client_secret };\n}\n","// Minimal JSON file I/O for the token store. Unlike the host's\n// `writeJsonAtomic` (and core's collection `atomic.ts`), this one supports a\n// file mode — the token file must be 0600.\nimport { promises as fsp } from \"node:fs\";\nimport path from \"node:path\";\n\nexport async function readJsonOrNull<T>(filePath: string): Promise<T | null> {\n try {\n const raw = await fsp.readFile(filePath, \"utf-8\");\n const parsed: T = JSON.parse(raw);\n return parsed;\n } catch {\n return null;\n }\n}\n\nconst IS_WINDOWS = process.platform === \"win32\";\nconst RENAME_RETRY_DELAYS_MS = [30, 100, 300];\n\nconst hasErrnoCode = (err: unknown): err is { code: string } =>\n typeof err === \"object\" && err !== null && \"code\" in err && typeof (err as { code: unknown }).code === \"string\";\n\n// On Windows, AV / Search Indexer / Defender briefly hold handles and rename\n// trips EPERM/EBUSY/EACCES. The retry is gated to Windows because POSIX EPERM\n// means a real permission problem — retrying would just delay the throw.\n// Ported from the host's server/utils/files/atomic.ts.\nconst isTransientRenameError = (err: unknown): boolean =>\n IS_WINDOWS && hasErrnoCode(err) && (err.code === \"EPERM\" || err.code === \"EBUSY\" || err.code === \"EACCES\");\n\nasync function renameWithWindowsRetry(fromPath: string, toPath: string): Promise<void> {\n for (const delayMs of RENAME_RETRY_DELAYS_MS) {\n try {\n await fsp.rename(fromPath, toPath);\n return;\n } catch (err) {\n if (!isTransientRenameError(err)) throw err;\n await new Promise((resolve) => setTimeout(resolve, delayMs));\n }\n }\n await fsp.rename(fromPath, toPath);\n}\n\n/** tmp-write + rename so readers never see a half-written file; `mode`\n * applies to the tmp file and survives the rename. */\nexport async function writeJsonAtomicWithMode(filePath: string, data: unknown, mode: number): Promise<void> {\n const tmp = `${filePath}.tmp`;\n await fsp.mkdir(path.dirname(filePath), { recursive: true });\n try {\n await fsp.writeFile(tmp, JSON.stringify(data, null, 2), { encoding: \"utf-8\", mode });\n await renameWithWindowsRetry(tmp, filePath);\n } catch (err) {\n await fsp.unlink(tmp).catch(() => undefined);\n throw err;\n }\n}\n","// Persistence for the Google OAuth tokens (refresh + access) at\n// `~/.config/mulmo/google-token.json`, mode 600. Google omits\n// `refresh_token` from refresh responses, so merges must preserve the one we\n// already hold — losing it forces the user through the browser consent again.\nimport { constants as fsConstants, copyFile, mkdir, rm, stat } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Credentials } from \"google-auth-library\";\nimport { readJsonOrNull, writeJsonAtomicWithMode } from \"./fsJson.js\";\nimport { googleTokenPath, legacyGoogleTokenPath } from \"./paths.js\";\n\nconst TOKEN_FILE_MODE = 0o600;\n\nexport function mergeGoogleTokens(existing: Credentials | null, incoming: Credentials): Credentials {\n const merged = { ...existing, ...incoming };\n if (!incoming.refresh_token && existing?.refresh_token) merged.refresh_token = existing.refresh_token;\n return merged;\n}\n\nconst fileExists = async (filePath: string): Promise<boolean> =>\n await stat(filePath).then(\n () => true,\n () => false,\n );\n\n// Tokens written before 0.20.1 live under the mulmoclaude-branded dir; move\n// them once. COPYFILE_EXCL makes the create atomic-and-non-clobbering — an\n// exists+rename sequence could overwrite a token a concurrent process wrote\n// to the new path in between (TOCTOU). The legacy file is deleted only after\n// a successful copy; on EEXIST (new path won a race, or both files already\n// exist) it is left for any older install still reading it. copyFile\n// preserves the 600 mode.\nasync function migrateLegacyTokenFile(home?: string): Promise<void> {\n const current = googleTokenPath(home);\n const legacy = legacyGoogleTokenPath(home);\n if (!(await fileExists(legacy))) return;\n await mkdir(path.dirname(current), { recursive: true });\n try {\n await copyFile(legacy, current, fsConstants.COPYFILE_EXCL);\n } catch {\n return;\n }\n await rm(legacy, { force: true });\n}\n\nexport async function loadGoogleTokens(home?: string): Promise<Credentials | null> {\n await migrateLegacyTokenFile(home).catch(() => undefined);\n const current = await readJsonOrNull<Credentials>(googleTokenPath(home));\n if (current) return current;\n // Migration is best-effort — a valid legacy token must still count as\n // linked even when the move failed (permissions, read-only fs, …).\n return await readJsonOrNull<Credentials>(legacyGoogleTokenPath(home));\n}\n\nexport async function saveGoogleTokens(incoming: Credentials, home?: string): Promise<Credentials> {\n const merged = mergeGoogleTokens(await loadGoogleTokens(home), incoming);\n await writeJsonAtomicWithMode(googleTokenPath(home), merged, TOKEN_FILE_MODE);\n return merged;\n}\n\nexport async function deleteGoogleTokens(home?: string): Promise<void> {\n await rm(googleTokenPath(home), { force: true });\n}\n","// `fetch` with a finite timeout, ported from the host (`server/utils/fetch.ts`)\n// so the Google engine carries no dependency on host utils (same convention as\n// `collection/registry/server/fetch.ts`).\n\nimport { ONE_SECOND_MS } from \"./util.js\";\n\nexport const DEFAULT_FETCH_TIMEOUT_MS = 10 * ONE_SECOND_MS;\n\n// `Parameters<typeof fetch>[1]` avoids referencing the ambient `RequestInit`\n// type, which ESLint's `no-undef` rule trips over in the server config.\nexport type FetchWithTimeoutInit = Parameters<typeof fetch>[1] & { timeoutMs?: number };\n\n/** `fetch` with a finite timeout. Rejects with a `TimeoutError` once\n * `timeoutMs` elapses. Composes with a caller-supplied `signal` so external\n * cancellation still works. */\nexport async function fetchWithTimeout(url: string | URL, init: FetchWithTimeoutInit = {}): Promise<Response> {\n const { timeoutMs = DEFAULT_FETCH_TIMEOUT_MS, signal: callerSignal, ...rest } = init;\n\n if (callerSignal?.aborted) {\n throw callerSignal.reason ?? new DOMException(\"Aborted\", \"AbortError\");\n }\n\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new DOMException(`fetch timed out after ${timeoutMs}ms`, \"TimeoutError\"));\n }, timeoutMs);\n\n const unsubscribeCaller = bridgeExternalSignal(callerSignal, controller);\n\n try {\n return await fetch(url, { ...rest, signal: controller.signal });\n } finally {\n clearTimeout(timer);\n unsubscribeCaller?.();\n }\n}\n\nfunction bridgeExternalSignal(external: AbortSignal | null | undefined, controller: AbortController): (() => void) | null {\n if (!external) return null;\n const onAbort = () => controller.abort(external.reason);\n external.addEventListener(\"abort\", onAbort, { once: true });\n return () => external.removeEventListener(\"abort\", onAbort);\n}\n","// Google OAuth for the host machine, independent of Firebase Auth (which\n// discards refresh tokens). Entry points:\n// - authorizeGoogle(): one-shot loopback + PKCE browser consent flow\n// (desktop-app clients may redirect to any 127.0.0.1 port), storing the\n// refresh token locally via tokenStore.\n// - getGoogleAccessToken(): mints a fresh access token from the stored\n// refresh token; the OAuth2Client \"tokens\" event persists rotations.\n// - unlinkGoogle(): best-effort revoke at Google + local token delete.\nimport { randomBytes } from \"node:crypto\";\nimport http from \"node:http\";\nimport { CodeChallengeMethod, OAuth2Client, type Credentials } from \"google-auth-library\";\nimport { log } from \"./host.js\";\nimport { errorMessage, ONE_MINUTE_MS, ONE_SECOND_MS } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\nimport { loadClientSecret, type InstalledClientSecret } from \"./clientSecret.js\";\nimport { deleteGoogleTokens, loadGoogleTokens, saveGoogleTokens } from \"./tokenStore.js\";\n\nexport const GOOGLE_CALENDAR_SCOPE = \"https://www.googleapis.com/auth/calendar.events\";\nexport const GOOGLE_TASKS_SCOPE = \"https://www.googleapis.com/auth/tasks\";\nexport const GOOGLE_DRIVE_FILE_SCOPE = \"https://www.googleapis.com/auth/drive.file\";\n/** Requested at consent as one set — matches the scopes registered on the\n * OAuth consent screen, so a single re-link covers every supported API\n * (Calendar now; Tasks / Drive tools ride the same grant later). */\nexport const GOOGLE_SCOPES = [GOOGLE_CALENDAR_SCOPE, GOOGLE_TASKS_SCOPE, GOOGLE_DRIVE_FILE_SCOPE];\nconst CALLBACK_PATH = \"/oauth2callback\";\nconst AUTH_TIMEOUT_MS = 5 * ONE_MINUTE_MS;\nconst STATE_BYTES = 16;\n\nexport interface AuthorizeGoogleOptions {\n home?: string;\n /** Called with the consent URL; open it in a browser (and/or print it). */\n onAuthUrl?: (url: string) => void;\n timeoutMs?: number;\n}\n\nconst createClient = (secret: InstalledClientSecret, redirectUri?: string): OAuth2Client =>\n new OAuth2Client({ clientId: secret.client_id, clientSecret: secret.client_secret, redirectUri });\n\nconst persistRotatedTokens = (client: OAuth2Client, home?: string): void => {\n client.on(\"tokens\", (tokens) => {\n saveGoogleTokens(tokens, home).catch((err: unknown) => {\n log.error(\"google\", \"failed to persist rotated tokens\", { error: String(err) });\n });\n });\n};\n\nexport async function getGoogleAccessToken(home?: string): Promise<string> {\n const saved = await loadGoogleTokens(home);\n if (!saved?.refresh_token) {\n // Host-neutral wording — this engine ships to multiple hosts whose link\n // flows differ (#2128); each host's own help carries the specific steps.\n throw new Error(\"Google account not linked on this host — ask the user to link their Google account in this app's settings, then retry\");\n }\n const client = createClient(await loadClientSecret(home));\n client.setCredentials(saved);\n persistRotatedTokens(client, home);\n const { token } = await client.getAccessToken();\n if (!token) {\n throw new Error(\"could not obtain a Google access token — the grant may have been revoked; re-link the account\");\n }\n return token;\n}\n\nconst REVOKE_URL = \"https://oauth2.googleapis.com/revoke\";\n\n/** The revoke POST, injectable for tests. */\nexport type RevokeFetch = typeof fetchWithTimeout;\n\n/** Revoke the grant at Google (best-effort) and delete the local token file.\n * Revoke failures are logged but never block the local delete — Google may\n * already consider the token invalid, and keeping the file would leave the\n * user unable to unlink. */\nexport async function unlinkGoogle(home?: string, revokeFetch: RevokeFetch = fetchWithTimeout): Promise<void> {\n const saved = await loadGoogleTokens(home);\n const token = saved?.refresh_token ?? saved?.access_token;\n if (token) {\n try {\n const response = await revokeFetch(REVOKE_URL, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams({ token }).toString(),\n });\n if (!response.ok) log.warn(\"google\", \"token revoke returned non-ok\", { status: response.status });\n } catch (err) {\n log.warn(\"google\", \"token revoke failed, deleting local tokens anyway\", { error: errorMessage(err) });\n }\n }\n await deleteGoogleTokens(home);\n}\n\nconst startLoopbackServer = (): Promise<{ server: http.Server; port: number }> =>\n new Promise((resolve, reject) => {\n const server = http.createServer();\n server.once(\"error\", reject);\n server.listen(0, \"127.0.0.1\", () => {\n const address = server.address();\n if (address === null || typeof address === \"string\") {\n reject(new Error(\"loopback server has no port\"));\n return;\n }\n resolve({ server, port: address.port });\n });\n });\n\n// State is validated before error/code — a callback that can't prove it\n// belongs to this flow must not influence it (its `error` text would\n// otherwise reach the terminal attacker-controlled).\nconst authCodeFromCallback = (url: URL, expectedState: string): string => {\n if (url.searchParams.get(\"state\") !== expectedState) throw new Error(\"OAuth state mismatch — possible CSRF, aborting\");\n const error = url.searchParams.get(\"error\");\n if (error) throw new Error(`Google authorization failed: ${error}`);\n const code = url.searchParams.get(\"code\");\n if (!code) throw new Error(\"authorization callback carried no code\");\n return code;\n};\n\nconst respondHtml = (res: http.ServerResponse, status: number, message: string): void => {\n res.writeHead(status, { \"Content-Type\": \"text/html; charset=utf-8\" });\n res.end(`<html><body><h3>${message}</h3></body></html>`);\n};\n\nexport const waitForAuthCode = (server: http.Server, expectedState: string, timeoutMs: number): Promise<string> =>\n new Promise((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(`authorization timed out after ${timeoutMs / ONE_SECOND_MS}s`)), timeoutMs);\n server.on(\"request\", (req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n if (url.pathname !== CALLBACK_PATH) {\n res.writeHead(404);\n res.end();\n return;\n }\n // A wrong-state request is not our callback (drive-by localhost probe\n // or stale tab) — answer it but keep waiting for the real redirect, so\n // an unauthenticated request can't abort the pending flow.\n if (url.searchParams.get(\"state\") !== expectedState) {\n respondHtml(res, 400, \"Invalid authorization callback. You can close this tab.\");\n return;\n }\n clearTimeout(timer);\n try {\n const code = authCodeFromCallback(url, expectedState);\n respondHtml(res, 200, \"Authorization complete — you can close this tab.\");\n resolve(code);\n } catch (err) {\n // Static text only — the failure detail echoes query-string content,\n // which must not be reflected into HTML. The CLI prints the detail.\n respondHtml(res, 400, \"Authorization failed — see the terminal for details. You can close this tab.\");\n reject(err);\n }\n });\n });\n\n// `access_type: offline` + `prompt: consent` force Google to return a refresh\n// token on every run (repeat consents otherwise omit it).\nconst buildConsentUrl = (client: OAuth2Client, codeChallenge: string, state: string): string =>\n client.generateAuthUrl({\n access_type: \"offline\",\n prompt: \"consent\",\n scope: GOOGLE_SCOPES,\n code_challenge_method: CodeChallengeMethod.S256,\n code_challenge: codeChallenge,\n state,\n });\n\nexport async function authorizeGoogle(opts: AuthorizeGoogleOptions = {}): Promise<Credentials> {\n const secret = await loadClientSecret(opts.home);\n const { server, port } = await startLoopbackServer();\n try {\n const client = createClient(secret, `http://127.0.0.1:${port}${CALLBACK_PATH}`);\n const { codeVerifier, codeChallenge } = await client.generateCodeVerifierAsync();\n if (!codeChallenge) throw new Error(\"failed to derive a PKCE code challenge\");\n const state = randomBytes(STATE_BYTES).toString(\"hex\");\n opts.onAuthUrl?.(buildConsentUrl(client, codeChallenge, state));\n const code = await waitForAuthCode(server, state, opts.timeoutMs ?? AUTH_TIMEOUT_MS);\n const { tokens } = await client.getToken({ code, codeVerifier });\n if (!tokens.refresh_token) {\n throw new Error(\"Google returned no refresh token — remove this app under Google Account → Security → Third-party access, then retry\");\n }\n return await saveGoogleTokens(tokens, opts.home);\n } finally {\n server.close();\n }\n}\n","// In-flight manager for the settings-UI OAuth flow. authorizeGoogle()\n// resolves only after the user finishes the browser consent, so the HTTP\n// layer starts it in the background, returns the consent URL immediately,\n// and reports progress via status polling. One flow at a time — the guard\n// is the in-flight start promise itself (set synchronously before any\n// await), so concurrent authorize requests share one flow instead of\n// spawning parallel loopback listeners.\nimport { log } from \"./host.js\";\nimport { errorMessage } from \"./util.js\";\nimport { authorizeGoogle } from \"./auth.js\";\n\nexport interface GoogleAuthFlowStatus {\n pending: boolean;\n lastError: string | null;\n}\n\nexport interface GoogleAuthFlow {\n start: () => Promise<{ authUrl: string }>;\n status: () => GoogleAuthFlowStatus;\n}\n\nexport const createGoogleAuthFlow = (authorize: typeof authorizeGoogle): GoogleAuthFlow => {\n let inFlightStart: Promise<{ authUrl: string }> | null = null;\n let flowRunning = false;\n let lastError: string | null = null;\n\n const launchFlow = (): Promise<{ authUrl: string }> =>\n new Promise((resolve, reject) => {\n flowRunning = true;\n authorize({\n onAuthUrl: (url) => resolve({ authUrl: url }),\n })\n .then(() => log.info(\"google\", \"authorize flow completed\"))\n .catch((err: unknown) => {\n lastError = errorMessage(err);\n log.warn(\"google\", \"authorize flow failed\", { error: lastError });\n // No-op when the URL already resolved; covers pre-URL failures\n // (missing client secret, port bind error).\n reject(err instanceof Error ? err : new Error(String(err)));\n })\n .finally(() => {\n flowRunning = false;\n inFlightStart = null;\n });\n });\n\n const start = (): Promise<{ authUrl: string }> => {\n if (inFlightStart) return inFlightStart;\n lastError = null;\n inFlightStart = launchFlow();\n return inFlightStart;\n };\n\n const status = (): GoogleAuthFlowStatus => ({ pending: flowRunning, lastError });\n\n return { start, status };\n};\n\nexport const googleAuthFlow = createGoogleAuthFlow(authorizeGoogle);\n","// Shared REST plumbing for the Google APIs this engine wraps (Calendar,\n// Tasks, Drive). Plain fetch instead of the `googleapis` SDK — a handful of\n// endpoints don't justify the dependency (see\n// plans/done/feat-google-oauth-calendar.md).\nimport { errorMessage, isRecord, ONE_SECOND_MS, truncate } from \"./util.js\";\nimport { fetchWithTimeout } from \"./fetch.js\";\n\nexport const GOOGLE_API_TIMEOUT_MS = 30 * ONE_SECOND_MS;\nconst ERROR_BODY_MAX_CHARS = 300;\nconst HTTP_FORBIDDEN = 403;\nexport const DEFAULT_LIST_MAX_RESULTS = 10;\nexport const MAX_LIST_RESULTS = 50;\n\n/** 403 usually means the API is not enabled for the user's Cloud project —\n * name the API so the agent's recovery guidance can be specific. */\nexport const googleApiError = (apiLabel: string, status: number, body: string): Error => {\n const hint = status === HTTP_FORBIDDEN ? ` (is the ${apiLabel} enabled for the Cloud project?)` : \"\";\n const detail = body ? ` — ${truncate(body, ERROR_BODY_MAX_CHARS)}` : \"\";\n return new Error(`${apiLabel}: HTTP ${status}${hint}${detail}`);\n};\n\nexport interface GoogleRequestInit {\n method?: string;\n body?: string;\n /** Overrides the default JSON content type (multipart upload, …). */\n contentType?: string;\n /** Response is not JSON (Drive media download) — return the raw text. */\n expectText?: boolean;\n}\n\nexport async function googleRequest(apiLabel: string, accessToken: string, url: string, init: GoogleRequestInit = {}): Promise<unknown> {\n const { contentType = \"application/json\", expectText = false, ...rest } = init;\n const response = await fetchWithTimeout(url, {\n ...rest,\n timeoutMs: GOOGLE_API_TIMEOUT_MS,\n headers: { Authorization: `Bearer ${accessToken}`, \"Content-Type\": contentType },\n });\n if (!response.ok) {\n const body = await response.text().catch((err: unknown) => errorMessage(err));\n throw googleApiError(apiLabel, response.status, body);\n }\n if (expectText) return await response.text();\n // 204 (Drive delete, …) has no body to parse.\n if (response.status === 204) return {};\n return await response.json();\n}\n\nexport const stringField = (record: Record<string, unknown>, key: string): string => (typeof record[key] === \"string\" ? record[key] : \"\");\n\nexport const asRecord = (value: unknown): Record<string, unknown> => (isRecord(value) ? value : {});\n\nexport const itemsOf = (value: unknown): unknown[] => {\n const record = asRecord(value);\n return Array.isArray(record.items) ? record.items : [];\n};\n","// Google Calendar v3 REST calls against the user's primary calendar.\nimport { asRecord, googleApiError, googleRequest, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\nimport { isRecord } from \"./util.js\";\n\nconst CALENDAR_EVENTS_URL = \"https://www.googleapis.com/calendar/v3/calendars/primary/events\";\nconst CALENDAR_API_LABEL = \"Google Calendar API\";\n\nexport interface CalendarEventInput {\n summary: string;\n startDateTime: string;\n endDateTime: string;\n description?: string;\n}\n\nexport interface ListEventsInput {\n timeMin?: string;\n maxResults?: number;\n}\n\nexport interface CalendarEventSummary {\n id: string;\n summary: string;\n start: string;\n end: string;\n htmlLink: string;\n status: string;\n}\n\n// All-day events carry `date`, timed events carry `dateTime`.\nconst eventTime = (value: unknown): string => {\n if (!isRecord(value)) return \"\";\n if (typeof value.dateTime === \"string\") return value.dateTime;\n if (typeof value.date === \"string\") return value.date;\n return \"\";\n};\n\nexport const toEventSummary = (value: unknown): CalendarEventSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n summary: stringField(record, \"summary\"),\n start: eventTime(record.start),\n end: eventTime(record.end),\n htmlLink: stringField(record, \"htmlLink\"),\n status: stringField(record, \"status\"),\n };\n};\n\n/** Kept as a named export for the existing unit tests / callers; the shared\n * helper now carries the wording. */\nexport const calendarApiError = (status: number, body: string): Error => googleApiError(CALENDAR_API_LABEL, status, body);\n\nexport async function createCalendarEvent(accessToken: string, input: CalendarEventInput): Promise<CalendarEventSummary> {\n const body = {\n summary: input.summary,\n description: input.description,\n start: { dateTime: input.startDateTime },\n end: { dateTime: input.endDateTime },\n };\n const created = await googleRequest(CALENDAR_API_LABEL, accessToken, CALENDAR_EVENTS_URL, { method: \"POST\", body: JSON.stringify(body) });\n return toEventSummary(created);\n}\n\nexport async function listCalendarEvents(accessToken: string, input: ListEventsInput = {}): Promise<CalendarEventSummary[]> {\n const params = new URLSearchParams({\n timeMin: input.timeMin ?? new Date().toISOString(),\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n singleEvents: \"true\",\n orderBy: \"startTime\",\n });\n const listed = await googleRequest(CALENDAR_API_LABEL, accessToken, `${CALENDAR_EVENTS_URL}?${params.toString()}`);\n const record = asRecord(listed);\n const items = Array.isArray(record.items) ? record.items : [];\n return items.map(toEventSummary);\n}\n","// Google Tasks v1 REST calls. `@default` is Google's alias for the user's\n// default task list, so callers can stay list-agnostic.\nimport { asRecord, googleRequest, itemsOf, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst TASKS_BASE_URL = \"https://tasks.googleapis.com/tasks/v1\";\nconst TASKS_API_LABEL = \"Google Tasks API\";\nconst DEFAULT_TASK_LIST_ID = \"@default\";\nconst TASK_STATUS_COMPLETED = \"completed\";\nconst MAX_TASK_LISTS = 50;\n\nexport interface TaskListSummary {\n id: string;\n title: string;\n}\n\nexport interface TaskSummary {\n id: string;\n title: string;\n status: string;\n due: string;\n notes: string;\n}\n\nexport interface ListTasksInput {\n taskListId?: string;\n maxResults?: number;\n showCompleted?: boolean;\n}\n\nexport interface CreateTaskInput {\n title: string;\n notes?: string;\n /** RFC3339. Google stores a DATE only — the time part is recorded but\n * ignored by the UI, so callers should not promise time-of-day fidelity. */\n due?: string;\n taskListId?: string;\n}\n\nexport interface CompleteTaskInput {\n taskId: string;\n taskListId?: string;\n}\n\nexport const toTaskListSummary = (value: unknown): TaskListSummary => {\n const record = asRecord(value);\n return { id: stringField(record, \"id\"), title: stringField(record, \"title\") };\n};\n\nexport const toTaskSummary = (value: unknown): TaskSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n title: stringField(record, \"title\"),\n status: stringField(record, \"status\"),\n due: stringField(record, \"due\"),\n notes: stringField(record, \"notes\"),\n };\n};\n\nconst tasksUrl = (taskListId: string | undefined, suffix = \"\"): string =>\n `${TASKS_BASE_URL}/lists/${encodeURIComponent(taskListId ?? DEFAULT_TASK_LIST_ID)}/tasks${suffix}`;\n\nexport async function listTaskLists(accessToken: string): Promise<TaskListSummary[]> {\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${TASKS_BASE_URL}/users/@me/lists?maxResults=${MAX_TASK_LISTS}`);\n return itemsOf(listed).map(toTaskListSummary);\n}\n\nexport async function listTasks(accessToken: string, input: ListTasksInput = {}): Promise<TaskSummary[]> {\n const params = new URLSearchParams({\n maxResults: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n showCompleted: String(input.showCompleted ?? false),\n });\n const listed = await googleRequest(TASKS_API_LABEL, accessToken, `${tasksUrl(input.taskListId)}?${params.toString()}`);\n return itemsOf(listed).map(toTaskSummary);\n}\n\nexport async function createTask(accessToken: string, input: CreateTaskInput): Promise<TaskSummary> {\n const body = { title: input.title, notes: input.notes, due: input.due };\n const created = await googleRequest(TASKS_API_LABEL, accessToken, tasksUrl(input.taskListId), { method: \"POST\", body: JSON.stringify(body) });\n return toTaskSummary(created);\n}\n\nexport async function completeTask(accessToken: string, input: CompleteTaskInput): Promise<TaskSummary> {\n // PATCH keeps the rest of the task intact — a PUT would need the full body\n // and would silently drop fields the caller never read.\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n const updated = await googleRequest(TASKS_API_LABEL, accessToken, url, {\n method: \"PATCH\",\n body: JSON.stringify({ status: TASK_STATUS_COMPLETED }),\n });\n return toTaskSummary(updated);\n}\n\nexport async function deleteTask(accessToken: string, input: CompleteTaskInput): Promise<void> {\n const url = tasksUrl(input.taskListId, `/${encodeURIComponent(input.taskId)}`);\n await googleRequest(TASKS_API_LABEL, accessToken, url, { method: \"DELETE\" });\n}\n","// Google Drive v3 REST calls under the `drive.file` scope — the app only\n// ever sees files IT created, never the user's wider Drive. That narrow\n// scope is why this is non-sensitive and needs no Google verification\n// review; keep every call inside it.\nimport { randomBytes } from \"node:crypto\";\nimport { asRecord, googleRequest, stringField, DEFAULT_LIST_MAX_RESULTS } from \"./apiClient.js\";\n\nconst DRIVE_FILES_URL = \"https://www.googleapis.com/drive/v3/files\";\nconst DRIVE_UPLOAD_URL = \"https://www.googleapis.com/upload/drive/v3/files\";\nconst DRIVE_API_LABEL = \"Google Drive API\";\nconst DEFAULT_MIME_TYPE = \"text/plain\";\nconst FILE_FIELDS = \"id,name,mimeType,webViewLink,modifiedTime\";\n// Reading a huge blob into a tool result would blow the context window; the\n// tool surface is for app-created text documents, not media.\nconst MAX_READ_CHARS = 100_000;\nconst TEXT_MIME_PREFIXES = [\"text/\", \"application/json\", \"application/xml\", \"application/javascript\"];\n\nexport interface DriveFileSummary {\n id: string;\n name: string;\n mimeType: string;\n webViewLink: string;\n modifiedTime: string;\n}\n\nexport interface ListDriveFilesInput {\n maxResults?: number;\n}\n\nexport interface CreateDriveFileInput {\n name: string;\n content: string;\n mimeType?: string;\n}\n\nexport interface ReadDriveFileInput {\n fileId: string;\n}\n\nexport const toDriveFileSummary = (value: unknown): DriveFileSummary => {\n const record = asRecord(value);\n return {\n id: stringField(record, \"id\"),\n name: stringField(record, \"name\"),\n mimeType: stringField(record, \"mimeType\"),\n webViewLink: stringField(record, \"webViewLink\"),\n modifiedTime: stringField(record, \"modifiedTime\"),\n };\n};\n\nexport const isTextMimeType = (mimeType: string): boolean => TEXT_MIME_PREFIXES.some((prefix) => mimeType.startsWith(prefix));\n\nexport async function listDriveFiles(accessToken: string, input: ListDriveFilesInput = {}): Promise<DriveFileSummary[]> {\n const params = new URLSearchParams({\n pageSize: String(input.maxResults ?? DEFAULT_LIST_MAX_RESULTS),\n fields: `files(${FILE_FIELDS})`,\n orderBy: \"modifiedTime desc\",\n });\n const listed = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}?${params.toString()}`);\n const record = asRecord(listed);\n const files = Array.isArray(record.files) ? record.files : [];\n return files.map(toDriveFileSummary);\n}\n\n// Multipart upload: metadata part + media part in one request. Built by hand\n// because the `googleapis` SDK is the only alternative and this is the sole\n// multipart call in the engine.\nconst BOUNDARY_BYTES = 16;\n\n/** Fresh per request: a fixed boundary appearing inside file content would\n * split the payload at the wrong place, so Drive would store a truncated or\n * mangled file. Random 128 bits makes an accidental — or crafted — collision\n * infeasible; `newMultipartBoundary` is re-derived until it is absent from\n * the body it must delimit. */\nconst newMultipartBoundary = (): string => `mulmo-drive-${randomBytes(BOUNDARY_BYTES).toString(\"hex\")}`;\n\n// RFC 2045 token + parameters: no CR/LF (header injection into the part\n// headers) and no quotes/semicolons that would let a crafted value forge\n// extra headers or parameters.\nconst MIME_TYPE_RE = /^[A-Za-z0-9!#$&^_.+-]+\\/[A-Za-z0-9!#$&^_.+-]+$/;\n\nexport const assertSafeMimeType = (mimeType: string): string => {\n if (!MIME_TYPE_RE.test(mimeType)) throw new Error(`Google Drive API: invalid mimeType '${mimeType}' — expected a plain type/subtype such as text/plain`);\n return mimeType;\n};\n\nexport const buildMultipartBody = (metadata: Record<string, string>, content: string, mimeType: string, boundary: string): string =>\n [\n `--${boundary}`,\n \"Content-Type: application/json; charset=UTF-8\",\n \"\",\n JSON.stringify(metadata),\n `--${boundary}`,\n `Content-Type: ${mimeType}`,\n \"\",\n content,\n `--${boundary}--`,\n \"\",\n ].join(\"\\r\\n\");\n\n/** A boundary that appears nowhere in the parts it delimits. */\nexport const pickBoundary = (parts: string[], generate: () => string = newMultipartBoundary): string => {\n const collides = (candidate: string): boolean => parts.some((part) => part.includes(candidate));\n let boundary = generate();\n while (collides(boundary)) boundary = generate();\n return boundary;\n};\n\nexport async function createDriveFile(accessToken: string, input: CreateDriveFileInput): Promise<DriveFileSummary> {\n const mimeType = assertSafeMimeType(input.mimeType ?? DEFAULT_MIME_TYPE);\n const metadata = { name: input.name, mimeType };\n const boundary = pickBoundary([input.content, JSON.stringify(metadata)]);\n const params = new URLSearchParams({ uploadType: \"multipart\", fields: FILE_FIELDS });\n const created = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_UPLOAD_URL}?${params.toString()}`, {\n method: \"POST\",\n contentType: `multipart/related; boundary=${boundary}`,\n body: buildMultipartBody(metadata, input.content, mimeType, boundary),\n });\n return toDriveFileSummary(created);\n}\n\nexport async function readDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<{ file: DriveFileSummary; content: string }> {\n const fileId = encodeURIComponent(input.fileId);\n const metadata = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?fields=${FILE_FIELDS}`);\n const file = toDriveFileSummary(metadata);\n if (!isTextMimeType(file.mimeType)) {\n throw new Error(`Google Drive API: '${file.name}' is ${file.mimeType || \"a binary file\"} — only text files can be read as content`);\n }\n const raw = await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${fileId}?alt=media`, { expectText: true });\n const content = typeof raw === \"string\" ? raw.slice(0, MAX_READ_CHARS) : \"\";\n return { file, content };\n}\n\nexport async function deleteDriveFile(accessToken: string, input: ReadDriveFileInput): Promise<void> {\n await googleRequest(DRIVE_API_LABEL, accessToken, `${DRIVE_FILES_URL}/${encodeURIComponent(input.fileId)}`, { method: \"DELETE\" });\n}\n"],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ export interface TaskListSummary {
2
+ id: string;
3
+ title: string;
4
+ }
5
+ export interface TaskSummary {
6
+ id: string;
7
+ title: string;
8
+ status: string;
9
+ due: string;
10
+ notes: string;
11
+ }
12
+ export interface ListTasksInput {
13
+ taskListId?: string;
14
+ maxResults?: number;
15
+ showCompleted?: boolean;
16
+ }
17
+ export interface CreateTaskInput {
18
+ title: string;
19
+ notes?: string;
20
+ /** RFC3339. Google stores a DATE only — the time part is recorded but
21
+ * ignored by the UI, so callers should not promise time-of-day fidelity. */
22
+ due?: string;
23
+ taskListId?: string;
24
+ }
25
+ export interface CompleteTaskInput {
26
+ taskId: string;
27
+ taskListId?: string;
28
+ }
29
+ export declare const toTaskListSummary: (value: unknown) => TaskListSummary;
30
+ export declare const toTaskSummary: (value: unknown) => TaskSummary;
31
+ export declare function listTaskLists(accessToken: string): Promise<TaskListSummary[]>;
32
+ export declare function listTasks(accessToken: string, input?: ListTasksInput): Promise<TaskSummary[]>;
33
+ export declare function createTask(accessToken: string, input: CreateTaskInput): Promise<TaskSummary>;
34
+ export declare function completeTask(accessToken: string, input: CompleteTaskInput): Promise<TaskSummary>;
35
+ export declare function deleteTask(accessToken: string, input: CompleteTaskInput): Promise<void>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mulmoclaude/core",
3
- "version": "0.20.2",
3
+ "version": "0.21.0",
4
4
  "description": "Shared server-side core for MulmoClaude and MulmoTerminal — the always-shipped-together subsystems consolidated behind subpath exports so the two hosts can't drift. Server-only except the browser-safe ./whisper/client, ./workspace-setup/slug, ./translation/client, ./remote-view and ./remote-host entries. All host specifics are injected.",
5
5
  "type": "module",
6
6
  "exports": {