seo-intel 1.5.28 → 1.5.29

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.29 (2026-05-17)
4
+
5
+ ### MCP — `ingest_insight` closes the loop (agents become collaborators, not consumers)
6
+ The MCP server now accepts write-back. An agent can read your raw data, do its own analysis with its own flagship LLM, and persist findings into the Intelligence Ledger — surviving across sessions, surfacing in the dashboard, deduplicating against future runs.
7
+
8
+ - **`ingest_insight(project, type, data, agent_name?)`** — **free tier**. The agent's LLM did the analysis; we just provide storage. Allowed types mirror what `analyze` writes: `keyword_gap`, `long_tail`, `quick_win`, `new_page`, `content_gap`, `technical_gap`, `positioning`.
9
+ - **Dedup contract**: same `(project, type, fingerprint)` returns the existing row with `deduped: true` and bumps `last_seen` — no duplicate accumulation across sessions.
10
+ - **Provenance**: source is stored as `agent:<name>` (e.g. `agent:claude-opus-4-7`) when `agent_name` is supplied, else just `agent`. Also stamped into the `data` JSON blob as `_source` for downstream consumers that only read `data`.
11
+ - **Schema**: idempotent `ALTER TABLE insights ADD COLUMN source TEXT DEFAULT 'cli'` — existing rows backfill to `'cli'`; analyze-time writes stay as `'cli'`; agent writes flip to `'agent:*'`. Safe on existing DBs.
12
+
13
+ ### Logo
14
+ - Updated product logo to the sharp / soft-corners v1 variant. Size dropped 1.46 MB → 953 KB. Dashboard favicon + npm package both pick up the new asset.
15
+
3
16
  ## 1.5.28 (2026-05-17)
4
17
 
5
18
  ### MCP — agents can now trigger crawls and watch progress
package/db/db.js CHANGED
@@ -29,6 +29,7 @@ export function getDb(dbPath = './seo-intel.db') {
29
29
  try { _db.exec('ALTER TABLE pages ADD COLUMN x_robots_tag TEXT'); } catch { /* already exists */ }
30
30
  try { _db.exec('ALTER TABLE analyses ADD COLUMN technical_gaps TEXT'); } catch { /* already exists */ }
31
31
  try { _db.exec('ALTER TABLE extractions ADD COLUMN intent_scores TEXT'); } catch { /* already exists */ }
32
+ try { _db.exec("ALTER TABLE insights ADD COLUMN source TEXT DEFAULT 'cli'"); } catch { /* already exists */ }
32
33
 
33
34
  // Backfill first_seen_at from crawled_at for existing rows
34
35
  _db.exec('UPDATE pages SET first_seen_at = crawled_at WHERE first_seen_at IS NULL');
@@ -225,6 +226,59 @@ export function upsertInsightsFromKeywords(db, project, keywordsReport) {
225
226
  }
226
227
  }
227
228
 
229
+ // ── Agent-ingested insight (write-back from MCP) ────────────────────────────
230
+
231
+ export const AGENT_INSIGHT_TYPES = ['keyword_gap', 'long_tail', 'quick_win', 'new_page', 'content_gap', 'technical_gap', 'positioning'];
232
+
233
+ /**
234
+ * Insert a single insight on behalf of an external agent (e.g. via MCP).
235
+ * Uses the same dedup contract as analyze-time inserts (UNIQUE on
236
+ * project + type + fingerprint), so an agent repeating the same finding
237
+ * across sessions updates `last_seen` instead of duplicating rows.
238
+ *
239
+ * Returns { ok, id, fingerprint, deduped } — `deduped: true` when the row
240
+ * already existed and we only refreshed last_seen.
241
+ */
242
+ export function insertAgentInsight(db, { project, type, data, agentName }) {
243
+ if (!AGENT_INSIGHT_TYPES.includes(type)) {
244
+ return { ok: false, error: `Unsupported type "${type}". Allowed: ${AGENT_INSIGHT_TYPES.join(', ')}` };
245
+ }
246
+ if (!project) return { ok: false, error: 'project is required' };
247
+ if (!data || typeof data !== 'object') return { ok: false, error: 'data must be an object' };
248
+
249
+ const fingerprint = _insightFingerprint(type, data);
250
+ if (!fingerprint) {
251
+ return { ok: false, error: `data is missing the identifier field this type needs (see _insightFingerprint in db/db.js for the per-type contract)` };
252
+ }
253
+
254
+ const source = agentName ? `agent:${agentName}` : 'agent';
255
+ const ts = Date.now();
256
+
257
+ // Stash provenance inside the data blob too — survives if/when the source
258
+ // column is ever queried separately, but also keeps it visible to consumers
259
+ // that only read `data`.
260
+ const enriched = { ...data, _source: source, _ingested_at: new Date(ts).toISOString() };
261
+
262
+ const existing = db.prepare(
263
+ 'SELECT id FROM insights WHERE project = ? AND type = ? AND fingerprint = ?'
264
+ ).get(project, type, fingerprint);
265
+
266
+ db.prepare(`
267
+ INSERT INTO insights (project, type, status, fingerprint, first_seen, last_seen, source_analysis_id, data, source)
268
+ VALUES (?, ?, 'active', ?, ?, ?, NULL, ?, ?)
269
+ ON CONFLICT(project, type, fingerprint) DO UPDATE SET
270
+ last_seen = excluded.last_seen,
271
+ data = excluded.data,
272
+ source = excluded.source
273
+ `).run(project, type, fingerprint, ts, ts, JSON.stringify(enriched), source);
274
+
275
+ const row = db.prepare(
276
+ 'SELECT id FROM insights WHERE project = ? AND type = ? AND fingerprint = ?'
277
+ ).get(project, type, fingerprint);
278
+
279
+ return { ok: true, id: row.id, fingerprint, deduped: !!existing, source, last_seen: ts };
280
+ }
281
+
228
282
  // ── Read active insights (accumulated across all runs) ──────────────────────
