@gscdump/cli 0.1.3 → 0.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/dist/index.mjs CHANGED
@@ -1,22 +1,19 @@
1
1
  #!/usr/bin/env node
2
- import { t as eq } from "./_chunks/libs/drizzle-orm.mjs";
3
2
  import "node:module";
4
3
  import process from "node:process";
5
4
  import { defineCommand, runMain } from "citty";
6
- import path from "node:path";
7
- import { batchInspectUrls, batchRequestIndexingForPaths, createGscDb, getIndexingStats, getLastSyncedDate, getSiteByProperty, setupSchema, sitePathDateAnalytics, syncCountries, syncDevices, syncKeywordPaths, syncKeywords, syncPages, syncSites, updateLastSynced } from "@gscdump/db";
8
- import { createProvider, daysAgo, fetchCannibalizationAnalysis, fetchDecayAnalysis, fetchMoversAnalysis, fetchOpportunityAnalysis, fetchStrikingDistanceAnalysis, fetchZeroClickAnalysis } from "@gscdump/query";
9
- import dayjs from "dayjs";
10
- import betterSqlite3 from "db0/connectors/better-sqlite3";
11
5
  import fs from "node:fs/promises";
12
6
  import { createServer } from "node:http";
13
- import { cancel, confirm, isCancel, multiselect, select, text } from "@clack/prompts";
7
+ import path from "node:path";
8
+ import { cancel, isCancel, multiselect, select, text } from "@clack/prompts";
14
9
  import { OAuth2Client } from "google-auth-library";
15
10
  import os from "node:os";
16
11
  import { consola } from "consola";
17
- import { deleteSitemap, fetchSitemap, fetchSitemaps, fetchSites, formatErrorForCli, googleSearchConsole, submitSitemap } from "gscdump";
18
- import { createGscMcpServer } from "@gscdump/mcp/server";
12
+ import { batchInspectUrls, batchRequestIndexing, deleteSitemap, fetchSitemap, fetchSitemaps, fetchSites, fetchSitesWithSitemaps, formatErrorForCli, getIndexingMetadata, googleSearchConsole, inspectUrl, requestIndexing, submitSitemap } from "gscdump";
13
+ import { between, country, date, device, gsc, page, query, searchAppearance } from "gscdump/query";
19
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
16
+ import { z } from "zod";
20
17
 
21
18
  //#region rolldown:runtime
22
19
  var __defProp = Object.defineProperty;
