@gscdump/cli 0.0.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +114 -0
  2. package/dist/index.mjs +56 -60
  3. package/package.json +25 -10
package/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @gscdump/cli
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@gscdump/cli?color=yellow)](https://npmjs.com/package/@gscdump/cli)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@gscdump/cli?color=yellow)](https://npm.chart.dev/@gscdump/cli)
5
+ [![license](https://img.shields.io/github/license/harlan-zw/gscdump?color=yellow)](https://github.com/harlan-zw/gscdump/blob/main/LICENSE)
6
+
7
+ > CLI for Google Search Console - dump, sync, compare, analyze, and run MCP server.
8
+
9
+ ## Features
10
+
11
+ - **Data Export** - Dump analytics to stdout, file, or SQLite database
12
+ - **Period Comparison** - Compare metrics across time periods
13
+ - **SEO Analysis** - Striking distance, movers & shakers, decay detection
14
+ - **Indexing Tools** - Check status, request indexing, batch operations
15
+ - **MCP Server** - Let AI agents query your search data directly
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ npm install -g @gscdump/cli
21
+ # or run with npx
22
+ npx @gscdump/cli
23
+ ```
24
+
25
+ ## Quick Start
26
+
27
+ ```bash
28
+ # First-run setup (choose cloud or local auth)
29
+ gscdump init
30
+
31
+ # List your sites
32
+ gscdump sites
33
+
34
+ # Dump last 7 days to stdout
35
+ gscdump dump --site https://example.com --period 7d
36
+
37
+ # Sync to SQLite database
38
+ gscdump sync --site https://example.com --db ./gsc.db
39
+
40
+ # Compare periods
41
+ gscdump compare --site https://example.com --period 28d
42
+
43
+ # Run SEO analysis
44
+ gscdump analyze striking-distance --site https://example.com
45
+ ```
46
+
47
+ ## Commands
48
+
49
+ | Command | Description |
50
+ |---------|-------------|
51
+ | `init` | First-run setup (choose cloud/local mode) |
52
+ | `auth` | OAuth2 login with Google |
53
+ | `sites` | List GSC properties |
54
+ | `dump` | Export analytics to stdout/file |
55
+ | `sync` | Persist to SQLite database |
56
+ | `compare` | Period-over-period comparison |
57
+ | `analyze` | Run SEO analysis (striking-distance, movers, decay, etc.) |
58
+ | `sitemaps` | List/manage sitemaps for a site |
59
+ | `index` | URL indexing (status, inspect, request) |
60
+ | `inspect` | Quick URL inspection |
61
+ | `config` | Manage CLI configuration |
62
+ | `mcp` | Start MCP server for AI assistants |
63
+
64
+ ## MCP Server
65
+
66
+ Start the MCP server for AI assistants:
67
+
68
+ ```bash
69
+ gscdump mcp
70
+ ```
71
+
72
+ Or add to your Claude config (`~/.claude.json` or VS Code settings):
73
+
74
+ ```json
75
+ {
76
+ "mcpServers": {
77
+ "gscdump": {
78
+ "command": "npx",
79
+ "args": ["@gscdump/mcp"]
80
+ }
81
+ }
82
+ }
83
+ ```
84
+
85
+ Then ask Claude:
86
+ - "What pages lost traffic this week?"
87
+ - "Find keywords in striking distance (position 4-20)"
88
+ - "Which queries have keyword cannibalization?"
89
+ - "Compare this month vs last month"
90
+
91
+ ## Auth Setup
92
+
93
+ **Cloud mode** (recommended):
94
+ ```bash
95
+ gscdump init # Select "cloud"
96
+ ```
97
+ Easy setup via cloud.gscdump.com - no API keys needed.
98
+
99
+ **Local mode** (bring your own credentials):
100
+ 1. Create a Google Cloud project
101
+ 2. Enable "Search Console API" and "Web Search Indexing API"
102
+ 3. Create OAuth2 credentials (Desktop app)
103
+ 4. Run `gscdump init` and select "local"
104
+
105
+ ## Related Packages
106
+
107
+ - [`gscdump`](../gscdump) - Core library
108
+ - [`@gscdump/mcp`](../mcp) - MCP server
109
+ - [`@gscdump/db`](../db) - SQLite persistence
110
+ - [`@gscdump/query`](../query) - Unified data provider
111
+
112
+ ## License
113
+
114
+ [MIT](../../LICENSE)
package/dist/index.mjs CHANGED
@@ -5,7 +5,7 @@ import process from "node:process";
5
5
  import { defineCommand, runMain } from "citty";
6
6
  import path from "node:path";
7
7
  import { batchInspectUrls, batchRequestIndexingForPaths, createGscDb, getIndexingStats, getLastSyncedDate, getSiteByProperty, setupSchema, sitePathDateAnalytics, syncCountries, syncDevices, syncKeywordPaths, syncKeywords, syncPages, syncSites, updateLastSynced } from "@gscdump/db";
8
- import { createProvider, fetchCannibalizationAnalysis, fetchDecayAnalysis, fetchMoversAnalysis, fetchOpportunityAnalysis, fetchStrikingDistanceAnalysis, fetchZeroClickAnalysis } from "@gscdump/query";
8
+ import { createProvider, daysAgo, fetchCannibalizationAnalysis, fetchDecayAnalysis, fetchMoversAnalysis, fetchOpportunityAnalysis, fetchStrikingDistanceAnalysis, fetchZeroClickAnalysis } from "@gscdump/query";
9
9
  import dayjs from "dayjs";
10
10
  import betterSqlite3 from "db0/connectors/better-sqlite3";
11
11
  import fs from "node:fs/promises";
@@ -14,7 +14,7 @@ import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/pro
14
14
  import { OAuth2Client } from "google-auth-library";
15
15
  import os from "node:os";
16
16
  import { consola } from "consola";
17
- import { deleteSitemap, fetchSitemap, fetchSitemaps, fetchSites, formatErrorForCli, googleSearchConsole, submitSitemap, userPeriodRange } from "gscdump";
17
+ import { deleteSitemap, fetchSitemap, fetchSitemaps, fetchSites, formatErrorForCli, googleSearchConsole, submitSitemap } from "gscdump";
18
18
  import { createGscMcpServer } from "@gscdump/mcp/server";
19
19
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
20
20
 
@@ -423,6 +423,17 @@ async function getAuth(opts = {}) {
423
423
 
424
424
  //#endregion
425
425
  //#region src/commands/analyze.ts
426
+ function getProviderForSource$2(auth, db, source) {
427
+ if (source === "api") return createProvider({ auth });
428
+ if (source === "db") {
429
+ if (!db) throw new Error("Database required for db source");
430
+ return createProvider({ db });
431
+ }
432
+ return db ? createProvider({
433
+ auth,
434
+ db
435
+ }) : createProvider({ auth });
436
+ }
426
437
  const ANALYSIS_TYPES = [
427
438
  "striking-distance",
428
439
  "opportunity",
@@ -532,13 +543,7 @@ const analyzeCommand = defineCommand({
532
543
  end: prevEndDate.format("YYYY-MM-DD")
533
544
  }
534
545
  };
535
- const provider = await createProvider({
536
- auth,
537
- db: dbPath ? createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db : null,
538
- source: args.source,
539
- siteUrls: [siteArg],
540
- range
541
- });
546
+ const provider = getProviderForSource$2(auth, dbPath ? createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db : null, args.source);
542
547
  const limit = Number.parseInt(args.limit, 10) || 20;
543
548
  if (!args.json) {
544
549
  console.log();
@@ -717,6 +722,17 @@ const authCommand = defineCommand({
717
722
 
718
723
  //#endregion
719
724
  //#region src/commands/compare.ts
725
+ function getProviderForSource$1(auth, db, source) {
726
+ if (source === "api") return createProvider({ auth });
727
+ if (source === "db") {
728
+ if (!db) throw new Error("Database required for db source");
729
+ return createProvider({ db });
730
+ }
731
+ return db ? createProvider({
732
+ auth,
733
+ db
734
+ }) : createProvider({ auth });
735
+ }
720
736
  const SEARCH_TYPES = [
721
737
  "web",
722
738
  "image",
@@ -876,13 +892,7 @@ const compareCommand = defineCommand({
876
892
  const searchType = SEARCH_TYPES.includes(args.searchType) ? args.searchType : "web";
877
893
  const effectiveSource = searchType !== "web" ? "api" : args.source || "auto";
878
894
  if (searchType !== "web" && args.source === "db") logger.warn(`Search type '${searchType}' requires API. Ignoring --source db.`);
879
- const provider = await createProvider({
880
- auth,
881
- db: dbPath ? createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db : null,
882
- source: effectiveSource,
883
- siteUrls: [siteArg],
884
- range
885
- });
895
+ const provider = getProviderForSource$1(auth, dbPath ? createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db : null, effectiveSource);
886
896
  if (args.json) {
887
897
  const [dates, pages, keywords] = await Promise.all([
888
898
  provider.getDatesWithComparison(siteArg, range),
@@ -1054,6 +1064,23 @@ const configCommand = defineCommand({
1054
1064
 
1055
1065
  //#endregion
1056
1066
  //#region src/commands/dump.ts
1067
+ /**
1068
+ * Maps CLI source option to provider options
1069
+ * - 'api': API only (no db)
1070
+ * - 'db': DB only (no auth, errors if data missing)
1071
+ * - 'auto': Hybrid (both auth and db, syncs on cache miss)
1072
+ */
1073
+ function getProviderForSource(auth, db, source) {
1074
+ if (source === "api") return createProvider({ auth });
1075
+ if (source === "db") {
1076
+ if (!db) throw new Error("Database required for db source");
1077
+ return createProvider({ db });
1078
+ }
1079
+ return db ? createProvider({
1080
+ auth,
1081
+ db
1082
+ }) : createProvider({ auth });
1083
+ }
1057
1084
  const DUMP_DATA_TYPES = [
1058
1085
  "pages",
1059
1086
  "keywords",
@@ -1252,29 +1279,11 @@ async function interactiveMode(auth, dbPath, source) {
1252
1279
  return;
1253
1280
  }
1254
1281
  console.log();
1255
- const endDate = dayjs().subtract(3, "days").format("YYYY-MM-DD");
1256
- const startDate = dayjs().subtract(period.amount, period.unit).subtract(3, "days").format("YYYY-MM-DD");
1257
- const range = {
1258
- period: {
1259
- start: startDate,
1260
- end: endDate
1261
- },
1262
- prevPeriod: {
1263
- start: dayjs(startDate).subtract(period.amount, period.unit).format("YYYY-MM-DD"),
1264
- end: dayjs(endDate).subtract(period.amount, period.unit).format("YYYY-MM-DD")
1265
- }
1266
- };
1267
1282
  let db = null;
1268
1283
  if (dbPath) {
1269
1284
  if (await fs.access(dbPath).then(() => true).catch(() => false)) db = createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db;
1270
1285
  }
1271
- const provider = await createProvider({
1272
- auth,
1273
- db,
1274
- source: finalSource,
1275
- siteUrls: selectedSites,
1276
- range
1277
- });
1286
+ const provider = getProviderForSource(auth, db, finalSource);
1278
1287
  logger.info(`Using ${provider.source.toUpperCase()} as data source`);
1279
1288
  await runDump(provider, selectedSites, dataTypes, period, format, null);
1280
1289
  }
@@ -1314,19 +1323,6 @@ async function nonInteractiveMode(auth, site, data, periodStr, format, output, d
1314
1323
  }
1315
1324
  normalizedSites.push(match);
1316
1325
  }
1317
- const daysOffset = options.fresh ? 1 : 3;
1318
- const endDate = dayjs().subtract(daysOffset, "days").format("YYYY-MM-DD");
1319
- const startDate = dayjs().subtract(period.amount, period.unit).subtract(daysOffset, "days").format("YYYY-MM-DD");
1320
- const range = {
1321
- period: {
1322
- start: startDate,
1323
- end: endDate
1324
- },
1325
- prevPeriod: {
1326
- start: dayjs(startDate).subtract(period.amount, period.unit).format("YYYY-MM-DD"),
1327
- end: dayjs(endDate).subtract(period.amount, period.unit).format("YYYY-MM-DD")
1328
- }
1329
- };
1330
1326
  let db = null;
1331
1327
  if (dbPath) {
1332
1328
  if (await fs.access(dbPath).then(() => true).catch(() => false)) db = createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db;
@@ -1336,13 +1332,7 @@ async function nonInteractiveMode(auth, site, data, periodStr, format, output, d
1336
1332
  process.exit(1);
1337
1333
  }
1338
1334
  }
1339
- const provider = await createProvider({
1340
- auth,
1341
- db,
1342
- source,
1343
- siteUrls: normalizedSites,
1344
- range
1345
- }).catch(gscErrorHandler);
1335
+ const provider = getProviderForSource(auth, db, source);
1346
1336
  const extras = [];
1347
1337
  if (options.fresh) extras.push("fresh");
1348
1338
  if (options.searchType && options.searchType !== "web") extras.push(options.searchType);
@@ -2158,6 +2148,12 @@ const sitesCommand = defineCommand({
2158
2148
 
2159
2149
  //#endregion
2160
2150
  //#region src/commands/sync.ts
2151
+ function parsePeriodDays(period) {
2152
+ if (period === "max") return 480;
2153
+ if (period.endsWith("y")) return Number.parseInt(period) * 365;
2154
+ if (period.endsWith("m") || period.endsWith("mo")) return Number.parseInt(period) * 30;
2155
+ return Number.parseInt(period.replace("d", ""));
2156
+ }
2161
2157
  async function runSync(client, dbPath, siteArg, period, granular, options) {
2162
2158
  const resolvedPath = path.resolve(dbPath);
2163
2159
  const report = {
@@ -2195,12 +2191,12 @@ async function runSync(client, dbPath, siteArg, period, granular, options) {
2195
2191
  siteId: s.siteId,
2196
2192
  siteUrl: s.property
2197
2193
  }));
2198
- const periodRange = userPeriodRange(period);
2194
+ const periodDays = parsePeriodDays(period);
2199
2195
  const daysOffset = options.fresh ? 1 : 3;
2200
- const adjustedEndDate = dayjs(periodRange.period.endDate).subtract(daysOffset, "day").format("YYYY-MM-DD");
2201
- const baseStartDate = dayjs(periodRange.period.startDate).subtract(daysOffset, "day").format("YYYY-MM-DD");
2202
- const adjustedPrevEndDate = dayjs(periodRange.prevPeriod.endDate).subtract(daysOffset, "day").format("YYYY-MM-DD");
2203
- const adjustedPrevStartDate = dayjs(periodRange.prevPeriod.startDate).subtract(daysOffset, "day").format("YYYY-MM-DD");
2196
+ const adjustedEndDate = daysAgo(daysOffset);
2197
+ const baseStartDate = daysAgo(periodDays + daysOffset);
2198
+ const adjustedPrevEndDate = daysAgo(periodDays + daysOffset + 1);
2199
+ const adjustedPrevStartDate = daysAgo(periodDays * 2 + daysOffset);
2204
2200
  const getRangeForSite = async (siteId) => {
2205
2201
  let startDate = baseStartDate;
2206
2202
  if (options.since) startDate = options.since;
package/package.json CHANGED
@@ -1,19 +1,34 @@
1
1
  {
2
2
  "name": "@gscdump/cli",
3
3
  "type": "module",
4
- "version": "0.0.1",
5
- "description": "CLI for GSCDump - Google Search Console data extraction",
4
+ "version": "0.1.2",
5
+ "description": "CLI for Google Search Console - dump, sync, compare, analyze, and run MCP server",
6
6
  "author": {
7
7
  "name": "Harlan Wilton",
8
8
  "email": "harlan@harlanzw.com",
9
9
  "url": "https://harlanzw.com/"
10
10
  },
11
11
  "license": "MIT",
12
+ "funding": "https://github.com/sponsors/harlan-zw",
13
+ "homepage": "https://github.com/harlan-zw/gscdump/tree/main/packages/cli#readme",
12
14
  "repository": {
13
15
  "type": "git",
14
16
  "url": "git+https://github.com/harlan-zw/gscdump.git",
15
17
  "directory": "packages/cli"
16
18
  },
19
+ "bugs": {
20
+ "url": "https://github.com/harlan-zw/gscdump/issues"
21
+ },
22
+ "keywords": [
23
+ "google-search-console",
24
+ "gsc",
25
+ "seo",
26
+ "cli",
27
+ "mcp",
28
+ "search-analytics",
29
+ "data-export",
30
+ "sqlite"
31
+ ],
17
32
  "bin": {
18
33
  "gscdump": "./dist/index.mjs"
19
34
  },
@@ -22,20 +37,20 @@
22
37
  ],
23
38
  "dependencies": {
24
39
  "@clack/prompts": "^0.11.0",
25
- "@modelcontextprotocol/sdk": "^1.25.0",
40
+ "@modelcontextprotocol/sdk": "^1.25.2",
26
41
  "citty": "^0.1.6",
27
42
  "consola": "^3.4.2",
28
- "dayjs": "^1.11.18",
43
+ "dayjs": "^1.11.19",
29
44
  "db0": "^0.3.4",
30
45
  "google-auth-library": "^10.5.0",
31
- "open": "^10.1.0",
32
- "@gscdump/query": "0.0.1",
33
- "@gscdump/db": "0.0.1",
34
- "gscdump": "0.0.1",
35
- "@gscdump/mcp": "0.0.1"
46
+ "open": "^11.0.0",
47
+ "@gscdump/db": "0.1.2",
48
+ "@gscdump/mcp": "0.1.2",
49
+ "@gscdump/query": "0.1.2",
50
+ "gscdump": "0.1.2"
36
51
  },
37
52
  "devDependencies": {
38
- "vitest": "^4.0.16"
53
+ "vitest": "^4.0.17"
39
54
  },
40
55
  "scripts": {
41
56
  "build": "obuild",