@gscdump/cli 4.2.2 → 4.3.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.
@@ -7,20 +7,27 @@ import { createGscMcpServer } from "../mcp/server/index.mjs";
7
7
  import { defineCommand } from "citty";
8
8
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
9
9
  const MCP_NO_AUTH_MESSAGE = [
10
- "gscdump has no Google authentication.",
11
- "Run `gscdump auth login` in a terminal, then call this tool again.",
12
- "For cloud mode, run `gscdump auth login --mode cloud` in a terminal.",
13
- "For local mode, set GSC_SERVICE_ACCOUNT_JSON or GOOGLE_APPLICATION_CREDENTIALS to a service-account key file, set GSC_ACCESS_TOKEN, or set GSC_CLIENT_ID, GSC_CLIENT_SECRET, and GSC_REFRESH_TOKEN.",
10
+ "gscdump has no Google credentials. The CLI MCP server runs in Local mode with your own Google credentials.",
11
+ "Recommended: set GSC_SERVICE_ACCOUNT_JSON or GOOGLE_APPLICATION_CREDENTIALS to a service-account key file.",
12
+ "Or set GSC_CLIENT_ID, GSC_CLIENT_SECRET, and GSC_REFRESH_TOKEN, or run `gscdump auth login --mode local` in a terminal.",
14
13
  "If you set environment variables, set them in the MCP server configuration and restart the MCP client.",
14
+ "For Hosted mode, connect your MCP client to https://gscdump.com/mcp instead.",
15
15
  "If the gscdump command is missing, run `npm install -g @gscdump/cli`."
16
16
  ].join(" ");
17
+ const MCP_HOSTED_MESSAGE = [
18
+ "The CLI MCP server calls Google, so it needs Local mode.",
19
+ "For Hosted mode, connect your MCP client to https://gscdump.com/mcp.",
20
+ "If you want Local mode, run `gscdump auth login --mode local` in a terminal, then restart the MCP client."
21
+ ].join(" ");
17
22
  const MCP_RETRIES = 1;
