@houtini/better-search-console 1.0.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.
Files changed (71) hide show
  1. package/LICENSE +189 -0
  2. package/README.md +334 -0
  3. package/better-search-console.jpg +0 -0
  4. package/dist/core/DataRetention.d.ts +49 -0
  5. package/dist/core/DataRetention.d.ts.map +1 -0
  6. package/dist/core/DataRetention.js +165 -0
  7. package/dist/core/DataRetention.js.map +1 -0
  8. package/dist/core/DataSync.d.ts +24 -0
  9. package/dist/core/DataSync.d.ts.map +1 -0
  10. package/dist/core/DataSync.js +247 -0
  11. package/dist/core/DataSync.js.map +1 -0
  12. package/dist/core/Database.d.ts +29 -0
  13. package/dist/core/Database.d.ts.map +1 -0
  14. package/dist/core/Database.js +205 -0
  15. package/dist/core/Database.js.map +1 -0
  16. package/dist/core/GscClient.d.ts +23 -0
  17. package/dist/core/GscClient.d.ts.map +1 -0
  18. package/dist/core/GscClient.js +92 -0
  19. package/dist/core/GscClient.js.map +1 -0
  20. package/dist/core/SyncManager.d.ts +66 -0
  21. package/dist/core/SyncManager.d.ts.map +1 -0
  22. package/dist/core/SyncManager.js +368 -0
  23. package/dist/core/SyncManager.js.map +1 -0
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +14 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/overview/src/ui/overview.html +185 -0
  29. package/dist/server.d.ts +6 -0
  30. package/dist/server.d.ts.map +1 -0
  31. package/dist/server.js +441 -0
  32. package/dist/server.js.map +1 -0
  33. package/dist/src/ui/dashboard.html +437 -0
  34. package/dist/sync-progress/src/ui/sync-progress.html +147 -0
  35. package/dist/tools/compare-periods.d.ts +3 -0
  36. package/dist/tools/compare-periods.d.ts.map +1 -0
  37. package/dist/tools/compare-periods.js +127 -0
  38. package/dist/tools/compare-periods.js.map +1 -0
  39. package/dist/tools/get-dashboard.d.ts +10 -0
  40. package/dist/tools/get-dashboard.d.ts.map +1 -0
  41. package/dist/tools/get-dashboard.js +310 -0
  42. package/dist/tools/get-dashboard.js.map +1 -0
  43. package/dist/tools/get-insights.d.ts +3 -0
  44. package/dist/tools/get-insights.d.ts.map +1 -0
  45. package/dist/tools/get-insights.js +509 -0
  46. package/dist/tools/get-insights.js.map +1 -0
  47. package/dist/tools/get-overview.d.ts +36 -0
  48. package/dist/tools/get-overview.d.ts.map +1 -0
  49. package/dist/tools/get-overview.js +111 -0
  50. package/dist/tools/get-overview.js.map +1 -0
  51. package/dist/tools/helpers.d.ts +67 -0
  52. package/dist/tools/helpers.d.ts.map +1 -0
  53. package/dist/tools/helpers.js +239 -0
  54. package/dist/tools/helpers.js.map +1 -0
  55. package/dist/tools/list-properties.d.ts +4 -0
  56. package/dist/tools/list-properties.d.ts.map +1 -0
  57. package/dist/tools/list-properties.js +35 -0
  58. package/dist/tools/list-properties.js.map +1 -0
  59. package/dist/tools/query-data.d.ts +12 -0
  60. package/dist/tools/query-data.d.ts.map +1 -0
  61. package/dist/tools/query-data.js +20 -0
  62. package/dist/tools/query-data.js.map +1 -0
  63. package/dist/tools/sync-data.d.ts +11 -0
  64. package/dist/tools/sync-data.d.ts.map +1 -0
  65. package/dist/tools/sync-data.js +62 -0
  66. package/dist/tools/sync-data.js.map +1 -0
  67. package/dist/types/index.d.ts +101 -0
  68. package/dist/types/index.d.ts.map +1 -0
  69. package/dist/types/index.js +3 -0
  70. package/dist/types/index.js.map +1 -0
  71. package/package.json +57 -0
