@ainyc/canonry 1.37.0 → 1.37.1

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-DR4zYsnw.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-BZX6FEZ4.css">
15
+ <script type="module" crossorigin src="./assets/index-Du9ZvTq9.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-CM92zXYn.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -10387,19 +10387,11 @@ async function apiRoutes(app, opts) {
10387
10387
  // ../provider-gemini/src/normalize.ts
10388
10388
  import { GoogleGenAI } from "@google/genai";
10389
10389
  var DEFAULT_MODEL = "gemini-3-flash";
10390
- var VALIDATION_PATTERN = /^gemini-/;
10391
10390
  function isVertexConfig(config) {
10392
10391
  return !!config.vertexProject;
10393
10392
  }
10394
10393
  function resolveModel(config) {
10395
- const m = config.model;
10396
- if (!m) return DEFAULT_MODEL;
10397
- if (VALIDATION_PATTERN.test(m)) return m;
10398
- const backend = isVertexConfig(config) ? "Vertex AI" : "AI Studio";
10399
- console.warn(
10400
- `[provider-gemini] Invalid model name "${m}" \u2014 this provider uses the Gemini ${backend} API which only accepts "gemini-*" model names. Falling back to ${DEFAULT_MODEL}.`
10401
- );
10402
- return DEFAULT_MODEL;
10394
+ return config.model || DEFAULT_MODEL;
10403
10395
  }
