@houtini/seo-audit-console 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.
Files changed (166) hide show
  1. package/LICENSE +92 -0
  2. package/README.md +211 -0
  3. package/dist/audit/checks.d.ts +35 -0
  4. package/dist/audit/checks.d.ts.map +1 -0
  5. package/dist/audit/checks.js +1475 -0
  6. package/dist/audit/checks.js.map +1 -0
  7. package/dist/audit/drift.d.ts +40 -0
  8. package/dist/audit/drift.d.ts.map +1 -0
  9. package/dist/audit/drift.js +148 -0
  10. package/dist/audit/drift.js.map +1 -0
  11. package/dist/audit/engine.d.ts +33 -0
  12. package/dist/audit/engine.d.ts.map +1 -0
  13. package/dist/audit/engine.js +186 -0
  14. package/dist/audit/engine.js.map +1 -0
  15. package/dist/audit/opportunities.d.ts +23 -0
  16. package/dist/audit/opportunities.d.ts.map +1 -0
  17. package/dist/audit/opportunities.js +149 -0
  18. package/dist/audit/opportunities.js.map +1 -0
  19. package/dist/audit/report.d.ts +11 -0
  20. package/dist/audit/report.d.ts.map +1 -0
  21. package/dist/audit/report.js +59 -0
  22. package/dist/audit/report.js.map +1 -0
  23. package/dist/audit/schema-validate.d.ts +35 -0
  24. package/dist/audit/schema-validate.d.ts.map +1 -0
  25. package/dist/audit/schema-validate.js +293 -0
  26. package/dist/audit/schema-validate.js.map +1 -0
  27. package/dist/audit/templates.d.ts +43 -0
  28. package/dist/audit/templates.d.ts.map +1 -0
  29. package/dist/audit/templates.js +129 -0
  30. package/dist/audit/templates.js.map +1 -0
  31. package/dist/audit/topicGaps.d.ts +42 -0
  32. package/dist/audit/topicGaps.d.ts.map +1 -0
  33. package/dist/audit/topicGaps.js +181 -0
  34. package/dist/audit/topicGaps.js.map +1 -0
  35. package/dist/core/AuditDatabase.d.ts +33 -0
  36. package/dist/core/AuditDatabase.d.ts.map +1 -0
  37. package/dist/core/AuditDatabase.js +481 -0
  38. package/dist/core/AuditDatabase.js.map +1 -0
  39. package/dist/core/Backlinks.d.ts +33 -0
  40. package/dist/core/Backlinks.d.ts.map +1 -0
  41. package/dist/core/Backlinks.js +110 -0
  42. package/dist/core/Backlinks.js.map +1 -0
  43. package/dist/core/Crawler.d.ts +23 -0
  44. package/dist/core/Crawler.d.ts.map +1 -0
  45. package/dist/core/Crawler.js +588 -0
  46. package/dist/core/Crawler.js.map +1 -0
  47. package/dist/core/DataForSeoClient.d.ts +86 -0
  48. package/dist/core/DataForSeoClient.d.ts.map +1 -0
  49. package/dist/core/DataForSeoClient.js +232 -0
  50. package/dist/core/DataForSeoClient.js.map +1 -0
  51. package/dist/core/Entities.d.ts +23 -0
  52. package/dist/core/Entities.d.ts.map +1 -0
  53. package/dist/core/Entities.js +62 -0
  54. package/dist/core/Entities.js.map +1 -0
  55. package/dist/core/GscClient.d.ts +22 -0
  56. package/dist/core/GscClient.d.ts.map +1 -0
  57. package/dist/core/GscClient.js +93 -0
  58. package/dist/core/GscClient.js.map +1 -0
  59. package/dist/core/GscSync.d.ts +20 -0
  60. package/dist/core/GscSync.d.ts.map +1 -0
  61. package/dist/core/GscSync.js +133 -0
  62. package/dist/core/GscSync.js.map +1 -0
  63. package/dist/core/JobManager.d.ts +30 -0
  64. package/dist/core/JobManager.d.ts.map +1 -0
  65. package/dist/core/JobManager.js +68 -0
  66. package/dist/core/JobManager.js.map +1 -0
  67. package/dist/core/RankTracker.d.ts +25 -0
  68. package/dist/core/RankTracker.d.ts.map +1 -0
  69. package/dist/core/RankTracker.js +78 -0
  70. package/dist/core/RankTracker.js.map +1 -0
  71. package/dist/core/Refresh.d.ts +32 -0
  72. package/dist/core/Refresh.d.ts.map +1 -0
  73. package/dist/core/Refresh.js +73 -0
  74. package/dist/core/Refresh.js.map +1 -0
  75. package/dist/core/UrlInspector.d.ts +22 -0
  76. package/dist/core/UrlInspector.d.ts.map +1 -0
  77. package/dist/core/UrlInspector.js +92 -0
  78. package/dist/core/UrlInspector.js.map +1 -0
  79. package/dist/core/WikidataClient.d.ts +19 -0
  80. package/dist/core/WikidataClient.d.ts.map +1 -0
  81. package/dist/core/WikidataClient.js +59 -0
  82. package/dist/core/WikidataClient.js.map +1 -0
  83. package/dist/core/agentReadiness.d.ts +34 -0
  84. package/dist/core/agentReadiness.d.ts.map +1 -0
  85. package/dist/core/agentReadiness.js +119 -0
  86. package/dist/core/agentReadiness.js.map +1 -0
  87. package/dist/core/ctrModel.d.ts +2 -0
  88. package/dist/core/ctrModel.d.ts.map +1 -0
  89. package/dist/core/ctrModel.js +6 -0
  90. package/dist/core/ctrModel.js.map +1 -0
  91. package/dist/core/dashboardData.d.ts +312 -0
  92. package/dist/core/dashboardData.d.ts.map +1 -0
  93. package/dist/core/dashboardData.js +550 -0
  94. package/dist/core/dashboardData.js.map +1 -0
  95. package/dist/core/dataStorage.d.ts +42 -0
  96. package/dist/core/dataStorage.d.ts.map +1 -0
  97. package/dist/core/dataStorage.js +193 -0
  98. package/dist/core/dataStorage.js.map +1 -0
  99. package/dist/core/draftBrief.d.ts +27 -0
  100. package/dist/core/draftBrief.d.ts.map +1 -0
  101. package/dist/core/draftBrief.js +69 -0
  102. package/dist/core/draftBrief.js.map +1 -0
  103. package/dist/core/extract.d.ts +67 -0
  104. package/dist/core/extract.d.ts.map +1 -0
  105. package/dist/core/extract.js +262 -0
  106. package/dist/core/extract.js.map +1 -0
  107. package/dist/core/gscFreshness.d.ts +18 -0
  108. package/dist/core/gscFreshness.d.ts.map +1 -0
  109. package/dist/core/gscFreshness.js +32 -0
  110. package/dist/core/gscFreshness.js.map +1 -0
  111. package/dist/core/linkGraph.d.ts +19 -0
  112. package/dist/core/linkGraph.d.ts.map +1 -0
  113. package/dist/core/linkGraph.js +125 -0
  114. package/dist/core/linkGraph.js.map +1 -0
  115. package/dist/core/passageScore.d.ts +19 -0
  116. package/dist/core/passageScore.d.ts.map +1 -0
  117. package/dist/core/passageScore.js +59 -0
  118. package/dist/core/passageScore.js.map +1 -0
  119. package/dist/core/paths.d.ts +5 -0
  120. package/dist/core/paths.d.ts.map +1 -0
  121. package/dist/core/paths.js +15 -0
  122. package/dist/core/paths.js.map +1 -0
  123. package/dist/core/queryData.d.ts +35 -0
  124. package/dist/core/queryData.d.ts.map +1 -0
  125. package/dist/core/queryData.js +200 -0
  126. package/dist/core/queryData.js.map +1 -0
  127. package/dist/core/reranker.d.ts +8 -0
  128. package/dist/core/reranker.d.ts.map +1 -0
  129. package/dist/core/reranker.js +68 -0
  130. package/dist/core/reranker.js.map +1 -0
  131. package/dist/core/robots.d.ts +11 -0
  132. package/dist/core/robots.d.ts.map +1 -0
  133. package/dist/core/robots.js +76 -0
  134. package/dist/core/robots.js.map +1 -0
  135. package/dist/core/sitemap.d.ts +15 -0
  136. package/dist/core/sitemap.d.ts.map +1 -0
  137. package/dist/core/sitemap.js +100 -0
  138. package/dist/core/sitemap.js.map +1 -0
  139. package/dist/core/sql.d.ts +6 -0
  140. package/dist/core/sql.d.ts.map +1 -0
  141. package/dist/core/sql.js +6 -0
  142. package/dist/core/sql.js.map +1 -0
  143. package/dist/core/types.d.ts +22 -0
  144. package/dist/core/types.d.ts.map +1 -0
  145. package/dist/core/types.js +2 -0
  146. package/dist/core/types.js.map +1 -0
  147. package/dist/core/url-key.d.ts +39 -0
  148. package/dist/core/url-key.d.ts.map +1 -0
  149. package/dist/core/url-key.js +106 -0
  150. package/dist/core/url-key.js.map +1 -0
  151. package/dist/generators/index.d.ts +45 -0
  152. package/dist/generators/index.d.ts.map +1 -0
  153. package/dist/generators/index.js +184 -0
  154. package/dist/generators/index.js.map +1 -0
  155. package/dist/index.d.ts +3 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +10 -0
  158. package/dist/index.js.map +1 -0
  159. package/dist/server.d.ts +7 -0
  160. package/dist/server.d.ts.map +1 -0
  161. package/dist/server.js +1387 -0
  162. package/dist/server.js.map +1 -0
  163. package/dist/src/ui/dashboard.html +347 -0
  164. package/dist/src/ui/sync-progress.html +104 -0
  165. package/package.json +101 -0
  166. package/server.json +57 -0
