@ainyc/canonry 4.27.2 → 4.29.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/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-BWjq1HP1.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-rPok6yk8.css">
15
+ <script type="module" crossorigin src="./assets/index-DC2S5T9p.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-BnALDZI7.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -3,6 +3,8 @@ import {
3
3
  AGENT_MEMORY_VALUE_MAX_BYTES,
4
4
  DISCOVERY_MAX_PROBES_CAP,
5
5
  competitorBatchRequestSchema,
6
+ discoveryBucketSchema,
7
+ discoveryPromoteRequestSchema,
6
8
  discoveryRunRequestSchema,
7
9
  keywordBatchRequestSchema,
8
10
  keywordGenerateRequestSchema,
@@ -18,7 +20,7 @@ import {
18
20
  trafficConnectCloudRunRequestSchema,
19
21
  trafficConnectWordpressRequestSchema,
20
22
  trafficEventKindSchema
21
- } from "./chunk-HVW665A4.js";
23
+ } from "./chunk-RLLFB3M3.js";
22
24
 
23
25
  // src/config.ts
24
26
  import fs from "fs";
@@ -847,6 +849,13 @@ var ApiClient = class {
847
849
  `/projects/${encodeURIComponent(project)}/discover/sessions/${encodeURIComponent(sessionId)}/promote`
848
850
  );
849
851
  }
