@gscdump/cli 3.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +113 -30
  2. package/bin/gscdump.mjs +21 -1
  3. package/dist/analysis-local.mjs +172 -53
  4. package/dist/auth-state.mjs +21 -6
  5. package/dist/auth.mjs +27 -18
  6. package/dist/bing-data.mjs +6 -2
  7. package/dist/bing-hosted.mjs +1 -1
  8. package/dist/cli-args.mjs +8 -0
  9. package/dist/cli.d.mts +5 -1
  10. package/dist/cli.mjs +41 -10
  11. package/dist/cloud-google.mjs +8 -1
  12. package/dist/command-meta.mjs +4 -4
  13. package/dist/command-registry.mjs +86 -10
  14. package/dist/commands/analyze.mjs +84 -43
  15. package/dist/commands/auth.mjs +46 -37
  16. package/dist/commands/bing.mjs +3 -3
  17. package/dist/commands/compact.mjs +12 -8
  18. package/dist/commands/config.mjs +14 -14
  19. package/dist/commands/doctor.mjs +26 -26
  20. package/dist/commands/dump.mjs +203 -105
  21. package/dist/commands/entities.mjs +10 -113
  22. package/dist/commands/gc.mjs +4 -3
  23. package/dist/commands/indexing-urls.mjs +185 -0
  24. package/dist/commands/indexing.mjs +12 -8
  25. package/dist/commands/init.mjs +77 -18
  26. package/dist/commands/inspect.mjs +97 -110
  27. package/dist/commands/mcp.mjs +37 -57
  28. package/dist/commands/papercut.mjs +1 -1
  29. package/dist/commands/profile.mjs +9 -17
  30. package/dist/commands/query.mjs +219 -210
  31. package/dist/commands/report.mjs +68 -85
  32. package/dist/commands/rollups.mjs +9 -6
  33. package/dist/commands/sitemaps.mjs +51 -104
  34. package/dist/commands/sites.mjs +40 -24
  35. package/dist/commands/stats.mjs +14 -17
  36. package/dist/commands/store-purge.mjs +9 -5
  37. package/dist/commands/store.mjs +0 -12
  38. package/dist/commands/sync.mjs +774 -296
  39. package/dist/config.mjs +7 -4
  40. package/dist/context.mjs +51 -21
  41. package/dist/coverage.mjs +251 -0
  42. package/dist/dump-bing.mjs +71 -0
  43. package/dist/dump-writers.mjs +246 -0
  44. package/dist/error-handler.mjs +108 -38
  45. package/dist/filters.mjs +125 -0
  46. package/dist/hosted-site.mjs +91 -0
  47. package/dist/inspect-urls.mjs +76 -0
  48. package/dist/inspection-record.mjs +93 -0
  49. package/dist/local-entities.mjs +658 -0
  50. package/dist/mcp/errors.mjs +71 -18
  51. package/dist/mcp/handlers/diagnostics.mjs +15 -12
  52. package/dist/mcp/handlers/reports.mjs +33 -42
  53. package/dist/mcp/server/index.mjs +80 -38
  54. package/dist/mcp/types.mjs +7 -7
  55. package/dist/package.mjs +1 -1
  56. package/dist/quota-ledger.mjs +370 -0
  57. package/dist/render/analysis.mjs +8 -1
  58. package/dist/render/report.mjs +8 -1
  59. package/dist/request-pacer.mjs +29 -0
  60. package/dist/route.mjs +295 -0
  61. package/dist/sitemap.mjs +4 -0
  62. package/dist/sql-views.mjs +117 -0
  63. package/dist/store-sites.mjs +134 -0
  64. package/dist/sync-plan.mjs +129 -0
  65. package/dist/sync-run.mjs +96 -0
  66. package/dist/table-sources.mjs +57 -0
  67. package/dist/token-info.mjs +54 -0
  68. package/dist/utils.mjs +28 -19
  69. package/dist/window.mjs +159 -0
  70. package/package.json +17 -14
  71. package/skills/gscdump/SKILL.md +167 -37
  72. package/dist/commands/export.mjs +0 -76
  73. package/dist/native-duckdb.mjs +0 -46
