@houtini/seo-audit-console 0.7.8 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/core/DataForSeoClient.d.ts +8 -7
- package/dist/core/DataForSeoClient.d.ts.map +1 -1
- package/dist/core/DataForSeoClient.js +7 -7
- package/dist/core/DataForSeoClient.js.map +1 -1
- package/dist/core/MajesticClient.d.ts +1 -1
- package/dist/core/MajesticClient.js +1 -1
- package/dist/core/webHandlers.d.ts +20 -0
- package/dist/core/webHandlers.d.ts.map +1 -0
- package/dist/core/webHandlers.js +67 -0
- package/dist/core/webHandlers.js.map +1 -0
- package/dist/core/webServer.d.ts +8 -1
- package/dist/core/webServer.d.ts.map +1 -1
- package/dist/core/webServer.js +12 -4
- package/dist/core/webServer.js.map +1 -1
- package/dist/dashboard.d.ts +3 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +58 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +35 -63
- package/dist/server.js.map +1 -1
- package/package.json +108 -107
- package/server.json +85 -85
package/dist/server.js
CHANGED
|
@@ -8,7 +8,8 @@ import path from 'node:path';
|
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { z } from 'zod';
|
|
10
10
|
import { getDashboardData } from './core/dashboardData.js';
|
|
11
|
-
import { startDashboardServer, stopDashboardServer, dashboardServerUrl
|
|
11
|
+
import { startDashboardServer, stopDashboardServer, dashboardServerUrl } from './core/webServer.js';
|
|
12
|
+
import { buildWebCallHandlers } from './core/webHandlers.js';
|
|
12
13
|
import { computeSerpFootprint, persistSerpFootprint } from './core/serpFootprint.js';
|
|
13
14
|
import { computeMarketSizing, persistMarketSizing } from './core/marketSizing.js';
|
|
14
15
|
import { FirecrawlClient } from './core/FirecrawlClient.js';
|
|
@@ -291,6 +292,28 @@ export function createServer() {
|
|
|
291
292
|
// (Links, Content research) and their affiliate-linked upsell states. The data layer has
|
|
292
293
|
// no env access, so it's injected here onto every dashboard payload surface.
|
|
293
294
|
const apiKeysStatus = () => ({ dataforseo: !!dfs, majestic: !!majestic, firecrawl: !!firecrawl, supadata: !!supadata });
|
|
295
|
+
// The dashboard webserver's options — one definition shared by serve_dashboard AND the
|
|
296
|
+
// auto-start at the end of refresh_property / run_audit, so a populated property always has
|
|
297
|
+
// a live browser link (browserLink() surfaces dashboardServerUrl() once this is running).
|
|
298
|
+
const buildWebOpts = (port) => ({
|
|
299
|
+
dataDir,
|
|
300
|
+
uiHtml: () => readFileSync(path.join(__dirname, 'src', 'ui', 'dashboard.html'), 'utf8'),
|
|
301
|
+
call: buildWebCallHandlers({ dataDir, dfs, apiKeys: apiKeysStatus }),
|
|
302
|
+
...(port != null ? { port } : {}),
|
|
303
|
+
});
|
|
304
|
+
// Idempotent auto-start used by refresh_property / run_audit. Best-effort: a served port that
|
|
305
|
+
// can't bind (e.g. Docker without a published port) must never fail the populate/audit — the
|
|
306
|
+
// in-chat get_dashboard and export_report still work, and browserLink() falls back to those.
|
|
307
|
+
// Opt-out with SAC_AUTOSERVE=0 for headless/CI/sandboxed runs where binding a localhost port
|
|
308
|
+
// is unwanted (default: on, since the whole point is to hand the user a link).
|
|
309
|
+
const autoServeDashboard = async () => {
|
|
310
|
+
if (/^(0|false|no|off)$/i.test(process.env.SAC_AUTOSERVE ?? ''))
|
|
311
|
+
return;
|
|
312
|
+
try {
|
|
313
|
+
await startDashboardServer(buildWebOpts());
|
|
314
|
+
}
|
|
315
|
+
catch { /* dashboard is a convenience, not a dependency */ }
|
|
316
|
+
};
|
|
294
317
|
// ── Introspection (no data / creds required) ────────────────────────────
|
|
295
318
|
server.registerTool('seo_audit_help', {
|
|
296
319
|
title: 'Help — what this audit can do',
|
|
@@ -414,9 +437,10 @@ export function createServer() {
|
|
|
414
437
|
},
|
|
415
438
|
}, async ({ siteUrl, scope, categories, includeJudgement }) => {
|
|
416
439
|
const result = runAudit(dataDir(), siteUrl, { scope, categories, includeJudgement });
|
|
440
|
+
await autoServeDashboard(); // spin up the browser dashboard so browserLink() hands over a live URL
|
|
417
441
|
return {
|
|
418
442
|
content: [{ type: 'text', text: buildAuditMarkdown(result, siteUrl) + browserLink(siteUrl) }],
|
|
419
|
-
structuredContent: result,
|
|
443
|
+
structuredContent: { ...result, dashboardUrl: dashboardServerUrl() },
|
|
420
444
|
};
|
|
421
445
|
});
|
|
422
446
|
server.registerTool('query_audit', {
|
|
@@ -730,7 +754,14 @@ export function createServer() {
|
|
|
730
754
|
},
|
|
731
755
|
_meta: { ui: { resourceUri: SYNC_PROGRESS_URI } },
|
|
732
756
|
}, async ({ siteUrl, gsc: doGsc, crawl, inspect, ranks, segments, full, location, startDate, endDate, maxPages, inspectLimit }) => {
|
|
733
|
-
const jobId = jobs.start('refresh', (update, signal) =>
|
|
757
|
+
const jobId = jobs.start('refresh', async (update, signal) => {
|
|
758
|
+
const r = await refresh.run(siteUrl, { gsc: doGsc, crawl, inspect, ranks, segments, full, location, startDate, endDate, maxPages, inspectLimit }, update, signal);
|
|
759
|
+
// Property is now populated — spin up the browser dashboard and hand back its URL so the
|
|
760
|
+
// finished job (polled via check_sync_status) carries a one-click link, no extra tool call.
|
|
761
|
+
await autoServeDashboard();
|
|
762
|
+
const dashboardUrl = dashboardServerUrl();
|
|
763
|
+
return dashboardUrl ? { ...r, dashboardUrl, dashboard: `${dashboardUrl}/dashboard?siteUrl=${encodeURIComponent(siteUrl)}` } : r;
|
|
764
|
+
});
|
|
734
765
|
return {
|
|
735
766
|
content: [{ type: 'text', text: `Refresh started for ${siteUrl} (job ${jobId}). Poll check_sync_status.` }],
|
|
736
767
|
structuredContent: { jobId, status: 'running', siteUrl },
|
|
@@ -2031,66 +2062,7 @@ export function createServer() {
|
|
|
2031
2062
|
structuredContent: { stopped },
|
|
2032
2063
|
};
|
|
2033
2064
|
}
|
|
2034
|
-
const { url } = await startDashboardServer(
|
|
2035
|
-
dataDir,
|
|
2036
|
-
uiHtml: () => readFileSync(path.join(__dirname, 'src', 'ui', 'dashboard.html'), 'utf8'),
|
|
2037
|
-
call: {
|
|
2038
|
-
get_dashboard_data: async (a) => {
|
|
2039
|
-
const want = String(a.siteUrl ?? '');
|
|
2040
|
-
// Only serve properties that actually exist locally — getDashboardData would
|
|
2041
|
-
// otherwise CREATE an empty DB file for any bogus siteUrl posted at the API.
|
|
2042
|
-
if (!listLocalProperties(dataDir()).some(p => p.siteUrl === want))
|
|
2043
|
-
throw new Error(`unknown property ${want}`);
|
|
2044
|
-
return { ...getDashboardData(dataDir(), want), apiKeys: apiKeysStatus() };
|
|
2045
|
-
},
|
|
2046
|
-
related_terms: async (a) => {
|
|
2047
|
-
const r = await requireDfs(dfs).relatedTerms(String(a.keyword ?? ''), a.location, a.languageCode);
|
|
2048
|
-
return r;
|
|
2049
|
-
},
|
|
2050
|
-
// Content research (on-demand) — News (free Google News + paid DFS) / Videos / Trends.
|
|
2051
|
-
news_discovery: async (a) => {
|
|
2052
|
-
const kw = String(a.keyword ?? '');
|
|
2053
|
-
const seen = new Set();
|
|
2054
|
-
const articles = [];
|
|
2055
|
-
const add = (x) => { const k = String(x.url || x.title || '').toLowerCase(); if (k && !seen.has(k)) {
|
|
2056
|
-
seen.add(k);
|
|
2057
|
-
articles.push(x);
|
|
2058
|
-
} };
|
|
2059
|
-
try {
|
|
2060
|
-
const g = await fetchGoogleNews(kw, { limit: 20 });
|
|
2061
|
-
for (const x of g.articles)
|
|
2062
|
-
add({ ...x, via: 'google-news' });
|
|
2063
|
-
}
|
|
2064
|
-
catch { /* free source optional */ }
|
|
2065
|
-
if (dfs) {
|
|
2066
|
-
try {
|
|
2067
|
-
const r = await dfs.serpNews(kw, a.location, a.languageCode);
|
|
2068
|
-
for (const x of r.articles)
|
|
2069
|
-
add({ ...x, via: 'dataforseo' });
|
|
2070
|
-
}
|
|
2071
|
-
catch { /* paid optional */ }
|
|
2072
|
-
}
|
|
2073
|
-
return { articles };
|
|
2074
|
-
},
|
|
2075
|
-
youtube_discovery: async (a) => {
|
|
2076
|
-
const r = await requireDfs(dfs).serpYoutube(String(a.keyword ?? ''), a.location, a.languageCode, a.blockDepth);
|
|
2077
|
-
return r;
|
|
2078
|
-
},
|
|
2079
|
-
topic_trend: async (a) => {
|
|
2080
|
-
const kws = Array.isArray(a.keywords) ? a.keywords : [String(a.keyword ?? '')].filter(Boolean);
|
|
2081
|
-
const r = await requireDfs(dfs).googleTrends(kws, a.location, a.languageCode, {});
|
|
2082
|
-
return r;
|
|
2083
|
-
},
|
|
2084
|
-
keyword_volume: async (a) => {
|
|
2085
|
-
const r = await requireDfs(dfs).searchVolume(a.keywords ?? [], a.location, a.languageCode);
|
|
2086
|
-
const items = (r.tasks[0]?.result ?? []).map((k) => ({
|
|
2087
|
-
keyword: k.keyword, searchVolume: k.search_volume, cpc: k.cpc, competition: k.competition,
|
|
2088
|
-
}));
|
|
2089
|
-
return { keywords: items, cached: r.cached, cost: r.cost };
|
|
2090
|
-
},
|
|
2091
|
-
},
|
|
2092
|
-
...(port != null ? { port } : {}),
|
|
2093
|
-
});
|
|
2065
|
+
const { url } = await startDashboardServer(buildWebOpts(port));
|
|
2094
2066
|
const open = siteUrl ? `${url}/dashboard?siteUrl=${encodeURIComponent(siteUrl)}` : `${url}/`;
|
|
2095
2067
|
const portNote = port != null && !url.endsWith(`:${port}`)
|
|
2096
2068
|
? ` (already running on its original port — requested port ${port} ignored; stop=true first to move it)`
|