@ainyc/canonry 1.35.0 → 1.37.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-DyipkdOb.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-B9SBdBOm.css">
15
+ <script type="module" crossorigin src="./assets/index-DR4zYsnw.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-BZX6FEZ4.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -1055,11 +1055,13 @@ var ga4TrafficSnapshotDtoSchema = z11.object({
1055
1055
  organicSessions: z11.number(),
1056
1056
  users: z11.number()
1057
1057
  });
1058
+ var ga4SourceDimensionSchema = z11.enum(["session", "first_user", "manual_utm"]);
1058
1059
  var ga4AiReferralDtoSchema = z11.object({
1059
1060
  source: z11.string(),
1060
1061
  medium: z11.string(),
1061
1062
  sessions: z11.number(),
1062
- users: z11.number()
1063
+ users: z11.number(),
1064
+ sourceDimension: ga4SourceDimensionSchema
1063
1065
  });
1064
1066
  var ga4TrafficSummaryDtoSchema = z11.object({
1065
1067
  totalSessions: z11.number(),
@@ -1072,6 +1074,10 @@ var ga4TrafficSummaryDtoSchema = z11.object({
1072
1074
  users: z11.number()
1073
1075
  })),
1074
1076
  aiReferrals: z11.array(ga4AiReferralDtoSchema),
1077
+ /** Deduped AI session total: MAX(sessions) per date+source+medium across attribution dimensions, then summed. */
1078
+ aiSessionsDeduped: z11.number(),
1079
+ /** Deduped AI user total: MAX(users) per date+source+medium across attribution dimensions, then summed. */
1080
+ aiUsersDeduped: z11.number(),
1075
1081
  lastSyncedAt: z11.string().nullable()
1076
1082
  });
