@ainyc/canonry 3.3.8 → 3.4.5
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/dist/{chunk-24C7RMIS.js → chunk-5OYYYY4I.js} +76 -1967
- package/dist/chunk-D4YFX3X4.js +1918 -0
- package/dist/{chunk-P6D3O5JB.js → chunk-P3PS3ZSN.js} +571 -492
- package/dist/{chunk-ZCPZOVVE.js → chunk-ZOJLW6WR.js} +167 -6
- package/dist/cli.js +159 -36
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-FNJTFSI3.js → intelligence-service-GV6CAJ3Q.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +8 -8
- package/dist/chunk-MLKGABMK.js +0 -9
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ContentActions,
|
|
3
|
+
RunKinds,
|
|
2
4
|
__export
|
|
3
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-D4YFX3X4.js";
|
|
4
6
|
|
|
5
7
|
// src/intelligence-service.ts
|
|
6
|
-
import { eq, desc, asc, and, or } from "drizzle-orm";
|
|
8
|
+
import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
|
|
7
9
|
|
|
8
10
|
// ../db/src/client.ts
|
|
9
11
|
import { mkdirSync } from "fs";
|
|
@@ -1946,6 +1948,103 @@ function clamp012(value) {
|
|
|
1946
1948
|
return value;
|
|
1947
1949
|
}
|
|
1948
1950
|
|
|
1951
|
+
// ../intelligence/src/next-steps.ts
|
|
1952
|
+
var TOP_N = 5;
|
|
1953
|
+
var IMMEDIATE_HORIZON = 3;
|
|
1954
|
+
var ACTION_TITLE = {
|
|
1955
|
+
[ContentActions.create]: (q) => `Create a page targeting "${q}"`,
|
|
1956
|
+
[ContentActions.refresh]: (q) => `Refresh the page targeting "${q}"`,
|
|
1957
|
+
[ContentActions.expand]: (q) => `Expand coverage of "${q}"`,
|
|
1958
|
+
[ContentActions["add-schema"]]: (q) => `Add structured data to the page targeting "${q}"`
|
|
1959
|
+
};
|
|
1960
|
+
function mapOpportunitiesToNextSteps(opportunities, existing) {
|
|
1961
|
+
if (existing.length > 0) return existing;
|
|
1962
|
+
if (opportunities.length === 0) return [];
|
|
1963
|
+
return opportunities.slice(0, TOP_N).map((opp, idx) => ({
|
|
1964
|
+
horizon: idx < IMMEDIATE_HORIZON ? "immediate" : "short-term",
|
|
1965
|
+
title: ACTION_TITLE[opp.action](opp.query),
|
|
1966
|
+
rationale: `Score ${Math.round(opp.score)} \xB7 demand ${opp.demandSource} \xB7 ${opp.actionConfidence} confidence.`
|
|
1967
|
+
}));
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// ../intelligence/src/insight-severity.ts
|
|
1971
|
+
var SEVERITY_THRESHOLDS = {
|
|
1972
|
+
highTrafficImpressions: 100,
|
|
1973
|
+
recurrenceCount: 2,
|
|
1974
|
+
mediumTrafficImpressions: 10
|
|
1975
|
+
};
|
|
1976
|
+
function classifyRegressionSeverity(signals) {
|
|
1977
|
+
const { gscImpressions, recurrenceCount } = signals;
|
|
1978
|
+
if (gscImpressions === void 0 && recurrenceCount === void 0) return "high";
|
|
1979
|
+
const isHighTraffic = gscImpressions !== void 0 && gscImpressions >= SEVERITY_THRESHOLDS.highTrafficImpressions;
|
|
1980
|
+
const isRecurring = recurrenceCount !== void 0 && recurrenceCount >= SEVERITY_THRESHOLDS.recurrenceCount;
|
|
1981
|
+
const isModerateTraffic = gscImpressions !== void 0 && gscImpressions >= SEVERITY_THRESHOLDS.mediumTrafficImpressions;
|
|
1982
|
+
if (isHighTraffic && isRecurring) return "critical";
|
|
1983
|
+
if (isHighTraffic || isRecurring) return "high";
|
|
1984
|
+
if (isModerateTraffic) return "medium";
|
|
1985
|
+
return "low";
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
// ../intelligence/src/insight-grouping.ts
|
|
1989
|
+
function groupInsights(insights2, keyFn = (i) => `${i.keyword} ${i.provider} ${i.type}`) {
|
|
1990
|
+
const order = [];
|
|
1991
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
1992
|
+
for (const i of insights2) {
|
|
1993
|
+
const key = keyFn(i);
|
|
1994
|
+
const bucket = buckets.get(key);
|
|
1995
|
+
if (bucket) {
|
|
1996
|
+
bucket.push(i);
|
|
1997
|
+
} else {
|
|
1998
|
+
buckets.set(key, [i]);
|
|
1999
|
+
order.push(key);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return order.map((key) => {
|
|
2003
|
+
const sorted = [...buckets.get(key)].sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
2004
|
+
const representative = sorted[sorted.length - 1];
|
|
2005
|
+
return {
|
|
2006
|
+
representative,
|
|
2007
|
+
count: sorted.length,
|
|
2008
|
+
instances: sorted,
|
|
2009
|
+
latest: representative.createdAt
|
|
2010
|
+
};
|
|
2011
|
+
});
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
// ../intelligence/src/query-categorize.ts
|
|
2015
|
+
var TRANSACTIONAL_RE = /\b(buy|price|pricing|cost|hire|near me|services?|agency|consultant|company)\b/i;
|
|
2016
|
+
var INFORMATIONAL_RE = /\b(what|how|why|when|guide|tutorial|vs|versus|alternatives?|examples?|definition)\b/i;
|
|
2017
|
+
var MIN_BRAND_TOKEN_LENGTH = 3;
|
|
2018
|
+
function compact(value) {
|
|
2019
|
+
return value.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
2020
|
+
}
|
|
2021
|
+
function buildBrandTokens(canonicalDomain, displayName) {
|
|
2022
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2023
|
+
const stem = canonicalDomain.toLowerCase().replace(/\.[a-z]{2,}$/, "");
|
|
2024
|
+
const stemCompact = compact(stem);
|
|
2025
|
+
if (stemCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(stemCompact);
|
|
2026
|
+
if (displayName) {
|
|
2027
|
+
const displayCompact = compact(displayName);
|
|
2028
|
+
if (displayCompact.length >= MIN_BRAND_TOKEN_LENGTH) seen.add(displayCompact);
|
|
2029
|
+
}
|
|
2030
|
+
return [...seen];
|
|
2031
|
+
}
|
|
2032
|
+
function categorizeQueryByIntent(query, brandTokens) {
|
|
2033
|
+
const compactQuery = compact(query);
|
|
2034
|
+
if (brandTokens.length > 0 && brandTokens.some((t) => compactQuery.includes(t))) {
|
|
2035
|
+
return "brand";
|
|
2036
|
+
}
|
|
2037
|
+
if (TRANSACTIONAL_RE.test(query)) return "lead-gen";
|
|
2038
|
+
if (INFORMATIONAL_RE.test(query)) return "industry";
|
|
2039
|
+
return "other";
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// ../intelligence/src/trend-stability.ts
|
|
2043
|
+
var MIN_TREND_POINTS = 4;
|
|
2044
|
+
function isTrendBaseline(points) {
|
|
2045
|
+
return points.length < MIN_TREND_POINTS;
|
|
2046
|
+
}
|
|
2047
|
+
|
|
1949
2048
|
// src/intelligence-service.ts
|
|
1950
2049
|
import crypto from "crypto";
|
|
1951
2050
|
|
|
@@ -1987,6 +2086,7 @@ function createLogger(module) {
|
|
|
1987
2086
|
}
|
|
1988
2087
|
|
|
1989
2088
|
// src/intelligence-service.ts
|
|
2089
|
+
var RECURRENCE_LOOKBACK_RUNS = 5;
|
|
1990
2090
|
var log = createLogger("IntelligenceService");
|
|
1991
2091
|
var IntelligenceService = class {
|
|
1992
2092
|
constructor(db) {
|
|
@@ -2040,8 +2140,9 @@ var IntelligenceService = class {
|
|
|
2040
2140
|
citedRate: result.health.overallCitedRate,
|
|
2041
2141
|
insights: result.insights.length
|
|
2042
2142
|
});
|
|
2043
|
-
this.
|
|
2044
|
-
|
|
2143
|
+
const tieredResult = this.tierResult(result, runId, projectId);
|
|
2144
|
+
this.persistResult(tieredResult, runId, projectId);
|
|
2145
|
+
return tieredResult;
|
|
2045
2146
|
}
|
|
2046
2147
|
/**
|
|
2047
2148
|
* Analyze a single run given an explicit previous run (or null for first run).
|
|
@@ -2059,8 +2160,9 @@ var IntelligenceService = class {
|
|
|
2059
2160
|
return result2;
|
|
2060
2161
|
}
|
|
2061
2162
|
const result = analyzeRuns(currentRun, previousRun);
|
|
2062
|
-
this.
|
|
2063
|
-
|
|
2163
|
+
const tieredResult = this.tierResult(result, runRecord.id, runRecord.projectId);
|
|
2164
|
+
this.persistResult(tieredResult, runRecord.id, runRecord.projectId);
|
|
2165
|
+
return tieredResult;
|
|
2064
2166
|
}
|
|
2065
2167
|
/**
|
|
2066
2168
|
* Backfill intelligence for all completed/partial runs of a project.
|
|
@@ -2151,6 +2253,59 @@ var IntelligenceService = class {
|
|
|
2151
2253
|
});
|
|
2152
2254
|
log.info("intelligence.persisted", { runId, insights: result.insights.length });
|
|
2153
2255
|
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Apply severity tiering to the insights of an AnalysisResult and return a
|
|
2258
|
+
* new result. Wraps `applySeverityTiering` so callers (analyzeAndPersist,
|
|
2259
|
+
* analyzeRunWithPrevious) can pass the same tiered shape both into the DB
|
|
2260
|
+
* write and back to the RunCoordinator / webhook dispatcher.
|
|
2261
|
+
*/
|
|
2262
|
+
tierResult(result, runId, projectId) {
|
|
2263
|
+
if (result.insights.length === 0) return result;
|
|
2264
|
+
return { ...result, insights: this.applySeverityTiering(result.insights, runId, projectId) };
|
|
2265
|
+
}
|
|
2266
|
+
/**
|
|
2267
|
+
* Re-classify each regression insight's severity using GSC traffic +
|
|
2268
|
+
* recurrence signals via the pure `classifyRegressionSeverity` primitive
|
|
2269
|
+
* in @ainyc/canonry-intelligence. Non-regression insights are returned
|
|
2270
|
+
* untouched.
|
|
2271
|
+
*/
|
|
2272
|
+
applySeverityTiering(rawInsights, excludeRunId, projectId) {
|
|
2273
|
+
const regressions = rawInsights.filter((i) => i.type === "regression");
|
|
2274
|
+
if (regressions.length === 0) return rawInsights;
|
|
2275
|
+
const gscRows = this.db.select({ query: gscSearchData.query, impressions: gscSearchData.impressions }).from(gscSearchData).where(eq(gscSearchData.projectId, projectId)).all();
|
|
2276
|
+
const gscConnected = gscRows.length > 0;
|
|
2277
|
+
const gscImpressionsByKeyword = /* @__PURE__ */ new Map();
|
|
2278
|
+
for (const row of gscRows) {
|
|
2279
|
+
const key = row.query.toLowerCase();
|
|
2280
|
+
gscImpressionsByKeyword.set(key, (gscImpressionsByKeyword.get(key) ?? 0) + row.impressions);
|
|
2281
|
+
}
|
|
2282
|
+
const recentRunIds = this.db.select({ id: runs.id }).from(runs).where(
|
|
2283
|
+
and(
|
|
2284
|
+
eq(runs.projectId, projectId),
|
|
2285
|
+
eq(runs.kind, RunKinds["answer-visibility"]),
|
|
2286
|
+
or(eq(runs.status, "completed"), eq(runs.status, "partial"))
|
|
2287
|
+
)
|
|
2288
|
+
).orderBy(desc(runs.createdAt)).limit(RECURRENCE_LOOKBACK_RUNS + 1).all().map((r) => r.id).filter((id) => id !== excludeRunId).slice(0, RECURRENCE_LOOKBACK_RUNS);
|
|
2289
|
+
const haveHistory = recentRunIds.length > 0;
|
|
2290
|
+
const priorRegressionsByPair = /* @__PURE__ */ new Map();
|
|
2291
|
+
if (haveHistory) {
|
|
2292
|
+
const priorRows = this.db.select({ keyword: insights.keyword, provider: insights.provider }).from(insights).where(and(eq(insights.type, "regression"), inArray(insights.runId, recentRunIds))).all();
|
|
2293
|
+
for (const row of priorRows) {
|
|
2294
|
+
const key = `${row.keyword}:${row.provider}`;
|
|
2295
|
+
priorRegressionsByPair.set(key, (priorRegressionsByPair.get(key) ?? 0) + 1);
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
return rawInsights.map((insight) => {
|
|
2299
|
+
if (insight.type !== "regression") return insight;
|
|
2300
|
+
const gscImpressions = gscConnected ? gscImpressionsByKeyword.get(insight.keyword.toLowerCase()) ?? 0 : void 0;
|
|
2301
|
+
const recurrenceCount = haveHistory ? priorRegressionsByPair.get(`${insight.keyword}:${insight.provider}`) ?? 0 : void 0;
|
|
2302
|
+
const severity = classifyRegressionSeverity({
|
|
2303
|
+
gscImpressions,
|
|
2304
|
+
recurrenceCount
|
|
2305
|
+
});
|
|
2306
|
+
return { ...insight, severity };
|
|
2307
|
+
});
|
|
2308
|
+
}
|
|
2154
2309
|
buildRunData(runId, projectId, completedAt) {
|
|
2155
2310
|
const rows = this.db.select({
|
|
2156
2311
|
keyword: keywords.keyword,
|
|
@@ -2211,6 +2366,12 @@ export {
|
|
|
2211
2366
|
buildContentTargetRows,
|
|
2212
2367
|
buildContentSourceRows,
|
|
2213
2368
|
buildContentGapRows,
|
|
2369
|
+
mapOpportunitiesToNextSteps,
|
|
2370
|
+
groupInsights,
|
|
2371
|
+
buildBrandTokens,
|
|
2372
|
+
categorizeQueryByIntent,
|
|
2373
|
+
MIN_TREND_POINTS,
|
|
2374
|
+
isTrendBaseline,
|
|
2214
2375
|
createLogger,
|
|
2215
2376
|
IntelligenceService
|
|
2216
2377
|
};
|
package/dist/cli.js
CHANGED
|
@@ -17,51 +17,55 @@ import {
|
|
|
17
17
|
setGoogleAuthConfig,
|
|
18
18
|
showFirstRunNotice,
|
|
19
19
|
trackEvent
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-P3PS3ZSN.js";
|
|
21
21
|
import {
|
|
22
|
-
CcReleaseSyncStatuses,
|
|
23
|
-
CheckScopes,
|
|
24
|
-
CheckStatuses,
|
|
25
22
|
CliError,
|
|
26
|
-
CodingAgents,
|
|
27
23
|
EXIT_SYSTEM_ERROR,
|
|
28
24
|
EXIT_USER_ERROR,
|
|
29
|
-
ProviderNames,
|
|
30
|
-
RunKinds,
|
|
31
|
-
RunStatuses,
|
|
32
|
-
SkillsClients,
|
|
33
25
|
configExists,
|
|
34
26
|
createApiClient,
|
|
35
|
-
determineAnswerMentioned,
|
|
36
|
-
effectiveDomains,
|
|
37
|
-
formatRunErrorOneLine,
|
|
38
27
|
getConfigDir,
|
|
39
28
|
getConfigPath,
|
|
40
29
|
isEndpointMissing,
|
|
41
30
|
loadConfig,
|
|
42
|
-
normalizeUrlPath,
|
|
43
|
-
notificationEventSchema,
|
|
44
31
|
printCliError,
|
|
45
|
-
providerQuotaPolicySchema,
|
|
46
|
-
resolveProviderInput,
|
|
47
32
|
saveConfig,
|
|
48
33
|
saveConfigPatch,
|
|
49
|
-
skillsClientSchema,
|
|
50
34
|
usageError
|
|
51
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-5OYYYY4I.js";
|
|
52
36
|
import {
|
|
37
|
+
MIN_TREND_POINTS,
|
|
53
38
|
apiKeys,
|
|
54
39
|
competitors,
|
|
55
40
|
createClient,
|
|
56
41
|
gaAiReferrals,
|
|
57
42
|
gaTrafficSnapshots,
|
|
43
|
+
groupInsights,
|
|
44
|
+
isTrendBaseline,
|
|
58
45
|
migrate,
|
|
59
46
|
parseJsonColumn,
|
|
60
47
|
projects,
|
|
61
48
|
querySnapshots,
|
|
62
49
|
runs
|
|
63
|
-
} from "./chunk-
|
|
64
|
-
import
|
|
50
|
+
} from "./chunk-ZOJLW6WR.js";
|
|
51
|
+
import {
|
|
52
|
+
CcReleaseSyncStatuses,
|
|
53
|
+
CheckScopes,
|
|
54
|
+
CheckStatuses,
|
|
55
|
+
CodingAgents,
|
|
56
|
+
ProviderNames,
|
|
57
|
+
RunKinds,
|
|
58
|
+
RunStatuses,
|
|
59
|
+
SkillsClients,
|
|
60
|
+
determineAnswerMentioned,
|
|
61
|
+
effectiveDomains,
|
|
62
|
+
formatRunErrorOneLine,
|
|
63
|
+
normalizeUrlPath,
|
|
64
|
+
notificationEventSchema,
|
|
65
|
+
providerQuotaPolicySchema,
|
|
66
|
+
resolveProviderInput,
|
|
67
|
+
skillsClientSchema
|
|
68
|
+
} from "./chunk-D4YFX3X4.js";
|
|
65
69
|
|
|
66
70
|
// src/cli.ts
|
|
67
71
|
import { pathToFileURL } from "url";
|
|
@@ -577,7 +581,7 @@ function readStoredGroundingSources(rawResponse) {
|
|
|
577
581
|
return result;
|
|
578
582
|
}
|
|
579
583
|
async function backfillInsightsCommand(project, opts) {
|
|
580
|
-
const { IntelligenceService } = await import("./intelligence-service-
|
|
584
|
+
const { IntelligenceService } = await import("./intelligence-service-GV6CAJ3Q.js");
|
|
581
585
|
const config = loadConfig();
|
|
582
586
|
const db = createClient(config.database);
|
|
583
587
|
migrate(db);
|
|
@@ -5206,6 +5210,48 @@ function formatNumber(value) {
|
|
|
5206
5210
|
if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
|
|
5207
5211
|
return value.toLocaleString("en-US");
|
|
5208
5212
|
}
|
|
5213
|
+
function summarizeQueryParams(params) {
|
|
5214
|
+
const keys = Array.from(params.keys());
|
|
5215
|
+
const total = keys.length;
|
|
5216
|
+
if (total === 0) return "";
|
|
5217
|
+
const noun = total === 1 ? "param" : "params";
|
|
5218
|
+
const tag = inferAdSource(params);
|
|
5219
|
+
return tag ? `${tag} \xB7 ${total} ${noun}` : `${total} tracking ${noun}`;
|
|
5220
|
+
}
|
|
5221
|
+
function inferAdSource(params) {
|
|
5222
|
+
if (params.has("fbclid")) return "Facebook Ad";
|
|
5223
|
+
if (params.has("gclid") || params.has("gbraid") || params.has("wbraid")) return "Google Ad";
|
|
5224
|
+
if (params.has("msclkid")) return "Microsoft Ad";
|
|
5225
|
+
if (params.has("ttclid")) return "TikTok Ad";
|
|
5226
|
+
if (params.has("li_fat_id")) return "LinkedIn Ad";
|
|
5227
|
+
if (params.has("twclid")) return "X / Twitter Ad";
|
|
5228
|
+
if (params.has("epik")) return "Pinterest Ad";
|
|
5229
|
+
for (const k of params.keys()) {
|
|
5230
|
+
if (k.startsWith("hsa_")) return "Search Ad";
|
|
5231
|
+
}
|
|
5232
|
+
const src = params.get("utm_source");
|
|
5233
|
+
const med = params.get("utm_medium");
|
|
5234
|
+
if (src && med) return `${src} / ${med}`;
|
|
5235
|
+
if (src) return `Source: ${src}`;
|
|
5236
|
+
if (med) return `Medium: ${med}`;
|
|
5237
|
+
return null;
|
|
5238
|
+
}
|
|
5239
|
+
function formatLandingPageHtml(raw) {
|
|
5240
|
+
const value = raw ?? "";
|
|
5241
|
+
const queryIdx = value.indexOf("?");
|
|
5242
|
+
const path10 = queryIdx === -1 ? value : value.slice(0, queryIdx);
|
|
5243
|
+
const query = queryIdx === -1 ? "" : value.slice(queryIdx + 1);
|
|
5244
|
+
const pathHtml = `<span class="page-path">${escapeHtml(path10 || "/")}</span>`;
|
|
5245
|
+
if (!query) return pathHtml;
|
|
5246
|
+
let summary = "";
|
|
5247
|
+
try {
|
|
5248
|
+
summary = summarizeQueryParams(new URLSearchParams(query));
|
|
5249
|
+
} catch {
|
|
5250
|
+
summary = "tracking params";
|
|
5251
|
+
}
|
|
5252
|
+
if (!summary) return pathHtml;
|
|
5253
|
+
return `${pathHtml}<span class="page-query" title="${escapeHtml(value)}">${escapeHtml(summary)}</span>`;
|
|
5254
|
+
}
|
|
5209
5255
|
function formatDate(iso) {
|
|
5210
5256
|
if (!iso) return "\u2014";
|
|
5211
5257
|
try {
|
|
@@ -5344,6 +5390,9 @@ table.report-table th, table.report-table td {
|
|
|
5344
5390
|
text-align: left;
|
|
5345
5391
|
padding: 10px 12px;
|
|
5346
5392
|
border-bottom: 1px solid ${COLORS.border};
|
|
5393
|
+
vertical-align: top;
|
|
5394
|
+
overflow-wrap: anywhere;
|
|
5395
|
+
word-break: break-word;
|
|
5347
5396
|
}
|
|
5348
5397
|
table.report-table th {
|
|
5349
5398
|
font-weight: 600;
|
|
@@ -5352,7 +5401,25 @@ table.report-table th {
|
|
|
5352
5401
|
letter-spacing: 0.06em;
|
|
5353
5402
|
font-size: 10px;
|
|
5354
5403
|
}
|
|
5355
|
-
table.report-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; }
|
|
5404
|
+
table.report-table td.numeric { text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
|
|
5405
|
+
table.report-table td.page-cell { max-width: 0; }
|
|
5406
|
+
table.report-table td.page-cell .page-path {
|
|
5407
|
+
display: block;
|
|
5408
|
+
font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
5409
|
+
font-size: 12px;
|
|
5410
|
+
color: ${COLORS.text};
|
|
5411
|
+
}
|
|
5412
|
+
table.report-table td.page-cell .page-query {
|
|
5413
|
+
display: inline-block;
|
|
5414
|
+
margin-top: 4px;
|
|
5415
|
+
padding: 1px 8px;
|
|
5416
|
+
font-size: 11px;
|
|
5417
|
+
color: ${COLORS.textMuted};
|
|
5418
|
+
background: ${COLORS.surface};
|
|
5419
|
+
border: 1px solid ${COLORS.border};
|
|
5420
|
+
border-radius: 999px;
|
|
5421
|
+
cursor: help;
|
|
5422
|
+
}
|
|
5356
5423
|
table.report-table td .badge {
|
|
5357
5424
|
display: inline-block;
|
|
5358
5425
|
padding: 2px 8px;
|
|
@@ -5601,15 +5668,19 @@ function renderCompetitorLandscape(report) {
|
|
|
5601
5668
|
}
|
|
5602
5669
|
const rows = competitors2.map((c) => {
|
|
5603
5670
|
const tone = pressureTone(c.pressureLabel);
|
|
5671
|
+
const pagesDisclosure = c.theirCitedPages.length > 0 ? `<details class="cited-pages"><summary>${c.theirCitedPages.length} cited URL${c.theirCitedPages.length > 1 ? "s" : ""}</summary>
|
|
5672
|
+
<ul>${c.theirCitedPages.map((p) => `<li><a href="${escapeHtml(p.url)}">${escapeHtml(p.url)}</a> <span class="cited-for">${escapeHtml(p.citedFor.join(", "))}</span></li>`).join("")}</ul>
|
|
5673
|
+
</details>` : "";
|
|
5604
5674
|
return `<tr>
|
|
5605
5675
|
<td>${escapeHtml(c.domain)}</td>
|
|
5606
5676
|
<td><span class="badge tone-${tone}">${escapeHtml(c.pressureLabel)}</span></td>
|
|
5607
5677
|
<td class="numeric">${c.citationCount} / ${c.totalCount}</td>
|
|
5608
|
-
<td
|
|
5678
|
+
<td class="numeric">${c.sharePct}%</td>
|
|
5679
|
+
<td>${escapeHtml(c.citedKeywords.slice(0, 5).join(", "))}${c.citedKeywords.length > 5 ? "\u2026" : ""}${pagesDisclosure}</td>
|
|
5609
5680
|
</tr>`;
|
|
5610
5681
|
}).join("");
|
|
5611
5682
|
const table = competitors2.length > 0 ? `<table class="report-table">
|
|
5612
|
-
<thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th>Cited keywords</th></tr></thead>
|
|
5683
|
+
<thead><tr><th>Domain</th><th>Pressure</th><th>Citations</th><th class="numeric">SOV</th><th>Cited keywords</th></tr></thead>
|
|
5613
5684
|
<tbody>${rows}</tbody>
|
|
5614
5685
|
</table>` : renderEmpty("No competitors configured.");
|
|
5615
5686
|
return section(
|
|
@@ -5741,6 +5812,19 @@ function renderGsc(report) {
|
|
|
5741
5812
|
COLORS.accent,
|
|
5742
5813
|
"Clicks over time"
|
|
5743
5814
|
);
|
|
5815
|
+
const crossoverBlocks = [];
|
|
5816
|
+
if (gsc.trackedButNoGsc.length > 0) {
|
|
5817
|
+
crossoverBlocks.push(`<div class="chart-card"><h3>AEO keywords without search demand</h3>
|
|
5818
|
+
<p class="section-intro">Tracked AEO keywords with no GSC impressions in this window \u2014 review whether they represent real search demand.</p>
|
|
5819
|
+
<ul>${gsc.trackedButNoGsc.map((k) => `<li>${escapeHtml(k)}</li>`).join("")}</ul>
|
|
5820
|
+
</div>`);
|
|
5821
|
+
}
|
|
5822
|
+
if (gsc.gscButNotTracked.length > 0) {
|
|
5823
|
+
crossoverBlocks.push(`<div class="chart-card"><h3>Search queries you should track</h3>
|
|
5824
|
+
<p class="section-intro">GSC top queries (by impressions) that aren't tracked in your AEO project \u2014 candidates to add as keywords.</p>
|
|
5825
|
+
<ul>${gsc.gscButNotTracked.map((q) => `<li>${escapeHtml(q)}</li>`).join("")}</ul>
|
|
5826
|
+
</div>`);
|
|
5827
|
+
}
|
|
5744
5828
|
return section(
|
|
5745
5829
|
{ id: "gsc", eyebrow: "Section 5", title: "GSC Performance", intro: "Top queries, category breakdown, and traffic trend from Google Search Console." },
|
|
5746
5830
|
`<div class="metric-grid">
|
|
@@ -5761,7 +5845,8 @@ function renderGsc(report) {
|
|
|
5761
5845
|
<thead><tr><th>Category</th><th class="numeric">Clicks</th><th class="numeric">Imp.</th><th class="numeric">Share</th></tr></thead>
|
|
5762
5846
|
<tbody>${breakdownRows}</tbody>
|
|
5763
5847
|
</table>
|
|
5764
|
-
</div
|
|
5848
|
+
</div>
|
|
5849
|
+
${crossoverBlocks.join("\n")}`
|
|
5765
5850
|
);
|
|
5766
5851
|
}
|
|
5767
5852
|
function renderGa(report) {
|
|
@@ -5774,7 +5859,7 @@ function renderGa(report) {
|
|
|
5774
5859
|
}
|
|
5775
5860
|
const pageRows = ga.topLandingPages.map((p) => `
|
|
5776
5861
|
<tr>
|
|
5777
|
-
<td>${
|
|
5862
|
+
<td class="page-cell">${formatLandingPageHtml(p.page)}</td>
|
|
5778
5863
|
<td class="numeric">${formatNumber(p.sessions)}</td>
|
|
5779
5864
|
<td class="numeric">${formatNumber(p.users)}</td>
|
|
5780
5865
|
<td class="numeric">${formatNumber(p.organicSessions)}</td>
|
|
@@ -5864,7 +5949,7 @@ function renderAiReferrals(report) {
|
|
|
5864
5949
|
</tr>`).join("");
|
|
5865
5950
|
const pageRows = ai.topLandingPages.map((p) => `
|
|
5866
5951
|
<tr>
|
|
5867
|
-
<td>${
|
|
5952
|
+
<td class="page-cell">${formatLandingPageHtml(p.page)}</td>
|
|
5868
5953
|
<td class="numeric">${formatNumber(p.sessions)}</td>
|
|
5869
5954
|
<td class="numeric">${formatNumber(p.users)}</td>
|
|
5870
5955
|
</tr>`).join("");
|
|
@@ -5941,6 +6026,12 @@ function renderCitationsTrend(report) {
|
|
|
5941
6026
|
renderEmpty("Run multiple visibility sweeps to see a trend.")
|
|
5942
6027
|
);
|
|
5943
6028
|
}
|
|
6029
|
+
if (isTrendBaseline(trend)) {
|
|
6030
|
+
return section(
|
|
6031
|
+
{ id: "citations-trend", eyebrow: "Section 10", title: "Citations Over Time" },
|
|
6032
|
+
renderEmpty(`Establishing baseline (${trend.length} of ${MIN_TREND_POINTS} runs collected). Trend will appear once more sweeps are recorded.`)
|
|
6033
|
+
);
|
|
6034
|
+
}
|
|
5944
6035
|
const chart = renderLineChart(
|
|
5945
6036
|
trend.map((t) => ({ x: t.date, y: t.citationRate, label: formatDate(t.date) })),
|
|
5946
6037
|
COLORS.positive,
|
|
@@ -5972,15 +6063,17 @@ function renderInsights(report) {
|
|
|
5972
6063
|
renderEmpty("No insights yet \u2014 run a visibility sweep to generate alerts.")
|
|
5973
6064
|
);
|
|
5974
6065
|
}
|
|
5975
|
-
const
|
|
6066
|
+
const haveDeduped = list.every((i) => typeof i.instanceCount === "number");
|
|
6067
|
+
const rows = (haveDeduped ? list.map((i) => ({ rep: i, count: i.instanceCount })) : groupInsights(list).map((g) => ({ rep: g.representative, count: g.count }))).map(({ rep: i, count }) => {
|
|
5976
6068
|
const tone = severityTone(i.severity);
|
|
6069
|
+
const countChip = count > 1 ? ` <span class="badge tone-neutral">\xD7 ${count}</span>` : "";
|
|
5977
6070
|
return `<tr>
|
|
5978
|
-
|
|
5979
|
-
|
|
5980
|
-
|
|
5981
|
-
|
|
5982
|
-
|
|
5983
|
-
|
|
6071
|
+
<td><span class="badge tone-${tone}">${escapeHtml(i.severity)}</span></td>
|
|
6072
|
+
<td>${escapeHtml(i.title)}${countChip}</td>
|
|
6073
|
+
<td>${escapeHtml(i.keyword)}</td>
|
|
6074
|
+
<td>${escapeHtml(i.provider)}</td>
|
|
6075
|
+
<td>${i.recommendation ? escapeHtml(i.recommendation) : '<span class="cell-pending">\u2014</span>'}</td>
|
|
6076
|
+
</tr>`;
|
|
5984
6077
|
}).join("");
|
|
5985
6078
|
return section(
|
|
5986
6079
|
{ id: "insights", eyebrow: "Section 11", title: "Insights & Alerts", intro: "Priority-ordered findings from the most recent runs." },
|
|
@@ -5990,11 +6083,40 @@ function renderInsights(report) {
|
|
|
5990
6083
|
</table>`
|
|
5991
6084
|
);
|
|
5992
6085
|
}
|
|
6086
|
+
function renderOpportunities(report) {
|
|
6087
|
+
const opps = report.contentOpportunities;
|
|
6088
|
+
if (opps.length === 0) return "";
|
|
6089
|
+
const rows = opps.slice(0, 10).map((o) => {
|
|
6090
|
+
const ourPage = o.ourBestPage ? `<a href="${escapeHtml(o.ourBestPage.url)}">${escapeHtml(o.ourBestPage.url)}</a>` : '<span class="cell-not-cited">\u2014</span>';
|
|
6091
|
+
const winning = o.winningCompetitor ? `<a href="${escapeHtml(o.winningCompetitor.url)}">${escapeHtml(o.winningCompetitor.domain)}</a>` : '<span class="cell-not-cited">\u2014</span>';
|
|
6092
|
+
return `<tr>
|
|
6093
|
+
<td>${escapeHtml(o.query)}</td>
|
|
6094
|
+
<td><span class="badge tone-neutral">${escapeHtml(o.action)}</span></td>
|
|
6095
|
+
<td class="numeric">${Math.round(o.score)}</td>
|
|
6096
|
+
<td>${ourPage}</td>
|
|
6097
|
+
<td>${winning}</td>
|
|
6098
|
+
<td><span class="badge tone-neutral">${escapeHtml(o.demandSource)}</span></td>
|
|
6099
|
+
<td><span class="badge tone-neutral">${escapeHtml(o.actionConfidence)}</span></td>
|
|
6100
|
+
</tr>`;
|
|
6101
|
+
}).join("");
|
|
6102
|
+
return section(
|
|
6103
|
+
{
|
|
6104
|
+
id: "content-opportunities",
|
|
6105
|
+
eyebrow: "Section 12",
|
|
6106
|
+
title: "Content Opportunities",
|
|
6107
|
+
intro: "Ranked, action-typed targets from the content recommendation engine. Top 10 shown."
|
|
6108
|
+
},
|
|
6109
|
+
`<table class="report-table">
|
|
6110
|
+
<thead><tr><th>Query</th><th>Action</th><th class="numeric">Score</th><th>Our page</th><th>Winning competitor</th><th>Demand</th><th>Confidence</th></tr></thead>
|
|
6111
|
+
<tbody>${rows}</tbody>
|
|
6112
|
+
</table>`
|
|
6113
|
+
);
|
|
6114
|
+
}
|
|
5993
6115
|
function renderRecommendedNextSteps(report) {
|
|
5994
6116
|
const steps = report.recommendedNextSteps;
|
|
5995
6117
|
if (steps.length === 0) {
|
|
5996
6118
|
return section(
|
|
5997
|
-
{ id: "recommended-next-steps", eyebrow: "Section
|
|
6119
|
+
{ id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
|
|
5998
6120
|
renderEmpty("No outstanding actions.")
|
|
5999
6121
|
);
|
|
6000
6122
|
}
|
|
@@ -6005,7 +6127,7 @@ function renderRecommendedNextSteps(report) {
|
|
|
6005
6127
|
<span class="rationale">${escapeHtml(s.rationale)}</span>
|
|
6006
6128
|
</div>`).join("");
|
|
6007
6129
|
return section(
|
|
6008
|
-
{ id: "recommended-next-steps", eyebrow: "Section
|
|
6130
|
+
{ id: "recommended-next-steps", eyebrow: "Section 13", title: "Recommended Next Steps" },
|
|
6009
6131
|
`<div class="steps">${items}</div>`
|
|
6010
6132
|
);
|
|
6011
6133
|
}
|
|
@@ -6026,6 +6148,7 @@ function renderReportHtml(report, opts = {}) {
|
|
|
6026
6148
|
renderIndexingHealth(report),
|
|
6027
6149
|
renderCitationsTrend(report),
|
|
6028
6150
|
renderInsights(report),
|
|
6151
|
+
renderOpportunities(report),
|
|
6029
6152
|
renderRecommendedNextSteps(report)
|
|
6030
6153
|
].join("\n");
|
|
6031
6154
|
const json = escapeJsonForScript(JSON.stringify(report));
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createServer
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-P3PS3ZSN.js";
|
|
4
4
|
import {
|
|
5
5
|
loadConfig
|
|
6
|
-
} from "./chunk-
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
6
|
+
} from "./chunk-5OYYYY4I.js";
|
|
7
|
+
import "./chunk-ZOJLW6WR.js";
|
|
8
|
+
import "./chunk-D4YFX3X4.js";
|
|
9
9
|
export {
|
|
10
10
|
createServer,
|
|
11
11
|
loadConfig
|
package/dist/mcp.js
CHANGED
|
@@ -2,8 +2,8 @@ import {
|
|
|
2
2
|
CliError,
|
|
3
3
|
canonryMcpTools,
|
|
4
4
|
createApiClient
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-5OYYYY4I.js";
|
|
6
|
+
import "./chunk-D4YFX3X4.js";
|
|
7
7
|
|
|
8
8
|
// src/mcp/cli.ts
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ainyc/canonry",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Agent-first open-source AEO operating platform - track how answer engines cite your domain",
|
|
6
6
|
"license": "FSL-1.1-ALv2",
|
|
@@ -59,21 +59,21 @@
|
|
|
59
59
|
"@types/node-cron": "^3.0.11",
|
|
60
60
|
"tsup": "^8.5.1",
|
|
61
61
|
"tsx": "^4.19.0",
|
|
62
|
-
"@ainyc/canonry-config": "0.0.0",
|
|
63
62
|
"@ainyc/canonry-api-routes": "0.0.0",
|
|
63
|
+
"@ainyc/canonry-config": "0.0.0",
|
|
64
64
|
"@ainyc/canonry-contracts": "0.0.0",
|
|
65
|
-
"@ainyc/canonry-intelligence": "0.0.0",
|
|
66
65
|
"@ainyc/canonry-db": "0.0.0",
|
|
67
|
-
"@ainyc/canonry-integration-commoncrawl": "0.0.0",
|
|
68
66
|
"@ainyc/canonry-integration-bing": "0.0.0",
|
|
67
|
+
"@ainyc/canonry-intelligence": "0.0.0",
|
|
69
68
|
"@ainyc/canonry-integration-google": "0.0.0",
|
|
69
|
+
"@ainyc/canonry-integration-commoncrawl": "0.0.0",
|
|
70
70
|
"@ainyc/canonry-integration-wordpress": "0.0.0",
|
|
71
|
-
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
72
71
|
"@ainyc/canonry-provider-claude": "0.0.0",
|
|
73
|
-
"@ainyc/canonry-provider-
|
|
74
|
-
"@ainyc/canonry-provider-local": "0.0.0",
|
|
72
|
+
"@ainyc/canonry-provider-cdp": "0.0.0",
|
|
75
73
|
"@ainyc/canonry-provider-openai": "0.0.0",
|
|
76
|
-
"@ainyc/canonry-provider-
|
|
74
|
+
"@ainyc/canonry-provider-gemini": "0.0.0",
|
|
75
|
+
"@ainyc/canonry-provider-perplexity": "0.0.0",
|
|
76
|
+
"@ainyc/canonry-provider-local": "0.0.0"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",
|