@ainyc/canonry 1.34.0 → 1.36.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-BPuIj1DV.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-CxMuEW6I.css">
15
+ <script type="module" crossorigin src="./assets/index-Du_w835k.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-CborW-lk.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -1074,6 +1074,19 @@ var ga4TrafficSummaryDtoSchema = z11.object({
1074
1074
  aiReferrals: z11.array(ga4AiReferralDtoSchema),
1075
1075
  lastSyncedAt: z11.string().nullable()
1076
1076
  });
1077
+ var ga4AiReferralHistoryEntrySchema = z11.object({
1078
+ date: z11.string(),
1079
+ source: z11.string(),
1080
+ medium: z11.string(),
1081
+ sessions: z11.number(),
1082
+ users: z11.number()
1083
+ });
1084
+ var ga4SessionHistoryEntrySchema = z11.object({
1085
+ date: z11.string(),
1086
+ sessions: z11.number(),
1087
+ organicSessions: z11.number(),
1088
+ users: z11.number()
1089
+ });
1077
1090
 
1078
1091
  // ../contracts/src/answer-visibility.ts
1079
1092
  var GENERIC_TOKENS = /* @__PURE__ */ new Set([
@@ -1512,6 +1525,7 @@ function createClient(databasePath) {
1512
1525
  const sqlite = new Database(databasePath);
1513
1526
  sqlite.pragma("journal_mode = WAL");
1514
1527
  sqlite.pragma("foreign_keys = ON");
1528
+ sqlite.pragma("busy_timeout = 5000");
1515
1529
  return drizzle(sqlite, { schema: schema_exports });
1516
1530
  }
1517
1531
 
@@ -1839,7 +1853,14 @@ var MIGRATIONS = [
1839
1853
  `CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_source ON ga_ai_referrals(source)`,
1840
1854
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique ON ga_ai_referrals(project_id, date, source, medium)`,
1841
1855
  // v18: Answer-level visibility derived from answer text
1842
- `ALTER TABLE query_snapshots ADD COLUMN answer_mentioned INTEGER`
1856
+ `ALTER TABLE query_snapshots ADD COLUMN answer_mentioned INTEGER`,
1857
+ // v19: Add named unique indexes and missing columns from early tables
1858
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_keywords_project_keyword ON keywords(project_id, keyword)`,
1859
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_competitors_project_domain ON competitors(project_id, domain)`,
1860
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_schedules_project ON schedules(project_id)`,
1861
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_usage_scope_period_metric ON usage_counters(scope, period, metric)`,
1862
+ `ALTER TABLE projects ADD COLUMN config_source TEXT NOT NULL DEFAULT 'cli'`,
1863
+ `ALTER TABLE projects ADD COLUMN config_revision INTEGER NOT NULL DEFAULT 1`
1843
1864
  ];
1844
1865
  function migrate(db) {
1845
1866
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -5754,6 +5775,30 @@ var routeCatalog = [
5754
5775
  404: { description: "Project not found." }
5755
5776
  }
5756
5777
  },
5778
+ {
5779
+ method: "get",
5780
+ path: "/api/v1/projects/{name}/ga/ai-referral-history",
5781
+ summary: "Get AI referral sessions per day grouped by source",
5782
+ tags: ["ga4"],
5783
+ parameters: [nameParameter],
5784
+ responses: {
5785
+ 200: { description: "AI referral history returned." },
5786
+ 400: { description: "GA4 is not connected." },
5787
+ 404: { description: "Project not found." }
5788
+ }
5789
+ },
5790
+ {
5791
+ method: "get",
5792
+ path: "/api/v1/projects/{name}/ga/session-history",
5793
+ summary: "Get total sessions per day for the project",
5794
+ tags: ["ga4"],
5795
+ parameters: [nameParameter],
5796
+ responses: {
5797
+ 200: { description: "Session history returned." },
5798
+ 400: { description: "GA4 is not connected." },
5799
+ 404: { description: "Project not found." }
5800
+ }
5801
+ },
5757
5802
  {
5758
5803
  method: "get",
5759
5804
  path: "/api/v1/projects/{name}/ga/coverage",
@@ -6263,6 +6308,7 @@ var GSC_MAX_ROWS_PER_REQUEST = 25e3;
6263
6308
  var GSC_DATA_LAG_DAYS = 3;
6264
6309
  var INDEXING_API_BASE = "https://indexing.googleapis.com/v3";
6265
6310
  var INDEXING_API_DAILY_LIMIT = 200;
6311
+ var GOOGLE_REQUEST_TIMEOUT_MS = 3e4;
6266
6312
 
6267
6313
  // ../integration-google/src/types.ts
6268
6314
  var GoogleAuthError = class extends Error {
@@ -6303,7 +6349,8 @@ async function exchangeCode(clientId, clientSecret, code, redirectUri) {
6303
6349
  code,
6304
6350
  redirect_uri: redirectUri,
6305
6351
  grant_type: "authorization_code"
6306
- })
6352
+ }),
6353
+ signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
6307
6354
  });
6308
6355
  if (!res.ok) {
6309
6356
  const body = await res.text();
@@ -6320,7 +6367,8 @@ async function refreshAccessToken(clientId, clientSecret, currentRefreshToken) {
6320
6367
  client_secret: clientSecret,
6321
6368
  refresh_token: currentRefreshToken,
6322
6369
  grant_type: "refresh_token"
6323
- })
6370
+ }),
6371
+ signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
6324
6372
  });
6325
6373
  if (!res.ok) {
6326
6374
  const body = await res.text();
@@ -6344,7 +6392,8 @@ async function gscFetch(accessToken, url, opts) {
6344
6392
  const res = await fetch(url, {
6345
6393
  method,
6346
6394
  headers,
6347
- body: opts?.body != null ? JSON.stringify(opts.body) : void 0
6395
+ body: opts?.body != null ? JSON.stringify(opts.body) : void 0,
6396
+ signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
6348
6397
  });
6349
6398
  if (res.status === 401) {
6350
6399
  const body = await res.text().catch(() => "");
@@ -6446,6 +6495,7 @@ var GA4_SCOPE = "https://www.googleapis.com/auth/analytics.readonly";
6446
6495
  var GOOGLE_TOKEN_URL2 = "https://oauth2.googleapis.com/token";
6447
6496
  var GA4_DEFAULT_SYNC_DAYS = 30;
6448
6497
  var GA4_MAX_SYNC_DAYS = 90;
6498
+ var GA4_REQUEST_TIMEOUT_MS = 3e4;
6449
6499
 
6450
6500
  // ../integration-google-analytics/src/types.ts
6451
6501
  var GA4ApiError = class extends Error {
@@ -6491,7 +6541,8 @@ async function getAccessToken(clientEmail, privateKey) {
6491
6541
  body: new URLSearchParams({
6492
6542
  grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
6493
6543
  assertion: jwt
6494
- })
6544
+ }),
6545
+ signal: AbortSignal.timeout(GA4_REQUEST_TIMEOUT_MS)
6495
6546
  });
6496
6547
  if (!res.ok) {
6497
6548
  const body = await res.text().catch(() => "");
@@ -6509,7 +6560,8 @@ async function runReport(accessToken, propertyId, request) {
6509
6560
  "Authorization": `Bearer ${accessToken}`,
6510
6561
  "Content-Type": "application/json"
6511
6562
  },
6512
- body: JSON.stringify(request)
6563
+ body: JSON.stringify(request),
6564
+ signal: AbortSignal.timeout(GA4_REQUEST_TIMEOUT_MS)
6513
6565
  });
6514
6566
  if (res.status === 401 || res.status === 403) {
6515
6567
  const body = await res.text().catch(() => "");
@@ -6549,7 +6601,8 @@ async function batchRunReports(accessToken, propertyId, requests) {
6549
6601
  "Authorization": `Bearer ${accessToken}`,
6550
6602
  "Content-Type": "application/json"
6551
6603
  },
6552
- body: JSON.stringify({ requests })
6604
+ body: JSON.stringify({ requests }),
6605
+ signal: AbortSignal.timeout(GA4_REQUEST_TIMEOUT_MS)
6553
6606
  });
6554
6607
  if (res.status === 401 || res.status === 403) {
6555
6608
  const body = await res.text().catch(() => "");
@@ -7488,6 +7541,7 @@ import { eq as eq14, and as and3, desc as desc5 } from "drizzle-orm";
7488
7541
  var BING_WMT_API_BASE = "https://ssl.bing.com/webmaster/api.svc/json";
7489
7542
  var BING_SUBMIT_URL_BATCH_LIMIT = 500;
7490
7543
  var BING_SUBMIT_URL_DAILY_LIMIT = 1e4;
7544
+ var BING_REQUEST_TIMEOUT_MS = 3e4;
7491
7545
 
7492
7546
  // ../integration-bing/src/types.ts
7493
7547
  var BingApiError = class extends Error {
@@ -7515,7 +7569,8 @@ async function bingFetch(apiKey, endpoint, opts) {
7515
7569
  const res = await fetch(url, {
7516
7570
  method,
7517
7571
  headers,
7518
- body: opts?.body != null ? JSON.stringify(opts.body) : void 0
7572
+ body: opts?.body != null ? JSON.stringify(opts.body) : void 0,
7573
+ signal: AbortSignal.timeout(BING_REQUEST_TIMEOUT_MS)
7519
7574
  });
7520
7575
  if (res.status === 401 || res.status === 403) {
7521
7576
  const body = await res.text().catch(() => "");
@@ -8463,6 +8518,34 @@ async function ga4Routes(app, opts) {
8463
8518
  lastSyncedAt: latestSync?.syncedAt ?? null
8464
8519
  };
8465
8520
  });
8521
+ app.get("/projects/:name/ga/ai-referral-history", async (request, _reply) => {
8522
+ const project = resolveProject(app.db, request.params.name);
8523
+ requireGa4Connection(opts, project.name, project.canonicalDomain);
8524
+ const rows = app.db.select({
8525
+ date: gaAiReferrals.date,
8526
+ source: gaAiReferrals.source,
8527
+ medium: gaAiReferrals.medium,
8528
+ sessions: gaAiReferrals.sessions,
8529
+ users: gaAiReferrals.users
8530
+ }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
8531
+ return rows;
8532
+ });
8533
+ app.get("/projects/:name/ga/session-history", async (request, _reply) => {
8534
+ const project = resolveProject(app.db, request.params.name);
8535
+ requireGa4Connection(opts, project.name, project.canonicalDomain);
8536
+ const rows = app.db.select({
8537
+ date: gaTrafficSnapshots.date,
8538
+ sessions: sql4`SUM(${gaTrafficSnapshots.sessions})`,
8539
+ organicSessions: sql4`SUM(${gaTrafficSnapshots.organicSessions})`,
8540
+ users: sql4`SUM(${gaTrafficSnapshots.users})`
8541
+ }).from(gaTrafficSnapshots).where(eq16(gaTrafficSnapshots.projectId, project.id)).groupBy(gaTrafficSnapshots.date).orderBy(gaTrafficSnapshots.date).all();
8542
+ return rows.map((r) => ({
8543
+ date: r.date,
8544
+ sessions: r.sessions ?? 0,
8545
+ organicSessions: r.organicSessions ?? 0,
8546
+ users: r.users ?? 0
8547
+ }));
8548
+ });
8466
8549
  app.get("/projects/:name/ga/coverage", async (request, _reply) => {
8467
8550
  const project = resolveProject(app.db, request.params.name);
8468
8551
  requireGa4Connection(opts, project.name, project.canonicalDomain);
@@ -8606,6 +8689,8 @@ function parseSchemaPageEntry(entry) {
8606
8689
 
8607
8690
  // ../integration-wordpress/src/wordpress-client.ts
8608
8691
  import crypto17 from "crypto";
8692
+ var WP_REQUEST_TIMEOUT_MS = 3e4;
8693
+ var WP_FETCH_TEXT_TIMEOUT_MS = 15e3;
8609
8694
  var PAGE_FIELDS = "id,slug,status,link,modified,modified_gmt,title,content,meta";
8610
8695
  var PAGE_LIST_FIELDS = "id,slug,status,link,modified,modified_gmt,title";
8611
8696
  var VERIFY_PAGE_FIELDS = "id,status";
@@ -8662,7 +8747,8 @@ async function fetchJson(connection, siteUrl, path7, init) {
8662
8747
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
8663
8748
  ...init?.body != null ? { "Content-Type": "application/json" } : {},
8664
8749
  ...init?.headers ?? {}
8665
- }
8750
+ },
8751
+ signal: AbortSignal.timeout(WP_REQUEST_TIMEOUT_MS)
8666
8752
  });
8667
8753
  if (res.status === 401 || res.status === 403) {
8668
8754
  const text2 = await res.text().catch(() => "");
@@ -8705,7 +8791,7 @@ async function fetchPageCollectionSummary(connection, siteUrl, options) {
8705
8791
  }
8706
8792
  async function fetchText(url) {
8707
8793
  try {
8708
- const res = await fetch(url);
8794
+ const res = await fetch(url, { signal: AbortSignal.timeout(WP_FETCH_TEXT_TIMEOUT_MS) });
8709
8795
  if (!res.ok) return null;
8710
8796
  return await res.text();
8711
8797
  } catch {
@@ -8886,12 +8972,20 @@ async function getSiteStatus(connection, env) {
8886
8972
  async function listActivePlugins(connection, env) {
8887
8973
  const site = resolveEnvironment(connection, env);
8888
8974
  try {
8889
- const { body } = await fetchJson(
8890
- connection,
8891
- site.siteUrl,
8892
- "/wp-json/wp/v2/plugins?per_page=100&_fields=plugin,status"
8893
- );
8894
- return body.filter((plugin) => plugin.status === "active").map((plugin) => plugin.plugin).sort();
8975
+ const allPlugins = [];
8976
+ let page = 1;
8977
+ let totalPages = 1;
8978
+ while (page <= totalPages) {
8979
+ const { body, response } = await fetchJson(
8980
+ connection,
8981
+ site.siteUrl,
8982
+ `/wp-json/wp/v2/plugins?per_page=100&page=${page}&_fields=plugin,status`
8983
+ );
8984
+ totalPages = Number.parseInt(response.headers.get("x-wp-totalpages") ?? "1", 10) || 1;
8985
+ allPlugins.push(...body);
8986
+ page += 1;
8987
+ }
8988
+ return allPlugins.filter((plugin) => plugin.status === "active").map((plugin) => plugin.plugin).sort();
8895
8989
  } catch (error) {
8896
8990
  if (error instanceof WordpressApiError && (error.statusCode === 403 || error.statusCode === 404)) {
8897
8991
  return null;
@@ -10699,7 +10793,7 @@ function extractCitedDomains2(raw) {
10699
10793
  function extractDomainFromUri2(uri) {
10700
10794
  try {
10701
10795
  const url = new URL(uri);
10702
- return url.hostname.replace(/^www\./, "");
10796
+ return url.hostname.replace(/^www\./, "").toLowerCase();
10703
10797
  } catch {
10704
10798
  return null;
10705
10799
  }
@@ -10707,11 +10801,11 @@ function extractDomainFromUri2(uri) {
10707
10801
  async function generateText2(prompt, config) {
10708
10802
  const model = config.model ?? DEFAULT_MODEL2;
10709
10803
  const client = new OpenAI({ apiKey: config.apiKey });
10710
- const response = await client.chat.completions.create({
10804
+ const response = await client.responses.create({
10711
10805
  model,
10712
- messages: [{ role: "user", content: prompt }]
10806
+ input: prompt
10713
10807
  });
10714
- return response.choices[0]?.message?.content ?? "";
10808
+ return extractResponseText(response);
10715
10809
  }
10716
10810
  function responseToRecord2(response) {
10717
10811
  try {
@@ -10960,7 +11054,7 @@ function extractCitedDomains3(raw) {
10960
11054
  function extractDomainFromUri3(uri) {
10961
11055
  try {
10962
11056
  const url = new URL(uri);
10963
- return url.hostname.replace(/^www\./, "");
11057
+ return url.hostname.replace(/^www\./, "").toLowerCase();
10964
11058
  } catch {
10965
11059
  return null;
10966
11060
  }
@@ -11126,7 +11220,7 @@ async function executeTrackedQuery4(input) {
11126
11220
  });
11127
11221
  return {
11128
11222
  provider: "local",
11129
- rawResponse: JSON.parse(JSON.stringify(response)),
11223
+ rawResponse: responseToRecord4(response),
11130
11224
  model,
11131
11225
  groundingSources: [],
11132
11226
  searchQueries: []
@@ -11181,6 +11275,13 @@ function extractDomainMentions(text2) {
11181
11275
  }
11182
11276
  return [...domains];
11183
11277
  }
11278
+ function responseToRecord4(response) {
11279
+ try {
11280
+ return JSON.parse(JSON.stringify(response));
11281
+ } catch {
11282
+ return { error: "failed to serialize response" };
11283
+ }
11284
+ }
11184
11285
 
11185
11286
  // ../provider-local/src/adapter.ts
11186
11287
  function toLocalConfig(config) {
@@ -11710,6 +11811,10 @@ function getConnection(config) {
11710
11811
  if (parts.length >= 1 && parts[0]) host = parts[0];
11711
11812
  if (parts.length >= 2 && parts[1]) port = parseInt(parts[1], 10) || 9222;
11712
11813
  if (!sharedConnection || sharedConnection.endpoint !== `${host}:${port}`) {
11814
+ if (sharedConnection) {
11815
+ sharedConnection.disconnect().catch(() => {
11816
+ });
11817
+ }
11713
11818
  sharedConnection = new CDPConnectionManager(host, port);
11714
11819
  }
11715
11820
  return sharedConnection;
@@ -11859,7 +11964,7 @@ async function executeTrackedQuery5(input) {
11859
11964
  { role: "user", content: prompt }
11860
11965
  ]
11861
11966
  });
11862
- const rawResponse = responseToRecord4(response);
11967
+ const rawResponse = responseToRecord5(response);
11863
11968
  const citations = extractCitations(rawResponse);
11864
11969
  const groundingSources = citations.map((url) => ({
11865
11970
  uri: url,
@@ -11923,7 +12028,7 @@ function extractCitedDomains5(groundingSources) {
11923
12028
  function extractDomainFromUri4(uri) {
11924
12029
  try {
11925
12030
  const url = new URL(uri);
11926
- return url.hostname.replace(/^www\./, "");
12031
+ return url.hostname.replace(/^www\./, "").toLowerCase();
11927
12032
  } catch {
11928
12033
  return null;
11929
12034
  }
@@ -11937,7 +12042,7 @@ async function generateText5(prompt, config) {
11937
12042
  });
11938
12043
  return response.choices[0]?.message?.content ?? "";
11939
12044
  }
11940
- function responseToRecord4(response) {
12045
+ function responseToRecord5(response) {
11941
12046
  try {
11942
12047
  return JSON.parse(JSON.stringify(response));
11943
12048
  } catch {
@@ -12569,7 +12674,7 @@ var JobRunner = class {
12569
12674
  });
12570
12675
  if (this.onRunCompleted) {
12571
12676
  this.onRunCompleted(runId, projectId).catch((notifErr) => {
12572
- console.error("[JobRunner] Notification callback failed:", notifErr);
12677
+ log.error("notification.callback-failed", { runId, error: notifErr instanceof Error ? notifErr.message : String(notifErr) });
12573
12678
  });
12574
12679
  }
12575
12680
  }
@@ -13269,45 +13374,49 @@ var Scheduler = class {
13269
13374
  log4.info("cron.registered", { projectId, schedule: label, timezone });
13270
13375
  }
13271
13376
  triggerRun(scheduleId, projectId) {
13272
- const now = (/* @__PURE__ */ new Date()).toISOString();
13273
- const currentSchedule = this.db.select().from(schedules).where(eq20(schedules.id, scheduleId)).get();
13274
- if (!currentSchedule || currentSchedule.enabled !== 1) {
13275
- log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
13276
- this.remove(projectId);
13277
- return;
13278
- }
13279
- const task = this.tasks.get(projectId);
13280
- const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
13281
- const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
13282
- if (!project) {
13283
- log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
13284
- this.remove(projectId);
13285
- return;
13286
- }
13287
- const queueResult = queueRunIfProjectIdle(this.db, {
13288
- createdAt: now,
13289
- kind: "answer-visibility",
13290
- projectId,
13291
- trigger: "scheduled"
13292
- });
13293
- if (queueResult.conflict) {
13294
- log4.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
13377
+ try {
13378
+ const now = (/* @__PURE__ */ new Date()).toISOString();
13379
+ const currentSchedule = this.db.select().from(schedules).where(eq20(schedules.id, scheduleId)).get();
13380
+ if (!currentSchedule || currentSchedule.enabled !== 1) {
13381
+ log4.warn("schedule.stale", { scheduleId, projectId, msg: "schedule no longer exists or is disabled" });
13382
+ this.remove(projectId);
13383
+ return;
13384
+ }
13385
+ const task = this.tasks.get(projectId);
13386
+ const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
13387
+ const project = this.db.select().from(projects).where(eq20(projects.id, projectId)).get();
13388
+ if (!project) {
13389
+ log4.error("project.not-found", { projectId, msg: "skipping scheduled run" });
13390
+ this.remove(projectId);
13391
+ return;
13392
+ }
13393
+ const queueResult = queueRunIfProjectIdle(this.db, {
13394
+ createdAt: now,
13395
+ kind: "answer-visibility",
13396
+ projectId,
13397
+ trigger: "scheduled"
13398
+ });
13399
+ if (queueResult.conflict) {
13400
+ log4.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
13401
+ this.db.update(schedules).set({
13402
+ nextRunAt,
13403
+ updatedAt: now
13404
+ }).where(eq20(schedules.id, currentSchedule.id)).run();
13405
+ return;
13406
+ }
13407
+ const runId = queueResult.runId;
13295
13408
  this.db.update(schedules).set({
13409
+ lastRunAt: now,
13296
13410
  nextRunAt,
13297
13411
  updatedAt: now
13298
13412
  }).where(eq20(schedules.id, currentSchedule.id)).run();
13299
- return;
13413
+ const scheduleProviders = JSON.parse(currentSchedule.providers);
13414
+ const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13415
+ log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
13416
+ this.callbacks.onRunCreated(runId, projectId, providers);
13417
+ } catch (err) {
13418
+ log4.error("trigger.error", { scheduleId, projectId, error: err instanceof Error ? err.message : String(err) });
13300
13419
  }
13301
- const runId = queueResult.runId;
13302
- this.db.update(schedules).set({
13303
- lastRunAt: now,
13304
- nextRunAt,
13305
- updatedAt: now
13306
- }).where(eq20(schedules.id, currentSchedule.id)).run();
13307
- const scheduleProviders = JSON.parse(currentSchedule.providers);
13308
- const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13309
- log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
13310
- this.callbacks.onRunCreated(runId, projectId, providers);
13311
13420
  }
13312
13421
  };
13313
13422
 
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  setGoogleAuthConfig,
27
27
  showFirstRunNotice,
28
28
  trackEvent
29
- } from "./chunk-4SRBJCNX.js";
29
+ } from "./chunk-PZKK53EX.js";
30
30
 
31
31
  // src/cli.ts
32
32
  import { pathToFileURL } from "url";
@@ -667,6 +667,12 @@ var ApiClient = class {
667
667
  async gaCoverage(project) {
668
668
  return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/coverage`);
669
669
  }
670
+ async gaAiReferralHistory(project) {
671
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/ai-referral-history`);
672
+ }
673
+ async gaSessionHistory(project) {
674
+ return this.request("GET", `/projects/${encodeURIComponent(project)}/ga/session-history`);
675
+ }
670
676
  async wordpressConnect(project, body) {
671
677
  return this.request("POST", `/projects/${encodeURIComponent(project)}/wordpress/connect`, body);
672
678
  }
@@ -1516,6 +1522,29 @@ async function gaTraffic(project, opts) {
1516
1522
  Last synced: ${result.lastSyncedAt}`);
1517
1523
  }
