@ainyc/canonry 1.35.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-DyipkdOb.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-B9SBdBOm.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>
@@ -1081,6 +1081,12 @@ var ga4AiReferralHistoryEntrySchema = z11.object({
1081
1081
  sessions: z11.number(),
1082
1082
  users: z11.number()
1083
1083
  });
1084
+ var ga4SessionHistoryEntrySchema = z11.object({
1085
+ date: z11.string(),
1086
+ sessions: z11.number(),
1087
+ organicSessions: z11.number(),
1088
+ users: z11.number()
1089
+ });
1084
1090
 
1085
1091
  // ../contracts/src/answer-visibility.ts
1086
1092
  var GENERIC_TOKENS = /* @__PURE__ */ new Set([
@@ -1519,6 +1525,7 @@ function createClient(databasePath) {
1519
1525
  const sqlite = new Database(databasePath);
1520
1526
  sqlite.pragma("journal_mode = WAL");
1521
1527
  sqlite.pragma("foreign_keys = ON");
1528
+ sqlite.pragma("busy_timeout = 5000");
1522
1529
  return drizzle(sqlite, { schema: schema_exports });
1523
1530
  }
1524
1531
 
@@ -1846,7 +1853,14 @@ var MIGRATIONS = [
1846
1853
  `CREATE INDEX IF NOT EXISTS idx_ga_ai_ref_source ON ga_ai_referrals(source)`,
1847
1854
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique ON ga_ai_referrals(project_id, date, source, medium)`,
1848
1855
  // v18: Answer-level visibility derived from answer text
1849
- `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`
1850
1864
  ];
1851
1865
  function migrate(db) {
1852
1866
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
@@ -5773,6 +5787,18 @@ var routeCatalog = [
5773
5787
  404: { description: "Project not found." }
5774
5788
  }
5775
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
+ },
5776
5802
  {
5777
5803
  method: "get",
5778
5804
  path: "/api/v1/projects/{name}/ga/coverage",
@@ -8504,6 +8530,22 @@ async function ga4Routes(app, opts) {
8504
8530
  }).from(gaAiReferrals).where(eq16(gaAiReferrals.projectId, project.id)).orderBy(gaAiReferrals.date).all();
8505
8531
  return rows;
8506
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
+ });
8507
8549
  app.get("/projects/:name/ga/coverage", async (request, _reply) => {
8508
8550
  const project = resolveProject(app.db, request.params.name);
8509
8551
  requireGa4Connection(opts, project.name, project.canonicalDomain);
@@ -8930,12 +8972,20 @@ async function getSiteStatus(connection, env) {
8930
8972
  async function listActivePlugins(connection, env) {
8931
8973
  const site = resolveEnvironment(connection, env);
8932
8974
  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();
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();
8939
8989
  } catch (error) {
8940
8990
  if (error instanceof WordpressApiError && (error.statusCode === 403 || error.statusCode === 404)) {
8941
8991
  return null;
@@ -10743,7 +10793,7 @@ function extractCitedDomains2(raw) {
10743
10793
  function extractDomainFromUri2(uri) {
10744
10794
  try {
10745
10795
  const url = new URL(uri);
10746
- return url.hostname.replace(/^www\./, "");
10796
+ return url.hostname.replace(/^www\./, "").toLowerCase();
10747
10797
  } catch {
10748
10798
  return null;
10749
10799
  }
@@ -10751,11 +10801,11 @@ function extractDomainFromUri2(uri) {
10751
10801
  async function generateText2(prompt, config) {
10752
10802
  const model = config.model ?? DEFAULT_MODEL2;
10753
10803
  const client = new OpenAI({ apiKey: config.apiKey });
10754
- const response = await client.chat.completions.create({
10804
+ const response = await client.responses.create({
10755
10805
  model,
10756
- messages: [{ role: "user", content: prompt }]
10806
+ input: prompt
10757
10807
  });
10758
- return response.choices[0]?.message?.content ?? "";
10808
+ return extractResponseText(response);
10759
10809
  }
10760
10810
  function responseToRecord2(response) {
10761
10811
  try {
@@ -11004,7 +11054,7 @@ function extractCitedDomains3(raw) {
11004
11054
  function extractDomainFromUri3(uri) {
11005
11055
  try {
11006
11056
  const url = new URL(uri);
11007
- return url.hostname.replace(/^www\./, "");
11057
+ return url.hostname.replace(/^www\./, "").toLowerCase();
11008
11058
  } catch {
11009
11059
  return null;
11010
11060
  }
@@ -11170,7 +11220,7 @@ async function executeTrackedQuery4(input) {
11170
11220
  });
11171
11221
  return {
11172
11222
  provider: "local",
11173
- rawResponse: JSON.parse(JSON.stringify(response)),
11223
+ rawResponse: responseToRecord4(response),
11174
11224
  model,
11175
11225
  groundingSources: [],
11176
11226
  searchQueries: []
@@ -11225,6 +11275,13 @@ function extractDomainMentions(text2) {
11225
11275
  }
11226
11276
  return [...domains];
11227
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
+ }
11228
11285
 
11229
11286
  // ../provider-local/src/adapter.ts
11230
11287
  function toLocalConfig(config) {
@@ -11754,6 +11811,10 @@ function getConnection(config) {
11754
11811
  if (parts.length >= 1 && parts[0]) host = parts[0];
11755
11812
  if (parts.length >= 2 && parts[1]) port = parseInt(parts[1], 10) || 9222;
11756
11813
  if (!sharedConnection || sharedConnection.endpoint !== `${host}:${port}`) {
11814
+ if (sharedConnection) {
11815
+ sharedConnection.disconnect().catch(() => {
11816
+ });
11817
+ }
11757
11818
  sharedConnection = new CDPConnectionManager(host, port);
11758
11819
  }
11759
11820
  return sharedConnection;
@@ -11903,7 +11964,7 @@ async function executeTrackedQuery5(input) {
11903
11964
  { role: "user", content: prompt }
11904
11965
  ]
11905
11966
  });
11906
- const rawResponse = responseToRecord4(response);
11967
+ const rawResponse = responseToRecord5(response);
11907
11968
  const citations = extractCitations(rawResponse);
11908
11969
  const groundingSources = citations.map((url) => ({
11909
11970
  uri: url,
@@ -11967,7 +12028,7 @@ function extractCitedDomains5(groundingSources) {
11967
12028
  function extractDomainFromUri4(uri) {
11968
12029
  try {
11969
12030
  const url = new URL(uri);
11970
- return url.hostname.replace(/^www\./, "");
12031
+ return url.hostname.replace(/^www\./, "").toLowerCase();
11971
12032
  } catch {
11972
12033
  return null;
11973
12034
  }
@@ -11981,7 +12042,7 @@ async function generateText5(prompt, config) {
11981
12042
  });
11982
12043
  return response.choices[0]?.message?.content ?? "";
11983
12044
  }
11984
- function responseToRecord4(response) {
12045
+ function responseToRecord5(response) {
11985
12046
  try {
11986
12047
  return JSON.parse(JSON.stringify(response));
11987
12048
  } catch {
@@ -12613,7 +12674,7 @@ var JobRunner = class {
12613
12674
  });
12614
12675
  if (this.onRunCompleted) {
12615
12676
  this.onRunCompleted(runId, projectId).catch((notifErr) => {
12616
- console.error("[JobRunner] Notification callback failed:", notifErr);
12677
+ log.error("notification.callback-failed", { runId, error: notifErr instanceof Error ? notifErr.message : String(notifErr) });
12617
12678
  });
12618
12679
  }
12619
12680
  }
@@ -13313,45 +13374,49 @@ var Scheduler = class {
13313
13374
  log4.info("cron.registered", { projectId, schedule: label, timezone });
13314
13375
  }
13315
13376
  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 });
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;
13339
13408
  this.db.update(schedules).set({
13409
+ lastRunAt: now,
13340
13410
  nextRunAt,
13341
13411
  updatedAt: now
13342
13412
  }).where(eq20(schedules.id, currentSchedule.id)).run();
13343
- 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) });
13344
13419
  }
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
13420
  }
13356
13421
  };
13357
13422
 
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-PZKK53EX.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
  }
@@ -2631,6 +2634,7 @@ async function importKeywords(project, filePath, format) {
2631
2634
  throw new CliError({
2632
2635
  code: "KEYWORD_IMPORT_FILE_NOT_FOUND",
2633
2636
  message: `File not found: ${filePath}`,
2637
+ displayMessage: `Error: file not found: ${filePath}`,
2634
2638
  details: {
2635
2639
  project,
2636
2640
  filePath
@@ -4852,6 +4856,7 @@ var envSchema = z.object({
4852
4856
  WORKER_PORT: z.coerce.number().int().positive().default(3001),
4853
4857
  WEB_PORT: z.coerce.number().int().positive().default(4173),
4854
4858
  BOOTSTRAP_SECRET: z.string().default("change-me"),
4859
+ CANONRY_BASE_PATH: z.string().default("/"),
4855
4860
  // Gemini
4856
4861
  GEMINI_API_KEY: z.string().optional(),
4857
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-ETP5IOHC.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.35.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",
@@ -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-contracts": "0.0.0",
59
60
  "@ainyc/canonry-integration-bing": "0.0.0",
60
61
  "@ainyc/canonry-db": "0.0.0",
61
- "@ainyc/canonry-contracts": "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",
67
66
  "@ainyc/canonry-provider-openai": "0.0.0",
67
+ "@ainyc/canonry-provider-gemini": "0.0.0",
68
68
  "@ainyc/canonry-provider-perplexity": "0.0.0",
69
- "@ainyc/canonry-provider-gemini": "0.0.0"
69
+ "@ainyc/canonry-provider-local": "0.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "build": "tsup && tsx build-web.ts",