@@ -0,0 +1,111 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { Database } from '../core/Database.js';
4
+ import { sanitizeSiteUrl, getDataDir, getPeriodDates } from './helpers.js';
5
+ export async function getOverviewData(gscClient, params) {
6
+ const { dateRange = '28d', sortBy = 'alpha', search } = params;
7
+ const { current, prior } = getPeriodDates(dateRange);
8
+ const dataDir = getDataDir();
9
+ // Get all properties from GSC API
10
+ const allProperties = await gscClient.listProperties();
11
+ const properties = [];
12
+ const pctChange = (curr, prev) => prev === 0 ? null : Math.round(((curr - prev) / prev) * 1000) / 10;
13
+ for (const prop of allProperties) {
14
+ const dbFilename = sanitizeSiteUrl(prop.siteUrl) + '.db';
15
+ const dbPath = join(dataDir, dbFilename);
16
+ // Skip properties without synced data
17
+ if (!existsSync(dbPath))
18
+ continue;
19
+ // Extract clean domain
20
+ let domain = prop.siteUrl
21
+ .replace(/^sc-domain:/, '')
22
+ .replace(/^https?:\/\//, '')
23
+ .replace(/\/+$/, '');
24
+ // Filter by search substring
25
+ if (search && !domain.toLowerCase().includes(search.toLowerCase()))
26
+ continue;
27
+ const db = new Database(dbPath);
28
+ try {
29
+ // Summary for current period
30
+ const currentSummary = db.queryOne(`
31
+ SELECT
32
+ COALESCE(SUM(clicks), 0) as clicks,
33
+ COALESCE(SUM(impressions), 0) as impressions,
34
+ ROUND(CAST(SUM(clicks) AS REAL) / NULLIF(SUM(impressions), 0), 4) as ctr,
35
+ ROUND(AVG(position), 1) as avg_position
36
+ FROM search_analytics
37
+ WHERE date BETWEEN ? AND ?
38
+ `, [current.startDate, current.endDate]);
39
+ // Summary for prior period
40
+ const priorSummary = db.queryOne(`
41
+ SELECT
42
+ COALESCE(SUM(clicks), 0) as clicks,
43
+ COALESCE(SUM(impressions), 0) as impressions,
44
+ ROUND(CAST(SUM(clicks) AS REAL) / NULLIF(SUM(impressions), 0), 4) as ctr,
45
+ ROUND(AVG(position), 1) as avg_position
46
+ FROM search_analytics
47
+ WHERE date BETWEEN ? AND ?
48
+ `, [prior.startDate, prior.endDate]);
49
+ // Skip if no data at all
50
+ if (currentSummary.clicks === 0 && currentSummary.impressions === 0 &&
51
+ priorSummary.clicks === 0 && priorSummary.impressions === 0)
52
+ continue;
53
+ // Daily sparkline data
54
+ const sparkline = db.query(`
55
+ SELECT date,
56
+ SUM(clicks) as clicks,
57
+ SUM(impressions) as impressions
58
+ FROM search_analytics
59
+ WHERE date BETWEEN ? AND ?
60
+ GROUP BY date
61
+ ORDER BY date ASC
62
+ `, [current.startDate, current.endDate]);
63
+ // Last synced
64
+ const meta = db.getPropertyMeta(prop.siteUrl);
65
+ properties.push({
66
+ siteUrl: prop.siteUrl,
67
+ domain,
68
+ lastSyncedAt: meta?.lastSyncedAt ?? null,
69
+ current: {
70
+ clicks: currentSummary.clicks,
71
+ impressions: currentSummary.impressions,
72
+ ctr: currentSummary.ctr ?? 0,
73
+ avgPosition: currentSummary.avg_position ?? null,
74
+ },
75
+ changes: {
76
+ clicksPct: pctChange(currentSummary.clicks, priorSummary.clicks),
77
+ impressionsPct: pctChange(currentSummary.impressions, priorSummary.impressions),
78
+ ctrPct: pctChange(currentSummary.ctr ?? 0, priorSummary.ctr ?? 0),
79
+ avgPositionPct: (currentSummary.avg_position != null && priorSummary.avg_position != null)
80
+ ? pctChange(currentSummary.avg_position, priorSummary.avg_position)
81
+ : null,
82
+ },
83
+ sparkline,
84
+ });
85
+ }
86
+ finally {
87
+ db.close();
88
+ }
89
+ }
90
+ // Sort
91
+ switch (sortBy) {
92
+ case 'clicks':
93
+ properties.sort((a, b) => b.current.clicks - a.current.clicks);
94
+ break;
95
+ case 'impressions':
96
+ properties.sort((a, b) => b.current.impressions - a.current.impressions);
97
+ break;
98
+ case 'ctr':
99
+ properties.sort((a, b) => b.current.ctr - a.current.ctr);
100
+ break;
101
+ case 'position':
102
+ properties.sort((a, b) => (a.current.avgPosition ?? 999) - (b.current.avgPosition ?? 999)); // lower is better, null sorts last
103
+ break;
104
+ case 'alpha':
105
+ default:
106
+ properties.sort((a, b) => a.domain.localeCompare(b.domain));
107
+ break;
108
+ }
109
+ return { dateRange, sortBy, properties };
110
+ }
111
+ //# sourceMappingURL=get-overview.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"get-overview.js","sourceRoot":"","sources":["../../src/tools/get-overview.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Sanitize a GSC site URL into a safe filename.
3
+ * e.g. "sc-domain:simracingcockpit.gg" → "simracingcockpit.gg"
4
+ * e.g. "https://www.example.com/" → "www.example.com"
5
+ */
6
+ export declare function sanitizeSiteUrl(siteUrl: string): string;
7
+ /**
8
+ * Get the data directory for SQLite databases.
9
+ * Priority: BSC_DATA_DIR env var > cross-platform home directory fallback.
10
+ *
11
+ * Defaults:
12
+ * Windows: C:\seo-audits\better-search-console
13
+ * macOS: ~/seo-audits/better-search-console
14
+ * Linux: ~/seo-audits/better-search-console
15
+ */
16
+ export declare function getDataDir(): string;
17
+ /**
18
+ * Get the database path for a given site URL.
19
+ */
20
+ export declare function getDbPath(siteUrl: string): string;
21
+ /**
22
+ * Parse a date range shorthand into start/end dates.
23
+ * Supports: "7d", "14d", "28d", "3m", "6m", "8m", "12m", "16m",
24
+ * plus named presets: "1d", "lw", "tm", "lm", "tq", "lq", "ytd"
25
+ */
26
+ export declare function parseDateRange(range: string, referenceDate?: Date): {
27
+ startDate: string;
28
+ endDate: string;
29
+ };
30
+ export type ComparisonMode = 'previous_period' | 'year_over_year' | 'previous_month' | 'disabled';
31
+ /**
32
+ * Get current + prior period date ranges from a date range shorthand.
33
+ * Supports multiple comparison modes.
34
+ */
35
+ export declare function getPeriodDates(range: string, referenceDate?: Date, comparisonMode?: ComparisonMode, matchWeekdays?: boolean): {
36
+ current: {
37
+ startDate: string;
38
+ endDate: string;
39
+ };
40
+ prior: {
41
+ startDate: string;
42
+ endDate: string;
43
+ };
44
+ };
45
+ /**
46
+ * Default start date: 3 months ago for fast initial sync.
47
+ * Users can pass startDate explicitly for longer ranges (up to 16 months).
48
+ */
49
+ export declare function defaultStartDate(): string;
50
+ /**
51
+ * Default end date: today.
52
+ */
53
+ export declare function defaultEndDate(): string;
54
+ /**
55
+ * Render an ASCII sparkline from an array of numbers.
56
+ * Uses Unicode block characters for a compact visual trend.
57
+ */
58
+ export declare function asciiSparkline(data: number[]): string;
59
+ /**
60
+ * Format a number with k/M suffixes for compact display.
61
+ */
62
+ export declare function formatCompact(n: number): string;
63
+ /**
64
+ * Format a percentage change with sign and arrow.
65
+ */
66
+ export declare function formatChange(pct: number | null): string;
67
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/tools/helpers.ts"],"names":[],"mappings":"AAIA;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAWvD;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,IAAI,MAAM,CAOnC;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjD;AAMD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,CAAC,EAAE,IAAI,GAAG;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAuD1G;AAED,MAAM,MAAM,cAAc,GAAG,iBAAiB,GAAG,gBAAgB,GAAG,gBAAgB,GAAG,UAAU,CAAC;AAElG;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,KAAK,EAAE,MAAM,EACb,aAAa,CAAC,EAAE,IAAI,EACpB,cAAc,GAAE,cAAkC,EAClD,aAAa,GAAE,OAAe,GAC7B;IACD,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAChD,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;CAC/C,CAmFA;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAIzC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAGD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,MAAM,CAOrD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAI/C;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,MAAM,CAIvD"}
@@ -0,0 +1,239 @@
1
+ import { existsSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ /**
5
+ * Sanitize a GSC site URL into a safe filename.
6
+ * e.g. "sc-domain:simracingcockpit.gg" → "simracingcockpit.gg"
7
+ * e.g. "https://www.example.com/" → "www.example.com"
8
+ */
9
+ export function sanitizeSiteUrl(siteUrl) {
10
+ let name = siteUrl;
11
+ // Remove sc-domain: prefix
12
+ name = name.replace(/^sc-domain:/, '');
13
+ // Remove protocol
14
+ name = name.replace(/^https?:\/\//, '');
15
+ // Remove trailing slash
16
+ name = name.replace(/\/+$/, '');
17
+ // Replace unsafe chars
18
+ name = name.replace(/[^a-zA-Z0-9.-]/g, '_');
19
+ return name;
20
+ }
21
+ /**
22
+ * Get the data directory for SQLite databases.
23
+ * Priority: BSC_DATA_DIR env var > cross-platform home directory fallback.
24
+ *
25
+ * Defaults:
26
+ * Windows: C:\seo-audits\better-search-console
27
+ * macOS: ~/seo-audits/better-search-console
28
+ * Linux: ~/seo-audits/better-search-console
29
+ */
30
+ export function getDataDir() {
31
+ const dataDir = process.env.BSC_DATA_DIR
32
+ || join(homedir(), 'seo-audits', 'better-search-console');
33
+ if (!existsSync(dataDir)) {
34
+ mkdirSync(dataDir, { recursive: true });
35
+ }
36
+ return dataDir;
37
+ }
38
+ /**
39
+ * Get the database path for a given site URL.
40
+ */
41
+ export function getDbPath(siteUrl) {
42
+ return join(getDataDir(), sanitizeSiteUrl(siteUrl) + '.db');
43
+ }
44
+ function formatDate(d) {
45
+ return d.toISOString().split('T')[0];
46
+ }
47
+ /**
48
+ * Parse a date range shorthand into start/end dates.
49
+ * Supports: "7d", "14d", "28d", "3m", "6m", "8m", "12m", "16m",
50
+ * plus named presets: "1d", "lw", "tm", "lm", "tq", "lq", "ytd"
51
+ */
52
+ export function parseDateRange(range, referenceDate) {
53
+ const now = referenceDate || new Date();
54
+ const today = formatDate(now);
55
+ // Named presets
56
+ switch (range) {
57
+ case 'lw': { // Last Week (Mon-Sun)
58
+ const d = new Date(now);
59
+ const day = d.getDay() || 7; // convert Sunday=0 to 7
60
+ d.setDate(d.getDate() - day - 6); // Monday of last week
61
+ const start = formatDate(d);
62
+ d.setDate(d.getDate() + 6); // Sunday of last week
63
+ return { startDate: start, endDate: formatDate(d) };
64
+ }
65
+ case 'tm': { // This Month
66
+ const d = new Date(now.getFullYear(), now.getMonth(), 1);
67
+ return { startDate: formatDate(d), endDate: today };
68
+ }
69
+ case 'lm': { // Last Month
70
+ const start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
71
+ const end = new Date(now.getFullYear(), now.getMonth(), 0); // last day of prev month
72
+ return { startDate: formatDate(start), endDate: formatDate(end) };
73
+ }
74
+ case 'tq': { // This Quarter
75
+ const qStart = new Date(now.getFullYear(), Math.floor(now.getMonth() / 3) * 3, 1);
76
+ return { startDate: formatDate(qStart), endDate: today };
77
+ }
78
+ case 'lq': { // Last Quarter
79
+ const qm = Math.floor(now.getMonth() / 3) * 3;
80
+ const start = new Date(now.getFullYear(), qm - 3, 1);
81
+ const end = new Date(now.getFullYear(), qm, 0);
82
+ return { startDate: formatDate(start), endDate: formatDate(end) };
83
+ }
84
+ case 'ytd': { // Year to Date
85
+ const start = new Date(now.getFullYear(), 0, 1);
86
+ return { startDate: formatDate(start), endDate: today };
87
+ }
88
+ }
89
+ // Numeric presets: "7d", "3m", etc.
90
+ const match = range.match(/^(\d+)(d|m)$/);
91
+ if (!match) {
92
+ throw new Error(`Invalid date range: "${range}". Use format like "7d", "28d", "3m", "6m", "12m", "16m", or named presets: "lw", "tm", "lm", "tq", "lq", "ytd".`);
93
+ }
94
+ const amount = parseInt(match[1], 10);
95
+ const unit = match[2];
96
+ const start = new Date(now);
97
+ if (unit === 'd') {
98
+ start.setDate(start.getDate() - amount);
99
+ }
100
+ else if (unit === 'm') {
101
+ start.setMonth(start.getMonth() - amount);
102
+ }
103
+ return { startDate: formatDate(start), endDate: today };
104
+ }
105
+ /**
106
+ * Get current + prior period date ranges from a date range shorthand.
107
+ * Supports multiple comparison modes.
108
+ */
109
+ export function getPeriodDates(range, referenceDate, comparisonMode = 'previous_period', matchWeekdays = false) {
110
+ const current = parseDateRange(range, referenceDate);
111
+ if (comparisonMode === 'disabled') {
112
+ // Return empty prior period — callers should check for this
113
+ return {
114
+ current,
115
+ prior: { startDate: current.startDate, endDate: current.startDate },
116
+ };
117
+ }
118
+ const startMs = new Date(current.startDate).getTime();
119
+ const endMs = new Date(current.endDate).getTime();
120
+ const DAY = 24 * 60 * 60 * 1000;
121
+ const durationMs = endMs - startMs;
122
+ let priorStart;
123
+ let priorEnd;
124
+ switch (comparisonMode) {
125
+ case 'year_over_year': {
126
+ const s = new Date(current.startDate);
127
+ const e = new Date(current.endDate);
128
+ const sDay = s.getDate();
129
+ const eDay = e.getDate();
130
+ s.setFullYear(s.getFullYear() - 1);
131
+ // Handle leap year: Feb 29 → Feb 28 (setFullYear may roll to Mar 1)
132
+ if (s.getDate() !== sDay)
133
+ s.setDate(0); // last day of previous month
134
+ e.setFullYear(e.getFullYear() - 1);
135
+ if (e.getDate() !== eDay)
136
+ e.setDate(0);
137
+ priorStart = s;
138
+ priorEnd = e;
139
+ break;
140
+ }
141
+ case 'previous_month': {
142
+ const s = new Date(current.startDate);
143
+ const e = new Date(current.endDate);
144
+ const sMonth = (s.getMonth() - 1 + 12) % 12;
145
+ const eMonth = (e.getMonth() - 1 + 12) % 12;
146
+ s.setMonth(s.getMonth() - 1);
147
+ // Handle day overflow: e.g. Mar 31 → setMonth(-1) → "Feb 31" → Mar 3
148
+ if (s.getMonth() !== sMonth)
149
+ s.setDate(0); // last day of intended month
150
+ e.setMonth(e.getMonth() - 1);
151
+ if (e.getMonth() !== eMonth)
152
+ e.setDate(0);
153
+ priorStart = s;
154
+ priorEnd = e;
155
+ break;
156
+ }
157
+ case 'previous_period':
158
+ default: {
159
+ priorEnd = new Date(startMs - DAY);
160
+ priorStart = new Date(priorEnd.getTime() - durationMs);
161
+ break;
162
+ }
163
+ }
164
+ // Match weekdays: shift prior period so that the start day-of-week matches
165
+ if (matchWeekdays) {
166
+ const currentStartDay = new Date(current.startDate).getDay();
167
+ const priorStartDay = priorStart.getDay();
168
+ let diff = currentStartDay - priorStartDay;
169
+ // Find the closest shift that aligns weekdays (within ±3 days)
170
+ if (diff > 3)
171
+ diff -= 7;
172
+ if (diff < -3)
173
+ diff += 7;
174
+ priorStart = new Date(priorStart.getTime() + diff * DAY);
175
+ priorEnd = new Date(priorEnd.getTime() + diff * DAY);
176
+ // Safety: ensure prior period never overlaps with current period
177
+ const currentStartMs = new Date(current.startDate).getTime();
178
+ if (priorEnd.getTime() >= currentStartMs) {
179
+ const overshoot = priorEnd.getTime() - currentStartMs + DAY;
180
+ priorStart = new Date(priorStart.getTime() - overshoot);
181
+ priorEnd = new Date(priorEnd.getTime() - overshoot);
182
+ }
183
+ }
184
+ return {
185
+ current,
186
+ prior: {
187
+ startDate: formatDate(priorStart),
188
+ endDate: formatDate(priorEnd),
189
+ },
190
+ };
191
+ }
192
+ /**
193
+ * Default start date: 3 months ago for fast initial sync.
194
+ * Users can pass startDate explicitly for longer ranges (up to 16 months).
195
+ */
196
+ export function defaultStartDate() {
197
+ const d = new Date();
198
+ d.setMonth(d.getMonth() - 3);
199
+ return formatDate(d);
200
+ }
201
+ /**
202
+ * Default end date: today.
203
+ */
204
+ export function defaultEndDate() {
205
+ return formatDate(new Date());
206
+ }
207
+ /**
208
+ * Render an ASCII sparkline from an array of numbers.
209
+ * Uses Unicode block characters for a compact visual trend.
210
+ */
211
+ export function asciiSparkline(data) {
212
+ if (data.length === 0)
213
+ return '';
214
+ const min = Math.min(...data);
215
+ const max = Math.max(...data);
216
+ const range = max - min || 1;
217
+ const blocks = ' \u2581\u2582\u2583\u2584\u2585\u2586\u2587\u2588';
218
+ return data.map(v => blocks[Math.round(((v - min) / range) * 8)]).join('');
219
+ }
220
+ /**
221
+ * Format a number with k/M suffixes for compact display.
222
+ */
223
+ export function formatCompact(n) {
224
+ if (n >= 1_000_000)
225
+ return (n / 1_000_000).toFixed(1).replace(/\.0$/, '') + 'M';
226
+ if (n >= 1_000)
227
+ return (n / 1_000).toFixed(1).replace(/\.0$/, '') + 'k';
228
+ return String(n);
229
+ }
230
+ /**
231
+ * Format a percentage change with sign and arrow.
232
+ */
233
+ export function formatChange(pct) {
234
+ if (pct === null)
235
+ return 'n/a';
236
+ const sign = pct > 0 ? '+' : '';
237
+ return `${sign}${pct}%`;
238
+ }
239
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/tools/helpers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ import { GscClient } from '../core/GscClient.js';
2
+ import type { PropertyStatus } from '../types/index.js';
3
+ export declare function listProperties(gscClient: GscClient): Promise<PropertyStatus[]>;
4
+ //# sourceMappingURL=list-properties.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-properties.d.ts","sourceRoot":"","sources":["../../src/tools/list-properties.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAGxD,wBAAsB,cAAc,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC,CAgCpF"}
@@ -0,0 +1,35 @@
1
+ import { existsSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { Database } from '../core/Database.js';
4
+ import { sanitizeSiteUrl, getDataDir } from './helpers.js';
5
+ export async function listProperties(gscClient) {
6
+ const properties = await gscClient.listProperties();
7
+ const dataDir = getDataDir();
8
+ const results = [];
9
+ for (const prop of properties) {
10
+ const dbFilename = sanitizeSiteUrl(prop.siteUrl) + '.db';
11
+ const dbPath = join(dataDir, dbFilename);
12
+ let lastSyncedAt = null;
13
+ let rowCount = null;
14
+ if (existsSync(dbPath)) {
15
+ const db = new Database(dbPath);
16
+ try {
17
+ const meta = db.getPropertyMeta(prop.siteUrl);
18
+ lastSyncedAt = meta?.lastSyncedAt || null;
19
+ rowCount = db.getRowCount();
20
+ }
21
+ finally {
22
+ db.close();
23
+ }
24
+ }
25
+ results.push({
26
+ siteUrl: prop.siteUrl,
27
+ permissionLevel: prop.permissionLevel,
28
+ lastSyncedAt,
29
+ rowCount,
30
+ dbPath: existsSync(dbPath) ? dbPath : null,
31
+ });
32
+ }
33
+ return results;
34
+ }
35
+ //# sourceMappingURL=list-properties.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list-properties.js","sourceRoot":"","sources":["../../src/tools/list-properties.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAE5B,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAE3D,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,SAAoB;IACvD,MAAM,UAAU,GAAG,MAAM,SAAS,CAAC,cAAc,EAAE,CAAC;IACpD,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,MAAM,OAAO,GAAqB,EAAE,CAAC;IAErC,KAAK,MAAM,IAAI,IAAI,UAAU,EAAE,CAAC;QAC9B,MAAM,UAAU,GAAG,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,KAAK,CAAC;QACzD,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QACzC,IAAI,YAAY,GAAkB,IAAI,CAAC;QACvC,IAAI,QAAQ,GAAkB,IAAI,CAAC;QAEnC,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;YACvB,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;YAChC,IAAI,CAAC;gBACH,MAAM,IAAI,GAAG,EAAE,CAAC,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAC9C,YAAY,GAAG,IAAI,EAAE,YAAY,IAAI,IAAI,CAAC;gBAC1C,QAAQ,GAAG,EAAE,CAAC,WAAW,EAAE,CAAC;YAC9B,CAAC;oBAAS,CAAC;gBACT,EAAE,CAAC,KAAK,EAAE,CAAC;YACb,CAAC;QACH,CAAC;QAED,OAAO,CAAC,IAAI,CAAC;YACX,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,eAAe,EAAE,IAAI,CAAC,eAAe;YACrC,YAAY;YACZ,QAAQ;YACR,MAAM,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI;SAC3C,CAAC,CAAC;IACL,CAAC;IAED,OAAO,OAAO,CAAC;AACjB,CAAC"}
@@ -0,0 +1,12 @@
1
+ export interface QueryDataArgs {
2
+ siteUrl: string;
3
+ sql: string;
4
+ params?: any[];
5
+ }
6
+ export interface QueryDataResult {
7
+ columns: string[];
8
+ rows: any[];
9
+ rowCount: number;
10
+ }
11
+ export declare function queryData(args: QueryDataArgs): QueryDataResult;
12
+ //# sourceMappingURL=query-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-data.d.ts","sourceRoot":"","sources":["../../src/tools/query-data.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,GAAG,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,IAAI,EAAE,GAAG,EAAE,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,eAAe,CAgB9D"}
@@ -0,0 +1,20 @@
1
+ import { existsSync } from 'fs';
2
+ import { Database } from '../core/Database.js';
3
+ import { getDbPath } from './helpers.js';
4
+ export function queryData(args) {
5
+ const { siteUrl, sql, params = [] } = args;
6
+ const dbPath = getDbPath(siteUrl);
7
+ if (!existsSync(dbPath)) {
8
+ throw new Error(`No database found for "${siteUrl}". Run sync_gsc_data first.`);
9
+ }
10
+ const db = new Database(dbPath);
11
+ try {
12
+ const rows = db.executeReadOnlyQuery(sql, params);
13
+ const columns = rows.length > 0 ? Object.keys(rows[0]) : [];
14
+ return { columns, rows, rowCount: rows.length };
15
+ }
16
+ finally {
17
+ db.close();
18
+ }
19
+ }
20
+ //# sourceMappingURL=query-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query-data.js","sourceRoot":"","sources":["../../src/tools/query-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAChC,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAczC,MAAM,UAAU,SAAS,CAAC,IAAmB;IAC3C,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,EAAE,EAAE,GAAG,IAAI,CAAC;IAC3C,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAElC,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,KAAK,CAAC,0BAA0B,OAAO,6BAA6B,CAAC,CAAC;IAClF,CAAC;IAED,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,EAAE,CAAC,oBAAoB,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC5D,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;IAClD,CAAC;YAAS,CAAC;QACT,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}
@@ -0,0 +1,11 @@
1
+ import { GscClient } from '../core/GscClient.js';
2
+ import type { SyncResult } from '../types/index.js';
3
+ export interface SyncDataArgs {
4
+ siteUrl: string;
5
+ startDate?: string;
6
+ endDate?: string;
7
+ dimensions?: string[];
8
+ searchType?: 'web' | 'discover' | 'googleNews' | 'image' | 'video';
9
+ }
10
+ export declare function syncData(gscClient: GscClient, args: SyncDataArgs, signal?: AbortSignal): Promise<SyncResult>;
11
+ //# sourceMappingURL=sync-data.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-data.d.ts","sourceRoot":"","sources":["../../src/tools/sync-data.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEjD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAGpD,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,UAAU,CAAC,EAAE,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,CAAC;CACpE;AAKD,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,SAAS,EACpB,IAAI,EAAE,YAAY,EAClB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,UAAU,CAAC,CAiErB"}
@@ -0,0 +1,62 @@
1
+ import { Database } from '../core/Database.js';
2
+ import { DataSync } from '../core/DataSync.js';
3
+ import { getDbPath, defaultStartDate, defaultEndDate } from './helpers.js';
4
+ // Track active syncs to prevent duplicate concurrent syncs
5
+ const activeSyncs = new Set();
6
+ export async function syncData(gscClient, args, signal) {
7
+ const { siteUrl, endDate = defaultEndDate(), dimensions, } = args;
8
+ let { startDate } = args;
9
+ // Prevent duplicate syncs for the same property
10
+ if (activeSyncs.has(siteUrl)) {
11
+ return {
12
+ siteUrl,
13
+ dateFrom: startDate || '',
14
+ dateTo: endDate,
15
+ rowsFetched: 0,
16
+ rowsInserted: 0,
17
+ durationMs: 0,
18
+ status: 'error',
19
+ error: `A sync is already running for ${siteUrl}. Wait for it to complete or restart Claude Desktop to cancel it.`,
20
+ };
21
+ }
22
+ const dbPath = getDbPath(siteUrl);
23
+ const db = new Database(dbPath);
24
+ activeSyncs.add(siteUrl);
25
+ try {
26
+ // Incremental sync: if no explicit start date, resume from last synced date
27
+ if (!startDate) {
28
+ const lastDate = db.getLastSyncDate(siteUrl);
29
+ if (lastDate) {
30
+ const d = new Date(lastDate);
31
+ d.setDate(d.getDate() + 1);
32
+ const nextDay = d.toISOString().slice(0, 10);
33
+ if (nextDay <= endDate) {
34
+ startDate = nextDay;
35
+ }
36
+ else {
37
+ return {
38
+ siteUrl,
39
+ dateFrom: lastDate,
40
+ dateTo: endDate,
41
+ rowsFetched: 0,
42
+ rowsInserted: 0,
43
+ durationMs: 0,
44
+ status: 'already_current',
45
+ };
46
+ }
47
+ }
48
+ else {
49
+ startDate = defaultStartDate();
50
+ }
51
+ }
52
+ db.upsertPropertyMeta(siteUrl, 'syncing');
53
+ const sync = new DataSync(db, gscClient);
54
+ const result = await sync.syncProperty(siteUrl, startDate, endDate, dimensions, args.searchType, signal);
55
+ return result;
56
+ }
57
+ finally {
58
+ activeSyncs.delete(siteUrl);
59
+ db.close();
60
+ }
61
+ }
62
+ //# sourceMappingURL=sync-data.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sync-data.js","sourceRoot":"","sources":["../../src/tools/sync-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,QAAQ,EAAE,MAAM,qBAAqB,CAAC;AAE/C,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAU3E,2DAA2D;AAC3D,MAAM,WAAW,GAAG,IAAI,GAAG,EAAU,CAAC;AAEtC,MAAM,CAAC,KAAK,UAAU,QAAQ,CAC5B,SAAoB,EACpB,IAAkB,EAClB,MAAoB;IAEpB,MAAM,EACJ,OAAO,EACP,OAAO,GAAG,cAAc,EAAE,EAC1B,UAAU,GACX,GAAG,IAAI,CAAC;IAET,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,CAAC;IAEzB,gDAAgD;IAChD,IAAI,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,OAAO;YACL,OAAO;YACP,QAAQ,EAAE,SAAS,IAAI,EAAE;YACzB,MAAM,EAAE,OAAO;YACf,WAAW,EAAE,CAAC;YACd,YAAY,EAAE,CAAC;YACf,UAAU,EAAE,CAAC;YACb,MAAM,EAAE,OAAO;YACf,KAAK,EAAE,iCAAiC,OAAO,mEAAmE;SACnH,CAAC;IACJ,CAAC;IAED,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,MAAM,CAAC,CAAC;IAChC,WAAW,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;IAEzB,IAAI,CAAC;QACH,4EAA4E;QAC5E,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,QAAQ,GAAG,EAAE,CAAC,eAAe,CAAC,OAAO,CAAC,CAAC;YAC7C,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,QAAQ,CAAC,CAAC;gBAC7B,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;gBAC3B,MAAM,OAAO,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;gBAC7C,IAAI,OAAO,IAAI,OAAO,EAAE,CAAC;oBACvB,SAAS,GAAG,OAAO,CAAC;gBACtB,CAAC;qBAAM,CAAC;oBACN,OAAO;wBACL,OAAO;wBACP,QAAQ,EAAE,QAAQ;wBAClB,MAAM,EAAE,OAAO;wBACf,WAAW,EAAE,CAAC;wBACd,YAAY,EAAE,CAAC;wBACf,UAAU,EAAE,CAAC;wBACb,MAAM,EAAE,iBAAiB;qBAC1B,CAAC;gBACJ,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,SAAS,GAAG,gBAAgB,EAAE,CAAC;YACjC,CAAC;QACH,CAAC;QAED,EAAE,CAAC,kBAAkB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;QAE1C,MAAM,IAAI,GAAG,IAAI,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,YAAY,CACpC,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,MAAM,CACjE,CAAC;QAEF,OAAO,MAAM,CAAC;IAChB,CAAC;YAAS,CAAC;QACT,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5B,EAAE,CAAC,KAAK,EAAE,CAAC;IACb,CAAC;AACH,CAAC"}