852
+ async promoteDiscovery(project, sessionId, body) {
853
+ return this.request(
854
+ "POST",
855
+ `/projects/${encodeURIComponent(project)}/discover/sessions/${encodeURIComponent(sessionId)}/promote`,
856
+ body ?? {}
857
+ );
858
+ }
850
859
  async wordpressConnect(project, body) {
851
860
  return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/connect`, body);
852
861
  }
@@ -1272,6 +1281,15 @@ var discoverySessionIdInputSchema = z2.object({
1272
1281
  project: projectNameSchema,
1273
1282
  sessionId: z2.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start.")
1274
1283
  });
1284
+ var discoveryPromoteInputSchema = z2.object({
1285
+ project: projectNameSchema,
1286
+ sessionId: z2.string().min(1).describe("Discovery session ID returned by canonry_discover_run_start."),
1287
+ request: discoveryPromoteRequestSchema.extend({
1288
+ // Stronger descriptions for the LLM. The base Zod schema enforces the shape.
1289
+ 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
+ }).optional()
1292
+ });
1275
1293
  var AGENT_WEBHOOK_EVENTS = [
1276
1294
  notificationEventSchema.enum["run.completed"],
1277
1295
  notificationEventSchema.enum["insight.critical"],
@@ -2202,7 +2220,7 @@ var canonryMcpTools = [
2202
2220
  defineTool({
2203
2221
  name: "canonry_discover_session_get",
2204
2222
  title: "Get discovery session",
2205
- description: 'Get one discovery session with the full probe list (per-query bucket + cited domains). Use after canonry_discover_run_start to inspect what the discovery pipeline produced; this is the canonical read for "what did discovery find" before PR 2 lands `canonry discover promote`.',
2223
+ description: 'Get one discovery session with the full probe list (per-query bucket + cited domains). Use after canonry_discover_run_start to inspect what the discovery pipeline produced; this is the canonical read for "what did discovery find" before calling canonry_discover_promote.',
2206
2224
  access: "read",
2207
2225
  tier: "discovery",
2208
2226
  inputSchema: discoverySessionIdInputSchema,
@@ -2213,13 +2231,24 @@ var canonryMcpTools = [
2213
2231
  defineTool({
2214
2232
  name: "canonry_discover_promote_preview",
2215
2233
  title: "Preview discovery promotion",
2216
- description: "Read-only preview of what `canonry discover promote` (PR 2) would persist for a session: bucketed query lists and suggested new competitor domains (those not already in the project's tracked competitor list). v1 returns the preview only; use it to confirm a basket before PR 2 ships the merge step.",
2234
+ description: "Read-only preview of available promotion candidates for a session: bucketed query lists and recurring suggested competitor domains not already in the project's tracked competitor list. Use it to confirm a basket before calling canonry_discover_promote.",
2217
2235
  access: "read",
2218
2236
  tier: "discovery",
2219
2237
  inputSchema: discoverySessionIdInputSchema,
2220
2238
  annotations: readAnnotations(),
2221
2239
  openApiOperations: ["GET /api/v1/projects/{name}/discover/sessions/{id}/promote"],
2222
2240
  handler: (client, input) => client.previewDiscoveryPromote(input.project, input.sessionId)
2241
+ }),
2242
+ defineTool({
2243
+ name: "canonry_discover_promote",
2244
+ 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.',
2246
+ access: "write",
2247
+ tier: "discovery",
2248
+ inputSchema: discoveryPromoteInputSchema,
2249
+ annotations: writeAnnotations({ idempotentHint: true }),
2250
+ openApiOperations: ["POST /api/v1/projects/{name}/discover/sessions/{id}/promote"],
2251
+ handler: (client, input) => client.promoteDiscovery(input.project, input.sessionId, input.request)
2223
2252
  })
2224
2253
  ];
2225
2254
  var CANONRY_MCP_TOOL_COUNT = canonryMcpTools.length;
@@ -5,7 +5,7 @@ import {
5
5
  loadConfig,
6
6
  loadConfigRaw,
7
7
  saveConfigPatch
8
- } from "./chunk-2FAEQ56I.js";
8
+ } from "./chunk-GB3QJURO.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-NXXD6TX7.js";
73
+ } from "./chunk-UEV3HSRL.js";
74
74
  import {
75
75
  AGENT_MEMORY_VALUE_MAX_BYTES,
76
76
  AGENT_PROVIDER_IDS,
@@ -81,6 +81,9 @@ import {
81
81
  CheckScopes,
82
82
  CheckStatuses,
83
83
  CitationStates,
84
+ DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
85
+ DISCOVERY_PROMOTE_COMPETITOR_CAP,
86
+ DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
84
87
  DiscoveryBuckets,
85
88
  DiscoverySessionStatuses,
86
89
  MemorySources,
@@ -119,6 +122,7 @@ import {
119
122
  deltaTone,
120
123
  determineAnswerMentioned,
121
124
  discoveryBucketSchema,
125
+ discoveryPromoteRequestSchema,
122
126
  discoveryRunRequestSchema,
123
127
  effectiveDomains,
124
128
  emptyCitationVisibility,
@@ -171,7 +175,7 @@ import {
171
175
  visibilityStateFromAnswerMentioned,
172
176
  windowCutoff,
173
177
  wordpressEnvSchema
174
- } from "./chunk-HVW665A4.js";
178
+ } from "./chunk-RLLFB3M3.js";
175
179
 
176
180
  // src/telemetry.ts
177
181
  import crypto from "crypto";
@@ -10406,7 +10410,7 @@ var routeCatalog = [
10406
10410
  method: "get",
10407
10411
  path: "/api/v1/projects/{name}/discover/sessions/{id}/promote",
10408
10412
  summary: "Preview a discovery promotion plan (read-only)",
10409
- description: "Returns the payload `canonry discover promote` (PR 2) will persist: queries grouped by bucket, plus suggested new competitor domains. v1 is preview-only; the actual merge ships in PR 2.",
10413
+ description: "Returns available promotion candidates: queries grouped by bucket, plus recurring suggested competitor domains not already tracked. Read-only \u2014 use the POST to actually adopt the default subset or an explicit bucket subset.",
10410
10414
  tags: ["discovery"],
10411
10415
  parameters: [
10412
10416
  nameParameter,
@@ -10416,6 +10420,43 @@ var routeCatalog = [
10416
10420
  200: { description: "Promote preview returned." },
10417
10421
  404: { description: "Project or session not found." }
10418
10422
  }
10423
+ },
10424
+ {
10425
+ method: "post",
10426
+ path: "/api/v1/projects/{name}/discover/sessions/{id}/promote",
10427
+ 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.',
10429
+ tags: ["discovery"],
10430
+ parameters: [
10431
+ nameParameter,
10432
+ { name: "id", in: "path", required: true, description: "Discovery session ID.", schema: stringSchema }
10433
+ ],
10434
+ requestBody: {
10435
+ required: false,
10436
+ content: {
10437
+ "application/json": {
10438
+ schema: {
10439
+ type: "object",
10440
+ properties: {
10441
+ buckets: {
10442
+ type: "array",
10443
+ items: { type: "string", enum: ["cited", "aspirational", "wasted-surface"] },
10444
+ description: "Which probe buckets to promote. Omitted means cited + aspirational."
10445
+ },
10446
+ includeCompetitors: {
10447
+ type: "boolean",
10448
+ description: "Whether to also merge recurring discovered competitor domains. Defaults to true."
10449
+ }
10450
+ }
10451
+ }
10452
+ }
10453
+ }
10454
+ },
10455
+ responses: {
10456
+ 200: { description: "Promotion applied; returns promoted + skipped query/competitor lists." },
10457
+ 400: { description: "Session is not completed, or invalid request body." },
10458
+ 404: { description: "Project or session not found." }
10459
+ }
10419
10460
  }
10420
10461
  ];
10421
10462
  var canonryLocalRouteCatalog = [
@@ -19685,7 +19726,7 @@ async function discoveryRoutes(app, opts) {
19685
19726
  else if (bucket === DiscoveryBuckets["wasted-surface"]) wasted.add(probe.query);
19686
19727
  }
19687
19728
  const competitorMap = parseJsonColumn(session.competitorMap, []);
19688
- const newCompetitors = competitorMap.filter((entry) => !seenCompetitors.has(entry.domain.toLowerCase())).slice(0, 20);
19729
+ const newCompetitors = selectEligibleCompetitors(competitorMap).filter((entry) => !seenCompetitors.has(entry.domain.toLowerCase()));
19689
19730
  return reply.send({
19690
19731
  sessionId: session.id,
19691
19732
  projectId: project.id,
@@ -19699,6 +19740,107 @@ async function discoveryRoutes(app, opts) {
19699
19740
  });
19700
19741
  }
19701
19742
  );
19743
+ app.post("/projects/:name/discover/sessions/:id/promote", async (request, reply) => {
19744
+ const project = resolveProject(app.db, request.params.name);
19745
+ const session = app.db.select().from(discoverySessions).where(eq25(discoverySessions.id, request.params.id)).get();
19746
+ if (!session || session.projectId !== project.id) {
19747
+ throw notFound("Discovery session", request.params.id);
19748
+ }
19749
+ const parsed = discoveryPromoteRequestSchema.safeParse(request.body ?? {});
19750
+ if (!parsed.success) {
19751
+ throw validationError("Invalid discovery promote request", {
19752
+ issues: parsed.error.issues.map((issue) => ({
19753
+ path: issue.path.join("."),
19754
+ message: issue.message
19755
+ }))
19756
+ });
19757
+ }
19758
+ if (session.status !== DiscoverySessionStatuses.completed) {
19759
+ throw validationError(
19760
+ `Discovery session is "${session.status}" \u2014 only completed sessions can be promoted.`,
19761
+ { status: session.status }
19762
+ );
19763
+ }
19764
+ const buckets = parsed.data.buckets ?? DEFAULT_DISCOVERY_PROMOTE_BUCKETS;
19765
+ const bucketSet = new Set(buckets);
19766
+ const includeCompetitors = parsed.data.includeCompetitors ?? true;
19767
+ const probeRows = app.db.select().from(discoveryProbes).where(eq25(discoveryProbes.sessionId, session.id)).all();
19768
+ const candidateQueries = /* @__PURE__ */ new Set();
19769
+ for (const probe of probeRows) {
19770
+ if (!probe.bucket) continue;
19771
+ const bucket = discoveryBucketSchema.safeParse(probe.bucket);
19772
+ if (bucket.success && bucketSet.has(bucket.data)) candidateQueries.add(probe.query);
19773
+ }
19774
+ const existingQueries = new Set(
19775
+ app.db.select({ query: queries.query }).from(queries).where(eq25(queries.projectId, project.id)).all().map((r) => r.query.toLowerCase())
19776
+ );
19777
+ const promotedQueries = [];
19778
+ const skippedQueries = [];
19779
+ for (const query of Array.from(candidateQueries).sort()) {
19780
+ if (existingQueries.has(query.toLowerCase())) {
19781
+ skippedQueries.push(query);
19782
+ } else {
19783
+ promotedQueries.push(query);
19784
+ existingQueries.add(query.toLowerCase());
19785
+ }
19786
+ }
19787
+ const promotedCompetitors = [];
19788
+ const skippedCompetitors = [];
19789
+ if (includeCompetitors) {
19790
+ const existingCompetitors = new Set(
19791
+ app.db.select({ domain: competitors.domain }).from(competitors).where(eq25(competitors.projectId, project.id)).all().map((r) => r.domain.toLowerCase())
19792
+ );
19793
+ const competitorMap = parseJsonColumn(session.competitorMap, []);
19794
+ for (const entry of selectEligibleCompetitors(competitorMap)) {
19795
+ const key = entry.domain.toLowerCase();
19796
+ if (existingCompetitors.has(key)) {
19797
+ skippedCompetitors.push(entry.domain);
19798
+ } else {
19799
+ promotedCompetitors.push(entry.domain);
19800
+ existingCompetitors.add(key);
19801
+ }
19802
+ }
19803
+ }
19804
+ const provenance = `discovery:${session.id}`;
19805
+ const now = (/* @__PURE__ */ new Date()).toISOString();
19806
+ if (promotedQueries.length > 0 || promotedCompetitors.length > 0) {
19807
+ app.db.transaction((tx) => {
19808
+ for (const query of promotedQueries) {
19809
+ tx.insert(queries).values({
19810
+ id: crypto21.randomUUID(),
19811
+ projectId: project.id,
19812
+ query,
19813
+ provenance,
19814
+ createdAt: now
19815
+ }).run();
19816
+ }
19817
+ for (const domain of promotedCompetitors) {
19818
+ tx.insert(competitors).values({
19819
+ id: crypto21.randomUUID(),
19820
+ projectId: project.id,
19821
+ domain,
19822
+ provenance,
19823
+ createdAt: now
19824
+ }).run();
19825
+ }
19826
+ writeAuditLog(tx, {
19827
+ projectId: project.id,
19828
+ actor: "api",
19829
+ action: "discovery.promoted",
19830
+ entityType: "discovery_session",
19831
+ entityId: session.id,
19832
+ diff: { queries: promotedQueries, competitors: promotedCompetitors }
19833
+ });
19834
+ });
19835
+ }
19836
+ const result = {
19837
+ sessionId: session.id,
19838
+ projectId: project.id,
19839
+ promoted: { queries: promotedQueries, competitors: promotedCompetitors },
19840
+ skipped: { queries: skippedQueries, competitors: skippedCompetitors }
19841
+ };
19842
+ return reply.send(result);
19843
+ });
19702
19844
  }
19703
19845
  function serializeSession(row) {
19704
19846
  return {
@@ -19735,6 +19877,9 @@ function serializeProbe(row) {
19735
19877
  createdAt: row.createdAt
19736
19878
  };
19737
19879
  }
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);
19882
+ }
19738
19883
 
19739
19884
  // ../api-routes/src/discovery/orchestrate.ts
19740
19885
  import crypto22 from "crypto";
@@ -24482,7 +24627,7 @@ function writeDiscoveryInsight(db, input) {
24482
24627
  provider: input.seedProvider,
24483
24628
  recommendation: JSON.stringify({
24484
24629
  action: "review-discovered-basket",
24485
- summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown. PR 2 will add \`canonry discover promote\` to merge the basket into the project.`,
24630
+ summary: `Run \`canonry discover show ${input.sessionId} --format json\` to inspect the per-query breakdown, then \`canonry discover promote <project> ${input.sessionId}\` to merge cited + aspirational findings into the project.`,
24486
24631
  bucketCounts: buckets,
24487
24632
  topCompetitors
24488
24633
  }),
