@ainyc/canonry 2.13.2 → 2.14.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-CKzK0os-.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-CAewPdsZ.css">
15
+ <script type="module" crossorigin src="./assets/index-BwFUCV6e.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-U2SLimrz.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -59,7 +59,7 @@ import {
59
59
  visibilityStateFromAnswerMentioned,
60
60
  windowCutoff,
61
61
  wordpressEnvSchema
62
- } from "./chunk-XJS7NALL.js";
62
+ } from "./chunk-RNMMN2WI.js";
63
63
  import {
64
64
  IntelligenceService,
65
65
  agentMemory,
@@ -5371,15 +5371,14 @@ var routeCatalog = [
5371
5371
  method: "post",
5372
5372
  path: "/api/v1/backlinks/syncs",
5373
5373
  summary: "Queue a workspace-wide Common Crawl release sync",
5374
- description: "Creates a `cc_release_syncs` row and fires the sync callback. Idempotent: an existing in-flight row for the same release is returned.",
5374
+ description: "Creates a `cc_release_syncs` row and fires the sync callback. Idempotent: an existing in-flight row for the same release is returned. When `release` is omitted, the server auto-discovers the latest available Common Crawl release.",
5375
5375
  tags: ["backlinks"],
5376
5376
  requestBody: {
5377
- required: true,
5377
+ required: false,
5378
5378
  content: {
5379
5379
  "application/json": {
5380
5380
  schema: {
5381
5381
  type: "object",
5382
- required: ["release"],
5383
5382
  properties: {
5384
5383
  release: stringSchema
5385
5384
  }
@@ -5422,6 +5421,17 @@ var routeCatalog = [
5422
5421
  200: { description: "Cached release metadata returned." }
5423
5422
  }
5424
5423
  },
5424
+ {
5425
+ method: "get",
5426
+ path: "/api/v1/backlinks/latest-release",
5427
+ summary: "Auto-discover the latest available Common Crawl hyperlinkgraph release",
5428
+ description: "Probes Common Crawl by HEAD-checking quarterly release slugs and returns the newest one published. The local server caches the result for ~5 minutes so repeated calls do not hammer Common Crawl.",
5429
+ tags: ["backlinks"],
5430
+ responses: {
5431
+ 200: { description: "Latest available release, or null when no candidate slug responded." },
5432
+ 422: { description: "Backlinks feature is not available on this deployment." }
5433
+ }
5434
+ },
5425
5435
  {
5426
5436
  method: "delete",
5427
5437
  path: "/api/v1/backlinks/cache/{release}",
@@ -10712,6 +10722,60 @@ function forwardDomain(revDomain) {
10712
10722
  function isValidReleaseId(id) {
10713
10723
  return RELEASE_ID_REGEX.test(id);
10714
10724
  }
10725
+ function formatReleaseId(year, quarter) {
10726
+ return `cc-main-${year}-${quarter}`;
10727
+ }
10728
+
10729
+ // ../integration-commoncrawl/src/release-discovery.ts
10730
+ var QUARTERS = [
10731
+ "oct-nov-dec",
10732
+ "jul-aug-sep",
10733
+ "apr-may-jun",
10734
+ "jan-feb-mar"
10735
+ ];
10736
+ function probeCandidates(now, maxBack) {
10737
+ const year = now.getUTCFullYear();
10738
+ const out = [];
10739
+ for (let y = year; y >= year - maxBack; y--) {
10740
+ for (const q of QUARTERS) {
10741
+ out.push({ year: y, quarter: q });
10742
+ }
10743
+ }
10744
+ return out;
10745
+ }
10746
+ async function probeRelease(release, fetchImpl = fetch) {
10747
+ const paths = ccReleasePaths(release);
10748
+ const [vertex, edges] = await Promise.all([
10749
+ fetchImpl(paths.vertexUrl, { method: "HEAD" }),
10750
+ fetchImpl(paths.edgesUrl, { method: "HEAD" })
10751
+ ]);
10752
+ if (!vertex.ok || !edges.ok) return null;
10753
+ return {
10754
+ release,
10755
+ vertexUrl: paths.vertexUrl,
10756
+ edgesUrl: paths.edgesUrl,
10757
+ vertexBytes: parseContentLength(vertex.headers.get("content-length")),
10758
+ edgesBytes: parseContentLength(edges.headers.get("content-length")),
10759
+ lastModified: vertex.headers.get("last-modified")
10760
+ };
10761
+ }
10762
+ async function probeLatestRelease(opts = {}) {
10763
+ const now = opts.now ?? /* @__PURE__ */ new Date();
10764
+ const maxBack = opts.maxQuartersBack ?? 3;
10765
+ const fetchImpl = opts.fetchImpl ?? fetch;
10766
+ const candidates = probeCandidates(now, maxBack);
10767
+ for (const { year, quarter } of candidates) {
10768
+ const release = formatReleaseId(year, quarter);
10769
+ const result = await probeRelease(release, fetchImpl);
10770
+ if (result) return result;
10771
+ }
10772
+ return null;
10773
+ }
10774
+ function parseContentLength(value) {
10775
+ if (!value) return null;
10776
+ const n = Number.parseInt(value, 10);
10777
+ return Number.isFinite(n) ? n : null;
10778
+ }
10715
10779
 
10716
10780
  // ../integration-commoncrawl/src/downloader.ts
10717
10781
  import { createHash } from "crypto";
@@ -10739,7 +10803,7 @@ async function downloadFile(opts) {
10739
10803
  if (!res.ok || !res.body) {
10740
10804
  throw new Error(`HTTP ${res.status} ${res.statusText} for ${opts.url}`);
10741
10805
  }
10742
- const total = parseContentLength(res.headers.get("content-length"));
10806
+ const total = parseContentLength2(res.headers.get("content-length"));
10743
10807
  const hasher = createHash("sha256");
10744
10808
  let bytes = 0;
10745
10809
  const hashAndCount = new Transform({
@@ -10790,7 +10854,7 @@ async function unlinkIfExists(p) {
10790
10854
  } catch {
10791
10855
  }
10792
10856
  }
10793
- function parseContentLength(value) {
10857
+ function parseContentLength2(value) {
10794
10858
  if (!value) return null;
10795
10859
  const n = Number.parseInt(value, 10);
10796
10860
  return Number.isFinite(n) ? n : null;
@@ -11093,8 +11157,22 @@ async function backlinksRoutes(app, opts) {
11093
11157
  return reply.status(200).send(result);
11094
11158
  });
11095
11159
  app.post("/backlinks/syncs", async (request, reply) => {
11096
- const release = request.body?.release;
11097
- if (!release || !isValidReleaseId(release)) {
11160
+ let release = request.body?.release;
11161
+ if (!release) {
11162
+ if (!opts.discoverLatestRelease) {
11163
+ throw validationError(
11164
+ "No `release` provided and auto-discovery is unavailable on this deployment. Pass an explicit release id (e.g., cc-main-2026-jan-feb-mar)."
11165
+ );
11166
+ }
11167
+ const discovered = await opts.discoverLatestRelease();
11168
+ if (!discovered) {
11169
+ throw validationError(
11170
+ "Could not auto-discover the latest Common Crawl release. Pass an explicit `release` body parameter."
11171
+ );
11172
+ }
11173
+ release = discovered.release;
11174
+ }
11175
+ if (!isValidReleaseId(release)) {
11098
11176
  throw validationError("Invalid release id. Expected form: cc-main-YYYY-{jan-feb-mar,apr-may-jun,jul-aug-sep,oct-nov-dec}");
11099
11177
  }
11100
11178
  if (!opts.getBacklinksStatus || !opts.onReleaseSyncRequested) {
@@ -11145,6 +11223,13 @@ async function backlinksRoutes(app, opts) {
11145
11223
  const releases = opts.listCachedReleases?.() ?? [];
11146
11224
  return reply.send(releases);
11147
11225
  });
11226
+ app.get("/backlinks/latest-release", async (_request, reply) => {
11227
+ if (!opts.discoverLatestRelease) {
11228
+ throw missingDependency(BACKLINKS_UNSUPPORTED_MESSAGE);
11229
+ }
11230
+ const discovered = await opts.discoverLatestRelease();
11231
+ return reply.send(discovered);
11232
+ });
11148
11233
  app.delete("/backlinks/cache/:release", async (request, reply) => {
11149
11234
  const release = request.params.release;
11150
11235
  if (!isValidReleaseId(release)) {
@@ -11941,7 +12026,8 @@ async function apiRoutes(app, opts) {
11941
12026
  onReleaseSyncRequested: opts.onReleaseSyncRequested,
11942
12027
  onBacklinkExtractRequested: opts.onBacklinkExtractRequested,
11943
12028
  onBacklinksPruneCache: opts.onBacklinksPruneCache,
11944
- listCachedReleases: opts.listCachedReleases
12029
+ listCachedReleases: opts.listCachedReleases,
12030
+ discoverLatestRelease: opts.discoverLatestRelease
11945
12031
  });
11946
12032
  await api.register(doctorRoutes, {
11947
12033
  googleConnectionStore: opts.googleConnectionStore,
@@ -19112,6 +19198,28 @@ async function createServer(opts) {
19112
19198
  }));
19113
19199
  return reply.status(204).send();
19114
19200
  });
19201
+ const LATEST_RELEASE_TTL_MS = 5 * 60 * 1e3;
19202
+ let latestReleaseCache = null;
19203
+ const discoverLatestRelease = async () => {
19204
+ const now = Date.now();
19205
+ if (latestReleaseCache && latestReleaseCache.expiresAt > now) {
19206
+ return latestReleaseCache.value;
19207
+ }
19208
+ const probed = await probeLatestRelease().catch((err) => {
19209
+ app.log.warn({ err }, "Common Crawl latest-release probe failed");
19210
+ return null;
19211
+ });
19212
+ const value = probed ? {
19213
+ release: probed.release,
19214
+ vertexUrl: probed.vertexUrl,
19215
+ edgesUrl: probed.edgesUrl,
19216
+ vertexBytes: probed.vertexBytes,
19217
+ edgesBytes: probed.edgesBytes,
19218
+ lastModified: probed.lastModified
19219
+ } : null;
19220
+ latestReleaseCache = { value, expiresAt: now + LATEST_RELEASE_TTL_MS };
19221
+ return value;
19222
+ };
19115
19223
  await app.register(apiRoutes, {
19116
19224
  db: opts.db,
19117
19225
  routePrefix: apiPrefix,
@@ -19220,6 +19328,7 @@ async function createServer(opts) {
19220
19328
  };
19221
19329
  });
19222
19330
  },
19331
+ discoverLatestRelease,
19223
19332
  openApiInfo: {
19224
19333
  title: "Canonry API",
19225
19334
  version: PKG_VERSION,
@@ -2409,7 +2409,10 @@ var ApiClient = class {
2409
2409
  return this.request("POST", "/backlinks/install");
2410
2410
  }
2411
2411
  async backlinksTriggerSync(release) {
2412
- return this.request("POST", "/backlinks/syncs", { release });
2412
+ return this.request("POST", "/backlinks/syncs", release ? { release } : void 0);
2413
+ }
2414
+ async backlinksLatestRelease() {
2415
+ return this.request("GET", "/backlinks/latest-release");
2413
2416
  }
2414
2417
  async backlinksLatestSync() {
2415
2418
  return this.request("GET", "/backlinks/syncs/latest");
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-DHMCIJMQ.js";
20
+ } from "./chunk-LD7Y4K4G.js";
21
21
  import {
22
22
  CcReleaseSyncStatuses,
23
23
  CheckScopes,
@@ -44,7 +44,7 @@ import {
44
44
  saveConfig,
45
45
  saveConfigPatch,
46
46
  usageError
47
- } from "./chunk-XJS7NALL.js";
47
+ } from "./chunk-RNMMN2WI.js";
48
48
  import {
49
49
  apiKeys,
50
50
  competitors,
@@ -638,8 +638,38 @@ async function backlinksSync(opts) {
638
638
  return;
639
639
  }
640
640
  if (opts.wait) process.stderr.write("\n");
641
+ if (!opts.release) {
642
+ process.stderr.write(`Auto-discovered release: ${sync.release}
643
+ `);
644
+ }
641
645
  console.log(formatSync(final));
642
646
  }
647
+ function formatBytesShort(n) {
648
+ if (n === null) return "\u2014";
649
+ if (n >= 1e9) return `${(n / 1e9).toFixed(1)} GB`;
650
+ if (n >= 1e6) return `${(n / 1e6).toFixed(1)} MB`;
651
+ if (n >= 1e3) return `${(n / 1e3).toFixed(1)} KB`;
652
+ return `${n} B`;
653
+ }
654
+ function formatLatestRelease(result) {
655
+ if (!result) return "No release discovered (Common Crawl probe returned no candidates).";
656
+ const lines = [];
657
+ lines.push(`Release: ${result.release}`);
658
+ lines.push(`Vertex: ${result.vertexUrl}`);
659
+ lines.push(` ${formatBytesShort(result.vertexBytes)}`);
660
+ lines.push(`Edges: ${result.edgesUrl}`);
661
+ lines.push(` ${formatBytesShort(result.edgesBytes)}`);
662
+ if (result.lastModified) lines.push(`Last modified: ${result.lastModified}`);
663
+ return lines.join("\n");
664
+ }
665
+ async function backlinksLatestRelease(opts = {}) {
666
+ const result = await getClient().backlinksLatestRelease();
667
+ if (opts.format === "json") {
668
+ printJson(result);
669
+ return;
670
+ }
671
+ console.log(formatLatestRelease(result));
672
+ }
643
673
  async function backlinksStatus(opts = {}) {
644
674
  const sync = await getClient().backlinksLatestSync();
645
675
  if (opts.format === "json") {
@@ -715,19 +745,14 @@ var BACKLINKS_CLI_COMMANDS = [
715
745
  },
716
746
  {
717
747
  path: ["backlinks", "sync"],
718
- usage: "canonry backlinks sync --release <id> [--wait] [--format json]",
748
+ usage: "canonry backlinks sync [--release <id>] [--wait] [--format json]",
719
749
  options: {
720
750
  release: stringOption(),
721
751
  wait: { type: "boolean" }
722
752
  },
723
753
  run: async (input) => {
724
- const release = requireStringOption(input, "release", {
725
- message: "--release is required",
726
- usage: "canonry backlinks sync --release <id> [--wait]",
727
- command: "backlinks sync"
728
- });
729
754
  await backlinksSync({
730
- release,
755
+ release: getString(input.values, "release"),
731
756
  wait: getBoolean(input.values, "wait"),
732
757
  format: input.format
733
758
  });
@@ -775,6 +800,14 @@ var BACKLINKS_CLI_COMMANDS = [
775
800
  await backlinksReleases({ format: input.format });
776
801
  }
777
802
  },
803
+ {
804
+ path: ["backlinks", "releases", "latest"],
805
+ usage: "canonry backlinks releases latest [--format json]",
806
+ options: {},
807
+ run: async (input) => {
808
+ await backlinksLatestRelease({ format: input.format });
809
+ }
810
+ },
778
811
  {
779
812
  path: ["backlinks", "extract"],
780
813
  usage: "canonry backlinks extract <project> [--release <id>] [--wait] [--format json]",
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-DHMCIJMQ.js";
3
+ } from "./chunk-LD7Y4K4G.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-XJS7NALL.js";
6
+ } from "./chunk-RNMMN2WI.js";
7
7
  import "./chunk-UM6RDSRJ.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
  export {
package/dist/mcp.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  projectUpsertRequestSchema,
11
11
  runTriggerRequestSchema,
12
12
  scheduleUpsertRequestSchema
13
- } from "./chunk-XJS7NALL.js";
13
+ } from "./chunk-RNMMN2WI.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
 
16
16
  // src/mcp/cli.ts
@@ -412,6 +412,17 @@ var canonryMcpTools = [
412
412
  openApiOperations: ["GET /api/v1/projects/{name}/schedule"],
413
413
  handler: (client, input) => client.getSchedule(input.project)
414
414
  }),
415
+ defineTool({
416
+ name: "canonry_backlinks_latest_release",
417
+ title: "Discover latest Common Crawl release",
418
+ description: "Probes Common Crawl to find the latest published hyperlinkgraph release. Returns the release id and file URLs/sizes ready to feed into a backlinks sync (or null if no candidate slug responded).",
419
+ access: "read",
420
+ tier: "setup",
421
+ inputSchema: emptyInputSchema,
422
+ annotations: readAnnotations(true),
423
+ openApiOperations: ["GET /api/v1/backlinks/latest-release"],
424
+ handler: (client) => client.backlinksLatestRelease()
425
+ }),
415
426
  defineTool({
416
427
  name: "canonry_settings_get",
417
428
  title: "Get settings",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "2.13.2",
3
+ "version": "2.14.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",
@@ -59,21 +59,21 @@
59
59
  "@types/node-cron": "^3.0.11",
60
60
  "tsup": "^8.5.1",
61
61
  "tsx": "^4.19.0",
62
+ "@ainyc/canonry-config": "0.0.0",
62
63
  "@ainyc/canonry-api-routes": "0.0.0",
64
+ "@ainyc/canonry-contracts": "0.0.0",
63
65
  "@ainyc/canonry-db": "0.0.0",
64
- "@ainyc/canonry-config": "0.0.0",
65
- "@ainyc/canonry-integration-bing": "0.0.0",
66
66
  "@ainyc/canonry-intelligence": "0.0.0",
67
+ "@ainyc/canonry-integration-bing": "0.0.0",
67
68
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
68
69
  "@ainyc/canonry-integration-google": "0.0.0",
69
70
  "@ainyc/canonry-integration-wordpress": "0.0.0",
70
71
  "@ainyc/canonry-provider-cdp": "0.0.0",
71
72
  "@ainyc/canonry-provider-claude": "0.0.0",
72
- "@ainyc/canonry-contracts": "0.0.0",
73
73
  "@ainyc/canonry-provider-gemini": "0.0.0",
74
- "@ainyc/canonry-provider-openai": "0.0.0",
75
74
  "@ainyc/canonry-provider-local": "0.0.0",
76
- "@ainyc/canonry-provider-perplexity": "0.0.0"
75
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
76
+ "@ainyc/canonry-provider-openai": "0.0.0"
77
77
  },
78
78
  "scripts": {
79
79
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",