@gscdump/cli 0.25.14 → 0.26.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.
Files changed (2) hide show
  1. package/dist/index.mjs +234 -41
  2. package/package.json +7 -7
package/dist/index.mjs CHANGED
@@ -7,6 +7,7 @@ import path, { join } from "node:path";
7
7
  import { AnalyzerCapabilityError, createEngineQuerySource, runAnalyzerFromSource } from "@gscdump/analysis";
8
8
  import { createGscApiQuerySource } from "@gscdump/engine-gsc-api";
9
9
  import { addSite, batchInspectUrls, batchRequestIndexing, createAuth, daysAgo, decodeSiteId, deleteSite, discoverSitemap, fetchSitemap, fetchSitemapUrls, fetchSitesWithSitemaps, formatErrorForCli, getDateRange, getIndexingMetadata, getVerificationToken, getVerifiedSite, googleSearchConsole, listVerifiedSites, normalizeSiteUrl, progressBar, requestIndexing, runSequentialBatch, siteUrlToVerificationSite, unverifySite, verificationMethodsFor, verifySite } from "gscdump";
10
+ import { err, ok, unwrapResult } from "gscdump/result";
10
11
  import os from "node:os";
11
12
  import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
12
13
  import { createServer } from "node:http";
@@ -15,10 +16,12 @@ import { ofetch } from "ofetch";
15
16
  import fs$1 from "node:fs";
16
17
  import { Buffer as Buffer$1 } from "node:buffer";
17
18
  import { createConsola } from "consola";
18
- import { SearchTypes, and, between, contains, country, date, device, eq, gsc, hour, notRegex, page, query, regex, searchAppearance } from "gscdump/query";
19
+ import { SearchTypes, and, between, contains, country, date, device, eq, gsc, hour, isQueryError, notRegex, page, query, regex, searchAppearance } from "gscdump/query";
19
20
  import { createNodeHarness } from "@gscdump/engine/node";
20
21
  import { TABLE_DIMS, transformGscRow } from "@gscdump/engine/ingest";
21
22
  import { allTables, inferTable } from "@gscdump/engine/schema";
23
+ import { isAnalysisError } from "@gscdump/analysis/errors";
24
+ import { isEngineError } from "@gscdump/engine/errors";
22
25
  import { DuckDBInstance } from "@duckdb/node-api";
23
26
  import { sqlEscape } from "@gscdump/engine/sql";
24
27
  import { createEmptyTypesStore, createIndexingMetadataStore, createInspectionStore, createSitemapStore } from "@gscdump/engine/entities";
@@ -122,7 +125,7 @@ function loadEnvFromCwd() {
122
125
  }
123
126
  return applied;
124
127
  }
125
- var version = "0.25.14";
128
+ var version = "0.26.1";
126
129
  const ALL_SEARCH_TYPES$1 = Object.values(SearchTypes);
127
130
  const VERSION = version;