229
283
 
230
284
  export function getActiveInsights(db, project) {
package/mcp/server.js CHANGED
@@ -25,7 +25,7 @@ import { spawn } from 'child_process';
25
25
  import { dirname, join } from 'path';
26
26
  import { fileURLToPath } from 'url';
27
27
 
28
- import { getDb } from '../db/db.js';
28
+ import { getDb, insertAgentInsight, AGENT_INSIGHT_TYPES } from '../db/db.js';
29
29
  import { getIntel, INTEL_SLICES, FREE_SLICES } from '../lib/intel.js';
30
30
  import { isPro } from '../lib/license.js';
31
31
  import { readProgress } from '../lib/progress.js';
@@ -307,11 +307,68 @@ server.registerTool(
307
307
  }
308
308
  );
309
309
 
310
+ // ── Tool: ingest_insight (free — write-back closes the loop) ──────────────
311
+ server.registerTool(
312
+ 'ingest_insight',
313
+ {
314
+ description: [
315
+ 'Persist an agent-generated insight into the SEO Intel Intelligence Ledger so it shows up in the dashboard and survives across sessions. Free tier — the agent\'s own LLM did the analysis; we just provide storage.',
316
+ '',
317
+ 'Dedup contract: same (project, type, fingerprint) updates `last_seen` instead of creating a duplicate row. So an agent rediscovering the same finding across sessions cleanly bumps the timestamp.',
318
+ '',
319
+ 'Allowed types (mirror what the cloud `analyze` command writes):',
320
+ ' keyword_gap data: { keyword, ... } fingerprint = keyword',
321
+ ' long_tail data: { phrase, ... } fingerprint = phrase',
322
+ ' quick_win data: { page, issue, ... } fingerprint = page::issue',
323
+ ' new_page data: { target_keyword | title, ... }',
324
+ ' content_gap data: { topic, ... } fingerprint = topic',
325
+ ' technical_gap data: { gap, ... } fingerprint = gap',
326
+ ' positioning data: { ...free-form... } one slot per project',
327
+ '',
328
+ 'data must include the identifier field above; otherwise the tool returns an error.',
329
+ ].join('\n'),
330
+ inputSchema: {
331
+ project: z.string().describe('Project slug'),
332
+ type: z.enum(AGENT_INSIGHT_TYPES).describe('Insight type from the allowed set'),
333
+ data: z.record(z.any()).describe('Insight payload — JSON object. Must include the identifier field for the chosen type.'),
334
+ agent_name: z.string().optional().describe('Optional provenance tag (e.g. "claude-opus-4-7"). Stored as source="agent:<name>".'),
335
+ },
336
+ },
337
+ async ({ project, type, data, agent_name }) => {
338
+ try {
339
+ const db = getDb();
340
+ const result = insertAgentInsight(db, { project, type, data, agentName: agent_name });
341
+ if (!result.ok) {
342
+ return { content: [{ type: 'text', text: `seo-intel ingest error: ${result.error}` }], isError: true };
343
+ }
344
+ const payload = {
345
+ ok: true,
346
+ project,
347
+ type,
348
+ insight_id: result.id,
349
+ fingerprint: result.fingerprint,
350
+ deduped: result.deduped,
351
+ source: result.source,
352
+ last_seen: new Date(result.last_seen).toISOString(),
353
+ hint: result.deduped
354
+ ? 'Insight already existed; last_seen refreshed.'
355
+ : 'New insight persisted. It will appear in the dashboard ledger and in get_intel(for=audit).',
356
+ };
357
+ return {
358
+ content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }],
359
+ structuredContent: payload,
360
+ };
361
+ } catch (err) {
362
+ return { content: [{ type: 'text', text: `seo-intel error: ${err.message}` }], isError: true };
363
+ }
364
+ }
365
+ );
366
+
310
367
  async function main() {
311
368
  const transport = new StdioServerTransport();
312
369
  await server.connect(transport);
313
370
  // stderr is fine; the host typically surfaces this in its MCP logs panel.
314
- console.error(`[seo-intel-mcp] v${VERSION} ready on stdio. Tools: list_projects, get_intel, get_pages, list_keywords, get_headings, run_crawl, get_crawl_status.`);
371
+ console.error(`[seo-intel-mcp] v${VERSION} ready on stdio. Tools: list_projects, get_intel, get_pages, list_keywords, get_headings, run_crawl, get_crawl_status, ingest_insight.`);
315
372
  }
316
373
 
317
374
  main().catch(err => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "seo-intel",
3
- "version": "1.5.28",
3
+ "version": "1.5.29",
4
4
  "description": "Local Ahrefs-style SEO competitor intelligence. Crawl → SQLite → cloud analysis.",
5
5
  "type": "module",
6
6
  "license": "SEE LICENSE IN LICENSE",
package/seo-intel.png CHANGED
Binary file