@@ -103,22 +100,6 @@ function progressBar(current, total, label, width = 30) {
103
100
  function clearLine() {
104
101
  process.stdout.write("\r\x1B[K");
105
102
  }
106
- function parsePeriod(periodStr) {
107
- const match = periodStr.match(/^(\d+)([dmy])$/i);
108
- if (!match) return null;
109
- const amount = Number.parseInt(match[1], 10);
110
- const unit = {
111
- d: "days",
112
- m: "months",
113
- y: "years"
114
- }[match[2].toLowerCase()];
115
- if (!unit || amount <= 0) return null;
116
- if (amount > 450 && unit === "days") return null;
117
- return {
118
- amount,
119
- unit
120
- };
121
- }
122
103
  function toCSV(data, columns) {
123
104
  return [columns.join(","), ...data.map((row) => columns.map((col) => {
124
105
  const val = row[col];
@@ -421,227 +402,6 @@ async function getAuth(opts = {}) {
421
402
  return authenticate(await getAuthCredentials(interactive), interactive);
422
403
  }
423
404
 
424
- //#endregion
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
- }
437
- const ANALYSIS_TYPES = [
438
- "striking-distance",
439
- "opportunity",
440
- "movers",
441
- "decay",
442
- "cannibalization",
443
- "zero-click"
444
- ];
445
- const analyzeCommand = defineCommand({
446
- meta: {
447
- name: "analyze",
448
- description: "Run SEO analysis on site data"
449
- },
450
- args: {
451
- type: {
452
- type: "positional",
453
- required: true,
454
- description: `Analysis type: ${ANALYSIS_TYPES.join(", ")}`
455
- },
456
- site: {
457
- type: "string",
458
- alias: "s",
459
- description: "Site URL (e.g., sc-domain:example.com)"
460
- },
461
- period: {
462
- type: "string",
463
- alias: "p",
464
- default: "28d",
465
- description: "Period to analyze (e.g., 7d, 28d, 3m)"
466
- },
467
- limit: {
468
- type: "string",
469
- alias: "l",
470
- default: "20",
471
- description: "Number of results to show"
472
- },
473
- json: {
474
- type: "boolean",
475
- default: false,
476
- description: "Output as JSON"
477
- },
478
- db: {
479
- type: "string",
480
- alias: "d",
481
- description: "Path to SQLite database"
482
- },
483
- source: {
484
- type: "string",
485
- default: "auto",
486
- description: "Data source: api, db, auto"
487
- },
488
- minPosition: {
489
- type: "string",
490
- description: "Minimum position (striking-distance)"
491
- },
492
- maxPosition: {
493
- type: "string",
494
- description: "Maximum position (striking-distance, zero-click)"
495
- },
496
- minImpressions: {
497
- type: "string",
498
- description: "Minimum impressions"
499
- },
500
- minClicks: {
501
- type: "string",
502
- description: "Minimum clicks (decay)"
503
- },
504
- threshold: {
505
- type: "string",
506
- description: "Threshold percentage 0-1 (decay, movers)"
507
- }
508
- },
509
- async run({ args }) {
510
- const analysisType = args.type;
511
- if (!ANALYSIS_TYPES.includes(analysisType)) {
512
- logger.error(`Unknown analysis type: ${analysisType}`);
513
- logger.info(`Available: ${ANALYSIS_TYPES.join(", ")}`);
514
- process.exit(1);
515
- }
516
- const config = await loadConfig();
517
- const siteArg = args.site || config.defaultSite;
518
- if (!siteArg) {
519
- logger.error("Site required. Use --site or set defaultSite in config.");
520
- process.exit(1);
521
- }
522
- const dbPath = args.db || config.defaultDb;
523
- const auth = await getAuth({
524
- interactive: false,
525
- config
526
- });
527
- const period = parsePeriod(args.period);
528
- if (!period) {
529
- logger.error(`Invalid period: ${args.period}`);
530
- process.exit(1);
531
- }
532
- const endDate = dayjs().subtract(3, "day");
533
- const startDate = endDate.subtract(period.amount, period.unit);
534
- const prevEndDate = startDate.subtract(1, "day");
535
- const prevStartDate = prevEndDate.subtract(period.amount, period.unit);
536
- const range = {
537
- period: {
538
- start: startDate.format("YYYY-MM-DD"),
539
- end: endDate.format("YYYY-MM-DD")
540
- },
541
- prevPeriod: {
542
- start: prevStartDate.format("YYYY-MM-DD"),
543
- end: prevEndDate.format("YYYY-MM-DD")
544
- }
545
- };
546
- const provider = getProviderForSource$2(auth, dbPath ? createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db : null, args.source);
547
- const limit = Number.parseInt(args.limit, 10) || 20;
548
- if (!args.json) {
549
- console.log();
550
- console.log(` \x1B[1m${siteArg}\x1B[0m`);
551
- console.log(` \x1B[90mSource: ${provider.source}\x1B[0m`);
552
- console.log(` \x1B[90mPeriod: ${range.period.start} → ${range.period.end}\x1B[0m`);
553
- console.log(` \x1B[90mAnalysis: ${analysisType}\x1B[0m`);
554
- console.log();
555
- }
556
- let results;
557
- switch (analysisType) {
558
- case "striking-distance":
559
- results = await fetchStrikingDistanceAnalysis(provider, siteArg, range, {
560
- minPosition: args.minPosition ? Number(args.minPosition) : void 0,
561
- maxPosition: args.maxPosition ? Number(args.maxPosition) : void 0,
562
- minImpressions: args.minImpressions ? Number(args.minImpressions) : void 0
563
- }).catch(gscErrorHandler);
564
- break;
565
- case "opportunity":
566
- results = await fetchOpportunityAnalysis(provider, siteArg, range, { minImpressions: args.minImpressions ? Number(args.minImpressions) : void 0 }).catch(gscErrorHandler);
567
- break;
568
- case "movers":
569
- results = await fetchMoversAnalysis(provider, siteArg, range, {
570
- changeThreshold: args.threshold ? Number(args.threshold) : void 0,
571
- minImpressions: args.minImpressions ? Number(args.minImpressions) : void 0
572
- }).catch(gscErrorHandler);
573
- break;
574
- case "decay":
575
- results = await fetchDecayAnalysis(provider, siteArg, range, {
576
- minPreviousClicks: args.minClicks ? Number(args.minClicks) : void 0,
577
- threshold: args.threshold ? Number(args.threshold) : void 0
578
- }).catch(gscErrorHandler);
579
- break;
580
- case "cannibalization":
581
- results = await fetchCannibalizationAnalysis(provider, siteArg, range, { minImpressions: args.minImpressions ? Number(args.minImpressions) : void 0 }).catch(gscErrorHandler);
582
- break;
583
- case "zero-click":
584
- results = await fetchZeroClickAnalysis(provider, siteArg, range, {
585
- minImpressions: args.minImpressions ? Number(args.minImpressions) : void 0,
586
- maxPosition: args.maxPosition ? Number(args.maxPosition) : void 0
587
- }).catch(gscErrorHandler);
588
- break;
589
- }
590
- if (args.json) {
591
- console.log(JSON.stringify({
592
- site: siteArg,
593
- source: provider.source,
594
- analysis: analysisType,
595
- range,
596
- results
597
- }, null, 2));
598
- return;
599
- }
600
- const arr = Array.isArray(results) ? results : [];
601
- const display = arr.slice(0, limit);
602
- if (display.length === 0) {
603
- logger.warn("No results found");
604
- return;
605
- }
606
- console.log(` \x1B[1mResults\x1B[0m \x1B[90m(${arr.length} total, showing ${display.length})\x1B[0m`);
607
- for (const item of display) formatResultItem(analysisType, item);
608
- console.log();
609
- logger.success("Analysis complete");
610
- }
611
- });
612
- function formatResultItem(type, item) {
613
- const keyword = item.keyword || item.query || "";
614
- const page = item.page || "";
615
- switch (type) {
616
- case "striking-distance":
617
- console.log(` ${keyword.slice(0, 40).padEnd(40)} pos ${String(item.position || 0).padStart(5)} ${formatNumber$1(item.impressions)} impr ${formatNumber$1(item.potentialClicks)} potential`);
618
- break;
619
- case "opportunity":
620
- console.log(` ${keyword.slice(0, 40).padEnd(40)} score ${String((item.score || 0).toFixed(1)).padStart(6)} ${formatNumber$1(item.impressions)} impr`);
621
- break;
622
- case "movers":
623
- console.log(` ${keyword.slice(0, 40).padEnd(40)} ${formatChange(item.clicksChange)} clicks ${formatChange(item.positionChange)} pos`);
624
- break;
625
- case "decay":
626
- console.log(` ${page.replace(/^https?:\/\/[^/]+/, "").slice(0, 50).padEnd(50)} -${formatNumber$1(item.lostClicks)} clicks (${((item.declinePercent || 0) * 100).toFixed(0)}% decline)`);
627
- break;
628
- case "cannibalization":
629
- console.log(` ${keyword.slice(0, 40).padEnd(40)} ${item.pageCount} pages ${formatNumber$1(item.clicks)} clicks spread ${item.positionSpread}`);
630
- break;
631
- case "zero-click":
632
- console.log(` ${keyword.slice(0, 40).padEnd(40)} pos ${String(item.position || 0).padStart(5)} ${formatNumber$1(item.impressions)} impr ${((item.ctr || 0) * 100).toFixed(2)}% ctr`);
633
- break;
634
- }
635
- }
636
- function formatNumber$1(val) {
637
- if (val === void 0 || val === null) return "-";
638
- return val.toLocaleString();
639
- }
640
- function formatChange(val) {
641
- if (val === void 0 || val === null || !Number.isFinite(val)) return "\x1B[90m-\x1B[0m";
642
- return `${val > 0 ? "\x1B[32m" : val < 0 ? "\x1B[31m" : "\x1B[90m"}${val > 0 ? "+" : ""}${val.toFixed(1)}%\x1B[0m`;
643
- }
644
-
645
405
  //#endregion
646
406
  //#region src/commands/auth.ts
647
407
  const statusCommand = defineCommand({
@@ -720,255 +480,6 @@ const authCommand = defineCommand({
720
480
  }
721
481
  });
722
482
 
723
- //#endregion
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
- }
736
- const SEARCH_TYPES = [
737
- "web",
738
- "image",
739
- "video",
740
- "news",
741
- "discover",
742
- "googleNews"
743
- ];
744
- function formatPercent(val) {
745
- if (val === void 0 || val === null || !Number.isFinite(val)) return "\x1B[90m-\x1B[0m";
746
- return `${val > 0 ? "\x1B[32m" : val < 0 ? "\x1B[31m" : "\x1B[90m"}${val > 0 ? "+" : ""}${val.toFixed(1)}%\x1B[0m`;
747
- }
748
- function formatNumber(val) {
749
- if (val === void 0 || val === null) return "-";
750
- return val.toLocaleString();
751
- }
752
- const compareCommand = defineCommand({
753
- meta: {
754
- name: "compare",
755
- description: "Compare site performance across periods"
756
- },
757
- args: {
758
- site: {
759
- type: "string",
760
- alias: "s",
761
- description: "Site URL (e.g., sc-domain:example.com)"
762
- },
763
- period: {
764
- type: "string",
765
- alias: "p",
766
- default: "7d",
767
- description: "Period to compare (e.g., 7d, 30d, 3m)"
768
- },
769
- from: {
770
- type: "string",
771
- description: "Custom start date (YYYY-MM-DD)"
772
- },
773
- to: {
774
- type: "string",
775
- description: "Custom end date (YYYY-MM-DD)"
776
- },
777
- vs: {
778
- type: "string",
779
- description: "Comparison period start date (YYYY-MM-DD)"
780
- },
781
- json: {
782
- type: "boolean",
783
- default: false,
784
- description: "Output as JSON"
785
- },
786
- type: {
787
- type: "string",
788
- alias: "t",
789
- default: "summary",
790
- description: "Output type: summary, pages, keywords"
791
- },
792
- limit: {
793
- type: "string",
794
- alias: "l",
795
- default: "10",
796
- description: "Number of items to show (for pages/keywords)"
797
- },
798
- fresh: {
799
- type: "boolean",
800
- default: false,
801
- description: "Include fresh/unfinalized data (last 3 days)"
802
- },
803
- searchType: {
804
- type: "string",
805
- default: "web",
806
- description: "Search type: web, image, video, news, discover, googleNews"
807
- },
808
- db: {
809
- type: "string",
810
- alias: "d",
811
- description: "Path to SQLite database for local queries"
812
- },
813
- source: {
814
- type: "string",
815
- default: "auto",
816
- description: "Data source: api, db, auto"
817
- },
818
- quiet: {
819
- type: "boolean",
820
- alias: "q",
821
- default: false,
822
- description: "Suppress output"
823
- }
824
- },
825
- async run({ args }) {
826
- const config = await loadConfig();
827
- const siteArg = args.site || config.defaultSite;
828
- if (!siteArg) {
829
- logger.error("Site required. Use --site or set defaultSite in config.");
830
- process.exit(1);
831
- }
832
- const dbPath = args.db || config.defaultDb;
833
- const periodArg = args.period === "7d" && config.defaultPeriod ? config.defaultPeriod : args.period;
834
- const auth = await getAuth({
835
- interactive: false,
836
- config
837
- });
838
- let range;
839
- if (args.from && args.to) {
840
- const start = dayjs(args.from);
841
- const end = dayjs(args.to);
842
- if (!start.isValid() || !end.isValid()) {
843
- logger.error("Invalid date format. Use YYYY-MM-DD");
844
- process.exit(1);
845
- }
846
- const periodDays = end.diff(start, "day");
847
- let prevStart;
848
- let prevEnd;
849
- if (args.vs) {
850
- prevStart = dayjs(args.vs);
851
- if (!prevStart.isValid()) {
852
- logger.error("Invalid --vs date format. Use YYYY-MM-DD");
853
- process.exit(1);
854
- }
855
- prevEnd = prevStart.add(periodDays, "day");
856
- } else {
857
- prevEnd = start.subtract(1, "day");
858
- prevStart = prevEnd.subtract(periodDays, "day");
859
- }
860
- range = {
861
- period: {
862
- start: start.format("YYYY-MM-DD"),
863
- end: end.format("YYYY-MM-DD")
864
- },
865
- prevPeriod: {
866
- start: prevStart.format("YYYY-MM-DD"),
867
- end: prevEnd.format("YYYY-MM-DD")
868
- }
869
- };
870
- } else {
871
- const period = parsePeriod(periodArg);
872
- if (!period) {
873
- logger.error(`Invalid period: ${periodArg}`);
874
- process.exit(1);
875
- }
876
- const daysOffset = args.fresh ? 1 : 3;
877
- const endDate = dayjs().subtract(daysOffset, "day");
878
- const startDate = endDate.subtract(period.amount, period.unit);
879
- const prevEndDate = startDate.subtract(1, "day");
880
- const prevStartDate = prevEndDate.subtract(period.amount, period.unit);
881
- range = {
882
- period: {
883
- start: startDate.format("YYYY-MM-DD"),
884
- end: endDate.format("YYYY-MM-DD")
885
- },
886
- prevPeriod: {
887
- start: prevStartDate.format("YYYY-MM-DD"),
888
- end: prevEndDate.format("YYYY-MM-DD")
889
- }
890
- };
891
- }
892
- const searchType = SEARCH_TYPES.includes(args.searchType) ? args.searchType : "web";
893
- const effectiveSource = searchType !== "web" ? "api" : args.source || "auto";
894
- if (searchType !== "web" && args.source === "db") logger.warn(`Search type '${searchType}' requires API. Ignoring --source db.`);
895
- const provider = getProviderForSource$1(auth, dbPath ? createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db : null, effectiveSource);
896
- if (args.json) {
897
- const [dates, pages, keywords] = await Promise.all([
898
- provider.getDatesWithComparison(siteArg, range),
899
- provider.getPagesWithComparison(siteArg, range),
900
- provider.getKeywordsWithComparison(siteArg, range)
901
- ]).catch(gscErrorHandler);
902
- console.log(JSON.stringify({
903
- site: siteArg,
904
- source: provider.source,
905
- range,
906
- dates,
907
- pages,
908
- keywords
909
- }, null, 2));
910
- return;
911
- }
912
- if (args.quiet) return;
913
- console.log();
914
- console.log(` \x1B[1m${siteArg}\x1B[0m`);
915
- console.log(` \x1B[90mSource: ${provider.source}\x1B[0m`);
916
- console.log(` \x1B[90mCurrent: ${range.period.start} → ${range.period.end}\x1B[0m`);
917
- console.log(` \x1B[90mPrevious: ${range.prevPeriod?.start} → ${range.prevPeriod?.end}\x1B[0m`);
918
- console.log();
919
- if (args.type === "summary" || args.type === "all") {
920
- const dates = await provider.getDatesWithComparison(siteArg, range).catch(gscErrorHandler);
921
- console.log(" \x1B[1mSummary\x1B[0m");
922
- console.log(` ┌─────────────┬──────────────┬──────────────┬─────────────┐`);
923
- console.log(` │ │ \x1B[1mCurrent\x1B[0m │ \x1B[1mPrevious\x1B[0m │ \x1B[1mChange\x1B[0m │`);
924
- console.log(` ├─────────────┼──────────────┼──────────────┼─────────────┤`);
925
- console.log(` │ Clicks │ ${formatNumber(dates.metadata.totals.current.clicks).padStart(12)} │ ${formatNumber(dates.metadata.totals.previous.clicks).padStart(12)} │ ${formatPercent(dates.metadata.totals.clicksPercent).padStart(19)} │`);
926
- console.log(` │ Impressions │ ${formatNumber(dates.metadata.totals.current.impressions).padStart(12)} │ ${formatNumber(dates.metadata.totals.previous.impressions).padStart(12)} │ ${formatPercent(dates.metadata.totals.impressionsPercent).padStart(19)} │`);
927
- console.log(` │ CTR │ ${(dates.metadata.totals.current.ctr * 100).toFixed(2).padStart(10)}% │ ${(dates.metadata.totals.previous.ctr * 100).toFixed(2).padStart(10)}% │ ${formatPercent(dates.metadata.totals.ctrPercent).padStart(19)} │`);
928
- console.log(` │ Position │ ${dates.metadata.totals.current.position.toFixed(1).padStart(12)} │ ${dates.metadata.totals.previous.position.toFixed(1).padStart(12)} │ ${formatPercent(dates.metadata.totals.positionPercent).padStart(19)} │`);
929
- console.log(` └─────────────┴──────────────┴──────────────┴─────────────┘`);
930
- console.log();
931
- }
932
- const limit = Number.parseInt(args.limit, 10) || 10;
933
- if (args.type === "pages" || args.type === "all") {
934
- const pages = await provider.getPagesWithComparison(siteArg, range).catch(gscErrorHandler);
935
- const topPages = pages.current.slice(0, limit);
936
- const lostPages = pages.previous.filter((p) => p.lost).slice(0, 5);
937
- console.log(` \x1B[1mTop Pages\x1B[0m \x1B[90m(${pages.current.length} total)\x1B[0m`);
938
- for (const p of topPages) {
939
- const pagePath = p.page.replace(/^https?:\/\/[^/]+/, "") || "/";
940
- console.log(` ${pagePath.slice(0, 50).padEnd(50)} ${formatNumber(p.clicks).padStart(8)} clicks ${formatPercent(p.clicksPercent)}`);
941
- }
942
- if (lostPages.length > 0) {
943
- console.log();
944
- console.log(` \x1B[1mLost Pages\x1B[0m \x1B[90m(had traffic in previous period)\x1B[0m`);
945
- for (const p of lostPages) {
946
- const pagePath = p.page.replace(/^https?:\/\/[^/]+/, "") || "/";
947
- console.log(` \x1B[31m${pagePath.slice(0, 50).padEnd(50)}\x1B[0m ${formatNumber(p.prevClicks).padStart(8)} prev clicks`);
948
- }
949
- }
950
- console.log();
951
- }
952
- if (args.type === "keywords" || args.type === "all") {
953
- const keywords = await provider.getKeywordsWithComparison(siteArg, range).catch(gscErrorHandler);
954
- const topKeywords = keywords.current.slice(0, limit);
955
- const lostKeywords = keywords.previous.filter((k) => k.lost).slice(0, 5);
956
- console.log(` \x1B[1mTop Keywords\x1B[0m \x1B[90m(${keywords.current.length} total)\x1B[0m`);
957
- for (const k of topKeywords) {
958
- const posChange = k.prevPosition ? formatPercent(-((k.position || 0) - k.prevPosition) / k.prevPosition * 100) : "";
959
- console.log(` ${k.keyword.slice(0, 40).padEnd(40)} pos ${(k.position || 0).toFixed(1).padStart(5)} ${posChange} ${formatNumber(k.clicks).padStart(6)} clicks`);
960
- }
961
- if (lostKeywords.length > 0) {
962
- console.log();
963
- console.log(` \x1B[1mLost Keywords\x1B[0m \x1B[90m(had traffic in previous period)\x1B[0m`);
964
- for (const k of lostKeywords) console.log(` \x1B[31m${k.keyword.slice(0, 40).padEnd(40)}\x1B[0m pos ${(k.prevPosition || 0).toFixed(1).padStart(5)} ${formatNumber(k.prevClicks).padStart(6)} prev clicks`);
965
- }
966
- console.log();
967
- }
968
- logger.success("Comparison complete");
969
- }
970
- });
971
-
972
483
  //#endregion
973
484
  //#region src/commands/config.ts
974
485
  const showCommand = defineCommand({
@@ -1064,747 +575,190 @@ const configCommand = defineCommand({
1064
575
 
1065
576
  //#endregion
1066
577
  //#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
- }
1084
578
  const DUMP_DATA_TYPES = [
1085
579
  "pages",
1086
580
  "keywords",
1087
581
  "countries",
1088
582
  "devices"
1089
583
  ];
1090
- async function getSites(client) {
1091
- return (await fetchSites(client)).filter((site) => site.siteUrl && site.permissionLevel !== "siteUnverifiedUser").map((site) => site.siteUrl);
1092
- }
1093
- async function runDump(provider, selectedSites, dataTypes, period, format, outputFile, options = {}) {
1094
- const daysOffset = options.fresh ? 1 : 3;
1095
- const endDate = dayjs().subtract(daysOffset, "days").format("YYYY-MM-DD");
1096
- const startDate = dayjs().subtract(period.amount, period.unit).subtract(daysOffset, "days").format("YYYY-MM-DD");
1097
- const range = {
1098
- period: {
1099
- start: startDate,
1100
- end: endDate
1101
- },
1102
- prevPeriod: {
1103
- start: dayjs(startDate).subtract(period.amount, period.unit).format("YYYY-MM-DD"),
1104
- end: dayjs(endDate).subtract(period.amount, period.unit).format("YYYY-MM-DD")
1105
- }
1106
- };
1107
- for (const siteUrl of selectedSites) {
1108
- const siteName = siteUrl.replace(/https?:\/\//, "").replace(/\/$/, "");
1109
- const output = {
1110
- siteUrl,
1111
- source: provider.source,
1112
- dateRange: {
1113
- startDate,
1114
- endDate
1115
- },
1116
- period: {
1117
- amount: period.amount,
1118
- unit: period.unit
1119
- },
1120
- dataTypes,
1121
- fresh: options.fresh || false,
1122
- searchType: options.searchType || "web"
1123
- };
1124
- const totalSteps = dataTypes.length;
1125
- let currentStep = 0;
1126
- for (const dataType of dataTypes) {
1127
- currentStep++;
1128
- clearLine();
1129
- process.stdout.write(progressBar(currentStep, totalSteps, `${dataType} (${siteName})`));
1130
- if (dataType === "pages") {
1131
- const pages = await provider.getPages(siteUrl, range);
1132
- output.pages = {
1133
- total: pages.length,
1134
- data: pages
1135
- };
1136
- } else if (dataType === "keywords") {
1137
- const keywordsData = await provider.getKeywordsWithComparison(siteUrl, range);
1138
- output.keywords = {
1139
- total: keywordsData.current.length,
1140
- current: keywordsData.current,
1141
- previous: keywordsData.previous,
1142
- metadata: keywordsData.metadata
1143
- };
1144
- } else if (dataType === "countries") {
1145
- const countriesData = await provider.getCountriesWithComparison(siteUrl, range);
1146
- output.countries = {
1147
- current: countriesData.current,
1148
- previous: countriesData.previous,
1149
- metadata: countriesData.metadata
1150
- };
1151
- } else if (dataType === "devices") {
1152
- const devicesData = await provider.getDevicesWithComparison(siteUrl, range);
1153
- output.devices = {
1154
- current: devicesData.current,
1155
- previous: devicesData.previous,
1156
- metadata: devicesData.metadata
1157
- };
1158
- }
1159
- }
1160
- clearLine();
1161
- const ext = format === "csv" ? "csv" : "json";
1162
- const filename = outputFile || `gsc-${siteName.replace(/\//g, "_")}-${dataTypes.join("-")}-${period.amount}${period.unit[0]}-${endDate}.${ext}`;
1163
- const content = format === "csv" ? exportToCSV(output) : JSON.stringify(output, null, 2);
1164
- await fs.writeFile(filename, content);
1165
- const totalItems = (output.pages?.total || 0) + (output.keywords?.total || 0) + (output.countries?.current?.length || 0) + (output.devices?.current?.length || 0);
1166
- logger.success(`Saved ${totalItems} items to ${filename} (source: ${provider.source})`);
1167
- }
1168
- }
1169
- async function interactiveMode(auth, dbPath, source) {
1170
- process.stdout.write(" Fetching sites...");
1171
- const sites = await getSites(googleSearchConsole(auth));
1172
- clearLine();
1173
- logger.success(`Found ${sites.length} sites`);
1174
- if (sites.length === 0) {
1175
- cancel("No sites found in your Google Search Console account.");
1176
- return;
1177
- }
1178
- const selectedSites = await multiselect({
1179
- message: "Select sites to dump data from:",
1180
- options: sites.map((site) => ({
1181
- value: site,
1182
- label: site
1183
- }))
1184
- });
1185
- if (isCancel(selectedSites) || selectedSites.length === 0) {
1186
- cancel("Operation cancelled.");
1187
- return;
1188
- }
1189
- const dataTypes = await multiselect({
1190
- message: "What data would you like to dump?",
1191
- options: [
1192
- {
1193
- value: "pages",
1194
- label: "Pages (URLs and performance data)",
1195
- hint: "recommended"
1196
- },
1197
- {
1198
- value: "keywords",
1199
- label: "Keywords (search queries and rankings)"
1200
- },
1201
- {
1202
- value: "countries",
1203
- label: "Countries (geographic performance)"
1204
- },
1205
- {
1206
- value: "devices",
1207
- label: "Devices (desktop, mobile, tablet)"
1208
- }
1209
- ],
1210
- required: true
1211
- });
1212
- if (isCancel(dataTypes) || dataTypes.length === 0) {
1213
- cancel("Operation cancelled.");
1214
- return;
1215
- }
1216
- const periodInput = await text({
1217
- message: "Time period (e.g., 90d, 6m, 1y):",
1218
- placeholder: "180d",
1219
- defaultValue: "180d",
1220
- validate: (value) => {
1221
- if (!value) return void 0;
1222
- return parsePeriod(value) ? void 0 : "Invalid format. Use: 90d, 6m, 1y";
1223
- }
1224
- });
1225
- if (isCancel(periodInput)) {
1226
- cancel("Operation cancelled.");
1227
- return;
1228
- }
1229
- const formatResult = await multiselect({
1230
- message: "Output format:",
1231
- options: [{
1232
- value: "json",
1233
- label: "JSON",
1234
- hint: "structured data"
1235
- }, {
1236
- value: "csv",
1237
- label: "CSV",
1238
- hint: "spreadsheet compatible"
1239
- }],
1240
- required: true
1241
- });
1242
- if (isCancel(formatResult)) {
1243
- cancel("Operation cancelled.");
1244
- return;
1245
- }
1246
- let finalSource = source;
1247
- if (dbPath && source === "auto") {
1248
- const sourceResult = await select({
1249
- message: "Data source:",
1250
- options: [
1251
- {
1252
- value: "auto",
1253
- label: "Auto",
1254
- hint: "use DB if data exists, else API"
1255
- },
1256
- {
1257
- value: "api",
1258
- label: "API",
1259
- hint: "always fetch from Google"
1260
- },
1261
- {
1262
- value: "db",
1263
- label: "Database",
1264
- hint: "use synced data"
1265
- }
1266
- ]
1267
- });
1268
- if (isCancel(sourceResult)) {
1269
- cancel("Operation cancelled.");
1270
- return;
1271
- }
1272
- finalSource = sourceResult;
1273
- }
1274
- const format = formatResult[0];
1275
- const period = parsePeriod(periodInput || "180d");
1276
- const ready = await confirm({ message: `Dump ${dataTypes.join(", ")} for ${selectedSites.length} site(s), ${period.amount}${period.unit[0]}, as ${format.toUpperCase()}?` });
1277
- if (isCancel(ready) || !ready) {
1278
- cancel("Operation cancelled.");
1279
- return;
1280
- }
1281
- console.log();
1282
- let db = null;
1283
- if (dbPath) {
1284
- if (await fs.access(dbPath).then(() => true).catch(() => false)) db = createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db;
584
+ function getDimensions(dataType) {
585
+ switch (dataType) {
586
+ case "pages": return [page, date];
587
+ case "keywords": return [query, date];
588
+ case "countries": return [country, date];
589
+ case "devices": return [device, date];
1285
590
  }
1286
- const provider = getProviderForSource(auth, db, finalSource);
1287
- logger.info(`Using ${provider.source.toUpperCase()} as data source`);
1288
- await runDump(provider, selectedSites, dataTypes, period, format, null);
1289
- }
1290
- async function nonInteractiveMode(auth, site, data, periodStr, format, output, dbPath, source, options = {}) {
1291
- if (site.length === 0) {
1292
- logger.error("--site required in non-interactive mode");
1293
- process.exit(1);
1294
- }
1295
- if (data.length === 0) {
1296
- logger.error("--data required in non-interactive mode");
1297
- process.exit(1);
1298
- }
1299
- const invalidData = data.filter((d) => !DUMP_DATA_TYPES.includes(d));
1300
- if (invalidData.length > 0) {
1301
- logger.error(`Invalid data types: ${invalidData.join(", ")}`);
1302
- logger.info("Valid types: pages, keywords, countries, devices");
1303
- process.exit(1);
1304
- }
1305
- const period = parsePeriod(periodStr);
1306
- if (!period) {
1307
- logger.error(`Invalid period: ${periodStr}`);
1308
- logger.info("Examples: 90d, 6m, 1y");
1309
- process.exit(1);
1310
- }
1311
- process.stdout.write(" Validating sites...");
1312
- const availableSites = await getSites(googleSearchConsole(auth));
1313
- clearLine();
1314
- const normalizedSites = [];
1315
- for (const s of site) {
1316
- const match = availableSites.find((avail) => avail === s || avail === `https://${s}` || avail === `http://${s}` || avail === `sc-domain:${s}` || avail.replace(/https?:\/\//, "").replace(/\/$/, "") === s.replace(/\/$/, ""));
1317
- if (!match) {
1318
- logger.error(`Site not found: ${s}`);
1319
- logger.info("Available sites:");
1320
- availableSites.slice(0, 5).forEach((a) => console.log(` - ${a}`));
1321
- if (availableSites.length > 5) console.log(` ... and ${availableSites.length - 5} more`);
1322
- process.exit(1);
1323
- }
1324
- normalizedSites.push(match);
1325
- }
1326
- let db = null;
1327
- if (dbPath) {
1328
- if (await fs.access(dbPath).then(() => true).catch(() => false)) db = createGscDb(betterSqlite3({ name: path.resolve(dbPath) })).db;
1329
- else if (source === "db") {
1330
- logger.error(`Database not found: ${dbPath}`);
1331
- logger.info("Run 'gscdump sync' first to create the database.");
1332
- process.exit(1);
1333
- }
1334
- }
1335
- const provider = getProviderForSource(auth, db, source);
1336
- const extras = [];
1337
- if (options.fresh) extras.push("fresh");
1338
- if (options.searchType && options.searchType !== "web") extras.push(options.searchType);
1339
- const extrasStr = extras.length ? ` [${extras.join(", ")}]` : "";
1340
- logger.info(`${normalizedSites.length} site(s), ${data.join("+")} data, ${period.amount}${period.unit[0]}, ${format}, source: ${provider.source}${extrasStr}`);
1341
- console.log();
1342
- await runDump(provider, normalizedSites, data, period, format, output, options);
1343
591
  }
1344
592
  const dumpCommand = defineCommand({
1345
593
  meta: {
1346
594
  name: "dump",
1347
- description: "Export GSC data to JSON or CSV files"
595
+ description: "Export search analytics data via GSC API"
1348
596
  },
1349
597
  args: {
1350
598
  site: {
1351
599
  type: "string",
1352
600
  alias: "s",
1353
- description: "Site URL (comma-separated for multiple)"
1354
- },
1355
- data: {
1356
- type: "string",
1357
- alias: "d",
1358
- description: "Data types: pages,keywords,countries,devices"
601
+ description: "Site URL (e.g., sc-domain:example.com)"
1359
602
  },
1360
- period: {
603
+ output: {
1361
604
  type: "string",
1362
- alias: "p",
1363
- default: "180d",
1364
- description: "Time period: 90d, 6m, 1y"
605
+ alias: "o",
606
+ description: "Output file path (default: stdout)"
1365
607
  },
1366
608
  format: {
1367
609
  type: "string",
1368
610
  alias: "f",
1369
611
  default: "json",
1370
- description: "Output format: json, csv"
612
+ description: "Output format: json or csv"
1371
613
  },
1372
- output: {
614
+ start: {
1373
615
  type: "string",
1374
- alias: "o",
1375
- description: "Output filename"
616
+ description: "Start date (YYYY-MM-DD)"
617
+ },
618
+ end: {
619
+ type: "string",
620
+ description: "End date (YYYY-MM-DD)"
621
+ },
622
+ days: {
623
+ type: "string",
624
+ alias: "d",
625
+ default: "28",
626
+ description: "Number of days to fetch (default: 28)"
1376
627
  },
1377
- source: {
628
+ types: {
1378
629
  type: "string",
1379
- default: "auto",
1380
- description: "Data source: auto, api, db"
630
+ alias: "t",
631
+ description: "Data types: pages,keywords,countries,devices"
1381
632
  },
1382
- db: {
633
+ limit: {
1383
634
  type: "string",
1384
- description: "SQLite database path (for db/auto source)"
635
+ alias: "l",
636
+ default: "25000",
637
+ description: "Max rows per data type"
1385
638
  },
1386
- fresh: {
639
+ quiet: {
1387
640
  type: "boolean",
641
+ alias: "q",
1388
642
  default: false,
1389
- description: "Include fresh/unfinalized data (last 3 days)"
643
+ description: "Suppress progress output"
1390
644
  },
1391
- type: {
1392
- type: "string",
1393
- alias: "t",
1394
- default: "web",
1395
- description: "Search type: web, image, video, news, discover, googleNews"
645
+ interactive: {
646
+ type: "boolean",
647
+ alias: "i",
648
+ default: false,
649
+ description: "Interactive mode - prompts for options"
1396
650
  }
1397
651
  },
1398
652
  async run({ args }) {
1399
653
  const config = await loadConfig();
1400
- const siteArg = args.site || config.defaultSite;
1401
- const sites = siteArg ? siteArg.split(",") : [];
1402
- const data = args.data ? args.data.split(",") : [];
1403
- const periodArg = args.period === "180d" && config.defaultPeriod ? config.defaultPeriod : args.period;
1404
- const formatArg = args.format === "json" && config.defaultFormat ? config.defaultFormat : args.format;
1405
- const dbPath = args.db || config.defaultDb || null;
1406
- const source = [
1407
- "auto",
1408
- "api",
1409
- "db"
1410
- ].includes(args.source) ? args.source : "auto";
1411
- const isInteractive = sites.length === 0 && data.length === 0;
1412
654
  const { getAuth: getAuth$1 } = await Promise.resolve().then(() => auth_exports);
1413
- const auth = await getAuth$1({
1414
- interactive: isInteractive,
1415
- config
1416
- });
1417
- const searchType = [
1418
- "web",
1419
- "image",
1420
- "video",
1421
- "news",
1422
- "discover",
1423
- "googleNews"
1424
- ].includes(args.type) ? args.type : "web";
1425
- const options = {
1426
- fresh: args.fresh,
1427
- searchType
1428
- };
1429
- if (isInteractive) await interactiveMode(auth, dbPath, source);
1430
- else await nonInteractiveMode(auth, sites, data, periodArg, formatArg === "csv" ? "csv" : "json", args.output || null, dbPath, source, options);
1431
- logger.success("Done!");
1432
- }
1433
- });
1434
-
1435
- //#endregion
1436
- //#region src/commands/indexing.ts
1437
- const inspectCommand = defineCommand({
1438
- meta: {
1439
- name: "inspect",
1440
- description: "Inspect URLs to check their indexing status (shorthand for `gscdump index inspect`)"
1441
- },
1442
- args: {
1443
- db: {
1444
- type: "string",
1445
- alias: "d",
1446
- default: "./gscdump.db",
1447
- description: "SQLite database path"
1448
- },
1449
- site: {
1450
- type: "string",
1451
- alias: "s",
1452
- description: "Site URL (e.g., sc-domain:example.com)"
1453
- },
1454
- limit: {
1455
- type: "string",
1456
- alias: "l",
1457
- default: "100",
1458
- description: "Max URLs to inspect"
1459
- },
1460
- delay: {
1461
- type: "string",
1462
- default: "200",
1463
- description: "Delay between requests (ms)"
1464
- },
1465
- quiet: {
1466
- type: "boolean",
1467
- alias: "q",
1468
- default: false,
1469
- description: "Suppress progress output"
1470
- },
1471
- json: {
1472
- type: "boolean",
1473
- default: false,
1474
- description: "Output as JSON"
1475
- }
1476
- },
1477
- async run({ args }) {
1478
- const config = await loadConfig();
1479
- const dbPath = String(args.db === "./gscdump.db" && config.defaultDb ? config.defaultDb : args.db);
1480
- const siteArg = String(args.site || config.defaultSite || "");
1481
- if (!siteArg) {
1482
- logger.error("Site required. Use --site or set defaultSite in config.");
1483
- process.exit(1);
1484
- }
1485
- const { getAuth: getAuth$1 } = await Promise.resolve().then(() => auth_exports);
1486
- const client = googleSearchConsole(await getAuth$1({
1487
- interactive: false,
655
+ const client = googleSearchConsole(await getAuth$1({
656
+ interactive: false,
1488
657
  config
1489
658
  }));
1490
- const { db, db0 } = createGscDb(betterSqlite3({ name: path.resolve(dbPath) }));
1491
- await setupSchema(db0);
1492
- await syncSites(db, client).catch(gscErrorHandler);
1493
- const siteRecord = await getSiteByProperty(db, siteArg);
1494
- if (!siteRecord) {
1495
- logger.error(`Site not found: ${siteArg}`);
1496
- process.exit(1);
1497
- }
1498
- const siteId = siteRecord.site_id || siteRecord.siteId;
1499
- const limit = Number.parseInt(String(args.limit), 10);
1500
- const delayMs = Number.parseInt(String(args.delay), 10);
1501
- const paths = await db.selectDistinct({ path: sitePathDateAnalytics.path }).from(sitePathDateAnalytics).where(eq(sitePathDateAnalytics.siteId, siteId)).limit(limit);
1502
- if (paths.length === 0) {
1503
- logger.warn("No URLs found. Run `gscdump sync` first.");
1504
- return;
1505
- }
1506
- if (!args.quiet && !args.json) logger.info(`Inspecting ${paths.length} URLs...`);
1507
- const results = [];
1508
- const stats = await batchInspectUrls(db, client, siteId, siteArg, paths.map((p) => p.path), {
1509
- delayMs,
1510
- onProgress: (result, i, total) => {
1511
- results.push(result);
1512
- if (!args.quiet && !args.json) {
1513
- clearLine();
1514
- process.stdout.write(progressBar(i + 1, total, result.path.slice(0, 40)));
1515
- }
659
+ let siteUrl = String(args.site || config.defaultSite || "");
660
+ if (!siteUrl || args.interactive) {
661
+ const verified = (await client.sites()).filter((s) => s.permissionLevel !== "siteUnverifiedUser");
662
+ if (verified.length === 0) {
663
+ logger.error("No verified sites found");
664
+ process.exit(1);
1516
665
  }
1517
- }).catch(gscErrorHandler);
1518
- if (!args.quiet && !args.json) clearLine();
1519
- if (args.json) console.log(JSON.stringify({
1520
- stats,
1521
- results
1522
- }, null, 2));
1523
- else {
1524
- console.log();
1525
- logger.success(`Inspected ${paths.length} URLs`);
1526
- console.log(` Indexed: ${stats.indexed}`);
1527
- console.log(` Not Indexed: ${stats.notIndexed}`);
1528
- console.log(` Errors: ${stats.errors}`);
1529
- console.log();
666
+ const selected = await select({
667
+ message: "Select a site",
668
+ options: verified.map((s) => ({
669
+ value: s.siteUrl,
670
+ label: s.siteUrl
671
+ })),
672
+ initialValue: siteUrl || verified[0]?.siteUrl
673
+ });
674
+ if (isCancel(selected)) {
675
+ cancel("Cancelled");
676
+ process.exit(0);
677
+ }
678
+ siteUrl = selected;
1530
679
  }
1531
- }
1532
- });
1533
- const indexingCommand = defineCommand({
1534
- meta: {
1535
- name: "index",
1536
- description: "Manage URL indexing via Google Indexing API"
1537
- },
1538
- subCommands: {
1539
- status: defineCommand({
1540
- meta: {
1541
- name: "status",
1542
- description: "Show indexing status for URLs"
1543
- },
1544
- args: {
1545
- db: {
1546
- type: "string",
1547
- alias: "d",
1548
- default: "./gscdump.db",
1549
- description: "SQLite database path"
1550
- },
1551
- site: {
1552
- type: "string",
1553
- alias: "s",
1554
- description: "Site URL (e.g., sc-domain:example.com)"
1555
- },
1556
- json: {
1557
- type: "boolean",
1558
- default: false,
1559
- description: "Output as JSON"
1560
- }
1561
- },
1562
- async run({ args }) {
1563
- const config = await loadConfig();
1564
- const dbPath = String(args.db === "./gscdump.db" && config.defaultDb ? config.defaultDb : args.db);
1565
- const siteArg = String(args.site || config.defaultSite || "");
1566
- if (!siteArg) {
1567
- logger.error("Site required. Use --site or set defaultSite in config.");
1568
- process.exit(1);
1569
- }
1570
- const { db, db0 } = createGscDb(betterSqlite3({ name: path.resolve(dbPath) }));
1571
- await setupSchema(db0);
1572
- const siteRecord = await getSiteByProperty(db, siteArg);
1573
- if (!siteRecord) {
1574
- logger.error(`Site not found: ${siteArg}`);
1575
- process.exit(1);
1576
- }
1577
- const stats = await getIndexingStats(db, siteRecord.site_id || siteRecord.siteId);
1578
- if (args.json) console.log(JSON.stringify(stats, null, 2));
1579
- else {
1580
- console.log();
1581
- logger.info(`Indexing Status for ${siteArg}`);
1582
- console.log();
1583
- console.log(` Total URLs: ${stats.total}`);
1584
- console.log(` Indexed: ${stats.indexed} (${stats.total ? Math.round(stats.indexed / stats.total * 100) : 0}%)`);
1585
- console.log(` Not Indexed: ${stats.notIndexed}`);
1586
- console.log(` Unknown: ${stats.unknown}`);
1587
- console.log(` Requested: ${stats.requested}`);
1588
- console.log();
1589
- }
680
+ let startDate;
681
+ let endDate;
682
+ if (args.start && args.end) {
683
+ startDate = String(args.start);
684
+ endDate = String(args.end);
685
+ } else if (args.interactive) {
686
+ const startInput = await text({
687
+ message: "Start date (YYYY-MM-DD)",
688
+ placeholder: (/* @__PURE__ */ new Date(Date.now() - Number(args.days) * 864e5)).toISOString().split("T")[0]
689
+ });
690
+ if (isCancel(startInput)) {
691
+ cancel("Cancelled");
692
+ process.exit(0);
1590
693
  }
1591
- }),
1592
- inspect: defineCommand({
1593
- meta: {
1594
- name: "inspect",
1595
- description: "Inspect URLs to check their indexing status"
1596
- },
1597
- args: {
1598
- db: {
1599
- type: "string",
1600
- alias: "d",
1601
- default: "./gscdump.db",
1602
- description: "SQLite database path"
1603
- },
1604
- site: {
1605
- type: "string",
1606
- alias: "s",
1607
- description: "Site URL (e.g., sc-domain:example.com)"
1608
- },
1609
- limit: {
1610
- type: "string",
1611
- alias: "l",
1612
- default: "100",
1613
- description: "Max URLs to inspect"
1614
- },
1615
- delay: {
1616
- type: "string",
1617
- default: "200",
1618
- description: "Delay between requests (ms)"
1619
- },
1620
- quiet: {
1621
- type: "boolean",
1622
- alias: "q",
1623
- default: false,
1624
- description: "Suppress progress output"
1625
- },
1626
- json: {
1627
- type: "boolean",
1628
- default: false,
1629
- description: "Output as JSON"
1630
- }
1631
- },
1632
- async run({ args }) {
1633
- const config = await loadConfig();
1634
- const dbPath = String(args.db === "./gscdump.db" && config.defaultDb ? config.defaultDb : args.db);
1635
- const siteArg = String(args.site || config.defaultSite || "");
1636
- if (!siteArg) {
1637
- logger.error("Site required. Use --site or set defaultSite in config.");
1638
- process.exit(1);
1639
- }
1640
- const { getAuth: getAuth$1 } = await Promise.resolve().then(() => auth_exports);
1641
- const client = googleSearchConsole(await getAuth$1({
1642
- interactive: false,
1643
- config
1644
- }));
1645
- const { db, db0 } = createGscDb(betterSqlite3({ name: path.resolve(dbPath) }));
1646
- await setupSchema(db0);
1647
- await syncSites(db, client).catch(gscErrorHandler);
1648
- const siteRecord = await getSiteByProperty(db, siteArg);
1649
- if (!siteRecord) {
1650
- logger.error(`Site not found: ${siteArg}`);
1651
- process.exit(1);
1652
- }
1653
- const siteId = siteRecord.site_id || siteRecord.siteId;
1654
- const limit = Number.parseInt(String(args.limit), 10);
1655
- const delayMs = Number.parseInt(String(args.delay), 10);
1656
- const paths = await db.selectDistinct({ path: sitePathDateAnalytics.path }).from(sitePathDateAnalytics).where(eq(sitePathDateAnalytics.siteId, siteId)).limit(limit);
1657
- if (paths.length === 0) {
1658
- logger.warn("No URLs found. Run `gscdump sync` first.");
1659
- return;
1660
- }
1661
- if (!args.quiet && !args.json) logger.info(`Inspecting ${paths.length} URLs...`);
1662
- const results = [];
1663
- const stats = await batchInspectUrls(db, client, siteId, siteArg, paths.map((p) => p.path), {
1664
- delayMs,
1665
- onProgress: (result, i, total) => {
1666
- results.push(result);
1667
- if (!args.quiet && !args.json) {
1668
- clearLine();
1669
- process.stdout.write(progressBar(i + 1, total, result.path.slice(0, 40)));
1670
- }
1671
- }
1672
- }).catch(gscErrorHandler);
1673
- if (!args.quiet && !args.json) clearLine();
1674
- if (args.json) console.log(JSON.stringify({
1675
- stats,
1676
- results
1677
- }, null, 2));
1678
- else {
1679
- console.log();
1680
- logger.success(`Inspected ${paths.length} URLs`);
1681
- console.log(` Indexed: ${stats.indexed}`);
1682
- console.log(` Not Indexed: ${stats.notIndexed}`);
1683
- console.log(` Errors: ${stats.errors}`);
1684
- console.log();
1685
- }
694
+ const endInput = await text({
695
+ message: "End date (YYYY-MM-DD)",
696
+ placeholder: (/* @__PURE__ */ new Date(Date.now() - 3 * 864e5)).toISOString().split("T")[0]
697
+ });
698
+ if (isCancel(endInput)) {
699
+ cancel("Cancelled");
700
+ process.exit(0);
1686
701
  }
1687
- }),
1688
- request: defineCommand({
1689
- meta: {
1690
- name: "request",
1691
- description: "Request indexing for URLs via Google Indexing API"
1692
- },
1693
- args: {
1694
- "db": {
1695
- type: "string",
1696
- alias: "d",
1697
- default: "./gscdump.db",
1698
- description: "SQLite database path"
1699
- },
1700
- "site": {
1701
- type: "string",
1702
- alias: "s",
1703
- description: "Site URL (e.g., sc-domain:example.com)"
1704
- },
1705
- "limit": {
1706
- type: "string",
1707
- alias: "l",
1708
- default: "200",
1709
- description: "Max URLs to request (API quota: 200/day)"
1710
- },
1711
- "delay": {
1712
- type: "string",
1713
- default: "100",
1714
- description: "Delay between requests (ms)"
1715
- },
1716
- "type": {
1717
- type: "string",
1718
- alias: "t",
1719
- default: "URL_UPDATED",
1720
- description: "Notification type: URL_UPDATED or URL_DELETED"
1721
- },
1722
- "not-indexed": {
1723
- type: "boolean",
1724
- default: true,
1725
- description: "Only request for non-indexed URLs"
1726
- },
1727
- "quiet": {
1728
- type: "boolean",
1729
- alias: "q",
1730
- default: false,
1731
- description: "Suppress progress output"
1732
- },
1733
- "json": {
1734
- type: "boolean",
1735
- default: false,
1736
- description: "Output as JSON"
1737
- }
702
+ startDate = String(startInput) || (/* @__PURE__ */ new Date(Date.now() - Number(args.days) * 864e5)).toISOString().split("T")[0];
703
+ endDate = String(endInput) || (/* @__PURE__ */ new Date(Date.now() - 3 * 864e5)).toISOString().split("T")[0];
704
+ } else {
705
+ const days = Number.parseInt(String(args.days), 10);
706
+ endDate = (/* @__PURE__ */ new Date(Date.now() - 3 * 864e5)).toISOString().split("T")[0];
707
+ startDate = (/* @__PURE__ */ new Date(Date.now() - (days + 3) * 864e5)).toISOString().split("T")[0];
708
+ }
709
+ let dataTypes;
710
+ if (args.types) dataTypes = String(args.types).split(",").filter((t) => DUMP_DATA_TYPES.includes(t));
711
+ else if (args.interactive) {
712
+ const selected = await multiselect({
713
+ message: "Select data types to export",
714
+ options: DUMP_DATA_TYPES.map((t) => ({
715
+ value: t,
716
+ label: t
717
+ })),
718
+ initialValues: ["pages", "keywords"]
719
+ });
720
+ if (isCancel(selected)) {
721
+ cancel("Cancelled");
722
+ process.exit(0);
723
+ }
724
+ dataTypes = selected;
725
+ } else dataTypes = ["pages", "keywords"];
726
+ const rowLimit = Number.parseInt(String(args.limit), 10);
727
+ const format = String(args.format);
728
+ const output = {
729
+ siteUrl,
730
+ dateRange: {
731
+ start: startDate,
732
+ end: endDate
1738
733
  },
1739
- async run({ args }) {
1740
- const config = await loadConfig();
1741
- const dbPath = String(args.db === "./gscdump.db" && config.defaultDb ? config.defaultDb : args.db);
1742
- const siteArg = String(args.site || config.defaultSite || "");
1743
- if (!siteArg) {
1744
- logger.error("Site required. Use --site or set defaultSite in config.");
1745
- process.exit(1);
1746
- }
1747
- const { getAuth: getAuth$1 } = await Promise.resolve().then(() => auth_exports);
1748
- const client = googleSearchConsole(await getAuth$1({
1749
- interactive: false,
1750
- config
1751
- }));
1752
- const { db, db0 } = createGscDb(betterSqlite3({ name: path.resolve(dbPath) }));
1753
- await setupSchema(db0);
1754
- await syncSites(db, client).catch(gscErrorHandler);
1755
- const siteRecord = await getSiteByProperty(db, siteArg);
1756
- if (!siteRecord) {
1757
- logger.error(`Site not found: ${siteArg}`);
1758
- process.exit(1);
1759
- }
1760
- const siteId = siteRecord.site_id || siteRecord.siteId;
1761
- const limit = Number.parseInt(String(args.limit), 10);
1762
- const delayMs = Number.parseInt(String(args.delay), 10);
1763
- const type = String(args.type);
1764
- const paths = await db.selectDistinct({ path: sitePathDateAnalytics.path }).from(sitePathDateAnalytics).where(eq(sitePathDateAnalytics.siteId, siteId)).limit(limit);
1765
- if (paths.length === 0) {
1766
- logger.warn("No URLs found. Run `gscdump sync` first.");
1767
- return;
1768
- }
1769
- if (!args.quiet && !args.json) {
1770
- logger.info(`Requesting indexing for ${paths.length} URLs...`);
1771
- logger.warn("Note: Indexing API quota is typically 200 requests/day");
1772
- }
1773
- const results = [];
1774
- const stats = await batchRequestIndexingForPaths(db, client, siteId, siteArg, paths.map((p) => p.path), {
1775
- type,
1776
- delayMs,
1777
- onProgress: (result, i, total) => {
1778
- results.push(result);
1779
- if (!args.quiet && !args.json) {
1780
- clearLine();
1781
- const status = result.error ? "ERR" : "OK";
1782
- process.stdout.write(progressBar(i + 1, total, `[${status}] ${result.url.slice(-40)}`));
1783
- }
1784
- }
1785
- }).catch(gscErrorHandler);
1786
- if (!args.quiet && !args.json) clearLine();
1787
- if (args.json) console.log(JSON.stringify({
1788
- stats,
1789
- results
1790
- }, null, 2));
1791
- else {
1792
- console.log();
1793
- logger.success(`Requested indexing for ${paths.length} URLs`);
1794
- console.log(` Success: ${stats.success}`);
1795
- console.log(` Errors: ${stats.errors}`);
1796
- console.log();
1797
- if (stats.errors > 0) {
1798
- const errorResults = results.filter((r) => r.error);
1799
- logger.warn("Errors:");
1800
- errorResults.slice(0, 5).forEach((r) => {
1801
- console.log(` ${r.url}: ${r.error}`);
1802
- });
1803
- if (errorResults.length > 5) console.log(` ... and ${errorResults.length - 5} more`);
1804
- }
1805
- }
734
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString()
735
+ };
736
+ const totalSteps = dataTypes.length;
737
+ let currentStep = 0;
738
+ for (const dataType of dataTypes) {
739
+ currentStep++;
740
+ if (!args.quiet) {
741
+ clearLine();
742
+ process.stdout.write(progressBar(currentStep, totalSteps, dataType));
1806
743
  }
1807
- })
744
+ const dimensions = getDimensions(dataType);
745
+ const builder = gsc.select(...dimensions).where(between(date, startDate, endDate)).limit(rowLimit);
746
+ const rows = [];
747
+ for await (const batch of client.query(siteUrl, builder)) rows.push(...batch);
748
+ output[dataType] = {
749
+ total: rows.length,
750
+ data: rows
751
+ };
752
+ }
753
+ if (!args.quiet) {
754
+ clearLine();
755
+ logger.success(`Exported ${dataTypes.join(", ")} for ${siteUrl}`);
756
+ }
757
+ const content = format === "csv" ? exportToCSV(output) : JSON.stringify(output, null, 2);
758
+ if (args.output) {
759
+ await fs.writeFile(String(args.output), content);
760
+ if (!args.quiet) logger.info(`Written to ${args.output}`);
761
+ } else console.log(content);
1808
762
  }
1809
763
  });
1810
764
 
@@ -1908,6 +862,383 @@ const initCommand = defineCommand({
1908
862
  }
1909
863
  });
1910
864
 
865
+ //#endregion
866
+ //#region src/mcp/handlers/analytics.ts
867
+ async function collectRows(ctx, siteUrl, builder) {
868
+ const rows = [];
869
+ for await (const batch of ctx.client.query(siteUrl, builder)) rows.push(...batch);
870
+ return rows;
871
+ }
872
+ async function fetchPages(input, ctx) {
873
+ const builder = gsc.select(page, date).where(between(date, input.period.start, input.period.end)).limit(25e3);
874
+ const rows = await collectRows(ctx, input.siteUrl, builder);
875
+ return {
876
+ total: rows.length,
877
+ data: rows
878
+ };
879
+ }
880
+ async function fetchKeywords(input, ctx) {
881
+ const builder = gsc.select(query, date).where(between(date, input.period.start, input.period.end)).limit(25e3);
882
+ const rows = await collectRows(ctx, input.siteUrl, builder);
883
+ return {
884
+ total: rows.length,
885
+ data: rows
886
+ };
887
+ }
888
+ async function fetchCountries(input, ctx) {
889
+ const builder = gsc.select(country, date).where(between(date, input.period.start, input.period.end)).limit(25e3);
890
+ const rows = await collectRows(ctx, input.siteUrl, builder);
891
+ return {
892
+ total: rows.length,
893
+ data: rows
894
+ };
895
+ }
896
+ async function fetchDevices(input, ctx) {
897
+ const builder = gsc.select(device, date).where(between(date, input.period.start, input.period.end)).limit(25e3);
898
+ const rows = await collectRows(ctx, input.siteUrl, builder);
899
+ return {
900
+ total: rows.length,
901
+ data: rows
902
+ };
903
+ }
904
+
905
+ //#endregion
906
+ //#region src/mcp/handlers/indexing.ts
907
+ async function inspectUrl$1(input, ctx) {
908
+ return inspectUrl(ctx.client, input.siteUrl, input.inspectionUrl);
909
+ }
910
+ async function requestIndexing$1(input, ctx) {
911
+ return requestIndexing(ctx.client, input.url, { type: input.type || "URL_UPDATED" }).catch((e) => ({
912
+ url: input.url,
913
+ type: input.type || "URL_UPDATED",
914
+ error: e.message
915
+ }));
916
+ }
917
+ async function getIndexingStatus(input, ctx) {
918
+ return getIndexingMetadata(ctx.client, input.url).catch((e) => ({
919
+ url: input.url,
920
+ error: e.message
921
+ }));
922
+ }
923
+ async function batchRequestIndexing$1(input, ctx) {
924
+ const results = await batchRequestIndexing(ctx.client, input.urls, {
925
+ type: input.type || "URL_UPDATED",
926
+ delayMs: input.delayMs || 100
927
+ });
928
+ return {
929
+ results,
930
+ success: results.length,
931
+ failed: 0
932
+ };
933
+ }
934
+ async function batchInspectUrls$1(input, ctx) {
935
+ const results = await batchInspectUrls(ctx.client, input.siteUrl, input.urls, { delayMs: input.delayMs || 200 });
936
+ return {
937
+ results,
938
+ indexed: results.filter((r) => r.isIndexed).length,
939
+ notIndexed: results.filter((r) => !r.isIndexed).length
940
+ };
941
+ }
942
+
943
+ //#endregion
944
+ //#region src/mcp/handlers/query.ts
945
+ const DIMENSION_MAP$1 = {
946
+ page,
947
+ query,
948
+ date,
949
+ country,
950
+ device,
951
+ searchAppearance
952
+ };
953
+ async function customQuery(input, ctx) {
954
+ const dimensions = input.dimensions.filter((d) => d in DIMENSION_MAP$1).map((d) => DIMENSION_MAP$1[d]);
955
+ if (dimensions.length === 0) throw new Error("At least one valid dimension required");
956
+ const builder = gsc.select(...dimensions).where(between(date, input.period.start, input.period.end)).limit(input.rowLimit || 25e3);
957
+ const rows = [];
958
+ for await (const batch of ctx.client.query(input.siteUrl, builder)) rows.push(...batch);
959
+ return {
960
+ total: rows.length,
961
+ data: rows
962
+ };
963
+ }
964
+
965
+ //#endregion
966
+ //#region src/mcp/handlers/sites.ts
967
+ async function listSites(_input, ctx) {
968
+ return fetchSites(ctx.client);
969
+ }
970
+ async function listSitesWithSitemaps(_input, ctx) {
971
+ return fetchSitesWithSitemaps(ctx.client);
972
+ }
973
+ async function listSitemaps(input, ctx) {
974
+ return fetchSitemaps(ctx.client, input.siteUrl);
975
+ }
976
+ async function getSitemap(input, ctx) {
977
+ return fetchSitemap(ctx.client, input.siteUrl, input.feedpath);
978
+ }
979
+ async function submitSitemap$1(input, ctx) {
980
+ await submitSitemap(ctx.client, input.siteUrl, input.feedpath);
981
+ return { success: true };
982
+ }
983
+ async function deleteSitemap$1(input, ctx) {
984
+ await deleteSitemap(ctx.client, input.siteUrl, input.feedpath);
985
+ return { success: true };
986
+ }
987
+
988
+ //#endregion
989
+ //#region src/mcp/types.ts
990
+ const periodSchema = z.object({
991
+ start: z.string().describe("Start date (YYYY-MM-DD)"),
992
+ end: z.string().describe("End date (YYYY-MM-DD)")
993
+ }).describe("Date range for the query");
994
+ const siteUrlSchema = z.string().describe("GSC property URL (e.g., sc-domain:example.com or https://example.com/)");
995
+ const queryOptionsSchema = z.object({
996
+ type: z.enum([
997
+ "web",
998
+ "image",
999
+ "video",
1000
+ "news",
1001
+ "discover",
1002
+ "googleNews"
1003
+ ]).optional().describe("Data type"),
1004
+ dataState: z.enum(["final", "all"]).optional().describe("Data state: final (settled) or all (includes fresh)"),
1005
+ aggregationType: z.enum(["byPage", "byProperty"]).optional().describe("Aggregation: byPage or byProperty")
1006
+ }).optional();
1007
+ const listSitesInput = z.object({});
1008
+ const listSitemapsInput = z.object({ siteUrl: siteUrlSchema });
1009
+ const fetchAnalyticsInput = z.object({
1010
+ siteUrl: siteUrlSchema,
1011
+ period: periodSchema,
1012
+ comparePrevious: z.boolean().optional().describe("Include previous period comparison"),
1013
+ options: queryOptionsSchema
1014
+ });
1015
+ const fetchPageInput = z.object({
1016
+ siteUrl: siteUrlSchema,
1017
+ period: periodSchema,
1018
+ url: z.string().describe("Page URL to fetch details for")
1019
+ });
1020
+ const fetchKeywordInput = z.object({
1021
+ siteUrl: siteUrlSchema,
1022
+ period: periodSchema,
1023
+ keyword: z.string().describe("Keyword to fetch details for")
1024
+ });
1025
+ const inspectUrlInput = z.object({
1026
+ siteUrl: siteUrlSchema,
1027
+ inspectionUrl: z.string().describe("URL to inspect")
1028
+ });
1029
+ const requestIndexingInput = z.object({
1030
+ url: z.string().describe("URL to request indexing for"),
1031
+ type: z.enum(["URL_UPDATED", "URL_DELETED"]).optional().describe("Notification type")
1032
+ });
1033
+ const getIndexingStatusInput = z.object({ url: z.string().describe("URL to get indexing status for") });
1034
+ const customQueryInput = z.object({
1035
+ siteUrl: siteUrlSchema,
1036
+ period: periodSchema,
1037
+ dimensions: z.array(z.enum([
1038
+ "date",
1039
+ "query",
1040
+ "page",
1041
+ "country",
1042
+ "device",
1043
+ "searchAppearance"
1044
+ ])).describe("Dimensions to group by"),
1045
+ rowLimit: z.number().optional().describe("Max rows (default 25000)"),
1046
+ options: queryOptionsSchema
1047
+ });
1048
+ const sitemapInput = z.object({
1049
+ siteUrl: siteUrlSchema,
1050
+ feedpath: z.string().describe("Sitemap URL (e.g., https://example.com/sitemap.xml)")
1051
+ });
1052
+ const batchRequestIndexingInput = z.object({
1053
+ urls: z.array(z.string()).describe("URLs to request indexing for"),
1054
+ type: z.enum(["URL_UPDATED", "URL_DELETED"]).optional().describe("Notification type"),
1055
+ delayMs: z.number().optional().describe("Delay between requests in ms (default 100)")
1056
+ });
1057
+ const batchInspectUrlsInput = z.object({
1058
+ siteUrl: siteUrlSchema,
1059
+ urls: z.array(z.string()).describe("URLs to inspect"),
1060
+ delayMs: z.number().optional().describe("Delay between requests in ms (default 200)")
1061
+ });
1062
+
1063
+ //#endregion
1064
+ //#region src/mcp/server/index.ts
1065
+ function createGscMcpServer(options) {
1066
+ const { name = "gscdump", version = "1.0.0", getAuth: getAuth$1 } = options;
1067
+ const server = new McpServer({
1068
+ name,
1069
+ version
1070
+ });
1071
+ const auth = async () => Promise.resolve(getAuth$1());
1072
+ const getContext = async () => {
1073
+ const a = await auth();
1074
+ return {
1075
+ auth: a,
1076
+ client: googleSearchConsole(a)
1077
+ };
1078
+ };
1079
+ server.registerTool("list-sites", {
1080
+ description: "List all Google Search Console sites the user has access to",
1081
+ inputSchema: listSitesInput.shape
1082
+ }, async (args) => {
1083
+ const result = await listSites(args, await getContext());
1084
+ return { content: [{
1085
+ type: "text",
1086
+ text: JSON.stringify(result, null, 2)
1087
+ }] };
1088
+ });
1089
+ server.registerTool("list-sites-with-sitemaps", {
1090
+ description: "List all GSC sites with their sitemaps",
1091
+ inputSchema: listSitesInput.shape
1092
+ }, async (args) => {
1093
+ const result = await listSitesWithSitemaps(args, await getContext());
1094
+ return { content: [{
1095
+ type: "text",
1096
+ text: JSON.stringify(result, null, 2)
1097
+ }] };
1098
+ });
1099
+ server.registerTool("list-sitemaps", {
1100
+ description: "List sitemaps for a specific site",
1101
+ inputSchema: listSitemapsInput.shape
1102
+ }, async (args) => {
1103
+ const result = await listSitemaps(args, await getContext());
1104
+ return { content: [{
1105
+ type: "text",
1106
+ text: JSON.stringify(result, null, 2)
1107
+ }] };
1108
+ });
1109
+ server.registerTool("get-sitemap", {
1110
+ description: "Get details for a specific sitemap",
1111
+ inputSchema: sitemapInput.shape
1112
+ }, async (args) => {
1113
+ const result = await getSitemap(args, await getContext());
1114
+ return { content: [{
1115
+ type: "text",
1116
+ text: JSON.stringify(result, null, 2)
1117
+ }] };
1118
+ });
1119
+ server.registerTool("submit-sitemap", {
1120
+ description: "Submit a sitemap to Google Search Console",
1121
+ inputSchema: sitemapInput.shape
1122
+ }, async (args) => {
1123
+ const result = await submitSitemap$1(args, await getContext());
1124
+ return { content: [{
1125
+ type: "text",
1126
+ text: JSON.stringify(result, null, 2)
1127
+ }] };
1128
+ });
1129
+ server.registerTool("delete-sitemap", {
1130
+ description: "Delete a sitemap from Google Search Console",
1131
+ inputSchema: sitemapInput.shape
1132
+ }, async (args) => {
1133
+ const result = await deleteSitemap$1(args, await getContext());
1134
+ return { content: [{
1135
+ type: "text",
1136
+ text: JSON.stringify(result, null, 2)
1137
+ }] };
1138
+ });
1139
+ server.registerTool("fetch-pages", {
1140
+ description: "Fetch page analytics data for a site",
1141
+ inputSchema: fetchAnalyticsInput.shape
1142
+ }, async (args) => {
1143
+ const result = await fetchPages(args, await getContext());
1144
+ return { content: [{
1145
+ type: "text",
1146
+ text: JSON.stringify(result, null, 2)
1147
+ }] };
1148
+ });
1149
+ server.registerTool("fetch-keywords", {
1150
+ description: "Fetch keyword/query analytics data for a site",
1151
+ inputSchema: fetchAnalyticsInput.shape
1152
+ }, async (args) => {
1153
+ const result = await fetchKeywords(args, await getContext());
1154
+ return { content: [{
1155
+ type: "text",
1156
+ text: JSON.stringify(result, null, 2)
1157
+ }] };
1158
+ });
1159
+ server.registerTool("fetch-countries", {
1160
+ description: "Fetch country analytics data for a site",
1161
+ inputSchema: fetchAnalyticsInput.shape
1162
+ }, async (args) => {
1163
+ const result = await fetchCountries(args, await getContext());
1164
+ return { content: [{
1165
+ type: "text",
1166
+ text: JSON.stringify(result, null, 2)
1167
+ }] };
1168
+ });
1169
+ server.registerTool("fetch-devices", {
1170
+ description: "Fetch device analytics data for a site",
1171
+ inputSchema: fetchAnalyticsInput.shape
1172
+ }, async (args) => {
1173
+ const result = await fetchDevices(args, await getContext());
1174
+ return { content: [{
1175
+ type: "text",
1176
+ text: JSON.stringify(result, null, 2)
1177
+ }] };
1178
+ });
1179
+ server.registerTool("custom-query", {
1180
+ description: "Run a custom search analytics query with specified dimensions",
1181
+ inputSchema: customQueryInput.shape
1182
+ }, async (args) => {
1183
+ const result = await customQuery(args, await getContext());
1184
+ return { content: [{
1185
+ type: "text",
1186
+ text: JSON.stringify(result, null, 2)
1187
+ }] };
1188
+ });
1189
+ server.registerTool("inspect-url", {
1190
+ description: "Inspect a URL in Google Search Console to check its indexing status",
1191
+ inputSchema: inspectUrlInput.shape
1192
+ }, async (args) => {
1193
+ const result = await inspectUrl$1(args, await getContext());
1194
+ return { content: [{
1195
+ type: "text",
1196
+ text: JSON.stringify(result, null, 2)
1197
+ }] };
1198
+ });
1199
+ server.registerTool("request-indexing", {
1200
+ description: "Request Google to index or remove a URL via the Indexing API",
1201
+ inputSchema: requestIndexingInput.shape
1202
+ }, async (args) => {
1203
+ const result = await requestIndexing$1(args, await getContext());
1204
+ return { content: [{
1205
+ type: "text",
1206
+ text: JSON.stringify(result, null, 2)
1207
+ }] };
1208
+ });
1209
+ server.registerTool("get-indexing-status", {
1210
+ description: "Get indexing status metadata for a URL",
1211
+ inputSchema: getIndexingStatusInput.shape
1212
+ }, async (args) => {
1213
+ const result = await getIndexingStatus(args, await getContext());
1214
+ return { content: [{
1215
+ type: "text",
1216
+ text: JSON.stringify(result, null, 2)
1217
+ }] };
1218
+ });
1219
+ server.registerTool("batch-request-indexing", {
1220
+ description: "Batch request indexing for multiple URLs with rate limiting",
1221
+ inputSchema: batchRequestIndexingInput.shape
1222
+ }, async (args) => {
1223
+ const result = await batchRequestIndexing$1(args, await getContext());
1224
+ return { content: [{
1225
+ type: "text",
1226
+ text: JSON.stringify(result, null, 2)
1227
+ }] };
1228
+ });
1229
+ server.registerTool("batch-inspect-urls", {
1230
+ description: "Batch inspect multiple URLs to check their indexing status",
1231
+ inputSchema: batchInspectUrlsInput.shape
1232
+ }, async (args) => {
1233
+ const result = await batchInspectUrls$1(args, await getContext());
1234
+ return { content: [{
1235
+ type: "text",
1236
+ text: JSON.stringify(result, null, 2)
1237
+ }] };
1238
+ });
1239
+ return server;
1240
+ }
1241
+
1911
1242
  //#endregion
1912
1243
  //#region src/commands/mcp.ts
1913
1244
  async function checkAuth() {
@@ -1969,6 +1300,177 @@ const mcpCommand = defineCommand({
1969
1300
  }
1970
1301
  });
1971
1302
 
1303
+ //#endregion
1304
+ //#region src/commands/query.ts
1305
+ const DIMENSION_MAP = {
1306
+ page,
1307
+ query,
1308
+ date,
1309
+ country,
1310
+ device,
1311
+ searchAppearance
1312
+ };
1313
+ const queryCommand = defineCommand({
1314
+ meta: {
1315
+ name: "query",
1316
+ description: "Run custom search analytics queries"
1317
+ },
1318
+ args: {
1319
+ site: {
1320
+ type: "string",
1321
+ alias: "s",
1322
+ description: "Site URL (e.g., sc-domain:example.com)"
1323
+ },
1324
+ dimensions: {
1325
+ type: "string",
1326
+ alias: "d",
1327
+ description: "Dimensions: page,query,date,country,device,searchAppearance"
1328
+ },
1329
+ start: {
1330
+ type: "string",
1331
+ description: "Start date (YYYY-MM-DD)"
1332
+ },
1333
+ end: {
1334
+ type: "string",
1335
+ description: "End date (YYYY-MM-DD)"
1336
+ },
1337
+ limit: {
1338
+ type: "string",
1339
+ alias: "l",
1340
+ default: "1000",
1341
+ description: "Max rows (default: 1000)"
1342
+ },
1343
+ output: {
1344
+ type: "string",
1345
+ alias: "o",
1346
+ description: "Output file path (default: stdout)"
1347
+ },
1348
+ format: {
1349
+ type: "string",
1350
+ alias: "f",
1351
+ default: "json",
1352
+ description: "Output format: json or csv"
1353
+ },
1354
+ quiet: {
1355
+ type: "boolean",
1356
+ alias: "q",
1357
+ default: false,
1358
+ description: "Suppress progress output"
1359
+ },
1360
+ interactive: {
1361
+ type: "boolean",
1362
+ alias: "i",
1363
+ default: false,
1364
+ description: "Interactive mode"
1365
+ }
1366
+ },
1367
+ async run({ args }) {
1368
+ const config = await loadConfig();
1369
+ const { getAuth: getAuth$1 } = await Promise.resolve().then(() => auth_exports);
1370
+ const client = googleSearchConsole(await getAuth$1({
1371
+ interactive: false,
1372
+ config
1373
+ }));
1374
+ let siteUrl = String(args.site || config.defaultSite || "");
1375
+ if (!siteUrl || args.interactive) {
1376
+ const verified = (await client.sites()).filter((s) => s.permissionLevel !== "siteUnverifiedUser");
1377
+ if (verified.length === 0) {
1378
+ logger.error("No verified sites found");
1379
+ process.exit(1);
1380
+ }
1381
+ const selected = await select({
1382
+ message: "Select a site",
1383
+ options: verified.map((s) => ({
1384
+ value: s.siteUrl,
1385
+ label: s.siteUrl
1386
+ })),
1387
+ initialValue: siteUrl || verified[0]?.siteUrl
1388
+ });
1389
+ if (isCancel(selected)) {
1390
+ cancel("Cancelled");
1391
+ process.exit(0);
1392
+ }
1393
+ siteUrl = selected;
1394
+ }
1395
+ let dimensions;
1396
+ if (args.dimensions) dimensions = String(args.dimensions).split(",").filter((d) => d in DIMENSION_MAP).map((d) => DIMENSION_MAP[d]);
1397
+ else if (args.interactive) {
1398
+ const selected = await multiselect({
1399
+ message: "Select dimensions",
1400
+ options: Object.keys(DIMENSION_MAP).map((d) => ({
1401
+ value: d,
1402
+ label: d
1403
+ })),
1404
+ initialValues: ["page", "query"]
1405
+ });
1406
+ if (isCancel(selected)) {
1407
+ cancel("Cancelled");
1408
+ process.exit(0);
1409
+ }
1410
+ dimensions = selected.map((d) => DIMENSION_MAP[d]);
1411
+ } else dimensions = [page, query];
1412
+ let startDate;
1413
+ let endDate;
1414
+ if (args.start && args.end) {
1415
+ startDate = String(args.start);
1416
+ endDate = String(args.end);
1417
+ } else if (args.interactive) {
1418
+ const startInput = await text({
1419
+ message: "Start date (YYYY-MM-DD)",
1420
+ placeholder: (/* @__PURE__ */ new Date(Date.now() - 28 * 864e5)).toISOString().split("T")[0]
1421
+ });
1422
+ if (isCancel(startInput)) {
1423
+ cancel("Cancelled");
1424
+ process.exit(0);
1425
+ }
1426
+ const endInput = await text({
1427
+ message: "End date (YYYY-MM-DD)",
1428
+ placeholder: (/* @__PURE__ */ new Date(Date.now() - 3 * 864e5)).toISOString().split("T")[0]
1429
+ });
1430
+ if (isCancel(endInput)) {
1431
+ cancel("Cancelled");
1432
+ process.exit(0);
1433
+ }
1434
+ startDate = String(startInput) || (/* @__PURE__ */ new Date(Date.now() - 28 * 864e5)).toISOString().split("T")[0];
1435
+ endDate = String(endInput) || (/* @__PURE__ */ new Date(Date.now() - 3 * 864e5)).toISOString().split("T")[0];
1436
+ } else {
1437
+ endDate = (/* @__PURE__ */ new Date(Date.now() - 3 * 864e5)).toISOString().split("T")[0];
1438
+ startDate = (/* @__PURE__ */ new Date(Date.now() - 31 * 864e5)).toISOString().split("T")[0];
1439
+ }
1440
+ const rowLimit = Number.parseInt(String(args.limit), 10);
1441
+ const format = String(args.format);
1442
+ const builder = gsc.select(...dimensions).where(between(date, startDate, endDate)).limit(rowLimit);
1443
+ if (!args.quiet) logger.info(`Querying ${siteUrl}...`);
1444
+ const rows = [];
1445
+ for await (const batch of client.query(siteUrl, builder)) {
1446
+ rows.push(...batch);
1447
+ if (!args.quiet) {
1448
+ clearLine();
1449
+ process.stdout.write(progressBar(rows.length, rowLimit, `${rows.length} rows`));
1450
+ }
1451
+ }
1452
+ if (!args.quiet) {
1453
+ clearLine();
1454
+ logger.success(`Fetched ${rows.length} rows`);
1455
+ }
1456
+ const output = {
1457
+ siteUrl,
1458
+ dimensions: dimensions.map((d) => String(d)),
1459
+ dateRange: {
1460
+ start: startDate,
1461
+ end: endDate
1462
+ },
1463
+ total: rows.length,
1464
+ data: rows
1465
+ };
1466
+ const content = format === "csv" ? exportToCSV(output) : JSON.stringify(output, null, 2);
1467
+ if (args.output) {
1468
+ await fs.writeFile(String(args.output), content);
1469
+ if (!args.quiet) logger.info(`Written to ${args.output}`);
1470
+ } else console.log(content);
1471
+ }
1472
+ });
1473
+
1972
1474
  //#endregion
1973
1475
  //#region src/commands/sitemaps.ts
1974
1476
  const listCommand = defineCommand({
@@ -2146,249 +1648,6 @@ const sitesCommand = defineCommand({
2146
1648
  }
2147
1649
  });
2148
1650
 
2149
- //#endregion
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
- }
2157
- async function runSync(client, dbPath, siteArg, period, granular, options) {
2158
- const resolvedPath = path.resolve(dbPath);
2159
- const report = {
2160
- database: resolvedPath,
2161
- period: {
2162
- start: "",
2163
- end: ""
2164
- },
2165
- sites: [],
2166
- totalRows: 0
2167
- };
2168
- if (!options.quiet && !options.json) logger.info(`Database: ${resolvedPath}`);
2169
- const { db, db0 } = createGscDb(betterSqlite3({ name: resolvedPath }));
2170
- await setupSchema(db0);
2171
- if (!options.quiet && !options.json) logger.start("Syncing sites...");
2172
- const syncedSites = await syncSites(db, client);
2173
- if (!options.quiet && !options.json) logger.success(`Synced ${syncedSites.length} sites`);
2174
- let sitesToSync = [];
2175
- if (siteArg) {
2176
- const siteRecord = await getSiteByProperty(db, siteArg);
2177
- if (!siteRecord) {
2178
- if (options.json) console.log(JSON.stringify({ error: `Site not found: ${siteArg}` }));
2179
- else {
2180
- logger.error(`Site not found: ${siteArg}`);
2181
- logger.info("Available sites:");
2182
- syncedSites.slice(0, 5).forEach((s) => console.log(` - ${s.property}`));
2183
- }
2184
- process.exit(1);
2185
- }
2186
- sitesToSync = [{
2187
- siteId: siteRecord.site_id || siteRecord.siteId,
2188
- siteUrl: siteRecord.property
2189
- }];
2190
- } else sitesToSync = syncedSites.map((s) => ({
2191
- siteId: s.siteId,
2192
- siteUrl: s.property
2193
- }));
2194
- const periodDays = parsePeriodDays(period);
2195
- const daysOffset = options.fresh ? 1 : 3;
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);
2200
- const getRangeForSite = async (siteId) => {
2201
- let startDate = baseStartDate;
2202
- if (options.since) startDate = options.since;
2203
- else if (options.incremental) {
2204
- const lastSynced = await getLastSyncedDate(db, siteId);
2205
- if (lastSynced) {
2206
- const incrementalStart = dayjs(lastSynced).add(1, "day").format("YYYY-MM-DD");
2207
- startDate = incrementalStart > baseStartDate ? incrementalStart : baseStartDate;
2208
- }
2209
- }
2210
- if (startDate > adjustedEndDate) return {
2211
- range: {
2212
- period: {
2213
- start: startDate,
2214
- end: adjustedEndDate
2215
- },
2216
- prevPeriod: {
2217
- start: adjustedPrevStartDate,
2218
- end: adjustedPrevEndDate
2219
- }
2220
- },
2221
- startDate,
2222
- skipped: true
2223
- };
2224
- return {
2225
- range: {
2226
- period: {
2227
- start: startDate,
2228
- end: adjustedEndDate
2229
- },
2230
- prevPeriod: {
2231
- start: adjustedPrevStartDate,
2232
- end: adjustedPrevEndDate
2233
- }
2234
- },
2235
- startDate,
2236
- skipped: false
2237
- };
2238
- };
2239
- report.period = {
2240
- start: options.since || baseStartDate,
2241
- end: adjustedEndDate
2242
- };
2243
- if (!options.quiet && !options.json) {
2244
- const modeNote = options.incremental ? " (incremental)" : options.since ? ` (since ${options.since})` : options.fresh ? " (fresh)" : "";
2245
- logger.info(`Period: ${options.since || baseStartDate} to ${adjustedEndDate}${modeNote}`);
2246
- console.log();
2247
- }
2248
- const dataTypes = granular ? [
2249
- "pages",
2250
- "keywords",
2251
- "keyword-paths",
2252
- "countries",
2253
- "devices"
2254
- ] : [
2255
- "pages",
2256
- "keywords",
2257
- "countries",
2258
- "devices"
2259
- ];
2260
- for (const { siteId, siteUrl } of sitesToSync) {
2261
- const siteName = siteUrl.replace(/^(sc-domain:|https?:\/\/)/, "");
2262
- const { range, startDate, skipped } = await getRangeForSite(siteId);
2263
- if (skipped) {
2264
- if (!options.quiet && !options.json) logger.info(`${siteName} is up to date (last synced: ${startDate})`);
2265
- continue;
2266
- }
2267
- const siteReport = {
2268
- site: siteUrl,
2269
- rows: {
2270
- pages: 0,
2271
- keywords: 0,
2272
- countries: 0,
2273
- devices: 0,
2274
- total: 0
2275
- }
2276
- };
2277
- if (!options.quiet && !options.json && (options.incremental || options.since)) logger.info(`${siteName}: syncing ${startDate} to ${adjustedEndDate}`);
2278
- for (let i = 0; i < dataTypes.length; i++) {
2279
- const dataType = dataTypes[i];
2280
- if (!options.quiet && !options.json) {
2281
- clearLine();
2282
- process.stdout.write(progressBar(i + 1, dataTypes.length, `${dataType} (${siteName})`));
2283
- }
2284
- let rows = [];
2285
- if (dataType === "pages") {
2286
- rows = await syncPages(db, client, siteId, siteUrl, range);
2287
- siteReport.rows.pages = rows.length;
2288
- } else if (dataType === "keywords") {
2289
- rows = await syncKeywords(db, client, siteId, siteUrl, range);
2290
- siteReport.rows.keywords = rows.length;
2291
- } else if (dataType === "keyword-paths") {
2292
- rows = await syncKeywordPaths(db, client, siteId, siteUrl, range);
2293
- siteReport.rows.keywordPaths = rows.length;
2294
- } else if (dataType === "countries") {
2295
- rows = await syncCountries(db, client, siteId, siteUrl, range);
2296
- siteReport.rows.countries = rows.length;
2297
- } else if (dataType === "devices") {
2298
- rows = await syncDevices(db, client, siteId, siteUrl, range);
2299
- siteReport.rows.devices = rows.length;
2300
- }
2301
- siteReport.rows.total += rows.length;
2302
- }
2303
- if (!options.quiet && !options.json) clearLine();
2304
- await updateLastSynced(db, siteId);
2305
- report.sites.push(siteReport);
2306
- report.totalRows += siteReport.rows.total;
2307
- if (!options.quiet && !options.json) logger.success(`Synced ${siteName} (${siteReport.rows.total.toLocaleString()} rows)`);
2308
- }
2309
- if (!options.quiet && !options.json) {
2310
- console.log();
2311
- logger.success(`Database saved to ${resolvedPath}`);
2312
- }
2313
- return report;
2314
- }
2315
- const syncCommand = defineCommand({
2316
- meta: {
2317
- name: "sync",
2318
- description: "Sync GSC data to SQLite database"
2319
- },
2320
- args: {
2321
- db: {
2322
- type: "string",
2323
- alias: "d",
2324
- default: "./gscdump.db",
2325
- description: "SQLite database path"
2326
- },
2327
- site: {
2328
- type: "string",
2329
- alias: "s",
2330
- description: "Site URL (e.g., sc-domain:example.com)"
2331
- },
2332
- period: {
2333
- type: "string",
2334
- alias: "p",
2335
- default: "90d",
2336
- description: "Time period: 90d, 6m, 1y, max (all GSC history ~16mo)"
2337
- },
2338
- granular: {
2339
- type: "boolean",
2340
- alias: "g",
2341
- default: false,
2342
- description: "Include keyword-per-page data (large dataset)"
2343
- },
2344
- quiet: {
2345
- type: "boolean",
2346
- alias: "q",
2347
- default: false,
2348
- description: "Suppress output"
2349
- },
2350
- json: {
2351
- type: "boolean",
2352
- default: false,
2353
- description: "Output sync report as JSON"
2354
- },
2355
- fresh: {
2356
- type: "boolean",
2357
- default: false,
2358
- description: "Include fresh/unfinalized data (last 3 days)"
2359
- },
2360
- incremental: {
2361
- type: "boolean",
2362
- alias: "i",
2363
- default: false,
2364
- description: "Only fetch data since last sync"
2365
- },
2366
- since: {
2367
- type: "string",
2368
- description: "Fetch data from specific date (YYYY-MM-DD)"
2369
- }
2370
- },
2371
- async run({ args }) {
2372
- const config = await loadConfig();
2373
- const dbPath = args.db === "./gscdump.db" && config.defaultDb ? config.defaultDb : args.db;
2374
- const siteArg = args.site || config.defaultSite || null;
2375
- const periodArg = args.period === "90d" && config.defaultPeriod ? config.defaultPeriod : args.period;
2376
- const { getAuth: getAuth$1 } = await Promise.resolve().then(() => auth_exports);
2377
- const report = await runSync(googleSearchConsole(await getAuth$1({
2378
- interactive: false,
2379
- config
2380
- })), dbPath, siteArg, periodArg, args.granular, {
2381
- quiet: args.quiet,
2382
- json: args.json,
2383
- fresh: args.fresh,
2384
- incremental: args.incremental,
2385
- since: args.since
2386
- }).catch(gscErrorHandler);
2387
- if (args.json) console.log(JSON.stringify(report, null, 2));
2388
- else if (!args.quiet) logger.success("Done!");
2389
- }
2390
- });
2391
-
2392
1651
  //#endregion
2393
1652
  //#region src/index.ts
2394
1653
  runMain(defineCommand({
@@ -2400,26 +1659,15 @@ runMain(defineCommand({
2400
1659
  subCommands: {
2401
1660
  init: initCommand,
2402
1661
  dump: dumpCommand,
2403
- sync: syncCommand,
2404
- compare: compareCommand,
2405
- analyze: analyzeCommand,
1662
+ query: queryCommand,
2406
1663
  sites: sitesCommand,
2407
1664
  sitemaps: sitemapsCommand,
2408
- index: indexingCommand,
2409
- inspect: inspectCommand,
2410
1665
  auth: authCommand,
2411
1666
  config: configCommand,
2412
1667
  mcp: mcpCommand
2413
1668
  },
2414
1669
  setup() {
2415
1670
  if (!process.argv.includes("mcp")) showSplash();
2416
- },
2417
- async run({ args }) {
2418
- if (!args._.length) await dumpCommand.run({
2419
- args,
2420
- rawArgs: [],
2421
- cmd: dumpCommand
2422
- });
2423
1671
  }
2424
1672
  }));
2425
1673