1518
1524
  }
1525
+ async function gaAiReferralHistory(project, format) {
1526
+ const client = getClient3();
1527
+ const result = await client.gaAiReferralHistory(project);
1528
+ if (format === "json") {
1529
+ console.log(JSON.stringify(result, null, 2));
1530
+ return;
1531
+ }
1532
+ if (result.length === 0) {
1533
+ console.log('No AI referral history. Run "canonry ga sync <project>" first.');
1534
+ return;
1535
+ }
1536
+ const dateWidth = 12;
1537
+ const sourceWidth = Math.min(30, Math.max(10, ...result.map((r) => r.source.length)));
1538
+ console.log(`GA4 AI Referral History for "${project}":
1539
+ `);
1540
+ console.log(` ${"DATE".padEnd(dateWidth)} ${"SOURCE".padEnd(sourceWidth)} ${"SESSIONS".padEnd(10)}${"USERS".padEnd(8)}`);
1541
+ console.log(` ${"\u2500".repeat(dateWidth)} ${"\u2500".repeat(sourceWidth)} ${"\u2500".repeat(10)}${"\u2500".repeat(8)}`);
1542
+ for (const row of result) {
1543
+ console.log(
1544
+ ` ${row.date.padEnd(dateWidth)} ${row.source.padEnd(sourceWidth)} ${String(row.sessions).padEnd(10)}${String(row.users).padEnd(8)}`
1545
+ );
1546
+ }
1547
+ }
1519
1548
  async function gaCoverage(project, format) {
1520
1549
  const client = getClient3();
1521
1550
  const result = await client.gaCoverage(project);
@@ -1620,14 +1649,22 @@ var GA_CLI_COMMANDS = [
1620
1649
  await gaCoverage(project, input.format);
1621
1650
  }
1622
1651
  },
