@ainyc/canonry 4.64.1 → 4.68.1

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 (28) hide show
  1. package/README.md +1 -0
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +38 -2
  3. package/assets/assets/{BacklinksPage-B9oyoljV.js → BacklinksPage-B9Q2r9zM.js} +1 -1
  4. package/assets/assets/ChartPrimitives-D-YWOWK-.js +1 -0
  5. package/assets/assets/ProjectPage-BXgxWKyT.js +6 -0
  6. package/assets/assets/{RunRow-vHM36Fdi.js → RunRow-CJpjUaht.js} +1 -1
  7. package/assets/assets/{RunsPage-Cr58nTet.js → RunsPage-DVl_pgqe.js} +1 -1
  8. package/assets/assets/{SettingsPage-BuiP8ZOv.js → SettingsPage-CAiVL2mo.js} +1 -1
  9. package/assets/assets/{TrafficPage-bwOxChyo.js → TrafficPage-B8dnHFqk.js} +1 -1
  10. package/assets/assets/{TrafficSourceDetailPage-B0uY6VIB.js → TrafficSourceDetailPage-37s8p8eZ.js} +1 -1
  11. package/assets/assets/{extract-error-message-CWdzuNp4.js → extract-error-message-DwqbPRoa.js} +1 -1
  12. package/assets/assets/index-BquJzH0t.css +1 -0
  13. package/assets/assets/{index-DiN_mzYU.js → index-DFo2OL9c.js} +79 -79
  14. package/assets/assets/{server-traffic-D3aICbxr.js → server-traffic-CMFP8-x2.js} +1 -1
  15. package/assets/assets/{trash-2-CVKno2W2.js → trash-2-C4sYVIa6.js} +1 -1
  16. package/assets/index.html +2 -2
  17. package/dist/{chunk-34PATQZM.js → chunk-D75O5A27.js} +36 -1
  18. package/dist/{chunk-MDNDIBUM.js → chunk-GZYLAE6M.js} +57 -19
  19. package/dist/{chunk-KHN3XMOR.js → chunk-Y24OE7R3.js} +64 -26
  20. package/dist/{chunk-64IDABSF.js → chunk-YYFBMDLC.js} +22 -31
  21. package/dist/cli.js +433 -219
  22. package/dist/index.js +4 -4
  23. package/dist/{intelligence-service-SMU5JVVD.js → intelligence-service-5V2JWQ6K.js} +2 -2
  24. package/dist/mcp.js +2 -2
  25. package/package.json +8 -7
  26. package/assets/assets/ChartPrimitives-CvfM24iC.js +0 -1
  27. package/assets/assets/ProjectPage-DELbOAlm.js +0 -6
  28. package/assets/assets/index-yMJe1bJR.css +0 -1
