@ainyc/canonry 3.6.4 → 4.1.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.
@@ -8,9 +8,11 @@ import {
8
8
  notificationEventSchema,
9
9
  projectConfigSchema,
10
10
  projectUpsertRequestSchema,
11
+ queryBatchRequestSchema,
12
+ queryGenerateRequestSchema,
11
13
  runTriggerRequestSchema,
12
14
  scheduleUpsertRequestSchema
13
- } from "./chunk-O7EVT3AF.js";
15
+ } from "./chunk-O5JZQUPX.js";
14
16
 
15
17
  // src/config.ts
16
18
  import fs from "fs";
@@ -454,6 +456,18 @@ var ApiClient = class {
454
456
  async deleteProject(name) {
455
457
  await this.request("DELETE", `/projects/${encodeURIComponent(name)}`);
456
458
  }
459
+ async putQueries(project, queries) {
460
+ await this.request("PUT", `/projects/${encodeURIComponent(project)}/queries`, { queries });
461
+ }
462
+ async listQueries(project) {
463
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/queries`);
464
+ }
465
+ async deleteQueries(project, queries) {
466
+ await this.request("DELETE", `/projects/${encodeURIComponent(project)}/queries`, { queries });
467
+ }
468
+ async appendQueries(project, queries) {
469
+ await this.request("POST", `/projects/${encodeURIComponent(project)}/queries`, { queries });
470
+ }
457
471
  async putKeywords(project, keywords) {
458
472
  await this.request("PUT", `/projects/${encodeURIComponent(project)}/keywords`, { keywords });
459
473
  }
@@ -567,6 +581,13 @@ var ApiClient = class {
567
581
  async updateTelemetry(enabled) {
568
582
  return this.request("PUT", "/telemetry", { enabled });
569
583
  }
584
+ async generateQueries(project, provider, count) {
585
+ return this.request(
586
+ "POST",
587
+ `/projects/${encodeURIComponent(project)}/queries/generate`,
588
+ { provider, count }
589
+ );
590
+ }
570
591
  async generateKeywords(project, provider, count) {
571
592
  return this.request(
572
593
  "POST",
@@ -1042,6 +1063,14 @@ var gaWindowInputSchema = z2.object({
1042
1063
  var gaTrafficInputSchema = gaWindowInputSchema.extend({
1043
1064
  limit: z2.number().int().positive().max(500).optional()
1044
1065
  });
1066
+ var queriesInputSchema = z2.object({
1067
+ project: projectNameSchema,
1068
+ request: queryBatchRequestSchema
1069
+ });
1070
+ var queryGenerateInputSchema = z2.object({
1071
+ project: projectNameSchema,
1072
+ request: queryGenerateRequestSchema
1073
+ });
1045
1074
  var keywordsInputSchema = z2.object({
1046
1075
  project: projectNameSchema,
1047
1076
  request: keywordBatchRequestSchema
@@ -1124,7 +1153,7 @@ var canonryMcpTools = [
1124
1153
  defineTool({
1125
1154
  name: "canonry_project_overview",
1126
1155
  title: "Get project overview (composite)",
1127
- description: 'One-call summary for "how is project X doing?" \u2014 bundles project info, latest run, top undismissed insights, latest health snapshot, keyword cited rate, per-provider breakdown, and gained/lost/emerging vs the previous run. Prefer this over fanning out to separate tools.',
1156
+ description: 'One-call summary for "how is project X doing?" \u2014 bundles project info, latest run, top undismissed insights, latest health snapshot, query cited rate, per-provider breakdown, and gained/lost/emerging vs the previous run. Prefer this over fanning out to separate tools.',
1128
1157
  access: "read",
1129
1158
  tier: "core",
1130
1159
  inputSchema: projectInputSchema,
@@ -1135,7 +1164,7 @@ var canonryMcpTools = [
1135
1164
  defineTool({
1136
1165
  name: "canonry_report",
1137
1166
  title: "Get aggregated AEO report",
1138
- description: "Returns the full client-facing AEO report bundle for a project \u2014 executive summary, per-keyword \xD7 per-provider citation matrix, competitor landscape, AI citation sources, GSC/GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload `canonry report <project>` consumes to render the self-contained HTML.",
1167
+ description: "Returns the full client-facing AEO report bundle for a project \u2014 executive summary, per-query \xD7 per-provider citation matrix, competitor landscape, AI citation sources, GSC/GA4 performance, social and AI referrals, indexing health, citations trend, prioritized insights, and recommended next steps. Same payload `canonry report <project>` consumes to render the self-contained HTML.",
1139
1168
  access: "read",
1140
1169
  tier: "monitoring",
1141
1170
  inputSchema: projectInputSchema,
@@ -1146,7 +1175,7 @@ var canonryMcpTools = [
1146
1175
  defineTool({
1147
1176
  name: "canonry_search",
1148
1177
  title: "Search project (composite)",
1149
- description: "Search query snapshots and intelligence insights for the given text. Looks at snapshot answer text, cited domains, raw provider responses, and insight title/keyword/recommendation/cause. Returns ranked hits with snippets \u2014 use it instead of paginating snapshots when you need to find a competitor mention or term.",
1178
+ description: "Search query snapshots and intelligence insights for the given text. Looks at snapshot answer text, cited domains, raw provider responses, and insight title/query/recommendation/cause. Returns ranked hits with snippets \u2014 use it instead of paginating snapshots when you need to find a competitor mention or term.",
1150
1179
  access: "read",
1151
1180
  tier: "core",
1152
1181
  inputSchema: z2.object({
@@ -1227,7 +1256,7 @@ var canonryMcpTools = [
1227
1256
  defineTool({
1228
1257
  name: "canonry_timeline_get",
1229
1258
  title: "Get project timeline",
1230
- description: "Get per-keyword citation history for a Canonry project.",
1259
+ description: "Get per-query citation history for a Canonry project.",
1231
1260
  access: "read",
1232
1261
  tier: "monitoring",
1233
1262
  inputSchema: timelineInputSchema,
@@ -1308,7 +1337,7 @@ var canonryMcpTools = [
1308
1337
  defineTool({
1309
1338
  name: "canonry_citations_visibility",
1310
1339
  title: "Get citation visibility",
1311
- description: 'Single-call AI citation surface for a Canonry project. Returns the project headline (cited by N of M engines), per-keyword engine coverage rows from the latest snapshot per (keyword \xD7 provider), and a competitor-gap list (keywords where a configured competitor is cited but the project is not). Carries `status: "no-data"` with `reason: "no-keywords"` or `"no-runs-yet"` when inputs are missing.',
1340
+ description: 'Single-call AI citation surface for a Canonry project. Returns the project headline (cited by N of M engines), per-query engine coverage rows from the latest snapshot per (query \xD7 provider), and a competitor-gap list (queries where a configured competitor is cited but the project is not). Carries `status: "no-data"` with `reason: "no-queries"` or `"no-runs-yet"` when inputs are missing.',
1312
1341
  access: "read",
1313
1342
  tier: "monitoring",
1314
1343
  inputSchema: projectInputSchema,
@@ -1352,10 +1381,21 @@ var canonryMcpTools = [
1352
1381
  openApiOperations: ["GET /api/v1/projects/{name}/content/gaps"],
1353
1382
  handler: (client, input) => client.getContentGaps(input.project)
1354
1383
  }),
1384
+ defineTool({
1385
+ name: "canonry_queries_list",
1386
+ title: "List queries",
1387
+ description: "List tracked queries for a Canonry project.",
1388
+ access: "read",
1389
+ tier: "setup",
1390
+ inputSchema: projectInputSchema,
1391
+ annotations: readAnnotations(),
1392
+ openApiOperations: ["GET /api/v1/projects/{name}/queries"],
1393
+ handler: (client, input) => client.listQueries(input.project)
1394
+ }),
1355
1395
  defineTool({
1356
1396
  name: "canonry_keywords_list",
1357
- title: "List keywords",
1358
- description: "List tracked keywords for a Canonry project.",
1397
+ title: "List keywords (legacy alias)",
1398
+ description: "Legacy alias for canonry_queries_list. Returns tracked queries using the pre-queries keyword response shape.",
1359
1399
  access: "read",
1360
1400
  tier: "setup",
1361
1401
  inputSchema: projectInputSchema,
@@ -1609,10 +1649,21 @@ var canonryMcpTools = [
1609
1649
  openApiOperations: ["POST /api/v1/apply"],
1610
1650
  handler: (client, input) => client.apply(input.config)
1611
1651
  }),
1652
+ defineTool({
1653
+ name: "canonry_queries_generate",
1654
+ title: "Generate query suggestions",
1655
+ description: "Generate candidate queries using a configured provider. Returns suggestions only; use canonry_queries_add to persist them.",
1656
+ access: "write",
1657
+ tier: "setup",
1658
+ inputSchema: queryGenerateInputSchema,
1659
+ annotations: writeAnnotations({ idempotentHint: false, openWorldHint: true }),
1660
+ openApiOperations: ["POST /api/v1/projects/{name}/queries/generate"],
1661
+ handler: (client, input) => client.generateQueries(input.project, input.request.provider, input.request.count)
1662
+ }),
1612
1663
  defineTool({
1613
1664
  name: "canonry_keywords_generate",
1614
- title: "Generate keyword suggestions",
1615
- description: "Generate candidate key phrases using a configured provider. Returns suggestions only; use canonry_keywords_add to persist them.",
1665
+ title: "Generate keyword suggestions (legacy alias)",
1666
+ description: "Legacy alias for canonry_queries_generate. Returns suggestions using the pre-queries keyword response shape.",
1616
1667
  access: "write",
1617
1668
  tier: "setup",
1618
1669
  inputSchema: keywordGenerateInputSchema,
@@ -1620,10 +1671,23 @@ var canonryMcpTools = [
1620
1671
  openApiOperations: ["POST /api/v1/projects/{name}/keywords/generate"],
1621
1672
  handler: (client, input) => client.generateKeywords(input.project, input.request.provider, input.request.count)
1622
1673
  }),
1674
+ defineTool({
1675
+ name: "canonry_queries_replace",
1676
+ title: "Replace queries",
1677
+ description: "Replace the tracked query set for a Canonry project.",
1678
+ access: "write",
1679
+ tier: "setup",
1680
+ inputSchema: queriesInputSchema,
1681
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
1682
+ openApiOperations: ["PUT /api/v1/projects/{name}/queries"],
1683
+ handler: async (client, input) => {
1684
+ await client.putQueries(input.project, uniqueStrings(input.request.queries));
1685
+ }
1686
+ }),
1623
1687
  defineTool({
1624
1688
  name: "canonry_keywords_replace",
1625
- title: "Replace keywords",
1626
- description: "Replace the tracked keyword set for a Canonry project.",
1689
+ title: "Replace keywords (legacy alias)",
1690
+ description: "Legacy alias for canonry_queries_replace. Replaces the same canonical tracked query set.",
1627
1691
  access: "write",
1628
1692
  tier: "setup",
1629
1693
  inputSchema: keywordsInputSchema,
@@ -1655,10 +1719,23 @@ var canonryMcpTools = [
1655
1719
  openApiOperations: ["POST /api/v1/runs/{id}/cancel"],
1656
1720
  handler: (client, input) => client.cancelRun(input.runId)
1657
1721
  }),
1722
+ defineTool({
1723
+ name: "canonry_queries_add",
1724
+ title: "Add queries",
1725
+ description: "Append tracked queries to a Canonry project; existing queries are skipped by the API.",
1726
+ access: "write",
1727
+ tier: "setup",
1728
+ inputSchema: queriesInputSchema,
1729
+ annotations: writeAnnotations({ idempotentHint: true }),
1730
+ openApiOperations: ["POST /api/v1/projects/{name}/queries"],
1731
+ handler: async (client, input) => {
1732
+ await client.appendQueries(input.project, uniqueStrings(input.request.queries));
1733
+ }
1734
+ }),
1658
1735
  defineTool({
1659
1736
  name: "canonry_keywords_add",
1660
- title: "Add keywords",
1661
- description: "Append tracked keywords to a Canonry project; existing keywords are skipped by the API.",
1737
+ title: "Add keywords (legacy alias)",
1738
+ description: "Legacy alias for canonry_queries_add. Appends to the same canonical tracked query set.",
1662
1739
  access: "write",
1663
1740
  tier: "setup",
1664
1741
  inputSchema: keywordsInputSchema,
@@ -1668,10 +1745,23 @@ var canonryMcpTools = [
1668
1745
  await client.appendKeywords(input.project, uniqueStrings(input.request.keywords));
1669
1746
  }
1670
1747
  }),
1748
+ defineTool({
1749
+ name: "canonry_queries_remove",
1750
+ title: "Remove queries",
1751
+ description: "Remove tracked queries from a Canonry project.",
1752
+ access: "write",
1753
+ tier: "setup",
1754
+ inputSchema: queriesInputSchema,
1755
+ annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
1756
+ openApiOperations: ["DELETE /api/v1/projects/{name}/queries"],
1757
+ handler: async (client, input) => {
1758
+ await client.deleteQueries(input.project, uniqueStrings(input.request.queries));
1759
+ }
1760
+ }),
1671
1761
  defineTool({
1672
1762
  name: "canonry_keywords_remove",
1673
- title: "Remove keywords",
1674
- description: "Remove tracked keywords from a Canonry project.",
1763
+ title: "Remove keywords (legacy alias)",
1764
+ description: "Legacy alias for canonry_queries_remove. Removes from the same canonical tracked query set.",
1675
1765
  access: "write",
1676
1766
  tier: "setup",
1677
1767
  inputSchema: keywordsInputSchema,
@@ -2,7 +2,7 @@ import {
2
2
  ContentActions,
3
3
  RunKinds,
4
4
  __export
5
- } from "./chunk-O7EVT3AF.js";
5
+ } from "./chunk-O5JZQUPX.js";
6
6
 
7
7
  // src/intelligence-service.ts
8
8
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
@@ -40,10 +40,10 @@ __export(schema_exports, {
40
40
  gscUrlInspections: () => gscUrlInspections,
41
41
  healthSnapshots: () => healthSnapshots,
42
42
  insights: () => insights,
43
- keywords: () => keywords,
44
43
  migrationsTable: () => migrationsTable,
45
44
  notifications: () => notifications,
46
45
  projects: () => projects,
46
+ queries: () => queries,
47
47
  querySnapshots: () => querySnapshots,
48
48
  runs: () => runs,
49
49
  schedules: () => schedules,
@@ -69,14 +69,14 @@ var projects = sqliteTable("projects", {
69
69
  createdAt: text("created_at").notNull(),
70
70
  updatedAt: text("updated_at").notNull()
71
71
  });
72
- var keywords = sqliteTable("keywords", {
72
+ var queries = sqliteTable("queries", {
73
73
  id: text("id").primaryKey(),
74
74
  projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
75
- keyword: text("keyword").notNull(),
75
+ query: text("query").notNull(),
76
76
  createdAt: text("created_at").notNull()
77
77
  }, (table) => [
78
- index("idx_keywords_project").on(table.projectId),
79
- uniqueIndex("idx_keywords_project_keyword").on(table.projectId, table.keyword)
78
+ index("idx_queries_project").on(table.projectId),
79
+ uniqueIndex("idx_queries_project_query").on(table.projectId, table.query)
80
80
  ]);
81
81
  var competitors = sqliteTable("competitors", {
82
82
  id: text("id").primaryKey(),
@@ -105,7 +105,7 @@ var runs = sqliteTable("runs", {
105
105
  var querySnapshots = sqliteTable("query_snapshots", {
106
106
  id: text("id").primaryKey(),
107
107
  runId: text("run_id").notNull().references(() => runs.id, { onDelete: "cascade" }),
108
- keywordId: text("keyword_id").notNull().references(() => keywords.id, { onDelete: "cascade" }),
108
+ queryId: text("query_id").notNull().references(() => queries.id, { onDelete: "cascade" }),
109
109
  provider: text("provider").notNull().default("gemini"),
110
110
  model: text("model"),
111
111
  citationState: text("citation_state").notNull(),
@@ -120,7 +120,7 @@ var querySnapshots = sqliteTable("query_snapshots", {
120
120
  createdAt: text("created_at").notNull()
121
121
  }, (table) => [
122
122
  index("idx_snapshots_run").on(table.runId),
123
- index("idx_snapshots_keyword").on(table.keywordId),
123
+ index("idx_snapshots_query").on(table.queryId),
124
124
  index("idx_snapshots_citation_state").on(table.citationState),
125
125
  index("idx_snapshots_provider_model").on(table.provider, table.model),
126
126
  index("idx_snapshots_location").on(table.location),
@@ -428,7 +428,7 @@ var insights = sqliteTable("insights", {
428
428
  type: text("type").notNull(),
429
429
  severity: text("severity").notNull(),
430
430
  title: text("title").notNull(),
431
- keyword: text("keyword").notNull(),
431
+ query: text("query").notNull(),
432
432
  provider: text("provider").notNull(),
433
433
  recommendation: text("recommendation"),
434
434
  cause: text("cause"),
@@ -438,7 +438,7 @@ var insights = sqliteTable("insights", {
438
438
  index("idx_insights_project").on(table.projectId),
439
439
  index("idx_insights_run").on(table.runId),
440
440
  index("idx_insights_created").on(table.createdAt),
441
- index("idx_insights_keyword_provider").on(table.keyword, table.provider)
441
+ index("idx_insights_query_provider").on(table.query, table.provider)
442
442
  ]);
443
443
  var healthSnapshots = sqliteTable("health_snapshots", {
444
444
  id: text("id").primaryKey(),
@@ -580,12 +580,12 @@ CREATE TABLE IF NOT EXISTS projects (
580
580
  updated_at TEXT NOT NULL
581
581
  );
582
582
 
583
- CREATE TABLE IF NOT EXISTS keywords (
583
+ CREATE TABLE IF NOT EXISTS queries (
584
584
  id TEXT PRIMARY KEY,
585
585
  project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
586
- keyword TEXT NOT NULL,
586
+ query TEXT NOT NULL,
587
587
  created_at TEXT NOT NULL,
588
- UNIQUE(project_id, keyword)
588
+ UNIQUE(project_id, query)
589
589
  );
590
590
 
591
591
  CREATE TABLE IF NOT EXISTS competitors (
@@ -611,7 +611,7 @@ CREATE TABLE IF NOT EXISTS runs (
611
611
  CREATE TABLE IF NOT EXISTS query_snapshots (
612
612
  id TEXT PRIMARY KEY,
613
613
  run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
614
- keyword_id TEXT NOT NULL REFERENCES keywords(id) ON DELETE CASCADE,
614
+ query_id TEXT NOT NULL REFERENCES queries(id) ON DELETE CASCADE,
615
615
  provider TEXT NOT NULL DEFAULT 'gemini',
616
616
  citation_state TEXT NOT NULL,
617
617
  answer_text TEXT,
@@ -653,12 +653,12 @@ CREATE TABLE IF NOT EXISTS usage_counters (
653
653
  UNIQUE(scope, period, metric)
654
654
  );
655
655
 
656
- CREATE INDEX IF NOT EXISTS idx_keywords_project ON keywords(project_id);
656
+ CREATE INDEX IF NOT EXISTS idx_queries_project ON queries(project_id);
657
657
  CREATE INDEX IF NOT EXISTS idx_competitors_project ON competitors(project_id);
658
658
  CREATE INDEX IF NOT EXISTS idx_runs_project ON runs(project_id);
659
659
  CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
660
660
  CREATE INDEX IF NOT EXISTS idx_snapshots_run ON query_snapshots(run_id);
661
- CREATE INDEX IF NOT EXISTS idx_snapshots_keyword ON query_snapshots(keyword_id);
661
+ CREATE INDEX IF NOT EXISTS idx_snapshots_query ON query_snapshots(query_id);
662
662
  CREATE INDEX IF NOT EXISTS idx_audit_log_project ON audit_log(project_id);
663
663
  CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at);
664
664
  CREATE TABLE IF NOT EXISTS schedules (
@@ -975,7 +975,7 @@ var MIGRATION_VERSIONS = [
975
975
  version: 19,
976
976
  name: "named-unique-indexes",
977
977
  statements: [
978
- `CREATE UNIQUE INDEX IF NOT EXISTS idx_keywords_project_keyword ON keywords(project_id, keyword)`,
978
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_queries_project_query ON queries(project_id, query)`,
979
979
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_competitors_project_domain ON competitors(project_id, domain)`,
980
980
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id)`,
981
981
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_scope_period_metric ON usage_counters(scope, period, metric)`,
@@ -1020,7 +1020,7 @@ var MIGRATION_VERSIONS = [
1020
1020
  type TEXT NOT NULL,
1021
1021
  severity TEXT NOT NULL,
1022
1022
  title TEXT NOT NULL,
1023
- keyword TEXT NOT NULL,
1023
+ query TEXT NOT NULL,
1024
1024
  provider TEXT NOT NULL,
1025
1025
  recommendation TEXT,
1026
1026
  cause TEXT,
@@ -1029,7 +1029,7 @@ var MIGRATION_VERSIONS = [
1029
1029
  )`,
1030
1030
  `CREATE INDEX IF NOT EXISTS idx_insights_project ON insights(project_id)`,
1031
1031
  `CREATE INDEX IF NOT EXISTS idx_insights_created ON insights(created_at)`,
1032
- `CREATE INDEX IF NOT EXISTS idx_insights_keyword_provider ON insights(keyword, provider)`
1032
+ `CREATE INDEX IF NOT EXISTS idx_insights_query_provider ON insights(query, provider)`
1033
1033
  ]
1034
1034
  },
