seo-intel 1.4.9 → 1.5.1
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/CHANGELOG.md +29 -0
- package/cli.js +16 -8
- package/extractor/qwen.js +15 -2
- package/package.json +1 -1
- package/reports/generate-html.js +27 -15
- package/server.js +239 -513
- package/setup/checks.js +44 -16
- package/setup/web-routes.js +4 -0
- package/setup/wizard.html +85 -15
package/setup/checks.js
CHANGED
|
@@ -10,6 +10,7 @@ import { execSync, spawnSync } from 'child_process';
|
|
|
10
10
|
import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
11
11
|
import { join, dirname } from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
|
+
import { createRequire } from 'module';
|
|
13
14
|
|
|
14
15
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
15
16
|
const ROOT = join(__dirname, '..');
|
|
@@ -141,24 +142,35 @@ export function checkPlaywright() {
|
|
|
141
142
|
// Check if Chromium binary is actually available
|
|
142
143
|
let chromiumReady = false;
|
|
143
144
|
try {
|
|
144
|
-
//
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
// 1. Shared cache (macOS: ~/Library/Caches/ms-playwright, Linux: ~/.cache/ms-playwright)
|
|
146
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
147
|
+
const cachePaths = [
|
|
148
|
+
join(home, 'Library', 'Caches', 'ms-playwright'), // macOS
|
|
149
|
+
join(home, '.cache', 'ms-playwright'), // Linux
|
|
150
|
+
join(process.env.LOCALAPPDATA || '', 'ms-playwright'), // Windows
|
|
151
|
+
join(pkgPath, '.local-browsers'), // legacy / bundled
|
|
152
|
+
];
|
|
153
|
+
for (const cachePath of cachePaths) {
|
|
154
|
+
if (existsSync(cachePath)) {
|
|
155
|
+
try {
|
|
156
|
+
const browsers = readdirSync(cachePath);
|
|
157
|
+
if (browsers.some(b => b.toLowerCase().includes('chromium'))) {
|
|
158
|
+
chromiumReady = true;
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
} catch { /* permission error, skip */ }
|
|
162
|
+
}
|
|
149
163
|
}
|
|
150
|
-
//
|
|
164
|
+
// 2. Fallback: require playwright and check chromium executablePath
|
|
151
165
|
if (!chromiumReady) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
chromiumReady = result.status === 0 && !result.stdout.includes('chromium');
|
|
166
|
+
try {
|
|
167
|
+
const req = createRequire(join(ROOT, 'package.json'));
|
|
168
|
+
const pw = req('playwright');
|
|
169
|
+
const execPath = pw.chromium?.executablePath?.();
|
|
170
|
+
if (execPath && existsSync(execPath)) chromiumReady = true;
|
|
171
|
+
} catch { /* playwright may not be requireable */ }
|
|
159
172
|
}
|
|
160
173
|
} catch {
|
|
161
|
-
// If we can't determine, assume it needs install
|
|
162
174
|
chromiumReady = false;
|
|
163
175
|
}
|
|
164
176
|
|
|
@@ -443,6 +455,7 @@ export function checkOpenClaw() {
|
|
|
443
455
|
hasSkillsDir: false,
|
|
444
456
|
skillsPath: null,
|
|
445
457
|
canAgentSetup: false,
|
|
458
|
+
gatewayModels: [], // model IDs available via OpenClaw gateway
|
|
446
459
|
};
|
|
447
460
|
|
|
448
461
|
// 1. Check if openclaw binary exists
|
|
@@ -456,10 +469,25 @@ export function checkOpenClaw() {
|
|
|
456
469
|
result.version = match ? match[1] : ver;
|
|
457
470
|
} catch { /* ok */ }
|
|
458
471
|
|
|
459
|
-
// 3. Check if gateway is running
|
|
472
|
+
// 3. Check if gateway is running + fetch available models
|
|
473
|
+
// Read gateway auth token from config
|
|
474
|
+
let gwToken = '';
|
|
475
|
+
try {
|
|
476
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
477
|
+
const ocConf = JSON.parse(readFileSync(join(homeDir, '.openclaw', 'openclaw.json'), 'utf8'));
|
|
478
|
+
gwToken = ocConf?.gateway?.auth?.token || '';
|
|
479
|
+
} catch { /* no config */ }
|
|
480
|
+
|
|
460
481
|
try {
|
|
461
|
-
|
|
482
|
+
const authHeader = gwToken ? `-H "Authorization: Bearer ${gwToken}"` : '';
|
|
483
|
+
const raw = execSync(`curl -s --max-time 2 ${authHeader} http://127.0.0.1:18789/v1/models 2>/dev/null`, { timeout: 5000 }).toString().trim();
|
|
462
484
|
result.gatewayRunning = true;
|
|
485
|
+
try {
|
|
486
|
+
const parsed = JSON.parse(raw);
|
|
487
|
+
if (parsed?.data && Array.isArray(parsed.data)) {
|
|
488
|
+
result.gatewayModels = parsed.data.map(m => m.id).filter(Boolean);
|
|
489
|
+
}
|
|
490
|
+
} catch { /* json parse fail — gateway running but no model list */ }
|
|
463
491
|
} catch {
|
|
464
492
|
result.gatewayRunning = false;
|
|
465
493
|
}
|
package/setup/web-routes.js
CHANGED
|
@@ -296,6 +296,10 @@ async function handleModels(req, res) {
|
|
|
296
296
|
...models,
|
|
297
297
|
gpu: status.vram,
|
|
298
298
|
ollama: status.ollama,
|
|
299
|
+
openclaw: {
|
|
300
|
+
gatewayRunning: status.openclaw?.gatewayRunning || false,
|
|
301
|
+
gatewayModels: status.openclaw?.gatewayModels || [],
|
|
302
|
+
},
|
|
299
303
|
});
|
|
300
304
|
} catch (err) {
|
|
301
305
|
jsonResponse(res, { error: err.message }, 500);
|
package/setup/wizard.html
CHANGED
|
@@ -1198,7 +1198,10 @@ input::placeholder {
|
|
|
1198
1198
|
<div class="wizard-header">
|
|
1199
1199
|
<h1>SEO Intel</h1>
|
|
1200
1200
|
<div class="subtitle">Setup Wizard</div>
|
|
1201
|
-
<
|
|
1201
|
+
<div style="position:absolute;top:14px;right:18px;display:flex;gap:14px;align-items:center;">
|
|
1202
|
+
<a href="/" title="Open Dashboard" style="font-size:0.68rem;color:var(--color-success,#8ecba8);text-decoration:none;opacity:0.8;transition:opacity 0.15s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.8'"><i class="fa-solid fa-chart-line"></i> Dashboard</a>
|
|
1203
|
+
<a href="http://localhost:3000/?autorun=setup-classic" title="Run setup in Terminal instead" style="font-size:0.68rem;color:var(--color-muted,#888);text-decoration:none;opacity:0.7;transition:opacity 0.15s;" onmouseover="this.style.opacity='1'" onmouseout="this.style.opacity='0.7'"><i class="fa-solid fa-terminal"></i> Terminal setup</a>
|
|
1204
|
+
</div>
|
|
1202
1205
|
</div>
|
|
1203
1206
|
|
|
1204
1207
|
<!-- ═══════════════════════════════════════════════════════════════════════
|
|
@@ -1894,7 +1897,7 @@ input::placeholder {
|
|
|
1894
1897
|
<circle cx="14" cy="14" r="13" stroke="var(--accent-purple)" stroke-width="1.5" opacity="0.6"/>
|
|
1895
1898
|
<path d="M9 12c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.8-1 3.4-2.4 4.3L18 20H10l1.4-3.7C10 15.4 9 13.8 9 12z" fill="var(--accent-purple)" opacity="0.8"/>
|
|
1896
1899
|
</svg>
|
|
1897
|
-
<span style="font-size:0.92rem; font-weight:600; color:var(--text-primary);">
|
|
1900
|
+
<span style="font-size:0.92rem; font-weight:600; color:var(--text-primary);">Agentic Installations</span>
|
|
1898
1901
|
</div>
|
|
1899
1902
|
|
|
1900
1903
|
<!-- Tab switcher: Agentic Setup vs Cloud Models -->
|
|
@@ -1925,7 +1928,7 @@ input::placeholder {
|
|
|
1925
1928
|
<button class="btn btn-sm" style="margin-left:auto; padding:2px 6px; font-size:0.55rem;" onclick="navigator.clipboard.writeText('clawhub install ukkometa/seo-intel');this.textContent='OK';setTimeout(()=>this.textContent='Copy',1200);">Copy</button>
|
|
1926
1929
|
</div>
|
|
1927
1930
|
<div style="position:relative; padding:7px 9px; background:rgba(10,10,10,0.5); border:1px solid var(--border-subtle); border-radius:var(--radius); font-family:var(--font-mono); font-size:0.62rem; color:var(--text-secondary); line-height:1.5;">
|
|
1928
|
-
<div id="genie-oc-prompt">Set up SEO Intel on my machine.
|
|
1931
|
+
<div id="genie-oc-prompt">Set up SEO Intel (npm package: seo-intel) on my machine. Steps: 1) Verify Node.js ≥22.5 and npm are installed. 2) Install Ollama and pull the gemma4:e4b model (default extraction engine, ~4GB). 3) Run `seo-intel setup` to create a project config — ask me for my website URL and up to 3 competitor URLs. 4) Run a test crawl with `seo-intel crawl <project>` to verify everything works. 5) Run `seo-intel extract <project>` to test local AI extraction. Report any errors with full context.</div>
|
|
1929
1932
|
<button class="btn btn-sm" style="position:absolute; top:5px; right:5px; padding:2px 5px; font-size:0.52rem;" onclick="navigator.clipboard.writeText(document.getElementById('genie-oc-prompt').textContent.trim());this.textContent='OK';setTimeout(()=>this.textContent='Copy',1200);">Copy</button>
|
|
1930
1933
|
</div>
|
|
1931
1934
|
<div style="display:flex; gap:6px; margin-top:10px;">
|
|
@@ -1936,21 +1939,21 @@ input::placeholder {
|
|
|
1936
1939
|
|
|
1937
1940
|
<div id="genieRuntime-claudecode" class="genie-runtime-panel" style="display:none;">
|
|
1938
1941
|
<div style="position:relative; padding:7px 9px; background:rgba(10,10,10,0.5); border:1px solid var(--border-subtle); border-radius:var(--radius); font-family:var(--font-mono); font-size:0.62rem; color:var(--text-secondary); line-height:1.5;">
|
|
1939
|
-
<div>Set up seo-intel:
|
|
1942
|
+
<div>Set up the npm package `seo-intel` on this machine. Requirements: Node.js ≥22.5, Ollama with gemma4:e4b model (~4GB download). Create a project config for my website — ask me for my URL and competitor domains. Then run a test crawl (`seo-intel crawl <project>`) and extraction (`seo-intel extract <project>`). The tool stores data in SQLite locally. If Ollama isn't installed, install it from ollama.com. After setup, open the dashboard with `seo-intel serve`.</div>
|
|
1940
1943
|
<button class="btn btn-sm" style="position:absolute; top:5px; right:5px; padding:2px 5px; font-size:0.52rem;" onclick="navigator.clipboard.writeText(this.previousElementSibling.textContent.trim());this.textContent='OK';setTimeout(()=>this.textContent='Copy',1200);">Copy</button>
|
|
1941
1944
|
</div>
|
|
1942
1945
|
</div>
|
|
1943
1946
|
|
|
1944
1947
|
<div id="genieRuntime-codex" class="genie-runtime-panel" style="display:none;">
|
|
1945
1948
|
<div style="position:relative; padding:7px 9px; background:rgba(10,10,10,0.5); border:1px solid var(--border-subtle); border-radius:var(--radius); font-family:var(--font-mono); font-size:0.62rem; color:var(--text-secondary); line-height:1.5;">
|
|
1946
|
-
<div>Install seo-intel
|
|
1949
|
+
<div>Install and configure the `seo-intel` npm package. Needs: Node.js ≥22.5, Ollama running locally with `gemma4:e4b` pulled. Create a JSON config at configs/<project>.json with target URL + competitor URLs (ask me). Run the full pipeline: `seo-intel crawl <project>` → `seo-intel extract <project>` → `seo-intel serve` to open the dashboard. Needs full disk access for Playwright browser automation.</div>
|
|
1947
1950
|
<button class="btn btn-sm" style="position:absolute; top:5px; right:5px; padding:2px 5px; font-size:0.52rem;" onclick="navigator.clipboard.writeText(this.previousElementSibling.textContent.trim());this.textContent='OK';setTimeout(()=>this.textContent='Copy',1200);">Copy</button>
|
|
1948
1951
|
</div>
|
|
1949
1952
|
</div>
|
|
1950
1953
|
|
|
1951
1954
|
<div id="genieRuntime-perplexity" class="genie-runtime-panel" style="display:none;">
|
|
1952
1955
|
<div style="position:relative; padding:7px 9px; background:rgba(10,10,10,0.5); border:1px solid var(--border-subtle); border-radius:var(--radius); font-family:var(--font-mono); font-size:0.62rem; color:var(--text-secondary); line-height:1.5;">
|
|
1953
|
-
<div>
|
|
1956
|
+
<div>I want to install SEO Intel (seo-intel on npm), a local-first SEO competitor analysis tool. Walk me through: 1) Installing Node.js 22.5+ if I don't have it. 2) Installing Ollama and downloading the Gemma 4 e4b model for AI extraction. 3) Running `npx seo-intel setup` to configure my first project. I need to provide my website URL and competitor URLs. 4) Testing the crawl pipeline. Ask me for my website URL to get started.</div>
|
|
1954
1957
|
<button class="btn btn-sm" style="position:absolute; top:5px; right:5px; padding:2px 5px; font-size:0.52rem;" onclick="navigator.clipboard.writeText(this.previousElementSibling.textContent.trim());this.textContent='OK';setTimeout(()=>this.textContent='Copy',1200);">Copy</button>
|
|
1955
1958
|
</div>
|
|
1956
1959
|
</div>
|
|
@@ -1959,21 +1962,15 @@ input::placeholder {
|
|
|
1959
1962
|
<!-- Cloud Models tab -->
|
|
1960
1963
|
<div id="genieCloud" style="display:none;">
|
|
1961
1964
|
<p style="font-size:0.75rem; color:var(--text-muted); line-height:1.6; margin-bottom:12px;">
|
|
1962
|
-
|
|
1965
|
+
Cloud frontier models for competitive analysis. Add API keys in the Models step, or use OpenClaw for automatic key management.
|
|
1963
1966
|
</p>
|
|
1964
1967
|
<div style="font-size:0.72rem; color:var(--text-secondary);">
|
|
1965
1968
|
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
|
|
1966
|
-
<i class="fa-solid fa-
|
|
1967
|
-
</div>
|
|
1968
|
-
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
|
|
1969
|
-
<i class="fa-solid fa-check" style="color:var(--accent-purple); font-size:0.62rem; width:12px;"></i> Gemini 3.1 Pro
|
|
1970
|
-
</div>
|
|
1971
|
-
<div style="display:flex; align-items:center; gap:6px; margin-bottom:6px;">
|
|
1972
|
-
<i class="fa-solid fa-check" style="color:var(--accent-purple); font-size:0.62rem; width:12px;"></i> GPT-5.4
|
|
1969
|
+
<i class="fa-solid fa-spinner fa-spin-pulse" style="color:var(--text-muted); font-size:0.62rem; width:12px;"></i> <span style="color:var(--text-muted);">Checking connections...</span>
|
|
1973
1970
|
</div>
|
|
1974
1971
|
</div>
|
|
1975
1972
|
<p style="font-size:0.65rem; color:var(--text-muted); line-height:1.5; margin-top:10px;">
|
|
1976
|
-
|
|
1973
|
+
Set up API keys in the <strong>Models & Auth</strong> step, or use OpenClaw to skip key management entirely.
|
|
1977
1974
|
</p>
|
|
1978
1975
|
</div>
|
|
1979
1976
|
</div>
|
|
@@ -2332,6 +2329,7 @@ input::placeholder {
|
|
|
2332
2329
|
state.modelData = await API.get('/api/setup/models');
|
|
2333
2330
|
renderExtractionModels();
|
|
2334
2331
|
renderAnalysisModels();
|
|
2332
|
+
renderCloudModelStatus();
|
|
2335
2333
|
} catch (err) {
|
|
2336
2334
|
extDiv.innerHTML = `<div style="color:var(--color-danger); font-size:0.75rem;">Failed: ${err.message}</div>`;
|
|
2337
2335
|
anaDiv.innerHTML = extDiv.innerHTML;
|
|
@@ -2493,6 +2491,76 @@ input::placeholder {
|
|
|
2493
2491
|
}
|
|
2494
2492
|
}
|
|
2495
2493
|
|
|
2494
|
+
function renderCloudModelStatus() {
|
|
2495
|
+
const cloudModels = (state.modelData?.allAnalysis || []).filter(m => m.type === 'cloud');
|
|
2496
|
+
const gwModels = state.modelData?.openclaw?.gatewayModels || [];
|
|
2497
|
+
const gwRunning = state.modelData?.openclaw?.gatewayRunning || false;
|
|
2498
|
+
|
|
2499
|
+
// Determine connection status per cloud model
|
|
2500
|
+
// A model is "connected" if: .env key exists OR OpenClaw gateway is active (routes all cloud models)
|
|
2501
|
+
function isConnected(cm) {
|
|
2502
|
+
if (cm.configured) return { connected: true, via: 'key' };
|
|
2503
|
+
// If OpenClaw gateway is running with models, all cloud models are available through it
|
|
2504
|
+
if (gwRunning && gwModels.length > 0) return { connected: true, via: 'openclaw' };
|
|
2505
|
+
return { connected: false };
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
// Mark cloud model cards with visible connection badges
|
|
2509
|
+
const cloudCards = document.querySelectorAll('#cloudAnalysisModels .cloud-model');
|
|
2510
|
+
cloudCards.forEach(card => {
|
|
2511
|
+
const modelId = card.dataset.model;
|
|
2512
|
+
const cm = cloudModels.find(m => m.id === modelId);
|
|
2513
|
+
const old = card.querySelector('.cloud-status-badge');
|
|
2514
|
+
if (old) old.remove();
|
|
2515
|
+
if (!cm) return;
|
|
2516
|
+
|
|
2517
|
+
const status = isConnected(cm);
|
|
2518
|
+
const badge = document.createElement('div');
|
|
2519
|
+
badge.className = 'cloud-status-badge';
|
|
2520
|
+
|
|
2521
|
+
if (status.connected) {
|
|
2522
|
+
const viaLabel = status.via === 'openclaw' ? 'via OpenClaw' : 'API key set';
|
|
2523
|
+
badge.innerHTML = `<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:rgba(142,203,168,0.15);border:1px solid rgba(142,203,168,0.3);font-size:0.6rem;font-weight:600;color:var(--color-success);letter-spacing:0.02em;"><i class="fa-solid fa-circle-check"></i> Connected <span style="font-weight:400;opacity:0.8;">— ${viaLabel}</span></span>`;
|
|
2524
|
+
badge.style.cssText = 'margin-top:8px;';
|
|
2525
|
+
card.style.borderColor = 'rgba(142,203,168,0.3)';
|
|
2526
|
+
} else {
|
|
2527
|
+
badge.innerHTML = `<span style="display:inline-flex;align-items:center;gap:4px;padding:3px 8px;border-radius:4px;background:rgba(255,255,255,0.03);border:1px solid var(--border-subtle);font-size:0.6rem;color:var(--text-muted);"><i class="fa-solid fa-circle-xmark"></i> No API key</span>`;
|
|
2528
|
+
badge.style.cssText = 'margin-top:8px;';
|
|
2529
|
+
card.style.borderColor = '';
|
|
2530
|
+
}
|
|
2531
|
+
card.appendChild(badge);
|
|
2532
|
+
});
|
|
2533
|
+
|
|
2534
|
+
// Update the genie Cloud Models tab with live status
|
|
2535
|
+
const genieCloud = document.getElementById('genieCloud');
|
|
2536
|
+
if (genieCloud) {
|
|
2537
|
+
const existingList = genieCloud.querySelector('div[style*="font-size:0.72rem"]');
|
|
2538
|
+
if (!existingList) return;
|
|
2539
|
+
|
|
2540
|
+
let html = '<div style="font-size:0.72rem; color:var(--text-secondary);">';
|
|
2541
|
+
for (const cm of cloudModels) {
|
|
2542
|
+
const status = isConnected(cm);
|
|
2543
|
+
if (status.connected) {
|
|
2544
|
+
const via = status.via === 'openclaw' ? ' <span style="font-size:0.58rem;color:var(--text-muted);">via OpenClaw</span>' : '';
|
|
2545
|
+
html += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
|
|
2546
|
+
<i class="fa-solid fa-circle-check" style="color:var(--color-success);font-size:0.62rem;width:12px;"></i> ${cm.name}${via}
|
|
2547
|
+
</div>`;
|
|
2548
|
+
} else {
|
|
2549
|
+
html += `<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;">
|
|
2550
|
+
<i class="fa-solid fa-circle-xmark" style="color:var(--text-muted);font-size:0.62rem;width:12px;"></i> <span style="color:var(--text-muted);">${cm.name}</span>
|
|
2551
|
+
</div>`;
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
if (gwRunning) {
|
|
2555
|
+
html += `<div style="margin-top:8px;padding:6px 8px;background:rgba(124,109,235,0.08);border:1px solid rgba(124,109,235,0.2);border-radius:var(--radius);font-size:0.6rem;color:var(--accent-purple);">
|
|
2556
|
+
<i class="fa-solid fa-plug" style="margin-right:4px;"></i> OpenClaw gateway active — ${gwModels.length} model(s)
|
|
2557
|
+
</div>`;
|
|
2558
|
+
}
|
|
2559
|
+
html += '</div>';
|
|
2560
|
+
existingList.outerHTML = html;
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2496
2564
|
function selectAnalysisModel(id) {
|
|
2497
2565
|
state.selectedAnalysis = id;
|
|
2498
2566
|
document.querySelectorAll('#analysisModels .model-radio-card').forEach(c => {
|
|
@@ -3613,6 +3681,8 @@ input::placeholder {
|
|
|
3613
3681
|
const data = await res.json();
|
|
3614
3682
|
if (!res.ok) throw new Error(data.error || 'Save failed');
|
|
3615
3683
|
if (saveStatus) { saveStatus.textContent = '✓ Saved to .env'; saveStatus.style.color = 'var(--color-success)'; }
|
|
3684
|
+
// Refresh model data to update cloud connection status
|
|
3685
|
+
try { state.modelData = await API.get('/api/setup/models'); renderCloudModelStatus(); } catch {}
|
|
3616
3686
|
} catch (err) {
|
|
3617
3687
|
if (saveStatus) { saveStatus.textContent = err.message; saveStatus.style.color = 'var(--color-danger)'; }
|
|
3618
3688
|
}
|