@ainyc/canonry 4.30.0 → 4.31.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.
Files changed (25) hide show
  1. package/README.md +15 -13
  2. package/assets/agent-workspace/skills/aero/SKILL.md +2 -2
  3. package/assets/agent-workspace/skills/aero/references/aeo-discovery.md +26 -17
  4. package/assets/agent-workspace/skills/aero/references/memory-patterns.md +9 -9
  5. package/assets/agent-workspace/skills/aero/references/orchestration.md +6 -6
  6. package/assets/agent-workspace/skills/aero/references/reporting.md +3 -3
  7. package/assets/agent-workspace/skills/canonry/SKILL.md +5 -3
  8. package/assets/agent-workspace/skills/canonry/references/aeo-analysis.md +9 -9
  9. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +203 -200
  10. package/assets/agent-workspace/skills/canonry/references/indexing.md +35 -35
  11. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +18 -18
  12. package/assets/agent-workspace/skills/canonry/references/wordpress-integration.md +11 -11
  13. package/assets/assets/index-C4UBTDDS.js +302 -0
  14. package/assets/assets/{index-BnALDZI7.css → index-CNKAwZMB.css} +1 -1
  15. package/assets/index.html +2 -2
  16. package/dist/{chunk-NIAAHWRF.js → chunk-5STLZRGB.js} +5 -3
  17. package/dist/{chunk-7UO3EGDB.js → chunk-HTNC6AWN.js} +24 -2
  18. package/dist/{chunk-PTFVEYUX.js → chunk-PUTJHEVR.js} +130 -13
  19. package/dist/{chunk-4EDC2P3J.js → chunk-U3YKRV47.js} +1 -1
  20. package/dist/cli.js +55 -14
  21. package/dist/index.js +4 -4
  22. package/dist/{intelligence-service-ASXADXLF.js → intelligence-service-CJONZ7ST.js} +2 -2
  23. package/dist/mcp.js +2 -2
  24. package/package.json +8 -7
  25. package/assets/assets/index-BYiZYtd9.js +0 -302
package/assets/index.html CHANGED
@@ -12,8 +12,8 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-BYiZYtd9.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-BnALDZI7.css">
15
+ <script type="module" crossorigin src="./assets/index-C4UBTDDS.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-CNKAwZMB.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -4,6 +4,7 @@ import {
4
4
  DISCOVERY_MAX_PROBES_CAP,
5
5
  competitorBatchRequestSchema,
6
6
  discoveryBucketSchema,
7
+ discoveryCompetitorTypeSchema,
7
8
  discoveryPromoteRequestSchema,
8
9
  discoveryRunRequestSchema,
9
10
  keywordBatchRequestSchema,
@@ -20,7 +21,7 @@ import {
20
21
  trafficConnectCloudRunRequestSchema,
21
22
  trafficConnectWordpressRequestSchema,
22
23
  trafficEventKindSchema
23
- } from "./chunk-7UO3EGDB.js";
24
+ } from "./chunk-HTNC6AWN.js";
24
25
 
25
26
  // src/config.ts
26
27
  import fs from "fs";
@@ -1287,7 +1288,8 @@ var discoveryPromoteInputSchema = z2.object({
1287
1288
  request: discoveryPromoteRequestSchema.extend({
1288
1289
  // Stronger descriptions for the LLM. The base Zod schema enforces the shape.
1289
1290
  buckets: z2.array(discoveryBucketSchema).min(1).optional().describe("Which probe buckets to adopt into the tracked basket. Omitted promotes cited + aspirational; include wasted-surface explicitly for off-ICP competitor gaps."),
1290
- includeCompetitors: z2.boolean().optional().describe("Whether to also merge recurring discovered competitor domains into the project. Defaults to true.")
1291
+ includeCompetitors: z2.boolean().optional().describe("Whether to also merge recurring discovered competitor domains into the project. Defaults to true."),
1292
+ competitorTypes: z2.array(discoveryCompetitorTypeSchema).min(1).optional().describe("Which classified competitor types to merge. Omitted promotes direct-competitor only; pass an explicit list to also adopt editorial-media channels or to recover legacy unknown entries. Ignored when includeCompetitors is false.")
1291
1293
  }).optional()
1292
1294
  });