@@ -27136,7 +27281,7 @@ async function createServer(opts) {
27136
27281
  content = `[system] Discovery run ${ctx.runId} failed for project ${project.name}: ${ctx.error ?? "unknown error"}. Surface a one-line diagnosis and a suggested next step.`;
27137
27282
  } else {
27138
27283
  const top = ctx.topCompetitors.map((c) => `${c.domain}(${c.hits})`).join(", ") || "none";
27139
- content = `[system] Discovery run ${ctx.runId} completed for project ${project.name} (session ${ctx.sessionId}). Buckets \u2014 cited:${ctx.buckets.cited}, wasted-surface:${ctx.buckets["wasted-surface"]}, aspirational:${ctx.buckets.aspirational} (${ctx.probeCount} probes; seed provider: ${ctx.seedProvider ?? "unknown"}). Top recurring competitor domains: ${top}. Use canonry_discover_session_get to pull per-query buckets and call out anything worth promoting to the tracked basket. Keep it tight.`;
27284
+ content = `[system] Discovery run ${ctx.runId} completed for project ${project.name} (session ${ctx.sessionId}). Buckets \u2014 cited:${ctx.buckets.cited}, wasted-surface:${ctx.buckets["wasted-surface"]}, aspirational:${ctx.buckets.aspirational} (${ctx.probeCount} probes; seed provider: ${ctx.seedProvider ?? "unknown"}). Top recurring competitor domains: ${top}. Use canonry_discover_session_get to pull per-query buckets and call out cited + aspirational findings worth promoting. Keep it tight.`;
27140
27285
  }
