@ainyc/canonry 4.55.1 → 4.56.0
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/agent-workspace/skills/aero/SKILL.md +2 -0
- package/assets/agent-workspace/skills/canonry/SKILL.md +2 -0
- package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +55 -7
- package/assets/assets/{BacklinksPage-DVmaM864.js → BacklinksPage-yWx_BBLG.js} +1 -1
- package/assets/assets/ChartPrimitives-gqioJ07n.js +1 -0
- package/assets/assets/ProjectPage-D1tnXJ2L.js +6 -0
- package/assets/assets/{RunRow-BRqiLxj2.js → RunRow-DEr_yQLw.js} +1 -1
- package/assets/assets/{RunsPage-UxZ93-cg.js → RunsPage-DMrl5Fhn.js} +1 -1
- package/assets/assets/{SettingsPage-Cr5_EGbk.js → SettingsPage-Dp0TXf34.js} +1 -1
- package/assets/assets/{TrafficPage-CUC_lfTe.js → TrafficPage-POy4iHHt.js} +1 -1
- package/assets/assets/TrafficSourceDetailPage-BewjZ53n.js +1 -0
- package/assets/assets/{extract-error-message-DD5MibWI.js → extract-error-message-C0GGpK4T.js} +1 -1
- package/assets/assets/{index-nnF1LnyK.js → index-D0A-UvNH.js} +79 -79
- package/assets/assets/index-_jdnW4nh.css +1 -0
- package/assets/assets/{server-traffic-DjRISEZ-.js → server-traffic-B5rtrB-q.js} +1 -1
- package/assets/assets/{trash-2-CJ5M--Le.js → trash-2-CUczQ2Yl.js} +1 -1
- package/assets/index.html +2 -2
- package/dist/{chunk-UTM3FPAJ.js → chunk-4KWPOVIT.js} +181 -3
- package/dist/{chunk-ZY3EDW3S.js → chunk-6X5TF73A.js} +49 -3
- package/dist/{chunk-2OI7HFAB.js → chunk-I2LAM5IM.js} +309 -222
- package/dist/{chunk-OFY3Z2F7.js → chunk-WFVUZVJD.js} +368 -361
- package/dist/cli.js +66 -11
- package/dist/index.js +4 -4
- package/dist/{intelligence-service-NKAEHHJ5.js → intelligence-service-NY3MAVPB.js} +2 -2
- package/dist/mcp.js +2 -2
- package/package.json +10 -10
- package/assets/assets/ChartPrimitives-9Kx3gzQL.js +0 -1
- package/assets/assets/ProjectPage-DtL3LFne.js +0 -6
- package/assets/assets/TrafficSourceDetailPage-DARPL2TU.js +0 -1
- package/assets/assets/index-Bm3JQsW0.css +0 -1
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
categorizeSourceWithCompetitors,
|
|
10
10
|
categoryLabel,
|
|
11
11
|
determineAnswerMentioned,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
effectiveDomains,
|
|
13
|
+
normalizeProjectDomain,
|
|
14
|
+
registrableDomain
|
|
15
|
+
} from "./chunk-WFVUZVJD.js";
|
|
14
16
|
|
|
15
17
|
// src/intelligence-service.ts
|
|
16
18
|
import { eq, desc, asc, and, ne, or, inArray } from "drizzle-orm";
|
|
@@ -4156,6 +4158,172 @@ function createLogger(module) {
|
|
|
4156
4158
|
};
|
|
4157
4159
|
}
|
|
4158
4160
|
|
|
4161
|
+
// src/citation-utils.ts
|
|
4162
|
+
function domainMatches(domain, canonicalDomain) {
|
|
4163
|
+
const normalized = normalizeProjectDomain(canonicalDomain);
|
|
4164
|
+
const d = normalizeProjectDomain(domain);
|
|
4165
|
+
return d === normalized || d.endsWith(`.${normalized}`);
|
|
4166
|
+
}
|
|
4167
|
+
function pickProjectCitedDomain(citedDomains, projectDomains) {
|
|
4168
|
+
for (const cited of citedDomains) {
|
|
4169
|
+
if (projectDomains.some((pd) => domainMatches(cited, pd))) return cited;
|
|
4170
|
+
}
|
|
4171
|
+
return void 0;
|
|
4172
|
+
}
|
|
4173
|
+
function determineCitationState(normalized, domains) {
|
|
4174
|
+
for (const canonicalDomain of domains) {
|
|
4175
|
+
const bareDomain = normalizeProjectDomain(canonicalDomain);
|
|
4176
|
+
if (normalized.citedDomains.some((d) => domainMatches(d, bareDomain))) {
|
|
4177
|
+
return "cited";
|
|
4178
|
+
}
|
|
4179
|
+
const lowerDomain = bareDomain.toLowerCase();
|
|
4180
|
+
for (const source of normalized.groundingSources) {
|
|
4181
|
+
try {
|
|
4182
|
+
const uri = source.uri.toLowerCase();
|
|
4183
|
+
if (lowerDomain.includes(".") && uri.includes(lowerDomain)) {
|
|
4184
|
+
return "cited";
|
|
4185
|
+
}
|
|
4186
|
+
} catch {
|
|
4187
|
+
}
|
|
4188
|
+
if (source.title) {
|
|
4189
|
+
const titleLower = source.title.toLowerCase().replace(/^www\./, "");
|
|
4190
|
+
if (titleLower === lowerDomain || titleLower.endsWith(`.${lowerDomain}`)) {
|
|
4191
|
+
return "cited";
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
}
|
|
4195
|
+
}
|
|
4196
|
+
return "not-cited";
|
|
4197
|
+
}
|
|
4198
|
+
function computeCompetitorOverlap(normalized, competitorDomains) {
|
|
4199
|
+
const overlapSet = /* @__PURE__ */ new Set();
|
|
4200
|
+
for (const d of normalized.citedDomains) {
|
|
4201
|
+
for (const cd of competitorDomains) {
|
|
4202
|
+
if (domainMatches(d, cd)) {
|
|
4203
|
+
overlapSet.add(cd);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
}
|
|
4207
|
+
for (const source of normalized.groundingSources) {
|
|
4208
|
+
const uri = source.uri.toLowerCase();
|
|
4209
|
+
for (const cd of competitorDomains) {
|
|
4210
|
+
if (uri.includes(cd.toLowerCase())) {
|
|
4211
|
+
overlapSet.add(cd);
|
|
4212
|
+
}
|
|
4213
|
+
}
|
|
4214
|
+
}
|
|
4215
|
+
if (normalized.answerText) {
|
|
4216
|
+
const lowerAnswer = normalized.answerText.toLowerCase();
|
|
4217
|
+
for (const cd of competitorDomains) {
|
|
4218
|
+
if (lowerAnswer.includes(cd.toLowerCase())) {
|
|
4219
|
+
overlapSet.add(cd);
|
|
4220
|
+
}
|
|
4221
|
+
const brand = brandLabelFromDomain(cd);
|
|
4222
|
+
if (brand.length >= 4 && new RegExp(`\\b${escapeRegExp(brand)}\\b`, "i").test(lowerAnswer)) {
|
|
4223
|
+
overlapSet.add(cd);
|
|
4224
|
+
}
|
|
4225
|
+
}
|
|
4226
|
+
}
|
|
4227
|
+
return [...overlapSet];
|
|
4228
|
+
}
|
|
4229
|
+
function escapeRegExp(value) {
|
|
4230
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
4231
|
+
}
|
|
4232
|
+
function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
|
|
4233
|
+
if (!answerText || answerText.length < 20) return [];
|
|
4234
|
+
const ownBrandKeys = new Set(
|
|
4235
|
+
ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
|
|
4236
|
+
);
|
|
4237
|
+
for (const name of ownBrandNames) {
|
|
4238
|
+
const key = brandKeyFromText(name);
|
|
4239
|
+
if (key.length >= 4) ownBrandKeys.add(key);
|
|
4240
|
+
}
|
|
4241
|
+
const knownCompetitorKeys = new Set(
|
|
4242
|
+
[...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
|
|
4243
|
+
);
|
|
4244
|
+
if (knownCompetitorKeys.size === 0) return [];
|
|
4245
|
+
const candidatePatterns = [
|
|
4246
|
+
/^\s*(?:[-*]|\d+\.)\s+(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?\s*[:\u2014\u2013-]/gm,
|
|
4247
|
+
/\*\*([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\*\*/g,
|
|
4248
|
+
/^#{1,4}\s+(?:\d+\.\s+)?(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?$/gm,
|
|
4249
|
+
/\[([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\]\(https?:\/\/[^\s)]+\)/g
|
|
4250
|
+
];
|
|
4251
|
+
const genericKeys = /* @__PURE__ */ new Set([
|
|
4252
|
+
"additional",
|
|
4253
|
+
"best",
|
|
4254
|
+
"benefits",
|
|
4255
|
+
"bottomline",
|
|
4256
|
+
"comparison",
|
|
4257
|
+
"conclusion",
|
|
4258
|
+
"directorylisting",
|
|
4259
|
+
"example",
|
|
4260
|
+
"expertise",
|
|
4261
|
+
"features",
|
|
4262
|
+
"finalthoughts",
|
|
4263
|
+
"howitworks",
|
|
4264
|
+
"important",
|
|
4265
|
+
"keybenefits",
|
|
4266
|
+
"keyfeatures",
|
|
4267
|
+
"major",
|
|
4268
|
+
"note",
|
|
4269
|
+
"notable",
|
|
4270
|
+
"option",
|
|
4271
|
+
"other",
|
|
4272
|
+
"overview",
|
|
4273
|
+
"pricing",
|
|
4274
|
+
"pros",
|
|
4275
|
+
"reviews",
|
|
4276
|
+
"step",
|
|
4277
|
+
"summary",
|
|
4278
|
+
"top",
|
|
4279
|
+
"verdict",
|
|
4280
|
+
"whattolookfor",
|
|
4281
|
+
"whyitmatters",
|
|
4282
|
+
"whyitstandsout",
|
|
4283
|
+
"whywechoseit"
|
|
4284
|
+
]);
|
|
4285
|
+
const seen = /* @__PURE__ */ new Map();
|
|
4286
|
+
for (const pattern of candidatePatterns) {
|
|
4287
|
+
let match;
|
|
4288
|
+
while ((match = pattern.exec(answerText)) !== null) {
|
|
4289
|
+
const candidate = cleanCandidateName(match[1] ?? "");
|
|
4290
|
+
const candidateKey = brandKeyFromText(candidate);
|
|
4291
|
+
if (!candidateKey) continue;
|
|
4292
|
+
if (genericKeys.has(candidateKey)) continue;
|
|
4293
|
+
if (candidate.split(/\s+/).length > 6) continue;
|
|
4294
|
+
if (matchesBrandKey(candidateKey, ownBrandKeys)) continue;
|
|
4295
|
+
if (!matchesBrandKey(candidateKey, knownCompetitorKeys)) continue;
|
|
4296
|
+
if (!seen.has(candidateKey)) seen.set(candidateKey, candidate);
|
|
4297
|
+
}
|
|
4298
|
+
}
|
|
4299
|
+
return [...seen.values()].slice(0, 10);
|
|
4300
|
+
}
|
|
4301
|
+
function cleanCandidateName(candidate) {
|
|
4302
|
+
return candidate.replace(/^[\s"'`]+|[\s"'`.,:;!?]+$/g, "").replace(/\s+/g, " ").trim();
|
|
4303
|
+
}
|
|
4304
|
+
function collectBrandKeysFromDomain(domain) {
|
|
4305
|
+
const reg = registrableDomain(domain);
|
|
4306
|
+
if (!reg) {
|
|
4307
|
+
const hostname = normalizeProjectDomain(domain).split("/")[0] ?? "";
|
|
4308
|
+
const fallback = hostname.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
4309
|
+
return fallback.length >= 4 ? [fallback] : [];
|
|
4310
|
+
}
|
|
4311
|
+
const keys = /* @__PURE__ */ new Set();
|
|
4312
|
+
const fullKey = reg.replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
4313
|
+
if (fullKey.length >= 4) keys.add(fullKey);
|
|
4314
|
+
const brand = brandLabelFromDomain(reg).replace(/[^a-z0-9]/gi, "").toLowerCase();
|
|
4315
|
+
if (brand.length >= 4) keys.add(brand);
|
|
4316
|
+
return [...keys];
|
|
4317
|
+
}
|
|
4318
|
+
function matchesBrandKey(candidateKey, brandKeys) {
|
|
4319
|
+
for (const brandKey of brandKeys) {
|
|
4320
|
+
if (candidateKey === brandKey) return true;
|
|
4321
|
+
if (candidateKey.startsWith(brandKey) || candidateKey.endsWith(brandKey)) return true;
|
|
4322
|
+
if (brandKey.startsWith(candidateKey) || brandKey.endsWith(candidateKey)) return true;
|
|
4323
|
+
}
|
|
4324
|
+
return false;
|
|
4325
|
+
}
|
|
4326
|
+
|
|
4159
4327
|
// src/intelligence-service.ts
|
|
4160
4328
|
var RECURRENCE_LOOKBACK_RUNS = 5;
|
|
4161
4329
|
var HISTORY_WINDOW_RUNS = Math.max(PERSISTENT_GAP_THRESHOLD, 5);
|
|
@@ -4537,6 +4705,11 @@ var IntelligenceService = class {
|
|
|
4537
4705
|
});
|
|
4538
4706
|
}
|
|
4539
4707
|
buildRunData(runId, projectId, completedAt, location = null) {
|
|
4708
|
+
const projectDomainRow = this.db.select({ canonicalDomain: projects.canonicalDomain, ownedDomains: projects.ownedDomains }).from(projects).where(eq(projects.id, projectId)).get();
|
|
4709
|
+
const projectDomains = projectDomainRow ? effectiveDomains({
|
|
4710
|
+
canonicalDomain: projectDomainRow.canonicalDomain,
|
|
4711
|
+
ownedDomains: projectDomainRow.ownedDomains
|
|
4712
|
+
}) : [];
|
|
4540
4713
|
const rows = this.db.select({
|
|
4541
4714
|
query: queries.query,
|
|
4542
4715
|
// Denormalized query text persisted by v58 — the fallback when the
|
|
@@ -4563,7 +4736,9 @@ var IntelligenceService = class {
|
|
|
4563
4736
|
query: resolvedQuery,
|
|
4564
4737
|
provider: r.provider,
|
|
4565
4738
|
cited: r.citationState === CitationStates.cited,
|
|
4566
|
-
|
|
4739
|
+
// The project's OWN cited domain — never a co-cited competitor that
|
|
4740
|
+
// happens to sort first in the full citedDomains set.
|
|
4741
|
+
citationUrl: pickProjectCitedDomain(domains, projectDomains),
|
|
4567
4742
|
// Snapshots carry their own location for downstream detectors. In
|
|
4568
4743
|
// practice every snapshot in a single runId shares the run's
|
|
4569
4744
|
// location; the per-row column is the same value duplicated, but
|
|
@@ -4629,6 +4804,9 @@ export {
|
|
|
4629
4804
|
groupRunsByCreatedAt,
|
|
4630
4805
|
pickGroupRepresentative,
|
|
4631
4806
|
filterTrackedSnapshots,
|
|
4807
|
+
determineCitationState,
|
|
4808
|
+
computeCompetitorOverlap,
|
|
4809
|
+
extractRecommendedCompetitors,
|
|
4632
4810
|
isBlogShapedQuery,
|
|
4633
4811
|
buildInventory,
|
|
4634
4812
|
buildContentTargetRows,
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
trafficConnectVercelRequestSchema,
|
|
23
23
|
trafficConnectWordpressRequestSchema,
|
|
24
24
|
trafficEventKindSchema
|
|
25
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-WFVUZVJD.js";
|
|
26
26
|
|
|
27
27
|
// src/config.ts
|
|
28
28
|
import fs from "fs";
|
|
@@ -3001,6 +3001,22 @@ var postApiV1ProjectsByNameTrafficSourcesByIdBackfill = (options) => {
|
|
|
3001
3001
|
}
|
|
3002
3002
|
});
|
|
3003
3003
|
};
|
|
3004
|
+
var postApiV1ProjectsByNameTrafficSourcesByIdReset = (options) => {
|
|
3005
|
+
return (options.client ?? client).post({
|
|
3006
|
+
security: [
|
|
3007
|
+
{
|
|
3008
|
+
scheme: "bearer",
|
|
3009
|
+
type: "http"
|
|
3010
|
+
}
|
|
3011
|
+
],
|
|
3012
|
+
url: "/api/v1/projects/{name}/traffic/sources/{id}/reset",
|
|
3013
|
+
...options,
|
|
3014
|
+
headers: {
|
|
3015
|
+
"Content-Type": "application/json",
|
|
3016
|
+
...options.headers
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
};
|
|
3004
3020
|
var getApiV1ProjectsByNameTrafficSources = (options) => {
|
|
3005
3021
|
return (options.client ?? client).get({
|
|
3006
3022
|
security: [
|
|
@@ -3564,9 +3580,14 @@ var ApiClient = class {
|
|
|
3564
3580
|
})
|
|
3565
3581
|
);
|
|
3566
3582
|
}
|
|
3567
|
-
async listRuns(project, limit) {
|
|
3583
|
+
async listRuns(project, limit, kind) {
|
|
3568
3584
|
return this.invoke(
|
|
3569
|
-
() => getApiV1ProjectsByNameRuns({
|
|
3585
|
+
() => getApiV1ProjectsByNameRuns({
|
|
3586
|
+
client: this.heyClient,
|
|
3587
|
+
path: { name: project },
|
|
3588
|
+
// kind arrives as a free CLI string; the server validates it against the enum.
|
|
3589
|
+
query: { limit, kind }
|
|
3590
|
+
})
|
|
3570
3591
|
);
|
|
3571
3592
|
}
|
|
3572
3593
|
async getLatestRun(project) {
|
|
@@ -4105,6 +4126,15 @@ var ApiClient = class {
|
|
|
4105
4126
|
() => getApiV1ProjectsByNameTrafficSources({ client: this.heyClient, path: { name: project } })
|
|
4106
4127
|
);
|
|
4107
4128
|
}
|
|
4129
|
+
async trafficReset(project, sourceId) {
|
|
4130
|
+
return this.invoke(
|
|
4131
|
+
() => postApiV1ProjectsByNameTrafficSourcesByIdReset({
|
|
4132
|
+
client: this.heyClient,
|
|
4133
|
+
path: { name: project, id: sourceId },
|
|
4134
|
+
body: { advanceToNow: true }
|
|
4135
|
+
})
|
|
4136
|
+
);
|
|
4137
|
+
}
|
|
4108
4138
|
async trafficStatus(project) {
|
|
4109
4139
|
return this.invoke(
|
|
4110
4140
|
() => getApiV1ProjectsByNameTrafficStatus({ client: this.heyClient, path: { name: project } })
|
|
@@ -4709,6 +4739,11 @@ var trafficBackfillInputSchema = z2.object({
|
|
|
4709
4739
|
sourceId: z2.string().min(1).describe("Traffic source ID returned by canonry_traffic_sources_list."),
|
|
4710
4740
|
days: z2.number().int().positive().max(30).optional().describe("Lookback window in days. Default 30, capped server-side at the upstream log retention ceiling (Cloud Logging _Default = 30d).")
|
|
4711
4741
|
});
|
|
4742
|
+
var trafficResetInputSchema = z2.object({
|
|
4743
|
+
project: projectNameSchema,
|
|
4744
|
+
sourceId: z2.string().min(1).describe("Traffic source ID returned by canonry_traffic_sources_list."),
|
|
4745
|
+
advanceToNow: z2.literal(true).describe("Must be `true`. Explicit gate against accidental resets. Advances lastSyncedAt to NOW and clears the source's error state.")
|
|
4746
|
+
});
|
|
4712
4747
|
var trafficEventsInputSchema = z2.object({
|
|
4713
4748
|
project: projectNameSchema,
|
|
4714
4749
|
since: z2.string().optional().describe("ISO 8601 lower bound. Defaults to 24h ago when omitted."),
|
|
@@ -5390,6 +5425,17 @@ var canonryMcpTools = [
|
|
|
5390
5425
|
openApiOperations: ["POST /api/v1/projects/{name}/traffic/sources/{id}/backfill"],
|
|
5391
5426
|
handler: (client2, input) => client2.trafficBackfill(input.project, input.sourceId, input.days !== void 0 ? { days: input.days } : void 0)
|
|
5392
5427
|
}),
|
|
5428
|
+
defineTool({
|
|
5429
|
+
name: "canonry_traffic_reset",
|
|
5430
|
+
title: "Advance traffic source lastSyncedAt to NOW",
|
|
5431
|
+
description: "Operator recovery for a stuck traffic source. Advances `lastSyncedAt` to NOW, sets `status` back to `connected`, and clears `last_error`. Accepts any non-archived source: the `lastSyncedAt` advance determines the next sync window for time-windowed sources (Vercel, Cloud Run); cursor-based sources (WordPress) keep their `last_cursor` so the advance is informational. Common trigger: an idle Vercel/Cloud Run source whose `lastSyncedAt` aged past the upstream retention boundary and every sync now throws a retention error. Historical events in the gap are unrecoverable from the sync path; run canonry_traffic_backfill separately if any of them are needed. Archived sources are rejected \u2014 re-connect via the appropriate canonry_traffic_connect_* tool instead.",
|
|
5432
|
+
access: "write",
|
|
5433
|
+
tier: "traffic",
|
|
5434
|
+
inputSchema: trafficResetInputSchema,
|
|
5435
|
+
annotations: writeAnnotations({ idempotentHint: true, destructiveHint: true }),
|
|
5436
|
+
openApiOperations: ["POST /api/v1/projects/{name}/traffic/sources/{id}/reset"],
|
|
5437
|
+
handler: (client2, input) => client2.trafficReset(input.project, input.sourceId)
|
|
5438
|
+
}),
|
|
5393
5439
|
defineTool({
|
|
5394
5440
|
name: "canonry_project_upsert",
|
|
5395
5441
|
title: "Create or replace project",
|