128
131
  function noSubcommandSelected(parent, subNames) {
@@ -293,20 +296,34 @@ function exportToCSV(output) {
293
296
  ])}`);
294
297
  return sections.join("\n\n");
295
298
  }
299
+ function authErrorToException(error) {
300
+ const exception = new Error(error.message);
301
+ if ("cause" in error && error.cause !== void 0) exception.cause = error.cause;
302
+ exception.authError = error;
303
+ return exception;
304
+ }
296
305
  const SCOPES = [
297
306
  "https://www.googleapis.com/auth/webmasters",
298
307
  "https://www.googleapis.com/auth/indexing",
299
308
  "https://www.googleapis.com/auth/siteverification"
300
309
  ];
301
- async function loadServiceAccount(jsonPath) {
310
+ async function loadServiceAccountResult(jsonPath) {
302
311
  const raw = await fs.readFile(jsonPath, "utf-8");
303
312
  const key = JSON.parse(raw);
304
- if (key.type !== "service_account") throw new Error(`${jsonPath} is not a service-account key (type=${key.type})`);
305
- return new JWT({
313
+ if (key.type !== "service_account") return err({
314
+ kind: "not-service-account",
315
+ path: jsonPath,
316
+ accountType: key.type,
317
+ message: `${jsonPath} is not a service-account key (type=${key.type})`
318
+ });
319
+ return ok(new JWT({
306
320
  email: key.client_email,
307
321
  key: key.private_key,
308
322
  scopes: SCOPES
309
- });
323
+ }));
324
+ }
325
+ async function loadServiceAccount(jsonPath) {
326
+ return unwrapResult(await loadServiceAccountResult(jsonPath), authErrorToException);
310
327
  }
311
328
  async function resolveServiceAccount(opts = {}) {
312
329
  let p = opts.path || process.env.GSC_SERVICE_ACCOUNT_JSON || process.env.GOOGLE_APPLICATION_CREDENTIALS;
@@ -315,15 +332,24 @@ async function resolveServiceAccount(opts = {}) {
315
332
  return loadServiceAccount(p);
316
333
  }
317
334
  async function authenticateDeviceCode(credentials) {
335
+ return unwrapResult(await authenticateDeviceCodeResult(credentials), authErrorToException);
336
+ }
337
+ async function authenticateDeviceCodeResult(credentials) {
318
338
  const init = await ofetch("https://oauth2.googleapis.com/device/code", {
319
339
  method: "POST",
320
340
  body: new URLSearchParams({
321
341
  client_id: credentials.clientId,
322
342
  scope: SCOPES.join(" ")
323
343
  })
324
- }).catch((e) => {
325
- throw new Error(`Device-code request failed: ${e.message}`);
326
- });
344
+ }).then(ok).catch((e) => err({
345
+ kind: "device-code-request-failed",
346
+ message: `Device-code request failed: ${e.message}`,
347
+ cause: e
348
+ }));
349
+ if (!init.ok) return init;
350
+ return pollDeviceCode(credentials, init.value);
351
+ }
352
+ async function pollDeviceCode(credentials, init) {
327
353
  console.log();
328
354
  console.log(` \x1B[1mDevice-code OAuth\x1B[0m`);
329
355
  console.log(` 1. On any device, open: \x1B[36m${init.verification_url}\x1B[0m`);
@@ -344,17 +370,30 @@ async function authenticateDeviceCode(credentials) {
344
370
  grant_type: "urn:ietf:params:oauth:grant-type:device_code"
345
371
  })
346
372
  }).catch((e) => e?.data ?? { error: "request_failed" });
347
- if (res.access_token) return {
373
+ if (res.access_token) return ok({
348
374
  access_token: res.access_token,
349
375
  refresh_token: res.refresh_token,
350
376
  expiry_date: res.expires_in ? Date.now() + res.expires_in * 1e3 : void 0
351
- };
377
+ });
352
378
  if (res.error === "authorization_pending" || res.error === "slow_down") continue;
353
- if (res.error === "access_denied") throw new Error("User denied authorization.");
354
- if (res.error === "expired_token") throw new Error("Device code expired. Re-run `gscdump auth login --no-browser`.");
355
- if (res.error) throw new Error(`Device-code poll failed: ${res.error_description || res.error}`);
379
+ if (res.error === "access_denied") return err({
380
+ kind: "device-code-denied",
381
+ message: "User denied authorization."
382
+ });
383
+ if (res.error === "expired_token") return err({
384
+ kind: "device-code-expired",
385
+ message: "Device code expired. Re-run `gscdump auth login --no-browser`."
386
+ });
387
+ if (res.error) return err({
388
+ kind: "device-code-failed",
389
+ reason: res.error,
390
+ message: `Device-code poll failed: ${res.error_description || res.error}`
391
+ });
356
392
  }
357
- throw new Error("Device-code flow timed out.");
393
+ return err({
394
+ kind: "device-code-timed-out",
395
+ message: "Device-code flow timed out."
396
+ });
358
397
  }
359
398
  function resolveBYOK(opts = {}) {
360
399
  const accessToken = opts.accessToken || process.env.GSC_ACCESS_TOKEN || process.env.GOOGLE_ACCESS_TOKEN;
@@ -780,6 +819,54 @@ async function createCommandContext(opts = {}) {
780
819
  resolveSite
781
820
  };
782
821
  }
822
+ function extractQueryError(error) {
823
+ if (isQueryError(error)) return error;
824
+ const tagged = error.queryError;
825
+ return isQueryError(tagged) ? tagged : null;
826
+ }
827
+ function extractEngineError(error) {
828
+ if (isEngineError(error)) return error;
829
+ const tagged = error.engineError;
830
+ return isEngineError(tagged) ? tagged : null;
831
+ }
832
+ function extractAnalysisError(error) {
833
+ if (isAnalysisError(error)) return error;
834
+ const tagged = error.analysisError;
835
+ return isAnalysisError(tagged) ? tagged : null;
836
+ }
837
+ function suggestionForTypedError(error) {
838
+ const query = extractQueryError(error);
839
+ if (query) switch (query.kind) {
840
+ case "unresolvable-dataset": return "This breakdown spans separate stored tables. Re-run with --live to compute it from the GSC API.";
841
+ case "unsupported-capability": return `The local engine lacks the "${query.capability}" capability for ${query.context}. Re-run with --live.`;
842
+ case "missing-date-range": return "Add a date range, e.g. --start YYYY-MM-DD --end YYYY-MM-DD.";
843
+ case "invalid-row-limit":
844
+ case "invalid-start-row":
845
+ case "invalid-data-state":
846
+ case "invalid-aggregation-type":
847
+ case "invalid-builder-state": return "";
848
+ }
849
+ const analysis = extractAnalysisError(error);
850
+ if (analysis) switch (analysis.kind) {
851
+ case "missing-report-param": return `Report "${analysis.report}" needs --${analysis.param}.`;
852
+ case "missing-comparison-window": return `Report "${analysis.report}" is a comparison report; pass --vs prev-period (or --vs yoy).`;
853
+ case "missing-brand-terms": return "Pass --brand-terms to segment branded vs non-branded queries.";
854
+ case "unknown-report": return `Unknown report. Available: ${analysis.available.join(", ")}.`;
855
+ case "unknown-analyzer": return "Run `gscdump report list` to see available reports/analyzers.";
856
+ case "required-step-failed": return suggestionForTypedError(analysis.cause);
857
+ }
858
+ const engine = extractEngineError(error);
859
+ if (engine) switch (engine.kind) {
860
+ case "attached-table-missing": return `Local store is missing table(s): ${engine.missing.join(", ")}. Run \`gscdump sync\` first, or pass --live.`;
861
+ case "analyzer-not-found":
862
+ case "analyzer-capability-missing": return "This analysis is not available for the chosen source. Re-run with --live.";
863
+ case "invalid-year-month":
864
+ case "invalid-snapshot-filename":
865
+ case "invalid-schema-identifier": return "The local store appears corrupt. Re-run `gscdump sync` to rebuild it.";
866
+ default: return "";
867
+ }
868
+ return "";
869
+ }
783
870
  var LocalStoreUnsupportedError = class extends Error {
784
871
  tool;
785
872
  mode;
@@ -799,6 +886,11 @@ async function gscErrorHandler(error) {
799
886
  process.exit(1);
800
887
  }
801
888
  console.error(formatErrorForCli(error));
889
+ const typedHint = suggestionForTypedError(error);
890
+ if (typedHint) {
891
+ console.error();
892
+ console.error(typedHint);
893
+ }
802
894
  if (isAuthError(error)) {
803
895
  console.error();
804
896
  console.error(await formatAuthProvenance());
@@ -823,12 +915,15 @@ function pickLocalSite(siteUrls, hint) {
823
915
  if (exact) return exact;
824
916
  return siteUrls.find((s) => s.includes(hint) || hint.includes(s)) ?? null;
825
917
  }
826
- function makeRunAnalysis(source, mode) {
827
- return (params) => runAnalyzerFromSource(source, params, defaultAnalyzerRegistry).catch((e) => {
828
- if (e instanceof AnalyzerCapabilityError) throw new LocalStoreUnsupportedError(params.type, mode);
918
+ async function runAnalysisResult(source, params, mode) {
919
+ return runAnalyzerFromSource(source, params, defaultAnalyzerRegistry).then(ok).catch((e) => {
920
+ if (e instanceof AnalyzerCapabilityError) return err(new LocalStoreUnsupportedError(params.type, mode));
829
921
  throw e;
830
922
  });
831
923
  }
924
+ function makeRunAnalysis(source, mode) {
925
+ return async (params) => unwrapResult(await runAnalysisResult(source, params, mode), (e) => e);
926
+ }
832
927
  async function resolveAnalysisSource(args) {
833
928
  const isLive = !!args.live;
834
929
  const format = args.json ? "json" : args.format ? String(args.format) : "table";
@@ -3748,17 +3843,10 @@ async function probeReachable(url) {
3748
3843
  }).then(() => true).catch(() => false);
3749
3844
  }
3750
3845
  async function requestIndexing$1(input, ctx) {
3751
- return requestIndexing(ctx.client, input.url, { type: input.type || "URL_UPDATED" }).catch((e) => ({
3752
- url: input.url,
3753
- type: input.type || "URL_UPDATED",
3754
- error: e.message
3755
- }));
3846
+ return requestIndexing(ctx.client, input.url, { type: input.type || "URL_UPDATED" });
3756
3847
  }
3757
3848
  async function getIndexingStatus(input, ctx) {
3758
- return getIndexingMetadata(ctx.client, input.url).catch((e) => ({
3759
- url: input.url,
3760
- error: e.message
3761
- }));
3849
+ return getIndexingMetadata(ctx.client, input.url);
3762
3850
  }
3763
3851
  async function batchRequestIndexing$1(input, ctx) {
3764
3852
  const results = await batchRequestIndexing(ctx.client, input.urls, {
@@ -3779,6 +3867,87 @@ async function batchInspectUrls$1(input, ctx) {
3779
3867
  notIndexed: results.filter((r) => !r.isIndexed).length
3780
3868
  };
3781
3869
  }
3870
+ const PERIODS = [
3871
+ "7d",
3872
+ "28d",
3873
+ "30d",
3874
+ "90d",
3875
+ "180d",
3876
+ "365d",
3877
+ "mtd",
3878
+ "ytd",
3879
+ "custom"
3880
+ ];
3881
+ const COMPARISONS = [
3882
+ "none",
3883
+ "prev-period",
3884
+ "yoy"
3885
+ ];
3886
+ const mcpHandlerErrors = {
3887
+ unknownReport(id, available) {
3888
+ return {
3889
+ kind: "unknown-report",
3890
+ id,
3891
+ available,
3892
+ message: `Unknown report id "${id}". Available: ${available.join(", ")}`
3893
+ };
3894
+ },
3895
+ unknownPeriod(value) {
3896
+ return {
3897
+ kind: "unknown-period",
3898
+ value,
3899
+ supported: PERIODS,
3900
+ message: `Unknown period "${value}". Supported: ${PERIODS.join(", ")}.`
3901
+ };
3902
+ },
3903
+ unknownComparison(value) {
3904
+ return {
3905
+ kind: "unknown-comparison",
3906
+ value,
3907
+ supported: COMPARISONS,
3908
+ message: `Unknown comparison "${value}". Supported: ${COMPARISONS.join(", ")}.`
3909
+ };
3910
+ },
3911
+ noValidDimension() {
3912
+ return {
3913
+ kind: "no-valid-dimension",
3914
+ message: "At least one valid dimension required"
3915
+ };
3916
+ }
3917
+ };
3918
+ function mcpHandlerErrorToException(error) {
3919
+ const exception = new Error(error.message);
3920
+ exception.mcpHandlerError = error;
3921
+ return exception;
3922
+ }
3923
+ function enrichToolError(error) {
3924
+ const query = asQueryError(error);
3925
+ if (query) return /* @__PURE__ */ new Error(`[query:${query.kind}] ${query.message}`);
3926
+ const analysis = asAnalysisError(error);
3927
+ if (analysis) {
3928
+ const cause = analysis.kind === "required-step-failed" ? asEngineError(analysis.cause) : null;
3929
+ const tail = cause ? ` (engine:${cause.kind})` : "";
3930
+ return /* @__PURE__ */ new Error(`[analysis:${analysis.kind}]${tail} ${analysis.message}`);
3931
+ }
3932
+ const engine = asEngineError(error);
3933
+ if (engine) return /* @__PURE__ */ new Error(`[engine:${engine.kind}] ${engine.message}`);
3934
+ return null;
3935
+ }
3936
+ function asQueryError(error) {
3937
+ if (isQueryError(error)) return error;
3938
+ const tagged = error?.queryError;
3939
+ return isQueryError(tagged) ? tagged : null;
3940
+ }
3941
+ function asEngineError(error) {
3942
+ if (isEngineError(error)) return error;
3943
+ const tagged = error?.engineError;
3944
+ return isEngineError(tagged) ? tagged : null;
3945
+ }
3946
+ function asAnalysisError(error) {
3947
+ if (isAnalysisError(error)) return error;
3948
+ const tagged = error?.analysisError;
3949
+ return isAnalysisError(tagged) ? tagged : null;
3950
+ }
3782
3951
  const PERIOD_ALIASES$1 = {
3783
3952
  "7d": "last-7d",
3784
3953
  "28d": "last-28d",
@@ -3806,13 +3975,13 @@ function listReports() {
3806
3975
  argsSpec: r.argsSpec
3807
3976
  }));
3808
3977
  }
3809
- async function runReportHandler(input, ctx) {
3978
+ async function runReportHandlerResult(input, ctx) {
3810
3979
  const report = defaultReportRegistry.getReport(input.id);
3811
- if (!report) throw new Error(`Unknown report id "${input.id}". Available: ${defaultReportRegistry.listReportIds().join(", ")}`);
3980
+ if (!report) return err(mcpHandlerErrors.unknownReport(input.id, defaultReportRegistry.listReportIds()));
3812
3981
  const preset = input.period ? PERIOD_ALIASES$1[input.period.toLowerCase()] ?? null : report.defaultPeriod;
3813
- if (!preset) throw new Error(`Unknown period "${input.period}". Supported: 7d, 28d, 30d, 90d, 180d, 365d, mtd, ytd, custom.`);
3982
+ if (!preset) return err(mcpHandlerErrors.unknownPeriod(input.period ?? ""));
3814
3983
  const comparison = input.comparison ? COMPARISON_ALIASES$1[input.comparison.toLowerCase()] ?? null : report.defaultComparison;
3815
- if (!comparison) throw new Error(`Unknown comparison "${input.comparison}". Supported: none, prev-period, yoy.`);
3984
+ if (!comparison) return err(mcpHandlerErrors.unknownComparison(input.comparison ?? ""));
3816
3985
  const window = resolveWindow({
3817
3986
  preset,
3818
3987
  comparison,
@@ -3825,7 +3994,7 @@ async function runReportHandler(input, ctx) {
3825
3994
  };
3826
3995
  const params = {};
3827
3996
  if (input.maxFindings != null) params.maxFindings = input.maxFindings;
3828
- return runReport(report, {
3997
+ return ok(await runReport(report, {
3829
3998
  source: createGscApiQuerySource({
3830
3999
  client: ctx.client,
3831
4000
  siteUrl: input.siteUrl
@@ -3837,7 +4006,12 @@ async function runReportHandler(input, ctx) {
3837
4006
  params,
3838
4007
  registryVersion: defaultReportRegistry.version
3839
4008
  }
3840
- });
4009
+ }));
4010
+ }
4011
+ async function runReportHandler(input, ctx) {
4012
+ return unwrapResult(await runReportHandlerResult(input, ctx).catch((thrown) => {
4013
+ throw enrichToolError(thrown) ?? thrown;
4014
+ }), mcpHandlerErrorToException);
3841
4015
  }
3842
4016
  async function listSitesWithSitemaps(_input, ctx) {
3843
4017
  return fetchSitesWithSitemaps(ctx.client);
@@ -4867,6 +5041,11 @@ async function writeOutput(opts) {
4867
5041
  function isKnownTable$1(name) {
4868
5042
  return allTables().includes(name);
4869
5043
  }
5044
+ function reportFlagErrorToException(error) {
5045
+ const exception = new Error(error.message);
5046
+ exception.reportFlagError = error;
5047
+ return exception;
5048
+ }
4870
5049
  const REPORT_IDS = defaultReportRegistry.listReportIds();
4871
5050
  const PERIOD_ALIASES = {
4872
5051
  "7d": "last-7d",
@@ -4893,17 +5072,31 @@ const COMPARISON_ALIASES = {
4893
5072
  "prior-period": "prev-period",
4894
5073
  "yoy": "yoy"
4895
5074
  };
4896
- function resolvePeriod(input, fallback) {
4897
- if (!input) return fallback;
5075
+ function resolvePeriodResult(input, fallback) {
5076
+ if (!input) return ok(fallback);
4898
5077
  const preset = PERIOD_ALIASES[input.toLowerCase()];
4899
- if (!preset) throw new Error(`Unknown --period "${input}". Supported: 7d, 28d, 30d, 90d, 180d, 365d, mtd, ytd, custom.`);
4900
- return preset;
5078
+ if (!preset) return err({
5079
+ kind: "unknown-period",
5080
+ value: input,
5081
+ message: `Unknown --period "${input}". Supported: 7d, 28d, 30d, 90d, 180d, 365d, mtd, ytd, custom.`
5082
+ });
5083
+ return ok(preset);
4901
5084
  }
4902
- function resolveComparison(input, fallback) {
4903
- if (!input) return fallback;
5085
+ function resolvePeriod(input, fallback) {
5086
+ return unwrapResult(resolvePeriodResult(input, fallback), reportFlagErrorToException);
5087
+ }
5088
+ function resolveComparisonResult(input, fallback) {
5089
+ if (!input) return ok(fallback);
4904
5090
  const mode = COMPARISON_ALIASES[input.toLowerCase()];
4905
- if (!mode) throw new Error(`Unknown --vs "${input}". Supported: none, prev-period, yoy.`);
4906
- return mode;
5091
+ if (!mode) return err({
5092
+ kind: "unknown-comparison",
5093
+ value: input,
5094
+ message: `Unknown --vs "${input}". Supported: none, prev-period, yoy.`
5095
+ });
5096
+ return ok(mode);
5097
+ }
5098
+ function resolveComparison(input, fallback) {
5099
+ return unwrapResult(resolveComparisonResult(input, fallback), reportFlagErrorToException);
4907
5100
  }
4908
5101
  function reportArgsToCitty(spec) {
4909
5102
  const out = {};
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "0.25.14",
4
+ "version": "0.26.1",
5
5
  "description": "CLI for Google Search Console - dump, query, and run MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
@@ -35,18 +35,18 @@
35
35
  "dist"
36
36
  ],
37
37
  "dependencies": {
38
- "@clack/prompts": "^1.5.0",
38
+ "@clack/prompts": "^1.5.1",
39
39
  "@modelcontextprotocol/sdk": "^1.29.0",
40
40
  "citty": "^0.2.2",
41
41
  "consola": "^3.4.2",
42
- "google-auth-library": "^10.6.2",
42
+ "google-auth-library": "^10.7.0",
43
43
  "ofetch": "^1.5.1",
44
44
  "open": "^11.0.0",
45
45
  "zod": "^4.4.3",
46
- "@gscdump/analysis": "0.25.14",
47
- "@gscdump/engine": "0.25.14",
48
- "@gscdump/engine-gsc-api": "0.25.14",
49
- "gscdump": "0.25.14"
46
+ "gscdump": "0.26.1",
47
+ "@gscdump/engine-gsc-api": "0.26.1",
48
+ "@gscdump/analysis": "0.26.1",
49
+ "@gscdump/engine": "0.26.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@duckdb/node-api": "1.5.1-r.2",