@ainyc/canonry 3.2.0 → 3.2.2

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.
@@ -62,7 +62,7 @@ import {
62
62
  visibilityStateFromAnswerMentioned,
63
63
  windowCutoff,
64
64
  wordpressEnvSchema
65
- } from "./chunk-YGIWXQGM.js";
65
+ } from "./chunk-TBFRAJU2.js";
66
66
  import {
67
67
  IntelligenceService,
68
68
  agentMemory,
@@ -8694,6 +8694,13 @@ function gaLog(level, action, ctx) {
8694
8694
  const stream = level === "error" ? process.stderr : process.stdout;
8695
8695
  stream.write(JSON.stringify(entry) + "\n");
8696
8696
  }
8697
+ function formatSharePct(numerator, total) {
8698
+ if (total <= 0 || numerator <= 0) return "0%";
8699
+ const pct = numerator / total * 100;
8700
+ const rounded = Math.round(pct);
8701
+ if (rounded === 0) return "<1%";
8702
+ return `${rounded}%`;
8703
+ }
8697
8704
  async function refreshOAuthTokenIfNeeded(googleStore, authConfig, canonicalDomain, oauthConn) {
8698
8705
  const expiresAt = oauthConn.tokenExpiresAt ? new Date(oauthConn.tokenExpiresAt).getTime() : 0;
8699
8706
  const fiveMinutes = 5 * 60 * 1e3;
@@ -9179,6 +9186,11 @@ async function ga4Routes(app, opts) {
9179
9186
  aiSharePctBySession: total > 0 ? Math.round((aiBySession?.sessions ?? 0) / total * 100) : 0,
9180
9187
  directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
9181
9188
  socialSharePct: total > 0 ? Math.round((socialTotals?.sessions ?? 0) / total * 100) : 0,
9189
+ organicSharePctDisplay: formatSharePct(summaryRow?.totalOrganicSessions ?? 0, total),
9190
+ aiSharePctDisplay: formatSharePct(aiDeduped?.sessions ?? 0, total),
9191
+ aiSharePctBySessionDisplay: formatSharePct(aiBySession?.sessions ?? 0, total),
9192
+ directSharePctDisplay: formatSharePct(totalDirectSessions, total),
9193
+ socialSharePctDisplay: formatSharePct(socialTotals?.sessions ?? 0, total),
9182
9194
  lastSyncedAt: latestSync?.syncedAt ?? null,
9183
9195
  periodStart: (() => {
9184
9196
  const start = cutoffDate ?? summaryMeta?.periodStart ?? null;
@@ -11021,7 +11033,7 @@ async function wordpressRoutes(app, opts) {
11021
11033
 
11022
11034
  // ../api-routes/src/backlinks.ts
11023
11035
  import crypto18 from "crypto";
11024
- import { and as and9, asc as asc2, desc as desc10, eq as eq21, sql as sql6 } from "drizzle-orm";
11036
+ import { and as and10, asc as asc2, desc as desc10, eq as eq21, sql as sql6 } from "drizzle-orm";
11025
11037
 
11026
11038
  // ../integration-commoncrawl/src/constants.ts
11027
11039
  import os2 from "os";
@@ -11417,6 +11429,35 @@ function pruneCachedRelease(release, opts = {}) {
11417
11429
  fs5.rmSync(dir, { recursive: true, force: true });
11418
11430
  }
11419
11431
 
11432
+ // ../api-routes/src/backlinks-filter.ts
11433
+ import { and as and9, ne, notLike } from "drizzle-orm";
11434
+ var BACKLINK_FILTER_PATTERNS = [
11435
+ "*.google.com",
11436
+ "*.googleusercontent.com",
11437
+ "*.translate.goog",
11438
+ "*.bing.com",
11439
+ "*.yandex.com",
11440
+ "*.yandex.ru",
11441
+ "*.baidu.com",
11442
+ "*.duckduckgo.com",
11443
+ "*.archive.org"
11444
+ ];
11445
+ function backlinkCrawlerExclusionClause() {
11446
+ const conditions = [];
11447
+ for (const pattern of BACKLINK_FILTER_PATTERNS) {
11448
+ if (pattern.startsWith("*.")) {
11449
+ const suffix = pattern.slice(2);
11450
+ conditions.push(ne(backlinkDomains.linkingDomain, suffix));
11451
+ conditions.push(notLike(backlinkDomains.linkingDomain, `%.${suffix}`));
11452
+ } else {
11453
+ conditions.push(ne(backlinkDomains.linkingDomain, pattern));
11454
+ }
11455
+ }
11456
+ const combined = and9(...conditions);
11457
+ if (!combined) throw new Error("BACKLINK_FILTER_PATTERNS is unexpectedly empty");
11458
+ return combined;
11459
+ }
11460
+
11420
11461
  // ../api-routes/src/backlinks.ts
11421
11462
  var BACKLINKS_UNSUPPORTED_MESSAGE = "Backlinks sync and install are only available from a local canonry install. Run `canonry backlinks install` locally to use this feature.";
11422
11463
  var NON_TERMINAL_SYNC_STATUSES = /* @__PURE__ */ new Set([
@@ -11473,9 +11514,47 @@ function mapRunRow(row) {
11473
11514
  };
11474
11515
  }
11475
11516
  function latestSummaryForProject(db, projectId, release) {
11476
- const condition = release ? and9(eq21(backlinkSummaries.projectId, projectId), eq21(backlinkSummaries.release, release)) : eq21(backlinkSummaries.projectId, projectId);
11517
+ const condition = release ? and10(eq21(backlinkSummaries.projectId, projectId), eq21(backlinkSummaries.release, release)) : eq21(backlinkSummaries.projectId, projectId);
11477
11518
  return db.select().from(backlinkSummaries).where(condition).orderBy(desc10(backlinkSummaries.queriedAt)).limit(1).get();
11478
11519
  }
11520
+ function parseExcludeCrawlers(value) {
11521
+ if (!value) return false;
11522
+ const lower = value.toLowerCase();
11523
+ return lower === "1" || lower === "true" || lower === "yes";
11524
+ }
11525
+ function computeFilteredSummary(db, base) {
11526
+ const baseDomainCondition = and10(
11527
+ eq21(backlinkDomains.projectId, base.projectId),
11528
+ eq21(backlinkDomains.release, base.release)
11529
+ );
11530
+ const filteredCondition = and10(baseDomainCondition, backlinkCrawlerExclusionClause());
11531
+ const unfilteredAgg = db.select({
11532
+ count: sql6`count(*)`,
11533
+ total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
11534
+ }).from(backlinkDomains).where(baseDomainCondition).get();
11535
+ const filteredAgg = db.select({
11536
+ count: sql6`count(*)`,
11537
+ total: sql6`coalesce(sum(${backlinkDomains.numHosts}), 0)`
11538
+ }).from(backlinkDomains).where(filteredCondition).get();
11539
+ const top10Rows = db.select({ numHosts: backlinkDomains.numHosts }).from(backlinkDomains).where(filteredCondition).orderBy(desc10(backlinkDomains.numHosts)).limit(10).all();
11540
+ const totalLinkingDomains = Number(filteredAgg?.count ?? 0);
11541
+ const totalHosts = Number(filteredAgg?.total ?? 0);
11542
+ const unfilteredLinkingDomains = Number(unfilteredAgg?.count ?? 0);
11543
+ const unfilteredHosts = Number(unfilteredAgg?.total ?? 0);
11544
+ const top10Sum = top10Rows.reduce((sum, r) => sum + r.numHosts, 0);
11545
+ const top10Share = totalHosts > 0 ? top10Sum / totalHosts : 0;
11546
+ return {
11547
+ projectId: base.projectId,
11548
+ release: base.release,
11549
+ targetDomain: base.targetDomain,
11550
+ totalLinkingDomains,
11551
+ totalHosts,
11552
+ top10HostsShare: top10Share.toFixed(6),
11553
+ queriedAt: base.queriedAt,
11554
+ excludedLinkingDomains: Math.max(0, unfilteredLinkingDomains - totalLinkingDomains),
11555
+ excludedHosts: Math.max(0, unfilteredHosts - totalHosts)
11556
+ };
11557
+ }
11479
11558
  async function backlinksRoutes(app, opts) {
11480
11559
  app.get("/backlinks/status", async (_request, reply) => {
11481
11560
  if (!opts.getBacklinksStatus) {
@@ -11608,7 +11687,9 @@ async function backlinksRoutes(app, opts) {
11608
11687
  async (request, reply) => {
11609
11688
  const project = resolveProject(app.db, request.params.name);
11610
11689
  const row = latestSummaryForProject(app.db, project.id, request.query.release);
11611
- return reply.send(row ? mapSummaryRow(row) : null);
11690
+ if (!row) return reply.send(null);
11691
+ const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
11692
+ return reply.send(excludeCrawlers ? computeFilteredSummary(app.db, row) : mapSummaryRow(row));
11612
11693
  }
11613
11694
  );
11614
11695
  app.get("/projects/:name/backlinks/domains", async (request, reply) => {
@@ -11621,17 +11702,23 @@ async function backlinksRoutes(app, opts) {
11621
11702
  }
11622
11703
  const limit = Math.min(Math.max(parseInt(request.query.limit ?? "50", 10) || 50, 1), 500);
11623
11704
  const offset = Math.max(parseInt(request.query.offset ?? "0", 10) || 0, 0);
11624
- const domainCondition = and9(
11705
+ const excludeCrawlers = parseExcludeCrawlers(request.query.excludeCrawlers);
11706
+ const baseDomainCondition = and10(
11625
11707
  eq21(backlinkDomains.projectId, project.id),
11626
11708
  eq21(backlinkDomains.release, targetRelease)
11627
11709
  );
11710
+ const domainCondition = excludeCrawlers ? and10(baseDomainCondition, backlinkCrawlerExclusionClause()) : baseDomainCondition;
11628
11711
  const totalRow = app.db.select({ count: sql6`count(*)` }).from(backlinkDomains).where(domainCondition).get();
11629
11712
  const rows = app.db.select({
11630
11713
  linkingDomain: backlinkDomains.linkingDomain,
11631
11714
  numHosts: backlinkDomains.numHosts
11632
11715
  }).from(backlinkDomains).where(domainCondition).orderBy(desc10(backlinkDomains.numHosts)).limit(limit).offset(offset).all();
11716
+ let summary = null;
11717
+ if (summaryRow) {
11718
+ summary = excludeCrawlers ? computeFilteredSummary(app.db, summaryRow) : mapSummaryRow(summaryRow);
11719
+ }
11633
11720
  const response = {
11634
- summary: summaryRow ? mapSummaryRow(summaryRow) : null,
11721
+ summary,
11635
11722
  total: Number(totalRow?.count ?? 0),
11636
11723
  rows
11637
11724
  };
@@ -14985,7 +15072,7 @@ import crypto19 from "crypto";
14985
15072
  import fs7 from "fs";
14986
15073
  import path9 from "path";
14987
15074
  import os4 from "os";
14988
- import { and as and10, eq as eq22, inArray as inArray5, sql as sql7 } from "drizzle-orm";
15075
+ import { and as and11, eq as eq22, inArray as inArray5, sql as sql7 } from "drizzle-orm";
14989
15076
 
14990
15077
  // src/citation-utils.ts
14991
15078
  function domainMatches(domain, canonicalDomain) {
@@ -15269,7 +15356,7 @@ var JobRunner = class {
15269
15356
  throw new Error(`Run ${runId} is not executable from status '${existingRun.status}'`);
15270
15357
  }
15271
15358
  if (existingRun.status === "queued") {
15272
- this.db.update(runs).set({ status: "running", startedAt: now }).where(and10(eq22(runs.id, runId), eq22(runs.status, "queued"))).run();
15359
+ this.db.update(runs).set({ status: "running", startedAt: now }).where(and11(eq22(runs.id, runId), eq22(runs.status, "queued"))).run();
15273
15360
  }
15274
15361
  this.throwIfRunCancelled(runId);
15275
15362
  const project = this.db.select().from(projects).where(eq22(projects.id, projectId)).get();
@@ -15572,7 +15659,7 @@ function getCurrentUsageDay() {
15572
15659
 
15573
15660
  // src/gsc-sync.ts
15574
15661
  import crypto20 from "crypto";
15575
- import { eq as eq23, and as and11, sql as sql8 } from "drizzle-orm";
15662
+ import { eq as eq23, and as and12, sql as sql8 } from "drizzle-orm";
15576
15663
  var log2 = createLogger("GscSync");
15577
15664
  function formatDate2(d) {
15578
15665
  return d.toISOString().split("T")[0];
@@ -15624,7 +15711,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15624
15711
  });
15625
15712
  log2.info("fetch.complete", { runId, projectId, rowCount: rows.length });
15626
15713
  db.delete(gscSearchData).where(
15627
- and11(
15714
+ and12(
15628
15715
  eq23(gscSearchData.projectId, projectId),
15629
15716
  sql8`${gscSearchData.date} >= ${startDate}`,
15630
15717
  sql8`${gscSearchData.date} <= ${endDate}`
@@ -15713,7 +15800,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15713
15800
  }
15714
15801
  }
15715
15802
  const snapshotDate = formatDate2(/* @__PURE__ */ new Date());
15716
- db.delete(gscCoverageSnapshots).where(and11(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
15803
+ db.delete(gscCoverageSnapshots).where(and12(eq23(gscCoverageSnapshots.projectId, projectId), eq23(gscCoverageSnapshots.date, snapshotDate))).run();
15717
15804
  db.insert(gscCoverageSnapshots).values({
15718
15805
  id: crypto20.randomUUID(),
15719
15806
  projectId,
@@ -15736,7 +15823,7 @@ async function executeGscSync(db, runId, projectId, opts) {
15736
15823
 
15737
15824
  // src/gsc-inspect-sitemap.ts
15738
15825
  import crypto21 from "crypto";
15739
- import { eq as eq24, and as and12 } from "drizzle-orm";
15826
+ import { eq as eq24, and as and13 } from "drizzle-orm";
15740
15827
 
15741
15828
  // src/sitemap-parser.ts
15742
15829
  var log3 = createLogger("SitemapParser");
@@ -15952,7 +16039,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
15952
16039
  }
15953
16040
  }
15954
16041
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
15955
- db.delete(gscCoverageSnapshots).where(and12(eq24(gscCoverageSnapshots.projectId, projectId), eq24(gscCoverageSnapshots.date, snapshotDate))).run();
16042
+ db.delete(gscCoverageSnapshots).where(and13(eq24(gscCoverageSnapshots.projectId, projectId), eq24(gscCoverageSnapshots.date, snapshotDate))).run();
15956
16043
  db.insert(gscCoverageSnapshots).values({
15957
16044
  id: crypto21.randomUUID(),
15958
16045
  projectId,
@@ -16163,7 +16250,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
16163
16250
  // src/commoncrawl-sync.ts
16164
16251
  import crypto23 from "crypto";
16165
16252
  import path10 from "path";
16166
- import { and as and13, eq as eq26, sql as sql9 } from "drizzle-orm";
16253
+ import { and as and14, eq as eq26, sql as sql9 } from "drizzle-orm";
16167
16254
  var log6 = createLogger("CommonCrawlSync");
16168
16255
  var INSERT_CHUNK_SIZE = 1e4;
16169
16256
  function defaultDeps() {
@@ -16354,7 +16441,7 @@ function computeSummary(rows) {
16354
16441
  // src/backlink-extract.ts
16355
16442
  import crypto24 from "crypto";
16356
16443
  import fs8 from "fs";
16357
- import { and as and14, desc as desc12, eq as eq27 } from "drizzle-orm";
16444
+ import { and as and15, desc as desc12, eq as eq27 } from "drizzle-orm";
16358
16445
  var log7 = createLogger("BacklinkExtract");
16359
16446
  function defaultDeps2() {
16360
16447
  return {
@@ -16400,7 +16487,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
16400
16487
  const targetDomain = project.canonicalDomain;
16401
16488
  db.transaction((tx) => {
16402
16489
  tx.delete(backlinkDomains).where(
16403
- and14(eq27(backlinkDomains.projectId, projectId), eq27(backlinkDomains.release, release))
16490
+ and15(eq27(backlinkDomains.projectId, projectId), eq27(backlinkDomains.release, release))
16404
16491
  ).run();
16405
16492
  if (rows.length > 0) {
16406
16493
  const values = rows.map((r) => ({
@@ -16656,7 +16743,7 @@ var Scheduler = class {
16656
16743
  };
16657
16744
 
16658
16745
  // src/notifier.ts
16659
- import { eq as eq29, desc as desc13, and as and15, or as or3 } from "drizzle-orm";
16746
+ import { eq as eq29, desc as desc13, and as and16, or as or3 } from "drizzle-orm";
16660
16747
  import crypto25 from "crypto";
16661
16748
  var log9 = createLogger("Notifier");
16662
16749
  var Notifier = class {
@@ -16762,7 +16849,7 @@ var Notifier = class {
16762
16849
  }
16763
16850
  computeTransitions(runId, projectId) {
16764
16851
  const recentRuns = this.db.select().from(runs).where(
16765
- and15(
16852
+ and16(
16766
16853
  eq29(runs.projectId, projectId),
16767
16854
  or3(eq29(runs.status, "completed"), eq29(runs.status, "partial"))
16768
16855
  )
@@ -17248,7 +17335,7 @@ function resolveSessionProviderAndModel(config, opts) {
17248
17335
 
17249
17336
  // src/agent/memory-store.ts
17250
17337
  import crypto26 from "crypto";
17251
- import { and as and16, desc as desc14, eq as eq30, like as like2, sql as sql10 } from "drizzle-orm";
17338
+ import { and as and17, desc as desc14, eq as eq30, like as like2, sql as sql10 } from "drizzle-orm";
17252
17339
  var COMPACTION_KEY_PREFIX = "compaction:";
17253
17340
  var COMPACTION_NOTES_PER_SESSION = 3;
17254
17341
  function rowToDto(row) {
@@ -17293,12 +17380,12 @@ function upsertMemoryEntry(db, args) {
17293
17380
  updatedAt: now
17294
17381
  }
17295
17382
  }).run();
17296
- const row = db.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, args.key))).get();
17383
+ const row = db.select().from(agentMemory).where(and17(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, args.key))).get();
17297
17384
  if (!row) throw new Error("memory upsert produced no row");
17298
17385
  return rowToDto(row);
17299
17386
  }
17300
17387
  function deleteMemoryEntry(db, projectId, key) {
17301
- const result = db.delete(agentMemory).where(and16(eq30(agentMemory.projectId, projectId), eq30(agentMemory.key, key))).run();
17388
+ const result = db.delete(agentMemory).where(and17(eq30(agentMemory.projectId, projectId), eq30(agentMemory.key, key))).run();
17302
17389
  const changes = result.changes ?? 0;
17303
17390
  return changes > 0;
17304
17391
  }
@@ -17327,7 +17414,7 @@ function writeCompactionNote(db, args) {
17327
17414
  }).run();
17328
17415
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
17329
17416
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
17330
- and16(
17417
+ and17(
17331
17418
  eq30(agentMemory.projectId, args.projectId),
17332
17419
  like2(agentMemory.key, `${sessionPrefix}%`)
17333
17420
  )
@@ -17336,7 +17423,7 @@ function writeCompactionNote(db, args) {
17336
17423
  if (stale.length > 0) {
17337
17424
  tx.delete(agentMemory).where(sql10`${agentMemory.id} IN (${sql10.join(stale.map((s) => sql10`${s}`), sql10`, `)})`).run();
17338
17425
  }
17339
- const row = tx.select().from(agentMemory).where(and16(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, key))).get();
17426
+ const row = tx.select().from(agentMemory).where(and17(eq30(agentMemory.projectId, args.projectId), eq30(agentMemory.key, key))).get();
17340
17427
  if (row) inserted = rowToDto(row);
17341
17428
  });
17342
17429
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -1342,6 +1342,16 @@ var ga4TrafficSummaryDtoSchema = z12.object({
1342
1342
  directSharePct: z12.number(),
1343
1343
  /** Social sessions as a percentage of total sessions (0–100, rounded). */
1344
1344
  socialSharePct: z12.number(),
1345
+ /** Display string for organicSharePct ('10%' or '<1%' when there are sessions but the rounded pct is 0). */
1346
+ organicSharePctDisplay: z12.string(),
1347
+ /** Display string for aiSharePct ('5%' or '<1%' when there are sessions but the rounded pct is 0). */
1348
+ aiSharePctDisplay: z12.string(),
1349
+ /** Display string for aiSharePctBySession ('5%' or '<1%' when there are sessions but the rounded pct is 0). */
1350
+ aiSharePctBySessionDisplay: z12.string(),
1351
+ /** Display string for directSharePct ('20%' or '<1%' when there are sessions but the rounded pct is 0). */
1352
+ directSharePctDisplay: z12.string(),
1353
+ /** Display string for socialSharePct ('15%' or '<1%' when there are sessions but the rounded pct is 0). */
1354
+ socialSharePctDisplay: z12.string(),
1345
1355
  lastSyncedAt: z12.string().nullable()
1346
1356
  });
1347
1357
  var ga4AiReferralHistoryEntrySchema = z12.object({
@@ -1567,7 +1577,12 @@ var backlinkSummaryDtoSchema = z14.object({
1567
1577
  totalLinkingDomains: z14.number().int(),
1568
1578
  totalHosts: z14.number().int(),
1569
1579
  top10HostsShare: z14.string(),
1570
- queriedAt: z14.string()
1580
+ queriedAt: z14.string(),
1581
+ // Populated when the response is filtered (e.g. ?excludeCrawlers=1).
1582
+ // Counts the rows omitted from totalLinkingDomains/totalHosts so callers
1583
+ // can show "N hidden" hints without re-deriving them.
1584
+ excludedLinkingDomains: z14.number().int().optional(),
1585
+ excludedHosts: z14.number().int().optional()
1571
1586
  });
1572
1587
  var backlinkListResponseSchema = z14.object({
1573
1588
  summary: backlinkSummaryDtoSchema.nullable(),
@@ -2640,15 +2655,19 @@ var ApiClient = class {
2640
2655
  async backlinksExtract(project, release) {
2641
2656
  return this.request("POST", `/projects/${encodeURIComponent(project)}/backlinks/extract`, release ? { release } : {});
2642
2657
  }
2643
- async backlinksSummary(project, release) {
2644
- const qs = release ? `?release=${encodeURIComponent(release)}` : "";
2645
- return this.request("GET", `/projects/${encodeURIComponent(project)}/backlinks/summary${qs}`);
2658
+ async backlinksSummary(project, opts = {}) {
2659
+ const qs = new URLSearchParams();
2660
+ if (opts.release) qs.set("release", opts.release);
2661
+ if (opts.excludeCrawlers) qs.set("excludeCrawlers", "1");
2662
+ const suffix = qs.toString() ? `?${qs.toString()}` : "";
2663
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/backlinks/summary${suffix}`);
2646
2664
  }
2647
2665
  async backlinksDomains(project, opts = {}) {
2648
2666
  const qs = new URLSearchParams();
2649
2667
  if (opts.limit !== void 0) qs.set("limit", String(opts.limit));
2650
2668
  if (opts.offset !== void 0) qs.set("offset", String(opts.offset));
2651
2669
  if (opts.release) qs.set("release", opts.release);
2670
+ if (opts.excludeCrawlers) qs.set("excludeCrawlers", "1");
2652
2671
  const suffix = qs.toString() ? `?${qs.toString()}` : "";
2653
2672
  return this.request("GET", `/projects/${encodeURIComponent(project)}/backlinks/domains${suffix}`);
2654
2673
  }
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-33ATDBGM.js";
20
+ } from "./chunk-QA5HRDRS.js";
21
21
  import {
22
22
  CcReleaseSyncStatuses,
23
23
  CheckScopes,
@@ -45,7 +45,7 @@ import {
45
45
  saveConfig,
46
46
  saveConfigPatch,
47
47
  usageError
48
- } from "./chunk-YGIWXQGM.js";
48
+ } from "./chunk-TBFRAJU2.js";
49
49
  import {
50
50
  apiKeys,
51
51
  competitors,
@@ -728,6 +728,9 @@ function formatSummaryAndDomains(project, response) {
728
728
  lines.push(`Linking domains: ${s.totalLinkingDomains}`);
729
729
  lines.push(`Total hosts: ${s.totalHosts}`);
730
730
  lines.push(`Top-10 share: ${s.top10HostsShare}`);
731
+ if (s.excludedLinkingDomains !== void 0 && s.excludedLinkingDomains > 0) {
732
+ lines.push(`Excluded: ${s.excludedLinkingDomains} crawler/proxy domains (${s.excludedHosts ?? 0} hosts)`);
733
+ }
731
734
  if (response.rows.length > 0) {
732
735
  lines.push("");
733
736
  lines.push(`Top ${response.rows.length} linking domains (of ${response.total}):`);
@@ -861,7 +864,8 @@ async function backlinksList(opts) {
861
864
  const client = getClient();
862
865
  const response = await client.backlinksDomains(opts.project, {
863
866
  limit: opts.limit ?? 50,
864
- release: opts.release
867
+ release: opts.release,
868
+ excludeCrawlers: opts.excludeCrawlers
865
869
  });
866
870
  if (opts.format === "json") {
867
871
  printJson(response);
@@ -943,16 +947,17 @@ var BACKLINKS_CLI_COMMANDS = [
943
947
  },
944
948
  {
945
949
  path: ["backlinks", "list"],
946
- usage: "canonry backlinks list <project> [--limit <n>] [--release <id>] [--format json]",
950
+ usage: "canonry backlinks list <project> [--limit <n>] [--release <id>] [--exclude-crawlers] [--format json]",
947
951
  options: {
948
952
  limit: stringOption(),
949
- release: stringOption()
953
+ release: stringOption(),
954
+ "exclude-crawlers": { type: "boolean" }
950
955
  },
951
956
  run: async (input) => {
952
957
  const project = requireProject(
953
958
  input,
954
959
  "backlinks list",
955
- "canonry backlinks list <project> [--limit <n>] [--release <id>]"
960
+ "canonry backlinks list <project> [--limit <n>] [--release <id>] [--exclude-crawlers]"
956
961
  );
957
962
  const limit = parseIntegerOption(input, "limit", {
958
963
  message: "--limit must be an integer",
@@ -963,6 +968,7 @@ var BACKLINKS_CLI_COMMANDS = [
963
968
  project,
964
969
  limit,
965
970
  release: getString(input.values, "release"),
971
+ excludeCrawlers: getBoolean(input.values, "exclude-crawlers"),
966
972
  format: input.format
967
973
  });
968
974
  }
@@ -2222,6 +2228,11 @@ async function gaAttribution(project, opts) {
2222
2228
  socialSharePct: traffic.socialSharePct,
2223
2229
  organicSharePct: traffic.organicSharePct,
2224
2230
  directSharePct: traffic.directSharePct,
2231
+ organicSharePctDisplay: traffic.organicSharePctDisplay,
2232
+ aiSharePctDisplay: traffic.aiSharePctDisplay,
2233
+ aiSharePctBySessionDisplay: traffic.aiSharePctBySessionDisplay,
2234
+ socialSharePctDisplay: traffic.socialSharePctDisplay,
2235
+ directSharePctDisplay: traffic.directSharePctDisplay,
2225
2236
  aiReferrals: traffic.aiReferrals,
2226
2237
  aiReferralLandingPages: traffic.aiReferralLandingPages,
2227
2238
  socialReferrals: traffic.socialReferrals,
@@ -2239,10 +2250,10 @@ async function gaAttribution(project, opts) {
2239
2250
  console.log(` Total Users: ${traffic.totalUsers}`);
2240
2251
  console.log();
2241
2252
  console.log(" CHANNEL BREAKDOWN 7d trend 30d trend");
2242
- console.log(` Organic Search: ${String(traffic.totalOrganicSessions).padEnd(6)} (${String(traffic.organicSharePct).padStart(2)}%) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
2243
- console.log(` Social: ${String(traffic.socialSessions).padEnd(6)} (${String(traffic.socialSharePct).padStart(2)}%) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
2244
- console.log(` Direct: ${String(traffic.totalDirectSessions).padEnd(6)} (${String(traffic.directSharePct).padStart(2)}%) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
2245
- console.log(` AI Referrals: ${String(traffic.aiSessionsBySession).padEnd(6)} (${String(traffic.aiSharePctBySession).padStart(2)}%) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2253
+ console.log(` Organic Search: ${String(traffic.totalOrganicSessions).padEnd(6)} (${traffic.organicSharePctDisplay.padStart(4)}) ${fmtTrend(trend.organic.trend7dPct).padEnd(12)} ${fmtTrend(trend.organic.trend30dPct)}`);
2254
+ console.log(` Social: ${String(traffic.socialSessions).padEnd(6)} (${traffic.socialSharePctDisplay.padStart(4)}) ${fmtTrend(trend.social.trend7dPct).padEnd(12)} ${fmtTrend(trend.social.trend30dPct)}`);
2255
+ console.log(` Direct: ${String(traffic.totalDirectSessions).padEnd(6)} (${traffic.directSharePctDisplay.padStart(4)}) ${fmtTrend(trend.direct.trend7dPct).padEnd(12)} ${fmtTrend(trend.direct.trend30dPct)}`);
2256
+ console.log(` AI Referrals: ${String(traffic.aiSessionsBySession).padEnd(6)} (${traffic.aiSharePctBySessionDisplay.padStart(4)}) ${fmtTrend(trend.ai.trend7dPct).padEnd(12)} ${fmtTrend(trend.ai.trend30dPct)} (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2246
2257
  const otherSessions2 = traffic.totalSessions - traffic.totalOrganicSessions - traffic.aiSessionsBySession - traffic.socialSessions - traffic.totalDirectSessions;
2247
2258
  if (otherSessions2 > 0) {
2248
2259
  const otherPct = traffic.totalSessions > 0 ? Math.round(otherSessions2 / traffic.totalSessions * 100) : 0;
@@ -2285,6 +2296,11 @@ async function gaAttribution(project, opts) {
2285
2296
  socialSharePct: traffic.socialSharePct,
2286
2297
  organicSharePct: traffic.organicSharePct,
2287
2298
  directSharePct: traffic.directSharePct,
2299
+ organicSharePctDisplay: traffic.organicSharePctDisplay,
2300
+ aiSharePctDisplay: traffic.aiSharePctDisplay,
2301
+ aiSharePctBySessionDisplay: traffic.aiSharePctBySessionDisplay,
2302
+ socialSharePctDisplay: traffic.socialSharePctDisplay,
2303
+ directSharePctDisplay: traffic.directSharePctDisplay,
2288
2304
  aiReferrals: traffic.aiReferrals,
2289
2305
  aiReferralLandingPages: traffic.aiReferralLandingPages,
2290
2306
  socialReferrals: traffic.socialReferrals,
@@ -2303,10 +2319,10 @@ async function gaAttribution(project, opts) {
2303
2319
  console.log(` Total Users: ${traffic.totalUsers}`);
2304
2320
  console.log();
2305
2321
  console.log(" CHANNEL BREAKDOWN");
2306
- console.log(` Organic Search: ${traffic.totalOrganicSessions} sessions (${traffic.organicSharePct}%)`);
2307
- console.log(` Social: ${traffic.socialSessions} sessions (${traffic.socialSharePct}%)`);
2308
- console.log(` Direct: ${traffic.totalDirectSessions} sessions (${traffic.directSharePct}%)`);
2309
- console.log(` AI Referrals: ${traffic.aiSessionsBySession} sessions (${traffic.aiSharePctBySession}%) (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2322
+ console.log(` Organic Search: ${traffic.totalOrganicSessions} sessions (${traffic.organicSharePctDisplay})`);
2323
+ console.log(` Social: ${traffic.socialSessions} sessions (${traffic.socialSharePctDisplay})`);
2324
+ console.log(` Direct: ${traffic.totalDirectSessions} sessions (${traffic.directSharePctDisplay})`);
2325
+ console.log(` AI Referrals: ${traffic.aiSessionsBySession} sessions (${traffic.aiSharePctBySessionDisplay}) (lower bound \u2014 sessionSource only; referrer-stripped traffic falls under Direct)`);
2310
2326
  const otherSessions = traffic.totalSessions - traffic.totalOrganicSessions - traffic.aiSessionsBySession - traffic.socialSessions - traffic.totalDirectSessions;
2311
2327
  if (otherSessions > 0) {
2312
2328
  const otherPct = traffic.totalSessions > 0 ? Math.round(otherSessions / traffic.totalSessions * 100) : 0;
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-33ATDBGM.js";
3
+ } from "./chunk-QA5HRDRS.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-YGIWXQGM.js";
6
+ } from "./chunk-TBFRAJU2.js";
7
7
  import "./chunk-UQH5SKM2.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
  export {
package/dist/mcp.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  CliError,
3
3
  canonryMcpTools,
4
4
  createApiClient
5
- } from "./chunk-YGIWXQGM.js";
5
+ } from "./chunk-TBFRAJU2.js";
6
6
  import "./chunk-MLKGABMK.js";
7
7
 
8
8
  // src/mcp/cli.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "3.2.0",
3
+ "version": "3.2.2",
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",
@@ -60,19 +60,19 @@
60
60
  "tsup": "^8.5.1",
61
61
  "tsx": "^4.19.0",
62
62
  "@ainyc/canonry-api-routes": "0.0.0",
63
+ "@ainyc/canonry-db": "0.0.0",
63
64
  "@ainyc/canonry-config": "0.0.0",
65
+ "@ainyc/canonry-intelligence": "0.0.0",
64
66
  "@ainyc/canonry-contracts": "0.0.0",
65
67
  "@ainyc/canonry-integration-bing": "0.0.0",
66
- "@ainyc/canonry-db": "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
- "@ainyc/canonry-provider-cdp": "0.0.0",
71
- "@ainyc/canonry-intelligence": "0.0.0",
72
71
  "@ainyc/canonry-provider-claude": "0.0.0",
73
72
  "@ainyc/canonry-provider-gemini": "0.0.0",
74
73
  "@ainyc/canonry-provider-local": "0.0.0",
75
74
  "@ainyc/canonry-provider-openai": "0.0.0",
75
+ "@ainyc/canonry-provider-cdp": "0.0.0",
76
76
  "@ainyc/canonry-provider-perplexity": "0.0.0"
77
77
  },
78
78
  "scripts": {