@@ -1 +1 @@
1
- import{c as d,j as a,bT as S,bN as l,bU as v,au as m,l as t,bV as y,bO as i,bW as T,bX as h,bY as p,bZ as b}from"./index-DiN_mzYU.js";import{u as s,r as C,n as c,o as u}from"./vendor-tanstack-Dq7p98wZ.js";const g=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],M=d("refresh-cw",g);function P(e){switch(e){case i.connected:return"positive";case i.paused:return"caution";case i.error:return"negative";case i.archived:return"neutral"}}function w(e){const r={};return e.kind&&e.kind!=="all"&&(r.kind=e.kind),e.sourceId&&(r.sourceId=e.sourceId),e.sinceMinutes!==void 0&&(r.since=new Date(Date.now()-e.sinceMinutes*6e4).toISOString()),e.limit!==void 0&&(r.limit=String(e.limit)),r}function o(e){e.invalidateQueries({predicate:r=>{const n=r.queryKey[0];return typeof n?._id=="string"&&n._id.startsWith("getApiV1ProjectsByNameTraffic")}})}function V(e){return s({...S({client:t,path:{name:e??""}}),enabled:!!e,staleTime:a})}function k(e){return s({...b({client:t,path:{name:e??""}}),enabled:!!e,staleTime:a})}function F(e,r){return s({...l({client:t,path:{name:e??"",id:r??""}}),enabled:!!(e&&r),staleTime:a})}function A(e,r){const n=C.useMemo(()=>w(r),[r.kind,r.sourceId,r.sinceMinutes,r.limit]);return s({...v({client:t,path:{name:e??""},query:n}),enabled:!!e,staleTime:a})}function E(e){const r=c();return u({mutationFn:n=>{if(!e)throw new Error("Project is required to connect a Cloud Run source");return p(e,n)},onSuccess:()=>{e&&o(r)}})}function I(e){const r=c();return u({mutationFn:n=>{if(!e)throw new Error("Project is required to connect a WordPress source");return T(e,n)},onSuccess:()=>{e&&o(r)}})}function N(e){const r=c();return u({mutationFn:n=>{if(!e)throw new Error("Project is required to connect a Vercel source");return h(e,n)},onSuccess:()=>{e&&o(r)}})}function Q(e,r){const n=c();return u({mutationFn:f=>{if(!e||!r)throw new Error("Project and sourceId are required to sync");return y(e,r,f??void 0)},onSuccess:()=>{e&&(o(n),n.invalidateQueries({queryKey:m({client:t})}))}})}export{M as R,I as a,N as b,E as c,V as d,F as e,A as f,Q as g,P as t,k as u};
1
+ import{c as d,j as a,bU as S,bO as l,bV as v,av as m,l as t,bW as y,bP as i,bX as T,bY as h,bZ as p,b_ as b}from"./index-DFo2OL9c.js";import{u as s,r as C,n as c,o as u}from"./vendor-tanstack-Dq7p98wZ.js";const g=[["path",{d:"M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8",key:"v9h5vc"}],["path",{d:"M21 3v5h-5",key:"1q7to0"}],["path",{d:"M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16",key:"3uifl3"}],["path",{d:"M8 16H3v5",key:"1cv678"}]],B=d("refresh-cw",g);function M(e){switch(e){case i.connected:return"positive";case i.paused:return"caution";case i.error:return"negative";case i.archived:return"neutral"}}function w(e){const r={};return e.kind&&e.kind!=="all"&&(r.kind=e.kind),e.sourceId&&(r.sourceId=e.sourceId),e.sinceMinutes!==void 0&&(r.since=new Date(Date.now()-e.sinceMinutes*6e4).toISOString()),e.limit!==void 0&&(r.limit=String(e.limit)),r}function o(e){e.invalidateQueries({predicate:r=>{const n=r.queryKey[0];return typeof n?._id=="string"&&n._id.startsWith("getApiV1ProjectsByNameTraffic")}})}function V(e){return s({...S({client:t,path:{name:e??""}}),enabled:!!e,staleTime:a})}function k(e){return s({...b({client:t,path:{name:e??""}}),enabled:!!e,staleTime:a})}function F(e,r){return s({...l({client:t,path:{name:e??"",id:r??""}}),enabled:!!(e&&r),staleTime:a})}function A(e,r){const n=C.useMemo(()=>w(r),[r.kind,r.sourceId,r.sinceMinutes,r.limit]);return s({...v({client:t,path:{name:e??""},query:n}),enabled:!!e,staleTime:a})}function E(e){const r=c();return u({mutationFn:n=>{if(!e)throw new Error("Project is required to connect a Cloud Run source");return p(e,n)},onSuccess:()=>{e&&o(r)}})}function I(e){const r=c();return u({mutationFn:n=>{if(!e)throw new Error("Project is required to connect a WordPress source");return T(e,n)},onSuccess:()=>{e&&o(r)}})}function Q(e){const r=c();return u({mutationFn:n=>{if(!e)throw new Error("Project is required to connect a Vercel source");return h(e,n)},onSuccess:()=>{e&&o(r)}})}function R(e,r){const n=c();return u({mutationFn:f=>{if(!e||!r)throw new Error("Project and sourceId are required to sync");return y(e,r,f??void 0)},onSuccess:()=>{e&&(o(n),n.invalidateQueries({queryKey:m({client:t})}))}})}export{B as R,I as a,Q as b,E as c,V as d,F as e,A as f,R as g,M as t,k as u};
@@ -1 +1 @@
1
- import{c}from"./index-DiN_mzYU.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
1
+ import{c}from"./index-DFo2OL9c.js";const a=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"m9 12 2 2 4-4",key:"dzmm74"}]],h=c("circle-check",a);const e=[["circle",{cx:"12",cy:"12",r:"10",key:"1mglay"}],["path",{d:"M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3",key:"1u773s"}],["path",{d:"M12 17h.01",key:"p32p05"}]],n=c("circle-question-mark",e);const o=[["path",{d:"M12 15V3",key:"m9g1x1"}],["path",{d:"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4",key:"ih7n3h"}],["path",{d:"m7 10 5 5 5-5",key:"brsn70"}]],s=c("download",o);const t=[["path",{d:"M10 11v6",key:"nco0om"}],["path",{d:"M14 11v6",key:"outv1u"}],["path",{d:"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6",key:"miytrc"}],["path",{d:"M3 6h18",key:"d0wm0j"}],["path",{d:"M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2",key:"e791ji"}]],r=c("trash-2",t);export{h as C,s as D,r as T,n as a};
package/assets/index.html CHANGED
@@ -12,12 +12,12 @@
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-DiN_mzYU.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-DFo2OL9c.js"></script>
16
16
  <link rel="modulepreload" crossorigin href="./assets/vendor-tanstack-Dq7p98wZ.js">
17
17
  <link rel="modulepreload" crossorigin href="./assets/vendor-radix-B57xfQbP.js">
18
18
  <link rel="modulepreload" crossorigin href="./assets/vendor-recharts-ClRVR6aX.js">
19
19
  <link rel="modulepreload" crossorigin href="./assets/vendor-markdown-DK7fbRNb.js">
20
- <link rel="stylesheet" crossorigin href="./assets/index-yMJe1bJR.css">
20
+ <link rel="stylesheet" crossorigin href="./assets/index-BquJzH0t.css">
21
21
  </head>
22
22
  <body>
23
23
  <div id="root"></div>
