gogcli-mcp 4.0.0 → 4.0.1
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +203 -201
- package/dist/lib.js +203 -201
- package/manifest.json +1 -1
- package/mint.yaml +4 -4
- package/package.json +1 -1
- package/server.json +2 -2
- package/src/runner.ts +4 -1
- package/tests/runner.test.ts +45 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
10
|
-
"version": "4.0.
|
|
10
|
+
"version": "4.0.1"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
18
|
-
"version": "4.0.
|
|
18
|
+
"version": "4.0.1",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
package/dist/index.js
CHANGED
|
@@ -33989,6 +33989,207 @@ var pageSchema = {
|
|
|
33989
33989
|
|
|
33990
33990
|
// src/runner.ts
|
|
33991
33991
|
import { delimiter, join } from "node:path";
|
|
33992
|
+
|
|
33993
|
+
// src/timestamps.ts
|
|
33994
|
+
var DEFAULT_DISPLAY_TZ = "America/New_York";
|
|
33995
|
+
function isValidTimeZone(tz) {
|
|
33996
|
+
try {
|
|
33997
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
33998
|
+
return true;
|
|
33999
|
+
} catch {
|
|
34000
|
+
return false;
|
|
34001
|
+
}
|
|
34002
|
+
}
|
|
34003
|
+
function displayTimeZone() {
|
|
34004
|
+
const configured = readEnvVar("DISPLAY_TZ");
|
|
34005
|
+
if (configured && isValidTimeZone(configured)) return configured;
|
|
34006
|
+
return DEFAULT_DISPLAY_TZ;
|
|
34007
|
+
}
|
|
34008
|
+
function naiveSourceTimeZone() {
|
|
34009
|
+
const configured = readEnvVar("GOG_TIMEZONE");
|
|
34010
|
+
if (configured && isValidTimeZone(configured)) return configured;
|
|
34011
|
+
return displayTimeZone();
|
|
34012
|
+
}
|
|
34013
|
+
function offsetAt(instant, tz) {
|
|
34014
|
+
const w = wallPartsIn(instant, tz);
|
|
34015
|
+
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
34016
|
+
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
34017
|
+
const sign = minutes < 0 ? "-" : "+";
|
|
34018
|
+
const abs = Math.abs(minutes);
|
|
34019
|
+
return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
34020
|
+
}
|
|
34021
|
+
function wallPartsIn(instant, tz) {
|
|
34022
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
34023
|
+
timeZone: tz,
|
|
34024
|
+
year: "numeric",
|
|
34025
|
+
month: "2-digit",
|
|
34026
|
+
day: "2-digit",
|
|
34027
|
+
hour: "2-digit",
|
|
34028
|
+
minute: "2-digit",
|
|
34029
|
+
second: "2-digit",
|
|
34030
|
+
// h23 pins midnight to hour 00; without it some ICU builds render hour 24.
|
|
34031
|
+
hourCycle: "h23"
|
|
34032
|
+
}).formatToParts(instant);
|
|
34033
|
+
const out = {};
|
|
34034
|
+
for (const p of parts) {
|
|
34035
|
+
if (p.type !== "literal") out[p.type] = Number(p.value);
|
|
34036
|
+
}
|
|
34037
|
+
return out;
|
|
34038
|
+
}
|
|
34039
|
+
function wallTimeToInstant(y, mo, d, h, mi, s, ms, tz) {
|
|
34040
|
+
let guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
|
|
34041
|
+
for (let i = 0; i < 2; i += 1) {
|
|
34042
|
+
const seen = wallPartsIn(new Date(guess), tz);
|
|
34043
|
+
const seenUTC = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second, ms);
|
|
34044
|
+
const drift = Date.UTC(y, mo - 1, d, h, mi, s, ms) - seenUTC;
|
|
34045
|
+
if (drift === 0) break;
|
|
34046
|
+
guess += drift;
|
|
34047
|
+
}
|
|
34048
|
+
return new Date(guess);
|
|
34049
|
+
}
|
|
34050
|
+
function pad(n, width = 2) {
|
|
34051
|
+
return String(n).padStart(width, "0");
|
|
34052
|
+
}
|
|
34053
|
+
function isoWithOffset(instant, tz, offset) {
|
|
34054
|
+
const w = wallPartsIn(instant, tz);
|
|
34055
|
+
const msPart = instant.getUTCMilliseconds();
|
|
34056
|
+
const frac = msPart ? `.${pad(msPart, 3)}` : "";
|
|
34057
|
+
return `${pad(w.year, 4)}-${pad(w.month)}-${pad(w.day)}T${pad(w.hour)}:${pad(w.minute)}:${pad(w.second)}${frac}${offset}`;
|
|
34058
|
+
}
|
|
34059
|
+
function formatInstant(instant, tz = displayTimeZone()) {
|
|
34060
|
+
const offset = offsetAt(instant, tz);
|
|
34061
|
+
const display = new Intl.DateTimeFormat("en-US", {
|
|
34062
|
+
timeZone: tz,
|
|
34063
|
+
weekday: "short",
|
|
34064
|
+
month: "short",
|
|
34065
|
+
day: "numeric",
|
|
34066
|
+
year: "numeric",
|
|
34067
|
+
hour: "numeric",
|
|
34068
|
+
minute: "2-digit",
|
|
34069
|
+
timeZoneName: "short"
|
|
34070
|
+
}).format(instant);
|
|
34071
|
+
return { iso: isoWithOffset(instant, tz, offset), display };
|
|
34072
|
+
}
|
|
34073
|
+
var TIMESTAMP_KEYS = /* @__PURE__ */ new Set([
|
|
34074
|
+
"date",
|
|
34075
|
+
// gog gmail message/thread listings ("2026-07-28 03:36")
|
|
34076
|
+
"dateTime",
|
|
34077
|
+
// Calendar event start/end
|
|
34078
|
+
"internalDate",
|
|
34079
|
+
// Gmail, epoch milliseconds (authoritative)
|
|
34080
|
+
// gog >= 0.35.0 Gmail message AND thread listings. Already offset-bearing
|
|
34081
|
+
// (RFC3339 from internalDate), so it needs no offset repair — it is
|
|
34082
|
+
// allowlisted purely to gain a Display sibling, and to be re-rendered in
|
|
34083
|
+
// DISPLAY_TZ like every other instant. Separately sourced from the sibling
|
|
34084
|
+
// `date`, which is a naive re-format of the sender's Date header; the two may
|
|
34085
|
+
// legitimately disagree. See docs/timestamps.md.
|
|
34086
|
+
"internalDateIso",
|
|
34087
|
+
"modifiedTime",
|
|
34088
|
+
// Drive
|
|
34089
|
+
"createdTime",
|
|
34090
|
+
// Drive
|
|
34091
|
+
"createTime",
|
|
34092
|
+
"updateTime",
|
|
34093
|
+
"updated",
|
|
34094
|
+
"originalStartTime",
|
|
34095
|
+
"expirationTime",
|
|
34096
|
+
"lastModified",
|
|
34097
|
+
"sentAt",
|
|
34098
|
+
"viewedAt",
|
|
34099
|
+
"modifiedAt",
|
|
34100
|
+
"fetchedBodyAt",
|
|
34101
|
+
"asOf"
|
|
34102
|
+
]);
|
|
34103
|
+
var ZONE_NAME_KEYS = /* @__PURE__ */ new Set(["timeZone", "timezone"]);
|
|
34104
|
+
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})$/;
|
|
34105
|
+
var NAIVE_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?$/;
|
|
34106
|
+
var EPOCH_MILLIS = /^\d{13}$/;
|
|
34107
|
+
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
34108
|
+
function isRealCalendarDate(p) {
|
|
34109
|
+
const utc = new Date(Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second));
|
|
34110
|
+
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;
|
|
34111
|
+
}
|
|
34112
|
+
function parseTimestampValue(key, value, assumeNaiveIn) {
|
|
34113
|
+
if (typeof value !== "string") return null;
|
|
34114
|
+
const raw = value.trim();
|
|
34115
|
+
if (raw === "" || DATE_ONLY.test(raw)) return null;
|
|
34116
|
+
if (key === "internalDate" && EPOCH_MILLIS.test(raw)) {
|
|
34117
|
+
return new Date(Number(raw));
|
|
34118
|
+
}
|
|
34119
|
+
if (RFC3339_WITH_OFFSET.test(raw)) {
|
|
34120
|
+
const parsed = new Date(raw.replace(" ", "T"));
|
|
34121
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
34122
|
+
}
|
|
34123
|
+
const naive = NAIVE_DATE_TIME.exec(raw);
|
|
34124
|
+
if (naive) {
|
|
34125
|
+
const [, y, mo, d, h, mi, s, frac] = naive;
|
|
34126
|
+
const ms = frac ? Number(frac.padEnd(3, "0").slice(0, 3)) : 0;
|
|
34127
|
+
const parts = {
|
|
34128
|
+
year: Number(y),
|
|
34129
|
+
month: Number(mo),
|
|
34130
|
+
day: Number(d),
|
|
34131
|
+
hour: Number(h),
|
|
34132
|
+
minute: Number(mi),
|
|
34133
|
+
second: Number(s ?? "0")
|
|
34134
|
+
};
|
|
34135
|
+
if (!isRealCalendarDate(parts)) return null;
|
|
34136
|
+
return wallTimeToInstant(
|
|
34137
|
+
parts.year,
|
|
34138
|
+
parts.month,
|
|
34139
|
+
parts.day,
|
|
34140
|
+
parts.hour,
|
|
34141
|
+
parts.minute,
|
|
34142
|
+
parts.second,
|
|
34143
|
+
ms,
|
|
34144
|
+
assumeNaiveIn
|
|
34145
|
+
);
|
|
34146
|
+
}
|
|
34147
|
+
return null;
|
|
34148
|
+
}
|
|
34149
|
+
function walk2(node2, tz, naiveTz) {
|
|
34150
|
+
let changed = false;
|
|
34151
|
+
if (Array.isArray(node2)) {
|
|
34152
|
+
for (const item of node2) {
|
|
34153
|
+
if (walk2(item, tz, naiveTz)) changed = true;
|
|
34154
|
+
}
|
|
34155
|
+
return changed;
|
|
34156
|
+
}
|
|
34157
|
+
if (node2 === null || typeof node2 !== "object") return false;
|
|
34158
|
+
const obj = node2;
|
|
34159
|
+
for (const key of Object.keys(obj)) {
|
|
34160
|
+
const value = obj[key];
|
|
34161
|
+
if (value !== null && typeof value === "object") {
|
|
34162
|
+
if (walk2(value, tz, naiveTz)) changed = true;
|
|
34163
|
+
continue;
|
|
34164
|
+
}
|
|
34165
|
+
if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
|
|
34166
|
+
const instant = parseTimestampValue(key, value, naiveTz);
|
|
34167
|
+
if (!instant) continue;
|
|
34168
|
+
const { iso, display } = formatInstant(instant, tz);
|
|
34169
|
+
obj[key] = iso;
|
|
34170
|
+
obj[`${key}Display`] = display;
|
|
34171
|
+
changed = true;
|
|
34172
|
+
}
|
|
34173
|
+
return changed;
|
|
34174
|
+
}
|
|
34175
|
+
function detectIndent(text) {
|
|
34176
|
+
const match = /\n(\s+)\S/.exec(text);
|
|
34177
|
+
return match ? match[1].replace(/\t/g, " ").length : 0;
|
|
34178
|
+
}
|
|
34179
|
+
function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSourceTimeZone()) {
|
|
34180
|
+
const trimmed = text.trim();
|
|
34181
|
+
if (trimmed === "" || !/^[[{]/.test(trimmed)) return text;
|
|
34182
|
+
let parsed;
|
|
34183
|
+
try {
|
|
34184
|
+
parsed = JSON.parse(trimmed);
|
|
34185
|
+
} catch {
|
|
34186
|
+
return text;
|
|
34187
|
+
}
|
|
34188
|
+
if (!walk2(parsed, tz, naiveTz)) return text;
|
|
34189
|
+
return JSON.stringify(parsed, null, detectIndent(text));
|
|
34190
|
+
}
|
|
34191
|
+
|
|
34192
|
+
// src/runner.ts
|
|
33992
34193
|
function isGogFileArg(arg) {
|
|
33993
34194
|
return typeof arg !== "string";
|
|
33994
34195
|
}
|
|
@@ -34109,7 +34310,7 @@ async function spawnGog(fullArgs, opts) {
|
|
|
34109
34310
|
const spawn = spawner ?? (await import("node:child_process")).spawn;
|
|
34110
34311
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
34111
34312
|
return new Promise((resolve, reject) => {
|
|
34112
|
-
const childEnv = { ...sanitizedEnv(), PATH: augmentedPath() };
|
|
34313
|
+
const childEnv = { ...sanitizedEnv(), GOG_TIMEZONE: naiveSourceTimeZone(), PATH: augmentedPath() };
|
|
34113
34314
|
const child = spawn(readEnvVar("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
|
|
34114
34315
|
const stdoutChunks = [];
|
|
34115
34316
|
const stderrChunks = [];
|
|
@@ -34191,205 +34392,6 @@ async function runBinary(args, options = {}) {
|
|
|
34191
34392
|
return spawnExecutor(fullArgs, { timeout, interactive: false, spawner, binary: true });
|
|
34192
34393
|
}
|
|
34193
34394
|
|
|
34194
|
-
// src/timestamps.ts
|
|
34195
|
-
var DEFAULT_DISPLAY_TZ = "America/New_York";
|
|
34196
|
-
function isValidTimeZone(tz) {
|
|
34197
|
-
try {
|
|
34198
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
34199
|
-
return true;
|
|
34200
|
-
} catch {
|
|
34201
|
-
return false;
|
|
34202
|
-
}
|
|
34203
|
-
}
|
|
34204
|
-
function displayTimeZone() {
|
|
34205
|
-
const configured = readEnvVar("DISPLAY_TZ");
|
|
34206
|
-
if (configured && isValidTimeZone(configured)) return configured;
|
|
34207
|
-
return DEFAULT_DISPLAY_TZ;
|
|
34208
|
-
}
|
|
34209
|
-
function naiveSourceTimeZone() {
|
|
34210
|
-
const configured = readEnvVar("GOG_TIMEZONE");
|
|
34211
|
-
if (configured && isValidTimeZone(configured)) return configured;
|
|
34212
|
-
return displayTimeZone();
|
|
34213
|
-
}
|
|
34214
|
-
function offsetAt(instant, tz) {
|
|
34215
|
-
const w = wallPartsIn(instant, tz);
|
|
34216
|
-
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
34217
|
-
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
34218
|
-
const sign = minutes < 0 ? "-" : "+";
|
|
34219
|
-
const abs = Math.abs(minutes);
|
|
34220
|
-
return `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
34221
|
-
}
|
|
34222
|
-
function wallPartsIn(instant, tz) {
|
|
34223
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
34224
|
-
timeZone: tz,
|
|
34225
|
-
year: "numeric",
|
|
34226
|
-
month: "2-digit",
|
|
34227
|
-
day: "2-digit",
|
|
34228
|
-
hour: "2-digit",
|
|
34229
|
-
minute: "2-digit",
|
|
34230
|
-
second: "2-digit",
|
|
34231
|
-
// h23 pins midnight to hour 00; without it some ICU builds render hour 24.
|
|
34232
|
-
hourCycle: "h23"
|
|
34233
|
-
}).formatToParts(instant);
|
|
34234
|
-
const out = {};
|
|
34235
|
-
for (const p of parts) {
|
|
34236
|
-
if (p.type !== "literal") out[p.type] = Number(p.value);
|
|
34237
|
-
}
|
|
34238
|
-
return out;
|
|
34239
|
-
}
|
|
34240
|
-
function wallTimeToInstant(y, mo, d, h, mi, s, ms, tz) {
|
|
34241
|
-
let guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
|
|
34242
|
-
for (let i = 0; i < 2; i += 1) {
|
|
34243
|
-
const seen = wallPartsIn(new Date(guess), tz);
|
|
34244
|
-
const seenUTC = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second, ms);
|
|
34245
|
-
const drift = Date.UTC(y, mo - 1, d, h, mi, s, ms) - seenUTC;
|
|
34246
|
-
if (drift === 0) break;
|
|
34247
|
-
guess += drift;
|
|
34248
|
-
}
|
|
34249
|
-
return new Date(guess);
|
|
34250
|
-
}
|
|
34251
|
-
function pad(n, width = 2) {
|
|
34252
|
-
return String(n).padStart(width, "0");
|
|
34253
|
-
}
|
|
34254
|
-
function isoWithOffset(instant, tz, offset) {
|
|
34255
|
-
const w = wallPartsIn(instant, tz);
|
|
34256
|
-
const msPart = instant.getUTCMilliseconds();
|
|
34257
|
-
const frac = msPart ? `.${pad(msPart, 3)}` : "";
|
|
34258
|
-
return `${pad(w.year, 4)}-${pad(w.month)}-${pad(w.day)}T${pad(w.hour)}:${pad(w.minute)}:${pad(w.second)}${frac}${offset}`;
|
|
34259
|
-
}
|
|
34260
|
-
function formatInstant(instant, tz = displayTimeZone()) {
|
|
34261
|
-
const offset = offsetAt(instant, tz);
|
|
34262
|
-
const display = new Intl.DateTimeFormat("en-US", {
|
|
34263
|
-
timeZone: tz,
|
|
34264
|
-
weekday: "short",
|
|
34265
|
-
month: "short",
|
|
34266
|
-
day: "numeric",
|
|
34267
|
-
year: "numeric",
|
|
34268
|
-
hour: "numeric",
|
|
34269
|
-
minute: "2-digit",
|
|
34270
|
-
timeZoneName: "short"
|
|
34271
|
-
}).format(instant);
|
|
34272
|
-
return { iso: isoWithOffset(instant, tz, offset), display };
|
|
34273
|
-
}
|
|
34274
|
-
var TIMESTAMP_KEYS = /* @__PURE__ */ new Set([
|
|
34275
|
-
"date",
|
|
34276
|
-
// gog gmail message/thread listings ("2026-07-28 03:36")
|
|
34277
|
-
"dateTime",
|
|
34278
|
-
// Calendar event start/end
|
|
34279
|
-
"internalDate",
|
|
34280
|
-
// Gmail, epoch milliseconds (authoritative)
|
|
34281
|
-
// gog >= 0.35.0 Gmail message AND thread listings. Already offset-bearing
|
|
34282
|
-
// (RFC3339 from internalDate), so it needs no offset repair — it is
|
|
34283
|
-
// allowlisted purely to gain a Display sibling, and to be re-rendered in
|
|
34284
|
-
// DISPLAY_TZ like every other instant. Separately sourced from the sibling
|
|
34285
|
-
// `date`, which is a naive re-format of the sender's Date header; the two may
|
|
34286
|
-
// legitimately disagree. See docs/timestamps.md.
|
|
34287
|
-
"internalDateIso",
|
|
34288
|
-
"modifiedTime",
|
|
34289
|
-
// Drive
|
|
34290
|
-
"createdTime",
|
|
34291
|
-
// Drive
|
|
34292
|
-
"createTime",
|
|
34293
|
-
"updateTime",
|
|
34294
|
-
"updated",
|
|
34295
|
-
"originalStartTime",
|
|
34296
|
-
"expirationTime",
|
|
34297
|
-
"lastModified",
|
|
34298
|
-
"sentAt",
|
|
34299
|
-
"viewedAt",
|
|
34300
|
-
"modifiedAt",
|
|
34301
|
-
"fetchedBodyAt",
|
|
34302
|
-
"asOf"
|
|
34303
|
-
]);
|
|
34304
|
-
var ZONE_NAME_KEYS = /* @__PURE__ */ new Set(["timeZone", "timezone"]);
|
|
34305
|
-
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})$/;
|
|
34306
|
-
var NAIVE_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?$/;
|
|
34307
|
-
var EPOCH_MILLIS = /^\d{13}$/;
|
|
34308
|
-
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
34309
|
-
function isRealCalendarDate(p) {
|
|
34310
|
-
const utc = new Date(Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second));
|
|
34311
|
-
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;
|
|
34312
|
-
}
|
|
34313
|
-
function parseTimestampValue(key, value, assumeNaiveIn) {
|
|
34314
|
-
if (typeof value !== "string") return null;
|
|
34315
|
-
const raw = value.trim();
|
|
34316
|
-
if (raw === "" || DATE_ONLY.test(raw)) return null;
|
|
34317
|
-
if (key === "internalDate" && EPOCH_MILLIS.test(raw)) {
|
|
34318
|
-
return new Date(Number(raw));
|
|
34319
|
-
}
|
|
34320
|
-
if (RFC3339_WITH_OFFSET.test(raw)) {
|
|
34321
|
-
const parsed = new Date(raw.replace(" ", "T"));
|
|
34322
|
-
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
34323
|
-
}
|
|
34324
|
-
const naive = NAIVE_DATE_TIME.exec(raw);
|
|
34325
|
-
if (naive) {
|
|
34326
|
-
const [, y, mo, d, h, mi, s, frac] = naive;
|
|
34327
|
-
const ms = frac ? Number(frac.padEnd(3, "0").slice(0, 3)) : 0;
|
|
34328
|
-
const parts = {
|
|
34329
|
-
year: Number(y),
|
|
34330
|
-
month: Number(mo),
|
|
34331
|
-
day: Number(d),
|
|
34332
|
-
hour: Number(h),
|
|
34333
|
-
minute: Number(mi),
|
|
34334
|
-
second: Number(s ?? "0")
|
|
34335
|
-
};
|
|
34336
|
-
if (!isRealCalendarDate(parts)) return null;
|
|
34337
|
-
return wallTimeToInstant(
|
|
34338
|
-
parts.year,
|
|
34339
|
-
parts.month,
|
|
34340
|
-
parts.day,
|
|
34341
|
-
parts.hour,
|
|
34342
|
-
parts.minute,
|
|
34343
|
-
parts.second,
|
|
34344
|
-
ms,
|
|
34345
|
-
assumeNaiveIn
|
|
34346
|
-
);
|
|
34347
|
-
}
|
|
34348
|
-
return null;
|
|
34349
|
-
}
|
|
34350
|
-
function walk2(node2, tz, naiveTz) {
|
|
34351
|
-
let changed = false;
|
|
34352
|
-
if (Array.isArray(node2)) {
|
|
34353
|
-
for (const item of node2) {
|
|
34354
|
-
if (walk2(item, tz, naiveTz)) changed = true;
|
|
34355
|
-
}
|
|
34356
|
-
return changed;
|
|
34357
|
-
}
|
|
34358
|
-
if (node2 === null || typeof node2 !== "object") return false;
|
|
34359
|
-
const obj = node2;
|
|
34360
|
-
for (const key of Object.keys(obj)) {
|
|
34361
|
-
const value = obj[key];
|
|
34362
|
-
if (value !== null && typeof value === "object") {
|
|
34363
|
-
if (walk2(value, tz, naiveTz)) changed = true;
|
|
34364
|
-
continue;
|
|
34365
|
-
}
|
|
34366
|
-
if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
|
|
34367
|
-
const instant = parseTimestampValue(key, value, naiveTz);
|
|
34368
|
-
if (!instant) continue;
|
|
34369
|
-
const { iso, display } = formatInstant(instant, tz);
|
|
34370
|
-
obj[key] = iso;
|
|
34371
|
-
obj[`${key}Display`] = display;
|
|
34372
|
-
changed = true;
|
|
34373
|
-
}
|
|
34374
|
-
return changed;
|
|
34375
|
-
}
|
|
34376
|
-
function detectIndent(text) {
|
|
34377
|
-
const match = /\n(\s+)\S/.exec(text);
|
|
34378
|
-
return match ? match[1].replace(/\t/g, " ").length : 0;
|
|
34379
|
-
}
|
|
34380
|
-
function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSourceTimeZone()) {
|
|
34381
|
-
const trimmed = text.trim();
|
|
34382
|
-
if (trimmed === "" || !/^[[{]/.test(trimmed)) return text;
|
|
34383
|
-
let parsed;
|
|
34384
|
-
try {
|
|
34385
|
-
parsed = JSON.parse(trimmed);
|
|
34386
|
-
} catch {
|
|
34387
|
-
return text;
|
|
34388
|
-
}
|
|
34389
|
-
if (!walk2(parsed, tz, naiveTz)) return text;
|
|
34390
|
-
return JSON.stringify(parsed, null, detectIndent(text));
|
|
34391
|
-
}
|
|
34392
|
-
|
|
34393
34395
|
// src/pagination.ts
|
|
34394
34396
|
function detectIndent2(text) {
|
|
34395
34397
|
const match = /\n(\s+)\S/.exec(text);
|
|
@@ -36863,7 +36865,7 @@ function registerTasksTools(server) {
|
|
|
36863
36865
|
}
|
|
36864
36866
|
|
|
36865
36867
|
// src/server.ts
|
|
36866
|
-
var VERSION = true ? "4.0.
|
|
36868
|
+
var VERSION = true ? "4.0.1" : "0.0.0";
|
|
36867
36869
|
var BASE_TOOL_REGISTRARS = [
|
|
36868
36870
|
registerApiTools,
|
|
36869
36871
|
registerAppScriptTools,
|
package/dist/lib.js
CHANGED
|
@@ -33037,6 +33037,207 @@ var pageSchema = {
|
|
|
33037
33037
|
|
|
33038
33038
|
// src/runner.ts
|
|
33039
33039
|
import { delimiter, join } from "node:path";
|
|
33040
|
+
|
|
33041
|
+
// src/timestamps.ts
|
|
33042
|
+
var DEFAULT_DISPLAY_TZ = "America/New_York";
|
|
33043
|
+
function isValidTimeZone(tz) {
|
|
33044
|
+
try {
|
|
33045
|
+
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
33046
|
+
return true;
|
|
33047
|
+
} catch {
|
|
33048
|
+
return false;
|
|
33049
|
+
}
|
|
33050
|
+
}
|
|
33051
|
+
function displayTimeZone() {
|
|
33052
|
+
const configured = readEnvVar("DISPLAY_TZ");
|
|
33053
|
+
if (configured && isValidTimeZone(configured)) return configured;
|
|
33054
|
+
return DEFAULT_DISPLAY_TZ;
|
|
33055
|
+
}
|
|
33056
|
+
function naiveSourceTimeZone() {
|
|
33057
|
+
const configured = readEnvVar("GOG_TIMEZONE");
|
|
33058
|
+
if (configured && isValidTimeZone(configured)) return configured;
|
|
33059
|
+
return displayTimeZone();
|
|
33060
|
+
}
|
|
33061
|
+
function offsetAt(instant, tz) {
|
|
33062
|
+
const w = wallPartsIn(instant, tz);
|
|
33063
|
+
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
33064
|
+
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
33065
|
+
const sign2 = minutes < 0 ? "-" : "+";
|
|
33066
|
+
const abs = Math.abs(minutes);
|
|
33067
|
+
return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
33068
|
+
}
|
|
33069
|
+
function wallPartsIn(instant, tz) {
|
|
33070
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
33071
|
+
timeZone: tz,
|
|
33072
|
+
year: "numeric",
|
|
33073
|
+
month: "2-digit",
|
|
33074
|
+
day: "2-digit",
|
|
33075
|
+
hour: "2-digit",
|
|
33076
|
+
minute: "2-digit",
|
|
33077
|
+
second: "2-digit",
|
|
33078
|
+
// h23 pins midnight to hour 00; without it some ICU builds render hour 24.
|
|
33079
|
+
hourCycle: "h23"
|
|
33080
|
+
}).formatToParts(instant);
|
|
33081
|
+
const out = {};
|
|
33082
|
+
for (const p of parts) {
|
|
33083
|
+
if (p.type !== "literal") out[p.type] = Number(p.value);
|
|
33084
|
+
}
|
|
33085
|
+
return out;
|
|
33086
|
+
}
|
|
33087
|
+
function wallTimeToInstant(y, mo, d, h, mi, s, ms, tz) {
|
|
33088
|
+
let guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
|
|
33089
|
+
for (let i = 0; i < 2; i += 1) {
|
|
33090
|
+
const seen = wallPartsIn(new Date(guess), tz);
|
|
33091
|
+
const seenUTC = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second, ms);
|
|
33092
|
+
const drift = Date.UTC(y, mo - 1, d, h, mi, s, ms) - seenUTC;
|
|
33093
|
+
if (drift === 0) break;
|
|
33094
|
+
guess += drift;
|
|
33095
|
+
}
|
|
33096
|
+
return new Date(guess);
|
|
33097
|
+
}
|
|
33098
|
+
function pad(n, width = 2) {
|
|
33099
|
+
return String(n).padStart(width, "0");
|
|
33100
|
+
}
|
|
33101
|
+
function isoWithOffset(instant, tz, offset) {
|
|
33102
|
+
const w = wallPartsIn(instant, tz);
|
|
33103
|
+
const msPart = instant.getUTCMilliseconds();
|
|
33104
|
+
const frac = msPart ? `.${pad(msPart, 3)}` : "";
|
|
33105
|
+
return `${pad(w.year, 4)}-${pad(w.month)}-${pad(w.day)}T${pad(w.hour)}:${pad(w.minute)}:${pad(w.second)}${frac}${offset}`;
|
|
33106
|
+
}
|
|
33107
|
+
function formatInstant(instant, tz = displayTimeZone()) {
|
|
33108
|
+
const offset = offsetAt(instant, tz);
|
|
33109
|
+
const display = new Intl.DateTimeFormat("en-US", {
|
|
33110
|
+
timeZone: tz,
|
|
33111
|
+
weekday: "short",
|
|
33112
|
+
month: "short",
|
|
33113
|
+
day: "numeric",
|
|
33114
|
+
year: "numeric",
|
|
33115
|
+
hour: "numeric",
|
|
33116
|
+
minute: "2-digit",
|
|
33117
|
+
timeZoneName: "short"
|
|
33118
|
+
}).format(instant);
|
|
33119
|
+
return { iso: isoWithOffset(instant, tz, offset), display };
|
|
33120
|
+
}
|
|
33121
|
+
var TIMESTAMP_KEYS = /* @__PURE__ */ new Set([
|
|
33122
|
+
"date",
|
|
33123
|
+
// gog gmail message/thread listings ("2026-07-28 03:36")
|
|
33124
|
+
"dateTime",
|
|
33125
|
+
// Calendar event start/end
|
|
33126
|
+
"internalDate",
|
|
33127
|
+
// Gmail, epoch milliseconds (authoritative)
|
|
33128
|
+
// gog >= 0.35.0 Gmail message AND thread listings. Already offset-bearing
|
|
33129
|
+
// (RFC3339 from internalDate), so it needs no offset repair — it is
|
|
33130
|
+
// allowlisted purely to gain a Display sibling, and to be re-rendered in
|
|
33131
|
+
// DISPLAY_TZ like every other instant. Separately sourced from the sibling
|
|
33132
|
+
// `date`, which is a naive re-format of the sender's Date header; the two may
|
|
33133
|
+
// legitimately disagree. See docs/timestamps.md.
|
|
33134
|
+
"internalDateIso",
|
|
33135
|
+
"modifiedTime",
|
|
33136
|
+
// Drive
|
|
33137
|
+
"createdTime",
|
|
33138
|
+
// Drive
|
|
33139
|
+
"createTime",
|
|
33140
|
+
"updateTime",
|
|
33141
|
+
"updated",
|
|
33142
|
+
"originalStartTime",
|
|
33143
|
+
"expirationTime",
|
|
33144
|
+
"lastModified",
|
|
33145
|
+
"sentAt",
|
|
33146
|
+
"viewedAt",
|
|
33147
|
+
"modifiedAt",
|
|
33148
|
+
"fetchedBodyAt",
|
|
33149
|
+
"asOf"
|
|
33150
|
+
]);
|
|
33151
|
+
var ZONE_NAME_KEYS = /* @__PURE__ */ new Set(["timeZone", "timezone"]);
|
|
33152
|
+
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})$/;
|
|
33153
|
+
var NAIVE_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?$/;
|
|
33154
|
+
var EPOCH_MILLIS = /^\d{13}$/;
|
|
33155
|
+
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
33156
|
+
function isRealCalendarDate(p) {
|
|
33157
|
+
const utc = new Date(Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second));
|
|
33158
|
+
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;
|
|
33159
|
+
}
|
|
33160
|
+
function parseTimestampValue(key, value, assumeNaiveIn) {
|
|
33161
|
+
if (typeof value !== "string") return null;
|
|
33162
|
+
const raw = value.trim();
|
|
33163
|
+
if (raw === "" || DATE_ONLY.test(raw)) return null;
|
|
33164
|
+
if (key === "internalDate" && EPOCH_MILLIS.test(raw)) {
|
|
33165
|
+
return new Date(Number(raw));
|
|
33166
|
+
}
|
|
33167
|
+
if (RFC3339_WITH_OFFSET.test(raw)) {
|
|
33168
|
+
const parsed = new Date(raw.replace(" ", "T"));
|
|
33169
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
33170
|
+
}
|
|
33171
|
+
const naive = NAIVE_DATE_TIME.exec(raw);
|
|
33172
|
+
if (naive) {
|
|
33173
|
+
const [, y, mo, d, h, mi, s, frac] = naive;
|
|
33174
|
+
const ms = frac ? Number(frac.padEnd(3, "0").slice(0, 3)) : 0;
|
|
33175
|
+
const parts = {
|
|
33176
|
+
year: Number(y),
|
|
33177
|
+
month: Number(mo),
|
|
33178
|
+
day: Number(d),
|
|
33179
|
+
hour: Number(h),
|
|
33180
|
+
minute: Number(mi),
|
|
33181
|
+
second: Number(s ?? "0")
|
|
33182
|
+
};
|
|
33183
|
+
if (!isRealCalendarDate(parts)) return null;
|
|
33184
|
+
return wallTimeToInstant(
|
|
33185
|
+
parts.year,
|
|
33186
|
+
parts.month,
|
|
33187
|
+
parts.day,
|
|
33188
|
+
parts.hour,
|
|
33189
|
+
parts.minute,
|
|
33190
|
+
parts.second,
|
|
33191
|
+
ms,
|
|
33192
|
+
assumeNaiveIn
|
|
33193
|
+
);
|
|
33194
|
+
}
|
|
33195
|
+
return null;
|
|
33196
|
+
}
|
|
33197
|
+
function walk2(node2, tz, naiveTz) {
|
|
33198
|
+
let changed = false;
|
|
33199
|
+
if (Array.isArray(node2)) {
|
|
33200
|
+
for (const item of node2) {
|
|
33201
|
+
if (walk2(item, tz, naiveTz)) changed = true;
|
|
33202
|
+
}
|
|
33203
|
+
return changed;
|
|
33204
|
+
}
|
|
33205
|
+
if (node2 === null || typeof node2 !== "object") return false;
|
|
33206
|
+
const obj = node2;
|
|
33207
|
+
for (const key of Object.keys(obj)) {
|
|
33208
|
+
const value = obj[key];
|
|
33209
|
+
if (value !== null && typeof value === "object") {
|
|
33210
|
+
if (walk2(value, tz, naiveTz)) changed = true;
|
|
33211
|
+
continue;
|
|
33212
|
+
}
|
|
33213
|
+
if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
|
|
33214
|
+
const instant = parseTimestampValue(key, value, naiveTz);
|
|
33215
|
+
if (!instant) continue;
|
|
33216
|
+
const { iso, display } = formatInstant(instant, tz);
|
|
33217
|
+
obj[key] = iso;
|
|
33218
|
+
obj[`${key}Display`] = display;
|
|
33219
|
+
changed = true;
|
|
33220
|
+
}
|
|
33221
|
+
return changed;
|
|
33222
|
+
}
|
|
33223
|
+
function detectIndent(text) {
|
|
33224
|
+
const match = /\n(\s+)\S/.exec(text);
|
|
33225
|
+
return match ? match[1].replace(/\t/g, " ").length : 0;
|
|
33226
|
+
}
|
|
33227
|
+
function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSourceTimeZone()) {
|
|
33228
|
+
const trimmed = text.trim();
|
|
33229
|
+
if (trimmed === "" || !/^[[{]/.test(trimmed)) return text;
|
|
33230
|
+
let parsed;
|
|
33231
|
+
try {
|
|
33232
|
+
parsed = JSON.parse(trimmed);
|
|
33233
|
+
} catch {
|
|
33234
|
+
return text;
|
|
33235
|
+
}
|
|
33236
|
+
if (!walk2(parsed, tz, naiveTz)) return text;
|
|
33237
|
+
return JSON.stringify(parsed, null, detectIndent(text));
|
|
33238
|
+
}
|
|
33239
|
+
|
|
33240
|
+
// src/runner.ts
|
|
33040
33241
|
function isGogFileArg(arg) {
|
|
33041
33242
|
return typeof arg !== "string";
|
|
33042
33243
|
}
|
|
@@ -33158,7 +33359,7 @@ async function spawnGog(fullArgs, opts) {
|
|
|
33158
33359
|
const spawn = spawner ?? (await import("node:child_process")).spawn;
|
|
33159
33360
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
33160
33361
|
return new Promise((resolve, reject) => {
|
|
33161
|
-
const childEnv = { ...sanitizedEnv(), PATH: augmentedPath() };
|
|
33362
|
+
const childEnv = { ...sanitizedEnv(), GOG_TIMEZONE: naiveSourceTimeZone(), PATH: augmentedPath() };
|
|
33162
33363
|
const child = spawn(readEnvVar("GOG_PATH") ?? "gog", fullArgs, { env: childEnv });
|
|
33163
33364
|
const stdoutChunks = [];
|
|
33164
33365
|
const stderrChunks = [];
|
|
@@ -33240,205 +33441,6 @@ async function runBinary(args, options = {}) {
|
|
|
33240
33441
|
return spawnExecutor(fullArgs, { timeout, interactive: false, spawner, binary: true });
|
|
33241
33442
|
}
|
|
33242
33443
|
|
|
33243
|
-
// src/timestamps.ts
|
|
33244
|
-
var DEFAULT_DISPLAY_TZ = "America/New_York";
|
|
33245
|
-
function isValidTimeZone(tz) {
|
|
33246
|
-
try {
|
|
33247
|
-
new Intl.DateTimeFormat("en-US", { timeZone: tz });
|
|
33248
|
-
return true;
|
|
33249
|
-
} catch {
|
|
33250
|
-
return false;
|
|
33251
|
-
}
|
|
33252
|
-
}
|
|
33253
|
-
function displayTimeZone() {
|
|
33254
|
-
const configured = readEnvVar("DISPLAY_TZ");
|
|
33255
|
-
if (configured && isValidTimeZone(configured)) return configured;
|
|
33256
|
-
return DEFAULT_DISPLAY_TZ;
|
|
33257
|
-
}
|
|
33258
|
-
function naiveSourceTimeZone() {
|
|
33259
|
-
const configured = readEnvVar("GOG_TIMEZONE");
|
|
33260
|
-
if (configured && isValidTimeZone(configured)) return configured;
|
|
33261
|
-
return displayTimeZone();
|
|
33262
|
-
}
|
|
33263
|
-
function offsetAt(instant, tz) {
|
|
33264
|
-
const w = wallPartsIn(instant, tz);
|
|
33265
|
-
const asUTC = Date.UTC(w.year, w.month - 1, w.day, w.hour, w.minute, w.second, instant.getUTCMilliseconds());
|
|
33266
|
-
const minutes = Math.round((asUTC - instant.getTime()) / 6e4);
|
|
33267
|
-
const sign2 = minutes < 0 ? "-" : "+";
|
|
33268
|
-
const abs = Math.abs(minutes);
|
|
33269
|
-
return `${sign2}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
33270
|
-
}
|
|
33271
|
-
function wallPartsIn(instant, tz) {
|
|
33272
|
-
const parts = new Intl.DateTimeFormat("en-US", {
|
|
33273
|
-
timeZone: tz,
|
|
33274
|
-
year: "numeric",
|
|
33275
|
-
month: "2-digit",
|
|
33276
|
-
day: "2-digit",
|
|
33277
|
-
hour: "2-digit",
|
|
33278
|
-
minute: "2-digit",
|
|
33279
|
-
second: "2-digit",
|
|
33280
|
-
// h23 pins midnight to hour 00; without it some ICU builds render hour 24.
|
|
33281
|
-
hourCycle: "h23"
|
|
33282
|
-
}).formatToParts(instant);
|
|
33283
|
-
const out = {};
|
|
33284
|
-
for (const p of parts) {
|
|
33285
|
-
if (p.type !== "literal") out[p.type] = Number(p.value);
|
|
33286
|
-
}
|
|
33287
|
-
return out;
|
|
33288
|
-
}
|
|
33289
|
-
function wallTimeToInstant(y, mo, d, h, mi, s, ms, tz) {
|
|
33290
|
-
let guess = Date.UTC(y, mo - 1, d, h, mi, s, ms);
|
|
33291
|
-
for (let i = 0; i < 2; i += 1) {
|
|
33292
|
-
const seen = wallPartsIn(new Date(guess), tz);
|
|
33293
|
-
const seenUTC = Date.UTC(seen.year, seen.month - 1, seen.day, seen.hour, seen.minute, seen.second, ms);
|
|
33294
|
-
const drift = Date.UTC(y, mo - 1, d, h, mi, s, ms) - seenUTC;
|
|
33295
|
-
if (drift === 0) break;
|
|
33296
|
-
guess += drift;
|
|
33297
|
-
}
|
|
33298
|
-
return new Date(guess);
|
|
33299
|
-
}
|
|
33300
|
-
function pad(n, width = 2) {
|
|
33301
|
-
return String(n).padStart(width, "0");
|
|
33302
|
-
}
|
|
33303
|
-
function isoWithOffset(instant, tz, offset) {
|
|
33304
|
-
const w = wallPartsIn(instant, tz);
|
|
33305
|
-
const msPart = instant.getUTCMilliseconds();
|
|
33306
|
-
const frac = msPart ? `.${pad(msPart, 3)}` : "";
|
|
33307
|
-
return `${pad(w.year, 4)}-${pad(w.month)}-${pad(w.day)}T${pad(w.hour)}:${pad(w.minute)}:${pad(w.second)}${frac}${offset}`;
|
|
33308
|
-
}
|
|
33309
|
-
function formatInstant(instant, tz = displayTimeZone()) {
|
|
33310
|
-
const offset = offsetAt(instant, tz);
|
|
33311
|
-
const display = new Intl.DateTimeFormat("en-US", {
|
|
33312
|
-
timeZone: tz,
|
|
33313
|
-
weekday: "short",
|
|
33314
|
-
month: "short",
|
|
33315
|
-
day: "numeric",
|
|
33316
|
-
year: "numeric",
|
|
33317
|
-
hour: "numeric",
|
|
33318
|
-
minute: "2-digit",
|
|
33319
|
-
timeZoneName: "short"
|
|
33320
|
-
}).format(instant);
|
|
33321
|
-
return { iso: isoWithOffset(instant, tz, offset), display };
|
|
33322
|
-
}
|
|
33323
|
-
var TIMESTAMP_KEYS = /* @__PURE__ */ new Set([
|
|
33324
|
-
"date",
|
|
33325
|
-
// gog gmail message/thread listings ("2026-07-28 03:36")
|
|
33326
|
-
"dateTime",
|
|
33327
|
-
// Calendar event start/end
|
|
33328
|
-
"internalDate",
|
|
33329
|
-
// Gmail, epoch milliseconds (authoritative)
|
|
33330
|
-
// gog >= 0.35.0 Gmail message AND thread listings. Already offset-bearing
|
|
33331
|
-
// (RFC3339 from internalDate), so it needs no offset repair — it is
|
|
33332
|
-
// allowlisted purely to gain a Display sibling, and to be re-rendered in
|
|
33333
|
-
// DISPLAY_TZ like every other instant. Separately sourced from the sibling
|
|
33334
|
-
// `date`, which is a naive re-format of the sender's Date header; the two may
|
|
33335
|
-
// legitimately disagree. See docs/timestamps.md.
|
|
33336
|
-
"internalDateIso",
|
|
33337
|
-
"modifiedTime",
|
|
33338
|
-
// Drive
|
|
33339
|
-
"createdTime",
|
|
33340
|
-
// Drive
|
|
33341
|
-
"createTime",
|
|
33342
|
-
"updateTime",
|
|
33343
|
-
"updated",
|
|
33344
|
-
"originalStartTime",
|
|
33345
|
-
"expirationTime",
|
|
33346
|
-
"lastModified",
|
|
33347
|
-
"sentAt",
|
|
33348
|
-
"viewedAt",
|
|
33349
|
-
"modifiedAt",
|
|
33350
|
-
"fetchedBodyAt",
|
|
33351
|
-
"asOf"
|
|
33352
|
-
]);
|
|
33353
|
-
var ZONE_NAME_KEYS = /* @__PURE__ */ new Set(["timeZone", "timezone"]);
|
|
33354
|
-
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})$/;
|
|
33355
|
-
var NAIVE_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,9}))?$/;
|
|
33356
|
-
var EPOCH_MILLIS = /^\d{13}$/;
|
|
33357
|
-
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
33358
|
-
function isRealCalendarDate(p) {
|
|
33359
|
-
const utc = new Date(Date.UTC(p.year, p.month - 1, p.day, p.hour, p.minute, p.second));
|
|
33360
|
-
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;
|
|
33361
|
-
}
|
|
33362
|
-
function parseTimestampValue(key, value, assumeNaiveIn) {
|
|
33363
|
-
if (typeof value !== "string") return null;
|
|
33364
|
-
const raw = value.trim();
|
|
33365
|
-
if (raw === "" || DATE_ONLY.test(raw)) return null;
|
|
33366
|
-
if (key === "internalDate" && EPOCH_MILLIS.test(raw)) {
|
|
33367
|
-
return new Date(Number(raw));
|
|
33368
|
-
}
|
|
33369
|
-
if (RFC3339_WITH_OFFSET.test(raw)) {
|
|
33370
|
-
const parsed = new Date(raw.replace(" ", "T"));
|
|
33371
|
-
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
33372
|
-
}
|
|
33373
|
-
const naive = NAIVE_DATE_TIME.exec(raw);
|
|
33374
|
-
if (naive) {
|
|
33375
|
-
const [, y, mo, d, h, mi, s, frac] = naive;
|
|
33376
|
-
const ms = frac ? Number(frac.padEnd(3, "0").slice(0, 3)) : 0;
|
|
33377
|
-
const parts = {
|
|
33378
|
-
year: Number(y),
|
|
33379
|
-
month: Number(mo),
|
|
33380
|
-
day: Number(d),
|
|
33381
|
-
hour: Number(h),
|
|
33382
|
-
minute: Number(mi),
|
|
33383
|
-
second: Number(s ?? "0")
|
|
33384
|
-
};
|
|
33385
|
-
if (!isRealCalendarDate(parts)) return null;
|
|
33386
|
-
return wallTimeToInstant(
|
|
33387
|
-
parts.year,
|
|
33388
|
-
parts.month,
|
|
33389
|
-
parts.day,
|
|
33390
|
-
parts.hour,
|
|
33391
|
-
parts.minute,
|
|
33392
|
-
parts.second,
|
|
33393
|
-
ms,
|
|
33394
|
-
assumeNaiveIn
|
|
33395
|
-
);
|
|
33396
|
-
}
|
|
33397
|
-
return null;
|
|
33398
|
-
}
|
|
33399
|
-
function walk2(node2, tz, naiveTz) {
|
|
33400
|
-
let changed = false;
|
|
33401
|
-
if (Array.isArray(node2)) {
|
|
33402
|
-
for (const item of node2) {
|
|
33403
|
-
if (walk2(item, tz, naiveTz)) changed = true;
|
|
33404
|
-
}
|
|
33405
|
-
return changed;
|
|
33406
|
-
}
|
|
33407
|
-
if (node2 === null || typeof node2 !== "object") return false;
|
|
33408
|
-
const obj = node2;
|
|
33409
|
-
for (const key of Object.keys(obj)) {
|
|
33410
|
-
const value = obj[key];
|
|
33411
|
-
if (value !== null && typeof value === "object") {
|
|
33412
|
-
if (walk2(value, tz, naiveTz)) changed = true;
|
|
33413
|
-
continue;
|
|
33414
|
-
}
|
|
33415
|
-
if (ZONE_NAME_KEYS.has(key) || !TIMESTAMP_KEYS.has(key)) continue;
|
|
33416
|
-
const instant = parseTimestampValue(key, value, naiveTz);
|
|
33417
|
-
if (!instant) continue;
|
|
33418
|
-
const { iso, display } = formatInstant(instant, tz);
|
|
33419
|
-
obj[key] = iso;
|
|
33420
|
-
obj[`${key}Display`] = display;
|
|
33421
|
-
changed = true;
|
|
33422
|
-
}
|
|
33423
|
-
return changed;
|
|
33424
|
-
}
|
|
33425
|
-
function detectIndent(text) {
|
|
33426
|
-
const match = /\n(\s+)\S/.exec(text);
|
|
33427
|
-
return match ? match[1].replace(/\t/g, " ").length : 0;
|
|
33428
|
-
}
|
|
33429
|
-
function normalizeTimestamps(text, tz = displayTimeZone(), naiveTz = naiveSourceTimeZone()) {
|
|
33430
|
-
const trimmed = text.trim();
|
|
33431
|
-
if (trimmed === "" || !/^[[{]/.test(trimmed)) return text;
|
|
33432
|
-
let parsed;
|
|
33433
|
-
try {
|
|
33434
|
-
parsed = JSON.parse(trimmed);
|
|
33435
|
-
} catch {
|
|
33436
|
-
return text;
|
|
33437
|
-
}
|
|
33438
|
-
if (!walk2(parsed, tz, naiveTz)) return text;
|
|
33439
|
-
return JSON.stringify(parsed, null, detectIndent(text));
|
|
33440
|
-
}
|
|
33441
|
-
|
|
33442
33444
|
// src/pagination.ts
|
|
33443
33445
|
function detectIndent2(text) {
|
|
33444
33446
|
const match = /\n(\s+)\S/.exec(text);
|
|
@@ -35915,7 +35917,7 @@ function registerTasksTools(server) {
|
|
|
35915
35917
|
}
|
|
35916
35918
|
|
|
35917
35919
|
// src/server.ts
|
|
35918
|
-
var VERSION = true ? "4.0.
|
|
35920
|
+
var VERSION = true ? "4.0.1" : "0.0.0";
|
|
35919
35921
|
var BASE_TOOL_REGISTRARS = [
|
|
35920
35922
|
registerApiTools,
|
|
35921
35923
|
registerAppScriptTools,
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp",
|
|
5
5
|
"display_name": "gogcli",
|
|
6
|
-
"version": "4.0.
|
|
6
|
+
"version": "4.0.1",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/mint.yaml
CHANGED
|
@@ -72,10 +72,10 @@ env:
|
|
|
72
72
|
- name: GOG_TIMEZONE
|
|
73
73
|
required: false
|
|
74
74
|
help: >-
|
|
75
|
-
The IANA zone gog
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
America/New_York.
|
|
75
|
+
The IANA zone gog formats its naive timestamps in. The wrapper passes
|
|
76
|
+
it to every spawned gog and reads it back (naiveSourceTimeZone) to
|
|
77
|
+
re-attach the offset, so the two always agree. Unset or invalid, both
|
|
78
|
+
use DISPLAY_TZ, then America/New_York — never the host's local zone.
|
|
79
79
|
- name: DISPLAY_TZ
|
|
80
80
|
required: false
|
|
81
81
|
help: >-
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "4.0.
|
|
10
|
+
"version": "4.0.1",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "4.0.
|
|
15
|
+
"version": "4.0.1",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/runner.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ChildProcess } from 'node:child_process';
|
|
2
2
|
import { delimiter, join } from 'node:path';
|
|
3
3
|
import { parseBoolEnv, readEnvVar, redactSecrets as redactSharedSecrets } from '@chrischall/mcp-utils';
|
|
4
|
+
import { naiveSourceTimeZone } from './timestamps.js';
|
|
4
5
|
|
|
5
6
|
export type Spawner = (
|
|
6
7
|
command: string,
|
|
@@ -371,7 +372,9 @@ async function spawnGog(
|
|
|
371
372
|
const effectiveTimeout = timeout ?? TIMEOUT_MS;
|
|
372
373
|
|
|
373
374
|
return new Promise((resolve, reject) => {
|
|
374
|
-
|
|
375
|
+
// gog must format naive dates in the zone normalizeTimestamps assumes; left
|
|
376
|
+
// to itself it falls back to the host's local zone (UTC on mcp-host).
|
|
377
|
+
const childEnv = { ...sanitizedEnv(), GOG_TIMEZONE: naiveSourceTimeZone(), PATH: augmentedPath() };
|
|
375
378
|
const child = spawn(readEnvVar('GOG_PATH') ?? 'gog', fullArgs, { env: childEnv });
|
|
376
379
|
const stdoutChunks: Buffer[] = [];
|
|
377
380
|
const stderrChunks: Buffer[] = [];
|
package/tests/runner.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
import { spawn as mockedSpawn } from 'node:child_process';
|
|
4
4
|
import { run, runBinary } from '../src/runner.js';
|
|
@@ -588,6 +588,50 @@ describe('run', () => {
|
|
|
588
588
|
}
|
|
589
589
|
});
|
|
590
590
|
|
|
591
|
+
// gog formats naive dates in GOG_TIMEZONE, else the process's local zone; the
|
|
592
|
+
// wrapper re-attaches offsets assuming GOG_TIMEZONE, else DISPLAY_TZ. Unless
|
|
593
|
+
// gog is TOLD the zone the wrapper assumes, a UTC host shifts every date.
|
|
594
|
+
describe('pins gog to the zone the wrapper reads its naive dates in', () => {
|
|
595
|
+
const keys = ['GOG_TIMEZONE', 'DISPLAY_TZ'] as const;
|
|
596
|
+
let snapshot: Record<string, string | undefined>;
|
|
597
|
+
beforeEach(() => { snapshot = Object.fromEntries(keys.map((k) => [k, process.env[k]])); });
|
|
598
|
+
afterEach(() => {
|
|
599
|
+
for (const [k, v] of Object.entries(snapshot)) {
|
|
600
|
+
if (v === undefined) delete process.env[k];
|
|
601
|
+
else process.env[k] = v;
|
|
602
|
+
}
|
|
603
|
+
});
|
|
604
|
+
const zoneGogGets = async (): Promise<string | undefined> => {
|
|
605
|
+
const spawner = makeSpawner(0, '{}');
|
|
606
|
+
await run(['gmail', 'search', 'x'], { spawner });
|
|
607
|
+
return ((spawner as ReturnType<typeof vi.fn>).mock.calls[0][2].env as NodeJS.ProcessEnv).GOG_TIMEZONE;
|
|
608
|
+
};
|
|
609
|
+
|
|
610
|
+
it('defaults gog to America/New_York when neither zone is set', async () => {
|
|
611
|
+
delete process.env.GOG_TIMEZONE;
|
|
612
|
+
delete process.env.DISPLAY_TZ;
|
|
613
|
+
expect(await zoneGogGets()).toBe('America/New_York');
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
it('hands gog DISPLAY_TZ when GOG_TIMEZONE is unset', async () => {
|
|
617
|
+
delete process.env.GOG_TIMEZONE;
|
|
618
|
+
process.env.DISPLAY_TZ = 'America/Los_Angeles';
|
|
619
|
+
expect(await zoneGogGets()).toBe('America/Los_Angeles');
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('passes a valid GOG_TIMEZONE through unchanged', async () => {
|
|
623
|
+
process.env.GOG_TIMEZONE = 'Europe/London';
|
|
624
|
+
process.env.DISPLAY_TZ = 'America/Los_Angeles';
|
|
625
|
+
expect(await zoneGogGets()).toBe('Europe/London');
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
it('replaces an invalid GOG_TIMEZONE with the zone the wrapper falls back to', async () => {
|
|
629
|
+
process.env.GOG_TIMEZONE = 'Not/AZone';
|
|
630
|
+
delete process.env.DISPLAY_TZ;
|
|
631
|
+
expect(await zoneGogGets()).toBe('America/New_York');
|
|
632
|
+
});
|
|
633
|
+
});
|
|
634
|
+
|
|
591
635
|
it('redacts Bearer with quoted/encoded characters', async () => {
|
|
592
636
|
const stderrLeak = 'http 401: header was Bearer eyJ.test+slash/equal=padding more text';
|
|
593
637
|
const spawner = makeSpawner(1, '', stderrLeak);
|