@@ -0,0 +1,20 @@
1
+ import { GscClient } from './GscClient.js';
2
+ import type { FetchOptions } from './types.js';
3
+ export declare const FULL_DIMENSIONS: string[];
4
+ export interface SyncResult {
5
+ siteUrl: string;
6
+ rowsFetched: number;
7
+ startDate: string;
8
+ endDate: string;
9
+ }
10
+ /**
11
+ * Fetch GSC search analytics into a property's SQLite DB, computing the
12
+ * normalised `page_key` on write so it joins to crawl data later.
13
+ */
14
+ export declare class GscSync {
15
+ private readonly gsc;
16
+ private readonly dataDir;
17
+ constructor(gsc: GscClient, dataDir: string);
18
+ run(siteUrl: string, options: FetchOptions, update: (p: Record<string, unknown>) => void, signal: AbortSignal): Promise<SyncResult>;
19
+ }
20
+ //# sourceMappingURL=GscSync.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GscSync.d.ts","sourceRoot":"","sources":["../../src/core/GscSync.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAG3C,OAAO,KAAK,EAAE,YAAY,EAAa,MAAM,YAAY,CAAC;AAM1D,eAAO,MAAM,eAAe,UAAiD,CAAC;AAE9E,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,qBAAa,OAAO;IACN,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAa,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAxC,GAAG,EAAE,SAAS,EAAmB,OAAO,EAAE,MAAM;IAEvE,GAAG,CACP,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,YAAY,EACrB,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,EAC5C,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,UAAU,CAAC;CA8GvB"}
@@ -0,0 +1,133 @@
1
+ import { AuditDatabase } from './AuditDatabase.js';
2
+ import { dbPathFor } from './paths.js';
3
+ import { urlKey, hostFormForProperty } from './url-key.js';
4
+ // Lean default: date×query×page is everything the audit + merged checks need, without
5
+ // the device×country multiplication that explodes row counts on large sites. Pass
6
+ // FULL_DIMENSIONS (segments) to also populate the device/country breakdowns.
7
+ const LITE_DIMENSIONS = ['date', 'query', 'page'];
8
+ export const FULL_DIMENSIONS = ['date', 'query', 'page', 'device', 'country'];
9
+ /**
10
+ * Fetch GSC search analytics into a property's SQLite DB, computing the
11
+ * normalised `page_key` on write so it joins to crawl data later.
12
+ */
13
+ export class GscSync {
14
+ gsc;
15
+ dataDir;
16
+ constructor(gsc, dataDir) {
17
+ this.gsc = gsc;
18
+ this.dataDir = dataDir;
19
+ }
20
+ async run(siteUrl, options, update, signal) {
21
+ const db = new AuditDatabase(dbPathFor(this.dataDir, siteUrl));
22
+ try {
23
+ const hostForm = hostFormForProperty(siteUrl);
24
+ db.upsertProperty(siteUrl, hostForm ?? 'asis');
25
+ const insert = db.db.prepare(`INSERT INTO search_analytics
26
+ (date, query, page, page_key, device, country, search_type, clicks, impressions, ctr, position)
27
+ VALUES (@date, @query, @page, @page_key, @device, @country, @search_type, @clicks, @impressions, @ctr, @position)
28
+ ON CONFLICT(date, query, page, device, country) DO UPDATE SET
29
+ clicks = excluded.clicks, impressions = excluded.impressions, search_type = excluded.search_type,
30
+ ctr = excluded.ctr, position = excluded.position, page_key = excluded.page_key`);
31
+ const insertMany = db.db.transaction((records) => {
32
+ for (const r of records)
33
+ insert.run(r);
34
+ });
35
+ let total = 0;
36
+ let options2 = { ...options, dimensions: options.dimensions ?? LITE_DIMENSIONS };
37
+ let dims = options2.dimensions;
38
+ // Summary/totals SUM across one grouping. Mixing lite (device='') with segmented
39
+ // (device='DESKTOP'…) rows would double-count, so the table holds ONE grouping shape.
40
+ const cnt = (sql) => db.db.prepare(sql).get().n;
41
+ const hasLite = cnt(`SELECT COUNT(*) n FROM search_analytics WHERE device = ''`) > 0;
42
+ const hasSegmented = cnt(`SELECT COUNT(*) n FROM search_analytics WHERE device <> ''`) > 0;
43
+ // PRESERVE existing history: a routine (default/lite) sync over segmented history must NOT
44
+ // wipe it — keep segmenting instead, so a short window sync never destroys months of data.
45
+ if (!dims.includes('device') && hasSegmented && !hasLite) {
46
+ options2 = { ...options2, dimensions: FULL_DIMENSIONS };
47
+ dims = options2.dimensions;
48
+ }
49
+ const wantsSegments = dims.includes('device');
50
+ // Only wipe on a genuine, unavoidable shape conflict (e.g. an explicit lite→segmented
51
+ // upgrade, or a pre-existing mixed table) — never on the routine preserve path above.
52
+ // The wipe is DEFERRED into the same transaction as the first fetched batch: if the
53
+ // API call fails or the job is aborted before any rows arrive, months of history are
54
+ // NOT destroyed and replaced by nothing.
55
+ let mustWipe = (wantsSegments && hasLite) || (!wantsSegments && hasSegmented);
56
+ // Search-type guard (mirrors the lite/segmented shape-guard): the unique index
57
+ // (date,query,page,device,country) does NOT include search_type — rebuilding it on existing
58
+ // rows would rewrite months of history — so the table holds ONE search type at a time.
59
+ // Syncing a different type (e.g. discover over web history) would silently mix/overwrite
60
+ // rows; treat it as a shape conflict instead, and let the wipe ride the first batch's
61
+ // transaction exactly like the lite/segmented wipe (a failed fetch destroys nothing).
62
+ const wantType = options.searchType ?? 'web';
63
+ const storedTypes = db.db
64
+ .prepare(`SELECT DISTINCT COALESCE(search_type, 'web') t FROM search_analytics`)
65
+ .all().map(r => r.t);
66
+ if (storedTypes.length && (storedTypes.length > 1 || storedTypes[0] !== wantType))
67
+ mustWipe = true;
68
+ const wipeAndInsert = db.db.transaction((records) => {
69
+ db.db.exec('DELETE FROM search_analytics');
70
+ for (const r of records)
71
+ insert.run(r);
72
+ });
73
+ // ── Incremental resume ─────────────────────────────────────────────────────────────────
74
+ // The first sync pulls the requested window; every sync after that only fetches from the
75
+ // last stored date minus a lag buffer (GSC restates the most recent ~3 days), instead of
76
+ // re-pulling the whole window. Turns a 30-minute, 1.8M-row refresh into a seconds-long delta.
77
+ // Runs AFTER the mode-guard: if the table was just wiped (shape conflict) MAX(date) is null →
78
+ // full window. `fullResync` forces the full window. Only ever NARROWS the window (never widens
79
+ // past the requested start), and resumes from wherever the data left off (covers any gap).
80
+ const LAG_DAYS = 3;
81
+ if (!options.fullResync && !mustWipe) { // a pending wipe means the stored MAX(date) is the OLD shape's — pull the full window
82
+ const mx = db.db.prepare('SELECT MAX(date) d FROM search_analytics').get().d;
83
+ if (mx) {
84
+ const resume = new Date(Date.parse(mx) - LAG_DAYS * 86400000).toISOString().slice(0, 10);
85
+ // Already synced past the requested window (e.g. a historical/comparison pull whose
86
+ // endDate predates the stored data): no-op instead of calling GSC with start > end.
87
+ if (resume > options2.endDate) {
88
+ db.db.prepare(`UPDATE property_meta SET last_synced_at = datetime('now') WHERE site_url = ?`).run(siteUrl);
89
+ return { siteUrl, rowsFetched: 0, startDate: options2.startDate, endDate: options2.endDate };
90
+ }
91
+ if (resume > options2.startDate) {
92
+ options2 = { ...options2, startDate: resume };
93
+ }
94
+ }
95
+ }
96
+ await this.gsc.fetchSearchAnalytics(siteUrl, options2, signal, ({ rows }) => {
97
+ const records = rows.map((row) => {
98
+ const keys = row.keys ?? [];
99
+ const rec = {};
100
+ dims.forEach((d, i) => (rec[d] = keys[i] ?? null));
101
+ const page = rec.page;
102
+ return {
103
+ date: rec.date,
104
+ query: rec.query ?? null,
105
+ page: page ?? null,
106
+ page_key: page ? urlKey(page, { hostForm }) : null,
107
+ device: rec.device ?? '', // '' = not segmented (lite); keeps the unique index dedup working
108
+ country: rec.country ?? '',
109
+ search_type: wantType,
110
+ clicks: row.clicks,
111
+ impressions: row.impressions,
112
+ ctr: row.ctr,
113
+ position: row.position,
114
+ };
115
+ });
116
+ if (mustWipe) {
117
+ wipeAndInsert(records);
118
+ mustWipe = false;
119
+ }
120
+ else
121
+ insertMany(records);
122
+ total += records.length;
123
+ update({ siteUrl, rowsFetched: total });
124
+ });
125
+ db.db.prepare(`UPDATE property_meta SET last_synced_at = datetime('now') WHERE site_url = ?`).run(siteUrl);
126
+ return { siteUrl, rowsFetched: total, startDate: options2.startDate, endDate: options2.endDate };
127
+ }
128
+ finally {
129
+ db.close();
130
+ }
131
+ }
132
+ }
133
+ //# sourceMappingURL=GscSync.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"GscSync.js","sourceRoot":"","sources":["../../src/core/GscSync.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ import type { JobState } from './types.js';
2
+ export interface Job {
3
+ id: string;
4
+ type: string;
5
+ state: JobState;
6
+ startedAt: string;
7
+ updatedAt: string;
8
+ progress: Record<string, unknown>;
9
+ error?: string;
10
+ result?: unknown;
11
+ }
12
+ type JobFn = (update: (p: Record<string, unknown>) => void, signal: AbortSignal) => Promise<unknown>;
13
+ /**
14
+ * Generic async-job registry — the return-jobId-then-poll pattern that keeps
15
+ * long operations (GSC sync, crawl) off the synchronous MCP tool-call path so
16
+ * they never hit the client timeout. Mirrors better-search-console's sync model.
17
+ */
18
+ export declare class JobManager {
19
+ private jobs;
20
+ private controllers;
21
+ private readonly maxRetained;
22
+ constructor(maxRetained?: number);
23
+ start(type: string, fn: JobFn): string;
24
+ get(id: string): Job | undefined;
25
+ list(): Job[];
26
+ cancel(id: string): boolean;
27
+ private prune;
28
+ }
29
+ export {};
30
+ //# sourceMappingURL=JobManager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JobManager.d.ts","sourceRoot":"","sources":["../../src/core/JobManager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3C,MAAM,WAAW,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,QAAQ,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAClC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,KAAK,KAAK,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,EAAE,MAAM,EAAE,WAAW,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;AAErG;;;;GAIG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,WAAW,CAAsC;IACzD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAEzB,WAAW,SAAK;IAI5B,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,KAAK,GAAG,MAAM;IAkCtC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,GAAG,GAAG,SAAS;IAIhC,IAAI,IAAI,GAAG,EAAE;IAIb,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAO3B,OAAO,CAAC,KAAK;CASd"}
@@ -0,0 +1,68 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ /**
3
+ * Generic async-job registry — the return-jobId-then-poll pattern that keeps
4
+ * long operations (GSC sync, crawl) off the synchronous MCP tool-call path so
5
+ * they never hit the client timeout. Mirrors better-search-console's sync model.
6
+ */
7
+ export class JobManager {
8
+ jobs = new Map();
9
+ controllers = new Map();
10
+ maxRetained;
11
+ constructor(maxRetained = 50) {
12
+ this.maxRetained = maxRetained;
13
+ }
14
+ start(type, fn) {
15
+ const id = randomUUID().slice(0, 8);
16
+ const now = new Date().toISOString();
17
+ const job = { id, type, state: 'running', startedAt: now, updatedAt: now, progress: {} };
18
+ const controller = new AbortController();
19
+ this.jobs.set(id, job);
20
+ this.controllers.set(id, controller);
21
+ this.prune();
22
+ const update = (p) => {
23
+ job.progress = { ...job.progress, ...p };
24
+ job.updatedAt = new Date().toISOString();
25
+ };
26
+ // Run detached — do NOT await. The tool returns the id immediately.
27
+ // Promise.resolve().then(...) so a synchronous throw in fn becomes a rejection
28
+ // (caught below) rather than an uncaught exception that crashes the MCP process.
29
+ Promise.resolve()
30
+ .then(() => fn(update, controller.signal))
31
+ .then(result => {
32
+ job.result = result;
33
+ job.state = controller.signal.aborted ? 'cancelled' : 'completed';
34
+ job.updatedAt = new Date().toISOString();
35
+ })
36
+ .catch((err) => {
37
+ job.state = 'failed';
38
+ job.error = err instanceof Error ? err.message : String(err);
39
+ job.updatedAt = new Date().toISOString();
40
+ })
41
+ .finally(() => this.controllers.delete(id));
42
+ return id;
43
+ }
44
+ get(id) {
45
+ return this.jobs.get(id);
46
+ }
47
+ list() {
48
+ return [...this.jobs.values()].sort((a, b) => (a.startedAt < b.startedAt ? 1 : -1));
49
+ }
50
+ cancel(id) {
51
+ const c = this.controllers.get(id);
52
+ if (!c)
53
+ return false;
54
+ c.abort();
55
+ return true;
56
+ }
57
+ prune() {
58
+ if (this.jobs.size <= this.maxRetained)
59
+ return;
60
+ const done = [...this.jobs.values()]
61
+ .filter(j => j.state === 'completed' || j.state === 'failed' || j.state === 'cancelled')
62
+ .sort((a, b) => (a.updatedAt < b.updatedAt ? -1 : 1));
63
+ while (this.jobs.size > this.maxRetained && done.length) {
64
+ this.jobs.delete(done.shift().id);
65
+ }
66
+ }
67
+ }
68
+ //# sourceMappingURL=JobManager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"JobManager.js","sourceRoot":"","sources":["../../src/core/JobManager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ import { DataForSeoClient } from './DataForSeoClient.js';
2
+ export interface RankTrackResult {
3
+ siteUrl: string;
4
+ target: string;
5
+ periods: number;
6
+ range: {
7
+ start: string;
8
+ end: string;
9
+ } | null;
10
+ cached: boolean;
11
+ cost: number;
12
+ }
13
+ /**
14
+ * Ingests the DataForSEO over-time sequence (monthly rank distribution + ETV) into
15
+ * rank_history, so charts have a real time axis to reconcile against GSC's daily data.
16
+ */
17
+ export declare class RankTracker {
18
+ private readonly dfs;
19
+ private readonly dataDir;
20
+ constructor(dfs: DataForSeoClient, dataDir: string);
21
+ run(siteUrl: string, opts: {
22
+ location?: string | number;
23
+ }, update: (p: Record<string, unknown>) => void, _signal: AbortSignal): Promise<RankTrackResult>;
24
+ }
25
+ //# sourceMappingURL=RankTracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RankTracker.d.ts","sourceRoot":"","sources":["../../src/core/RankTracker.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAIzD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC7C,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAQD;;;GAGG;AACH,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAoB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAA/C,GAAG,EAAE,gBAAgB,EAAmB,OAAO,EAAE,MAAM;IAE9E,GAAG,CACP,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,EACpC,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,EAC5C,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,CAAC;CA2D5B"}
@@ -0,0 +1,78 @@
1
+ import { AuditDatabase } from './AuditDatabase.js';
2
+ import { dbPathFor } from './paths.js';
3
+ import { hostFormForProperty } from './url-key.js';
4
+ function domainFromProperty(siteUrl) {
5
+ return siteUrl.replace(/^sc-domain:/, '').replace(/^https?:\/\//, '').replace(/\/+$/, '');
6
+ }
7
+ const n = (v) => Number(v) || 0;
8
+ /**
9
+ * Ingests the DataForSEO over-time sequence (monthly rank distribution + ETV) into
10
+ * rank_history, so charts have a real time axis to reconcile against GSC's daily data.
11
+ */
12
+ export class RankTracker {
13
+ dfs;
14
+ dataDir;
15
+ constructor(dfs, dataDir) {
16
+ this.dfs = dfs;
17
+ this.dataDir = dataDir;
18
+ }
19
+ async run(siteUrl, opts, update, _signal) {
20
+ const target = domainFromProperty(siteUrl);
21
+ const db = new AuditDatabase(dbPathFor(this.dataDir, siteUrl));
22
+ try {
23
+ db.upsertProperty(siteUrl, hostFormForProperty(siteUrl) ?? 'asis');
24
+ if (opts.location !== undefined)
25
+ db.setDfsLocation(siteUrl, opts.location);
26
+ const location = opts.location ?? db.getDfsLocation(siteUrl);
27
+ const r = await this.dfs.historicalRankOverview(target, location);
28
+ const items = r.tasks[0]?.result?.[0]?.items ?? [];
29
+ const upsert = db.db.prepare(`INSERT INTO rank_history (period, pos_1_3, pos_4_10, pos_11_20, pos_21_100, etv, keyword_count,
30
+ is_new, is_up, is_down, is_lost, estimated_paid_traffic_cost, source, fetched_at)
31
+ VALUES (@period,@pos_1_3,@pos_4_10,@pos_11_20,@pos_21_100,@etv,@keyword_count,
32
+ @is_new,@is_up,@is_down,@is_lost,@estimated_paid_traffic_cost,'dataforseo:historical_rank_overview',datetime('now'))
33
+ ON CONFLICT(period) DO UPDATE SET pos_1_3=excluded.pos_1_3, pos_4_10=excluded.pos_4_10,
34
+ pos_11_20=excluded.pos_11_20, pos_21_100=excluded.pos_21_100, etv=excluded.etv,
35
+ keyword_count=excluded.keyword_count, is_new=excluded.is_new, is_up=excluded.is_up,
36
+ is_down=excluded.is_down, is_lost=excluded.is_lost,
37
+ estimated_paid_traffic_cost=excluded.estimated_paid_traffic_cost, fetched_at=datetime('now')`);
38
+ const rows = items.filter(it => Number.isInteger(it?.year) && Number.isInteger(it?.month)).map(it => {
39
+ const o = it.metrics?.organic ?? {};
40
+ // Guard year/month — a malformed item would upsert an "undefined-NaN" period row
41
+ // that then poisons the dashboard's rank-history axis and date reconciliation.
42
+ const period = `${it.year}-${String(it.month).padStart(2, '0')}`;
43
+ const pos_1_3 = n(o.pos_1) + n(o.pos_2_3);
44
+ const pos_4_10 = n(o.pos_4_10);
45
+ const pos_11_20 = n(o.pos_11_20);
46
+ const pos_21_100 = n(o.pos_21_30) + n(o.pos_31_40) + n(o.pos_41_50) + n(o.pos_51_60) +
47
+ n(o.pos_61_70) + n(o.pos_71_80) + n(o.pos_81_90) + n(o.pos_91_100);
48
+ // DataForSEO names the total field `count`; fall back to summing the buckets
49
+ // so keyword_count isn't a silent 0 if the field name differs.
50
+ return {
51
+ period, pos_1_3, pos_4_10, pos_11_20, pos_21_100,
52
+ etv: n(o.etv),
53
+ keyword_count: n(o.count) || (pos_1_3 + pos_4_10 + pos_11_20 + pos_21_100),
54
+ // Keyword-movement counts + paid-value equivalent — same response, previously dropped.
55
+ is_new: n(o.is_new), is_up: n(o.is_up), is_down: n(o.is_down), is_lost: n(o.is_lost),
56
+ estimated_paid_traffic_cost: o.estimated_paid_traffic_cost != null ? n(o.estimated_paid_traffic_cost) : null,
57
+ };
58
+ });
59
+ const tx = db.db.transaction((rs) => { for (const row of rs)
60
+ upsert.run(row); });
61
+ tx(rows);
62
+ const periods = rows.map(r2 => r2.period).sort();
63
+ update({ siteUrl, periods: periods.length });
64
+ return {
65
+ siteUrl,
66
+ target,
67
+ periods: periods.length,
68
+ range: periods.length ? { start: periods[0], end: periods[periods.length - 1] } : null,
69
+ cached: r.cached,
70
+ cost: r.cost,
71
+ };
72
+ }
73
+ finally {
74
+ db.close();
75
+ }
76
+ }
77
+ }
78
+ //# sourceMappingURL=RankTracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RankTracker.js","sourceRoot":"","sources":["../../src/core/RankTracker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ import { GscSync } from './GscSync.js';
2
+ import { Crawler } from './Crawler.js';
3
+ import { UrlInspector } from './UrlInspector.js';
4
+ import { RankTracker } from './RankTracker.js';
5
+ export interface RefreshOptions {
6
+ gsc?: boolean;
7
+ crawl?: boolean;
8
+ inspect?: boolean;
9
+ ranks?: boolean;
10
+ segments?: boolean;
11
+ full?: boolean;
12
+ location?: string | number;
13
+ startDate?: string;
14
+ endDate?: string;
15
+ maxPages?: number;
16
+ inspectLimit?: number;
17
+ }
18
+ /**
19
+ * Orchestrates a full property refresh — GSC sync → crawl → URL inspection — as a
20
+ * single job. Each phase is skippable, so the same entry point serves both
21
+ * "sync everything" and "just update X". Inspection runs last so it can target
22
+ * top pages by freshly-synced clicks (else crawl in-degree).
23
+ */
24
+ export declare class Refresh {
25
+ private readonly sync;
26
+ private readonly crawler;
27
+ private readonly inspector;
28
+ private readonly rankTracker;
29
+ constructor(sync: GscSync | null, crawler: Crawler, inspector: UrlInspector | null, rankTracker: RankTracker | null);
30
+ run(siteUrl: string, opts: RefreshOptions, update: (p: Record<string, unknown>) => void, signal: AbortSignal): Promise<Record<string, unknown>>;
31
+ }
32
+ //# sourceMappingURL=Refresh.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Refresh.d.ts","sourceRoot":"","sources":["../../src/core/Refresh.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAmB,MAAM,cAAc,CAAC;AACxD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAID;;;;;GAKG;AACH,qBAAa,OAAO;IAEhB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW;gBAHX,IAAI,EAAE,OAAO,GAAG,IAAI,EACpB,OAAO,EAAE,OAAO,EAChB,SAAS,EAAE,YAAY,GAAG,IAAI,EAC9B,WAAW,EAAE,WAAW,GAAG,IAAI;IAG5C,GAAG,CACP,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,EAC5C,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAqDpC"}
@@ -0,0 +1,73 @@
1
+ import { FULL_DIMENSIONS } from './GscSync.js';
2
+ const isoDaysAgo = (d) => new Date(Date.now() - d * 86400000).toISOString().slice(0, 10);
3
+ /**
4
+ * Orchestrates a full property refresh — GSC sync → crawl → URL inspection — as a
5
+ * single job. Each phase is skippable, so the same entry point serves both
6
+ * "sync everything" and "just update X". Inspection runs last so it can target
7
+ * top pages by freshly-synced clicks (else crawl in-degree).
8
+ */
9
+ export class Refresh {
10
+ sync;
11
+ crawler;
12
+ inspector;
13
+ rankTracker;
14
+ constructor(sync, crawler, inspector, rankTracker) {
15
+ this.sync = sync;
16
+ this.crawler = crawler;
17
+ this.inspector = inspector;
18
+ this.rankTracker = rankTracker;
19
+ }
20
+ async run(siteUrl, opts, update, signal) {
21
+ const out = { siteUrl, phases: [] };
22
+ const phases = out.phases;
23
+ if (opts.gsc !== false) {
24
+ if (!this.sync) {
25
+ out.gscSkipped = 'no GSC credentials';
26
+ }
27
+ else {
28
+ update({ phase: 'gsc' });
29
+ phases.push('gsc');
30
+ out.gsc = await this.sync.run(siteUrl, {
31
+ startDate: opts.startDate ?? isoDaysAgo(90),
32
+ endDate: opts.endDate ?? isoDaysAgo(0),
33
+ ...(opts.segments ? { dimensions: FULL_DIMENSIONS } : {}),
34
+ ...(opts.full ? { fullResync: true } : {}),
35
+ }, p => update({ phase: 'gsc', ...p }), signal);
36
+ }
37
+ }
38
+ if (signal.aborted)
39
+ return out;
40
+ if (opts.crawl !== false) {
41
+ update({ phase: 'crawl' });
42
+ phases.push('crawl');
43
+ out.crawl = await this.crawler.run(siteUrl, { maxPages: opts.maxPages }, p => update({ phase: 'crawl', ...p }), signal);
44
+ }
45
+ if (signal.aborted)
46
+ return out;
47
+ if (opts.inspect !== false) {
48
+ if (!this.inspector) {
49
+ out.inspectSkipped = 'no GSC credentials';
50
+ }
51
+ else {
52
+ update({ phase: 'inspect' });
53
+ phases.push('inspect');
54
+ out.inspect = await this.inspector.run(siteUrl, { limit: opts.inspectLimit }, p => update({ phase: 'inspect', ...p }), signal);
55
+ }
56
+ }
57
+ if (signal.aborted)
58
+ return out;
59
+ if (opts.ranks !== false) {
60
+ if (!this.rankTracker) {
61
+ out.ranksSkipped = 'no DataForSEO credentials';
62
+ }
63
+ else {
64
+ update({ phase: 'ranks' });
65
+ phases.push('ranks');
66
+ out.ranks = await this.rankTracker.run(siteUrl, { location: opts.location }, p => update({ phase: 'ranks', ...p }), signal);
67
+ }
68
+ }
69
+ update({ phase: 'done' });
70
+ return out;
71
+ }
72
+ }
73
+ //# sourceMappingURL=Refresh.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Refresh.js","sourceRoot":"","sources":["../../src/core/Refresh.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import { GscClient } from './GscClient.js';
2
+ export interface InspectResult {
3
+ siteUrl: string;
4
+ inspected: number;
5
+ failed: number;
6
+ }
7
+ /**
8
+ * Sampled GSC URL Inspection pass → url_inspection table. The API is quota-limited
9
+ * (~2k/day, ~600/min per site), so we inspect priority URLs only: top pages by GSC
10
+ * clicks if synced, else top crawled pages by in-degree.
11
+ */
12
+ export declare class UrlInspector {
13
+ private readonly gsc;
14
+ private readonly dataDir;
15
+ constructor(gsc: GscClient, dataDir: string);
16
+ private targetUrls;
17
+ run(siteUrl: string, opts: {
18
+ limit?: number;
19
+ delayMs?: number;
20
+ }, update: (p: Record<string, unknown>) => void, signal: AbortSignal): Promise<InspectResult>;
21
+ }
22
+ //# sourceMappingURL=UrlInspector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"UrlInspector.d.ts","sourceRoot":"","sources":["../../src/core/UrlInspector.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAI3C,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;CAChB;AAID;;;;GAIG;AACH,qBAAa,YAAY;IACX,OAAO,CAAC,QAAQ,CAAC,GAAG;IAAa,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAxC,GAAG,EAAE,SAAS,EAAmB,OAAO,EAAE,MAAM;IAE7E,OAAO,CAAC,UAAU;IAcZ,GAAG,CACP,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,EAC1C,MAAM,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,EAC5C,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC,aAAa,CAAC;CA4D1B"}
@@ -0,0 +1,92 @@
1
+ import { AuditDatabase } from './AuditDatabase.js';
2
+ import { dbPathFor } from './paths.js';
3
+ import { urlKey, hostFormForProperty } from './url-key.js';
4
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
5
+ /**
6
+ * Sampled GSC URL Inspection pass → url_inspection table. The API is quota-limited
7
+ * (~2k/day, ~600/min per site), so we inspect priority URLs only: top pages by GSC
8
+ * clicks if synced, else top crawled pages by in-degree.
9
+ */
10
+ export class UrlInspector {
11
+ gsc;
12
+ dataDir;
13
+ constructor(gsc, dataDir) {
14
+ this.gsc = gsc;
15
+ this.dataDir = dataDir;
16
+ }
17
+ targetUrls(db, limit) {
18
+ const byClicks = db.db
19
+ .prepare(`SELECT page AS url FROM search_analytics WHERE page IS NOT NULL
20
+ GROUP BY page ORDER BY SUM(clicks) DESC LIMIT ?`)
21
+ .all(limit);
22
+ if (byClicks.length)
23
+ return byClicks.map(r => r.url);
24
+ const byLinks = db.db
25
+ .prepare(`SELECT url FROM pages WHERE status_code = 200 ORDER BY inlink_count DESC LIMIT ?`)
26
+ .all(limit);
27
+ return byLinks.map(r => r.url);
28
+ }
29
+ async run(siteUrl, opts, update, signal) {
30
+ const limit = Math.max(1, Math.min(opts.limit ?? 50, 500));
31
+ const delayMs = opts.delayMs ?? 200;
32
+ const hostForm = hostFormForProperty(siteUrl);
33
+ const db = new AuditDatabase(dbPathFor(this.dataDir, siteUrl));
34
+ try {
35
+ const urls = this.targetUrls(db, limit);
36
+ const upsert = db.db.prepare(`INSERT INTO url_inspection
37
+ (url_key, url, verdict, coverage_state, indexing_state, robots_txt_state, page_fetch_state,
38
+ last_crawl_time, google_canonical, user_canonical, crawled_as, mobile_usability, rich_results, referring_urls, inspected_at)
39
+ VALUES (@url_key,@url,@verdict,@coverage_state,@indexing_state,@robots_txt_state,@page_fetch_state,
40
+ @last_crawl_time,@google_canonical,@user_canonical,@crawled_as,@mobile_usability,@rich_results,@referring_urls,datetime('now'))
41
+ ON CONFLICT(url_key) DO UPDATE SET
42
+ verdict=excluded.verdict, coverage_state=excluded.coverage_state, indexing_state=excluded.indexing_state,
43
+ robots_txt_state=excluded.robots_txt_state, page_fetch_state=excluded.page_fetch_state,
44
+ last_crawl_time=excluded.last_crawl_time, google_canonical=excluded.google_canonical,
45
+ user_canonical=excluded.user_canonical, crawled_as=excluded.crawled_as,
46
+ mobile_usability=excluded.mobile_usability, rich_results=excluded.rich_results,
47
+ referring_urls=excluded.referring_urls, inspected_at=datetime('now')`);
48
+ let inspected = 0;
49
+ let failed = 0;
50
+ for (const url of urls) {
51
+ if (signal.aborted)
52
+ break;
53
+ try {
54
+ const r = await this.gsc.inspectUrl(siteUrl, url);
55
+ const idx = r.indexStatusResult ?? {};
56
+ upsert.run({
57
+ url_key: urlKey(url, { hostForm }),
58
+ url,
59
+ verdict: idx.verdict ?? null,
60
+ coverage_state: idx.coverageState ?? null,
61
+ indexing_state: idx.indexingState ?? null,
62
+ robots_txt_state: idx.robotsTxtState ?? null,
63
+ page_fetch_state: idx.pageFetchState ?? null,
64
+ last_crawl_time: idx.lastCrawlTime ?? null,
65
+ google_canonical: idx.googleCanonical ?? null,
66
+ user_canonical: idx.userCanonical ?? null,
67
+ crawled_as: idx.crawledAs ?? null,
68
+ mobile_usability: r.mobileUsabilityResult?.verdict ?? null,
69
+ rich_results: r.richResultsResult ? JSON.stringify(r.richResultsResult) : null,
70
+ // Sample of pages Google found linking here — external corroboration for orphan checks.
71
+ referring_urls: Array.isArray(idx.referringUrls) && idx.referringUrls.length ? JSON.stringify(idx.referringUrls) : null,
72
+ });
73
+ inspected++;
74
+ }
75
+ catch (err) {
76
+ failed++;
77
+ console.error(`[inspect] ${url}: ${err instanceof Error ? err.message : String(err)}`);
78
+ }
79
+ if (inspected % 10 === 0)
80
+ update({ siteUrl, inspected, failed });
81
+ if (delayMs)
82
+ await sleep(delayMs);
83
+ }
84
+ update({ siteUrl, inspected, failed });
85
+ return { siteUrl, inspected, failed };
86
+ }
87
+ finally {
88
+ db.close();
89
+ }
90
+ }
91
+ }
92
+ //# sourceMappingURL=UrlInspector.js.map