1652
+ {
1653
+ path: ["ga", "ai-referral-history"],
1654
+ usage: "canonry ga ai-referral-history <project> [--format json]",
1655
+ run: async (input) => {
1656
+ const project = requireProject(input, "ga.ai-referral-history", "canonry ga ai-referral-history <project> [--format json]");
1657
+ await gaAiReferralHistory(project, input.format);
1658
+ }
1659
+ },
1623
1660
  {
1624
1661
  path: ["ga"],
1625
- usage: "canonry ga <connect|disconnect|status|sync|traffic|coverage> <project> [args]",
1662
+ usage: "canonry ga <connect|disconnect|status|sync|traffic|coverage|ai-referral-history> <project> [args]",
1626
1663
  run: async (input) => {
1627
1664
  unknownSubcommand(input.positionals[0], {
1628
1665
  command: "ga",
1629
- usage: "canonry ga <connect|disconnect|status|sync|traffic|coverage> <project> [args]",
1630
- available: ["connect", "disconnect", "status", "sync", "traffic", "coverage"]
1666
+ usage: "canonry ga <connect|disconnect|status|sync|traffic|coverage|ai-referral-history> <project> [args]",
1667
+ available: ["connect", "disconnect", "status", "sync", "traffic", "coverage", "ai-referral-history"]
1631
1668
  });
1632
1669
  }
1633
1670
  }