@@ -1498,7 +1498,7 @@ var snapshotReportSchema = z12.object({
1498
1498
 
1499
1499
  // ../contracts/src/schedule.ts
1500
1500
  import { z as z13 } from "zod";
1501
- var schedulableRunKindSchema = z13.enum(["answer-visibility", "traffic-sync", "gbp-sync", "data-refresh"]);
1501
+ var schedulableRunKindSchema = z13.enum(["answer-visibility", "traffic-sync", "gbp-sync", "data-refresh", "backlinks-sync"]);
1502
1502
  var SchedulableRunKinds = schedulableRunKindSchema.enum;
1503
1503
  var scheduleDtoSchema = z13.object({
1504
1504
  id: z13.string(),
@@ -1534,8 +1534,42 @@ var scheduleUpsertRequestSchema = z13.object({
1534
1534
 
1535
1535
  // ../contracts/src/analytics.ts
1536
1536
  import { z as z14 } from "zod";
1537
+ var metricsWindowSchema = z14.enum(["7d", "30d", "90d", "all"]);
1538
+ var trendDirectionSchema = z14.enum(["improving", "declining", "stable"]);
1537
1539
  var visibilityMetricModeSchema = z14.enum(["mentioned", "cited"]);
1538
1540
  var VisibilityMetricModes = visibilityMetricModeSchema.enum;
1541
+ var providerMetricSchema = z14.object({
1542
+ citationRate: z14.number(),
1543
+ cited: z14.number().int(),
1544
+ total: z14.number().int(),
1545
+ mentionRate: z14.number(),
1546
+ mentionedCount: z14.number().int()
1547
+ });
1548
+ var timeBucketSchema = z14.object({
1549
+ startDate: z14.string(),
1550
+ endDate: z14.string(),
1551
+ citationRate: z14.number(),
1552
+ cited: z14.number().int(),
1553
+ total: z14.number().int(),
1554
+ queryCount: z14.number().int(),
1555
+ mentionRate: z14.number(),
1556
+ mentionedCount: z14.number().int(),
1557
+ byProvider: z14.record(z14.string(), providerMetricSchema)
1558
+ });
1559
+ var queryChangeEventSchema = z14.object({
1560
+ date: z14.string(),
1561
+ delta: z14.number().int(),
1562
+ label: z14.string()
1563
+ });
1564
+ var brandMetricsDtoSchema = z14.object({
1565
+ window: metricsWindowSchema,
1566
+ buckets: z14.array(timeBucketSchema),
1567
+ overall: providerMetricSchema,
1568
+ byProvider: z14.record(z14.string(), providerMetricSchema),
1569
+ trend: trendDirectionSchema,
1570
+ mentionTrend: trendDirectionSchema,
1571
+ queryChanges: z14.array(queryChangeEventSchema)
1572
+ });
1539
1573
  function parseWindow(value) {
1540
1574
  if (value === "7d" || value === "30d" || value === "90d" || value === "all") return value;
1541
1575
  return "all";
@@ -4010,6 +4044,7 @@ export {
4010
4044
  SchedulableRunKinds,
4011
4045
  scheduleDtoSchema,
4012
4046
  scheduleUpsertRequestSchema,
4047
+ brandMetricsDtoSchema,
4013
4048
  parseWindow,
4014
4049
  windowCutoff,
4015
4050
  categorizeSource,
@@ -5,10 +5,11 @@ import {
5
5
  canonryMcpTools,
6
6
  configExists,
7
7
  getConfigPath,
8
+ isMachineFormat,
8
9
  loadConfig,
9
10
  loadConfigRaw,
10
11
  saveConfigPatch
11
- } from "./chunk-64IDABSF.js";
12
+ } from "./chunk-YYFBMDLC.js";
12
13
  import {
13
14
  CC_CACHE_DIR,
14
15
  DUCKDB_SPEC,
@@ -76,6 +77,7 @@ import {
76
77
  loadDuckdb,
77
78
  markSessionFailed,
78
79
  migrate,
80
+ nextRunFromCron,
79
81
  notifications,
80
82
  parseJsonColumn,
81
83
  probeLatestRelease,
@@ -93,7 +95,7 @@ import {
93
95
  runs,
94
96
  schedules,
95
97
  usageCounters
96
- } from "./chunk-KHN3XMOR.js";
98
+ } from "./chunk-Y24OE7R3.js";
97
99
  import {
98
100
  AGENT_MEMORY_VALUE_MAX_BYTES,
99
101
  AGENT_PROVIDER_IDS,
@@ -141,7 +143,7 @@ import {
141
143
  skillsClientSchema,
142
144
  validationError,
143
145
  withRetry
144
- } from "./chunk-34PATQZM.js";
146
+ } from "./chunk-D75O5A27.js";
145
147
 
146
148
  // src/telemetry.ts
147
149
  import crypto from "crypto";
@@ -433,7 +435,7 @@ import crypto17 from "crypto";
433
435
  import fs8 from "fs";
434
436
  import path9 from "path";
435
437
  import { fileURLToPath as fileURLToPath3 } from "url";
436
- import { eq as eq17 } from "drizzle-orm";
438
+ import { and as and13, eq as eq17 } from "drizzle-orm";
437
439
  import Fastify from "fastify";
438
440
  import os5 from "os";
439
441
 
@@ -5296,7 +5298,7 @@ async function backfillAnswerVisibilityCommand(opts) {
5296
5298
  result.dryRun = true;
5297
5299
  result.wouldUpdate = wouldUpdate;
5298
5300
  }
5299
- if (opts?.format === "json") {
5301
+ if (isMachineFormat(opts?.format)) {
5300
5302
  console.log(JSON.stringify(result, null, 2));
5301
5303
  return;
5302
5304
  }
@@ -5366,7 +5368,7 @@ async function backfillNormalizedPathsCommand(opts) {
5366
5368
  updated: 0,
5367
5369
  unchanged: 0
5368
5370
  };
5369
- if (opts?.format === "json") {
5371
+ if (isMachineFormat(opts?.format)) {
5370
5372
  console.log(JSON.stringify(result2, null, 2));
5371
5373
  return;
5372
5374
  }
@@ -5382,7 +5384,7 @@ async function backfillNormalizedPathsCommand(opts) {
5382
5384
  updated,
5383
5385
  unchanged
5384
5386
  };
5385
- if (opts?.format === "json") {
5387
+ if (isMachineFormat(opts?.format)) {
5386
5388
  console.log(JSON.stringify(result, null, 2));
5387
5389
  return;
5388
5390
  }
@@ -5438,7 +5440,7 @@ async function backfillAiReferralPathsCommand(opts) {
5438
5440
  updated: 0,
5439
5441
  unchanged: 0
5440
5442
  };
5441
- if (opts?.format === "json") {
5443
+ if (isMachineFormat(opts?.format)) {
5442
5444
  console.log(JSON.stringify(result2, null, 2));
5443
5445
  return;
5444
5446
  }
@@ -5454,7 +5456,7 @@ async function backfillAiReferralPathsCommand(opts) {
5454
5456
  updated,
5455
5457
  unchanged
5456
5458
  };
5457
- if (opts?.format === "json") {
5459
+ if (isMachineFormat(opts?.format)) {
5458
5460
  console.log(JSON.stringify(result, null, 2));
5459
5461
  return;
5460
5462
  }
@@ -5579,7 +5581,7 @@ async function backfillAnswerMentionsCommand(opts) {
5579
5581
  result.dryRun = true;
5580
5582
  result.wouldUpdate = wouldUpdate;
5581
5583
  }
5582
- if (opts?.format === "json") {
5584
+ if (isMachineFormat(opts?.format)) {
5583
5585
  console.log(JSON.stringify(result, null, 2));
5584
5586
  return;
5585
5587
  }
@@ -5616,12 +5618,12 @@ function readStoredGroundingSources(rawResponse) {
5616
5618
  return result;
5617
5619
  }
5618
5620
  async function backfillInsightsCommand(project, opts) {
5619
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-SMU5JVVD.js");
5621
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-5V2JWQ6K.js");
5620
5622
  const config = loadConfig();
5621
5623
  const db = createClient(config.database);
5622
5624
  migrate(db);
5623
5625
  const service = new IntelligenceService2(db);
5624
- const isJson = opts?.format === "json";
5626
+ const isJson = isMachineFormat(opts?.format);
5625
5627
  const isDryRun = opts?.dryRun === true;
5626
5628
  if (!isJson) {
5627
5629
  const scope = opts?.since ? ` (since ${opts.since})` : "";
@@ -5779,7 +5781,7 @@ async function backfillSnapshotAttributionCommand(opts) {
5779
5781
  if (!project) {
5780
5782
  throw new Error(`Project "${opts.project}" not found`);
5781
5783
  }
5782
- const isJson = opts.format === "json";
5784
+ const isJson = isMachineFormat(opts.format);
5783
5785
  const isDryRun = opts.dryRun === true;
5784
5786
  if (!isJson) {
5785
5787
  const mode = isDryRun ? " [DRY RUN \u2014 no writes]" : "";
@@ -5958,7 +5960,7 @@ async function backfillTrafficClassificationCommand(opts) {
5958
5960
  migrate(db);
5959
5961
  const projectFilter = opts?.project?.trim();
5960
5962
  const isDryRun = opts?.dryRun === true;
5961
- const isJson = opts?.format === "json";
5963
+ const isJson = isMachineFormat(opts?.format);
5962
5964
  const scopedProjects = projectFilter ? db.select().from(projects).where(eq10(projects.name, projectFilter)).all() : db.select().from(projects).all();
5963
5965
  if (scopedProjects.length === 0) {
5964
5966
  if (projectFilter && !isJson) {
@@ -6395,7 +6397,7 @@ async function installSkills(opts = {}) {
6395
6397
  }
6396
6398
  async function listSkills(opts = {}) {
6397
6399
  const skills = getBundledSkills();
6398
- if (opts.format === "json") {
6400
+ if (isMachineFormat(opts.format)) {
6399
6401
  console.log(JSON.stringify({
6400
6402
  skills: skills.map((s) => ({
6401
6403
  name: s.name,
@@ -6416,7 +6418,7 @@ async function listSkills(opts = {}) {
6416
6418
  }
6417
6419
  }
6418
6420
  function emitInstallSummary(summary, format) {
6419
- if (format === "json") {
6421
+ if (isMachineFormat(format)) {
6420
6422
  console.log(JSON.stringify(summary, null, 2));
6421
6423
  return;
6422
6424
  }
@@ -6601,7 +6603,7 @@ var Scheduler = class {
6601
6603
  });
6602
6604
  this.tasks.set(taskKey(projectId, kind), task);
6603
6605
  this.db.update(schedules).set({
6604
- nextRunAt: task.getNextRun()?.toISOString() ?? null,
6606
+ nextRunAt: nextRunFromCron(cronExpr, timezone),
6605
6607
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
6606
6608
  }).where(eq11(schedules.id, scheduleId)).run();
6607
6609
  const label = schedule.preset ?? cronExpr;
@@ -6616,8 +6618,7 @@ var Scheduler = class {
6616
6618
  this.remove(projectId, kind);
6617
6619
  return;
6618
6620
  }
6619
- const task = this.tasks.get(taskKey(projectId, kind));
6620
- const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
6621
+ const nextRunAt = nextRunFromCron(currentSchedule.cronExpr, currentSchedule.timezone);
6621
6622
  const project = this.db.select().from(projects).where(eq11(projects.id, projectId)).get();
6622
6623
  if (!project) {
6623
6624
  log11.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
@@ -6680,6 +6681,20 @@ var Scheduler = class {
6680
6681
  this.callbacks.onDataRefreshRequested(project.name);
6681
6682
  return;
6682
6683
  }
6684
+ if (kind === SchedulableRunKinds["backlinks-sync"]) {
6685
+ if (!this.callbacks.onBacklinksSyncRequested) {
6686
+ log11.warn("backlinks-sync.no-callback", { scheduleId, projectId, msg: "host did not register onBacklinksSyncRequested" });
6687
+ return;
6688
+ }
6689
+ this.db.update(schedules).set({
6690
+ lastRunAt: now,
6691
+ nextRunAt,
6692
+ updatedAt: now
6693
+ }).where(eq11(schedules.id, currentSchedule.id)).run();
6694
+ log11.info("backlinks-sync.triggered", { projectName: project.name });
6695
+ this.callbacks.onBacklinksSyncRequested(project.name);
6696
+ return;
6697
+ }
6683
6698
  const projectLocations = project.locations;
6684
6699
  let resolvedLocation;
6685
6700
  if (project.defaultLocation) {
@@ -9451,6 +9466,29 @@ async function createServer(opts) {
9451
9466
  },
9452
9467
  onDataRefreshRequested: (projectName) => {
9453
9468
  void refreshAllIntegrations(aeroClient, projectName);
9469
+ },
9470
+ onBacklinksSyncRequested: (projectName) => {
9471
+ void (async () => {
9472
+ const probed = await probeLatestRelease().catch((err) => {
9473
+ app.log.warn({ projectName, err }, "Scheduled backlinks sync: latest-release probe failed");
9474
+ return null;
9475
+ });
9476
+ if (!probed) return;
9477
+ const alreadySynced = opts.db.select().from(ccReleaseSyncs).where(and13(
9478
+ eq17(ccReleaseSyncs.release, probed.release),
9479
+ eq17(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)
9480
+ )).limit(1).get();
9481
+ if (alreadySynced) {
9482
+ app.log.info({ projectName, release: probed.release }, "Scheduled backlinks sync: already up to date, skipping");
9483
+ return;
9484
+ }
9485
+ aeroClient.backlinksTriggerSync(probed.release).catch((err) => {
9486
+ app.log.error(
9487
+ { projectName, release: probed.release, err: err instanceof Error ? err.message : String(err) },
9488
+ "Scheduled backlinks sync failed"
9489
+ );
9490
+ });
9491
+ })();
9454
9492
  }
9455
9493
  });
9456
9494
  const providerSummary = API_ADAPTERS.map((adapter) => ({
@@ -51,6 +51,7 @@ import {
51
51
  bingUrlInspectionDtoSchema,
52
52
  brandKeyFromText,
53
53
  brandLabelFromDomain,
54
+ brandMetricsDtoSchema,
54
55
  categorizeSource,
55
56
  categorizeSourceWithCompetitors,
56
57
  categoryLabel,
@@ -207,7 +208,7 @@ import {
207
208
  wordpressSchemaDeployResultDtoSchema,
208
209
  wordpressSchemaStatusResultDtoSchema,
209
210
  wordpressStatusDtoSchema
210
- } from "./chunk-34PATQZM.js";
211
+ } from "./chunk-D75O5A27.js";
211
212
 
212
213
  // src/intelligence-service.ts
213
214
  import { eq as eq30, desc as desc15, asc as asc3, and as and23, ne as ne5, or as or5, inArray as inArray11, gte as gte6, lte as lte3 } from "drizzle-orm";
@@ -6131,6 +6132,7 @@ import crypto10 from "crypto";
6131
6132
  import { and as and3, eq as eq8 } from "drizzle-orm";
6132
6133
 
6133
6134
  // ../api-routes/src/schedule-utils.ts
6135
+ import { CronExpressionParser } from "cron-parser";
6134
6136
  var DAY_MAP = {
6135
6137
  sun: "0",
6136
6138
  mon: "1",
@@ -6217,6 +6219,17 @@ function isValidTimezone(tz) {
6217
6219
  return false;
6218
6220
  }
6219
6221
  }
6222
+ function nextRunFromCron(cronExpr, timezone, from = /* @__PURE__ */ new Date()) {
6223
+ try {
6224
+ const interval = CronExpressionParser.parse(cronExpr, {
6225
+ currentDate: from,
6226
+ tz: timezone
6227
+ });
6228
+ return interval.next().toDate().toISOString();
6229
+ } catch {
6230
+ return null;
6231
+ }
6232
+ }
6220
6233
 
6221
6234
  // ../api-routes/src/webhooks.ts
6222
6235
  import crypto9 from "crypto";
@@ -7243,6 +7256,10 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
7243
7256
  }
7244
7257
  const metric = computeProviderMetric(usable);
7245
7258
  const queryCount = new Set(usable.map((s) => s.queryId)).size;
7259
+ const byProvider = {};
7260
+ for (const provider of new Set(usable.map((s) => s.provider))) {
7261
+ byProvider[provider] = computeProviderMetric(usable.filter((s) => s.provider === provider));
7262
+ }
7246
7263
  buckets.push({
7247
7264
  startDate: startISO,
7248
7265
  endDate: endISO,
@@ -7251,7 +7268,8 @@ function computeBuckets(snapshots, projectRuns, bucketDays, queryCreatedAt) {
7251
7268
  total: metric.total,
7252
7269
  queryCount,
7253
7270
  mentionRate: metric.mentionRate,
7254
- mentionedCount: metric.mentionedCount
7271
+ mentionedCount: metric.mentionedCount,
7272
+ byProvider
7255
7273
  });
7256
7274
  }
7257
7275
  start = end;
@@ -12012,6 +12030,7 @@ async function compositeRoutes(app) {
12012
12030
  const attentionItems = buildAttentionItems(insightRows, allRuns);
12013
12031
  const sparklineRuns = visibilityRuns.slice(0, DEFAULT_RUN_HISTORY_LIMIT).map((r) => ({ id: r.id, createdAt: r.createdAt, status: r.status }));
12014
12032
  const runHistory = buildRunHistory(sparklineRuns, snapshotsByRun);
12033
+ scores.visibility.trend = runHistory.map((p) => p.citationRate);
12015
12034
  const suggestedQueries = buildSuggestedQueriesFromGsc(
12016
12035
  app,
12017
12036
  project.id,
@@ -12544,6 +12563,7 @@ var SCHEMA_TABLE = {
12544
12563
  BingSitesResponseDto: bingSitesResponseDtoSchema,
12545
12564
  BingStatusDto: bingStatusDtoSchema,
12546
12565
  BingUrlInspectionDto: bingUrlInspectionDtoSchema,
12566
+ BrandMetricsDto: brandMetricsDtoSchema,
12547
12567
  CcAvailableRelease: ccAvailableReleaseSchema,
12548
12568
  CcCachedRelease: ccCachedReleaseSchema,
12549
12569
  CcReleaseSyncDto: ccReleaseSyncDtoSchema,
@@ -12595,6 +12615,7 @@ var SCHEMA_TABLE = {
12595
12615
  QueryDto: queryDtoSchema,
12596
12616
  RunDetailDto: runDetailDtoSchema,
12597
12617
  RunDto: runDtoSchema,
12618
+ SchedulableRunKind: schedulableRunKindSchema,
12598
12619
  ScheduleDto: scheduleDtoSchema,
12599
12620
  SettingsDto: settingsDtoSchema,
12600
12621
  SnapshotDiffResponse: snapshotDiffResponseSchema,
@@ -12630,11 +12651,20 @@ var SCHEMA_TABLE = {
12630
12651
  })
12631
12652
  })
12632
12653
  };
12654
+ var SHARED_ENUM_REFS = [
12655
+ { schema: "ScheduleDto", property: "kind", component: "SchedulableRunKind" }
12656
+ ];
12633
12657
  function buildComponentSchemas() {
12634
12658
  const out = {};
12635
12659
  for (const [name, schema] of Object.entries(SCHEMA_TABLE)) {
12636
12660
  out[name] = z.toJSONSchema(schema, { target: "openapi-3.0" });
12637
12661
  }
12662
+ for (const { schema, property, component } of SHARED_ENUM_REFS) {
12663
+ const properties = out[schema]?.properties;
12664
+ if (properties && properties[property]) {
12665
+ properties[property] = { $ref: `#/components/schemas/${component}` };
12666
+ }
12667
+ }
12638
12668
  return out;
12639
12669
  }
12640
12670
  function jsonResponse(description, schemaName) {
@@ -12680,7 +12710,6 @@ var integerSchema = { type: "integer" };
12680
12710
  var objectSchema = { type: "object", additionalProperties: true };
12681
12711
  var stringArraySchema = { type: "array", items: stringSchema };
12682
12712
  var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4", "gbp"] };
12683
- var scheduleKindEnum = Object.values(SchedulableRunKinds);
12684
12713
  var locationSchema = {
12685
12714
  type: "object",
12686
12715
  required: ["label", "city", "region", "country"],
@@ -12770,7 +12799,7 @@ var scheduleKindQueryParameter = {
12770
12799
  name: "kind",
12771
12800
  in: "query",
12772
12801
  description: 'Schedulable run kind. Defaults to "answer-visibility" for backward compatibility.',
12773
- schema: { type: "string", enum: scheduleKindEnum }
12802
+ schema: { $ref: "#/components/schemas/SchedulableRunKind" }
12774
12803
  };
12775
12804
  var runsListKindQueryParameter = {
12776
12805
  name: "kind",
@@ -13516,8 +13545,7 @@ var routeCatalog = [
13516
13545
  tags: ["analytics"],
13517
13546
  parameters: [nameParameter, analyticsWindowParameter],
13518
13547
  responses: {
13519
- // TODO: Add `BrandMetricsDto` Zod schema in contracts.
13520
- 200: rawJsonResponse("Citation metrics returned.", looseObjectSchema),
13548
+ 200: jsonResponse("Citation metrics returned.", "BrandMetricsDto"),
13521
13549
  404: errorResponse("Project not found.")
13522
13550
  }
13523
13551
  },
@@ -13731,7 +13759,7 @@ var routeCatalog = [
13731
13759
  schema: {
13732
13760
  type: "object",
13733
13761
  properties: {
13734
- kind: { type: "string", enum: scheduleKindEnum },
13762
+ kind: { $ref: "#/components/schemas/SchedulableRunKind" },
13735
13763
  preset: stringSchema,
13736
13764
  cron: stringSchema,
13737
13765
  timezone: stringSchema,
@@ -16583,6 +16611,9 @@ async function scheduleRoutes(app, opts) {
16583
16611
  } else if (sourceId) {
16584
16612
  throw validationError(`"sourceId" is only valid when kind is "traffic-sync"`);
16585
16613
  }
16614
+ if (kind === SchedulableRunKinds["backlinks-sync"] && providers && providers.length > 0) {
16615
+ throw validationError('"providers" is not valid for kind "backlinks-sync"');
16616
+ }
16586
16617
  const validNames = opts.validProviderNames ?? [];
16587
16618
  if (validNames.length && providers?.length) {
16588
16619
  const invalid = providers.filter((p) => !validNames.includes(p));
@@ -22781,7 +22812,8 @@ var PLUGIN_DIR = path2.join(os2.homedir(), ".canonry", "plugins");
22781
22812
  var PLUGIN_PKG_JSON = path2.join(PLUGIN_DIR, "package.json");
22782
22813
  var DUCKDB_SPEC = process.env.CANONRY_DUCKDB_SPEC ?? "@duckdb/node-api@1.4.4-r.3";
22783
22814
  var CC_CACHE_DIR = process.env.CANONRY_CC_CACHE_DIR ?? path2.join(os2.homedir(), ".canonry", "cache", "commoncrawl");
22784
- var RELEASE_ID_REGEX = /^cc-main-(\d{4})-(jan-feb-mar|apr-may-jun|jul-aug-sep|oct-nov-dec)$/;
22815
+ var CC_MONTH = "(?:jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)";
22816
+ var RELEASE_ID_REGEX = new RegExp(`^cc-main-(\\d{4})-(${CC_MONTH}-${CC_MONTH}-${CC_MONTH})$`);
22785
22817
  function ccReleasePaths(release) {
22786
22818
  const base = `${CC_BASE_URL}/${release}/domain`;
22787
22819
  const vertexFilename = `${release}-domain-vertices.txt.gz`;
@@ -22806,23 +22838,28 @@ function forwardDomain(revDomain) {
22806
22838
  function isValidReleaseId(id) {
22807
22839
  return RELEASE_ID_REGEX.test(id);
22808
22840
  }
22809
- function formatReleaseId(year, quarter) {
22810
- return `cc-main-${year}-${quarter}`;
22841
+ function formatReleaseId(year, window) {
22842
+ return `cc-main-${year}-${window}`;
22811
22843
  }
22812
22844
 
22813
22845
  // ../integration-commoncrawl/src/release-discovery.ts
22814
- var QUARTERS = [
22815
- "oct-nov-dec",
22816
- "jul-aug-sep",
22817
- "apr-may-jun",
22818
- "jan-feb-mar"
22819
- ];
22820
- function probeCandidates(now, maxBack) {
22821
- const year = now.getUTCFullYear();
22846
+ var MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
22847
+ function windowSlug(firstMonthIndex) {
22848
+ const a = MONTHS[firstMonthIndex % 12];
22849
+ const b = MONTHS[(firstMonthIndex + 1) % 12];
22850
+ const c = MONTHS[(firstMonthIndex + 2) % 12];
22851
+ return `${a}-${b}-${c}`;
22852
+ }
22853
+ function probeCandidates(now, maxMonthsBack) {
22854
+ let year = now.getUTCFullYear();
22855
+ let month = now.getUTCMonth();
22822
22856
  const out = [];
22823
- for (let y = year; y >= year - maxBack; y--) {
22824
- for (const q of QUARTERS) {
22825
- out.push({ year: y, quarter: q });
22857
+ for (let step = 0; step <= maxMonthsBack; step++) {
22858
+ out.push({ year, window: windowSlug(month) });
22859
+ month -= 1;
22860
+ if (month < 0) {
22861
+ month = 11;
22862
+ year -= 1;
22826
22863
  }
22827
22864
  }
22828
22865
  return out;
@@ -22845,11 +22882,11 @@ async function probeRelease(release, fetchImpl = fetch) {
22845
22882
  }
22846
22883
  async function probeLatestRelease(opts = {}) {
22847
22884
  const now = opts.now ?? /* @__PURE__ */ new Date();
22848
- const maxBack = opts.maxQuartersBack ?? 3;
22885
+ const maxBack = opts.maxMonthsBack ?? 14;
22849
22886
  const fetchImpl = opts.fetchImpl ?? fetch;
22850
22887
  const candidates = probeCandidates(now, maxBack);
22851
- for (const { year, quarter } of candidates) {
22852
- const release = formatReleaseId(year, quarter);
22888
+ for (const { year, window } of candidates) {
22889
+ const release = formatReleaseId(year, window);
22853
22890
  const result = await probeRelease(release, fetchImpl);
22854
22891
  if (result) return result;
22855
22892
  }
@@ -23312,7 +23349,7 @@ async function backlinksRoutes(app, opts) {
23312
23349
  if (!release) {
23313
23350
  if (!opts.discoverLatestRelease) {
23314
23351
  throw validationError(
23315
- "No `release` provided and auto-discovery is unavailable on this deployment. Pass an explicit release id (e.g., cc-main-2026-jan-feb-mar)."
23352
+ "No `release` provided and auto-discovery is unavailable on this deployment. Pass an explicit release id (e.g., cc-main-2026-mar-apr-may)."
23316
23353
  );
23317
23354
  }
23318
23355
  const discovered = await opts.discoverLatestRelease();
@@ -23324,7 +23361,7 @@ async function backlinksRoutes(app, opts) {
23324
23361
  release = discovered.release;
23325
23362
  }
23326
23363
  if (!isValidReleaseId(release)) {
23327
- throw validationError("Invalid release id. Expected form: cc-main-YYYY-{jan-feb-mar,apr-may-jun,jul-aug-sep,oct-nov-dec}");
23364
+ throw validationError("Invalid release id. Expected form: cc-main-YYYY-<mon>-<mon>-<mon> (a rolling 3-month window, e.g. cc-main-2026-mar-apr-may).");
23328
23365
  }
23329
23366
  if (!opts.getBacklinksStatus || !opts.onReleaseSyncRequested) {
23330
23367
  throw missingDependency(BACKLINKS_UNSUPPORTED_MESSAGE);
@@ -31959,6 +31996,7 @@ export {
31959
31996
  getPlaceDetails,
31960
31997
  hashPlaceDetails,
31961
31998
  queueRunIfProjectIdle,
31999
+ nextRunFromCron,
31962
32000
  resolveWebhookTarget,
31963
32001
  deliverWebhook,
31964
32002
  redactNotificationUrl,
@@ -22,7 +22,7 @@ import {
22
22
  trafficConnectVercelRequestSchema,
23
23
  trafficConnectWordpressRequestSchema,
24
24
  trafficEventKindSchema
25
- } from "./chunk-34PATQZM.js";
25
+ } from "./chunk-D75O5A27.js";
26
26
 
27
27
  // src/config.ts
28
28
  import fs from "fs";
@@ -190,6 +190,9 @@ function configExists() {
190
190
  }
191
191
 
192
192
  // src/cli-error.ts
193
+ function isMachineFormat(format) {
194
+ return format === "json" || format === "jsonl";
195
+ }
193
196
  var EXIT_USER_ERROR = 1;
194
197
  var EXIT_SYSTEM_ERROR = 2;
195
198
  var CliError = class extends Error {
@@ -221,36 +224,9 @@ function isEndpointMissing(err) {
221
224
  return status === 404 || status === 405;
222
225
  }
223
226
  function printCliError(err, format) {
224
- if (format === "json") {
225
- if (err instanceof CliError) {
226
- console.error(
227
- JSON.stringify(
228
- {
229
- error: {
230
- code: err.code,
231
- message: err.message,
232
- ...err.details ? { details: err.details } : {}
233
- }
234
- },
235
- null,
236
- 2
237
- )
238
- );
239
- return;
240
- }
241
- const message = err instanceof Error ? err.message : "An unexpected error occurred";
242
- console.error(
243
- JSON.stringify(
244
- {
245
- error: {
246
- code: "CLI_ERROR",
247
- message
248
- }
249
- },
250
- null,
251
- 2
252
- )
253
- );
227
+ if (isMachineFormat(format)) {
228
+ const envelope = err instanceof CliError ? { error: { code: err.code, message: err.message, ...err.details ? { details: err.details } : {} } } : { error: { code: "CLI_ERROR", message: err instanceof Error ? err.message : "An unexpected error occurred" } };
229
+ console.error(JSON.stringify(envelope, null, format === "jsonl" ? 0 : 2));
254
230
  return;
255
231
  }
256
232
  if (err instanceof CliError && err.displayMessage) {
@@ -5155,6 +5131,20 @@ var canonryMcpTools = [
5155
5131
  openApiOperations: ["GET /api/v1/projects/{name}/report"],
5156
5132
  handler: (client2, input) => client2.getReport(input.project)
5157
5133
  }),
5134
+ defineTool({
5135
+ name: "canonry_analytics_metrics",
5136
+ title: "Get citation & mention trend",
5137
+ description: "Citation and mention rates over time for a project, bucketed adaptively (daily \u2192 monthly by span) and probe-excluded. Returns overall + per-provider window aggregates AND a per-bucket `byProvider` breakdown so you can read how each engine's cited/mentioned rate moved run-over-run \u2014 the same data the dashboard's \"Citations & mentions over time\" chart plots. Includes trend direction (improving/declining/stable) for both signals and query-set-change annotations. Filter the range with `window` (7d/30d/90d/all).",
5138
+ access: "read",
5139
+ tier: "monitoring",
5140
+ inputSchema: z2.object({
5141
+ project: projectNameSchema,
5142
+ window: analyticsWindowSchema.optional().describe("Time range: 7d, 30d, 90d, or all (default all).")
5143
+ }),
5144
+ annotations: readAnnotations(),
5145
+ openApiOperations: ["GET /api/v1/projects/{name}/analytics/metrics"],
5146
+ handler: (client2, input) => client2.getAnalyticsMetrics(input.project, input.window)
5147
+ }),
5158
5148
  defineTool({
5159
5149
  name: "canonry_search",
5160
5150
  title: "Search project (composite)",
@@ -6256,6 +6246,7 @@ export {
6256
6246
  saveConfig,
6257
6247
  saveConfigPatch,
6258
6248
  configExists,
6249
+ isMachineFormat,
6259
6250
  EXIT_USER_ERROR,
6260
6251
  EXIT_SYSTEM_ERROR,
6261
6252
  CliError,