gogcli-mcp-contacts 2.18.2 → 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 +196 -3
- package/manifest.json +1 -1
- package/package.json +1 -1
- package/src/tools/contacts-extra.ts +2 -1
- package/tests/tools/contacts-extra.test.ts +3 -3
package/dist/index.js
CHANGED
|
@@ -31275,6 +31275,198 @@ async function run(args, options = {}) {
|
|
|
31275
31275
|
}
|
|
31276
31276
|
}
|
|
31277
31277
|
|
|
31278
|
+
// ../gogcli-mcp/src/timestamps.ts
|
|
31279
|
+
var DEFAULT_DISPLAY_TZ = "America/New_York";
|
|
31280
|
+
function isValidTimeZone(tz) {
|
|
31281
|
+
try {
|
|
31282
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
31283
|
+
return true;
|
|
31284
|
+
} catch {
|
|
31285
|
+
return false;
|
|
31286
|
+
}
|
|
31287
|
+
}
|
|
31288
|
+
function displayTimeZone() {
|
|
31289
|
+
const configured = readEnvVar("DISPLAY_TZ");
|
|
31290
|
+
if (configured && isValidTimeZone(configured)) return configured;
|
|
31291
|
+
return DEFAULT_DISPLAY_TZ;
|
|
31292
|
+
}
|
|
31293
|
+
function naiveSourceTimeZone() {
|
|
31294
|
+
const configured = readEnvVar("GOG_TIMEZONE");
|
|
31295
|
+
if (configured && isValidTimeZone(configured)) return configured;
|
|
31296
|
+
return displayTimeZone();
|
|
31297
|
+
}
|
|
31298
|
+
function offsetAt(instant, tz) {
|
|
31299
|
+
const w = wallPartsIn(instant, tz);
|
|
31300
|
+
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
31301
|
+
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
31302
|
+
const sign = minutes < 0 ? "-" : "+";
|
|
31303
|
+
const abs = Math.abs(minutes);
|
|
31304
|
+
return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
31305
|
+
}
|
|
31306
|
+
function wallPartsIn(instant, tz) {
|
|
31307
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
31308
|
+
timeZone: tz,
|
|
31309
|
+
year: "numeric",
|
|
31310
|
+
month: "2-digit",
|
|
31311
|
+
day: "2-digit",
|
|
31312
|
+
hour: "2-digit",
|
|
31313
|
+
minute: "2-digit",
|
|
31314
|
+
second: "2-digit",
|
|
31315
|
+
// h23 pins midnight to hour 00; without it some ICU builds render hour 24.
|
|
31316
|
+
hourCycle: "h23"
|
|
31317
|
+
}).formatToParts(instant);
|
|
31318
|
+
const out = {};
|
|
31319
|
+
for (const p of parts) {
|
|
31320
|
+
if (p.type !== "literal") out[p.type] = Number(p.value);
|
|
31321
|
+
}
|
|
31322
|
+
return out;
|
|
31323
|
+
}
|
|
31324
|
+
function wallTimeToInstant(y, mo, d, h, mi, s, ms, tz) {
|
|
31325
|
+
let guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
|
|
31326
|
+
for (let i = 0; i < 2; i += 1) {
|
|
31327
|
+
const seen = wallPartsIn(new Date(guess), tz);
|
|
31328
|
+
const seenUTC = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second, ms);
|
|
31329
|
+
const drift = Date.UTC(y, mo - 1, d, h, mi, s, ms) - seenUTC;
|
|
31330
|
+
if (drift === 0) break;
|
|
31331
|
+
guess += drift;
|
|
31332
|
+
}
|
|
31333
|
+
return new Date(guess);
|
|
31334
|
+
}
|
|
31335
|
+
function pad(n, width = 2) {
|
|
31336
|
+
return String(n).padStart(width, "0");
|
|
31337
|
+
}
|
|
31338
|
+
function isoWithOffset(instant, tz, offset) {
|
|
31339
|
+
const w = wallPartsIn(instant, tz);
|
|
31340
|
+
const msPart = instant.getUTCMilliseconds();
|
|
31341
|
+
const frac = msPart ? `.${pad(msPart, 3)}` : "";
|
|
31342
|
+
return `${pad(w.year, 4)}-${pad(w.month)}-${pad(w.day)}T${pad(w.hour)}:${pad(w.minute)}:${pad(w.second)}${frac}${offset}`;
|
|
31343
|
+
}
|
|
31344
|
+
function formatInstant(instant, tz = displayTimeZone()) {
|
|
31345
|
+
const offset = offsetAt(instant, tz);
|
|
31346
|
+
const display = new Intl.DateTimeFormat("en-US", {
|
|
31347
|
+
timeZone: tz,
|
|
31348
|
+
weekday: "short",
|
|
31349
|
+
month: "short",
|
|
31350
|
+
day: "numeric",
|
|
31351
|
+
year: "numeric",
|
|
31352
|
+
hour: "numeric",
|
|
31353
|
+
minute: "2-digit",
|
|
31354
|
+
timeZoneName: "short"
|
|
31355
|
+
}).format(instant);
|
|
31356
|
+
return { iso: isoWithOffset(instant, tz, offset), display };
|
|
31357
|
+
}
|
|
31358
|
+
var TIMESTAMP_KEYS = /* @__PURE__ */ new Set([
|
|
31359
|
+
"date",
|
|
31360
|
+
// gog gmail message/thread listings ("2026-07-28 03:36")
|
|
31361
|
+
"dateTime",
|
|
31362
|
+
// Calendar event start/end
|
|
31363
|
+
"internalDate",
|
|
31364
|
+
// Gmail, epoch milliseconds (authoritative)
|
|
31365
|
+
"modifiedTime",
|
|
31366
|
+
// Drive
|
|
31367
|
+
"createdTime",
|
|
31368
|
+
// Drive
|
|
31369
|
+
"createTime",
|
|
31370
|
+
"updateTime",
|
|
31371
|
+
"updated",
|
|
31372
|
+
"originalStartTime",
|
|
31373
|
+
"expirationTime",
|
|
31374
|
+
"lastModified",
|
|
31375
|
+
"sentAt",
|
|
31376
|
+
"viewedAt",
|
|
31377
|
+
"modifiedAt",
|
|
31378
|
+
"fetchedBodyAt",
|
|
31379
|
+
"asOf"
|
|
31380
|
+
]);
|
|
31381
|
+
var ZONE_NAME_KEYS = /* @__PURE__ */ new Set(["timeZone", "timezone"]);
|
|
31382
|
+
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})$/;
|
|
31383
|
+
var NAIVE_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?$/;
|
|
31384
|
+
var EPOCH_MILLIS = /^\d{13}$/;
|
|
31385
|
+
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
31386
|
+
function isRealCalendarDate(p) {
|
|
31387
|
+
const utc = new Date(Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second));
|
|
31388
|
+
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;
|
|
31389
|
+
}
|
|
31390
|
+
function parseTimestampValue(key, value, assumeNaiveIn) {
|
|
31391
|
+
if (typeof value !== "string") return null;
|
|
31392
|
+
const raw = value.trim();
|
|
31393
|
+
if (raw === "" || DATE_ONLY.test(raw)) return null;
|
|
31394
|
+
if (key === "internalDate" && EPOCH_MILLIS.test(raw)) {
|
|
31395
|
+
return new Date(Number(raw));
|
|
31396
|
+
}
|
|
31397
|
+
if (RFC3339_WITH_OFFSET.test(raw)) {
|
|
31398
|
+
const parsed = new Date(raw.replace(" ", "T"));
|
|
31399
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
31400
|
+
}
|
|
31401
|
+
const naive = NAIVE_DATE_TIME.exec(raw);
|
|
31402
|
+
if (naive) {
|
|
31403
|
+
const [, y, mo, d, h, mi, s, frac] = naive;
|
|
31404
|
+
const ms = frac ? Number(frac.padEnd(3, "0").slice(0, 3)) : 0;
|
|
31405
|
+
const parts = {
|
|
31406
|
+
year: Number(y),
|
|
31407
|
+
month: Number(mo),
|
|
31408
|
+
day: Number(d),
|
|
31409
|
+
hour: Number(h),
|
|
31410
|
+
minute: Number(mi),
|
|
31411
|
+
second: Number(s ?? "0")
|
|
31412
|
+
};
|
|
31413
|
+
if (!isRealCalendarDate(parts)) return null;
|
|
31414
|
+
return wallTimeToInstant(
|
|
31415
|
+
parts.year,
|
|
31416
|
+
parts.month,
|
|
31417
|
+
parts.day,
|
|
31418
|
+
parts.hour,
|
|
31419
|
+
parts.minute,
|
|
31420
|
+
parts.second,
|
|
31421
|
+
ms,
|
|
31422
|
+
assumeNaiveIn
|
|
31423
|
+
);
|
|
31424
|
+
}
|
|
31425
|
+
return null;
|
|
31426
|
+
}
|
|
31427
|
+
function walk(node, tz, naiveTz) {
|
|
31428
|
+
let changed = false;
|
|
31429
|
+
if (Array.isArray(node)) {
|
|
31430
|
+
for (const item of node) {
|
|
31431
|
+
if (walk(item, tz, naiveTz)) changed = true;
|
|
31432
|
+
}
|
|
31433
|
+
return changed;
|
|
31434
|
+
}
|
|
31435
|
+
if (node === null || typeof node !== "object") return false;
|
|
31436
|
+
const obj = node;
|
|
31437
|
+
for (const key of Object.keys(obj)) {
|
|
31438
|
+
const value = obj[key];
|
|
31439
|
+
if (value !== null && typeof value === "object") {
|
|
31440
|
+
if (walk(value, tz, naiveTz)) changed = true;
|
|
31441
|
+
continue;
|
|
31442
|
+
}
|
|
31443
|
+
if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
|
|
31444
|
+
const instant = parseTimestampValue(key, value, naiveTz);
|
|
31445
|
+
if (!instant) continue;
|
|
31446
|
+
const { iso, display } = formatInstant(instant, tz);
|
|
31447
|
+
obj[key] = iso;
|
|
31448
|
+
obj[`${key}Display`] = display;
|
|
31449
|
+
changed = true;
|
|
31450
|
+
}
|
|
31451
|
+
return changed;
|
|
31452
|
+
}
|
|
31453
|
+
function detectIndent(text) {
|
|
31454
|
+
const match = /\n(\s+)\S/.exec(text);
|
|
31455
|
+
return match ? match[1].replace(/\t/g, " ").length : 0;
|
|
31456
|
+
}
|
|
31457
|
+
function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSourceTimeZone()) {
|
|
31458
|
+
const trimmed = text.trim();
|
|
31459
|
+
if (trimmed === "" || !/^[[{]/.test(trimmed)) return text;
|
|
31460
|
+
let parsed;
|
|
31461
|
+
try {
|
|
31462
|
+
parsed = JSON.parse(trimmed);
|
|
31463
|
+
} catch {
|
|
31464
|
+
return text;
|
|
31465
|
+
}
|
|
31466
|
+
if (!walk(parsed, tz, naiveTz)) return text;
|
|
31467
|
+
return JSON.stringify(parsed, null, detectIndent(text));
|
|
31468
|
+
}
|
|
31469
|
+
|
|
31278
31470
|
// ../gogcli-mcp/src/tools/utils.ts
|
|
31279
31471
|
var accountParam = external_exports.string().optional().describe(
|
|
31280
31472
|
"Google account email to use, e.g. you@gmail.com \u2014 must be the full address, not a bare username. Overrides the GOG_ACCOUNT env var. Omit to use the single configured account."
|
|
@@ -31368,7 +31560,8 @@ ${accounts || "(none)"}${hint}`);
|
|
|
31368
31560
|
}
|
|
31369
31561
|
async function runOrDiagnose(args, options) {
|
|
31370
31562
|
try {
|
|
31371
|
-
|
|
31563
|
+
const raw = await run(args, options);
|
|
31564
|
+
return rawTextResult(options.lossless ? raw : normalizeTimestamps(raw));
|
|
31372
31565
|
} catch (err) {
|
|
31373
31566
|
return diagnose(err);
|
|
31374
31567
|
}
|
|
@@ -31604,7 +31797,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
|
|
|
31604
31797
|
);
|
|
31605
31798
|
|
|
31606
31799
|
// ../gogcli-mcp/src/server.ts
|
|
31607
|
-
var VERSION = true ? "2.18.
|
|
31800
|
+
var VERSION = true ? "2.18.4" : "0.0.0";
|
|
31608
31801
|
|
|
31609
31802
|
// src/tools/contacts-extra.ts
|
|
31610
31803
|
function registerExtraContactsTools(server) {
|
|
@@ -31801,7 +31994,7 @@ function registerExtraContactsTools(server) {
|
|
|
31801
31994
|
const args = ["people", "raw", userId];
|
|
31802
31995
|
if (personFields) args.push(`--person-fields=${personFields}`);
|
|
31803
31996
|
if (pretty) args.push("--pretty");
|
|
31804
|
-
return runOrDiagnose(args, { account });
|
|
31997
|
+
return runOrDiagnose(args, { account, lossless: true });
|
|
31805
31998
|
});
|
|
31806
31999
|
}
|
|
31807
32000
|
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-contacts",
|
|
5
5
|
"display_name": "gogcli (Contacts)",
|
|
6
|
-
"version": "2.18.
|
|
6
|
+
"version": "2.18.4",
|
|
7
7
|
"description": "Extended Google Contacts for Claude via gogcli — auth + Contacts + Workspace directory (People API)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-contacts",
|
|
3
|
-
"version": "2.18.
|
|
3
|
+
"version": "2.18.4",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-contacts",
|
|
5
5
|
"description": "Extended Google Contacts + People MCP server via gogcli — auth + Contacts + Workspace directory (People API)",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -209,6 +209,7 @@ export function registerExtraContactsTools(server: McpServer): void {
|
|
|
209
209
|
const args = ['people', 'raw', userId];
|
|
210
210
|
if (personFields) args.push(`--person-fields=${personFields}`);
|
|
211
211
|
if (pretty) args.push('--pretty');
|
|
212
|
-
|
|
212
|
+
// Verbatim by contract: see the `lossless` note on runOrDiagnose.
|
|
213
|
+
return runOrDiagnose(args, { account, lossless: true });
|
|
213
214
|
});
|
|
214
215
|
}
|
|
@@ -236,7 +236,7 @@ describe('gog_contacts_other_search', () => {
|
|
|
236
236
|
describe('gog_people_raw', () => {
|
|
237
237
|
it('calls runOrDiagnose with userId', async () => {
|
|
238
238
|
await harness.callTool('gog_people_raw', { userId: 'people/c123' });
|
|
239
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'raw', 'people/c123'], { account: undefined });
|
|
239
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'raw', 'people/c123'], { account: undefined, lossless: true });
|
|
240
240
|
});
|
|
241
241
|
|
|
242
242
|
it('passes --person-fields and --pretty when provided', async () => {
|
|
@@ -247,12 +247,12 @@ describe('gog_people_raw', () => {
|
|
|
247
247
|
});
|
|
248
248
|
expect(lib.runOrDiagnose).toHaveBeenCalledWith(
|
|
249
249
|
['people', 'raw', 'people/c123', '--person-fields=names,emailAddresses', '--pretty'],
|
|
250
|
-
{ account: undefined },
|
|
250
|
+
{ account: undefined, lossless: true },
|
|
251
251
|
);
|
|
252
252
|
});
|
|
253
253
|
|
|
254
254
|
it('omits --pretty when false', async () => {
|
|
255
255
|
await harness.callTool('gog_people_raw', { userId: 'people/c123', pretty: false });
|
|
256
|
-
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'raw', 'people/c123'], { account: undefined });
|
|
256
|
+
expect(lib.runOrDiagnose).toHaveBeenCalledWith(['people', 'raw', 'people/c123'], { account: undefined, lossless: true });
|
|
257
257
|
});
|
|
258
258
|
});
|