@ainyc/canonry 3.6.3 → 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.
- package/README.md +10 -10
- package/assets/agent-workspace/AGENTS.md +4 -4
- package/assets/agent-workspace/USER.md +1 -1
- package/assets/agent-workspace/skills/aero/SKILL.md +4 -4
- package/assets/agent-workspace/skills/aero/references/memory-patterns.md +3 -3
- package/assets/agent-workspace/skills/aero/references/orchestration.md +6 -6
- package/assets/agent-workspace/skills/aero/references/regression-playbook.md +7 -7
- package/assets/agent-workspace/skills/aero/references/reporting.md +8 -8
- package/assets/agent-workspace/skills/aero/soul.md +1 -1
- package/assets/agent-workspace/skills/canonry-setup/SKILL.md +5 -5
- package/assets/agent-workspace/skills/canonry-setup/references/aeo-analysis.md +15 -15
- package/assets/agent-workspace/skills/canonry-setup/references/canonry-cli.md +8 -8
- package/assets/assets/index-D7T5wSBj.css +1 -0
- package/assets/assets/index-Dtgn4FDp.js +302 -0
- package/assets/index.html +2 -2
- package/dist/{chunk-CG4HEQAK.js → chunk-BQN6BBHI.js} +707 -456
- package/dist/{chunk-GLPZ5NVP.js → chunk-KCETXLDF.js} +106 -16
- package/dist/{chunk-W463NVVC.js → chunk-NCWCPBOT.js} +111 -49
- package/dist/{chunk-RDX6GBWM.js → chunk-O5JZQUPX.js} +91 -45
- package/dist/cli.js +472 -193
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-WZUM3AX6.js → intelligence-service-EITZP4KG.js} +2 -2
- package/dist/mcp.js +4 -4
- package/package.json +9 -9
- package/assets/assets/index-1wUFsyjk.js +0 -302
- package/assets/assets/index-f8lqs-ju.css +0 -1
|
@@ -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-
|
|
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,
|
|
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-
|
|
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/
|
|
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-
|
|
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-
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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-
|
|
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
|
|
72
|
+
var queries = sqliteTable("queries", {
|
|
73
73
|
id: text("id").primaryKey(),
|
|
74
74
|
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
75
|
-
|
|
75
|
+
query: text("query").notNull(),
|
|
76
76
|
createdAt: text("created_at").notNull()
|
|
77
77
|
}, (table) => [
|
|
78
|
-
index("
|
|
79
|
-
uniqueIndex("
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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("
|
|
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
|
|
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
|
-
|
|
586
|
+
query TEXT NOT NULL,
|
|
587
587
|
created_at TEXT NOT NULL,
|
|
588
|
-
UNIQUE(project_id,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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.
|
|
1587
|
+
previousCited.add(`${snap.query}:${snap.provider}`);
|
|
1526
1588
|
}
|
|
1527
1589
|
}
|
|
1528
1590
|
for (const snap of currentRun.snapshots) {
|
|
1529
|
-
const key = `${snap.
|
|
1591
|
+
const key = `${snap.query}:${snap.provider}`;
|
|
1530
1592
|
if (snap.cited && !previousCited.has(key)) {
|
|
1531
1593
|
gains.push({
|
|
1532
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
1619
|
-
|
|
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.
|
|
1636
|
-
|
|
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.
|
|
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.
|
|
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({
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
2380
|
+
const gscImpressionsByQuery = /* @__PURE__ */ new Map();
|
|
2319
2381
|
for (const row of gscRows) {
|
|
2320
2382
|
const key = row.query.toLowerCase();
|
|
2321
|
-
|
|
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({
|
|
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.
|
|
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 ?
|
|
2342
|
-
const recurrenceCount = haveHistory ? priorRegressionsByPair.get(`${insight.
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
2437
|
+
queries,
|
|
2376
2438
|
competitors,
|
|
2377
2439
|
runs,
|
|
2378
2440
|
querySnapshots,
|