@@ -2597,6 +2634,7 @@ async function importKeywords(project, filePath, format) {
2597
2634
  throw new CliError({
2598
2635
  code: "KEYWORD_IMPORT_FILE_NOT_FOUND",
2599
2636
  message: `File not found: ${filePath}`,
2637
+ displayMessage: `Error: file not found: ${filePath}`,
2600
2638
  details: {
2601
2639
  project,
2602
2640
  filePath
@@ -4818,6 +4856,7 @@ var envSchema = z.object({
4818
4856
  WORKER_PORT: z.coerce.number().int().positive().default(3001),
4819
4857
  WEB_PORT: z.coerce.number().int().positive().default(4173),
4820
4858
  BOOTSTRAP_SECRET: z.string().default("change-me"),
4859
+ CANONRY_BASE_PATH: z.string().default("/"),
4821
4860
  // Gemini
4822
4861
  GEMINI_API_KEY: z.string().optional(),
4823
4862
  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-4SRBJCNX.js";
4
+ } from "./chunk-PZKK53EX.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.34.0",
3
+ "version": "1.36.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",
@@ -55,18 +55,18 @@
55
55
  "tsup": "^8.5.1",
56
56
  "tsx": "^4.19.0",
57
57
  "@ainyc/canonry-api-routes": "0.0.0",
58
+ "@ainyc/canonry-config": "0.0.0",
58
59
  "@ainyc/canonry-contracts": "0.0.0",
59
60
  "@ainyc/canonry-integration-bing": "0.0.0",
60
61
  "@ainyc/canonry-db": "0.0.0",
61
62
  "@ainyc/canonry-integration-google": "0.0.0",
62
63
  "@ainyc/canonry-integration-wordpress": "0.0.0",
63
- "@ainyc/canonry-config": "0.0.0",
64
+ "@ainyc/canonry-provider-cdp": "0.0.0",
64
65
  "@ainyc/canonry-provider-claude": "0.0.0",
65
- "@ainyc/canonry-provider-local": "0.0.0",
66
+ "@ainyc/canonry-provider-openai": "0.0.0",
66
67
  "@ainyc/canonry-provider-gemini": "0.0.0",
67
68
  "@ainyc/canonry-provider-perplexity": "0.0.0",
68
- "@ainyc/canonry-provider-openai": "0.0.0",
69
- "@ainyc/canonry-provider-cdp": "0.0.0"
69
+ "@ainyc/canonry-provider-local": "0.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "build": "tsup && tsx build-web.ts",