@ainyc/canonry 4.1.3 → 4.2.2
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/assets/assets/{index-BbhhYPML.js → index-DoJfQkim.js} +122 -122
- package/assets/index.html +1 -1
- package/dist/{chunk-AXMSAMKN.js → chunk-7YSI4GFA.js} +758 -26
- package/dist/{chunk-JV6X6AFT.js → chunk-HJZY4EOE.js} +274 -232
- package/dist/{chunk-KCETXLDF.js → chunk-SR7TGHHG.js} +18 -6
- package/dist/{chunk-O5JZQUPX.js → chunk-T2I6AO7D.js} +10 -1
- package/dist/cli.js +87 -18
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-WPY4PDBU.js → intelligence-service-CQGAXKKN.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
|
@@ -4,8 +4,9 @@ import {
|
|
|
4
4
|
configExists,
|
|
5
5
|
loadConfig,
|
|
6
6
|
saveConfigPatch
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-SR7TGHHG.js";
|
|
8
8
|
import {
|
|
9
|
+
DEFAULT_RUN_HISTORY_LIMIT,
|
|
9
10
|
IntelligenceService,
|
|
10
11
|
MIN_TREND_POINTS,
|
|
11
12
|
agentMemory,
|
|
@@ -16,11 +17,22 @@ import {
|
|
|
16
17
|
backlinkSummaries,
|
|
17
18
|
bingCoverageSnapshots,
|
|
18
19
|
bingUrlInspections,
|
|
20
|
+
buildAiSourceOrigin,
|
|
19
21
|
buildBrandTokens,
|
|
22
|
+
buildCitationScorecard,
|
|
23
|
+
buildCompetitorLandscape,
|
|
24
|
+
buildCompetitorPressureScore,
|
|
20
25
|
buildContentGapRows,
|
|
21
26
|
buildContentSourceRows,
|
|
22
27
|
buildContentTargetRows,
|
|
28
|
+
buildGapQueryScore,
|
|
23
29
|
buildInventory,
|
|
30
|
+
buildMentionLandscape,
|
|
31
|
+
buildMovementSummary,
|
|
32
|
+
buildOverviewCompetitors,
|
|
33
|
+
buildProviderScores,
|
|
34
|
+
buildRunHistory,
|
|
35
|
+
buildVisibilityScore,
|
|
24
36
|
categorizeQueryByIntent,
|
|
25
37
|
ccReleaseSyncs,
|
|
26
38
|
competitors,
|
|
@@ -49,7 +61,7 @@ import {
|
|
|
49
61
|
runs,
|
|
50
62
|
schedules,
|
|
51
63
|
usageCounters
|
|
52
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-7YSI4GFA.js";
|
|
53
65
|
import {
|
|
54
66
|
AGENT_MEMORY_VALUE_MAX_BYTES,
|
|
55
67
|
AGENT_PROVIDER_IDS,
|
|
@@ -115,7 +127,7 @@ import {
|
|
|
115
127
|
visibilityStateFromAnswerMentioned,
|
|
116
128
|
windowCutoff,
|
|
117
129
|
wordpressEnvSchema
|
|
118
|
-
} from "./chunk-
|
|
130
|
+
} from "./chunk-T2I6AO7D.js";
|
|
119
131
|
|
|
120
132
|
// src/telemetry.ts
|
|
121
133
|
import crypto from "crypto";
|
|
@@ -3846,7 +3858,6 @@ function extractPath(url) {
|
|
|
3846
3858
|
var TOP_QUERIES_LIMIT = 20;
|
|
3847
3859
|
var TOP_LANDING_PAGES_LIMIT = 20;
|
|
3848
3860
|
var TOP_AI_REFERRAL_PAGES_LIMIT = 10;
|
|
3849
|
-
var TOP_SOURCE_DOMAINS_LIMIT = 20;
|
|
3850
3861
|
var TOP_CAMPAIGN_LIMIT = 10;
|
|
3851
3862
|
var INSIGHT_LOOKBACK_RUNS = 5;
|
|
3852
3863
|
function safeNum(value) {
|
|
@@ -3857,14 +3868,6 @@ function safeNum(value) {
|
|
|
3857
3868
|
}
|
|
3858
3869
|
return 0;
|
|
3859
3870
|
}
|
|
3860
|
-
function citedDomainBelongsToProject(citedDomain, projectDomains) {
|
|
3861
|
-
const candidate = normalizeProjectDomain(citedDomain);
|
|
3862
|
-
for (const domain of projectDomains) {
|
|
3863
|
-
const normalized = normalizeProjectDomain(domain);
|
|
3864
|
-
if (candidate === normalized || candidate.endsWith(`.${normalized}`)) return true;
|
|
3865
|
-
}
|
|
3866
|
-
return false;
|
|
3867
|
-
}
|
|
3868
3871
|
function categorizeQuery(query, projectDisplayName, canonicalDomain) {
|
|
3869
3872
|
return categorizeQueryByIntent(query, buildBrandTokens(canonicalDomain, projectDisplayName));
|
|
3870
3873
|
}
|
|
@@ -3891,186 +3894,6 @@ function loadQueryLookup(db, projectId) {
|
|
|
3891
3894
|
for (const row of rows) byId.set(row.id, row.query);
|
|
3892
3895
|
return { byId };
|
|
3893
3896
|
}
|
|
3894
|
-
function buildCitationScorecard(snapshots, queryLookup) {
|
|
3895
|
-
if (snapshots.length === 0) {
|
|
3896
|
-
return { queries: [], providers: [], matrix: [], providerRates: [] };
|
|
3897
|
-
}
|
|
3898
|
-
const querySet = /* @__PURE__ */ new Set();
|
|
3899
|
-
const providerSet = /* @__PURE__ */ new Set();
|
|
3900
|
-
for (const snap of snapshots) {
|
|
3901
|
-
const q = queryLookup.byId.get(snap.queryId);
|
|
3902
|
-
if (!q) continue;
|
|
3903
|
-
querySet.add(q);
|
|
3904
|
-
providerSet.add(snap.provider);
|
|
3905
|
-
}
|
|
3906
|
-
const queryList = [...querySet].sort();
|
|
3907
|
-
const providerList = [...providerSet].sort();
|
|
3908
|
-
const matrix = queryList.map(
|
|
3909
|
-
() => providerList.map(() => null)
|
|
3910
|
-
);
|
|
3911
|
-
const providerCounts = /* @__PURE__ */ new Map();
|
|
3912
|
-
for (const snap of snapshots) {
|
|
3913
|
-
const q = queryLookup.byId.get(snap.queryId);
|
|
3914
|
-
if (!q) continue;
|
|
3915
|
-
const qi = queryList.indexOf(q);
|
|
3916
|
-
const pi = providerList.indexOf(snap.provider);
|
|
3917
|
-
if (qi < 0 || pi < 0) continue;
|
|
3918
|
-
matrix[qi][pi] = {
|
|
3919
|
-
citationState: snap.citationState === CitationStates.cited ? CitationStates.cited : CitationStates["not-cited"],
|
|
3920
|
-
answerMentioned: snap.answerMentioned ?? null,
|
|
3921
|
-
model: snap.model
|
|
3922
|
-
};
|
|
3923
|
-
const counts = providerCounts.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
3924
|
-
counts.total++;
|
|
3925
|
-
if (snap.citationState === CitationStates.cited) counts.cited++;
|
|
3926
|
-
providerCounts.set(snap.provider, counts);
|
|
3927
|
-
}
|
|
3928
|
-
const providerRates = providerList.map((provider) => {
|
|
3929
|
-
const counts = providerCounts.get(provider) ?? { cited: 0, total: 0 };
|
|
3930
|
-
const citationRate = counts.total > 0 ? Math.round(counts.cited / counts.total * 100) : 0;
|
|
3931
|
-
return {
|
|
3932
|
-
provider,
|
|
3933
|
-
citedCount: counts.cited,
|
|
3934
|
-
totalCount: counts.total,
|
|
3935
|
-
citationRate
|
|
3936
|
-
};
|
|
3937
|
-
});
|
|
3938
|
-
return { queries: queryList, providers: providerList, matrix, providerRates };
|
|
3939
|
-
}
|
|
3940
|
-
function buildCompetitorLandscape(snapshots, competitorDomains, projectDomains, queryLookup) {
|
|
3941
|
-
let projectCitationCount = 0;
|
|
3942
|
-
const competitorMap = /* @__PURE__ */ new Map();
|
|
3943
|
-
for (const c of competitorDomains) {
|
|
3944
|
-
competitorMap.set(c, { count: 0, queries: /* @__PURE__ */ new Set(), pages: /* @__PURE__ */ new Map() });
|
|
3945
|
-
}
|
|
3946
|
-
for (const snap of snapshots) {
|
|
3947
|
-
const q = queryLookup.byId.get(snap.queryId);
|
|
3948
|
-
const allDomains = [...snap.citedDomains, ...snap.competitorOverlap];
|
|
3949
|
-
if (allDomains.some((d) => citedDomainBelongsToProject(d, projectDomains))) {
|
|
3950
|
-
projectCitationCount++;
|
|
3951
|
-
}
|
|
3952
|
-
for (const competitor of competitorDomains) {
|
|
3953
|
-
if (allDomains.some((d) => citedDomainBelongsToProject(d, [competitor]))) {
|
|
3954
|
-
const entry = competitorMap.get(competitor);
|
|
3955
|
-
entry.count++;
|
|
3956
|
-
if (q) entry.queries.add(q);
|
|
3957
|
-
}
|
|
3958
|
-
const competitorNorm = normalizeDomain(competitor);
|
|
3959
|
-
for (const gs of snap.groundingSources) {
|
|
3960
|
-
const host = normalizeDomain(extractHostFromUri(gs.uri));
|
|
3961
|
-
if (!host) continue;
|
|
3962
|
-
if (host === competitorNorm || host.endsWith(`.${competitorNorm}`)) {
|
|
3963
|
-
const entry = competitorMap.get(competitor);
|
|
3964
|
-
const pageQueries = entry.pages.get(gs.uri) ?? /* @__PURE__ */ new Set();
|
|
3965
|
-
if (q) pageQueries.add(q);
|
|
3966
|
-
entry.pages.set(gs.uri, pageQueries);
|
|
3967
|
-
}
|
|
3968
|
-
}
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
|
-
const totalCitedSlots = projectCitationCount + [...competitorMap.values()].reduce((sum, v) => sum + v.count, 0);
|
|
3972
|
-
const competitorRows = [...competitorMap.entries()].map(([domain, data]) => {
|
|
3973
|
-
const total = snapshots.length;
|
|
3974
|
-
const ratio = total > 0 ? data.count / total : 0;
|
|
3975
|
-
let pressureLabel = "None";
|
|
3976
|
-
if (data.count > 0) {
|
|
3977
|
-
if (ratio >= 0.5) pressureLabel = "High";
|
|
3978
|
-
else if (ratio >= 0.2) pressureLabel = "Moderate";
|
|
3979
|
-
else pressureLabel = "Low";
|
|
3980
|
-
}
|
|
3981
|
-
const sharePct = totalCitedSlots > 0 ? Math.round(data.count / totalCitedSlots * 100) : 0;
|
|
3982
|
-
const theirCitedPages = [...data.pages.entries()].map(([url, qs]) => ({ url, citedFor: [...qs].sort() })).sort((a, b) => b.citedFor.length - a.citedFor.length);
|
|
3983
|
-
return {
|
|
3984
|
-
domain,
|
|
3985
|
-
citationCount: data.count,
|
|
3986
|
-
totalCount: total,
|
|
3987
|
-
pressureLabel,
|
|
3988
|
-
citedQueries: [...data.queries].sort(),
|
|
3989
|
-
sharePct,
|
|
3990
|
-
theirCitedPages
|
|
3991
|
-
};
|
|
3992
|
-
});
|
|
3993
|
-
competitorRows.sort((a, b) => b.citationCount - a.citationCount);
|
|
3994
|
-
return { projectCitationCount, competitors: competitorRows };
|
|
3995
|
-
}
|
|
3996
|
-
function buildMentionLandscape(snapshots, competitorDomains, projectDisplayName, projectDomains, queryLookup) {
|
|
3997
|
-
let projectMentionCount = 0;
|
|
3998
|
-
let totalAnswerSnapshots = 0;
|
|
3999
|
-
const competitorMap = /* @__PURE__ */ new Map();
|
|
4000
|
-
for (const c of competitorDomains) {
|
|
4001
|
-
competitorMap.set(c, { count: 0, queries: /* @__PURE__ */ new Set() });
|
|
4002
|
-
}
|
|
4003
|
-
for (const snap of snapshots) {
|
|
4004
|
-
const text = snap.answerText;
|
|
4005
|
-
if (!text) continue;
|
|
4006
|
-
totalAnswerSnapshots++;
|
|
4007
|
-
const q = queryLookup.byId.get(snap.queryId);
|
|
4008
|
-
const projectMentioned = snap.answerMentioned ?? determineAnswerMentioned(
|
|
4009
|
-
text,
|
|
4010
|
-
projectDisplayName,
|
|
4011
|
-
projectDomains
|
|
4012
|
-
);
|
|
4013
|
-
if (projectMentioned) projectMentionCount++;
|
|
4014
|
-
for (const competitor of competitorDomains) {
|
|
4015
|
-
const brand = brandLabelFromDomain(competitor);
|
|
4016
|
-
const mentioned = determineAnswerMentioned(text, brand, [competitor]);
|
|
4017
|
-
if (mentioned) {
|
|
4018
|
-
const entry = competitorMap.get(competitor);
|
|
4019
|
-
entry.count++;
|
|
4020
|
-
if (q) entry.queries.add(q);
|
|
4021
|
-
}
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
const totalMentionedSlots = projectMentionCount + [...competitorMap.values()].reduce((sum, v) => sum + v.count, 0);
|
|
4025
|
-
const competitorRows = [...competitorMap.entries()].map(([domain, data]) => {
|
|
4026
|
-
const ratio = totalAnswerSnapshots > 0 ? data.count / totalAnswerSnapshots : 0;
|
|
4027
|
-
let pressureLabel = "None";
|
|
4028
|
-
if (data.count > 0) {
|
|
4029
|
-
if (ratio >= 0.5) pressureLabel = "High";
|
|
4030
|
-
else if (ratio >= 0.2) pressureLabel = "Moderate";
|
|
4031
|
-
else pressureLabel = "Low";
|
|
4032
|
-
}
|
|
4033
|
-
const sharePct = totalMentionedSlots > 0 ? Math.round(data.count / totalMentionedSlots * 100) : 0;
|
|
4034
|
-
return {
|
|
4035
|
-
domain,
|
|
4036
|
-
mentionCount: data.count,
|
|
4037
|
-
totalCount: totalAnswerSnapshots,
|
|
4038
|
-
pressureLabel,
|
|
4039
|
-
mentionedQueries: [...data.queries].sort(),
|
|
4040
|
-
sharePct
|
|
4041
|
-
};
|
|
4042
|
-
});
|
|
4043
|
-
competitorRows.sort((a, b) => b.mentionCount - a.mentionCount);
|
|
4044
|
-
return { projectMentionCount, totalAnswerSnapshots, competitors: competitorRows };
|
|
4045
|
-
}
|
|
4046
|
-
function buildAiSourceOrigin(snapshots, projectDomains, competitorDomains) {
|
|
4047
|
-
const categoryCounts = /* @__PURE__ */ new Map();
|
|
4048
|
-
const domainCounts = /* @__PURE__ */ new Map();
|
|
4049
|
-
let totalCitations = 0;
|
|
4050
|
-
for (const snap of snapshots) {
|
|
4051
|
-
for (const raw of snap.citedDomains) {
|
|
4052
|
-
if (citedDomainBelongsToProject(raw, projectDomains)) continue;
|
|
4053
|
-
const { category, label, domain } = categorizeSource(raw);
|
|
4054
|
-
const cat = categoryCounts.get(category) ?? { label, count: 0 };
|
|
4055
|
-
cat.count++;
|
|
4056
|
-
categoryCounts.set(category, cat);
|
|
4057
|
-
domainCounts.set(domain, (domainCounts.get(domain) ?? 0) + 1);
|
|
4058
|
-
totalCitations++;
|
|
4059
|
-
}
|
|
4060
|
-
}
|
|
4061
|
-
const categories = [...categoryCounts.entries()].map(([category, { label, count }]) => ({
|
|
4062
|
-
category,
|
|
4063
|
-
label,
|
|
4064
|
-
count,
|
|
4065
|
-
sharePct: totalCitations > 0 ? Math.round(count / totalCitations * 100) : 0
|
|
4066
|
-
})).sort((a, b) => b.count - a.count);
|
|
4067
|
-
const topDomains = [...domainCounts.entries()].map(([domain, count]) => ({
|
|
4068
|
-
domain,
|
|
4069
|
-
count,
|
|
4070
|
-
isCompetitor: citedDomainBelongsToProject(domain, competitorDomains)
|
|
4071
|
-
})).sort((a, b) => b.count - a.count).slice(0, TOP_SOURCE_DOMAINS_LIMIT);
|
|
4072
|
-
return { categories, topDomains };
|
|
4073
|
-
}
|
|
4074
3897
|
function buildGscSection(db, projectId, projectDisplayName, canonicalDomain, trackedQueries) {
|
|
4075
3898
|
const rows = db.select().from(gscSearchData).where(eq13(gscSearchData.projectId, projectId)).all();
|
|
4076
3899
|
if (rows.length === 0) return null;
|
|
@@ -4785,24 +4608,68 @@ function normalizeDomain2(domain) {
|
|
|
4785
4608
|
}
|
|
4786
4609
|
|
|
4787
4610
|
// ../api-routes/src/composites.ts
|
|
4788
|
-
import { eq as eq15, and as and5, desc as desc7, sql as sql3, like, or as or3 } from "drizzle-orm";
|
|
4611
|
+
import { eq as eq15, and as and5, desc as desc7, sql as sql3, like, or as or3, inArray as inArray6 } from "drizzle-orm";
|
|
4789
4612
|
var TOP_INSIGHT_LIMIT = 5;
|
|
4790
4613
|
var SEARCH_HIT_HARD_LIMIT = 50;
|
|
4791
4614
|
var SEARCH_SNIPPET_RADIUS = 80;
|
|
4792
4615
|
async function compositeRoutes(app) {
|
|
4793
4616
|
app.get("/projects/:name/overview", async (request, reply) => {
|
|
4794
4617
|
const project = resolveProject(app.db, request.params.name);
|
|
4795
|
-
const
|
|
4796
|
-
const
|
|
4797
|
-
const
|
|
4798
|
-
const
|
|
4618
|
+
const filterLocation = (request.query.location ?? "").trim() || null;
|
|
4619
|
+
const sinceIso = parseSinceFilter(request.query.since);
|
|
4620
|
+
const allRunsRaw = app.db.select().from(runs).where(eq15(runs.projectId, project.id)).orderBy(desc7(runs.createdAt)).all();
|
|
4621
|
+
const allRuns = allRunsRaw.filter((r) => runMatchesFilters(r, filterLocation, sinceIso));
|
|
4622
|
+
const totalRuns = allRuns.length;
|
|
4623
|
+
const visibilityRuns = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]);
|
|
4624
|
+
const completedVisRuns = visibilityRuns.filter(
|
|
4625
|
+
(r) => r.status === RunStatuses.completed || r.status === RunStatuses.partial
|
|
4626
|
+
);
|
|
4627
|
+
const latestVisibilityRun = completedVisRuns[0] ?? null;
|
|
4628
|
+
const previousVisibilityRun = completedVisRuns[1] ?? null;
|
|
4629
|
+
const latestRunRow = allRuns[0] ?? null;
|
|
4799
4630
|
const latestRun = latestRunRow ? { totalRuns, run: summarizeRun(latestRunRow) } : { totalRuns: 0, run: null };
|
|
4800
4631
|
const healthRow = app.db.select().from(healthSnapshots).where(eq15(healthSnapshots.projectId, project.id)).orderBy(desc7(healthSnapshots.createdAt)).limit(1).get();
|
|
4801
4632
|
const health = healthRow ? mapHealthRow2(healthRow) : null;
|
|
4802
4633
|
const insightRows = app.db.select().from(insights).where(eq15(insights.projectId, project.id)).orderBy(desc7(insights.createdAt)).all();
|
|
4803
4634
|
const topInsights = insightRows.filter((row) => !row.dismissed).slice(0, TOP_INSIGHT_LIMIT).map(mapInsightRow2);
|
|
4804
|
-
const
|
|
4805
|
-
const
|
|
4635
|
+
const sparklineRunIds = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => r.id);
|
|
4636
|
+
const snapshotRunIds = new Set(sparklineRunIds);
|
|
4637
|
+
if (latestVisibilityRun) snapshotRunIds.add(latestVisibilityRun.id);
|
|
4638
|
+
if (previousVisibilityRun) snapshotRunIds.add(previousVisibilityRun.id);
|
|
4639
|
+
const snapshotsByRun = loadSnapshotsByRunIds(app, [...snapshotRunIds]);
|
|
4640
|
+
const latestSnapshots = latestVisibilityRun ? snapshotsByRun.get(latestVisibilityRun.id) ?? [] : [];
|
|
4641
|
+
const previousSnapshots = previousVisibilityRun ? snapshotsByRun.get(previousVisibilityRun.id) ?? [] : [];
|
|
4642
|
+
const { queryCounts, providers } = summarizeFromSnapshots(latestSnapshots);
|
|
4643
|
+
const transitions = summarizeTransitionsFromSnapshots(
|
|
4644
|
+
latestSnapshots,
|
|
4645
|
+
previousSnapshots,
|
|
4646
|
+
previousVisibilityRun?.createdAt ?? null
|
|
4647
|
+
);
|
|
4648
|
+
const competitorRows = app.db.select().from(competitors).where(eq15(competitors.projectId, project.id)).all();
|
|
4649
|
+
const projectQueries = app.db.select({ id: queries.id, query: queries.query }).from(queries).where(eq15(queries.projectId, project.id)).all();
|
|
4650
|
+
const queryLookup = { byId: new Map(projectQueries.map((q) => [q.id, q.query])) };
|
|
4651
|
+
const configuredApiProviders = parseJsonColumn(project.providers, []).filter((p) => !p.startsWith("cdp:"));
|
|
4652
|
+
const scores = {
|
|
4653
|
+
visibility: buildVisibilityScore(latestSnapshots, { configuredApiProviders }),
|
|
4654
|
+
gapQueries: buildGapQueryScore(latestSnapshots),
|
|
4655
|
+
indexCoverage: buildIndexCoverageScore(app, project.id),
|
|
4656
|
+
competitorPressure: buildCompetitorPressureScore(
|
|
4657
|
+
latestSnapshots,
|
|
4658
|
+
competitorRows.map((c) => c.domain),
|
|
4659
|
+
competitorRows.length
|
|
4660
|
+
),
|
|
4661
|
+
runStatus: buildRunStatusScore(allRuns)
|
|
4662
|
+
};
|
|
4663
|
+
const movementSummary = buildMovementSummary(latestSnapshots, previousSnapshots);
|
|
4664
|
+
const providerScores = buildProviderScores(latestSnapshots);
|
|
4665
|
+
const overviewCompetitors = buildOverviewCompetitors(
|
|
4666
|
+
latestSnapshots,
|
|
4667
|
+
competitorRows.map((c) => ({ id: c.id, domain: c.domain })),
|
|
4668
|
+
queryLookup
|
|
4669
|
+
);
|
|
4670
|
+
const attentionItems = buildAttentionItems(insightRows, allRuns);
|
|
4671
|
+
const sparklineRuns = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => ({ id: r.id, createdAt: r.createdAt, status: r.status }));
|
|
4672
|
+
const runHistory = buildRunHistory(sparklineRuns, snapshotsByRun);
|
|
4806
4673
|
const result = {
|
|
4807
4674
|
project: formatProject2(project),
|
|
4808
4675
|
latestRun,
|
|
@@ -4810,7 +4677,15 @@ async function compositeRoutes(app) {
|
|
|
4810
4677
|
topInsights,
|
|
4811
4678
|
queryCounts,
|
|
4812
4679
|
providers,
|
|
4813
|
-
transitions
|
|
4680
|
+
transitions,
|
|
4681
|
+
scores,
|
|
4682
|
+
movementSummary,
|
|
4683
|
+
competitors: overviewCompetitors,
|
|
4684
|
+
providerScores,
|
|
4685
|
+
attentionItems,
|
|
4686
|
+
runHistory,
|
|
4687
|
+
dateRangeLabel: "All time",
|
|
4688
|
+
contextLabel: `${project.country} / ${project.language.toUpperCase()}`
|
|
4814
4689
|
};
|
|
4815
4690
|
return reply.send(result);
|
|
4816
4691
|
});
|
|
@@ -4876,6 +4751,21 @@ async function compositeRoutes(app) {
|
|
|
4876
4751
|
return reply.send(response);
|
|
4877
4752
|
});
|
|
4878
4753
|
}
|
|
4754
|
+
function parseSinceFilter(raw) {
|
|
4755
|
+
if (!raw) return null;
|
|
4756
|
+
const trimmed = raw.trim();
|
|
4757
|
+
if (!trimmed) return null;
|
|
4758
|
+
const parsed = Date.parse(trimmed);
|
|
4759
|
+
if (Number.isNaN(parsed)) {
|
|
4760
|
+
throw validationError('"since" must be an ISO 8601 datetime');
|
|
4761
|
+
}
|
|
4762
|
+
return new Date(parsed).toISOString();
|
|
4763
|
+
}
|
|
4764
|
+
function runMatchesFilters(run, location, sinceIso) {
|
|
4765
|
+
if (location !== null && (run.location ?? "") !== location) return false;
|
|
4766
|
+
if (sinceIso !== null && run.createdAt < sinceIso) return false;
|
|
4767
|
+
return true;
|
|
4768
|
+
}
|
|
4879
4769
|
function clampSearchLimit(raw) {
|
|
4880
4770
|
if (!raw) return 25;
|
|
4881
4771
|
const parsed = Number.parseInt(raw, 10);
|
|
@@ -4901,29 +4791,49 @@ function summarizeRun(run) {
|
|
|
4901
4791
|
createdAt: run.createdAt
|
|
4902
4792
|
};
|
|
4903
4793
|
}
|
|
4904
|
-
function
|
|
4794
|
+
function loadSnapshotsByRunIds(app, runIds) {
|
|
4795
|
+
const result = /* @__PURE__ */ new Map();
|
|
4796
|
+
if (runIds.length === 0) return result;
|
|
4797
|
+
const rows = app.db.select({
|
|
4798
|
+
runId: querySnapshots.runId,
|
|
4799
|
+
queryId: querySnapshots.queryId,
|
|
4800
|
+
provider: querySnapshots.provider,
|
|
4801
|
+
model: querySnapshots.model,
|
|
4802
|
+
citationState: querySnapshots.citationState,
|
|
4803
|
+
competitorOverlap: querySnapshots.competitorOverlap,
|
|
4804
|
+
citedDomains: querySnapshots.citedDomains
|
|
4805
|
+
}).from(querySnapshots).where(inArray6(querySnapshots.runId, [...runIds])).all();
|
|
4806
|
+
for (const row of rows) {
|
|
4807
|
+
const list = result.get(row.runId) ?? [];
|
|
4808
|
+
list.push({
|
|
4809
|
+
queryId: row.queryId,
|
|
4810
|
+
provider: row.provider,
|
|
4811
|
+
model: row.model,
|
|
4812
|
+
citationState: row.citationState,
|
|
4813
|
+
competitorOverlap: parseJsonColumn(row.competitorOverlap, []),
|
|
4814
|
+
citedDomains: parseJsonColumn(row.citedDomains, [])
|
|
4815
|
+
});
|
|
4816
|
+
result.set(row.runId, list);
|
|
4817
|
+
}
|
|
4818
|
+
return result;
|
|
4819
|
+
}
|
|
4820
|
+
function summarizeFromSnapshots(snapshots) {
|
|
4905
4821
|
const empty = {
|
|
4906
4822
|
queryCounts: { totalQueries: 0, citedQueries: 0, notCitedQueries: 0, citedRate: 0 },
|
|
4907
4823
|
providers: []
|
|
4908
4824
|
};
|
|
4909
|
-
if (
|
|
4910
|
-
const rows = app.db.select({
|
|
4911
|
-
queryId: querySnapshots.queryId,
|
|
4912
|
-
provider: querySnapshots.provider,
|
|
4913
|
-
citationState: querySnapshots.citationState
|
|
4914
|
-
}).from(querySnapshots).where(eq15(querySnapshots.runId, run.id)).all();
|
|
4915
|
-
if (rows.length === 0) return empty;
|
|
4825
|
+
if (snapshots.length === 0) return empty;
|
|
4916
4826
|
const perQuery = /* @__PURE__ */ new Map();
|
|
4917
4827
|
const perProvider = /* @__PURE__ */ new Map();
|
|
4918
|
-
for (const
|
|
4919
|
-
const cited =
|
|
4920
|
-
if (!perQuery.has(
|
|
4921
|
-
perQuery.set(
|
|
4828
|
+
for (const snap of snapshots) {
|
|
4829
|
+
const cited = snap.citationState === CitationStates.cited;
|
|
4830
|
+
if (!perQuery.has(snap.queryId) || cited) {
|
|
4831
|
+
perQuery.set(snap.queryId, cited);
|
|
4922
4832
|
}
|
|
4923
|
-
const bucket = perProvider.get(
|
|
4833
|
+
const bucket = perProvider.get(snap.provider) ?? { cited: 0, total: 0 };
|
|
4924
4834
|
bucket.total += 1;
|
|
4925
4835
|
if (cited) bucket.cited += 1;
|
|
4926
|
-
perProvider.set(
|
|
4836
|
+
perProvider.set(snap.provider, bucket);
|
|
4927
4837
|
}
|
|
4928
4838
|
const totalQueries = perQuery.size;
|
|
4929
4839
|
let citedQueries = 0;
|
|
@@ -4943,23 +4853,20 @@ function summarizeLatestRun(app, run) {
|
|
|
4943
4853
|
providers
|
|
4944
4854
|
};
|
|
4945
4855
|
}
|
|
4946
|
-
function
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
for (const row of rows) {
|
|
4956
|
-
const cited = row.citationState === CitationStates.cited;
|
|
4957
|
-
if (!map.has(row.queryId) || cited) map.set(row.queryId, cited);
|
|
4856
|
+
function summarizeTransitionsFromSnapshots(latest, previous, since) {
|
|
4857
|
+
if (!since || previous.length === 0) {
|
|
4858
|
+
return { since: null, gained: 0, lost: 0, emerging: 0 };
|
|
4859
|
+
}
|
|
4860
|
+
const buildMap = (snaps) => {
|
|
4861
|
+
const m = /* @__PURE__ */ new Map();
|
|
4862
|
+
for (const s of snaps) {
|
|
4863
|
+
const cited = s.citationState === CitationStates.cited;
|
|
4864
|
+
if (!m.has(s.queryId) || cited) m.set(s.queryId, cited);
|
|
4958
4865
|
}
|
|
4959
|
-
return
|
|
4866
|
+
return m;
|
|
4960
4867
|
};
|
|
4961
|
-
const latestMap =
|
|
4962
|
-
const previousMap =
|
|
4868
|
+
const latestMap = buildMap(latest);
|
|
4869
|
+
const previousMap = buildMap(previous);
|
|
4963
4870
|
let gained = 0;
|
|
4964
4871
|
let lost = 0;
|
|
4965
4872
|
let emerging = 0;
|
|
@@ -4972,7 +4879,142 @@ function summarizeTransitions(app, latest, previous) {
|
|
|
4972
4879
|
if (latestCited && !previousCited) gained += 1;
|
|
4973
4880
|
else if (!latestCited && previousCited) lost += 1;
|
|
4974
4881
|
}
|
|
4975
|
-
return { since
|
|
4882
|
+
return { since, gained, lost, emerging };
|
|
4883
|
+
}
|
|
4884
|
+
function buildIndexCoverageScore(app, projectId) {
|
|
4885
|
+
const tooltip = "Percentage of inspected URLs currently indexed. Google Search Console is preferred when available, otherwise Bing Webmaster Tools is used.";
|
|
4886
|
+
const empty = {
|
|
4887
|
+
label: "Index Coverage",
|
|
4888
|
+
value: "No data",
|
|
4889
|
+
delta: "Connect GSC or Bing",
|
|
4890
|
+
tone: "neutral",
|
|
4891
|
+
description: "Connect Google Search Console or Bing Webmaster Tools and inspect your sitemap to populate coverage.",
|
|
4892
|
+
tooltip,
|
|
4893
|
+
trend: []
|
|
4894
|
+
};
|
|
4895
|
+
const gscRow = app.db.select().from(gscCoverageSnapshots).where(eq15(gscCoverageSnapshots.projectId, projectId)).orderBy(desc7(gscCoverageSnapshots.date)).limit(1).get();
|
|
4896
|
+
const bingRow = app.db.select().from(bingCoverageSnapshots).where(eq15(bingCoverageSnapshots.projectId, projectId)).orderBy(desc7(bingCoverageSnapshots.date)).limit(1).get();
|
|
4897
|
+
const chosen = pickIndexCoverageRow(gscRow, bingRow);
|
|
4898
|
+
if (!chosen) return empty;
|
|
4899
|
+
const total = chosen.indexed + chosen.notIndexed;
|
|
4900
|
+
if (total === 0) return empty;
|
|
4901
|
+
const deindexed = chosen.provider === "Google" ? countGoogleDeindexedUrls(app, projectId) : 0;
|
|
4902
|
+
const percentage = chosen.indexed / total * 100;
|
|
4903
|
+
const tone = deindexed > 0 ? "negative" : percentage >= 90 ? "positive" : percentage >= 70 ? "caution" : "negative";
|
|
4904
|
+
const notIndexedLabel = chosen.notIndexed === 1 ? "URL is" : "URLs are";
|
|
4905
|
+
const deindexedLabel = deindexed === 1 ? "URL" : "URLs";
|
|
4906
|
+
return {
|
|
4907
|
+
label: "Index Coverage",
|
|
4908
|
+
value: `${Math.round(percentage)}`,
|
|
4909
|
+
delta: `${chosen.provider} \xB7 ${chosen.indexed} of ${total} indexed`,
|
|
4910
|
+
tone,
|
|
4911
|
+
description: deindexed > 0 ? `${deindexed} deindexed ${deindexedLabel} detected in the latest Google Search Console inspection.` : `${chosen.notIndexed} ${notIndexedLabel} not indexed in ${chosen.provider === "Google" ? "Google Search Console" : "Bing Webmaster Tools"}.`,
|
|
4912
|
+
tooltip,
|
|
4913
|
+
trend: [],
|
|
4914
|
+
progress: Math.round(percentage)
|
|
4915
|
+
};
|
|
4916
|
+
}
|
|
4917
|
+
function countGoogleDeindexedUrls(app, projectId) {
|
|
4918
|
+
const rows = app.db.select({
|
|
4919
|
+
url: gscUrlInspections.url,
|
|
4920
|
+
indexingState: gscUrlInspections.indexingState,
|
|
4921
|
+
inspectedAt: gscUrlInspections.inspectedAt
|
|
4922
|
+
}).from(gscUrlInspections).where(eq15(gscUrlInspections.projectId, projectId)).orderBy(desc7(gscUrlInspections.inspectedAt)).all();
|
|
4923
|
+
if (rows.length === 0) return 0;
|
|
4924
|
+
const canonicalUrl = (url) => url.replace(/^http:\/\//, "https://");
|
|
4925
|
+
const historyByUrl = /* @__PURE__ */ new Map();
|
|
4926
|
+
for (const row of rows) {
|
|
4927
|
+
const key = canonicalUrl(row.url);
|
|
4928
|
+
const list = historyByUrl.get(key);
|
|
4929
|
+
if (list) list.push(row);
|
|
4930
|
+
else historyByUrl.set(key, [row]);
|
|
4931
|
+
}
|
|
4932
|
+
let deindexed = 0;
|
|
4933
|
+
for (const history of historyByUrl.values()) {
|
|
4934
|
+
if (history.length < 2) continue;
|
|
4935
|
+
const latest = history[0];
|
|
4936
|
+
const previous = history[1];
|
|
4937
|
+
if (previous.indexingState === "INDEXING_ALLOWED" && latest.indexingState !== "INDEXING_ALLOWED") {
|
|
4938
|
+
deindexed++;
|
|
4939
|
+
}
|
|
4940
|
+
}
|
|
4941
|
+
return deindexed;
|
|
4942
|
+
}
|
|
4943
|
+
function pickIndexCoverageRow(gsc, bing) {
|
|
4944
|
+
if (gsc && gsc.indexed + gsc.notIndexed > 0) {
|
|
4945
|
+
return { provider: "Google", indexed: gsc.indexed, notIndexed: gsc.notIndexed };
|
|
4946
|
+
}
|
|
4947
|
+
if (bing && bing.indexed + bing.notIndexed > 0) {
|
|
4948
|
+
return { provider: "Bing", indexed: bing.indexed, notIndexed: bing.notIndexed };
|
|
4949
|
+
}
|
|
4950
|
+
if (gsc) return { provider: "Google", indexed: gsc.indexed, notIndexed: gsc.notIndexed };
|
|
4951
|
+
if (bing) return { provider: "Bing", indexed: bing.indexed, notIndexed: bing.notIndexed };
|
|
4952
|
+
return null;
|
|
4953
|
+
}
|
|
4954
|
+
function buildRunStatusScore(allRuns) {
|
|
4955
|
+
const tooltip = "Current execution state of visibility sweeps. Shows the status of the most recent run and total run count.";
|
|
4956
|
+
if (allRuns.length === 0) {
|
|
4957
|
+
return {
|
|
4958
|
+
label: "Run Status",
|
|
4959
|
+
value: "None",
|
|
4960
|
+
delta: "No runs yet",
|
|
4961
|
+
tone: "neutral",
|
|
4962
|
+
description: "Trigger a visibility sweep to start tracking.",
|
|
4963
|
+
tooltip,
|
|
4964
|
+
trend: []
|
|
4965
|
+
};
|
|
4966
|
+
}
|
|
4967
|
+
const latestVisibility = allRuns.find((r) => r.kind === RunKinds["answer-visibility"]);
|
|
4968
|
+
const latest = latestVisibility ?? allRuns[0];
|
|
4969
|
+
const value = latest.status === RunStatuses.completed ? "Healthy" : latest.status === RunStatuses.running ? "Running" : latest.status === RunStatuses.queued ? "Queued" : latest.status === RunStatuses.partial ? "Partial" : "Failed";
|
|
4970
|
+
const tone = latest.status === RunStatuses.completed ? "positive" : latest.status === RunStatuses.failed ? "negative" : latest.status === RunStatuses.partial ? "caution" : "neutral";
|
|
4971
|
+
const visibilityRunCount = allRuns.filter((r) => r.kind === RunKinds["answer-visibility"]).length;
|
|
4972
|
+
const syncRunCount = allRuns.length - visibilityRunCount;
|
|
4973
|
+
const delta = syncRunCount > 0 ? `${visibilityRunCount} visibility \xB7 ${syncRunCount} sync` : `${visibilityRunCount} visibility run${visibilityRunCount === 1 ? "" : "s"}`;
|
|
4974
|
+
return {
|
|
4975
|
+
label: "Run Status",
|
|
4976
|
+
value,
|
|
4977
|
+
delta,
|
|
4978
|
+
tone,
|
|
4979
|
+
description: `Latest run ${value.toLowerCase()}. ${allRuns.length} total run${allRuns.length === 1 ? "" : "s"}.`,
|
|
4980
|
+
tooltip,
|
|
4981
|
+
trend: []
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
var ATTENTION_INSIGHT_LIMIT = 5;
|
|
4985
|
+
function buildAttentionItems(insightRows, allRuns) {
|
|
4986
|
+
const items = [];
|
|
4987
|
+
for (const row of insightRows) {
|
|
4988
|
+
if (row.dismissed) continue;
|
|
4989
|
+
if (row.severity !== "critical" && row.severity !== "high") continue;
|
|
4990
|
+
if (items.length >= ATTENTION_INSIGHT_LIMIT) break;
|
|
4991
|
+
items.push({
|
|
4992
|
+
id: `insight_${row.id}`,
|
|
4993
|
+
tone: row.severity === "critical" ? "negative" : "caution",
|
|
4994
|
+
title: row.title,
|
|
4995
|
+
detail: row.query ? `On query: ${row.query}` : "",
|
|
4996
|
+
actionLabel: row.severity === "critical" ? "Critical" : "High",
|
|
4997
|
+
href: `#insight-${row.id}`
|
|
4998
|
+
});
|
|
4999
|
+
}
|
|
5000
|
+
const sortedRuns = [...allRuns].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
5001
|
+
const latestVisRun = sortedRuns.find((r) => r.kind === RunKinds["answer-visibility"]);
|
|
5002
|
+
const latestSyncRun = sortedRuns.find((r) => r.kind !== RunKinds["answer-visibility"]);
|
|
5003
|
+
if (latestVisRun && latestSyncRun) {
|
|
5004
|
+
const visibilityAge = new Date(latestSyncRun.createdAt).getTime() - new Date(latestVisRun.createdAt).getTime();
|
|
5005
|
+
const ONE_DAY = 24 * 60 * 60 * 1e3;
|
|
5006
|
+
if (visibilityAge > ONE_DAY) {
|
|
5007
|
+
items.push({
|
|
5008
|
+
id: "stale_visibility",
|
|
5009
|
+
tone: "caution",
|
|
5010
|
+
title: "Stale visibility data",
|
|
5011
|
+
detail: `Last visibility sweep was ${latestVisRun.createdAt}; integration syncs have run since.`,
|
|
5012
|
+
actionLabel: "Stale",
|
|
5013
|
+
href: "#runs"
|
|
5014
|
+
});
|
|
5015
|
+
}
|
|
5016
|
+
}
|
|
5017
|
+
return items;
|
|
4976
5018
|
}
|
|
4977
5019
|
function mapInsightRow2(r) {
|
|
4978
5020
|
return {
|
|
@@ -17299,7 +17341,7 @@ import crypto19 from "crypto";
|
|
|
17299
17341
|
import fs7 from "fs";
|
|
17300
17342
|
import path9 from "path";
|
|
17301
17343
|
import os4 from "os";
|
|
17302
|
-
import { and as and12, eq as eq23, inArray as
|
|
17344
|
+
import { and as and12, eq as eq23, inArray as inArray7, sql as sql7 } from "drizzle-orm";
|
|
17303
17345
|
|
|
17304
17346
|
// src/citation-utils.ts
|
|
17305
17347
|
function domainMatches(domain, canonicalDomain) {
|
|
@@ -17556,7 +17598,7 @@ var JobRunner = class {
|
|
|
17556
17598
|
this.registry = registry;
|
|
17557
17599
|
}
|
|
17558
17600
|
recoverStaleRuns() {
|
|
17559
|
-
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(
|
|
17601
|
+
const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray7(runs.status, ["running", "queued"])).all();
|
|
17560
17602
|
if (stale.length === 0) return;
|
|
17561
17603
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
17562
17604
|
for (const run of stale) {
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
queryGenerateRequestSchema,
|
|
13
13
|
runTriggerRequestSchema,
|
|
14
14
|
scheduleUpsertRequestSchema
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-T2I6AO7D.js";
|
|
16
16
|
|
|
17
17
|
// src/config.ts
|
|
18
18
|
import fs from "fs";
|
|
@@ -864,8 +864,13 @@ var ApiClient = class {
|
|
|
864
864
|
async getHealth(project) {
|
|
865
865
|
return this.request("GET", `/projects/${encodeURIComponent(project)}/health/latest`);
|
|
866
866
|
}
|
|
867
|
-
async getProjectOverview(project) {
|
|
868
|
-
|
|
867
|
+
async getProjectOverview(project, opts) {
|
|
868
|
+
const params = new URLSearchParams();
|
|
869
|
+
if (opts?.location) params.set("location", opts.location);
|
|
870
|
+
if (opts?.since) params.set("since", opts.since);
|
|
871
|
+
const query = params.toString();
|
|
872
|
+
const path2 = `/projects/${encodeURIComponent(project)}/overview${query ? `?${query}` : ""}`;
|
|
873
|
+
return this.request("GET", path2);
|
|
869
874
|
}
|
|
870
875
|
async searchProject(project, opts) {
|
|
871
876
|
const params = new URLSearchParams({ q: opts.q });
|
|
@@ -1153,13 +1158,20 @@ var canonryMcpTools = [
|
|
|
1153
1158
|
defineTool({
|
|
1154
1159
|
name: "canonry_project_overview",
|
|
1155
1160
|
title: "Get project overview (composite)",
|
|
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,
|
|
1161
|
+
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, gained/lost/emerging vs the previous run, the five score gauges (visibility, gap queries, index coverage, competitor pressure, run status), per-(provider, model) scores, configured competitors with pressure labels, an attention queue of critical/high insights, and a recent-runs sparkline. Filterable by location and time window. Prefer this over fanning out to separate tools.',
|
|
1157
1162
|
access: "read",
|
|
1158
1163
|
tier: "core",
|
|
1159
|
-
inputSchema:
|
|
1164
|
+
inputSchema: z2.object({
|
|
1165
|
+
project: projectNameSchema,
|
|
1166
|
+
location: z2.string().optional().describe('Filter to runs from this location label (e.g. "Boston, MA, US"). Omit for all locations.'),
|
|
1167
|
+
since: z2.string().optional().describe("ISO 8601 datetime \u2014 only include runs at or after this time. Omit for full history.")
|
|
1168
|
+
}),
|
|
1160
1169
|
annotations: readAnnotations(),
|
|
1161
1170
|
openApiOperations: ["GET /api/v1/projects/{name}/overview"],
|
|
1162
|
-
handler: (client, input) => client.getProjectOverview(input.project
|
|
1171
|
+
handler: (client, input) => client.getProjectOverview(input.project, {
|
|
1172
|
+
location: input.location,
|
|
1173
|
+
since: input.since
|
|
1174
|
+
})
|
|
1163
1175
|
}),
|
|
1164
1176
|
defineTool({
|
|
1165
1177
|
name: "canonry_report",
|