gogcli-mcp-gmail 2.18.3 → 2.18.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -31280,6 +31280,198 @@ async function run(args, options = {}) {
31280
31280
  }
31281
31281
  }
31282
31282
 
31283
+ // ../gogcli-mcp/src/timestamps.ts
31284
+ var DEFAULT_DISPLAY_TZ = "America/New_York";
31285
+ function isValidTimeZone(tz) {
31286
+ try {
31287
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
31288
+ return true;
31289
+ } catch {
31290
+ return false;
31291
+ }
31292
+ }
31293
+ function displayTimeZone() {
31294
+ const configured = readEnvVar("DISPLAY_TZ");
31295
+ if (configured && isValidTimeZone(configured)) return configured;
31296
+ return DEFAULT_DISPLAY_TZ;
31297
+ }
31298
+ function naiveSourceTimeZone() {
31299
+ const configured = readEnvVar("GOG_TIMEZONE");
31300
+ if (configured && isValidTimeZone(configured)) return configured;
31301
+ return displayTimeZone();
31302
+ }
31303
+ function offsetAt(instant, tz) {
31304
+ const w = wallPartsIn(instant, tz);
31305
+ const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
31306
+ const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
31307
+ const sign = minutes < 0 ? "-" : "+";
31308
+ const abs = Math.abs(minutes);
31309
+ return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
31310
+ }
31311
+ function wallPartsIn(instant, tz) {
31312
+ const parts = new Intl.DateTimeFormat("en-US", {
31313
+ timeZone: tz,
31314
+ year: "numeric",
31315
+ month: "2-digit",
31316
+ day: "2-digit",
31317
+ hour: "2-digit",
31318
+ minute: "2-digit",
31319
+ second: "2-digit",
31320
+ // h23 pins midnight to hour 00; without it some ICU builds render hour 24.
31321
+ hourCycle: "h23"
31322
+ }).formatToParts(instant);
31323
+ const out = {};
31324
+ for (const p of parts) {
31325
+ if (p.type !== "literal") out[p.type] = Number(p.value);
31326
+ }
31327
+ return out;
31328
+ }
31329
+ function wallTimeToInstant(y, mo, d, h, mi, s, ms, tz) {
31330
+ let guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
31331
+ for (let i = 0; i < 2; i += 1) {
31332
+ const seen = wallPartsIn(new Date(guess), tz);
31333
+ const seenUTC = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second, ms);
31334
+ const drift = Date.UTC(y, mo - 1, d, h, mi, s, ms) - seenUTC;
31335
+ if (drift === 0) break;
31336
+ guess += drift;
31337
+ }
31338
+ return new Date(guess);
31339
+ }
31340
+ function pad(n, width = 2) {
31341
+ return String(n).padStart(width, "0");
31342
+ }
31343
+ function isoWithOffset(instant, tz, offset) {
31344
+ const w = wallPartsIn(instant, tz);
31345
+ const msPart = instant.getUTCMilliseconds();
31346
+ const frac = msPart ? `.${pad(msPart, 3)}` : "";
31347
+ return `${pad(w.year, 4)}-${pad(w.month)}-${pad(w.day)}T${pad(w.hour)}:${pad(w.minute)}:${pad(w.second)}${frac}${offset}`;
31348
+ }
31349
+ function formatInstant(instant, tz = displayTimeZone()) {
31350
+ const offset = offsetAt(instant, tz);
31351
+ const display = new Intl.DateTimeFormat("en-US", {
31352
+ timeZone: tz,
31353
+ weekday: "short",
31354
+ month: "short",
31355
+ day: "numeric",
31356
+ year: "numeric",
31357
+ hour: "numeric",
31358
+ minute: "2-digit",
31359
+ timeZoneName: "short"
31360
+ }).format(instant);
31361
+ return { iso: isoWithOffset(instant, tz, offset), display };
31362
+ }
31363
+ var TIMESTAMP_KEYS = /* @__PURE__ */ new Set([
31364
+ "date",
31365
+ // gog gmail message/thread listings ("2026-07-28 03:36")
31366
+ "dateTime",
31367
+ // Calendar event start/end
31368
+ "internalDate",
31369
+ // Gmail, epoch milliseconds (authoritative)
31370
+ "modifiedTime",
31371
+ // Drive
31372
+ "createdTime",
31373
+ // Drive
31374
+ "createTime",
31375
+ "updateTime",
31376
+ "updated",
31377
+ "originalStartTime",
31378
+ "expirationTime",
31379
+ "lastModified",
31380
+ "sentAt",
31381
+ "viewedAt",
31382
+ "modifiedAt",
31383
+ "fetchedBodyAt",
31384
+ "asOf"
31385
+ ]);
31386
+ var ZONE_NAME_KEYS = /* @__PURE__ */ new Set(["timeZone", "timezone"]);
31387
+ var RFC3339_WITH_OFFSET = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?(Z|[+-]\d{2}:?\d{2})$/;
31388
+ var NAIVE_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?$/;
31389
+ var EPOCH_MILLIS = /^\d{13}$/;
31390
+ var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
31391
+ function isRealCalendarDate(p) {
31392
+ const utc = new Date(Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second));
31393
+ return utc.getUTCFullYear() === p.year && utc.getUTCMonth() === p.month - 1 && utc.getUTCDate() === p.day && utc.getUTCHours() === p.hour && utc.getUTCMinutes() === p.minute && utc.getUTCSeconds() === p.second;
31394
+ }
31395
+ function parseTimestampValue(key, value, assumeNaiveIn) {
31396
+ if (typeof value !== "string") return null;
31397
+ const raw = value.trim();
31398
+ if (raw === "" || DATE_ONLY.test(raw)) return null;
31399
+ if (key === "internalDate" && EPOCH_MILLIS.test(raw)) {
31400
+ return new Date(Number(raw));
31401
+ }
31402
+ if (RFC3339_WITH_OFFSET.test(raw)) {
31403
+ const parsed = new Date(raw.replace(" ", "T"));
31404
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
31405
+ }
31406
+ const naive = NAIVE_DATE_TIME.exec(raw);
31407
+ if (naive) {
31408
+ const [, y, mo, d, h, mi, s, frac] = naive;
31409
+ const ms = frac ? Number(frac.padEnd(3, "0").slice(0, 3)) : 0;
31410
+ const parts = {
31411
+ year: Number(y),
31412
+ month: Number(mo),
31413
+ day: Number(d),
31414
+ hour: Number(h),
31415
+ minute: Number(mi),
31416
+ second: Number(s ?? "0")
31417
+ };
31418
+ if (!isRealCalendarDate(parts)) return null;
31419
+ return wallTimeToInstant(
31420
+ parts.year,
31421
+ parts.month,
31422
+ parts.day,
31423
+ parts.hour,
31424
+ parts.minute,
31425
+ parts.second,
31426
+ ms,
31427
+ assumeNaiveIn
31428
+ );
31429
+ }
31430
+ return null;
31431
+ }
31432
+ function walk(node, tz, naiveTz) {
31433
+ let changed = false;
31434
+ if (Array.isArray(node)) {
31435
+ for (const item of node) {
31436
+ if (walk(item, tz, naiveTz)) changed = true;
31437
+ }
31438
+ return changed;
31439
+ }
31440
+ if (node === null || typeof node !== "object") return false;
31441
+ const obj = node;
31442
+ for (const key of Object.keys(obj)) {
31443
+ const value = obj[key];
31444
+ if (value !== null && typeof value === "object") {
31445
+ if (walk(value, tz, naiveTz)) changed = true;
31446
+ continue;
31447
+ }
31448
+ if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
31449
+ const instant = parseTimestampValue(key, value, naiveTz);
31450
+ if (!instant) continue;
31451
+ const { iso, display } = formatInstant(instant, tz);
31452
+ obj[key] = iso;
31453
+ obj[`${key}Display`] = display;
31454
+ changed = true;
31455
+ }
31456
+ return changed;
31457
+ }
31458
+ function detectIndent(text) {
31459
+ const match = /\n(\s+)\S/.exec(text);
31460
+ return match ? match[1].replace(/\t/g, " ").length : 0;
31461
+ }
31462
+ function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSourceTimeZone()) {
31463
+ const trimmed = text.trim();
31464
+ if (trimmed === "" || !/^[[{]/.test(trimmed)) return text;
31465
+ let parsed;
31466
+ try {
31467
+ parsed = JSON.parse(trimmed);
31468
+ } catch {
31469
+ return text;
31470
+ }
31471
+ if (!walk(parsed, tz, naiveTz)) return text;
31472
+ return JSON.stringify(parsed, null, detectIndent(text));
31473
+ }
31474
+
31283
31475
  // ../gogcli-mcp/src/tools/utils.ts
31284
31476
  var PAYLOAD_INLINE_MAX = 4096;
31285
31477
  function payloadArg(inlineFlag, fileFlag, value, ext) {
@@ -31380,7 +31572,8 @@ ${accounts || "(none)"}${hint}`);
31380
31572
  }
31381
31573
  async function runOrDiagnose(args, options) {
31382
31574
  try {
31383
- return rawTextResult(await run(args, options));
31575
+ const raw = await run(args, options);
31576
+ return rawTextResult(options.lossless ? raw : normalizeTimestamps(raw));
31384
31577
  } catch (err) {
31385
31578
  return diagnose(err);
31386
31579
  }
@@ -31617,7 +31810,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
31617
31810
  );
31618
31811
 
31619
31812
  // ../gogcli-mcp/src/server.ts
31620
- var VERSION = true ? "2.18.3" : "0.0.0";
31813
+ var VERSION = true ? "2.18.4" : "0.0.0";
31621
31814
 
31622
31815
  // src/tools/gmail-extra.ts
31623
31816
  function assertNotBoth(inlineParam, fileParam, inlineValue, fileValue) {
@@ -31790,7 +31983,7 @@ function registerExtraGmailTools(server) {
31790
31983
  const args = ["gmail", "raw", messageId];
31791
31984
  if (format) args.push(`--format=${format}`);
31792
31985
  if (pretty) args.push("--pretty");
31793
- return runOrDiagnose(args, { account });
31986
+ return runOrDiagnose(args, { account, lossless: true });
31794
31987
  });
31795
31988
  server.registerTool("gog_gmail_attachment", {
31796
31989
  description: `Download a Gmail attachment and deliver its contents so you can actually read them. The real filename and MIME type are resolved from the message part metadata, so the saved file and response are named correctly (e.g. Guest_Copy.pdf), never a generic *.bin. deliver="auto" (default) is transport-aware: images always come back as a native image block; anything else is delivered by the channel that works on your transport \u2014 a readable server-side file PATH on local (stdio) clients that share the filesystem, or a Google Drive link on the remote connector (whose backend filesystem you can't read, and which rejects inline PDF/binary blobs). deliver="inline" forces the bytes inline as an image or embedded resource blob (use only if your client consumes resource blobs; errors if over gog's 3 MiB cap). deliver="drive" always uploads to Drive; deliver="off" writes the file server-side and returns {path, fileName, mimeType, bytes}. Drive delivery creates a file in your Drive (blocked when GOG_READONLY is set).`,
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-gmail",
5
5
  "display_name": "gogcli (Gmail)",
6
- "version": "2.18.3",
6
+ "version": "2.18.4",
7
7
  "description": "Extended Gmail for Claude via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-gmail",
3
- "version": "2.18.3",
3
+ "version": "2.18.4",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-gmail",
5
5
  "description": "Extended Gmail MCP server via gogcli — auth + full Gmail support (threads, labels, drafts, attachments, forward, autoreply, bulk operations)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -327,7 +327,8 @@ export function registerExtraGmailTools(server: McpServer): void {
327
327
  const args = ['gmail', 'raw', messageId];
328
328
  if (format) args.push(`--format=${format}`);
329
329
  if (pretty) args.push('--pretty');
330
- return runOrDiagnose(args, { account });
330
+ // Verbatim by contract: see the `lossless` note on runOrDiagnose.
331
+ return runOrDiagnose(args, { account, lossless: true });
331
332
  });
332
333
 
333
334
  server.registerTool('gog_gmail_attachment', {
@@ -27,20 +27,20 @@ beforeEach(async () => {
27
27
  describe('gog_gmail_raw', () => {
28
28
  it('calls runOrDiagnose with messageId', async () => {
29
29
  await harness.callTool('gog_gmail_raw', { messageId: 'm1' });
30
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'raw', 'm1'], { account: undefined });
30
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'raw', 'm1'], { account: undefined, lossless: true });
31
31
  });
32
32
 
33
33
  it('passes --format and --pretty when provided', async () => {
34
34
  await harness.callTool('gog_gmail_raw', { messageId: 'm1', format: 'metadata', pretty: true });
35
35
  expect(lib.runOrDiagnose).toHaveBeenCalledWith(
36
36
  ['gmail', 'raw', 'm1', '--format=metadata', '--pretty'],
37
- { account: undefined },
37
+ { account: undefined, lossless: true },
38
38
  );
39
39
  });
40
40
 
41
41
  it('omits --pretty when false', async () => {
42
42
  await harness.callTool('gog_gmail_raw', { messageId: 'm1', pretty: false });
43
- expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'raw', 'm1'], { account: undefined });
43
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['gmail', 'raw', 'm1'], { account: undefined, lossless: true });
44
44
  });
45
45
  });
46
46