10404
10396
  function createClient2(config) {
10405
10397
  if (isVertexConfig(config)) {
@@ -10413,6 +10405,9 @@ function createClient2(config) {
10413
10405
  return new GoogleGenAI({ apiKey: config.apiKey });
10414
10406
  }
10415
10407
  function validateConfig(config) {
10408
+ if ("vertexProject" in config && config.vertexProject !== void 0 && config.vertexProject.trim().length === 0) {
10409
+ return { ok: false, provider: "gemini", message: "missing Vertex AI project ID" };
10410
+ }
10416
10411
  if (isVertexConfig(config)) {
10417
10412
  const model2 = resolveModel(config);
10418
10413
  return {
@@ -10426,11 +10421,10 @@ function validateConfig(config) {
10426
10421
  return { ok: false, provider: "gemini", message: "missing api key" };
10427
10422
  }
10428
10423
  const model = resolveModel(config);
10429
- const warning = config.model && !VALIDATION_PATTERN.test(config.model) ? ` (invalid model "${config.model}" replaced with default)` : "";
10430
10424
  return {
10431
10425
  ok: true,
10432
10426
  provider: "gemini",
10433
- message: `config valid${warning}`,
10427
+ message: "config valid",
10434
10428
  model
10435
10429
  };
10436
10430
  }
@@ -10623,12 +10617,14 @@ var geminiAdapter = {
10623
10617
  displayName: "Gemini",
10624
10618
  mode: "api",
10625
10619
  keyUrl: "https://aistudio.google.com/apikey",
10620
+ // Upstream model list: https://ai.google.dev/gemini-api/docs/models
10626
10621
  modelRegistry: {
10627
10622
  defaultModel: "gemini-3-flash",
10628
- validationPattern: /^gemini-/,
10629
- validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
10623
+ validationPattern: /./,
10624
+ validationHint: "any valid Google model name (e.g. gemini-3-flash, learnlm-1.5-pro-experimental)",
10630
10625
  knownModels: [
10631
10626
  { id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
10627
+ { id: "gemini-3-flash", displayName: "Gemini 3 Flash", tier: "standard" },
10632
10628
  { id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
10633
10629
  { id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
10634
10630
  { id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
@@ -10791,13 +10787,15 @@ function extractResponseText(response) {
10791
10787
  }
10792
10788
  function extractGroundingSources(response) {
10793
10789
  const sources = [];
10790
+ const seen = /* @__PURE__ */ new Set();
10794
10791
  try {
10795
10792
  for (const item of response.output) {
10796
10793
  if (item.type === "message") {
10797
10794
  for (const content of item.content) {
10798
10795
  if (content.type === "output_text" && content.annotations) {
10799
10796
  for (const annotation of content.annotations) {
10800
- if (annotation.type === "url_citation") {
10797
+ if (annotation.type === "url_citation" && !seen.has(annotation.url)) {
10798
+ seen.add(annotation.url);
10801
10799
  sources.push({
10802
10800
  uri: annotation.url,
10803
10801
  title: annotation.title ?? ""
@@ -10817,7 +10815,10 @@ function extractSearchQueries2(response) {
10817
10815
  try {
10818
10816
  for (const item of response.output) {
10819
10817
  if (item.type === "web_search_call" && "query" in item) {
10820
- queries.push(item.query);
10818
+ const query = item.query;
10819
+ if (typeof query === "string" && query.length > 0) {
10820
+ queries.push(query);
10821
+ }
10821
10822
  }
10822
10823
  }
10823
10824
  } catch {
@@ -10889,10 +10890,11 @@ var openaiAdapter = {
10889
10890
  displayName: "OpenAI",
10890
10891
  mode: "api",
10891
10892
  keyUrl: "https://platform.openai.com/api-keys",
10893
+ // Upstream model list: https://platform.openai.com/docs/models
10892
10894
  modelRegistry: {
10893
10895
  defaultModel: "gpt-5.4",
10894
- validationPattern: /^(gpt-|o\d)/,
10895
- validationHint: "expected a GPT or o-series model name (e.g. gpt-5.4, o3)",
10896
+ validationPattern: /./,
10897
+ validationHint: "any valid OpenAI model name (e.g. gpt-5.4, o3, chatgpt-4o-latest)",
10896
10898
  knownModels: [
10897
10899
  { id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
10898
10900
  { id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
@@ -10961,24 +10963,37 @@ var openaiAdapter = {
10961
10963
  // ../provider-claude/src/normalize.ts
10962
10964
  import Anthropic from "@anthropic-ai/sdk";
10963
10965
  var DEFAULT_MODEL3 = "claude-sonnet-4-6";
10966
+ var VALIDATION_PATTERN = /^claude-/;
10967
+ function resolveModel2(config) {
10968
+ const m = config.model;
10969
+ if (!m) return DEFAULT_MODEL3;
10970
+ if (VALIDATION_PATTERN.test(m)) return m;
10971
+ console.warn(
10972
+ `[provider-claude] Invalid model name "${m}" \u2014 this provider uses the Anthropic API which only accepts "claude-*" model names. Falling back to ${DEFAULT_MODEL3}.`
10973
+ );
10974
+ return DEFAULT_MODEL3;
10975
+ }
10964
10976
  function validateConfig3(config) {
10965
10977
  if (!config.apiKey || config.apiKey.length === 0) {
10966
10978
  return { ok: false, provider: "claude", message: "missing api key" };
10967
10979
  }
10980
+ const model = resolveModel2(config);
10981
+ const warning = config.model && !VALIDATION_PATTERN.test(config.model) ? ` (invalid model "${config.model}" replaced with default)` : "";
10968
10982
  return {
10969
10983
  ok: true,
10970
10984
  provider: "claude",
10971
- message: "config valid",
10972
- model: config.model ?? DEFAULT_MODEL3
10985
+ message: `config valid${warning}`,
10986
+ model
10973
10987
  };
10974
10988
  }
10975
10989
  async function healthcheck3(config) {
10976
10990
  const validation = validateConfig3(config);
10977
10991
  if (!validation.ok) return validation;
10978
10992
  try {
10993
+ const model = resolveModel2(config);
10979
10994
  const client = new Anthropic({ apiKey: config.apiKey });
10980
10995
  const response = await client.messages.create({
10981
- model: config.model ?? DEFAULT_MODEL3,
10996
+ model,
10982
10997
  max_tokens: 32,
10983
10998
  messages: [{ role: "user", content: 'Say "ok"' }]
10984
10999
  });
@@ -10987,19 +11002,19 @@ async function healthcheck3(config) {
10987
11002
  ok: text2.length > 0,
10988
11003
  provider: "claude",
10989
11004
  message: text2.length > 0 ? "claude api key verified" : "empty response from claude",
10990
- model: config.model ?? DEFAULT_MODEL3
11005
+ model
10991
11006
  };
10992
11007
  } catch (err) {
10993
11008
  return {
10994
11009
  ok: false,
10995
11010
  provider: "claude",
10996
11011
  message: err instanceof Error ? err.message : String(err),
10997
- model: config.model ?? DEFAULT_MODEL3
11012
+ model: resolveModel2(config)
10998
11013
  };
10999
11014
  }
11000
11015
  }
11001
11016
  async function executeTrackedQuery3(input) {
11002
- const model = input.config.model ?? DEFAULT_MODEL3;
11017
+ const model = resolveModel2(input.config);
11003
11018
  const client = new Anthropic({ apiKey: input.config.apiKey });
11004
11019
  const webSearchTool = {
11005
11020
  type: "web_search_20250305",
@@ -11121,7 +11136,7 @@ function extractDomainFromUri3(uri) {
11121
11136
  }
11122
11137
  }
11123
11138
  async function generateText3(prompt, config) {
11124
- const model = config.model ?? DEFAULT_MODEL3;
11139
+ const model = resolveModel2(config);
11125
11140
  const client = new Anthropic({ apiKey: config.apiKey });
11126
11141
  const response = await client.messages.create({
11127
11142
  model,
@@ -11151,6 +11166,7 @@ var claudeAdapter = {
11151
11166
  displayName: "Claude",
11152
11167
  mode: "api",
11153
11168
  keyUrl: "https://platform.claude.com/settings/keys",
11169
+ // Upstream model list: https://platform.claude.com/docs/en/about-claude/models/overview
11154
11170
  modelRegistry: {
11155
11171
  defaultModel: "claude-sonnet-4-6",
11156
11172
  validationPattern: /^claude-/,
@@ -11860,7 +11876,7 @@ function normalizeResult5(raw) {
11860
11876
  }
11861
11877
 
11862
11878
  // ../provider-cdp/src/adapter.ts
11863
- var sharedConnection = null;
11879
+ var connectionPool = /* @__PURE__ */ new Map();
11864
11880
  function getConnection(config) {
11865
11881
  if (!config.cdpEndpoint) {
11866
11882
  throw new CDPProviderError("CDP_CONNECTION_REFUSED", "CDP endpoint not configured");
@@ -11871,14 +11887,13 @@ function getConnection(config) {
11871
11887
  const parts = endpoint.split(":");
11872
11888
  if (parts.length >= 1 && parts[0]) host = parts[0];
11873
11889
  if (parts.length >= 2 && parts[1]) port = parseInt(parts[1], 10) || 9222;
11874
- if (!sharedConnection || sharedConnection.endpoint !== `${host}:${port}`) {
11875
- if (sharedConnection) {
11876
- sharedConnection.disconnect().catch(() => {
11877
- });
11878
- }
11879
- sharedConnection = new CDPConnectionManager(host, port);
11890
+ const key = `${host}:${port}`;
11891
+ let conn = connectionPool.get(key);
11892
+ if (!conn) {
11893
+ conn = new CDPConnectionManager(host, port);
11894
+ connectionPool.set(key, conn);
11880
11895
  }
11881
- return sharedConnection;
11896
+ return conn;
11882
11897
  }
11883
11898
  function getScreenshotDir2() {
11884
11899
  return path4.join(os3.homedir(), ".canonry", "screenshots");
@@ -12124,6 +12139,7 @@ var perplexityAdapter = {
12124
12139
  displayName: "Perplexity",
12125
12140
  mode: "api",
12126
12141
  keyUrl: "https://www.perplexity.ai/settings/api",
12142
+ // Upstream model list: https://docs.perplexity.ai/guides/model-cards
12127
12143
  modelRegistry: {
12128
12144
  defaultModel: "sonar",
12129
12145
  validationPattern: /^sonar/,
@@ -12352,7 +12368,7 @@ import crypto18 from "crypto";
12352
12368
  import fs4 from "fs";
12353
12369
  import path5 from "path";
12354
12370
  import os4 from "os";
12355
- import { and as and6, eq as eq17, inArray as inArray3 } from "drizzle-orm";
12371
+ import { and as and6, eq as eq17, inArray as inArray3, sql as sql5 } from "drizzle-orm";
12356
12372
 
12357
12373
  // src/logger.ts
12358
12374
  var IS_TTY = process.stdout.isTTY === true;
@@ -12514,12 +12530,12 @@ var JobRunner = class {
12514
12530
  } else if (locationOverride) {
12515
12531
  runLocation = locationOverride;
12516
12532
  } else {
12517
- const projectLocations = JSON.parse(project.locations || "[]");
12533
+ const projectLocations = parseJsonColumn(project.locations, []);
12518
12534
  if (project.defaultLocation && projectLocations.length > 0) {
12519
12535
  runLocation = projectLocations.find((l) => l.label === project.defaultLocation);
12520
12536
  }
12521
12537
  }
12522
- const projectProviders = providerOverride ?? JSON.parse(project.providers || "[]");
12538
+ const projectProviders = providerOverride ?? parseJsonColumn(project.providers, []);
12523
12539
  activeProviders = this.registry.getForProject(projectProviders);
12524
12540
  if (activeProviders.length === 0) {
12525
12541
  throw new Error("No providers configured. Add at least one provider API key.");
@@ -12530,7 +12546,7 @@ var JobRunner = class {
12530
12546
  const competitorDomains = projectCompetitors.map((c) => c.domain);
12531
12547
  const allDomains = effectiveDomains({
12532
12548
  canonicalDomain: project.canonicalDomain,
12533
- ownedDomains: JSON.parse(project.ownedDomains || "[]")
12549
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
12534
12550
  });
12535
12551
  const executionContext = {
12536
12552
  providerCount: activeProviders.length,
@@ -12741,22 +12757,19 @@ var JobRunner = class {
12741
12757
  }
12742
12758
  }
12743
12759
  incrementUsage(scope, metric, count) {
12744
- const now = /* @__PURE__ */ new Date();
12745
- const period = now.toISOString().slice(0, 10);
12746
- const id = crypto18.randomUUID();
12747
- const existing = this.db.select().from(usageCounters).where(eq17(usageCounters.scope, scope)).all().find((r) => r.period === period && r.metric === metric);
12748
- if (existing) {
12749
- this.db.update(usageCounters).set({ count: existing.count + count, updatedAt: now.toISOString() }).where(eq17(usageCounters.id, existing.id)).run();
12750
- } else {
12751
- this.db.insert(usageCounters).values({
12752
- id,
12753
- scope,
12754
- period,
12755
- metric,
12756
- count,
12757
- updatedAt: now.toISOString()
12758
- }).run();
12759
- }
12760
+ const now = (/* @__PURE__ */ new Date()).toISOString();
12761
+ const period = now.slice(0, 10);
12762
+ this.db.insert(usageCounters).values({
12763
+ id: crypto18.randomUUID(),
12764
+ scope,
12765
+ period,
12766
+ metric,
12767
+ count,
12768
+ updatedAt: now
12769
+ }).onConflictDoUpdate({
12770
+ target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
12771
+ set: { count: sql5`${usageCounters.count} + ${count}`, updatedAt: now }
12772
+ }).run();
12760
12773
  }
12761
12774
  flushProviderUsage(projectId, providerDispatchCounts) {
12762
12775
  for (const [providerName, count] of providerDispatchCounts.entries()) {
@@ -12957,7 +12970,7 @@ function matchesBrandKey(candidateKey, brandKeys) {
12957
12970
 
12958
12971
  // src/gsc-sync.ts
12959
12972
  import crypto19 from "crypto";
12960
- import { eq as eq18, and as and7, sql as sql5 } from "drizzle-orm";
12973
+ import { eq as eq18, and as and7, sql as sql6 } from "drizzle-orm";
12961
12974
  var log2 = createLogger("GscSync");
12962
12975
  function formatDate2(d) {
12963
12976
  return d.toISOString().split("T")[0];
@@ -13011,8 +13024,8 @@ async function executeGscSync(db, runId, projectId, opts) {
13011
13024
  db.delete(gscSearchData).where(
13012
13025
  and7(
13013
13026
  eq18(gscSearchData.projectId, projectId),
13014
- sql5`${gscSearchData.date} >= ${startDate}`,
13015
- sql5`${gscSearchData.date} <= ${endDate}`
13027
+ sql6`${gscSearchData.date} >= ${startDate}`,
13028
+ sql6`${gscSearchData.date} <= ${endDate}`
13016
13029
  )
13017
13030
  ).run();
13018
13031
  const batchSize = 500;
@@ -13471,7 +13484,7 @@ var Scheduler = class {
13471
13484
  nextRunAt,
13472
13485
  updatedAt: now
13473
13486
  }).where(eq20(schedules.id, currentSchedule.id)).run();
13474
- const scheduleProviders = JSON.parse(currentSchedule.providers);
13487
+ const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
13475
13488
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13476
13489
  log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
13477
13490
  this.callbacks.onRunCreated(runId, projectId, providers);
package/dist/cli.js CHANGED
@@ -26,7 +26,7 @@ import {
26
26
  setGoogleAuthConfig,
27
27
  showFirstRunNotice,
28
28
  trackEvent
29
- } from "./chunk-5MOWJJND.js";
29
+ } from "./chunk-U2ZNKIWK.js";
30
30
 
31
31
  // src/cli.ts
32
32
  import { pathToFileURL } from "url";
@@ -1698,7 +1698,11 @@ async function addCompetitors(project, domains, format) {
1698
1698
  }, null, 2));
1699
1699
  return;
1700
1700
  }
1701
- console.log(`Added ${domains.length} competitor(s) to "${project}".`);
1701
+ if (addedDomains.length === 0) {
1702
+ console.log(`No new competitors added to "${project}" (all already tracked).`);
1703
+ } else {
1704
+ console.log(`Added ${addedDomains.length} competitor(s) to "${project}".`);
1705
+ }
1702
1706
  }
1703
1707
  async function listCompetitors(project, format) {
1704
1708
  const client = getClient4();
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-5MOWJJND.js";
4
+ } from "./chunk-U2ZNKIWK.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.37.0",
3
+ "version": "1.37.1",
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,17 +55,17 @@
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",
59
58
  "@ainyc/canonry-contracts": "0.0.0",
59
+ "@ainyc/canonry-integration-google": "0.0.0",
60
60
  "@ainyc/canonry-db": "0.0.0",
61
+ "@ainyc/canonry-config": "0.0.0",
61
62
  "@ainyc/canonry-integration-bing": "0.0.0",
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
+ "@ainyc/canonry-provider-local": "0.0.0",
66
+ "@ainyc/canonry-provider-openai": "0.0.0",
65
67
  "@ainyc/canonry-provider-claude": "0.0.0",
66
68
  "@ainyc/canonry-provider-gemini": "0.0.0",
67
- "@ainyc/canonry-provider-openai": "0.0.0",
68
- "@ainyc/canonry-provider-local": "0.0.0",
69
69
  "@ainyc/canonry-provider-perplexity": "0.0.0"
70
70
  },
71
71
  "scripts": {