@ainyc/canonry 3.1.0 → 3.2.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-DOxzPUzl.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-CEF4UuGT.css">
15
+ <script type="module" crossorigin src="./assets/index-Z2rYL3K8.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-DZm9xxNs.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -62,7 +62,7 @@ import {
62
62
  visibilityStateFromAnswerMentioned,
63
63
  windowCutoff,
64
64
  wordpressEnvSchema
65
- } from "./chunk-5X3TJ5BC.js";
65
+ } from "./chunk-YGIWXQGM.js";
66
66
  import {
67
67
  IntelligenceService,
68
68
  agentMemory,
@@ -100,7 +100,7 @@ import {
100
100
  runs,
101
101
  schedules,
102
102
  usageCounters
103
- } from "./chunk-NEDRCOOL.js";
103
+ } from "./chunk-UQH5SKM2.js";
104
104
 
105
105
  // src/telemetry.ts
106
106
  import crypto from "crypto";
@@ -5252,7 +5252,7 @@ var routeCatalog = [
5252
5252
  {
5253
5253
  method: "get",
5254
5254
  path: "/api/v1/projects/{name}/ga/traffic",
5255
- summary: "Get GA4 landing page traffic and AI referral sources",
5255
+ summary: "Get GA4 landing page traffic, channel breakdown, and AI referral landing pages",
5256
5256
  tags: ["ga4"],
5257
5257
  parameters: [nameParameter, limitQueryParameter, analyticsWindowParameter],
5258
5258
  responses: {
@@ -5264,7 +5264,7 @@ var routeCatalog = [
5264
5264
  {
5265
5265
  method: "get",
5266
5266
  path: "/api/v1/projects/{name}/ga/ai-referral-history",
5267
- summary: "Get AI referral sessions per day grouped by source",
5267
+ summary: "Get AI referral sessions per day grouped by source and landing page",
5268
5268
  tags: ["ga4"],
5269
5269
  parameters: [nameParameter, analyticsWindowParameter],
5270
5270
  responses: {
@@ -7072,7 +7072,8 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
7072
7072
  dimensions: [
7073
7073
  { name: "date" },
7074
7074
  { name: sourceDim },
7075
- { name: mediumDim }
7075
+ { name: mediumDim },
7076
+ { name: "landingPagePlusQueryString" }
7076
7077
  ],
7077
7078
  metrics: [
7078
7079
  { name: "sessions" },
@@ -7097,6 +7098,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
7097
7098
  date: row.dimensionValues[0].value,
7098
7099
  source: row.dimensionValues[1].value,
7099
7100
  medium: row.dimensionValues[2].value,
7101
+ landingPage: row.dimensionValues[3]?.value ?? "(not set)",
7100
7102
  sessions: parseInt(row.metricValues[0].value, 10) || 0,
7101
7103
  users: parseInt(row.metricValues[1].value, 10) || 0,
7102
7104
  sourceDimension: dimLabel
@@ -7109,7 +7111,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
7109
7111
  }
7110
7112
  const deduped = /* @__PURE__ */ new Map();
7111
7113
  for (const row of rows) {
7112
- const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}`;
7114
+ const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}::${row.landingPage}`;
7113
7115
  const existing = deduped.get(key);
7114
7116
  if (!existing) {
7115
7117
  deduped.set(key, row);
@@ -8964,6 +8966,8 @@ async function ga4Routes(app, opts) {
8964
8966
  source: row.source,
8965
8967
  medium: row.medium,
8966
8968
  sourceDimension: row.sourceDimension,
8969
+ landingPage: row.landingPage,
8970
+ landingPageNormalized: normalizeUrlPath(row.landingPage),
8967
8971
  sessions: row.sessions,
8968
8972
  users: row.users,
8969
8973
  syncedAt: now,
@@ -9080,16 +9084,35 @@ async function ga4Routes(app, opts) {
9080
9084
  sessions: sql5`SUM(${gaAiReferrals.sessions})`,
9081
9085
  users: sql5`SUM(${gaAiReferrals.users})`
9082
9086
  }).from(gaAiReferrals).where(and8(...aiConditions)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql5`SUM(${gaAiReferrals.sessions}) DESC`).all();
9087
+ const aiReferralLandingPages = app.db.select({
9088
+ source: gaAiReferrals.source,
9089
+ medium: gaAiReferrals.medium,
9090
+ sourceDimension: gaAiReferrals.sourceDimension,
9091
+ landingPage: sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
9092
+ sessions: sql5`SUM(${gaAiReferrals.sessions})`,
9093
+ users: sql5`SUM(${gaAiReferrals.users})`
9094
+ }).from(gaAiReferrals).where(and8(...aiConditions)).groupBy(
9095
+ gaAiReferrals.source,
9096
+ gaAiReferrals.medium,
9097
+ gaAiReferrals.sourceDimension,
9098
+ sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
9099
+ ).orderBy(sql5`SUM(${gaAiReferrals.sessions}) DESC`).all();
9083
9100
  const aiDeduped = app.db.select({
9084
- sessions: sql5`SUM(max_sessions)`,
9085
- users: sql5`SUM(max_users)`
9101
+ sessions: sql5`COALESCE(SUM(max_sessions), 0)`,
9102
+ users: sql5`COALESCE(SUM(max_users), 0)`
9086
9103
  }).from(
9087
9104
  sql5`(
9088
9105
  SELECT date, source, medium,
9089
- MAX(sessions) AS max_sessions,
9090
- MAX(users) AS max_users
9091
- FROM ga_ai_referrals
9092
- WHERE project_id = ${project.id}${cutoffDate ? sql5` AND date >= ${cutoffDate}` : sql5``}
9106
+ MAX(dimension_sessions) AS max_sessions,
9107
+ MAX(dimension_users) AS max_users
9108
+ FROM (
9109
+ SELECT date, source, medium, source_dimension,
9110
+ SUM(sessions) AS dimension_sessions,
9111
+ SUM(users) AS dimension_users
9112
+ FROM ga_ai_referrals
9113
+ WHERE project_id = ${project.id}${cutoffDate ? sql5` AND date >= ${cutoffDate}` : sql5``}
9114
+ GROUP BY date, source, medium, source_dimension
9115
+ )
9093
9116
  GROUP BY date, source, medium
9094
9117
  )`
9095
9118
  ).get();
@@ -9130,6 +9153,14 @@ async function ga4Routes(app, opts) {
9130
9153
  sessions: r.sessions ?? 0,
9131
9154
  users: r.users ?? 0
9132
9155
  })),
9156
+ aiReferralLandingPages: aiReferralLandingPages.map((r) => ({
9157
+ source: r.source,
9158
+ medium: r.medium,
9159
+ sourceDimension: r.sourceDimension,
9160
+ landingPage: r.landingPage,
9161
+ sessions: r.sessions ?? 0,
9162
+ users: r.users ?? 0
9163
+ })),
9133
9164
  aiSessionsDeduped: aiDeduped?.sessions ?? 0,
9134
9165
  aiUsersDeduped: aiDeduped?.users ?? 0,
9135
9166
  aiSessionsBySession: aiBySession?.sessions ?? 0,
@@ -9168,10 +9199,17 @@ async function ga4Routes(app, opts) {
9168
9199
  date: gaAiReferrals.date,
9169
9200
  source: gaAiReferrals.source,
9170
9201
  medium: gaAiReferrals.medium,
9202
+ landingPage: sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`,
9171
9203
  sourceDimension: gaAiReferrals.sourceDimension,
9172
- sessions: gaAiReferrals.sessions,
9173
- users: gaAiReferrals.users
9174
- }).from(gaAiReferrals).where(and8(...conditions)).orderBy(gaAiReferrals.date).all();
9204
+ sessions: sql5`SUM(${gaAiReferrals.sessions})`,
9205
+ users: sql5`SUM(${gaAiReferrals.users})`
9206
+ }).from(gaAiReferrals).where(and8(...conditions)).groupBy(
9207
+ gaAiReferrals.date,
9208
+ gaAiReferrals.source,
9209
+ gaAiReferrals.medium,
9210
+ gaAiReferrals.sourceDimension,
9211
+ sql5`COALESCE(${gaAiReferrals.landingPageNormalized}, ${gaAiReferrals.landingPage})`
9212
+ ).orderBy(gaAiReferrals.date).all();
9175
9213
  return rows;
9176
9214
  });
9177
9215
  app.get("/projects/:name/ga/social-referral-history", async (request, _reply) => {
@@ -344,6 +344,8 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
344
344
  medium: text("medium").notNull(),
345
345
  /** Which GA4 dimension produced this row: 'session' | 'first_user' | 'manual_utm' */
346
346
  sourceDimension: text("source_dimension").notNull().default("session"),
347
+ landingPage: text("landing_page").notNull().default("(not set)"),
348
+ landingPageNormalized: text("landing_page_normalized"),
347
349
  sessions: integer("sessions").notNull().default(0),
348
350
  users: integer("users").notNull().default(0),
349
351
  syncedAt: text("synced_at").notNull(),
@@ -351,7 +353,8 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
351
353
  }, (table) => [
352
354
  index("idx_ga_ai_ref_project_date").on(table.projectId, table.date),
353
355
  index("idx_ga_ai_ref_source").on(table.source),
354
- uniqueIndex("idx_ga_ai_ref_unique_v2").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension),
356
+ index("idx_ga_ai_ref_landing_page").on(table.projectId, table.date, table.landingPageNormalized),
357
+ uniqueIndex("idx_ga_ai_ref_unique_v3").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension, table.landingPage),
355
358
  index("idx_ga_ai_ref_run").on(table.syncRunId)
356
359
  ]);
357
360
  var gaSocialReferrals = sqliteTable("ga_social_referrals", {
@@ -1081,7 +1084,20 @@ var MIGRATIONS = [
1081
1084
  // separate commit. Unblocks an honest channel breakdown for the project
1082
1085
  // dashboard (organic / social / direct / known-AI) — see
1083
1086
  // plans/ai-attribution-research.md scope A.
1084
- `ALTER TABLE ga_traffic_snapshots ADD COLUMN direct_sessions INTEGER`
1087
+ `ALTER TABLE ga_traffic_snapshots ADD COLUMN direct_sessions INTEGER`,
1088
+ // v46: Landing-page breakdown for GA4 known-AI referral rows. The raw
1089
+ // landing_page participates in the unique key so distinct query strings can
1090
+ // be ingested without collision; API reads group by landing_page_normalized.
1091
+ // Default '(not set)' matches GA4's own sentinel for missing dimension
1092
+ // values, so legacy rows surface as the same bucket new ingestion uses
1093
+ // when GA4 returns nothing.
1094
+ `ALTER TABLE ga_ai_referrals ADD COLUMN landing_page TEXT NOT NULL DEFAULT '(not set)'`,
1095
+ `ALTER TABLE ga_ai_referrals ADD COLUMN landing_page_normalized TEXT`,
1096
+ `DROP INDEX IF EXISTS idx_ga_ai_ref_unique_v2`,
1097
+ `CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_landing_page
1098
+ ON ga_ai_referrals(project_id, date, landing_page_normalized)`,
1099
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v3
1100
+ ON ga_ai_referrals(project_id, date, source, medium, source_dimension, landing_page)`
1085
1101
  ];
1086
1102
  function isDuplicateColumnError(err) {
1087
1103
  if (!(err instanceof Error)) return false;
@@ -1287,6 +1287,14 @@ var ga4AiReferralDtoSchema = z12.object({
1287
1287
  users: z12.number(),
1288
1288
  sourceDimension: ga4SourceDimensionSchema
1289
1289
  });
1290
+ var ga4AiReferralLandingPageDtoSchema = z12.object({
1291
+ source: z12.string(),
1292
+ medium: z12.string(),
1293
+ sourceDimension: ga4SourceDimensionSchema,
1294
+ landingPage: z12.string(),
1295
+ sessions: z12.number(),
1296
+ users: z12.number()
1297
+ });
1290
1298
  var ga4SocialReferralDtoSchema = z12.object({
1291
1299
  source: z12.string(),
1292
1300
  medium: z12.string(),
@@ -1310,6 +1318,7 @@ var ga4TrafficSummaryDtoSchema = z12.object({
1310
1318
  users: z12.number()
1311
1319
  })),
1312
1320
  aiReferrals: z12.array(ga4AiReferralDtoSchema),
1321
+ aiReferralLandingPages: z12.array(ga4AiReferralLandingPageDtoSchema),
1313
1322
  /** Deduped AI session total: MAX(sessions) per date+source+medium across attribution dimensions, then summed. Cross-cutting: can overlap with Direct/Organic/Social via firstUserSource. */
1314
1323
  aiSessionsDeduped: z12.number(),
1315
1324
  /** Deduped AI user total: MAX(users) per date+source+medium across attribution dimensions, then summed. */
@@ -1339,6 +1348,7 @@ var ga4AiReferralHistoryEntrySchema = z12.object({
1339
1348
  date: z12.string(),
1340
1349
  source: z12.string(),
1341
1350
  medium: z12.string(),
1351
+ landingPage: z12.string(),
1342
1352
  sessions: z12.number(),
1343
1353
  users: z12.number(),
1344
1354
  /** Which GA4 dimension this row came from: session (sessionSource), first_user (firstUserSource), or manual_utm (utm_source parameter) */
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  setGoogleAuthConfig,
18
18
  showFirstRunNotice,
19
19
  trackEvent
20
- } from "./chunk-VUP7AQTD.js";
20
+ } from "./chunk-33ATDBGM.js";
21
21
  import {
22
22
  CcReleaseSyncStatuses,
23
23
  CheckScopes,
@@ -45,18 +45,19 @@ import {
45
45
  saveConfig,
46
46
  saveConfigPatch,
47
47
  usageError
48
- } from "./chunk-5X3TJ5BC.js";
48
+ } from "./chunk-YGIWXQGM.js";
49
49
  import {
50
50
  apiKeys,
51
51
  competitors,
52
52
  createClient,
53
+ gaAiReferrals,
53
54
  gaTrafficSnapshots,
54
55
  migrate,
55
56
  parseJsonColumn,
56
57
  projects,
57
58
  querySnapshots,
58
59
  runs
59
- } from "./chunk-NEDRCOOL.js";
60
+ } from "./chunk-UQH5SKM2.js";
60
61
  import "./chunk-MLKGABMK.js";
61
62
 
62
63
  // src/cli.ts
@@ -375,8 +376,80 @@ async function backfillNormalizedPathsCommand(opts) {
375
376
  console.log(` Updated: ${updated}`);
376
377
  console.log(` Unchanged: ${unchanged}`);
377
378
  }
379
+ function backfillAiReferralPaths(db, opts) {
380
+ const baseConditions = [];
381
+ if (opts?.projectId) {
382
+ baseConditions.push(eq(gaAiReferrals.projectId, opts.projectId));
383
+ }
384
+ const rows = db.select({
385
+ id: gaAiReferrals.id,
386
+ landingPage: gaAiReferrals.landingPage,
387
+ landingPageNormalized: gaAiReferrals.landingPageNormalized
388
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and(...baseConditions) : void 0).all();
389
+ let updated = 0;
390
+ let unchanged = 0;
391
+ if (rows.length > 0) {
392
+ db.transaction((tx) => {
393
+ for (const row of rows) {
394
+ const next = normalizeUrlPath(row.landingPage);
395
+ if (next === null) {
396
+ unchanged++;
397
+ continue;
398
+ }
399
+ if (row.landingPageNormalized === next) {
400
+ unchanged++;
401
+ continue;
402
+ }
403
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq(gaAiReferrals.id, row.id)).run();
404
+ updated++;
405
+ }
406
+ });
407
+ }
408
+ return { examined: rows.length, updated, unchanged };
409
+ }
410
+ async function backfillAiReferralPathsCommand(opts) {
411
+ const config = loadConfig();
412
+ const db = createClient(config.database);
413
+ migrate(db);
414
+ const projectFilter = opts?.project?.trim();
415
+ let projectId;
416
+ if (projectFilter) {
417
+ const project = db.select({ id: projects.id }).from(projects).where(eq(projects.name, projectFilter)).get();
418
+ if (!project) {
419
+ const result2 = {
420
+ project: projectFilter,
421
+ examined: 0,
422
+ updated: 0,
423
+ unchanged: 0
424
+ };
425
+ if (opts?.format === "json") {
426
+ console.log(JSON.stringify(result2, null, 2));
427
+ return;
428
+ }
429
+ console.log(`Backfill ai-referral-paths: project "${projectFilter}" not found.`);
430
+ return;
431
+ }
432
+ projectId = project.id;
433
+ }
434
+ const { examined, updated, unchanged } = backfillAiReferralPaths(db, { projectId });
435
+ const result = {
436
+ project: projectFilter ?? null,
437
+ examined,
438
+ updated,
439
+ unchanged
440
+ };
441
+ if (opts?.format === "json") {
442
+ console.log(JSON.stringify(result, null, 2));
443
+ return;
444
+ }
445
+ console.log("AI referral landing-page backfill complete.\n");
446
+ if (projectFilter) console.log(` Project: ${projectFilter}`);
447
+ console.log(` Examined: ${examined}`);
448
+ console.log(` Updated: ${updated}`);
449
+ console.log(` Unchanged: ${unchanged}`);
450
+ }
378
451
  async function backfillInsightsCommand(project, opts) {
379
- const { IntelligenceService } = await import("./intelligence-service-6S5YKANX.js");
452
+ const { IntelligenceService } = await import("./intelligence-service-GYY23WHR.js");
380
453
  const config = loadConfig();
381
454
  const db = createClient(config.database);
382
455
  migrate(db);
@@ -586,14 +659,28 @@ var BACKFILL_CLI_COMMANDS = [
586
659
  });
587
660
  }
588
661
  },
662
+ {
663
+ path: ["backfill", "ai-referral-paths"],
664
+ usage: "canonry backfill ai-referral-paths [--project <name>] [--format json]",
665
+ options: {
666
+ project: stringOption()
667
+ },
668
+ allowPositionals: false,
669
+ run: async (input) => {
670
+ await backfillAiReferralPathsCommand({
671
+ project: getString(input.values, "project"),
672
+ format: input.format
673
+ });
674
+ }
675
+ },
589
676
  {
590
677
  path: ["backfill"],
591
- usage: "canonry backfill <answer-visibility|insights|normalized-paths> [options]",
678
+ usage: "canonry backfill <answer-visibility|insights|normalized-paths|ai-referral-paths> [options]",
592
679
  run: async (input) => {
593
680
  unknownSubcommand(input.positionals[0], {
594
681
  command: "backfill",
595
- usage: "canonry backfill <answer-visibility|insights|normalized-paths> [options]",
596
- available: ["answer-visibility", "insights", "normalized-paths"]
682
+ usage: "canonry backfill <answer-visibility|insights|normalized-paths|ai-referral-paths> [options]",
683
+ available: ["answer-visibility", "insights", "normalized-paths", "ai-referral-paths"]
597
684
  });
598
685
  }
599
686
  }
@@ -1876,7 +1963,7 @@ async function gaTraffic(project, opts) {
1876
1963
  console.log(JSON.stringify(result, null, 2));
1877
1964
  return;
1878
1965
  }
1879
- if (result.topPages.length === 0 && result.aiReferrals.length === 0 && result.socialReferrals.length === 0) {
1966
+ if (result.topPages.length === 0 && result.aiReferrals.length === 0 && result.aiReferralLandingPages.length === 0 && result.socialReferrals.length === 0) {
1880
1967
  if (!result.lastSyncedAt) {
1881
1968
  console.log('No GA4 traffic data. Run "canonry ga sync <project>" first.');
1882
1969
  } else {
@@ -1907,6 +1994,21 @@ async function gaTraffic(project, opts) {
1907
1994
  }
1908
1995
  console.log();
1909
1996
  }
1997
+ if (result.aiReferralLandingPages.length > 0) {
1998
+ const attrWidth = 12;
1999
+ console.log(" AI REFERRAL LANDING PAGES");
2000
+ console.log(` ${"LANDING PAGE".padEnd(30)} ${"SOURCE".padEnd(25)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
2001
+ console.log(` ${"\u2500".repeat(30)} ${"\u2500".repeat(25)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
2002
+ for (const row of result.aiReferralLandingPages) {
2003
+ const dimLabel = row.sourceDimension === "first_user" ? "first-visit" : row.sourceDimension === "manual_utm" ? "utm" : "session";
2004
+ const page = row.landingPage.length > 30 ? row.landingPage.slice(0, 27) + "..." : row.landingPage;
2005
+ const source = row.source.length > 25 ? row.source.slice(0, 22) + "..." : row.source;
2006
+ console.log(
2007
+ ` ${page.padEnd(30)} ${source.padEnd(25)} ${dimLabel.padEnd(attrWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
2008
+ );
2009
+ }
2010
+ console.log();
2011
+ }
1910
2012
  if (result.socialReferrals.length > 0) {
1911
2013
  const chanWidth = 12;
1912
2014
  if (result.socialSessions > 0) {
@@ -2121,6 +2223,7 @@ async function gaAttribution(project, opts) {
2121
2223
  organicSharePct: traffic.organicSharePct,
2122
2224
  directSharePct: traffic.directSharePct,
2123
2225
  aiReferrals: traffic.aiReferrals,
2226
+ aiReferralLandingPages: traffic.aiReferralLandingPages,
2124
2227
  socialReferrals: traffic.socialReferrals,
2125
2228
  trend
2126
2229
  }, null, 2));
@@ -2183,6 +2286,7 @@ async function gaAttribution(project, opts) {
2183
2286
  organicSharePct: traffic.organicSharePct,
2184
2287
  directSharePct: traffic.directSharePct,
2185
2288
  aiReferrals: traffic.aiReferrals,
2289
+ aiReferralLandingPages: traffic.aiReferralLandingPages,
2186
2290
  socialReferrals: traffic.socialReferrals,
2187
2291
  periodStart: traffic.periodStart,
2188
2292
  periodEnd: traffic.periodEnd
@@ -2216,6 +2320,16 @@ async function gaAttribution(project, opts) {
2216
2320
  console.log(` ${ref.source.padEnd(25)} ${String(ref.sessions).padEnd(8)} sessions (${dimLabel})`);
2217
2321
  }
2218
2322
  }
2323
+ if (traffic.aiReferralLandingPages.length > 0) {
2324
+ console.log();
2325
+ console.log(" AI LANDING PAGES");
2326
+ for (const row of traffic.aiReferralLandingPages.slice(0, 10)) {
2327
+ const dimLabel = row.sourceDimension === "first_user" ? "first-visit" : row.sourceDimension === "manual_utm" ? "utm" : "session";
2328
+ const page = row.landingPage.length > 30 ? row.landingPage.slice(0, 27) + "..." : row.landingPage;
2329
+ const source = row.source.length > 22 ? row.source.slice(0, 19) + "..." : row.source;
2330
+ console.log(` ${page.padEnd(30)} ${source.padEnd(22)} ${String(row.sessions).padEnd(8)} sessions (${dimLabel})`);
2331
+ }
2332
+ }
2219
2333
  if (traffic.socialReferrals.length > 0) {
2220
2334
  console.log();
2221
2335
  console.log(" SOCIAL SOURCES");
@@ -7287,6 +7401,18 @@ async function serveCommand(format = "text") {
7287
7401
  } catch (err) {
7288
7402
  const msg = err instanceof Error ? err.message : String(err);
7289
7403
  process.stderr.write(`warning: normalized-path backfill skipped: ${msg}
7404
+ `);
7405
+ }
7406
+ try {
7407
+ const result = backfillAiReferralPaths(db);
7408
+ if (result.updated > 0 && format === "text") {
7409
+ console.log(
7410
+ `Migrated ${result.updated} GA AI referral row${result.updated === 1 ? "" : "s"} to canonical form.`
7411
+ );
7412
+ }
7413
+ } catch (err) {
7414
+ const msg = err instanceof Error ? err.message : String(err);
7415
+ process.stderr.write(`warning: ai-referral-paths backfill skipped: ${msg}
7290
7416
  `);
7291
7417
  }
7292
7418
  const app = await createServer({ config, db });
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  createServer
3
- } from "./chunk-VUP7AQTD.js";
3
+ } from "./chunk-33ATDBGM.js";
4
4
  import {
5
5
  loadConfig
6
- } from "./chunk-5X3TJ5BC.js";
7
- import "./chunk-NEDRCOOL.js";
6
+ } from "./chunk-YGIWXQGM.js";
7
+ import "./chunk-UQH5SKM2.js";
8
8
  import "./chunk-MLKGABMK.js";
9
9
  export {
10
10
  createServer,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-NEDRCOOL.js";
3
+ } from "./chunk-UQH5SKM2.js";
4
4
  import "./chunk-MLKGABMK.js";
5
5
  export {
6
6
  IntelligenceService
package/dist/mcp.js CHANGED
@@ -2,7 +2,7 @@ import {
2
2
  CliError,
3
3
  canonryMcpTools,
4
4
  createApiClient
5
- } from "./chunk-5X3TJ5BC.js";
5
+ } from "./chunk-YGIWXQGM.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.1.0",
3
+ "version": "3.2.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-contracts": "0.0.0",
63
62
  "@ainyc/canonry-api-routes": "0.0.0",
64
63
  "@ainyc/canonry-config": "0.0.0",
65
- "@ainyc/canonry-db": "0.0.0",
66
- "@ainyc/canonry-intelligence": "0.0.0",
64
+ "@ainyc/canonry-contracts": "0.0.0",
67
65
  "@ainyc/canonry-integration-bing": "0.0.0",
66
+ "@ainyc/canonry-db": "0.0.0",
68
67
  "@ainyc/canonry-integration-commoncrawl": "0.0.0",
69
68
  "@ainyc/canonry-integration-google": "0.0.0",
70
- "@ainyc/canonry-provider-cdp": "0.0.0",
71
69
  "@ainyc/canonry-integration-wordpress": "0.0.0",
70
+ "@ainyc/canonry-provider-cdp": "0.0.0",
71
+ "@ainyc/canonry-intelligence": "0.0.0",
72
+ "@ainyc/canonry-provider-claude": "0.0.0",
72
73
  "@ainyc/canonry-provider-gemini": "0.0.0",
73
74
  "@ainyc/canonry-provider-local": "0.0.0",
74
- "@ainyc/canonry-provider-perplexity": "0.0.0",
75
- "@ainyc/canonry-provider-claude": "0.0.0",
76
- "@ainyc/canonry-provider-openai": "0.0.0"
75
+ "@ainyc/canonry-provider-openai": "0.0.0",
76
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
77
77
  },
78
78
  "scripts": {
79
79
  "build": "tsx scripts/copy-agent-assets.ts && tsup && tsx build-web.ts",