18
23
  async function startMcpServer(transport, runtime = useCliRuntime()) {
19
24
  const server = createGscMcpServer({
20
25
  name: "gscdump",
21
26
  version: VERSION,
22
27
  getContext: async () => {
23
- if (await probeAuth() === "none") throw new Error(MCP_NO_AUTH_MESSAGE);
28
+ const probe = await probeAuth();
29
+ if (probe === "hosted") throw new Error(MCP_HOSTED_MESSAGE);
30
+ if (probe === "none") throw new Error(MCP_NO_AUTH_MESSAGE);
24
31
  const ctx = await createCommandContext({
25
32
  needsAuth: true,
26
33
  fetchOptions: { retry: MCP_RETRIES }
@@ -29,7 +36,6 @@ async function startMcpServer(transport, runtime = useCliRuntime()) {
29
36
  throw error;
30
37
  });
31
38
  return {
32
- authentication: ctx.authentication,
33
39
  auth: ctx.auth,
34
40
  client: ctx.client
35
41
  };
@@ -46,4 +52,4 @@ const mcpCommand = defineCommand({
46
52
  await startMcpServer(new StdioServerTransport());
47
53
  }
48
54
  });
49
- export { MCP_NO_AUTH_MESSAGE, mcpCommand, startMcpServer };
55
+ export { MCP_HOSTED_MESSAGE, MCP_NO_AUTH_MESSAGE, mcpCommand, startMcpServer };
@@ -1,5 +1,6 @@
1
1
  import { queryCommandMeta } from "../command-meta.mjs";
2
2
  import { useCliRuntime } from "../runtime.mjs";
3
+ import { resolveAuthentication } from "../auth-state.mjs";
3
4
  import { loadConfig } from "../config.mjs";
4
5
  import { terminalOutputOptions } from "../render/terminal.mjs";
5
6
  import { ALL_SEARCH_TYPES, logger, parseSearchType, toCSV } from "../utils.mjs";
@@ -9,7 +10,9 @@ import { decideRoute, describeStop, liveNote, readRouteState, readSiteStates, re
9
10
  import { DEFAULT_WINDOW, checkWindowFlags, newestDoneDate, parseWindowFlags } from "../window.mjs";
10
11
  import { renderTable } from "../render/layout.mjs";
11
12
  import { asRecord, columnsFor } from "../render/analysis.mjs";
13
+ import { resolveHostedSite } from "../hosted-site.mjs";
12
14
  import { FILTER_DIMS, filterDimensions, parseFilterArgs, toLiveFilter, toLocalFilter } from "../filters.mjs";
15
+ import { queryHostedRows } from "../hosted-query.mjs";
13
16
  import { renderQuery } from "../render/query.mjs";
14
17
  import { openSqlViews, referencedTables } from "../sql-views.mjs";
15
18
  import process from "node:process";
@@ -148,7 +151,7 @@ const queryCommand = defineCommand({
148
151
  "live": {
149
152
  type: "boolean",
150
153
  default: false,
151
- description: "Bypass local store; hit the GSC API directly"
154
+ description: "Call the Search Console API directly (Local mode only)"
152
155
  },
153
156
  "quiet": {
154
157
  type: "boolean",
@@ -256,6 +259,46 @@ const queryCommand = defineCommand({
256
259
  process.exit(1);
257
260
  }
258
261
  const forceLive = Boolean(args.live);
262
+ if (!forceLive && (await resolveAuthentication())._tag === "Hosted") {
263
+ if (dataState || aggregationType) logger.warn("--data-state / --aggregation-type are ignored without --live");
264
+ const { client, site } = await resolveHostedSite(args, {
265
+ name: "query",
266
+ localAlternative: "run `gscdump sync --site <site>` first, or pass --live"
267
+ });
268
+ const { start: startDate, end: endDate } = windowOrExit(windowFlags, getLatestGscDate());
269
+ const state = {
270
+ ...buildLocalState(dimNames, startDate, endDate, rowLimit, toLocalFilter(filters)),
271
+ searchType: localSearchType
272
+ };
273
+ if (args.explain) {
274
+ console.log(JSON.stringify({
275
+ siteUrl: site.siteUrl,
276
+ siteId: site.siteId,
277
+ source: "hosted",
278
+ state
279
+ }, null, 2));
280
+ return;
281
+ }
282
+ if (!args.quiet) logger.debug(`Querying ${site.siteUrl} from the hosted record...`);
283
+ const rows = await queryHostedRows(client, site.siteId, state);
284
+ await writeOutput({
285
+ output: {
286
+ siteUrl: site.siteUrl,
287
+ dimensions: dimNames,
288
+ dateRange: {
289
+ start: startDate,
290
+ end: endDate
291
+ },
292
+ total: rows.length,
293
+ data: rows,
294
+ meta: { source: "hosted" }
295
+ },
296
+ format,
297
+ path: args.output ? String(args.output) : void 0,
298
+ quiet: Boolean(args.quiet)
299
+ });
300
+ return;
301
+ }
259
302
  const resolvable = isDatasetResolvable(dimNames, filterDims);
260
303
  if (!resolvable && !forceLive) throw new Error(`No Store table holds ${[.../* @__PURE__ */ new Set([...dimNames, ...filterDims])].join(" with ")}. Remove a dimension or filter, or pass --live.`);
261
304
  const storeCtx = await createCommandContext({ needsStore: true });
@@ -1,5 +1,5 @@
1
1
  import { sitesCommandMeta } from "../command-meta.mjs";
2
- import { formatHostedSync, getCloudSites } from "../auth-state.mjs";
2
+ import { LOCAL_MODE_REQUIRED, formatHostedSync, getHostedAccount, resolveAuthentication } from "../auth-state.mjs";
3
3
  import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
4
4
  import { createCommandContext, formatSiteResolution } from "../context.mjs";
5
5
  import process from "node:process";
@@ -421,8 +421,37 @@ const LIST_ARGS = {
421
421
  description: "Filter to permissionLevel=siteOwner"
422
422
  }
423
423
  };
424
+ async function runListHostedSites(args, authentication) {
425
+ if (args["with-sitemaps"] || args["owner-only"]) throw new Error(`--with-sitemaps and --owner-only read Search Console permissions.\n${LOCAL_MODE_REQUIRED}`);
426
+ const { sites } = await getHostedAccount(authentication);
427
+ if (args.json) {
428
+ console.log(JSON.stringify(sites.map((site) => ({
429
+ siteId: site.siteId,
430
+ siteUrl: site.siteUrl,
431
+ hostedSync: {
432
+ syncStatus: site.syncStatus ?? null,
433
+ syncProgress: site.syncProgress ?? null,
434
+ oldestDateSynced: site.oldestDateSynced ?? null,
435
+ newestDateSynced: site.newestDateSynced ?? null
436
+ }
437
+ })), null, 2));
438
+ return;
439
+ }
440
+ if (sites.length === 0) {
441
+ logger.warn("Your hosted record has no Sites. Connect a Site at https://gscdump.com/app/onboarding?step=connect-sites.");
442
+ return;
443
+ }
444
+ logger.success(`Found ${sites.length} hosted sites:`);
445
+ console.log();
446
+ for (const site of sites) console.log(` ${site.siteUrl} \x1B[90m(${site.siteId})\x1B[0m hosted sync ${formatHostedSync(site)}`);
447
+ }
424
448
  async function runListSites(args) {
425
449
  applyOutputMode(args);
450
+ const authentication = await resolveAuthentication();
451
+ if (authentication._tag === "Hosted") {
452
+ await runListHostedSites(args, authentication);
453
+ return;
454
+ }
426
455
  const ctx = await createCommandContext({ needsAuth: true });
427
456
  const ownerOnly = Boolean(args["owner-only"]);
428
457
  if (args["with-sitemaps"]) {
@@ -458,22 +487,8 @@ async function runListSites(args) {
458
487
  }
459
488
  const all = await ctx.loadSites();
460
489
  const sites = ownerOnly ? all.filter((s) => s.permissionLevel === "siteOwner") : all;
461
- const hosted = ctx.authentication._tag === "Cloud" ? await getCloudSites(ctx.authentication).then((list) => new Map(list.map((site) => [site.siteUrl, site])), (error) => {
462
- logger.warn(`Hosted sync status is not available: ${error.message}`);
463
- }) : void 0;
464
490
  if (args.json) {
465
- console.log(JSON.stringify(hosted ? sites.map((site) => {
466
- const entry = hosted.get(site.siteUrl);
467
- return {
468
- ...site,
469
- hostedSync: entry?.registered ? {
470
- syncStatus: entry.syncStatus ?? null,
471
- syncProgress: entry.syncProgress ?? null,
472
- oldestDateSynced: entry.oldestDateSynced ?? null,
473
- newestDateSynced: entry.newestDateSynced ?? null
474
- } : null
475
- };
476
- }) : sites, null, 2));
491
+ console.log(JSON.stringify(sites, null, 2));
477
492
  return;
478
493
  }
479
494
  if (sites.length === 0) {
@@ -484,9 +499,7 @@ async function runListSites(args) {
484
499
  console.log();
485
500
  for (const site of sites) {
486
501
  const perm = site.permissionLevel === "siteOwner" ? "\x1B[32m" : "\x1B[90m";
487
- const entry = hosted?.get(site.siteUrl);
488
- const sync = entry ? formatHostedSync(entry) : void 0;
489
- console.log(` ${site.siteUrl} ${perm}(${site.permissionLevel})\x1B[0m${sync ? ` hosted sync ${sync}` : ""}`);
502
+ console.log(` ${site.siteUrl} ${perm}(${site.permissionLevel})\x1B[0m`);
490
503
  }
491
504
  }
492
505
  const listCommand = defineCommand({
package/dist/context.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  import { openQuotaLedger, quotaFetchOptions } from "./quota-ledger.mjs";
2
- import { resolveAuthentication } from "./auth-state.mjs";
2
+ import { LOCAL_MODE_REQUIRED, resolveAuthentication } from "./auth-state.mjs";
3
3
  import { loadResolvedConfig } from "./config.mjs";
4
4
  import { resolveAuth } from "./auth.mjs";
5
- import { createCloudGoogleClient } from "./cloud-google.mjs";
6
5
  import { createLocalStore } from "./local-store.mjs";
7
6
  import { listStoreSites } from "./store-sites.mjs";
8
7
  import process from "node:process";
@@ -13,14 +12,14 @@ const FULL_SITE_URL_RE = /^(?:sc-domain:|https?:\/\/)/i;
13
12
  async function createCommandContext(opts = {}) {
14
13
  const { needsAuth = false, needsStore = false, interactive = false, byok, quota = true } = opts;
15
14
  const { config, dataDir } = await loadResolvedConfig();
16
- const authentication = needsAuth ? await resolveAuthentication() : { _tag: "Local" };
15
+ if (needsAuth && (await resolveAuthentication())._tag === "Hosted") throw new Error(LOCAL_MODE_REQUIRED);
17
16
  const fetchOptions = needsAuth && quota ? quotaFetchOptions(await openQuotaLedger({ dataDir }), opts.fetchOptions) : opts.fetchOptions;
18
- const auth = needsAuth && authentication._tag === "Local" ? await resolveAuth({
17
+ const auth = needsAuth ? await resolveAuth({
19
18
  interactive,
20
19
  config,
21
20
  byok
22
21
  }) : null;
23
- const client = needsAuth && authentication._tag === "Cloud" ? createCloudGoogleClient(authentication, fetchOptions) : auth ? googleSearchConsole(auth, { fetchOptions }) : null;
22
+ const client = auth ? googleSearchConsole(auth, { fetchOptions }) : null;
24
23
  const store = needsStore ? createLocalStore({ dataDir }) : null;
25
24
  const loadSites = async () => {
26
25
  if (!client) throw new Error("loadSites requires needsAuth: true");
@@ -69,7 +68,6 @@ async function createCommandContext(opts = {}) {
69
68
  return {
70
69
  config,
71
70
  dataDir,
72
- authentication,
73
71
  auth,
74
72
  client,
75
73
  store,
@@ -30,7 +30,7 @@ async function dumpBing(opts) {
30
30
  const sites = [];
31
31
  const failures = [];
32
32
  const authentication = await resolveAuthentication();
33
- if (authentication._tag === "Cloud") {
33
+ if (authentication._tag === "Hosted") {
34
34
  const hosted = (await listHostedBingSites(authentication)).filter((site) => site.connection._tag === "connected");
35
35
  const wanted = new Set(matchBingSites(hosted.map((site) => site.siteUrl), opts.googleSites));
36
36
  const selected = hosted.filter((site) => wanted.has(site.siteUrl));
@@ -5,7 +5,7 @@ import { isEngineError } from "@gscdump/engine/errors";
5
5
  import { classifyError } from "gscdump/errors";
6
6
  import { isQueryError } from "gscdump/query";
7
7
  const HOSTED_KEY_REJECTED_REASON = "gscdump.com rejected the API key";
8
- const HOSTED_KEY_REJECTED = `${HOSTED_KEY_REJECTED_REASON}. Run \`gscdump auth login --mode cloud --api-key KEY\` with a valid key.`;
8
+ const HOSTED_KEY_REJECTED = `${HOSTED_KEY_REJECTED_REASON}. Run \`gscdump auth login --mode hosted --api-key KEY\` with a valid key.`;
9
9
  const QUOTA_MESSAGE_RE = /quota|rate\s*limit/i;
10
10
  const ANSI_RE = /\x1B\[[0-9;]*m/g;
11
11
  function suggestionForGscError(error) {
@@ -1,75 +1,34 @@
1
1
  import { z } from "zod";
2
2
  const ORIGIN = "https://gscdump.com";
3
- const accessSchema = z.object({
4
- accessToken: z.string().min(1),
5
- expiresAt: z.number().int().positive()
6
- });
7
3
  const pollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal("pending") }), z.object({
8
- status: z.literal("complete"),
9
- tokens: accessSchema.extend({ refreshToken: z.string().min(1) })
10
- })]);
11
- const cloudPollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal("pending") }), z.object({
12
4
  status: z.literal("complete"),
13
5
  sessionId: z.string().regex(/^[a-f0-9]{64}$/)
14
6
  })]);
15
- async function requestJson(request, route, init = {}, failureMessage) {
7
+ const HOSTED_LOGIN_FAILURE = "Hosted login failed. Run `gscdump auth login --mode hosted` to try again.";
8
+ async function requestJson(request, route, init = {}) {
16
9
  const response = await request(`${ORIGIN}/api/cli/auth/${route}`, {
17
10
  ...init,
18
11
  redirect: "error",
19
12
  signal: AbortSignal.timeout(3e4)
20
13
  });
21
14
  if (!response.ok) {
22
- if (response.status === 429 || response.status >= 500) throw new Error("Google authorization is temporarily unavailable. Try again later.");
23
- throw new Error(failureMessage ?? "Google authorization failed. Run `gscdump auth login --mode local --force` to reconnect.");
15
+ if (response.status === 429 || response.status >= 500) throw new Error("gscdump.com login is temporarily unavailable. Try again later.");
16
+ throw new Error(HOSTED_LOGIN_FAILURE);
24
17
  }
25
18
  return response.json();
26
19
  }
27
- async function refreshWithPlatform(refreshToken, request = fetch) {
28
- const result = accessSchema.parse(await requestJson(request, "refresh", {
29
- method: "POST",
30
- headers: { "Content-Type": "application/json" },
31
- body: JSON.stringify({ refreshToken })
32
- }));
33
- return {
34
- access_token: result.accessToken,
35
- expiry_date: result.expiresAt
36
- };
37
- }
38
- async function loginWithPlatform(deps) {
39
- const init = z.object({
40
- code: z.string().regex(/^[A-F0-9]{20}$/),
41
- expiresIn: z.number().int().positive().max(600)
42
- }).parse(await requestJson(deps.request, "init", { method: "POST" }));
43
- const deadline = deps.now() + init.expiresIn * 1e3;
44
- const redirect = `/app/cli/auth?code=${init.code}`;
45
- const route = deps.force ? `/auth/google?reauth=1&redirect=${encodeURIComponent(redirect)}` : redirect;
46
- await deps.authorize(`${ORIGIN}${route}`);
47
- while (deps.now() < deadline) {
48
- const result = pollSchema.parse(await requestJson(deps.request, `poll?code=${init.code}`));
49
- if (result.status === "complete") return {
50
- provider: "gscdump",
51
- access_token: result.tokens.accessToken,
52
- refresh_token: result.tokens.refreshToken,
53
- expiry_date: result.tokens.expiresAt
54
- };
55
- await deps.wait(2e3);
56
- }
57
- throw new Error("Authorization expired. Run `gscdump auth login` to try again.");
58
- }
59
- const CLOUD_SESSION_FAILURE = "Cloud authorization failed. Run `gscdump auth login --mode cloud` to try again.";
60
- async function loginWithCloudSession(deps) {
20
+ async function loginWithHostedSession(deps) {
61
21
  const init = z.object({
62
22
  code: z.string().regex(/^S-[A-F0-9]{20}$/),
63
23
  expiresIn: z.number().int().positive().max(600)
64
- }).parse(await requestJson(deps.request, "init?mode=cloud", { method: "POST" }, CLOUD_SESSION_FAILURE));
24
+ }).parse(await requestJson(deps.request, "init?mode=cloud", { method: "POST" }));
65
25
  const deadline = deps.now() + init.expiresIn * 1e3;
66
- const url = `${ORIGIN}/app/cli/auth?code=${init.code}`;
67
- await deps.authorize(url);
26
+ await deps.authorize(`${ORIGIN}/app/cli/auth?code=${init.code}`);
68
27
  while (deps.now() < deadline) {
69
- const result = cloudPollSchema.parse(await requestJson(deps.request, `poll?code=${init.code}`, {}, CLOUD_SESSION_FAILURE));
28
+ const result = pollSchema.parse(await requestJson(deps.request, `poll?code=${init.code}`));
70
29
  if (result.status === "complete") return result.sessionId;
71
30
  await deps.wait(2e3);
72
31
  }
73
- throw new Error("Authorization expired. Run `gscdump auth login --mode cloud` to try again.");
32
+ throw new Error("Authorization expired. Run `gscdump auth login --mode hosted` to try again.");
74
33
  }
75
- export { loginWithCloudSession, loginWithPlatform, refreshWithPlatform };
34
+ export { loginWithHostedSession };
@@ -0,0 +1,31 @@
1
+ const PAGE_SIZE = 25e3;
2
+ function toWireFilter(filter) {
3
+ const source = filter;
4
+ return {
5
+ _filters: source._filters,
6
+ ...source._nestedGroups ? { _nestedGroups: source._nestedGroups.map((group) => toWireFilter(group)) } : {},
7
+ ...source._groupType ? { _groupType: source._groupType } : {}
8
+ };
9
+ }
10
+ async function queryHostedRows(client, siteId, state) {
11
+ const limit = state.rowLimit ?? PAGE_SIZE;
12
+ const rows = [];
13
+ while (rows.length < limit) {
14
+ const rowLimit = Math.min(PAGE_SIZE, limit - rows.length);
15
+ const body = {
16
+ ...state,
17
+ ...state.filter ? { filter: toWireFilter(state.filter) } : {},
18
+ ...state.prefilter ? { prefilter: toWireFilter(state.prefilter) } : {},
19
+ rowLimit,
20
+ ...rows.length > 0 ? { startRow: (state.startRow ?? 0) + rows.length } : {}
21
+ };
22
+ const page = (await client.queryAnalyticsRows({
23
+ params: { siteId },
24
+ body
25
+ })).data.rows;
26
+ rows.push(...page);
27
+ if (page.length < rowLimit) break;
28
+ }
29
+ return rows;
30
+ }
31
+ export { queryHostedRows };
@@ -1,4 +1,4 @@
1
- import { cloudCredential, getCloudAccount, parseAuthentication, resolveAuthentication } from "./auth-state.mjs";
1
+ import { getHostedAccount, hostedCredential, parseAuthentication, resolveAuthentication } from "./auth-state.mjs";
2
2
  import { loadConfig } from "./config.mjs";
3
3
  import { resolveCliEnvironment } from "./environment.mjs";
4
4
  import { logger } from "./utils.mjs";
@@ -12,7 +12,7 @@ const HOSTED_ARGS = {
12
12
  },
13
13
  "api-root": {
14
14
  type: "string",
15
- description: "Hosted API root; defaults to saved cloud authentication or https://gscdump.com/api"
15
+ description: "Hosted API root; defaults to the saved Hosted mode or https://gscdump.com/api"
16
16
  },
17
17
  "api-key": {
18
18
  type: "string",
@@ -71,19 +71,19 @@ async function resolveHostedSite(args, command) {
71
71
  if (args._?.length) fail(`\`gscdump ${command.name}\` does not accept a positional Site ID. Pass --site instead.`);
72
72
  const environment = resolveCliEnvironment().values;
73
73
  const authentication = args["api-key"] ? parseAuthentication({
74
- _tag: "Cloud",
74
+ _tag: "Hosted",
75
75
  apiKey: args["api-key"],
76
76
  apiRoot: String(args["api-root"] || environment.GSCDUMP_API_ROOT || "https://gscdump.com/api")
77
77
  }) : await resolveAuthentication();
78
- if (authentication._tag !== "Cloud") fail(`\`gscdump ${command.name}\` needs hosted authentication. Run \`gscdump auth login --mode cloud\` or pass --api-key. With local authentication, ${command.localAlternative}.`);
78
+ if (authentication._tag !== "Hosted") fail(`\`gscdump ${command.name}\` reads the hosted record, so it needs Hosted mode. Run \`gscdump auth login --mode hosted\`, or pass --api-key. In Local mode, ${command.localAlternative}.`);
79
79
  if (args["api-root"] && String(args["api-root"]).replace(/\/+$/, "") !== authentication.apiRoot) fail("The API root changed. Pass --api-key for the new API root.");
80
80
  const target = args.site ? String(args.site) : (await loadConfig()).defaultSite;
81
- const match = matchHostedSite((await getCloudAccount(authentication)).sites, target);
81
+ const match = matchHostedSite((await getHostedAccount(authentication)).sites, target);
82
82
  if (match.kind !== "found") fail(describeMatchFailure(match));
83
83
  return {
84
84
  client: createGscdumpV1Client({
85
85
  apiRoot: authentication.apiRoot,
86
- credential: cloudCredential(authentication)
86
+ credential: hostedCredential(authentication)
87
87
  }),
88
88
  site: match.site
89
89
  };
@@ -1,5 +1,3 @@
1
- import { HOSTED_KEY_REJECTED, HOSTED_KEY_REJECTED_REASON } from "../error-handler.mjs";
2
- import { resolveAuthentication } from "../auth-state.mjs";
3
1
  import { resolveBYOK, resolveServiceAccount } from "../auth.mjs";
4
2
  import { COMPARISON_FLAGS, PERIOD_FLAGS } from "../window.mjs";
5
3
  import { isAnalysisError } from "@gscdump/analysis/errors";
@@ -93,9 +91,8 @@ function httpStatus(error) {
93
91
  return typeof status === "number" ? status : void 0;
94
92
  }
95
93
  async function authenticationMode() {
96
- if ((await resolveAuthentication().catch(() => null))?._tag === "Cloud") return "cloud";
97
94
  if (await resolveServiceAccount().then(Boolean).catch(() => null)) return "service-account";
98
- return await Promise.resolve().then(() => resolveBYOK()).catch(() => null) ? "byok" : "local";
95
+ return await Promise.resolve().then(() => resolveBYOK()).catch(() => null) ? "env" : "local";
99
96
  }
100
97
  const OAUTH_TOKEN_FAILURE_RE = /invalid_grant|invalid_client|unauthorized_client|token has been expired|token has been revoked/i;
101
98
  function classifyGoogleError(error) {
@@ -109,14 +106,10 @@ function classifyGoogleError(error) {
109
106
  }
110
107
  function nextStep(error, status, mode) {
111
108
  if (error.kind === "rate-limited") return `Google quota or rate limit reached. Try again in ${error.retryAfter ? `${error.retryAfter}s` : "a few minutes"}.`;
112
- if (status === 403) {
113
- if (mode === "cloud") return "The gscdump.com account cannot open this Site. Check the Site is connected to your account at gscdump.com.";
114
- return "The signed-in account cannot open this Site. Check its Search Console permissions, or run `gscdump auth status` to see the account.";
115
- }
109
+ if (status === 403) return "The signed-in account cannot open this Site. Check its Search Console permissions, or run `gscdump auth status` to see the account.";
116
110
  if (status === 401 || error.kind === "auth-expired") {
117
- if (mode === "cloud") return "Run `gscdump auth login --mode cloud` in a terminal, then restart the MCP client.";
118
111
  if (mode === "service-account") return "Fix the service-account key (GSC_SERVICE_ACCOUNT_JSON or GOOGLE_APPLICATION_CREDENTIALS) in the MCP server configuration and restart the MCP client, or run `gscdump auth status`.";
119
- if (mode === "byok") return "Refresh GSC_ACCESS_TOKEN (or GSC_CLIENT_ID, GSC_CLIENT_SECRET, and GSC_REFRESH_TOKEN) in the MCP server configuration and restart the MCP client.";
112
+ if (mode === "env") return "Refresh GSC_ACCESS_TOKEN (or GSC_CLIENT_ID, GSC_CLIENT_SECRET, and GSC_REFRESH_TOKEN) in the MCP server configuration and restart the MCP client.";
120
113
  return "Run `gscdump auth login` in a terminal to connect again.";
121
114
  }
122
115
  if (error.kind === "not-found") return "Check the Site and URL. Call list-sites to see the Sites of this account.";
@@ -127,10 +120,7 @@ function describeApiError(error, mode = "local") {
127
120
  const status = httpStatus(error);
128
121
  if (status === void 0) return null;
129
122
  const classified = classifyGoogleError(error);
130
- const message = googleMessage(error) ?? classified.message;
131
- if (message === HOSTED_KEY_REJECTED) return `API error ${status}: ${HOSTED_KEY_REJECTED_REASON}. Replace GSCDUMP_API_KEY with a key from gscdump.com Agent setup, then restart the MCP client.`;
132
- if (message === "gscdump.com rejected the CLI session. Run `gscdump auth login --mode cloud` again.") return `API error ${status}: gscdump.com rejected the CLI session. Run \`gscdump auth login --mode cloud\` again, then restart the MCP client.`;
133
- return `API error ${status}: ${message.replace(/\s+/g, " ").trim().replace(/\.$/, "")}. ${nextStep(classified, status, mode)}`.trim();
123
+ return `API error ${status}: ${(googleMessage(error) ?? classified.message).replace(/\s+/g, " ").trim().replace(/\.$/, "")}. ${nextStep(classified, status, mode)}`.trim();
134
124
  }
135
125
  function googleMessage(error) {
136
126
  const bodies = [error?.data, error?.response?.data];
@@ -1,4 +1,3 @@
1
- import { getCloudAccount } from "../../auth-state.mjs";
2
1
  import { missingRequiredScopes } from "../../auth-scopes.mjs";
3
2
  import { currentAccessToken, fetchTokenInfo, redactTokens } from "../../token-info.mjs";
4
3
  import { ofetch } from "ofetch";
@@ -6,36 +5,6 @@ const FETCH_TIMEOUT_MS = 5e3;
6
5
  const TIME_SKEW_WARN_MS = 3e5;
7
6
  async function diagnostics(_input, ctx) {
8
7
  const checks = [];
9
- if (ctx.authentication?._tag === "Cloud") {
10
- const [account, sites] = await Promise.all([getCloudAccount(ctx.authentication).catch((error) => error), ctx.client.sites().catch((error) => error)]);
11
- checks.push(account instanceof Error ? {
12
- name: "auth",
13
- status: "fail",
14
- detail: account.message
15
- } : {
16
- name: "auth",
17
- status: "pass",
18
- detail: `Cloud account: ${account.user.email}`
19
- });
20
- checks.push(sites instanceof Error ? {
21
- name: "gsc.sites",
22
- status: "fail",
23
- detail: sites.message
24
- } : {
25
- name: "gsc.sites",
26
- status: "pass",
27
- detail: `${sites.length} Site(s) accessible through cloud authentication`
28
- });
29
- checks.push({
30
- name: "auth.capabilities",
31
- status: "info",
32
- detail: "Google indexing and Site Verification require --mode local."
33
- });
34
- return {
35
- ok: checks.every((check) => check.status !== "fail"),
36
- checks
37
- };
38
- }
39
8
  const current = await resolveAccessToken(ctx.auth);
40
9
  if (current.kind === "failed") {
41
10
  checks.push({
package/dist/package.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var version = "4.2.2";
1
+ var version = "4.3.0";
2
2
  export { version };
package/dist/route.mjs CHANGED
@@ -7,6 +7,7 @@ import { addDays } from "gscdump/dates";
7
7
  import { buildCoveragePlan, gapsForRange } from "@gscdump/engine/analysis-range";
8
8
  const CONNECT_COMMAND = "gscdump init";
9
9
  const LOGIN_COMMAND = "gscdump auth login";
10
+ const LOCAL_LOGIN_COMMAND = "gscdump auth login --mode local";
10
11
  function isCovered(need) {
11
12
  return need.kind === "window" ? need.gaps.length === 0 : need.stored;
12
13
  }
@@ -76,7 +77,7 @@ function decideRoute(req, state) {
76
77
  const { auth, coverage, syncRun } = state;
77
78
  const stored = coverage.some((need) => need.stored);
78
79
  const covered = coverage.length > 0 && coverage.every(isCovered);
79
- const connected = auth !== "none";
80
+ const connected = auth === "google";
80
81
  const syncCommand = syncCommandFor(req.site ?? req.siteHint, coverage);
81
82
  const tables = [...new Set(coverage.flatMap((need) => need.kind === "window" ? [need.table] : need.tables))];
82
83
  const searchTypes = [...new Set(coverage.flatMap((need) => need.searchType && need.searchType !== "web" ? [need.searchType] : []))];
@@ -87,7 +88,7 @@ function decideRoute(req, state) {
87
88
  tables,
88
89
  ...searchTypes.length > 0 ? { searchTypes } : {}
89
90
  },
90
- nextCommand: CONNECT_COMMAND
91
+ nextCommand: auth === "hosted" ? LOCAL_LOGIN_COMMAND : CONNECT_COMMAND
91
92
  };
92
93
  if (req.forceLive) {
93
94
  if (!req.liveCapable) return {
@@ -150,7 +151,7 @@ function decideRoute(req, state) {
150
151
  missing: gaps,
151
152
  windows: windowGaps(coverage)
152
153
  },
153
- nextCommand: connected ? syncCommand : LOGIN_COMMAND
154
+ nextCommand: connected ? syncCommand : auth === "hosted" ? LOCAL_LOGIN_COMMAND : LOGIN_COMMAND
154
155
  };
155
156
  }
156
157
  function siteLabel(site) {
@@ -165,6 +166,10 @@ function routeMessage(route, req, auth) {
165
166
  const next = route.nextCommand;
166
167
  switch (route.reason.kind) {
167
168
  case "not-connected": {
169
+ if (auth === "hosted") {
170
+ const read = req.site ?? req.siteHint ? ` If you want the hosted record, run \`gscdump query --site ${siteArg(req.site ?? req.siteHint)}\`.` : "";
171
+ return `${route.reason.tables.length > 0 ? `The Store has no ${route.reason.tables.join(", ")} data for ${site}.` : `\`${req.label}\` needs Search Console.`} Hosted mode never calls Google, so it cannot sync or query live.${read} If you want Local mode, run \`${next}\`.`;
172
+ }
168
173
  const types = route.reason.searchTypes ?? [];
169
174
  const slice = types.length > 0 ? `${types.join(", ")} ` : "";
170
175
  return `${route.reason.tables.length > 0 ? `The Store has no ${slice}${route.reason.tables.join(", ")} data for ${site}, and Google is not connected.` : `\`${req.label}\` needs Search Console, and Google is not connected.`} Run \`${next}\` to connect Google and sync the Site, or \`${LOGIN_COMMAND}\` to query Search Console directly.`;
@@ -175,6 +180,7 @@ function routeMessage(route, req, auth) {
175
180
  case "partial": {
176
181
  const { done, total, windows } = route.reason;
177
182
  const head = [`The Store has ${done.toLocaleString("en-US")} of ${total.toLocaleString("en-US")} days for ${site}.`, ...windows.map(windowGapLine)].join("\n");
183
+ if (auth === "hosted") return `${head}\nHosted mode never calls Google, so it cannot sync the Store. If you want Local mode, run \`${next}\`, then \`${syncCommandFor(req.site ?? req.siteHint, [])}\`.`;
178
184
  if (auth === "none") return `${head}\nRun \`${next}\` to connect Google, then \`${syncCommandFor(req.site ?? req.siteHint, [])}\` to sync the rest.`;
179
185
  return `${head}\nRun \`${next}\` to sync the rest${req.liveCapable ? ", or pass --live to ask Search Console directly" : ""}.`;
180
186
  }
@@ -252,7 +258,7 @@ async function readSiteStates(store, site) {
252
258
  }
253
259
  async function resolveReadSite(ctx, target, opts) {
254
260
  const auth = await probeAuth();
255
- const connected = auth !== "none";
261
+ const connected = auth === "google";
256
262
  if (opts.forceLive && connected) return {
257
263
  site: await (await opts.connect()).resolveSite(target, { scope: "account" }),
258
264
  auth
@@ -1,7 +1,7 @@
1
1
  import { ConsolaInstance } from "consola";
2
2
  export type CliEnvironmentSource = Record<string, string | undefined>;
3
3
  export interface CliRuntime {
4
- authModeOverride?: 'cloud' | 'local';
4
+ authModeOverride?: 'local' | 'hosted';
5
5
  activeProfileOverride: string | null;
6
6
  colorEnabled: boolean;
7
7
  configDir: string;
@@ -1,5 +1,5 @@
1
- import { searchTypeSupportsDimensions, searchTypeSupportsQueries } from "@gscdump/contracts";
2
1
  import { addDays, getDateRange } from "gscdump/dates";
2
+ import { searchTypeSupportsDimensions, searchTypeSupportsQueries } from "@gscdump/contracts";
3
3
  const TABLE_NEEDS = {
4
4
  pages: {
5
5
  queries: false,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "4.2.2",
4
+ "version": "4.3.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",
@@ -44,16 +44,16 @@
44
44
  "dependencies": {
45
45
  "@clack/prompts": "^1.8.1",
46
46
  "@duckdb/node-api": "1.5.5-r.5",
47
- "@gscdump/analysis": "^4.2.2",
48
- "@gscdump/contracts": "^4.2.2",
49
- "@gscdump/engine": "^4.2.2",
50
- "@gscdump/engine-gsc-api": "^4.2.2",
51
- "@gscdump/sdk": "^4.2.2",
47
+ "@gscdump/analysis": "^4.3.0",
48
+ "@gscdump/contracts": "^4.3.0",
49
+ "@gscdump/engine": "^4.3.0",
50
+ "@gscdump/engine-gsc-api": "^4.3.0",
51
+ "@gscdump/sdk": "^4.3.0",
52
52
  "@modelcontextprotocol/sdk": "^1.30.1",
53
53
  "citty": "^0.2.2",
54
54
  "consola": "^3.4.2",
55
55
  "google-auth-library": "^11.1.0",
56
- "gscdump": "^4.2.2",
56
+ "gscdump": "^4.3.0",
57
57
  "ofetch": "^1.5.1",
58
58
  "open": "^11.0.4",
59
59
  "proper-lockfile": "^4.1.2",