@@ -0,0 +1,96 @@
1
+ import process from "node:process";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ const SYNC_HEARTBEAT_STALE_MS = 12e4;
5
+ const HEARTBEAT_INTERVAL_MS = 1e4;
6
+ function syncRunStatus(record, probe) {
7
+ if (!record) return { kind: "none" };
8
+ if (record.finishedAt !== void 0) return {
9
+ kind: "finished",
10
+ record
11
+ };
12
+ if (!probe.isAlive(record.pid)) return {
13
+ kind: "stale",
14
+ record,
15
+ reason: "process-gone"
16
+ };
17
+ if (probe.now - record.heartbeatAt > 12e4) return {
18
+ kind: "stale",
19
+ record,
20
+ reason: "no-heartbeat"
21
+ };
22
+ return {
23
+ kind: "running",
24
+ record
25
+ };
26
+ }
27
+ function isProcessAlive(pid) {
28
+ try {
29
+ process.kill(pid, 0);
30
+ return true;
31
+ } catch (error) {
32
+ return error.code === "EPERM";
33
+ }
34
+ }
35
+ function syncRunPath(dataDir) {
36
+ return path.join(dataDir, "sync-run.json");
37
+ }
38
+ async function readSyncRun(dataDir) {
39
+ const body = await fs.readFile(syncRunPath(dataDir), "utf8").catch((error) => {
40
+ if (error.code === "ENOENT") return void 0;
41
+ throw error;
42
+ });
43
+ return body === void 0 ? void 0 : JSON.parse(body);
44
+ }
45
+ async function writeSyncRun(dataDir, record) {
46
+ await fs.mkdir(dataDir, { recursive: true });
47
+ const file = syncRunPath(dataDir);
48
+ const temporary = `${file}.${record.pid}.tmp`;
49
+ await fs.writeFile(temporary, JSON.stringify(record, null, 2), "utf8");
50
+ await fs.rename(temporary, file);
51
+ }
52
+ async function startSyncRun(opts) {
53
+ const now = opts.now ?? Date.now;
54
+ const record = {
55
+ pid: process.pid,
56
+ startedAt: now(),
57
+ heartbeatAt: now(),
58
+ sites: opts.sites,
59
+ planned: 0,
60
+ done: 0
61
+ };
62
+ let writing = Promise.resolve();
63
+ const save = () => {
64
+ record.heartbeatAt = now();
65
+ const snapshot = { ...record };
66
+ writing = writing.then(() => writeSyncRun(opts.dataDir, snapshot));
67
+ return writing;
68
+ };
69
+ await save();
70
+ const timer = setInterval(() => {
71
+ save().then(() => opts.onHeartbeat?.()).catch((error) => {
72
+ process.stderr.write(`gscdump: could not update the sync-run record: ${error.message}\n`);
73
+ });
74
+ }, HEARTBEAT_INTERVAL_MS);
75
+ timer.unref();
76
+ return {
77
+ record,
78
+ setSite(site) {
79
+ record.site = site;
80
+ },
81
+ addPlanned(n) {
82
+ record.planned += n;
83
+ },
84
+ tick(n = 1) {
85
+ record.done += n;
86
+ },
87
+ save,
88
+ async finish(outcome) {
89
+ clearInterval(timer);
90
+ record.finishedAt = now();
91
+ record.outcome = outcome;
92
+ await save();
93
+ }
94
+ };
95
+ }
96
+ export { SYNC_HEARTBEAT_STALE_MS, isProcessAlive, readSyncRun, startSyncRun, syncRunPath, syncRunStatus };
@@ -0,0 +1,57 @@
1
+ import { siteUrlForId } from "./store-sites.mjs";
2
+ import path from "node:path";
3
+ import { SCHEMAS } from "@gscdump/engine/schema";
4
+ function groupTableSources(entries, dataDir, siteMap) {
5
+ const groups = /* @__PURE__ */ new Map();
6
+ for (const entry of entries) {
7
+ if (!entry.siteId || entry.rowCount === 0) continue;
8
+ const searchType = entry.searchType ?? "web";
9
+ const key = `${entry.table}\u0000${entry.siteId}\u0000${searchType}`;
10
+ let group = groups.get(key);
11
+ if (!group) {
12
+ group = {
13
+ table: entry.table,
14
+ siteId: entry.siteId,
15
+ site: siteUrlForId(siteMap, entry.siteId),
16
+ searchType,
17
+ files: [],
18
+ rows: 0
19
+ };
20
+ groups.set(key, group);
21
+ }
22
+ group.files.push(path.join(dataDir, entry.objectKey));
23
+ group.rows += entry.rowCount;
24
+ }
25
+ return [...groups.values()].sort((a, b) => a.table.localeCompare(b.table) || a.site.localeCompare(b.site) || a.searchType.localeCompare(b.searchType));
26
+ }
27
+ function exportColumns(table) {
28
+ return [
29
+ {
30
+ name: "site",
31
+ type: "VARCHAR",
32
+ nullable: false
33
+ },
34
+ {
35
+ name: "search_type",
36
+ type: "VARCHAR",
37
+ nullable: false
38
+ },
39
+ ...SCHEMAS[table].columns
40
+ ];
41
+ }
42
+ function sqlString(value) {
43
+ return `'${value.replace(/'/g, "''")}'`;
44
+ }
45
+ function sourceSelectSql(source, present, opts) {
46
+ const columns = SCHEMAS[source.table].columns.map((column) => {
47
+ if (!present.has(column.name)) return `NULL::${column.type} AS ${column.name}`;
48
+ if (column.type !== "DATE") return column.name;
49
+ return opts.dates === "date" ? `CAST(${column.name} AS DATE) AS ${column.name}` : `strftime(CAST(${column.name} AS DATE), '%Y-%m-%d') AS ${column.name}`;
50
+ });
51
+ const position = opts.position ? [`sum_position / NULLIF(impressions, 0) + 1 AS position`] : [];
52
+ return `SELECT ${sqlString(source.site)} AS site, ${sqlString(source.searchType)} AS search_type, ${[...columns, ...position].join(", ")} FROM ${readParquetSql(source.files)}`;
53
+ }
54
+ function readParquetSql(files) {
55
+ return `read_parquet([${files.map(sqlString).join(", ")}], union_by_name = true)`;
56
+ }
57
+ export { exportColumns, groupTableSources, readParquetSql, sourceSelectSql, sqlString };
@@ -0,0 +1,54 @@
1
+ import { ofetch } from "ofetch";
2
+ const TOKENINFO_URL = "https://oauth2.googleapis.com/tokeninfo";
3
+ const TIMEOUT_MS = 5e3;
4
+ const TOKEN_PATTERNS = [
5
+ /ya29\.[\w.-]+/g,
6
+ /1\/\/[\w.-]{20,}/g,
7
+ /(access_token|refresh_token)=[^&\s"']+/gi
8
+ ];
9
+ function redactTokens(text, secrets = []) {
10
+ let out = text;
11
+ for (const secret of secrets) if (secret) out = out.split(secret).join("<redacted>");
12
+ for (const pattern of TOKEN_PATTERNS) out = out.replace(pattern, (match) => match.includes("=") ? `${match.slice(0, match.indexOf("="))}=<redacted>` : "<redacted>");
13
+ return out;
14
+ }
15
+ function describeFailure(error) {
16
+ const data = error?.data;
17
+ const reason = typeof data?.error_description === "string" ? data.error_description : typeof data?.error === "string" ? data.error : void 0;
18
+ if (reason) return reason;
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ async function fetchTokenInfo(accessToken, request = (url, init) => ofetch(url, init)) {
22
+ return request(TOKENINFO_URL, {
23
+ method: "POST",
24
+ body: new URLSearchParams({ access_token: accessToken }),
25
+ timeout: TIMEOUT_MS
26
+ }).then((info) => ({
27
+ kind: "valid",
28
+ info
29
+ }), (error) => ({
30
+ kind: "invalid",
31
+ detail: redactTokens(describeFailure(error), [accessToken])
32
+ }));
33
+ }
34
+ async function currentAccessToken(auth) {
35
+ if (typeof auth === "string") return {
36
+ kind: "token",
37
+ token: auth
38
+ };
39
+ if (!("getAccessToken" in auth) || typeof auth.getAccessToken !== "function") return {
40
+ kind: "failed",
41
+ detail: "These credentials cannot produce an access token."
42
+ };
43
+ return auth.getAccessToken().then((result) => result.token ? {
44
+ kind: "token",
45
+ token: result.token
46
+ } : {
47
+ kind: "failed",
48
+ detail: "Google returned no access token."
49
+ }, (error) => ({
50
+ kind: "failed",
51
+ detail: redactTokens(describeFailure(error))
52
+ }));
53
+ }
54
+ export { currentAccessToken, fetchTokenInfo, redactTokens };
package/dist/utils.mjs CHANGED
@@ -2,19 +2,12 @@ import { useCliRuntime } from "./runtime.mjs";
2
2
  import { version } from "./package.mjs";
3
3
  import process from "node:process";
4
4
  import fs from "node:fs/promises";
5
- import os from "node:os";
6
5
  import { SearchTypes } from "gscdump/query";
6
+ import os from "node:os";
7
7
  import { Buffer } from "node:buffer";
8
+ import { MAX_FETCH_BUDGET } from "@gscdump/engine/analysis-types";
8
9
  const ALL_SEARCH_TYPES = Object.values(SearchTypes);
9
10
  const VERSION = version;
10
- function noSubcommandSelected(parent, subNames) {
11
- const argv = useCliRuntime().rawArgs;
12
- const idx = argv.indexOf(parent);
13
- if (idx < 0) return true;
14
- const next = argv[idx + 1];
15
- if (!next) return true;
16
- return !subNames.includes(next);
17
- }
18
11
  const logger = new Proxy({}, {
19
12
  get(_target, property) {
20
13
  const active = useCliRuntime().logger;
@@ -65,6 +58,13 @@ function parseSearchType(value, flag = "--search-type") {
65
58
  }
66
59
  return v;
67
60
  }
61
+ function parseNameList(value, known, flag) {
62
+ const names = String(value ?? "").split(",").map((name) => name.trim()).filter(Boolean);
63
+ const unknown = names.filter((name) => !known.includes(name));
64
+ if (unknown.length > 0) throw new Error(`Unknown ${flag} name: ${unknown.join(", ")}. Valid names: ${known.join(", ")}.`);
65
+ if (names.length === 0) throw new Error(`${flag} needs at least one name. Valid names: ${known.join(", ")}.`);
66
+ return names;
67
+ }
68
68
  function parseIntegerOption(value, flag, minimum = 1) {
69
69
  if (value === void 0 || value === null) return void 0;
70
70
  const text = typeof value === "number" || typeof value === "string" ? String(value).trim() : "";
@@ -72,6 +72,11 @@ function parseIntegerOption(value, flag, minimum = 1) {
72
72
  if (!/^\d+$/.test(text) || !Number.isSafeInteger(parsed) || parsed < minimum) throw new Error(`${flag} must be ${minimum === 0 ? "a non-negative integer" : "a positive integer"}.`);
73
73
  return parsed;
74
74
  }
75
+ function parseFetchBudget(value) {
76
+ const budget = parseIntegerOption(value, "--fetch-budget");
77
+ if (budget !== void 0 && budget > MAX_FETCH_BUDGET) throw new Error(`--fetch-budget must be at most ${MAX_FETCH_BUDGET}. Larger fetches use up the GSC quota.`);
78
+ return budget;
79
+ }
75
80
  const ANSI_RE = /\x1B\[[0-9;]*m/g;
76
81
  function setNoColor(disable) {
77
82
  if (disable) useCliRuntime().colorEnabled = false;
@@ -110,6 +115,9 @@ function color(c, s) {
110
115
  function dim(s) {
111
116
  return color("dim", s);
112
117
  }
118
+ function red(s) {
119
+ return color("red", s);
120
+ }
113
121
  function cyan(s) {
114
122
  return color("cyan", s);
115
123
  }
@@ -174,14 +182,15 @@ function toCSV(data, columns) {
174
182
  return str.includes(",") || str.includes("\"") || str.includes("\n") ? `"${str.replace(/"/g, "\"\"")}"` : str;
175
183
  }).join(","))].join("\n");
176
184
  }
177
- async function readUrlList(args) {
178
- if (args.file) return (await fs.readFile(String(args.file), "utf-8")).split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
179
- if (args.urls) return (Array.isArray(args.urls) ? args.urls : [args.urls]).map(String).filter(Boolean);
180
- if (!process.stdin.isTTY) {
181
- const chunks = [];
182
- for await (const chunk of process.stdin) chunks.push(chunk);
183
- return Buffer.concat(chunks).toString("utf-8").split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
184
- }
185
- return [];
185
+ function parseUrlLines(text) {
186
+ return text.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
187
+ }
188
+ async function readUrlList(input) {
189
+ const urls = (input.positionals ?? []).map(String).filter(Boolean);
190
+ if (input.file) urls.push(...parseUrlLines(await fs.readFile(String(input.file), "utf-8")));
191
+ if (urls.length > 0 || input.file || process.stdin.isTTY) return urls;
192
+ const chunks = [];
193
+ for await (const chunk of process.stdin) chunks.push(chunk);
194
+ return parseUrlLines(Buffer.concat(chunks).toString("utf-8"));
186
195
  }
187
- export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger, noSubcommandSelected, parseIntegerOption, parseSearchType, progressBar, readUrlList, runWithConcurrency, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };
196
+ export { ALL_SEARCH_TYPES, OUTPUT_ARGS, VERSION, applyOutputMode, clearLine, color, cyan, dim, displayPath, formatAge, isColorEnabled, logger, parseFetchBudget, parseIntegerOption, parseNameList, parseSearchType, progressBar, readUrlList, red, runWithConcurrency, setNoColor, setQuiet, showSplash, toCSV, withConfiguredOutput };
@@ -0,0 +1,159 @@
1
+ import { err, ok } from "gscdump/result";
2
+ import { resolveWindow } from "@gscdump/engine/period";
3
+ const PERIOD_ALIASES = {
4
+ "7d": "last-7d",
5
+ "28d": "last-28d",
6
+ "30d": "last-30d",
7
+ "90d": "last-90d",
8
+ "180d": "last-180d",
9
+ "365d": "last-365d",
10
+ "last-7d": "last-7d",
11
+ "last-28d": "last-28d",
12
+ "last-30d": "last-30d",
13
+ "last-90d": "last-90d",
14
+ "last-180d": "last-180d",
15
+ "last-365d": "last-365d",
16
+ "mtd": "mtd",
17
+ "qtd": "qtd",
18
+ "ytd": "ytd",
19
+ "last-quarter": "last-quarter",
20
+ "custom": "custom"
21
+ };
22
+ const PERIOD_FLAGS = [
23
+ "7d",
24
+ "28d",
25
+ "30d",
26
+ "90d",
27
+ "180d",
28
+ "365d",
29
+ "mtd",
30
+ "qtd",
31
+ "ytd",
32
+ "last-quarter",
33
+ "custom"
34
+ ];
35
+ const COMPARISON_ALIASES = {
36
+ "none": "none",
37
+ "prev": "prev-period",
38
+ "prev-period": "prev-period",
39
+ "prior": "prev-period",
40
+ "prior-period": "prev-period",
41
+ "yoy": "yoy"
42
+ };
43
+ const COMPARISON_FLAGS = [
44
+ "none",
45
+ "prev-period",
46
+ "yoy"
47
+ ];
48
+ const DEFAULT_WINDOW = {
49
+ preset: "last-28d",
50
+ comparison: "none"
51
+ };
52
+ const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
53
+ function isCalendarDate(value) {
54
+ if (!ISO_DATE_RE.test(value)) return false;
55
+ const date = /* @__PURE__ */ new Date(`${value}T00:00:00Z`);
56
+ return Number.isFinite(date.getTime()) && date.toISOString().slice(0, 10) === value;
57
+ }
58
+ function checkDate(flag, value) {
59
+ if (value === void 0 || isCalendarDate(value)) return void 0;
60
+ return {
61
+ kind: "invalid-date",
62
+ flag,
63
+ value,
64
+ message: `Invalid ${flag} "${value}". Use a calendar date in YYYY-MM-DD format.`
65
+ };
66
+ }
67
+ function checkWindowFlags(flags) {
68
+ for (const [flag, value] of [
69
+ ["--start", flags.start],
70
+ ["--end", flags.end],
71
+ ["--prev-start", flags.prevStart],
72
+ ["--prev-end", flags.prevEnd]
73
+ ]) {
74
+ const invalid = checkDate(flag, value);
75
+ if (invalid) return invalid;
76
+ }
77
+ const preset = flags.period ? PERIOD_ALIASES[flags.period.toLowerCase()] : void 0;
78
+ if (flags.period && !preset) return {
79
+ kind: "unknown-period",
80
+ value: flags.period,
81
+ message: `Unknown --period "${flags.period}". Supported: ${PERIOD_FLAGS.join(", ")}.`
82
+ };
83
+ if (flags.vs && !COMPARISON_ALIASES[flags.vs.toLowerCase()]) return {
84
+ kind: "unknown-comparison",
85
+ value: flags.vs,
86
+ message: `Unknown --vs "${flags.vs}". Supported: ${COMPARISON_FLAGS.join(", ")}.`
87
+ };
88
+ if (flags.prevStart === void 0 !== (flags.prevEnd === void 0)) return {
89
+ kind: "partial-comparison",
90
+ message: "Pass --prev-start and --prev-end together."
91
+ };
92
+ if (preset && preset !== "custom" && (flags.start !== void 0 || flags.end !== void 0)) return {
93
+ kind: "period-conflict",
94
+ message: `--period ${flags.period} sets its own dates. Remove --start/--end, or pass --period custom.`
95
+ };
96
+ if (flags.start !== void 0 && flags.end !== void 0 && flags.start > flags.end) return {
97
+ kind: "inverted-range",
98
+ message: `Invalid date range. --start ${flags.start} is after --end ${flags.end}.`
99
+ };
100
+ if (flags.prevStart !== void 0 && flags.prevEnd !== void 0 && flags.prevStart > flags.prevEnd) return {
101
+ kind: "inverted-range",
102
+ message: `Invalid comparison range. --prev-start ${flags.prevStart} is after --prev-end ${flags.prevEnd}.`
103
+ };
104
+ }
105
+ function parseWindowFlags(flags, defaults, anchor) {
106
+ const invalid = checkWindowFlags(flags);
107
+ if (invalid) return err(invalid);
108
+ const preset = flags.period ? PERIOD_ALIASES[flags.period.toLowerCase()] : void 0;
109
+ const comparison = flags.vs ? COMPARISON_ALIASES[flags.vs.toLowerCase()] : defaults.comparison;
110
+ const hasBounds = flags.start !== void 0 || flags.end !== void 0;
111
+ let window;
112
+ if (preset === "custom" || hasBounds) {
113
+ const end = flags.end ?? anchor;
114
+ const start = flags.start ?? resolveWindow({
115
+ preset: defaults.preset,
116
+ anchor: end
117
+ }).start;
118
+ if (start > end) return err({
119
+ kind: "inverted-range",
120
+ message: `Invalid date range. --start ${start} is after --end ${end}.`
121
+ });
122
+ window = resolveWindow({
123
+ preset: "custom",
124
+ start,
125
+ end,
126
+ comparison
127
+ });
128
+ } else window = resolveWindow({
129
+ preset: preset ?? defaults.preset,
130
+ anchor,
131
+ comparison
132
+ });
133
+ if (flags.prevStart !== void 0 && flags.prevEnd !== void 0) window = {
134
+ ...window,
135
+ comparison: {
136
+ start: flags.prevStart,
137
+ end: flags.prevEnd
138
+ }
139
+ };
140
+ return ok(window);
141
+ }
142
+ function windowFlagErrorToException(error) {
143
+ const exception = new Error(error.message);
144
+ exception.windowFlagError = error;
145
+ return exception;
146
+ }
147
+ function newestDoneDate(states, tables) {
148
+ const wanted = tables.length ? tables : [...new Set(states.map((state) => state.table))];
149
+ if (!wanted.length) return void 0;
150
+ let anchor;
151
+ for (const table of wanted) {
152
+ let newest;
153
+ for (const state of states) if (state.table === table && state.state === "done" && (state.searchType ?? "web") === "web" && (!newest || state.date > newest)) newest = state.date;
154
+ if (!newest) return void 0;
155
+ if (!anchor || newest < anchor) anchor = newest;
156
+ }
157
+ return anchor;
158
+ }
159
+ export { COMPARISON_ALIASES, COMPARISON_FLAGS, DEFAULT_WINDOW, PERIOD_ALIASES, PERIOD_FLAGS, checkWindowFlags, newestDoneDate, parseWindowFlags, windowFlagErrorToException };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "3.7.0",
4
+ "version": "4.0.0",
5
5
  "description": "CLI for Google Search Console and Bing with hosted or local authentication, data exports, and an MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -39,28 +39,31 @@
39
39
  "skills"
40
40
  ],
41
41
  "engines": {
42
- "node": ">=22"
42
+ "node": ">=22.13.0"
43
43
  },
44
44
  "dependencies": {
45
- "@clack/prompts": "^1.8.0",
46
- "@duckdb/node-api": "1.5.5-r.4",
47
- "@gscdump/analysis": "^3.7.0",
48
- "@gscdump/engine": "^3.7.0",
49
- "@gscdump/engine-gsc-api": "^3.7.0",
50
- "@gscdump/sdk": "^3.7.0",
51
- "@modelcontextprotocol/sdk": "^1.30.0",
45
+ "@clack/prompts": "^1.8.1",
46
+ "@duckdb/node-api": "1.5.5-r.5",
47
+ "@gscdump/analysis": "^4.0.0",
48
+ "@gscdump/contracts": "^4.0.0",
49
+ "@gscdump/engine": "^4.0.0",
50
+ "@gscdump/engine-gsc-api": "^4.0.0",
51
+ "@gscdump/sdk": "^4.0.0",
52
+ "@modelcontextprotocol/sdk": "^1.30.1",
52
53
  "citty": "^0.2.2",
53
54
  "consola": "^3.4.2",
54
- "google-auth-library": "^11.0.2",
55
- "gscdump": "^3.7.0",
55
+ "google-auth-library": "^11.1.0",
56
+ "gscdump": "^4.0.0",
56
57
  "ofetch": "^1.5.1",
57
- "open": "^11.0.2",
58
+ "open": "^11.0.4",
59
+ "proper-lockfile": "^4.1.2",
58
60
  "sitemapd": "^0.2.2",
59
61
  "string-width": "^8.2.2",
60
- "zod": "^4.6.1"
62
+ "zod": "^4.6.5"
61
63
  },
62
64
  "devDependencies": {
63
- "vitest": "^5.0.0"
65
+ "@types/proper-lockfile": "^4.1.4",
66
+ "vitest": "^5.0.1"
64
67
  },
65
68
  "scripts": {
66
69
  "build": "obuild",