1077
1083
  var ga4AiReferralHistoryEntrySchema = z11.object({
@@ -1079,6 +1085,14 @@ var ga4AiReferralHistoryEntrySchema = z11.object({
1079
1085
  source: z11.string(),
1080
1086
  medium: z11.string(),
1081
1087
  sessions: z11.number(),
1088
+ users: z11.number(),
1089
+ /** Which GA4 dimension this row came from: session (sessionSource), first_user (firstUserSource), or manual_utm (utm_source parameter) */
1090
+ sourceDimension: ga4SourceDimensionSchema
1091
+ });
1092
+ var ga4SessionHistoryEntrySchema = z11.object({
1093
+ date: z11.string(),
1094
+ sessions: z11.number(),
1095
+ organicSessions: z11.number(),
1082
1096
  users: z11.number()
1083
1097
  });
1084
1098
 
@@ -1481,13 +1495,15 @@ var gaAiReferrals = sqliteTable("ga_ai_referrals", {
1481
1495
  date: text("date").notNull(),
1482
1496
  source: text("source").notNull(),
1483
1497
  medium: text("medium").notNull(),
1498
+ /** Which GA4 dimension produced this row: 'session' | 'first_user' | 'manual_utm' */
1499
+ sourceDimension: text("source_dimension").notNull().default("session"),
1484
1500
  sessions: integer("sessions").notNull().default(0),
1485
1501
  users: integer("users").notNull().default(0),
1486
1502
  syncedAt: text("synced_at").notNull()
1487
1503
  }, (table) => [
1488
1504
  index("idx_ga_ai_ref_project_date").on(table.projectId, table.date),
1489
1505
  index("idx_ga_ai_ref_source").on(table.source),
1490
- uniqueIndex("idx_ga_ai_ref_unique").on(table.projectId, table.date, table.source, table.medium)
1506
+ uniqueIndex("idx_ga_ai_ref_unique_v2").on(table.projectId, table.date, table.source, table.medium, table.sourceDimension)
1491
1507
  ]);
1492
1508
  var gaTrafficSummaries = sqliteTable("ga_traffic_summaries", {
1493
1509
  id: text("id").primaryKey(),
@@ -1519,6 +1535,7 @@ function createClient(databasePath) {
1519
1535
  const sqlite = new Database(databasePath);
1520
1536
  sqlite.pragma("journal_mode = WAL");
1521
1537
  sqlite.pragma("foreign_keys = ON");
1538
+ sqlite.pragma("busy_timeout = 5000");
1522
1539
  return drizzle(sqlite, { schema: schema_exports });
1523
1540
  }
1524
1541
 
@@ -1846,7 +1863,20 @@ var MIGRATIONS = [
1846
1863
  `CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_source ON ga_ai_referrals(source)`,
1847
1864
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique ON ga_ai_referrals(project_id, date, source, medium)`,
1848
1865
  // v18: Answer-level visibility derived from answer text
1849
- `ALTER TABLE query_snapshots ADD COLUMN answer_mentioned INTEGER`
1866
+ `ALTER TABLE query_snapshots ADD COLUMN answer_mentioned INTEGER`,
1867
+ // v19: Add named unique indexes and missing columns from early tables
1868
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_keywords_project_keyword ON keywords(project_id, keyword)`,
1869
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_competitors_project_domain ON competitors(project_id, domain)`,
1870
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id)`,
1871
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_scope_period_metric ON usage_counters(scope, period, metric)`,
1872
+ `ALTER TABLE projects ADD COLUMN config_source TEXT NOT NULL DEFAULT 'cli'`,
1873
+ `ALTER TABLE projects ADD COLUMN config_revision INTEGER NOT NULL DEFAULT 1`,
1874
+ // v20: Track which GA4 dimension produced each AI referral row
1875
+ // Values: 'session' (sessionSource), 'first_user' (firstUserSource), 'manual_utm' (manualSource/utm_source)
1876
+ `ALTER TABLE ga_ai_referrals ADD COLUMN source_dimension TEXT NOT NULL DEFAULT 'session'`,
1877
+ // Replace old unique index with one that includes source_dimension
1878
+ `DROP INDEX IF EXISTS idx_ga_ai_ref_unique`,
1879
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`
1850
1880
  ];
1851
1881
  function migrate(db) {
1852
1882
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -5773,6 +5803,18 @@ var routeCatalog = [
5773
5803
  404: { description: "Project not found." }
5774
5804
  }
5775
5805
  },
5806
+ {
5807
+ method: "get",
5808
+ path: "/api/v1/projects/{name}/ga/session-history",
5809
+ summary: "Get total sessions per day for the project",
5810
+ tags: ["ga4"],
5811
+ parameters: [nameParameter],
5812
+ responses: {
5813
+ 200: { description: "Session history returned." },
5814
+ 400: { description: "GA4 is not connected." },
5815
+ 404: { description: "Project not found." }
5816
+ }
5817
+ },
5776
5818
  {
5777
5819
  method: "get",
5778
5820
  path: "/api/v1/projects/{name}/ga/coverage",
@@ -6608,7 +6650,10 @@ var AI_REFERRAL_SOURCE_FILTERS = [
6608
6650
  { matchType: "CONTAINS", value: "openai" },
6609
6651
  { matchType: "CONTAINS", value: "claude" },
6610
6652
  { matchType: "CONTAINS", value: "anthropic" },
6611
- { matchType: "CONTAINS", value: "copilot" }
6653
+ { matchType: "CONTAINS", value: "copilot" },
6654
+ { matchType: "CONTAINS", value: "phind" },
6655
+ { matchType: "EXACT", value: "you.com" },
6656
+ { matchType: "CONTAINS", value: "meta.ai" }
6612
6657
  ];
6613
6658
  async function fetchTrafficByLandingPage(accessToken, propertyId, days) {
6614
6659
  const syncDays = Math.min(Math.max(1, days ?? GA4_DEFAULT_SYNC_DAYS), GA4_MAX_SYNC_DAYS);
@@ -6747,52 +6792,75 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
6747
6792
  ga4Log("info", "fetch-ai-referrals.start", { propertyId, days: syncDays });
6748
6793
  const PAGE_SIZE = 1e3;
6749
6794
  const rows = [];
6750
- let offset = 0;
6751
- while (true) {
6752
- const request = {
6753
- dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
6754
- dimensions: [
6755
- { name: "date" },
6756
- { name: "sessionSource" },
6757
- { name: "sessionMedium" }
6758
- ],
6759
- metrics: [
6760
- { name: "sessions" },
6761
- { name: "totalUsers" }
6762
- ],
6763
- dimensionFilter: {
6764
- orGroup: {
6765
- expressions: AI_REFERRAL_SOURCE_FILTERS.map(({ matchType, value }) => ({
6766
- filter: {
6767
- fieldName: "sessionSource",
6768
- stringFilter: { matchType, value }
6769
- }
6770
- }))
6771
- }
6772
- },
6773
- limit: PAGE_SIZE,
6774
- offset
6775
- };
6776
- const response = await runReport(accessToken, propertyId, request);
6777
- const pageRows = (response.rows ?? []).map((row) => ({
6778
- date: row.dimensionValues[0].value,
6779
- source: row.dimensionValues[1].value,
6780
- medium: row.dimensionValues[2].value,
6781
- sessions: parseInt(row.metricValues[0].value, 10) || 0,
6782
- users: parseInt(row.metricValues[1].value, 10) || 0
6783
- }));
6784
- rows.push(...pageRows);
6785
- const totalRows = response.rowCount ?? 0;
6786
- offset += pageRows.length;
6787
- if (pageRows.length < PAGE_SIZE || offset >= totalRows) break;
6795
+ const dimensionPairs = [
6796
+ ["sessionSource", "sessionMedium", "session"],
6797
+ ["firstUserSource", "firstUserMedium", "first_user"],
6798
+ ["manualSource", "manualMedium", "manual_utm"]
6799
+ ];
6800
+ for (const [sourceDim, mediumDim, dimLabel] of dimensionPairs) {
6801
+ let offset = 0;
6802
+ while (true) {
6803
+ const request = {
6804
+ dateRanges: [{ startDate: formatDate(startDate), endDate: formatDate(endDate) }],
6805
+ dimensions: [
6806
+ { name: "date" },
6807
+ { name: sourceDim },
6808
+ { name: mediumDim }
6809
+ ],
6810
+ metrics: [
6811
+ { name: "sessions" },
6812
+ { name: "totalUsers" }
6813
+ ],
6814
+ dimensionFilter: {
6815
+ orGroup: {
6816
+ expressions: AI_REFERRAL_SOURCE_FILTERS.map(({ matchType, value }) => ({
6817
+ filter: {
6818
+ fieldName: sourceDim,
6819
+ stringFilter: { matchType, value }
6820
+ }
6821
+ }))
6822
+ }
6823
+ },
6824
+ limit: PAGE_SIZE,
6825
+ offset
6826
+ };
6827
+ const response = await runReport(accessToken, propertyId, request);
6828
+ const pageRows = (response.rows ?? []).map((row) => ({
6829
+ date: row.dimensionValues[0].value,
6830
+ source: row.dimensionValues[1].value,
6831
+ medium: row.dimensionValues[2].value,
6832
+ sessions: parseInt(row.metricValues[0].value, 10) || 0,
6833
+ users: parseInt(row.metricValues[1].value, 10) || 0,
6834
+ sourceDimension: dimLabel
6835
+ }));
6836
+ rows.push(...pageRows);
6837
+ const totalRows = response.rowCount ?? 0;
6838
+ offset += pageRows.length;
6839
+ if (pageRows.length < PAGE_SIZE || offset >= totalRows) break;
6840
+ }
6788
6841
  }
6842
+ const deduped = /* @__PURE__ */ new Map();
6789
6843
  for (const row of rows) {
6844
+ const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}`;
6845
+ const existing = deduped.get(key);
6846
+ if (!existing) {
6847
+ deduped.set(key, row);
6848
+ } else {
6849
+ deduped.set(key, {
6850
+ ...existing,
6851
+ sessions: Math.max(existing.sessions, row.sessions),
6852
+ users: Math.max(existing.users, row.users)
6853
+ });
6854
+ }
6855
+ }
6856
+ const dedupedRows = [...deduped.values()];
6857
+ for (const row of dedupedRows) {
6790
6858
  if (row.date.length === 8 && !row.date.includes("-")) {
6791
6859
  row.date = `${row.date.slice(0, 4)}-${row.date.slice(4, 6)}-${row.date.slice(6, 8)}`;
6792
6860
  }
6793
6861
  }
6794
- ga4Log("info", "fetch-ai-referrals.done", { propertyId, rowCount: rows.length });
6795
- return rows;
6862
+ ga4Log("info", "fetch-ai-referrals.done", { propertyId, rowCount: dedupedRows.length });
6863
+ return dedupedRows;
6796
6864
  }
6797
6865
 
6798
6866
  // ../api-routes/src/google.ts
@@ -8418,6 +8486,7 @@ async function ga4Routes(app, opts) {
8418
8486
  date: row.date,
8419
8487
  source: row.source,
8420
8488
  medium: row.medium,
8489
+ sourceDimension: row.sourceDimension,
8421
8490
  sessions: row.sessions,
8422
8491
  users: row.users,
8423
8492
  syncedAt: now
@@ -8469,9 +8538,23 @@ async function ga4Routes(app, opts) {
8469
8538
  const aiReferrals = app.db.select({
8470
8539
  source: gaAiReferrals.source,
8471
8540
  medium: gaAiReferrals.medium,
8541
+ sourceDimension: gaAiReferrals.sourceDimension,
8472
8542
  sessions: sql4`SUM(${gaAiReferrals.sessions})`,
8473
8543
  users: sql4`SUM(${gaAiReferrals.users})`
8474
- }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8544
+ }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).groupBy(gaAiReferrals.source, gaAiReferrals.medium, gaAiReferrals.sourceDimension).orderBy(sql4`SUM(${gaAiReferrals.sessions}) DESC`).all();
8545
+ const aiDeduped = app.db.select({
8546
+ sessions: sql4`SUM(max_sessions)`,
8547
+ users: sql4`SUM(max_users)`
8548
+ }).from(
8549
+ sql4`(
8550
+ SELECT date, source, medium,
8551
+ MAX(sessions) AS max_sessions,
8552
+ MAX(users) AS max_users
8553
+ FROM ga_ai_referrals
8554
+ WHERE project_id = ${project.id}
8555
+ GROUP BY date, source, medium
8556
+ )`
8557
+ ).get();
8475
8558
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq16(gaTrafficSummaries.projectId, project.id)).orderBy(desc6(gaTrafficSummaries.syncedAt)).limit(1).get();
8476
8559
  return {
8477
8560
  totalSessions: summary?.totalSessions ?? 0,
@@ -8486,9 +8569,12 @@ async function ga4Routes(app, opts) {
8486
8569
  aiReferrals: aiReferrals.map((r) => ({
8487
8570
  source: r.source,
8488
8571
  medium: r.medium,
8572
+ sourceDimension: r.sourceDimension,
8489
8573
  sessions: r.sessions ?? 0,
8490
8574
  users: r.users ?? 0
8491
8575
  })),
8576
+ aiSessionsDeduped: aiDeduped?.sessions ?? 0,
8577
+ aiUsersDeduped: aiDeduped?.users ?? 0,
8492
8578
  lastSyncedAt: latestSync?.syncedAt ?? null
8493
8579
  };
8494
8580
  });
@@ -8499,11 +8585,28 @@ async function ga4Routes(app, opts) {
8499
8585
  date: gaAiReferrals.date,
8500
8586
  source: gaAiReferrals.source,
8501
8587
  medium: gaAiReferrals.medium,
8588
+ sourceDimension: gaAiReferrals.sourceDimension,
8502
8589
  sessions: gaAiReferrals.sessions,
8503
8590
  users: gaAiReferrals.users
8504
8591
  }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
8505
8592
  return rows;
8506
8593
  });
8594
+ app.get("/projects/:name/ga/session-history", async (request, _reply) => {
8595
+ const project = resolveProject(app.db, request.params.name);
8596
+ requireGa4Connection(opts, project.name, project.canonicalDomain);
8597
+ const rows = app.db.select({
8598
+ date: gaTrafficSnapshots.date,
8599
+ sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8600
+ organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8601
+ users: sql4`SUM(${gaTrafficSnapshots.users})`
8602
+ }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8603
+ return rows.map((r) => ({
8604
+ date: r.date,
8605
+ sessions: r.sessions ?? 0,
8606
+ organicSessions: r.organicSessions ?? 0,
8607
+ users: r.users ?? 0
8608
+ }));
8609
+ });
8507
8610
  app.get("/projects/:name/ga/coverage", async (request, _reply) => {
8508
8611
  const project = resolveProject(app.db, request.params.name);
8509
8612
  requireGa4Connection(opts, project.name, project.canonicalDomain);
@@ -8930,12 +9033,20 @@ async function getSiteStatus(connection, env) {
8930
9033
  async function listActivePlugins(connection, env) {
8931
9034
  const site = resolveEnvironment(connection, env);
8932
9035
  try {
8933
- const { body } = await fetchJson(
8934
- connection,
8935
- site.siteUrl,
8936
- "/wp-json/wp/v2/plugins?per_page=100&_fields=plugin,status"
8937
- );
8938
- return body.filter((plugin) => plugin.status === "active").map((plugin) => plugin.plugin).sort();
9036
+ const allPlugins = [];
9037
+ let page = 1;
9038
+ let totalPages = 1;
9039
+ while (page <= totalPages) {
9040
+ const { body, response } = await fetchJson(
9041
+ connection,
9042
+ site.siteUrl,
9043
+ `/wp-json/wp/v2/plugins?per_page=100&page=${page}&_fields=plugin,status`
9044
+ );
9045
+ totalPages = Number.parseInt(response.headers.get("x-wp-totalpages") ?? "1", 10) || 1;
9046
+ allPlugins.push(...body);
9047
+ page += 1;
9048
+ }
9049
+ return allPlugins.filter((plugin) => plugin.status === "active").map((plugin) => plugin.plugin).sort();
8939
9050
  } catch (error) {
8940
9051
  if (error instanceof WordpressApiError && (error.statusCode === 403 || error.statusCode === 404)) {
8941
9052
  return null;
@@ -10743,7 +10854,7 @@ function extractCitedDomains2(raw) {
10743
10854
  function extractDomainFromUri2(uri) {
10744
10855
  try {
10745
10856
  const url = new URL(uri);
10746
- return url.hostname.replace(/^www\./, "");
10857
+ return url.hostname.replace(/^www\./, "").toLowerCase();
10747
10858
  } catch {
10748
10859
  return null;
10749
10860
  }
@@ -10751,11 +10862,11 @@ function extractDomainFromUri2(uri) {
10751
10862
  async function generateText2(prompt, config) {
10752
10863
  const model = config.model ?? DEFAULT_MODEL2;
10753
10864
  const client = new OpenAI({ apiKey: config.apiKey });
10754
- const response = await client.chat.completions.create({
10865
+ const response = await client.responses.create({
10755
10866
  model,
10756
- messages: [{ role: "user", content: prompt }]
10867
+ input: prompt
10757
10868
  });
10758
- return response.choices[0]?.message?.content ?? "";
10869
+ return extractResponseText(response);
10759
10870
  }
10760
10871
  function responseToRecord2(response) {
10761
10872
  try {
@@ -11004,7 +11115,7 @@ function extractCitedDomains3(raw) {
11004
11115
  function extractDomainFromUri3(uri) {
11005
11116
  try {
11006
11117
  const url = new URL(uri);
11007
- return url.hostname.replace(/^www\./, "");
11118
+ return url.hostname.replace(/^www\./, "").toLowerCase();
11008
11119
  } catch {
11009
11120
  return null;
11010
11121
  }
@@ -11170,7 +11281,7 @@ async function executeTrackedQuery4(input) {
11170
11281
  });
11171
11282
  return {
11172
11283
  provider: "local",
11173
- rawResponse: JSON.parse(JSON.stringify(response)),
11284
+ rawResponse: responseToRecord4(response),
11174
11285
  model,
11175
11286
  groundingSources: [],
11176
11287
  searchQueries: []
@@ -11225,6 +11336,13 @@ function extractDomainMentions(text2) {
11225
11336
  }
11226
11337
  return [...domains];
11227
11338
  }
11339
+ function responseToRecord4(response) {
11340
+ try {
11341
+ return JSON.parse(JSON.stringify(response));
11342
+ } catch {
11343
+ return { error: "failed to serialize response" };
11344
+ }
11345
+ }
11228
11346
 
11229
11347
  // ../provider-local/src/adapter.ts
11230
11348
  function toLocalConfig(config) {
@@ -11754,6 +11872,10 @@ function getConnection(config) {
11754
11872
  if (parts.length >= 1 && parts[0]) host = parts[0];
11755
11873
  if (parts.length >= 2 && parts[1]) port = parseInt(parts[1], 10) || 9222;
11756
11874
  if (!sharedConnection || sharedConnection.endpoint !== `${host}:${port}`) {
11875
+ if (sharedConnection) {
11876
+ sharedConnection.disconnect().catch(() => {
11877
+ });
11878
+ }
11757
11879
  sharedConnection = new CDPConnectionManager(host, port);
11758
11880
  }
11759
11881
  return sharedConnection;
@@ -11903,7 +12025,7 @@ async function executeTrackedQuery5(input) {
11903
12025
  { role: "user", content: prompt }
11904
12026
  ]
11905
12027
  });
11906
- const rawResponse = responseToRecord4(response);
12028
+ const rawResponse = responseToRecord5(response);
11907
12029
  const citations = extractCitations(rawResponse);
11908
12030
  const groundingSources = citations.map((url) => ({
11909
12031
  uri: url,
@@ -11967,7 +12089,7 @@ function extractCitedDomains5(groundingSources) {
11967
12089
  function extractDomainFromUri4(uri) {
11968
12090
  try {
11969
12091
  const url = new URL(uri);
11970
- return url.hostname.replace(/^www\./, "");
12092
+ return url.hostname.replace(/^www\./, "").toLowerCase();
11971
12093
  } catch {
11972
12094
  return null;
11973
12095
  }
@@ -11981,7 +12103,7 @@ async function generateText5(prompt, config) {
11981
12103
  });
11982
12104
  return response.choices[0]?.message?.content ?? "";
11983
12105
  }
11984
- function responseToRecord4(response) {
12106
+ function responseToRecord5(response) {
11985
12107
  try {
11986
12108
  return JSON.parse(JSON.stringify(response));
11987
12109
  } catch {
@@ -12613,7 +12735,7 @@ var JobRunner = class {
12613
12735
  });
12614
12736
  if (this.onRunCompleted) {
12615
12737
  this.onRunCompleted(runId, projectId).catch((notifErr) => {
12616
- console.error("[JobRunner] Notification callback failed:", notifErr);
12738
+ log.error("notification.callback-failed", { runId, error: notifErr instanceof Error ? notifErr.message : String(notifErr) });
12617
12739
  });
12618
12740
  }
12619
12741
  }
@@ -13313,45 +13435,49 @@ var Scheduler = class {
13313
13435
  log4.info("cron.registered", { projectId, schedule: label, timezone });
13314
13436
  }
13315
13437
  triggerRun(scheduleId, projectId) {
13316
- const now = (/* @__PURE__ */ new Date()).toISOString();
13317
- const currentSchedule = this.db.select().from(schedules).where(eq20(schedules.id, scheduleId)).get();
13318
- if (!currentSchedule || currentSchedule.enabled !== 1) {
13319
- log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
13320
- this.remove(projectId);
13321
- return;
13322
- }
13323
- const task = this.tasks.get(projectId);
13324
- const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
13325
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
13326
- if (!project) {
13327
- log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
13328
- this.remove(projectId);
13329
- return;
13330
- }
13331
- const queueResult = queueRunIfProjectIdle(this.db, {
13332
- createdAt: now,
13333
- kind: "answer-visibility",
13334
- projectId,
13335
- trigger: "scheduled"
13336
- });
13337
- if (queueResult.conflict) {
13338
- log4.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
13438
+ try {
13439
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13440
+ const currentSchedule = this.db.select().from(schedules).where(eq20(schedules.id, scheduleId)).get();
13441
+ if (!currentSchedule || currentSchedule.enabled !== 1) {
13442
+ log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
13443
+ this.remove(projectId);
13444
+ return;
13445
+ }
13446
+ const task = this.tasks.get(projectId);
13447
+ const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
13448
+ const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
13449
+ if (!project) {
13450
+ log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
13451
+ this.remove(projectId);
13452
+ return;
13453
+ }
13454
+ const queueResult = queueRunIfProjectIdle(this.db, {
13455
+ createdAt: now,
13456
+ kind: "answer-visibility",
13457
+ projectId,
13458
+ trigger: "scheduled"
13459
+ });
13460
+ if (queueResult.conflict) {
13461
+ log4.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
13462
+ this.db.update(schedules).set({
13463
+ nextRunAt,
13464
+ updatedAt: now
13465
+ }).where(eq20(schedules.id, currentSchedule.id)).run();
13466
+ return;
13467
+ }
13468
+ const runId = queueResult.runId;
13339
13469
  this.db.update(schedules).set({
13470
+ lastRunAt: now,
13340
13471
  nextRunAt,
13341
13472
  updatedAt: now
13342
13473
  }).where(eq20(schedules.id, currentSchedule.id)).run();
13343
- return;
13474
+ const scheduleProviders = JSON.parse(currentSchedule.providers);
13475
+ const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13476
+ log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
13477
+ this.callbacks.onRunCreated(runId, projectId, providers);
13478
+ } catch (err) {
13479
+ log4.error("trigger.error", { scheduleId, projectId, error: err instanceof Error ? err.message : String(err) });
13344
13480
  }
13345
- const runId = queueResult.runId;
13346
- this.db.update(schedules).set({
13347
- lastRunAt: now,
13348
- nextRunAt,
13349
- updatedAt: now
13350
- }).where(eq20(schedules.id, currentSchedule.id)).run();
13351
- const scheduleProviders = JSON.parse(currentSchedule.providers);
13352
- const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13353
- log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
13354
- this.callbacks.onRunCreated(runId, projectId, providers);
13355
13481
  }
13356
13482
  };
13357
13483
 
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  setGoogleAuthConfig,
27
27
  showFirstRunNotice,
28
28
  trackEvent
29
- } from "./chunk-ETP5IOHC.js";
29
+ } from "./chunk-5MOWJJND.js";
30
30
 
31
31
  // src/cli.ts
32
32
  import { pathToFileURL } from "url";
@@ -670,6 +670,9 @@ var ApiClient = class {
670
670
  async gaAiReferralHistory(project) {
671
671
  return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/ai-referral-history`);
672
672
  }
673
+ async gaSessionHistory(project) {
674
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/session-history`);
675
+ }
673
676
  async wordpressConnect(project, body) {
674
677
  return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/connect`, body);
675
678
  }
@@ -1490,14 +1493,20 @@ async function gaTraffic(project, opts) {
1490
1493
  console.log(` Total Sessions: ${result.totalSessions}`);
1491
1494
  console.log(` Organic Sessions: ${result.totalOrganicSessions}`);
1492
1495
  console.log(` Total Users: ${result.totalUsers}`);
1496
+ if (result.aiSessionsDeduped > 0) {
1497
+ const share = result.totalSessions > 0 ? Math.round(result.aiSessionsDeduped / result.totalSessions * 100) : 0;
1498
+ console.log(` AI Sessions (deduped): ${result.aiSessionsDeduped} (${share}% of total)`);
1499
+ }
1493
1500
  console.log();
1494
1501
  if (result.aiReferrals.length > 0) {
1502
+ const attrWidth = 12;
1495
1503
  console.log(" AI REFERRAL SOURCES");
1496
- console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1497
- console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1504
+ console.log(` ${"SOURCE".padEnd(25)} ${"MEDIUM".padEnd(15)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1505
+ console.log(` ${"\u2500".repeat(25)} ${"\u2500".repeat(15)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1498
1506
  for (const ref of result.aiReferrals) {
1507
+ const dimLabel = ref.sourceDimension === "first_user" ? "first-visit" : ref.sourceDimension === "manual_utm" ? "utm" : "session";
1499
1508
  console.log(
1500
- ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${String(ref.sessions).padEnd(10)}${String(ref.users).padEnd(8)}`
1509
+ ` ${ref.source.padEnd(25)} ${ref.medium.padEnd(15)} ${dimLabel.padEnd(attrWidth)} ${String(ref.sessions).padEnd(10)}${String(ref.users).padEnd(8)}`
1501
1510
  );
1502
1511
  }
1503
1512
  console.log();
@@ -1532,13 +1541,15 @@ async function gaAiReferralHistory(project, format) {
1532
1541
  }
1533
1542
  const dateWidth = 12;
1534
1543
  const sourceWidth = Math.min(30, Math.max(10, ...result.map((r) => r.source.length)));
1544
+ const attrWidth = 12;
1535
1545
  console.log(`GA4 AI Referral History for "${project}":
1536
1546
  `);
1537
- console.log(` ${"DATE".padEnd(dateWidth)} ${"SOURCE".padEnd(sourceWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1538
- console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(sourceWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1547
+ console.log(` ${"DATE".padEnd(dateWidth)} ${"SOURCE".padEnd(sourceWidth)} ${"ATTRIBUTION".padEnd(attrWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1548
+ console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(sourceWidth)} ${"\u2500".repeat(attrWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1539
1549
  for (const row of result) {
1550
+ const dimLabel = row.sourceDimension === "first_user" ? "first-visit" : row.sourceDimension === "manual_utm" ? "utm" : "session";
1540
1551
  console.log(
1541
- ` ${row.date.padEnd(dateWidth)} ${row.source.padEnd(sourceWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
1552
+ ` ${row.date.padEnd(dateWidth)} ${row.source.padEnd(sourceWidth)} ${dimLabel.padEnd(attrWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
1542
1553
  );
1543
1554
  }
1544
1555
  }
@@ -2631,6 +2642,7 @@ async function importKeywords(project, filePath, format) {
2631
2642
  throw new CliError({
2632
2643
  code: "KEYWORD_IMPORT_FILE_NOT_FOUND",
2633
2644
  message: `File not found: ${filePath}`,
2645
+ displayMessage: `Error: file not found: ${filePath}`,
2634
2646
  details: {
2635
2647
  project,
2636
2648
  filePath
@@ -4852,6 +4864,7 @@ var envSchema = z.object({
4852
4864
  WORKER_PORT: z.coerce.number().int().positive().default(3001),
4853
4865
  WEB_PORT: z.coerce.number().int().positive().default(4173),
4854
4866
  BOOTSTRAP_SECRET: z.string().default("change-me"),
4867
+ CANONRY_BASE_PATH: z.string().default("/"),
4855
4868
  // Gemini
4856
4869
  GEMINI_API_KEY: z.string().optional(),
4857
4870
  GEMINI_MODEL: z.string().optional(),
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-ETP5IOHC.js";
4
+ } from "./chunk-5MOWJJND.js";
5
5
  export {
6
6
  createServer,
7
7
  loadConfig
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.35.0",
3
+ "version": "1.37.0",
4
4
  "type": "module",
5
5
  "description": "The ultimate open-source AEO monitoring tool - track how answer engines cite your domain",
6
6
  "license": "FSL-1.1-ALv2",
@@ -56,17 +56,17 @@
56
56
  "tsx": "^4.19.0",
57
57
  "@ainyc/canonry-api-routes": "0.0.0",
58
58
  "@ainyc/canonry-config": "0.0.0",
59
- "@ainyc/canonry-integration-bing": "0.0.0",
60
- "@ainyc/canonry-db": "0.0.0",
61
59
  "@ainyc/canonry-contracts": "0.0.0",
60
+ "@ainyc/canonry-db": "0.0.0",
61
+ "@ainyc/canonry-integration-bing": "0.0.0",
62
62
  "@ainyc/canonry-integration-google": "0.0.0",
63
63
  "@ainyc/canonry-integration-wordpress": "0.0.0",
64
64
  "@ainyc/canonry-provider-cdp": "0.0.0",
65
65
  "@ainyc/canonry-provider-claude": "0.0.0",
66
- "@ainyc/canonry-provider-local": "0.0.0",
66
+ "@ainyc/canonry-provider-gemini": "0.0.0",
67
67
  "@ainyc/canonry-provider-openai": "0.0.0",
68
- "@ainyc/canonry-provider-perplexity": "0.0.0",
69
- "@ainyc/canonry-provider-gemini": "0.0.0"
68
+ "@ainyc/canonry-provider-local": "0.0.0",
69
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "build": "tsup && tsx build-web.ts",