1035
1035
  {
@@ -1373,6 +1373,26 @@ var MIGRATION_VERSIONS = [
1373
1373
  `CREATE INDEX IF NOT EXISTS idx_ga_window_summary_run
1374
1374
  ON ga_traffic_window_summaries(sync_run_id)`
1375
1375
  ]
1376
+ },
1377
+ {
1378
+ version: 48,
1379
+ name: "rename-keywords-to-queries",
1380
+ // The actual legacy rename runs before bootstrap SQL so existing DBs never
1381
+ // see new-name indexes before their old columns have been renamed. This
1382
+ // version records the schema cutover and lands the final index names.
1383
+ statements: [
1384
+ `DROP INDEX IF EXISTS idx_keywords_project`,
1385
+ `DROP INDEX IF EXISTS idx_keywords_project_keyword`,
1386
+ `DROP INDEX IF EXISTS idx_snapshots_keyword`,
1387
+ `DROP INDEX IF EXISTS idx_insights_keyword_provider`,
1388
+ `CREATE INDEX IF NOT EXISTS idx_queries_project ON queries(project_id)`,
1389
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_queries_project_query ON queries(project_id, query)`,
1390
+ `CREATE INDEX IF NOT EXISTS idx_snapshots_query ON query_snapshots(query_id)`,
1391
+ `CREATE INDEX IF NOT EXISTS idx_insights_query_provider ON insights(query, provider)`
1392
+ ],
1393
+ run: (tx) => {
1394
+ normalizeLegacyQuerySchema(tx);
1395
+ }
1376
1396
  }
1377
1397
  ];
1378
1398
  function isDuplicateColumnError(err) {
@@ -1387,6 +1407,44 @@ function columnExists(db, table, column) {
1387
1407
  ));
1388
1408
  return (rows[0]?.c ?? 0) > 0;
1389
1409
  }
1410
+ function tableExists(db, table) {
1411
+ const rows = db.all(sql.raw(
1412
+ `SELECT COUNT(*) as c FROM sqlite_master WHERE type = 'table' AND name = '${table}'`
1413
+ ));
1414
+ return (rows[0]?.c ?? 0) > 0;
1415
+ }
1416
+ function tableIsEmpty(db, table) {
1417
+ const rows = db.all(sql.raw(`SELECT COUNT(*) as c FROM ${table}`));
1418
+ return (rows[0]?.c ?? 0) === 0;
1419
+ }
1420
+ function hasLegacyQuerySchema(db) {
1421
+ return tableExists(db, "keywords") || columnExists(db, "query_snapshots", "keyword_id") || columnExists(db, "insights", "keyword");
1422
+ }
1423
+ function normalizeLegacyQuerySchema(db) {
1424
+ if (!hasLegacyQuerySchema(db)) return;
1425
+ if (tableExists(db, "keywords") && tableExists(db, "queries")) {
1426
+ if (!tableIsEmpty(db, "queries")) {
1427
+ throw new Error("Cannot migrate keywords to queries because both tables contain data");
1428
+ }
1429
+ db.run(sql.raw(`DROP TABLE queries`));
1430
+ }
1431
+ db.run(sql.raw(`DROP INDEX IF EXISTS idx_keywords_project`));
1432
+ db.run(sql.raw(`DROP INDEX IF EXISTS idx_keywords_project_keyword`));
1433
+ db.run(sql.raw(`DROP INDEX IF EXISTS idx_snapshots_keyword`));
1434
+ db.run(sql.raw(`DROP INDEX IF EXISTS idx_insights_keyword_provider`));
1435
+ if (tableExists(db, "keywords")) {
1436
+ db.run(sql.raw(`ALTER TABLE keywords RENAME TO queries`));
1437
+ }
1438
+ if (columnExists(db, "queries", "keyword")) {
1439
+ db.run(sql.raw(`ALTER TABLE queries RENAME COLUMN keyword TO query`));
1440
+ }
1441
+ if (columnExists(db, "query_snapshots", "keyword_id")) {
1442
+ db.run(sql.raw(`ALTER TABLE query_snapshots RENAME COLUMN keyword_id TO query_id`));
1443
+ }
1444
+ if (columnExists(db, "insights", "keyword")) {
1445
+ db.run(sql.raw(`ALTER TABLE insights RENAME COLUMN keyword TO query`));
1446
+ }
1447
+ }
1390
1448
  function dropColumnIfExists(db, table, column) {
1391
1449
  try {
1392
1450
  db.run(sql.raw(`ALTER TABLE ${table} DROP COLUMN ${column}`));
@@ -1466,6 +1524,9 @@ function recordMigration(db, version, name) {
1466
1524
  db.run(sql`INSERT OR IGNORE INTO _migrations (version, name) VALUES (${version}, ${name})`);
1467
1525
  }
1468
1526
  function migrate(db) {
1527
+ db.transaction((tx) => {
1528
+ normalizeLegacyQuerySchema(tx);
1529
+ });
1469
1530
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
1470
1531
  for (const statement of statements) {
1471
1532
  db.run(sql.raw(statement));
@@ -1482,6 +1543,7 @@ function migrate(db) {
1482
1543
  throw err;
1483
1544
  }
1484
1545
  }
1546
+ mv.run?.(tx);
1485
1547
  recordMigration(tx, mv.version, mv.name);
1486
1548
  });
1487
1549
  }
@@ -1493,18 +1555,18 @@ function detectRegressions(currentRun, previousRun) {
1493
1555
  const previousCited = /* @__PURE__ */ new Map();
1494
1556
  for (const snap of previousRun.snapshots) {
1495
1557
  if (snap.cited) {
1496
- previousCited.set(`${snap.keyword}:${snap.provider}`, {
1558
+ previousCited.set(`${snap.query}:${snap.provider}`, {
1497
1559
  citationUrl: snap.citationUrl,
1498
1560
  position: snap.position
1499
1561
  });
1500
1562
  }
1501
1563
  }
1502
1564
  for (const snap of currentRun.snapshots) {
1503
- const key = `${snap.keyword}:${snap.provider}`;
1565
+ const key = `${snap.query}:${snap.provider}`;
1504
1566
  if (!snap.cited && previousCited.has(key)) {
1505
1567
  const prev = previousCited.get(key);
1506
1568
  regressions.push({
1507
- keyword: snap.keyword,
1569
+ query: snap.query,
1508
1570
  provider: snap.provider,
1509
1571
  previousCitationUrl: prev.citationUrl,
1510
1572
  previousPosition: prev.position,
@@ -1522,14 +1584,14 @@ function detectGains(currentRun, previousRun) {
1522
1584
  const previousCited = /* @__PURE__ */ new Set();
1523
1585
  for (const snap of previousRun.snapshots) {
1524
1586
  if (snap.cited) {
1525
- previousCited.add(`${snap.keyword}:${snap.provider}`);
1587
+ previousCited.add(`${snap.query}:${snap.provider}`);
1526
1588
  }
1527
1589
  }
1528
1590
  for (const snap of currentRun.snapshots) {
1529
- const key = `${snap.keyword}:${snap.provider}`;
1591
+ const key = `${snap.query}:${snap.provider}`;
1530
1592
  if (snap.cited && !previousCited.has(key)) {
1531
1593
  gains.push({
1532
- keyword: snap.keyword,
1594
+ query: snap.query,
1533
1595
  provider: snap.provider,
1534
1596
  citationUrl: snap.citationUrl,
1535
1597
  position: snap.position,
@@ -1588,18 +1650,18 @@ function computeHealthTrend(runs2) {
1588
1650
  // ../intelligence/src/causes.ts
1589
1651
  function analyzeCause(regression, currentSnapshots) {
1590
1652
  const currentSnap = currentSnapshots.find(
1591
- (s) => s.keyword === regression.keyword && s.provider === regression.provider && !s.cited && s.competitorDomain
1653
+ (s) => s.query === regression.query && s.provider === regression.provider && !s.cited && s.competitorDomain
1592
1654
  );
1593
1655
  if (currentSnap) {
1594
1656
  return {
1595
1657
  cause: "competitor_gain",
1596
1658
  competitorDomain: currentSnap.competitorDomain,
1597
- details: `Competitor ${currentSnap.competitorDomain} now cited for "${regression.keyword}" on ${regression.provider}`
1659
+ details: `Competitor ${currentSnap.competitorDomain} now cited for "${regression.query}" on ${regression.provider}`
1598
1660
  };
1599
1661
  }
1600
1662
  return {
1601
1663
  cause: "unknown",
1602
- details: `No specific cause identified for loss of "${regression.keyword}" on ${regression.provider}`
1664
+ details: `No specific cause identified for loss of "${regression.query}" on ${regression.provider}`
1603
1665
  };
1604
1666
  }
1605
1667
 
@@ -1609,14 +1671,14 @@ function generateInsights(regressions, gains, health, causes) {
1609
1671
  const insights2 = [];
1610
1672
  const now = (/* @__PURE__ */ new Date()).toISOString();
1611
1673
  for (const reg of regressions) {
1612
- const key = `${reg.keyword}:${reg.provider}`;
1674
+ const key = `${reg.query}:${reg.provider}`;
1613
1675
  const cause = causes.get(key);
1614
1676
  insights2.push({
1615
1677
  id: `ins_${randomUUID().slice(0, 8)}`,
1616
1678
  type: "regression",
1617
1679
  severity: "high",
1618
- title: `Lost ${reg.provider} citation for "${reg.keyword}"`,
1619
- keyword: reg.keyword,
1680
+ title: `Lost ${reg.provider} citation for "${reg.query}"`,
1681
+ query: reg.query,
1620
1682
  provider: reg.provider,
1621
1683
  recommendation: {
1622
1684
  action: "audit",
@@ -1632,8 +1694,8 @@ function generateInsights(regressions, gains, health, causes) {
1632
1694
  id: `ins_${randomUUID().slice(0, 8)}`,
1633
1695
  type: "gain",
1634
1696
  severity: "low",
1635
- title: `New ${gain.provider} citation for "${gain.keyword}"`,
1636
- keyword: gain.keyword,
1697
+ title: `New ${gain.provider} citation for "${gain.query}"`,
1698
+ query: gain.query,
1637
1699
  provider: gain.provider,
1638
1700
  recommendation: {
1639
1701
  action: "monitor",
@@ -1655,7 +1717,7 @@ function analyzeRuns(currentRun, previousRun, allRuns) {
1655
1717
  const causes = /* @__PURE__ */ new Map();
1656
1718
  for (const reg of regressions) {
1657
1719
  const cause = analyzeCause(reg, currentRun.snapshots);
1658
- causes.set(`${reg.keyword}:${reg.provider}`, cause);
1720
+ causes.set(`${reg.query}:${reg.provider}`, cause);
1659
1721
  }
1660
1722
  const insights2 = generateInsights(regressions, gains, health, causes);
1661
1723
  return {
@@ -2027,7 +2089,7 @@ function classifyRegressionSeverity(signals) {
2027
2089
  }
2028
2090
 
2029
2091
  // ../intelligence/src/insight-grouping.ts
2030
- function groupInsights(insights2, keyFn = (i) => `${i.keyword} ${i.provider} ${i.type}`) {
2092
+ function groupInsights(insights2, keyFn = (i) => `${i.query} ${i.provider} ${i.type}`) {
2031
2093
  const order = [];
2032
2094
  const buckets = /* @__PURE__ */ new Map();
2033
2095
  for (const i of insights2) {
@@ -2254,10 +2316,10 @@ var IntelligenceService = class {
2254
2316
  }
2255
2317
  persistResult(result, runId, projectId) {
2256
2318
  const previouslyDismissed = /* @__PURE__ */ new Set();
2257
- const existingInsights = this.db.select({ keyword: insights.keyword, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq(insights.runId, runId)).all();
2319
+ const existingInsights = this.db.select({ query: insights.query, provider: insights.provider, type: insights.type, dismissed: insights.dismissed }).from(insights).where(eq(insights.runId, runId)).all();
2258
2320
  for (const row of existingInsights) {
2259
2321
  if (row.dismissed) {
2260
- previouslyDismissed.add(`${row.keyword}:${row.provider}:${row.type}`);
2322
+ previouslyDismissed.add(`${row.query}:${row.provider}:${row.type}`);
2261
2323
  }
2262
2324
  }
2263
2325
  this.db.transaction((tx) => {
@@ -2265,7 +2327,7 @@ var IntelligenceService = class {
2265
2327
  tx.delete(healthSnapshots).where(eq(healthSnapshots.runId, runId)).run();
2266
2328
  const now = (/* @__PURE__ */ new Date()).toISOString();
2267
2329
  for (const insight of result.insights) {
2268
- const wasDismissed = previouslyDismissed.has(`${insight.keyword}:${insight.provider}:${insight.type}`);
2330
+ const wasDismissed = previouslyDismissed.has(`${insight.query}:${insight.provider}:${insight.type}`);
2269
2331
  tx.insert(insights).values({
2270
2332
  id: insight.id,
2271
2333
  projectId,
@@ -2273,7 +2335,7 @@ var IntelligenceService = class {
2273
2335
  type: insight.type,
2274
2336
  severity: insight.severity,
2275
2337
  title: insight.title,
2276
- keyword: insight.keyword,
2338
+ query: insight.query,
2277
2339
  provider: insight.provider,
2278
2340
  recommendation: insight.recommendation ? JSON.stringify(insight.recommendation) : null,
2279
2341
  cause: insight.cause ? JSON.stringify(insight.cause) : null,
@@ -2315,10 +2377,10 @@ var IntelligenceService = class {
2315
2377
  if (regressions.length === 0) return rawInsights;
2316
2378
  const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq(gscSearchData.projectId, projectId)).all();
2317
2379
  const gscConnected = gscRows.length > 0;
2318
- const gscImpressionsByKeyword = /* @__PURE__ */ new Map();
2380
+ const gscImpressionsByQuery = /* @__PURE__ */ new Map();
2319
2381
  for (const row of gscRows) {
2320
2382
  const key = row.query.toLowerCase();
2321
- gscImpressionsByKeyword.set(key, (gscImpressionsByKeyword.get(key) ?? 0) + row.impressions);
2383
+ gscImpressionsByQuery.set(key, (gscImpressionsByQuery.get(key) ?? 0) + row.impressions);
2322
2384
  }
2323
2385
  const recentRunIds = this.db.select({ id: runs.id }).from(runs).where(
2324
2386
  and(
@@ -2330,16 +2392,16 @@ var IntelligenceService = class {
2330
2392
  const haveHistory = recentRunIds.length > 0;
2331
2393
  const priorRegressionsByPair = /* @__PURE__ */ new Map();
2332
2394
  if (haveHistory) {
2333
- const priorRows = this.db.select({ keyword: insights.keyword, provider: insights.provider }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
2395
+ const priorRows = this.db.select({ query: insights.query, provider: insights.provider }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
2334
2396
  for (const row of priorRows) {
2335
- const key = `${row.keyword}:${row.provider}`;
2397
+ const key = `${row.query}:${row.provider}`;
2336
2398
  priorRegressionsByPair.set(key, (priorRegressionsByPair.get(key) ?? 0) + 1);
2337
2399
  }
2338
2400
  }
2339
2401
  return rawInsights.map((insight) => {
2340
2402
  if (insight.type !== "regression") return insight;
2341
- const gscImpressions = gscConnected ? gscImpressionsByKeyword.get(insight.keyword.toLowerCase()) ?? 0 : void 0;
2342
- const recurrenceCount = haveHistory ? priorRegressionsByPair.get(`${insight.keyword}:${insight.provider}`) ?? 0 : void 0;
2403
+ const gscImpressions = gscConnected ? gscImpressionsByQuery.get(insight.query.toLowerCase()) ?? 0 : void 0;
2404
+ const recurrenceCount = haveHistory ? priorRegressionsByPair.get(`${insight.query}:${insight.provider}`) ?? 0 : void 0;
2343
2405
  const severity = classifyRegressionSeverity({
2344
2406
  gscImpressions,
2345
2407
  recurrenceCount
@@ -2349,17 +2411,17 @@ var IntelligenceService = class {
2349
2411
  }
2350
2412
  buildRunData(runId, projectId, completedAt) {
2351
2413
  const rows = this.db.select({
2352
- keyword: keywords.keyword,
2414
+ query: queries.query,
2353
2415
  provider: querySnapshots.provider,
2354
2416
  citationState: querySnapshots.citationState,
2355
2417
  citedDomains: querySnapshots.citedDomains,
2356
2418
  competitorOverlap: querySnapshots.competitorOverlap
2357
- }).from(querySnapshots).leftJoin(keywords, eq(querySnapshots.keywordId, keywords.id)).where(eq(querySnapshots.runId, runId)).all();
2419
+ }).from(querySnapshots).leftJoin(queries, eq(querySnapshots.queryId, queries.id)).where(eq(querySnapshots.runId, runId)).all();
2358
2420
  const snapshots = rows.map((r) => {
2359
2421
  const domains = parseJsonColumn(r.citedDomains, []);
2360
2422
  const competitors2 = parseJsonColumn(r.competitorOverlap, []);
2361
2423
  return {
2362
- keyword: r.keyword ?? "",
2424
+ query: r.query ?? "",
2363
2425
  provider: r.provider,
2364
2426
  cited: r.citationState === "cited",
2365
2427
  citationUrl: domains[0] ?? void 0,
@@ -2372,7 +2434,7 @@ var IntelligenceService = class {
2372
2434
 
2373
2435
  export {
2374
2436
  projects,
2375
- keywords,
2437
+ queries,
2376
2438
  competitors,
2377
2439
  runs,
2378
2440
  querySnapshots,