27141
27286
  } else {
27142
27287
  content = `[system] Run ${ctx.runId} completed for project ${project.name}. ${ctx.insightCount} insights generated (${ctx.criticalOrHigh} critical/high). Use canonry_run_get to inspect the run and canonry_insights_list to review new findings. Surface anything notable briefly \u2014 skip chit-chat.`;
@@ -2356,6 +2356,12 @@ var trafficEventsResponseSchema = z20.object({
2356
2356
  import { z as z21 } from "zod";
2357
2357
  var discoveryBucketSchema = z21.enum(["cited", "aspirational", "wasted-surface"]);
2358
2358
  var DiscoveryBuckets = discoveryBucketSchema.enum;
2359
+ var DEFAULT_DISCOVERY_PROMOTE_BUCKETS = [
2360
+ DiscoveryBuckets.cited,
2361
+ DiscoveryBuckets.aspirational
2362
+ ];
2363
+ var DISCOVERY_PROMOTE_COMPETITOR_CAP = 20;
2364
+ var DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS = 2;
2359
2365
  var discoverySessionStatusSchema = z21.enum(["queued", "seeding", "probing", "completed", "failed"]);
2360
2366
  var DiscoverySessionStatuses = discoverySessionStatusSchema.enum;
2361
2367
  var discoveryCompetitorMapEntrySchema = z21.object({
@@ -2400,6 +2406,33 @@ var discoveryRunRequestSchema = z21.object({
2400
2406
  dedupThreshold: z21.number().min(0).max(1).optional(),
2401
2407
  maxProbes: z21.number().int().positive().max(DISCOVERY_MAX_PROBES_CAP).optional()
2402
2408
  });
2409
+ var discoveryPromoteRequestSchema = z21.object({
2410
+ buckets: z21.array(discoveryBucketSchema).min(1).optional(),
2411
+ includeCompetitors: z21.boolean().optional()
2412
+ });
2413
+ var discoveryPromotePreviewSchema = z21.object({
2414
+ sessionId: z21.string(),
2415
+ projectId: z21.string(),
2416
+ status: discoverySessionStatusSchema,
2417
+ queriesByBucket: z21.object({
2418
+ cited: z21.array(z21.string()),
2419
+ aspirational: z21.array(z21.string()),
2420
+ "wasted-surface": z21.array(z21.string())
2421
+ }),
2422
+ suggestedCompetitors: z21.array(discoveryCompetitorMapEntrySchema)
2423
+ });
2424
+ var discoveryPromoteResultSchema = z21.object({
2425
+ sessionId: z21.string(),
2426
+ projectId: z21.string(),
2427
+ promoted: z21.object({
2428
+ queries: z21.array(z21.string()),
2429
+ competitors: z21.array(z21.string())
2430
+ }),
2431
+ skipped: z21.object({
2432
+ queries: z21.array(z21.string()),
2433
+ competitors: z21.array(z21.string())
2434
+ })
2435
+ });
2403
2436
  var queryProvenanceSchema = z21.union([
2404
2437
  z21.literal("cli"),
2405
2438
  z21.string().regex(/^discovery:.+$/)
@@ -2637,9 +2670,13 @@ export {
2637
2670
  TrafficEventKinds,
2638
2671
  discoveryBucketSchema,
2639
2672
  DiscoveryBuckets,
2673
+ DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
2674
+ DISCOVERY_PROMOTE_COMPETITOR_CAP,
2675
+ DISCOVERY_PROMOTE_COMPETITOR_MIN_HITS,
2640
2676
  DiscoverySessionStatuses,
2641
2677
  DISCOVERY_MAX_PROBES_CAP,
2642
2678
  discoveryRunRequestSchema,
2679
+ discoveryPromoteRequestSchema,
2643
2680
  clusterByCosine,
2644
2681
  pickClusterRepresentative,
2645
2682
  formatRatio,
@@ -8,7 +8,7 @@ import {
8
8
  categoryLabel,
9
9
  determineAnswerMentioned,
10
10
  normalizeProjectDomain
11
- } from "./chunk-HVW665A4.js";
11
+ } from "./chunk-RLLFB3M3.js";
12
12
 
13
13
  // src/intelligence-service.ts
14
14
  import { eq, desc, asc, and, or, inArray } from "drizzle-orm";