1293
1295
  var AGENT_WEBHOOK_EVENTS = [
@@ -2242,7 +2244,7 @@ var canonryMcpTools = [
2242
2244
  defineTool({
2243
2245
  name: "canonry_discover_promote",
2244
2246
  title: "Promote discovery session",
2245
- description: 'Adopt a completed discovery session\'s bucketed queries into the project\'s tracked basket, tagged with provenance "discovery:<sessionId>". By default, only cited + aspirational queries are promoted; include wasted-surface explicitly when off-ICP competitor gaps should also be tracked. Recurring discovered competitor domains are also merged by default. Add-only and idempotent: queries/domains already tracked are returned under `skipped`, never inserted twice. Only sessions with status "completed" can be promoted. Call canonry_discover_promote_preview first to inspect candidates.',
2247
+ description: 'Adopt a completed discovery session\'s bucketed queries into the project\'s tracked basket, tagged with provenance "discovery:<sessionId>". By default, only cited + aspirational queries are promoted; include wasted-surface explicitly when off-ICP competitor gaps should also be tracked. Recurring discovered competitor domains classified as direct-competitor are also merged by default \u2014 pass request.competitorTypes to adopt editorial-media channels or recover legacy unknown entries. Add-only and idempotent: queries/domains already tracked are returned under `skipped`, never inserted twice. Only sessions with status "completed" can be promoted. Call canonry_discover_promote_preview first to inspect candidates.',
2246
2248
  access: "write",
2247
2249
  tier: "discovery",
2248
2250
  inputSchema: discoveryPromoteInputSchema,
@@ -2382,11 +2382,29 @@ var DEFAULT_DISCOVERY_PROMOTE_BUCKETS = [
2382
2382
  ];
2383
2383
  var DISCOVERY_PROMOTE_COMPETITOR_CAP = 20;
2384
2384
  var DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS = 2;
2385
+ var discoveryCompetitorTypeSchema = z21.enum([
2386
+ "direct-competitor",
2387
+ "ota-aggregator",
2388
+ "editorial-media",
2389
+ "other",
2390
+ "unknown"
2391
+ ]);
2392
+ var DiscoveryCompetitorTypes = discoveryCompetitorTypeSchema.enum;
2393
+ var DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES = [
2394
+ DiscoveryCompetitorTypes["direct-competitor"]
2395
+ ];
2385
2396
  var discoverySessionStatusSchema = z21.enum(["queued", "seeding", "probing", "completed", "failed"]);
2386
2397
  var DiscoverySessionStatuses = discoverySessionStatusSchema.enum;
2387
2398
  var discoveryCompetitorMapEntrySchema = z21.object({
2388
2399
  domain: z21.string().min(1),
2389
- hits: z21.number().int().positive()
2400
+ hits: z21.number().int().positive(),
2401
+ /**
2402
+ * Domain classification from the session's post-probe AI classification
2403
+ * pass. Defaults to `unknown` so competitor maps persisted before
2404
+ * classification existed (or by a session whose classification call failed)
2405
+ * still parse — those entries are excluded from the default promote filter.
2406
+ */
2407
+ competitorType: discoveryCompetitorTypeSchema.default("unknown")
2390
2408
  });
2391
2409
  var discoveryProbeDtoSchema = z21.object({
2392
2410
  id: z21.string(),
@@ -2428,7 +2446,8 @@ var discoveryRunRequestSchema = z21.object({
2428
2446
  });
2429
2447
  var discoveryPromoteRequestSchema = z21.object({
2430
2448
  buckets: z21.array(discoveryBucketSchema).min(1).optional(),
2431
- includeCompetitors: z21.boolean().optional()
2449
+ includeCompetitors: z21.boolean().optional(),
2450
+ competitorTypes: z21.array(discoveryCompetitorTypeSchema).min(1).optional()
2432
2451
  });
2433
2452
  var discoveryPromotePreviewSchema = z21.object({
2434
2453
  sessionId: z21.string(),
@@ -2693,6 +2712,9 @@ export {
2693
2712
  DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
2694
2713
  DISCOVERY_PROMOTE_COMPETITOR_CAP,
2695
2714
  DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
2715
+ discoveryCompetitorTypeSchema,
2716
+ DiscoveryCompetitorTypes,
2717
+ DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
2696
2718
  DiscoverySessionStatuses,
2697
2719
  DISCOVERY_MAX_PROBES_CAP,
2698
2720
  discoveryRunRequestSchema,
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-NIAAHWRF.js";
8
+ } from "./chunk-5STLZRGB.js";
9
9
  import {
10
10
  DEFAULT_RUN_HISTORY_LIMIT,
11
11
  IntelligenceService,
@@ -70,7 +70,7 @@ import {
70
70
  schedules,
71
71
  trafficSources,
72
72
  usageCounters
73
- } from "./chunk-4EDC2P3J.js";
73
+ } from "./chunk-U3YKRV47.js";
74
74
  import {
75
75
  AGENT_MEMORY_VALUE_MAX_BYTES,
76
76
  AGENT_PROVIDER_IDS,
@@ -82,9 +82,11 @@ import {
82
82
  CheckStatuses,
83
83
  CitationStates,
84
84
  DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
85
+ DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
85
86
  DISCOVERY_PROMOTE_COMPETITOR_CAP,
86
87
  DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
87
88
  DiscoveryBuckets,
89
+ DiscoveryCompetitorTypes,
88
90
  DiscoverySessionStatuses,
89
91
  MemorySources,
90
92
  RunKinds,
@@ -175,7 +177,7 @@ import {
175
177
  visibilityStateFromAnswerMentioned,
176
178
  windowCutoff,
177
179
  wordpressEnvSchema
178
- } from "./chunk-7UO3EGDB.js";
180
+ } from "./chunk-HTNC6AWN.js";
179
181
 
180
182
  // src/telemetry.ts
181
183
  import crypto from "crypto";
@@ -10425,7 +10427,7 @@ var routeCatalog = [
10425
10427
  method: "post",
10426
10428
  path: "/api/v1/projects/{name}/discover/sessions/{id}/promote",
10427
10429
  summary: "Promote a discovery session into the tracked basket",
10428
- description: 'Adopts a completed session\'s bucketed queries into the project\'s tracked basket, tagged with `provenance="discovery:<sessionId>"`. By default, only `cited` and `aspirational` queries are promoted; include `wasted-surface` explicitly when off-ICP competitor gaps should also be tracked. Recurring discovered competitor domains are also merged by default. Add-only and idempotent: queries/domains already tracked are returned under `skipped` rather than inserted twice. Only sessions with `status: "completed"` can be promoted.',
10430
+ description: 'Adopts a completed session\'s bucketed queries into the project\'s tracked basket, tagged with `provenance="discovery:<sessionId>"`. By default, only `cited` and `aspirational` queries are promoted; include `wasted-surface` explicitly when off-ICP competitor gaps should also be tracked. Recurring discovered competitor domains classified as `direct-competitor` are also merged by default \u2014 pass `competitorTypes` to adopt other classified types or to recover legacy `unknown` entries. Add-only and idempotent: queries/domains already tracked are returned under `skipped` rather than inserted twice. Only sessions with `status: "completed"` can be promoted.',
10429
10431
  tags: ["discovery"],
10430
10432
  parameters: [
10431
10433
  nameParameter,
@@ -10446,6 +10448,14 @@ var routeCatalog = [
10446
10448
  includeCompetitors: {
10447
10449
  type: "boolean",
10448
10450
  description: "Whether to also merge recurring discovered competitor domains. Defaults to true."
10451
+ },
10452
+ competitorTypes: {
10453
+ type: "array",
10454
+ items: {
10455
+ type: "string",
10456
+ enum: ["direct-competitor", "ota-aggregator", "editorial-media", "other", "unknown"]
10457
+ },
10458
+ description: "Which classified competitor types to merge. Omitted means direct-competitor only. Ignored when includeCompetitors is false."
10449
10459
  }
10450
10460
  }
10451
10461
  }
@@ -19725,7 +19735,7 @@ async function discoveryRoutes(app, opts) {
19725
19735
  else if (bucket === DiscoveryBuckets.aspirational) aspirational.add(probe.query);
19726
19736
  else if (bucket === DiscoveryBuckets["wasted-surface"]) wasted.add(probe.query);
19727
19737
  }
19728
- const competitorMap = parseJsonColumn(session.competitorMap, []);
19738
+ const competitorMap = parseCompetitorMap(session.competitorMap);
19729
19739
  const newCompetitors = selectEligibleCompetitors(competitorMap).filter((entry) => !seenCompetitors.has(entry.domain.toLowerCase()));
19730
19740
  return reply.send({
19731
19741
  sessionId: session.id,
@@ -19764,6 +19774,7 @@ async function discoveryRoutes(app, opts) {
19764
19774
  const buckets = parsed.data.buckets ?? DEFAULT_DISCOVERY_PROMOTE_BUCKETS;
19765
19775
  const bucketSet = new Set(buckets);
19766
19776
  const includeCompetitors = parsed.data.includeCompetitors ?? true;
19777
+ const competitorTypes = parsed.data.competitorTypes ?? DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES;
19767
19778
  const probeRows = app.db.select().from(discoveryProbes).where(eq25(discoveryProbes.sessionId, session.id)).all();
19768
19779
  const candidateQueries = /* @__PURE__ */ new Set();
19769
19780
  for (const probe of probeRows) {
@@ -19790,8 +19801,8 @@ async function discoveryRoutes(app, opts) {
19790
19801
  const existingCompetitors = new Set(
19791
19802
  app.db.select({ domain: competitors.domain }).from(competitors).where(eq25(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
19792
19803
  );
19793
- const competitorMap = parseJsonColumn(session.competitorMap, []);
19794
- for (const entry of selectEligibleCompetitors(competitorMap)) {
19804
+ const competitorMap = parseCompetitorMap(session.competitorMap);
19805
+ for (const entry of selectEligibleCompetitors(competitorMap, competitorTypes)) {
19795
19806
  const key = entry.domain.toLowerCase();
19796
19807
  if (existingCompetitors.has(key)) {
19797
19808
  skippedCompetitors.push(entry.domain);
@@ -19856,7 +19867,7 @@ function serializeSession(row) {
19856
19867
  citedCount: row.citedCount ?? null,
19857
19868
  aspirationalCount: row.aspirationalCount ?? null,
19858
19869
  wastedCount: row.wastedCount ?? null,
19859
- competitorMap: parseJsonColumn(row.competitorMap, []),
19870
+ competitorMap: parseCompetitorMap(row.competitorMap),
19860
19871
  error: row.error ?? null,
19861
19872
  startedAt: row.startedAt ?? null,
19862
19873
  finishedAt: row.finishedAt ?? null,
@@ -19877,8 +19888,20 @@ function serializeProbe(row) {
19877
19888
  createdAt: row.createdAt
19878
19889
  };
19879
19890
  }
19880
- function selectEligibleCompetitors(competitorMap) {
19881
- return competitorMap.filter((entry) => entry.hits >= DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS).sort((a, b) => b.hits - a.hits || a.domain.localeCompare(b.domain)).slice(0, DISCOVERY_PROMOTE_COMPETITOR_CAP);
19891
+ function parseCompetitorMap(json) {
19892
+ const raw = parseJsonColumn(
19893
+ json,
19894
+ []
19895
+ );
19896
+ return raw.map((entry) => ({
19897
+ domain: entry.domain,
19898
+ hits: entry.hits,
19899
+ competitorType: entry.competitorType ?? DiscoveryCompetitorTypes.unknown
19900
+ }));
19901
+ }
19902
+ function selectEligibleCompetitors(competitorMap, competitorTypes) {
19903
+ const typeFilter = competitorTypes ? new Set(competitorTypes) : null;
19904
+ return competitorMap.filter((entry) => entry.hits >= DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS).filter((entry) => !typeFilter || typeFilter.has(entry.competitorType)).sort((a, b) => b.hits - a.hits || a.domain.localeCompare(b.domain)).slice(0, DISCOVERY_PROMOTE_COMPETITOR_CAP);
19882
19905
  }
19883
19906
 
19884
19907
  // ../api-routes/src/discovery/orchestrate.ts
@@ -19895,7 +19918,7 @@ function classifyProbeBucket(input) {
19895
19918
  if (competitorHit) return DiscoveryBuckets["wasted-surface"];
19896
19919
  return DiscoveryBuckets.aspirational;
19897
19920
  }
19898
- function buildCompetitorMap(probes, project) {
19921
+ function buildCompetitorMap(probes, project, classification = {}) {
19899
19922
  const canonical = new Set(project.canonicalDomains.map((d) => d.toLowerCase()));
19900
19923
  const counts = /* @__PURE__ */ new Map();
19901
19924
  for (const probe of probes) {
@@ -19908,7 +19931,19 @@ function buildCompetitorMap(probes, project) {
19908
19931
  counts.set(domain, (counts.get(domain) ?? 0) + 1);
19909
19932
  }
19910
19933
  }
19911
- return Array.from(counts.entries()).map(([domain, hits]) => ({ domain, hits })).sort((a, b) => b.hits - a.hits || a.domain.localeCompare(b.domain));
19934
+ return Array.from(counts.entries()).map(([domain, hits]) => ({
19935
+ domain,
19936
+ hits,
19937
+ competitorType: classification[domain] ?? DiscoveryCompetitorTypes.unknown
19938
+ })).sort((a, b) => b.hits - a.hits || a.domain.localeCompare(b.domain));
19939
+ }
19940
+ async function classifyCompetitorDomains(deps, project, icpDescription, domains) {
19941
+ if (domains.length === 0) return {};
19942
+ try {
19943
+ return await deps.classifyDomains({ project, icpDescription, domains });
19944
+ } catch {
19945
+ return {};
19946
+ }
19912
19947
  }
19913
19948
  async function pickCanonicals(candidates, deps, dedupThreshold) {
19914
19949
  if (candidates.length === 0) return [];
@@ -19969,7 +20004,14 @@ async function executeDiscovery(opts) {
19969
20004
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
19970
20005
  }).run();
19971
20006
  }
19972
- const competitorMap = buildCompetitorMap(probeRows, opts.project);
20007
+ const domains = buildCompetitorMap(probeRows, opts.project).map((entry) => entry.domain);
20008
+ const classification = await classifyCompetitorDomains(
20009
+ opts.deps,
20010
+ opts.project,
20011
+ opts.icpDescription,
20012
+ domains
20013
+ );
20014
+ const competitorMap = buildCompetitorMap(probeRows, opts.project, classification);
19973
20015
  opts.db.update(discoverySessions).set({
19974
20016
  status: DiscoverySessionStatuses.completed,
19975
20017
  probeCount: probedCanonicals.length,
@@ -24560,9 +24602,84 @@ function buildDefaultDeps(registry) {
24560
24602
  citedDomains: normalized.citedDomains,
24561
24603
  rawResponse: raw.rawResponse
24562
24604
  };
24605
+ },
24606
+ async classifyDomains(input) {
24607
+ const prompt = buildClassificationPrompt(input);
24608
+ const text = await adapter.generateText(prompt, cfg);
24609
+ return parseClassificationResponse(text, input.domains);
24563
24610
  }
24564
24611
  };
24565
24612
  }
24613
+ var CLASSIFICATION_CATEGORIES = [
24614
+ DiscoveryCompetitorTypes["direct-competitor"],
24615
+ DiscoveryCompetitorTypes["ota-aggregator"],
24616
+ DiscoveryCompetitorTypes["editorial-media"],
24617
+ DiscoveryCompetitorTypes.other
24618
+ ];
24619
+ var CLASSIFICATION_CATEGORY_MATCHERS = CLASSIFICATION_CATEGORIES.map((category) => ({
24620
+ category,
24621
+ pattern: new RegExp(`(?<![a-z0-9])${category}(?![a-z0-9])`)
24622
+ }));
24623
+ function buildClassificationPrompt(input) {
24624
+ const tracked = input.project.competitorDomains.length > 0 ? input.project.competitorDomains.join(", ") : "none";
24625
+ return [
24626
+ "You are an AEO (Answer Engine Optimization) analyst classifying the domains that AI answer engines cited for a customer's tracked queries.",
24627
+ "",
24628
+ `Customer: ${input.project.name} (own domains: ${input.project.canonicalDomains.join(", ")})`,
24629
+ `ICP: ${input.icpDescription}`,
24630
+ `Already-tracked competitors: ${tracked}`,
24631
+ "",
24632
+ "Classify EACH domain below into exactly one category:",
24633
+ " - direct-competitor: a business competing directly with the customer for the same customers (another company in the same category). Every already-tracked competitor above is a direct-competitor.",
24634
+ " - ota-aggregator: online travel agencies, marketplaces, directories, booking platforms, or review aggregators that list many businesses (e.g. expedia.com, booking.com, tripadvisor.com, yelp.com, g2.com).",
24635
+ ' - editorial-media: news sites, magazines, blogs, or "best of" listicle / round-up articles (e.g. timeout.com, nytimes.com, personal blogs).',
24636
+ " - other: anything else \u2014 government sites, social media, the customer itself, or domains unrelated to the competitive space.",
24637
+ "",
24638
+ "Domains:",
24639
+ ...input.domains,
24640
+ "",
24641
+ "Return ONE line per domain in EXACTLY this format:",
24642
+ "<domain> => <category>",
24643
+ "",
24644
+ "Plain text only. No numbering, bullets, commentary, or markdown."
24645
+ ].join("\n");
24646
+ }
24647
+ function parseClassificationResponse(text, domains) {
24648
+ const lines = text.split("\n").map((l) => l.trim().toLowerCase()).filter(Boolean);
24649
+ const result = {};
24650
+ for (const domain of domains) {
24651
+ const key = domain.toLowerCase();
24652
+ const line = lines.find((l) => startsWithDomainToken(l, key)) ?? lines.find((l) => containsDomainToken(l, key));
24653
+ if (!line) continue;
24654
+ const category = extractClassificationCategory(line);
24655
+ if (category) result[domain] = category;
24656
+ }
24657
+ return result;
24658
+ }
24659
+ function isDomainChar(ch) {
24660
+ return /[a-z0-9.-]/.test(ch);
24661
+ }
24662
+ function startsWithDomainToken(line, domain) {
24663
+ return line.startsWith(domain) && !isDomainChar(line[domain.length] ?? "");
24664
+ }
24665
+ function containsDomainToken(line, domain) {
24666
+ let idx = line.indexOf(domain);
24667
+ while (idx !== -1) {
24668
+ const before = line[idx - 1] ?? "";
24669
+ const after = line[idx + domain.length] ?? "";
24670
+ if (!isDomainChar(before) && !isDomainChar(after)) return true;
24671
+ idx = line.indexOf(domain, idx + 1);
24672
+ }
24673
+ return false;
24674
+ }
24675
+ function extractClassificationCategory(line) {
24676
+ const arrowIdx = line.indexOf("=>");
24677
+ const haystack = arrowIdx >= 0 ? line.slice(arrowIdx + 2) : line;
24678
+ for (const { category, pattern } of CLASSIFICATION_CATEGORY_MATCHERS) {
24679
+ if (pattern.test(haystack)) return category;
24680
+ }
24681
+ return null;
24682
+ }
24566
24683
  function buildSeedPrompt(input) {
24567
24684
  return [
24568
24685
  "You are an AEO (Answer Engine Optimization) analyst expanding a tracked-query basket for a customer.",
@@ -8,7 +8,7 @@ import {
8
8
  categoryLabel,
9
9
  determineAnswerMentioned,
10
10
  normalizeProjectDomain
11
- } from "./chunk-7UO3EGDB.js";
11
+ } from "./chunk-HTNC6AWN.js";
12
12
 
13
13
  // src/intelligence-service.ts
14
14
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";
package/dist/cli.js CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  setTelemetrySource,
21
21
  showFirstRunNotice,
22
22
  trackEvent
23
- } from "./chunk-PTFVEYUX.js";
23
+ } from "./chunk-PUTJHEVR.js";
24
24
  import {
25
25
  CliError,
26
26
  EXIT_SYSTEM_ERROR,
@@ -36,7 +36,7 @@ import {
36
36
  saveConfig,
37
37
  saveConfigPatch,
38
38
  usageError
39
- } from "./chunk-NIAAHWRF.js";
39
+ } from "./chunk-5STLZRGB.js";
40
40
  import {
41
41
  apiKeys,
42
42
  competitors,
@@ -49,7 +49,7 @@ import {
49
49
  queries,
50
50
  querySnapshots,
51
51
  runs
52
- } from "./chunk-4EDC2P3J.js";
52
+ } from "./chunk-U3YKRV47.js";
53
53
  import {
54
54
  CcReleaseSyncStatuses,
55
55
  CheckScopes,
@@ -63,6 +63,7 @@ import {
63
63
  TrafficEventKinds,
64
64
  determineAnswerMentioned,
65
65
  discoveryBucketSchema,
66
+ discoveryCompetitorTypeSchema,
66
67
  effectiveDomains,
67
68
  formatRunErrorOneLine,
68
69
  normalizeUrlPath,
@@ -70,7 +71,7 @@ import {
70
71
  providerQuotaPolicySchema,
71
72
  resolveProviderInput,
72
73
  skillsClientSchema
73
- } from "./chunk-7UO3EGDB.js";
74
+ } from "./chunk-HTNC6AWN.js";
74
75
 
75
76
  // src/cli.ts
76
77
  import { pathToFileURL } from "url";
@@ -622,7 +623,7 @@ function readStoredGroundingSources(rawResponse) {
622
623
  return result;
623
624
  }
624
625
  async function backfillInsightsCommand(project, opts) {
625
- const { IntelligenceService } = await import("./intelligence-service-ASXADXLF.js");
626
+ const { IntelligenceService } = await import("./intelligence-service-CJONZ7ST.js");
626
627
  const config = loadConfig();
627
628
  const db = createClient(config.database);
628
629
  migrate(db);
@@ -2142,7 +2143,10 @@ async function discoverPromotePreview(project, sessionId, opts) {
2142
2143
  for (const q of preview.queriesByBucket.aspirational.slice(0, 10)) console.log(` + ${q}`);
2143
2144
  if (preview.suggestedCompetitors.length > 0) {
2144
2145
  console.log(` Suggested new competitors:`);
2145
- for (const c of preview.suggestedCompetitors) console.log(` - ${c.domain} (${c.hits} hits)`);
2146
+ for (const c of preview.suggestedCompetitors) {
2147
+ console.log(` - ${c.domain} (${c.hits} hits, ${c.competitorType})`);
2148
+ }
2149
+ console.log(" Only direct-competitor is promoted by default \u2014 pass --competitor-types to include other types.");
2146
2150
  }
2147
2151
  console.log(`
2148
2152
  Run \`canonry discover promote ${project} ${sessionId}\` to merge cited + aspirational queries.`);
@@ -2152,6 +2156,7 @@ async function discoverPromote(project, sessionId, opts) {
2152
2156
  const client = getClient4();
2153
2157
  const body = {};
2154
2158
  if (opts.buckets && opts.buckets.length > 0) body.buckets = opts.buckets;
2159
+ if (opts.competitorTypes && opts.competitorTypes.length > 0) body.competitorTypes = opts.competitorTypes;
2155
2160
  if (opts.includeCompetitors === false) body.includeCompetitors = false;
2156
2161
  const result = await client.promoteDiscovery(project, sessionId, body);
2157
2162
  if (opts.format === "json") {
@@ -2181,7 +2186,9 @@ function printSessionDetail(session) {
2181
2186
  console.log(` Buckets: cited=${session.citedCount ?? 0} wasted-surface=${session.wastedCount ?? 0} aspirational=${session.aspirationalCount ?? 0}`);
2182
2187
  if (session.competitorMap.length > 0) {
2183
2188
  console.log(` Top recurring competitor domains:`);
2184
- for (const c of session.competitorMap.slice(0, 10)) console.log(` - ${c.domain} (${c.hits} hits)`);
2189
+ for (const c of session.competitorMap.slice(0, 10)) {
2190
+ console.log(` - ${c.domain} (${c.hits} hits, ${c.competitorType})`);
2191
+ }
2185
2192
  }
2186
2193
  if (session.error) console.log(` Error: ${session.error}`);
2187
2194
  if (session.startedAt) console.log(` Started: ${session.startedAt}`);
@@ -2265,6 +2272,38 @@ Usage: ${usage}`,
2265
2272
  }
2266
2273
  return buckets;
2267
2274
  }
2275
+ var COMPETITOR_TYPE_VALUES = "direct-competitor, ota-aggregator, editorial-media, other, unknown";
2276
+ function parseCompetitorTypesOption(values, usage) {
2277
+ const raw = getStringArray(values, "competitor-types");
2278
+ if (!raw || raw.length === 0) return void 0;
2279
+ const expanded = raw.flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
2280
+ if (expanded.length === 0) {
2281
+ throw usageError(
2282
+ `Error: --competitor-types must include at least one value (valid: ${COMPETITOR_TYPE_VALUES})
2283
+ Usage: ${usage}`,
2284
+ {
2285
+ message: "--competitor-types must include at least one value",
2286
+ details: { command: "discover.promote", usage, option: "competitor-types", value: raw }
2287
+ }
2288
+ );
2289
+ }
2290
+ const types = [];
2291
+ for (const value of expanded) {
2292
+ const parsed = discoveryCompetitorTypeSchema.safeParse(value);
2293
+ if (!parsed.success) {
2294
+ throw usageError(
2295
+ `Error: invalid --competitor-types value "${value}" (valid: ${COMPETITOR_TYPE_VALUES})
2296
+ Usage: ${usage}`,
2297
+ {
2298
+ message: `invalid --competitor-types value "${value}"`,
2299
+ details: { command: "discover.promote", usage, option: "competitor-types", value }
2300
+ }
2301
+ );
2302
+ }
2303
+ types.push(parsed.data);
2304
+ }
2305
+ return types;
2306
+ }
2268
2307
  var DISCOVER_CLI_COMMANDS = [
2269
2308
  {
2270
2309
  path: ["discover", "run"],
@@ -2378,13 +2417,14 @@ var DISCOVER_CLI_COMMANDS = [
2378
2417
  },
2379
2418
  {
2380
2419
  path: ["discover", "promote"],
2381
- usage: "canonry discover promote <project> <session-id> [--bucket cited,aspirational,wasted-surface] [--no-competitors] [--format json]",
2420
+ usage: "canonry discover promote <project> <session-id> [--bucket cited,aspirational,wasted-surface] [--competitor-types direct-competitor,editorial-media] [--no-competitors] [--format json]",
2382
2421
  options: {
2383
2422
  bucket: multiStringOption(),
2423
+ "competitor-types": multiStringOption(),
2384
2424
  "no-competitors": { type: "boolean", default: false }
2385
2425
  },
2386
2426
  run: async (input) => {
2387
- const usage = "canonry discover promote <project> <session-id> [--bucket cited,aspirational,wasted-surface] [--no-competitors] [--format json]";
2427
+ const usage = "canonry discover promote <project> <session-id> [--bucket cited,aspirational,wasted-surface] [--competitor-types direct-competitor,editorial-media] [--no-competitors] [--format json]";
2388
2428
  const project = requireProject(input, "discover.promote", usage);
2389
2429
  const sessionId = requirePositional(input, 1, {
2390
2430
  command: "discover.promote",
@@ -2393,6 +2433,7 @@ var DISCOVER_CLI_COMMANDS = [
2393
2433
  });
2394
2434
  await discoverPromote(project, sessionId, {
2395
2435
  buckets: parseBucketsOption(input.values, usage),
2436
+ competitorTypes: parseCompetitorTypesOption(input.values, usage),
2396
2437
  includeCompetitors: !getBoolean(input.values, "no-competitors"),
2397
2438
  format: input.format
2398
2439
  });
@@ -11293,9 +11334,9 @@ var REGISTERED_CLI_COMMANDS = [
11293
11334
  // src/cli.ts
11294
11335
  import { createRequire as createRequire2 } from "module";
11295
11336
  var USAGE3 = `
11296
- canonry \u2014 AEO monitoring CLI
11337
+ cnry \u2014 AEO monitoring CLI ('canonry' also works)
11297
11338
 
11298
- Usage: canonry <command> [options]
11339
+ Usage: cnry <command> [options]
11299
11340
 
11300
11341
  Setup:
11301
11342
  init Initialize config and database
@@ -11343,7 +11384,7 @@ Global options:
11343
11384
  --help, -h Show help (use with any command group)
11344
11385
  --version, -v Show version
11345
11386
 
11346
- Run 'canonry <command> --help' for details on a specific command.
11387
+ Run 'cnry <command> --help' for details on a specific command.
11347
11388
  `.trim();
11348
11389
  var _require2 = createRequire2(import.meta.url);
11349
11390
  var { version: VERSION } = _require2("../package.json");
@@ -11394,11 +11435,11 @@ async function runCli(args = process.argv.slice(2)) {
11394
11435
  return 0;
11395
11436
  }
11396
11437
  throw usageError(`Error: unknown command: ${command}
11397
- Run "canonry --help" for usage.`, {
11438
+ Run "cnry --help" for usage.`, {
11398
11439
  message: `unknown command: ${command}`,
11399
11440
  details: {
11400
11441
  command,
11401
- usage: "canonry --help"
11442
+ usage: "cnry --help"
11402
11443
  }
11403
11444
  });
11404
11445
  } catch (err) {
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-PTFVEYUX.js";
3
+ } from "./chunk-PUTJHEVR.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-NIAAHWRF.js";
7
- import "./chunk-4EDC2P3J.js";
8
- import "./chunk-7UO3EGDB.js";
6
+ } from "./chunk-5STLZRGB.js";
7
+ import "./chunk-U3YKRV47.js";
8
+ import "./chunk-HTNC6AWN.js";
9
9
  export {
10
10
  createServer,
11
11
  loadConfig
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-4EDC2P3J.js";
4
- import "./chunk-7UO3EGDB.js";
3
+ } from "./chunk-U3YKRV47.js";
4
+ import "./chunk-HTNC6AWN.js";
5
5
  export {
6
6
  IntelligenceService
7
7
  };
package/dist/mcp.js CHANGED
@@ -2,8 +2,8 @@ import {
2
2
  CliError,
3
3
  canonryMcpTools,
4
4
  createApiClient
5
- } from "./chunk-NIAAHWRF.js";
6
- import "./chunk-7UO3EGDB.js";
5
+ } from "./chunk-5STLZRGB.js";
6
+ import "./chunk-HTNC6AWN.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": "4.30.0",
3
+ "version": "4.31.0",
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",
@@ -15,6 +15,7 @@
15
15
  },
16
16
  "bin": {
17
17
  "canonry": "./bin/canonry.mjs",
18
+ "cnry": "./bin/canonry.mjs",
18
19
  "canonry-mcp": "./bin/canonry-mcp.mjs"
19
20
  },
20
21
  "exports": {
@@ -62,20 +63,20 @@
62
63
  "@ainyc/canonry-api-routes": "0.0.0",
63
64
  "@ainyc/canonry-config": "0.0.0",
64
65
  "@ainyc/canonry-contracts": "0.0.0",
65
- "@ainyc/canonry-db": "0.0.0",
66
- "@ainyc/canonry-integration-bing": "0.0.0",
67
66
  "@ainyc/canonry-intelligence": "0.0.0",
68
- "@ainyc/canonry-integration-commoncrawl": "0.0.0",
67
+ "@ainyc/canonry-integration-bing": "0.0.0",
68
+ "@ainyc/canonry-integration-cloud-run": "0.0.0",
69
+ "@ainyc/canonry-db": "0.0.0",
69
70
  "@ainyc/canonry-integration-google": "0.0.0",
70
71
  "@ainyc/canonry-integration-traffic": "0.0.0",
71
- "@ainyc/canonry-integration-cloud-run": "0.0.0",
72
+ "@ainyc/canonry-integration-commoncrawl": "0.0.0",
72
73
  "@ainyc/canonry-integration-wordpress": "0.0.0",
73
- "@ainyc/canonry-provider-claude": "0.0.0",
74
74
  "@ainyc/canonry-provider-cdp": "0.0.0",
75
75
  "@ainyc/canonry-provider-local": "0.0.0",
76
76
  "@ainyc/canonry-provider-openai": "0.0.0",
77
+ "@ainyc/canonry-provider-gemini": "0.0.0",
77
78
  "@ainyc/canonry-provider-perplexity": "0.0.0",
78
- "@ainyc/canonry-provider-gemini": "0.0.0"
79
+ "@ainyc/canonry-provider-claude": "0.0.0"
79
80
  },
80
81
  "scripts": {
81
82
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",