@gscdump/cli 4.1.0 → 4.2.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.
package/README.md CHANGED
@@ -2,14 +2,16 @@
2
2
 
3
3
  Google Search Console CLI and MCP server.
4
4
 
5
+ Choose [cloud or local access](../../docs/gscdump-cli/guides/1.start/2.choose-access.md) before login. Cloud login opens gscdump.com and saves a CLI session. Local login calls Google directly and can use your own Google credentials.
6
+
5
7
  ```bash
6
8
  npm install -g @gscdump/cli
7
9
  ```
8
10
 
9
- - [Getting started](../../docs/gscdump-cli/guides/1.getting-started.md)
10
- - [Authentication](../../docs/gscdump-cli/guides/2.authentication.md)
11
+ - [Getting started](../../docs/gscdump-cli/guides/1.start/1.first-result.md)
12
+ - [Authentication](../../docs/gscdump-cli/guides/1.start/2.choose-access.md)
11
13
  - [Command reference](../../docs/gscdump-cli/api/1.commands.md)
12
- - [AI integration and MCP](../../docs/gscdump-cli/guides/3.ai-integration.md)
14
+ - [AI integration and MCP](../../docs/gscdump-cli/guides/1.start/3.connect-an-agent.md)
13
15
 
14
16
  ## License
15
17
 
@@ -5,6 +5,7 @@ import fs from "node:fs/promises";
5
5
  import path from "node:path";
6
6
  import { gscdumpAvailableSiteSchema } from "@gscdump/contracts";
7
7
  import { z } from "zod";
8
+ const HOSTED_SESSION_REJECTED = "gscdump.com rejected the CLI session. Run `gscdump auth login --mode cloud` again.";
8
9
  const apiRootSchema = z.url().transform((value) => value.replace(/\/+$/, "")).refine((value) => {
9
10
  const url = new URL(value);
10
11
  return !url.username && !url.password && !url.search && !url.hash && (url.protocol === "https:" || url.protocol === "http:" && [
@@ -13,11 +14,19 @@ const apiRootSchema = z.url().transform((value) => value.replace(/\/+$/, "")).re
13
14
  "[::1]"
14
15
  ].includes(url.hostname));
15
16
  }, "Use HTTPS, or HTTP on loopback, for the API root");
