@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.
- package/README.md +1 -1
- package/dist/auth-scopes.mjs +4 -4
- package/dist/auth-state.mjs +84 -41
- package/dist/auth.mjs +35 -59
- package/dist/bing-hosted.mjs +4 -4
- package/dist/command-meta.mjs +2 -2
- package/dist/commands/auth.mjs +71 -86
- package/dist/commands/bing.mjs +10 -10
- package/dist/commands/doctor.mjs +64 -14
- package/dist/commands/init.mjs +31 -15
- package/dist/commands/mcp.mjs +13 -7
- package/dist/commands/query.mjs +44 -1
- package/dist/commands/sites.mjs +32 -19
- package/dist/context.mjs +4 -6
- package/dist/dump-bing.mjs +1 -1
- package/dist/error-handler.mjs +1 -1
- package/dist/hosted-auth.mjs +10 -51
- package/dist/hosted-query.mjs +31 -0
- package/dist/hosted-site.mjs +6 -6
- package/dist/mcp/errors.mjs +4 -14
- package/dist/mcp/handlers/diagnostics.mjs +0 -31
- package/dist/package.mjs +1 -1
- package/dist/route.mjs +10 -4
- package/dist/runtime.d.mts +1 -1
- package/dist/sync-plan.mjs +1 -1
- package/package.json +7 -7
- package/skills/gscdump/SKILL.md +77 -62
- package/dist/cloud-google.mjs +0 -99
package/dist/commands/mcp.mjs
CHANGED
|
@@ -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
|
|
11
|
-
"
|
|
12
|
-
"
|
|
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
|
-
|
|
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 };
|
package/dist/commands/query.mjs
CHANGED
|
@@ -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: "
|
|
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 });
|
package/dist/commands/sites.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sitesCommandMeta } from "../command-meta.mjs";
|
|
2
|
-
import { formatHostedSync,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
17
|
+
const auth = needsAuth ? await resolveAuth({
|
|
19
18
|
interactive,
|
|
20
19
|
config,
|
|
21
20
|
byok
|
|
22
21
|
}) : null;
|
|
23
|
-
const client =
|
|
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,
|
package/dist/dump-bing.mjs
CHANGED
|
@@ -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 === "
|
|
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));
|
package/dist/error-handler.mjs
CHANGED
|
@@ -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
|
|
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) {
|
package/dist/hosted-auth.mjs
CHANGED
|
@@ -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
|
-
|
|
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("
|
|
23
|
-
throw new Error(
|
|
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
|
|
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" }
|
|
24
|
+
}).parse(await requestJson(deps.request, "init?mode=cloud", { method: "POST" }));
|
|
65
25
|
const deadline = deps.now() + init.expiresIn * 1e3;
|
|
66
|
-
|
|
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 =
|
|
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
|
|
32
|
+
throw new Error("Authorization expired. Run `gscdump auth login --mode hosted` to try again.");
|
|
74
33
|
}
|
|
75
|
-
export {
|
|
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 };
|
package/dist/hosted-site.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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: "
|
|
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 !== "
|
|
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
|
|
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:
|
|
86
|
+
credential: hostedCredential(authentication)
|
|
87
87
|
}),
|
|
88
88
|
site: match.site
|
|
89
89
|
};
|
package/dist/mcp/errors.mjs
CHANGED
|
@@ -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) ? "
|
|
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 === "
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
package/dist/runtime.d.mts
CHANGED
|
@@ -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?: '
|
|
4
|
+
authModeOverride?: 'local' | 'hosted';
|
|
5
5
|
activeProfileOverride: string | null;
|
|
6
6
|
colorEnabled: boolean;
|
|
7
7
|
configDir: string;
|
package/dist/sync-plan.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
48
|
-
"@gscdump/contracts": "^4.
|
|
49
|
-
"@gscdump/engine": "^4.
|
|
50
|
-
"@gscdump/engine-gsc-api": "^4.
|
|
51
|
-
"@gscdump/sdk": "^4.
|
|
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.
|
|
56
|
+
"gscdump": "^4.3.0",
|
|
57
57
|
"ofetch": "^1.5.1",
|
|
58
58
|
"open": "^11.0.4",
|
|
59
59
|
"proper-lockfile": "^4.1.2",
|