seo-intel 1.5.0 → 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 +19 -0
- package/cli.js +16 -8
- package/extractor/qwen.js +15 -2
- package/package.json +1 -1
- package/reports/generate-html.js +26 -14
- package/setup/checks.js +44 -16
- package/setup/web-routes.js +4 -0
- package/setup/wizard.html +85 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.5.1 (2026-04-11)
|
|
4
|
+
|
|
5
|
+
### Setup Wizard
|
|
6
|
+
- Fixed Playwright detection on macOS — now checks correct browser cache paths instead of legacy node_modules location
|
|
7
|
+
- Added persistent "Open Dashboard" link in wizard header, visible on all setup steps
|
|
8
|
+
- Renamed floating helper card to "Agentic Installations" with extended per-runtime setup prompts
|
|
9
|
+
- Cloud model cards now show live connection status (Connected via API key or OpenClaw gateway)
|
|
10
|
+
- OpenClaw gateway model detection with authenticated `/v1/models` query
|
|
11
|
+
|
|
12
|
+
### Extraction: LAN host model fix
|
|
13
|
+
- Fixed LAN/fallback hosts checking for wrong model (used stale `OLLAMA_FALLBACK_MODEL` instead of project-selected model)
|
|
14
|
+
- All Ollama hosts now use the project's configured extraction model consistently
|
|
15
|
+
- Added `OLLAMA_HOSTS` support — comma-separated LAN hosts from setup wizard are picked up by extractor
|
|
16
|
+
|
|
17
|
+
### Dashboard
|
|
18
|
+
- Stealth toggle moved next to Crawl button (only affects crawl, not extract)
|
|
19
|
+
- Analysis buttons (Analyze, Brief, Keywords, Templates) get subtle blue accent border
|
|
20
|
+
- Visual separator between action and intelligence command groups
|
|
21
|
+
|
|
3
22
|
## 1.5.0 (2026-04-10)
|
|
4
23
|
|
|
5
24
|
### Export: dashboard data, not raw DB dumps
|
package/cli.js
CHANGED
|
@@ -69,19 +69,27 @@ function defaultSiteUrl(domain) {
|
|
|
69
69
|
function resolveExtractionRuntime(config) {
|
|
70
70
|
const primaryUrl = config?.crawl?.ollamaHost || process.env.OLLAMA_URL || 'http://localhost:11434';
|
|
71
71
|
const primaryModel = config?.crawl?.extractionModel || process.env.OLLAMA_MODEL || 'gemma4:e4b';
|
|
72
|
-
const fallbackUrl = process.env.OLLAMA_FALLBACK_URL || '';
|
|
73
|
-
const fallbackModel = process.env.OLLAMA_FALLBACK_MODEL || primaryModel;
|
|
74
72
|
const localhost = 'http://localhost:11434';
|
|
73
|
+
const norm = h => String(h || '').trim().replace(/\/+$/, '');
|
|
75
74
|
|
|
76
75
|
const candidates = [
|
|
77
|
-
{ host:
|
|
76
|
+
{ host: norm(primaryUrl), model: String(primaryModel).trim() || 'gemma4:e4b' },
|
|
78
77
|
];
|
|
79
78
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
79
|
+
// Legacy single fallback — always use project-selected model, not OLLAMA_FALLBACK_MODEL
|
|
80
|
+
const fallbackUrl = norm(process.env.OLLAMA_FALLBACK_URL || '');
|
|
81
|
+
if (fallbackUrl && !candidates.some(c => c.host === fallbackUrl)) {
|
|
82
|
+
candidates.push({ host: fallbackUrl, model: String(primaryModel).trim() || 'gemma4:e4b' });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// OLLAMA_HOSTS — comma-separated LAN hosts from setup wizard
|
|
86
|
+
if (process.env.OLLAMA_HOSTS) {
|
|
87
|
+
for (const h of process.env.OLLAMA_HOSTS.split(',')) {
|
|
88
|
+
const host = norm(h);
|
|
89
|
+
if (host && !candidates.some(c => c.host === host)) {
|
|
90
|
+
candidates.push({ host, model: String(primaryModel).trim() || 'gemma4:e4b' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
if (!candidates.some(candidate => candidate.host === localhost)) {
|
package/extractor/qwen.js
CHANGED
|
@@ -24,16 +24,29 @@ function getConfiguredOllamaRoutes() {
|
|
|
24
24
|
const primaryUrl = normalizeHost(process.env.OLLAMA_URL || DEFAULT_OLLAMA_URL) || DEFAULT_OLLAMA_URL;
|
|
25
25
|
const primaryModel = String(process.env.OLLAMA_MODEL || DEFAULT_OLLAMA_MODEL).trim() || DEFAULT_OLLAMA_MODEL;
|
|
26
26
|
const fallbackUrl = normalizeHost(process.env.OLLAMA_FALLBACK_URL || '');
|
|
27
|
-
|
|
27
|
+
// BUG FIX: fallback hosts MUST use the project-selected model (primaryModel),
|
|
28
|
+
// not a separate OLLAMA_FALLBACK_MODEL env var. The project config sets
|
|
29
|
+
// OLLAMA_MODEL to the user's choice — all hosts should respect that.
|
|
30
|
+
const fallbackModel = primaryModel;
|
|
28
31
|
|
|
29
32
|
const candidates = [
|
|
30
33
|
{ label: 'primary', host: primaryUrl, model: primaryModel },
|
|
31
34
|
];
|
|
32
35
|
|
|
33
|
-
if (fallbackUrl) {
|
|
36
|
+
if (fallbackUrl && !candidates.some(r => r.host === normalizeHost(fallbackUrl))) {
|
|
34
37
|
candidates.push({ label: 'fallback', host: fallbackUrl, model: fallbackModel });
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
// Support OLLAMA_HOSTS — comma-separated list of additional LAN Ollama hosts
|
|
41
|
+
if (process.env.OLLAMA_HOSTS) {
|
|
42
|
+
for (const h of process.env.OLLAMA_HOSTS.split(',')) {
|
|
43
|
+
const host = normalizeHost(h);
|
|
44
|
+
if (host && !candidates.some(r => r.host === host)) {
|
|
45
|
+
candidates.push({ label: 'lan', host, model: primaryModel });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
if (!candidates.some(route => route.host === LOCALHOST_OLLAMA_URL)) {
|
|
38
51
|
candidates.push({ label: 'localhost', host: LOCALHOST_OLLAMA_URL, model: primaryModel });
|
|
39
52
|
}
|
package/package.json
CHANGED
package/reports/generate-html.js
CHANGED
|
@@ -1466,6 +1466,12 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
1466
1466
|
.term-btn:hover { border-color: var(--accent-gold); color: var(--accent-gold); }
|
|
1467
1467
|
.term-btn:active { background: rgba(232,213,163,0.1); }
|
|
1468
1468
|
.term-btn i { margin-right: 3px; font-size: 0.55rem; }
|
|
1469
|
+
.term-btn-intel { border-color: rgba(96,165,250,0.2); }
|
|
1470
|
+
.term-btn-intel:hover { border-color: rgba(96,165,250,0.6); color: rgba(150,200,255,0.9); }
|
|
1471
|
+
.term-btn-intel:active { background: rgba(96,165,250,0.08); }
|
|
1472
|
+
.term-stealth { display:inline-flex; align-items:center; gap:4px; font-size:0.58rem; color:var(--text-muted); cursor:pointer; user-select:none; margin-left:2px; padding:2px 6px; border-radius:4px; border:1px solid transparent; transition:all 0.15s; }
|
|
1473
|
+
.term-stealth:hover { border-color: rgba(124,109,235,0.3); color: var(--text-secondary); }
|
|
1474
|
+
.term-stealth input[type="checkbox"] { accent-color: var(--accent-purple,#7c6deb); width:12px; height:12px; cursor:pointer; }
|
|
1469
1475
|
|
|
1470
1476
|
/* ─── Terminal + Export split layout ───────────────────────────────── */
|
|
1471
1477
|
.term-split {
|
|
@@ -2122,10 +2128,6 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2122
2128
|
<button class="es-btn es-btn-restart" id="btnRestart${suffix}" onclick="restartServer()">
|
|
2123
2129
|
<i class="fa-solid fa-rotate-right"></i> Restart
|
|
2124
2130
|
</button>
|
|
2125
|
-
<label class="es-stealth-toggle">
|
|
2126
|
-
<input type="checkbox" id="stealthToggle${suffix}"${extractionStatus.liveProgress?.stealth ? ' checked' : ''}>
|
|
2127
|
-
<i class="fa-solid fa-user-ninja"></i> Stealth
|
|
2128
|
-
</label>
|
|
2129
2131
|
</div>
|
|
2130
2132
|
</div>
|
|
2131
2133
|
</div>
|
|
@@ -2147,11 +2149,13 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2147
2149
|
<div style="padding:8px 12px;background:#111;border-bottom:1px solid var(--border-subtle);display:flex;flex-wrap:wrap;gap:6px;align-items:center;">
|
|
2148
2150
|
<span style="font-size:0.6rem;color:var(--text-muted);margin-right:4px;"><i class="fa-solid fa-play" style="margin-right:3px;"></i>Run:</span>
|
|
2149
2151
|
<button class="term-btn" data-cmd="crawl" data-project="${project}"><i class="fa-solid fa-spider"></i> Crawl</button>
|
|
2152
|
+
<label class="term-stealth"><input type="checkbox" id="stealthToggle${suffix}"${extractionStatus.liveProgress?.stealth ? ' checked' : ''}><i class="fa-solid fa-user-ninja"></i></label>
|
|
2150
2153
|
${pro ? `<button class="term-btn" data-cmd="extract" data-project="${project}"><i class="fa-solid fa-brain"></i> Extract</button>
|
|
2151
|
-
<
|
|
2152
|
-
<button class="term-btn" data-cmd="
|
|
2153
|
-
<button class="term-btn" data-cmd="
|
|
2154
|
-
<button class="term-btn" data-cmd="
|
|
2154
|
+
<span style="width:1px;height:16px;background:var(--border-subtle);margin:0 2px;"></span>
|
|
2155
|
+
<button class="term-btn term-btn-intel" data-cmd="analyze" data-project="${project}"><i class="fa-solid fa-chart-column"></i> Analyze</button>
|
|
2156
|
+
<button class="term-btn term-btn-intel" data-cmd="brief" data-project="${project}"><i class="fa-solid fa-file-lines"></i> Brief</button>
|
|
2157
|
+
<button class="term-btn term-btn-intel" data-cmd="keywords" data-project="${project}"><i class="fa-solid fa-key"></i> Keywords</button>
|
|
2158
|
+
<button class="term-btn term-btn-intel" data-cmd="templates" data-project="${project}"><i class="fa-solid fa-clone"></i> Templates</button>` : ''}
|
|
2155
2159
|
<button class="term-btn" data-cmd="status" data-project=""><i class="fa-solid fa-circle-info"></i> Status</button>
|
|
2156
2160
|
<button class="term-btn" data-cmd="guide" data-project="${project}"><i class="fa-solid fa-map"></i> Guide</button>
|
|
2157
2161
|
<button class="term-btn" data-cmd="setup" data-project="" style="margin-left:auto;border-color:rgba(232,213,163,0.25);"><i class="fa-solid fa-gear"></i> Setup</button>
|
|
@@ -2368,10 +2372,12 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
2368
2372
|
const proj = btn.getAttribute('data-project');
|
|
2369
2373
|
const scope = btn.getAttribute('data-scope');
|
|
2370
2374
|
var extra = scope ? { scope: scope } : {};
|
|
2371
|
-
// Crawl
|
|
2372
|
-
if (cmd === 'crawl'
|
|
2375
|
+
// Crawl: read stealth toggle; Crawl/extract: update status bar
|
|
2376
|
+
if (cmd === 'crawl') {
|
|
2373
2377
|
var stealthEl = btn.closest('.project-panel')?.querySelector('[id^="stealthToggle"]') || document.getElementById('stealthToggle' + suffix);
|
|
2374
2378
|
if (stealthEl?.checked) extra.stealth = true;
|
|
2379
|
+
}
|
|
2380
|
+
if (cmd === 'crawl' || cmd === 'extract') {
|
|
2375
2381
|
if (window._setButtonsState) window._setButtonsState(true, cmd);
|
|
2376
2382
|
if (window._startPolling) window._startPolling();
|
|
2377
2383
|
}
|
|
@@ -4057,9 +4063,12 @@ function buildHtmlTemplate(data, opts = {}) {
|
|
|
4057
4063
|
let pollTimer = null;
|
|
4058
4064
|
|
|
4059
4065
|
window.startJob = function(command, proj) {
|
|
4060
|
-
var stealth = document.getElementById('stealthToggle' + sfx)?.checked || false;
|
|
4061
4066
|
var extra = {};
|
|
4062
|
-
|
|
4067
|
+
// Stealth only applies to crawl — extract has no network
|
|
4068
|
+
if (command === 'crawl') {
|
|
4069
|
+
var stealth = document.getElementById('stealthToggle' + sfx)?.checked || false;
|
|
4070
|
+
if (stealth) extra.stealth = true;
|
|
4071
|
+
}
|
|
4063
4072
|
|
|
4064
4073
|
// Route through terminal for visible output
|
|
4065
4074
|
if (window._terminalRun) {
|
|
@@ -5279,9 +5288,12 @@ function buildMultiHtmlTemplate(allProjectData) {
|
|
|
5279
5288
|
|
|
5280
5289
|
window.startJob = function(command, proj) {
|
|
5281
5290
|
var sfx = '-' + proj;
|
|
5282
|
-
var stealth = document.getElementById('stealthToggle' + sfx)?.checked || false;
|
|
5283
5291
|
var extra = {};
|
|
5284
|
-
|
|
5292
|
+
// Stealth only applies to crawl — extract has no network
|
|
5293
|
+
if (command === 'crawl') {
|
|
5294
|
+
var stealth = document.getElementById('stealthToggle' + sfx)?.checked || false;
|
|
5295
|
+
if (stealth) extra.stealth = true;
|
|
5296
|
+
}
|
|
5285
5297
|
|
|
5286
5298
|
// Route through terminal for visible output
|
|
5287
5299
|
if (window._terminalRun) {
|
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
|
}
|