16
- const stateSchema = z.discriminatedUnion("_tag", [z.object({ _tag: z.literal("Local") }), z.object({
17
- _tag: z.literal("Cloud"),
18
- apiRoot: apiRootSchema,
19
- apiKey: z.string().trim().regex(/^gsd_user_\S+$/)
20
- })]);
17
+ const stateSchema = z.union([
18
+ z.object({ _tag: z.literal("Local") }),
19
+ z.object({
20
+ _tag: z.literal("Cloud"),
21
+ apiRoot: apiRootSchema,
22
+ apiKey: z.string().trim().regex(/^gsd_user_\S+$/)
23
+ }),
24
+ z.object({
25
+ _tag: z.literal("Cloud"),
26
+ apiRoot: apiRootSchema,
27
+ sessionId: z.string().regex(/^[a-f0-9]{64}$/)
28
+ })
29
+ ]);
21
30
  function parseAuthMode(value) {
22
31
  if (value === void 0 || value === "") return void 0;
23
32
  if (value === "cloud" || value === "local") return value;
@@ -25,7 +34,7 @@ function parseAuthMode(value) {
25
34
  }
26
35
  function parseAuthentication(value) {
27
36
  const parsed = stateSchema.safeParse(value);
28
- if (!parsed.success) throw new Error("Invalid authentication. Use a gscdump user API key and a trusted API root.");
37
+ if (!parsed.success) throw new Error("Invalid authentication. Use a CLI session or gscdump user API key with a trusted API root.");
29
38
  return parsed.data;
30
39
  }
31
40
  async function saveAuthentication(state) {
@@ -50,6 +59,10 @@ async function saveAuthentication(state) {
50
59
  async function clearAuthentication() {
51
60
  await fs.rm(path.join(useCliRuntime().configDir, "authentication.json"), { force: true });
52
61
  }
62
+ async function revokeCloudSession(state) {
63
+ if (!("sessionId" in state)) return;
64
+ await cloudRequest(state, "/cli/auth/logout", { method: "POST" });
65
+ }
53
66
  async function resolveAuthentication() {
54
67
  const runtime = useCliRuntime();
55
68
  const env = runtime.environment;
@@ -73,7 +86,7 @@ async function resolveAuthentication() {
73
86
  return parsed.data;
74
87
  }
75
88
  if (state?._tag === "Cloud") {
76
- if (env.GSCDUMP_API_ROOT && env.GSCDUMP_API_ROOT.replace(/\/+$/, "") !== state.apiRoot) throw new Error("The API root changed. Supply GSCDUMP_API_KEY explicitly for the new API root.");
89
+ if (env.GSCDUMP_API_ROOT && env.GSCDUMP_API_ROOT.replace(/\/+$/, "") !== state.apiRoot) throw new Error("The API root changed. Run `gscdump auth login --mode cloud` for the new API root.");
77
90
  return state;
78
91
  }
79
92
  if (mode === "cloud") throw new Error("Hosted credentials are missing. Run `gscdump auth login --mode cloud`.");
@@ -94,13 +107,13 @@ async function cloudRequest(state, route, options = {}) {
94
107
  ...options,
95
108
  headers: {
96
109
  "Content-Type": "application/json",
97
- "x-api-key": state.apiKey
110
+ ..."sessionId" in state ? { "x-cli-session": state.sessionId } : { "x-api-key": state.apiKey }
98
111
  },
99
112
  signal: options.signal ?? AbortSignal.timeout(3e4),
100
113
  redirect: "error"
101
114
  });
102
115
  if (!response.ok) {
103
- const message = response.status === 401 ? HOSTED_KEY_REJECTED : `Hosted request failed (${response.status}) for ${route.split("?")[0]}. Check \`gscdump auth status\`.`;
116
+ const message = response.status === 401 ? "sessionId" in state ? HOSTED_SESSION_REJECTED : HOSTED_KEY_REJECTED : `Hosted request failed (${response.status}) for ${route.split("?")[0]}. Check \`gscdump auth status\`.`;
104
117
  throw Object.assign(new Error(message), {
105
118
  statusCode: response.status,
106
119
  retryAfter: response.headers.get("retry-after"),
@@ -109,6 +122,9 @@ async function cloudRequest(state, route, options = {}) {
109
122
  }
110
123
  return response.status === 204 ? void 0 : response.json();
111
124
  }
125
+ function cloudCredential(state) {
126
+ return "sessionId" in state ? state.sessionId : state.apiKey;
127
+ }
112
128
  async function getCloudAccount(state) {
113
129
  const result = accountSchema.safeParse(await cloudRequest(state, "/cli/me"));
114
130
  if (!result.success) throw new Error("The hosted API returned invalid account data.");
@@ -124,4 +140,4 @@ function formatHostedSync(site) {
124
140
  const status = site.syncStatus ?? "pending";
125
141
  return `${status}${site.syncProgress && site.syncProgress.total > 0 && status !== "synced" ? `: ${site.syncProgress.completed.toLocaleString("en-US")} of ${site.syncProgress.total.toLocaleString("en-US")} days (${Math.round(site.syncProgress.percent)}%)` : ""}${status === "synced" && site.oldestDateSynced && site.newestDateSynced ? `: ${site.oldestDateSynced} to ${site.newestDateSynced}` : ""}`;
126
142
  }
127
- export { clearAuthentication, cloudRequest, formatHostedSync, getCloudAccount, getCloudSites, parseAuthMode, parseAuthentication, resolveAuthentication, saveAuthentication };
143
+ export { HOSTED_SESSION_REJECTED, clearAuthentication, cloudCredential, cloudRequest, formatHostedSync, getCloudAccount, getCloudSites, parseAuthMode, parseAuthentication, resolveAuthentication, revokeCloudSession, saveAuthentication };
package/dist/auth.mjs CHANGED
@@ -70,7 +70,7 @@ function getTokensPath() {
70
70
  const GOOGLE_NOT_CONNECTED = [
71
71
  "Google is not connected. Use one of these:",
72
72
  " Local: gscdump auth login",
73
- " Hosted: gscdump auth login --mode cloud --api-key KEY (a gscdump.com API key)",
73
+ " Cloud: gscdump auth login --mode cloud (opens a browser)",
74
74
  " BYOK: set GSC_ACCESS_TOKEN, or GSC_CLIENT_ID, GSC_CLIENT_SECRET and GSC_REFRESH_TOKEN"
75
75
  ].join("\n");
76
76
  async function loadTokens() {
@@ -1,11 +1,11 @@
1
- import { getCloudAccount } from "./auth-state.mjs";
1
+ import { cloudCredential, getCloudAccount } from "./auth-state.mjs";
2
2
  import { logger } from "./utils.mjs";
3
3
  import { writeBingDump } from "./bing-data.mjs";
4
4
  import { createGscdumpV1Client } from "@gscdump/sdk/v1";
5
5
  function hostedBingClient(state) {
6
6
  return createGscdumpV1Client({
7
7
  apiRoot: state.apiRoot,
8
- credential: state.apiKey
8
+ credential: cloudCredential(state)
9
9
  });
10
10
  }
11
11
  async function listHostedBingSites(state) {
@@ -1,5 +1,5 @@
1
1
  import { HOSTED_KEY_REJECTED } from "./error-handler.mjs";
2
- import { cloudRequest } from "./auth-state.mjs";
2
+ import { HOSTED_SESSION_REJECTED, cloudRequest } from "./auth-state.mjs";
3
3
  import { googleSearchConsole } from "gscdump/client";
4
4
  import { ofetch } from "ofetch";
5
5
  function createCloudGoogleClient(state, fetchOptions) {
@@ -69,7 +69,7 @@ function createCloudGoogleClient(state, fetchOptions) {
69
69
  if (error && typeof error === "object" && "response" in error && error.response instanceof Response) {
70
70
  if (error.response.status === 401) return Response.json({ error: {
71
71
  code: 401,
72
- message: HOSTED_KEY_REJECTED
72
+ message: "sessionId" in state ? HOSTED_SESSION_REJECTED : HOSTED_KEY_REJECTED
73
73
  } }, { status: 401 });
74
74
  return error.response;
75
75
  }
@@ -1,8 +1,9 @@
1
1
  import { authCommandMeta } from "../command-meta.mjs";
2
2
  import { useCliRuntime } from "../runtime.mjs";
3
- import { clearAuthentication, formatHostedSync, getCloudAccount, getCloudSites, parseAuthMode, parseAuthentication, resolveAuthentication, saveAuthentication } from "../auth-state.mjs";
3
+ import { clearAuthentication, formatHostedSync, getCloudAccount, getCloudSites, parseAuthMode, parseAuthentication, resolveAuthentication, revokeCloudSession, saveAuthentication } from "../auth-state.mjs";
4
4
  import { loadConfig, saveConfig } from "../config.mjs";
5
5
  import { OUTPUT_ARGS, applyOutputMode, logger } from "../utils.mjs";
6
+ import { loginWithCloudSession } from "../hosted-auth.mjs";
6
7
  import { GOOGLE_NOT_CONNECTED, clearTokens, formatAuthProvenance, getAuth, loadServiceAccount, loadTokens, resolveBYOK, saveTokens } from "../auth.mjs";
7
8
  import { missingRequiredScopes } from "../auth-scopes.mjs";
8
9
  import { clearBingCredentials, getBingClient, inspectBingCredentials } from "../bing-auth.mjs";
@@ -12,7 +13,8 @@ import { adoptCurrentConfigAsProfile, profileNameFromEmail, resolveActiveProfile
12
13
  import process from "node:process";
13
14
  import { defineCommand } from "citty";
14
15
  import path from "node:path";
15
- import { isCancel, password } from "@clack/prompts";
16
+ import { setTimeout } from "node:timers/promises";
17
+ import open from "open";
16
18
  const MODE_ARG = {
17
19
  type: "string",
18
20
  description: "Authentication mode: cloud or local"
@@ -23,21 +25,39 @@ function applyAuthMode(args) {
23
25
  }
24
26
  async function requireLocalAuth(args) {
25
27
  applyAuthMode(args);
26
- if ((await resolveAuthentication())._tag === "Cloud") throw new Error("Cloud authentication uses an API key. OAuth scopes and token refresh require --mode local.");
28
+ if ((await resolveAuthentication())._tag === "Cloud") throw new Error("Cloud authentication uses a CLI session. Google OAuth scopes and token refresh require --mode local.");
27
29
  }
28
30
  async function loginCloud(args) {
29
31
  const env = useCliRuntime().environment;
30
- let apiKey = String(args["api-key"] ?? env.GSCDUMP_API_KEY ?? "");
32
+ const apiKey = String(args["api-key"] ?? env.GSCDUMP_API_KEY ?? "");
33
+ const apiRoot = String(args["api-root"] ?? env.GSCDUMP_API_ROOT ?? "https://gscdump.com/api");
31
34
  if (!apiKey) {
32
- if (!process.stdin.isTTY) throw new Error("Cloud login requires --api-key or GSCDUMP_API_KEY.");
33
- const answer = await password({ message: "gscdump user API key" });
34
- if (isCancel(answer) || typeof answer !== "string") throw new Error("Login cancelled.");
35
- apiKey = answer;
35
+ if (apiRoot.replace(/\/+$/, "") !== "https://gscdump.com/api") throw new Error("Browser login uses gscdump.com. Supply --api-key for a custom API root.");
36
+ const sessionId = await loginWithCloudSession({
37
+ request: fetch,
38
+ now: Date.now,
39
+ wait: setTimeout,
40
+ authorize: async (url) => {
41
+ logger.info(`Open this URL to connect cloud access:\n${url}`);
42
+ if (args.browser !== false) await open(url).catch((error) => logger.warn(`Browser could not open: ${error.message}. Open the URL above.`));
43
+ }
44
+ });
45
+ const state = parseAuthentication({
46
+ _tag: "Cloud",
47
+ apiRoot,
48
+ sessionId
49
+ });
50
+ if (state._tag !== "Cloud") throw new Error("Cloud login did not return a CLI session.");
51
+ const account = await getCloudAccount(state);
52
+ await saveAuthentication(state);
53
+ logger.success(`Cloud authentication saved for ${account.user.email}`);
54
+ logger.info("Google and Bing commands use connections saved on gscdump.com.");
55
+ return;
36
56
  }
37
57
  const state = parseAuthentication({
38
58
  _tag: "Cloud",
39
59
  apiKey,
40
- apiRoot: String(args["api-root"] ?? env.GSCDUMP_API_ROOT ?? "https://gscdump.com/api")
60
+ apiRoot
41
61
  });
42
62
  if (state._tag !== "Cloud") throw new Error("Cloud login requires a gscdump user API key.");
43
63
  const account = await getCloudAccount(state);
@@ -365,6 +385,10 @@ const logoutCommand = defineCommand({
365
385
  args: { ...OUTPUT_ARGS },
366
386
  async run({ args }) {
367
387
  applyOutputMode(args);
388
+ await resolveAuthentication().then((authentication) => authentication._tag === "Cloud" ? revokeCloudSession(authentication) : void 0).catch((error) => {
389
+ const message = error instanceof Error ? error.message : String(error);
390
+ logger.warn(`Cloud session revocation failed (${message}). Local credentials are still cleared.`);
391
+ });
368
392
  await clearTokens();
369
393
  await clearBingCredentials();
370
394
  await clearAuthentication();
@@ -148,7 +148,7 @@ const initCommand = defineCommand({
148
148
  logger.error([
149
149
  "No Google credentials found. Init cannot prompt without a terminal.",
150
150
  "Run `gscdump auth login` in a terminal, or set GSC_CLIENT_ID, GSC_CLIENT_SECRET and GSC_REFRESH_TOKEN.",
151
- "To use gscdump.com, run `gscdump init --mode cloud --api-key <key>`."
151
+ "To use gscdump.com, run `gscdump auth login --mode cloud`."
152
152
  ].join("\n"));
153
153
  process.exit(1);
154
154
  }
@@ -9,7 +9,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
9
9
  const MCP_NO_AUTH_MESSAGE = [
10
10
  "gscdump has no Google authentication.",
11
11
  "Run `gscdump auth login` in a terminal, then call this tool again.",
12
- "For cloud mode, set GSCDUMP_API_KEY to a user API key from your gscdump.com settings.",
12
+ "For cloud mode, run `gscdump auth login --mode cloud` in a terminal.",
13
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.",
14
14
  "If you set environment variables, set them in the MCP server configuration and restart the MCP client.",
15
15
  "If the gscdump command is missing, run `npm install -g @gscdump/cli`."
@@ -8,7 +8,11 @@ const pollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal(
8
8
  status: z.literal("complete"),
9
9
  tokens: accessSchema.extend({ refreshToken: z.string().min(1) })
10
10
  })]);
11
- async function requestJson(request, route, init = {}) {
11
+ const cloudPollSchema = z.discriminatedUnion("status", [z.object({ status: z.literal("pending") }), z.object({
12
+ status: z.literal("complete"),
13
+ sessionId: z.string().regex(/^[a-f0-9]{64}$/)
14
+ })]);
15
+ async function requestJson(request, route, init = {}, failureMessage) {
12
16
  const response = await request(`${ORIGIN}/api/cli/auth/${route}`, {
13
17
  ...init,
14
18
  redirect: "error",
@@ -16,7 +20,7 @@ async function requestJson(request, route, init = {}) {
16
20
  });
17
21
  if (!response.ok) {
18
22
  if (response.status === 429 || response.status >= 500) throw new Error("Google authorization is temporarily unavailable. Try again later.");
19
- throw new Error("Google authorization failed. Run `gscdump auth login --mode local --force` to reconnect.");
23
+ throw new Error(failureMessage ?? "Google authorization failed. Run `gscdump auth login --mode local --force` to reconnect.");
20
24
  }
21
25
  return response.json();
22
26
  }
@@ -52,4 +56,20 @@ async function loginWithPlatform(deps) {
52
56
  }
53
57
  throw new Error("Authorization expired. Run `gscdump auth login` to try again.");
54
58
  }
55
- export { loginWithPlatform, refreshWithPlatform };
59
+ const CLOUD_SESSION_FAILURE = "Cloud authorization failed. Run `gscdump auth login --mode cloud` to try again.";
60
+ async function loginWithCloudSession(deps) {
61
+ const init = z.object({
62
+ code: z.string().regex(/^S-[A-F0-9]{20}$/),
63
+ expiresIn: z.number().int().positive().max(600)
64
+ }).parse(await requestJson(deps.request, "init?mode=cloud", { method: "POST" }, CLOUD_SESSION_FAILURE));
65
+ const deadline = deps.now() + init.expiresIn * 1e3;
66
+ const url = `${ORIGIN}/app/cli/auth?code=${init.code}`;
67
+ await deps.authorize(url);
68
+ while (deps.now() < deadline) {
69
+ const result = cloudPollSchema.parse(await requestJson(deps.request, `poll?code=${init.code}`, {}, CLOUD_SESSION_FAILURE));
70
+ if (result.status === "complete") return result.sessionId;
71
+ await deps.wait(2e3);
72
+ }
73
+ throw new Error("Authorization expired. Run `gscdump auth login --mode cloud` to try again.");
74
+ }
75
+ export { loginWithCloudSession, loginWithPlatform, refreshWithPlatform };
@@ -1,4 +1,4 @@
1
- import { getCloudAccount, parseAuthentication, resolveAuthentication } from "./auth-state.mjs";
1
+ import { cloudCredential, getCloudAccount, 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";
@@ -83,7 +83,7 @@ async function resolveHostedSite(args, command) {
83
83
  return {
84
84
  client: createGscdumpV1Client({
85
85
  apiRoot: authentication.apiRoot,
86
- credential: authentication.apiKey
86
+ credential: cloudCredential(authentication)
87
87
  }),
88
88
  site: match.site
89
89
  };
@@ -114,7 +114,7 @@ function nextStep(error, status, mode) {
114
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
115
  }
116
116
  if (status === 401 || error.kind === "auth-expired") {
117
- if (mode === "cloud") return "Set or refresh GSCDUMP_API_KEY to a user API key from your gscdump.com settings.";
117
+ if (mode === "cloud") return "Run `gscdump auth login --mode cloud` in a terminal, then restart the MCP client.";
118
118
  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
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.";
120
120
  return "Run `gscdump auth login` in a terminal to connect again.";
@@ -128,7 +128,9 @@ function describeApiError(error, mode = "local") {
128
128
  if (status === void 0) return null;
129
129
  const classified = classifyGoogleError(error);
130
130
  const message = googleMessage(error) ?? classified.message;
131
- return `API error ${status}: ${(message === HOSTED_KEY_REJECTED ? HOSTED_KEY_REJECTED_REASON : message).replace(/\s+/g, " ").trim().replace(/\.$/, "")}. ${nextStep(classified, status, mode)}`.trim();
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();
132
134
  }
133
135
  function googleMessage(error) {
134
136
  const bodies = [error?.data, error?.response?.data];
package/dist/package.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var version = "4.1.0";
1
+ var version = "4.2.0";
2
2
  export { version };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "4.1.0",
4
+ "version": "4.2.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.1.0",
48
- "@gscdump/contracts": "^4.1.0",
49
- "@gscdump/engine": "^4.1.0",
50
- "@gscdump/engine-gsc-api": "^4.1.0",
51
- "@gscdump/sdk": "^4.1.0",
47
+ "@gscdump/analysis": "^4.2.0",
48
+ "@gscdump/contracts": "^4.2.0",
49
+ "@gscdump/engine": "^4.2.0",
50
+ "@gscdump/engine-gsc-api": "^4.2.0",
51
+ "@gscdump/sdk": "^4.2.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.1.0",
56
+ "gscdump": "^4.2.0",
57
57
  "ofetch": "^1.5.1",
58
58
  "open": "^11.0.4",
59
59
  "proper-lockfile": "^4.1.2",
@@ -38,7 +38,7 @@ In cloud mode, `hostedSync` lists each gscdump.com Site with `syncStatus` and `s
38
38
 
39
39
  | Mode | Credentials | Query path |
40
40
  | --- | --- | --- |
41
- | `cloud` | gscdump user API key | `https://gscdump.com/api` uses saved Search Engine connections |
41
+ | `cloud` | Browser-approved CLI session; optional user API key for automation | `https://gscdump.com/api` uses saved Search Engine connections |
42
42
  | `local` | Google OAuth/service account or Bing API key/OAuth | Calls the Search Engine directly |
43
43
 
44
44
  `--mode cloud|local` overrides one invocation. `GSCDUMP_AUTH_MODE` also overrides the saved mode.
@@ -47,28 +47,32 @@ If no mode is saved, `GSCDUMP_API_KEY` selects cloud mode.
47
47
  With neither source, the CLI defaults to local mode.
48
48
  When a saved mode exists, it remains selected unless an explicit override applies.
49
49
  Never switch modes to bypass an authentication failure.
50
- `GSCDUMP_API_ROOT` defaults to `https://gscdump.com/api`. Supply the API key explicitly when changing a saved API root.
50
+ `GSCDUMP_API_ROOT` defaults to `https://gscdump.com/api`. Browser login uses this root. Supply an API key explicitly for a custom root.
51
51
 
52
52
  ```sh
53
- # The user supplies a user API key from gscdump.com settings.
53
+ # Open gscdump.com in a browser and approve the CLI session.
54
54
  gscdump auth login --mode cloud
55
55
  gscdump bing sites --json
56
56
  gscdump bing login --site s_SITE_ID
57
57
  gscdump bing dump --site s_SITE_ID --out ./bing-export --format json
58
58
 
59
- # Local Google and Bing credentials stay separate.
59
+ # Local Google login uses gscdump.com for OAuth by default.
60
+ # Google data requests go directly from the CLI to Google.
60
61
  gscdump auth login --mode local
61
62
  gscdump bing login --mode local
62
63
  gscdump bing dump --site https://example.com/ --out ./bing-export
63
64
  ```
64
65
 
66
+ For a fully local Google login, set `GSC_CLIENT_ID` and `GSC_CLIENT_SECRET` before `auth login --mode local`.
67
+ The CLI then opens Google directly. A service account can use `--service-account` instead.
68
+
65
69
  Hosted Bing login opens the existing connection flow on gscdump.com.
66
70
  Local Bing login uses `BING_API_KEY` or a password prompt.
67
71
  Local `--oauth` uses `BING_CLIENT_ID`, `BING_CLIENT_SECRET`, and a registered loopback callback.
68
72
  The default callback is `http://127.0.0.1:53683/oauth/bing`. `BING_ACCESS_TOKEN` accepts an existing OAuth access token.
69
73
  After cloud Bing login opens a browser, use `bing status --site s_SITE_ID` to confirm the connection.
70
74
 
71
- `auth logout` removes the saved mode and saved Google and Bing credentials.
75
+ `auth logout` revokes a saved cloud CLI session and removes saved mode and Google and Bing credentials.
72
76
  `bing logout --mode local` removes only saved Bing credentials. Environment credentials remain active until unset.
73
77
 
74
78
  Hosted Bing commands use the API's plan and preview access rules.
@@ -154,6 +158,8 @@ If local Google credentials are missing, use one of these paths:
154
158
 
155
159
  Default local login opens gscdump.com for free Google login and token refresh.
156
160
  Data queries call Google directly. No Google Cloud project or hosted activation is required.
161
+ For a fully local connection, set `GSC_CLIENT_ID` and `GSC_CLIENT_SECRET` before login.
162
+ The CLI then opens Google directly and uses a loopback callback.
157
163
  Use `gscdump auth login --mode local --no-browser` when a browser runs on another host.
158
164
  The default grant is read-only Search Console access.
159
165
  For Google write operations, use your own OAuth client with the required scopes.