@ainyc/canonry 1.37.0 → 1.38.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.
@@ -1878,6 +1878,12 @@ var MIGRATIONS = [
1878
1878
  `DROP INDEX IF EXISTS idx_ga_ai_ref_unique`,
1879
1879
  `CREATE UNIQUE INDEX IF NOT EXISTS idx_ga_ai_ref_unique_v2 ON ga_ai_referrals(project_id, date, source, medium, source_dimension)`
1880
1880
  ];
1881
+ function isDuplicateColumnError(err) {
1882
+ if (!(err instanceof Error)) return false;
1883
+ if (err.message.includes("duplicate column name")) return true;
1884
+ if (err.cause instanceof Error && err.cause.message.includes("duplicate column name")) return true;
1885
+ return false;
1886
+ }
1881
1887
  function migrate(db) {
1882
1888
  const statements = MIGRATION_SQL.split(";").map((s) => s.trim()).filter((s) => s.length > 0);
1883
1889
  for (const statement of statements) {
@@ -1886,7 +1892,9 @@ function migrate(db) {
1886
1892
  for (const migration of MIGRATIONS) {
1887
1893
  try {
1888
1894
  db.run(sql.raw(migration));
1889
- } catch {
1895
+ } catch (err) {
1896
+ if (isDuplicateColumnError(err)) continue;
1897
+ throw err;
1890
1898
  }
1891
1899
  }
1892
1900
  }
@@ -10386,20 +10394,33 @@ async function apiRoutes(app, opts) {
10386
10394
 
10387
10395
  // ../provider-gemini/src/normalize.ts
10388
10396
  import { GoogleGenAI } from "@google/genai";
10397
+
10398
+ // ../provider-gemini/src/utils.ts
10399
+ async function withRetry(fn, options = {}) {
10400
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
10401
+ let lastError;
10402
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
10403
+ try {
10404
+ return await fn();
10405
+ } catch (err) {
10406
+ lastError = err;
10407
+ if (attempt < maxRetries) {
10408
+ const delay = initialDelay * Math.pow(2, attempt);
10409
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
10410
+ await new Promise((resolve) => setTimeout(resolve, delay));
10411
+ }
10412
+ }
10413
+ }
10414
+ throw lastError;
10415
+ }
10416
+
10417
+ // ../provider-gemini/src/normalize.ts
10389
10418
  var DEFAULT_MODEL = "gemini-3-flash";
10390
- var VALIDATION_PATTERN = /^gemini-/;
10391
10419
  function isVertexConfig(config) {
10392
10420
  return !!config.vertexProject;
10393
10421
  }
10394
10422
  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;
10423
+ return config.model || DEFAULT_MODEL;
10403
10424
  }
10404
10425
  function createClient2(config) {
10405
10426
  if (isVertexConfig(config)) {
@@ -10413,6 +10434,9 @@ function createClient2(config) {
10413
10434
  return new GoogleGenAI({ apiKey: config.apiKey });
10414
10435
  }
10415
10436
  function validateConfig(config) {
10437
+ if ("vertexProject" in config && config.vertexProject !== void 0 && config.vertexProject.trim().length === 0) {
10438
+ return { ok: false, provider: "gemini", message: "missing Vertex AI project ID" };
10439
+ }
10416
10440
  if (isVertexConfig(config)) {
10417
10441
  const model2 = resolveModel(config);
10418
10442
  return {
@@ -10426,11 +10450,10 @@ function validateConfig(config) {
10426
10450
  return { ok: false, provider: "gemini", message: "missing api key" };
10427
10451
  }
10428
10452
  const model = resolveModel(config);
10429
- const warning = config.model && !VALIDATION_PATTERN.test(config.model) ? ` (invalid model "${config.model}" replaced with default)` : "";
10430
10453
  return {
10431
10454
  ok: true,
10432
10455
  provider: "gemini",
10433
- message: `config valid${warning}`,
10456
+ message: "config valid",
10434
10457
  model
10435
10458
  };
10436
10459
  }
@@ -10440,10 +10463,12 @@ async function healthcheck(config) {
10440
10463
  try {
10441
10464
  const model = resolveModel(config);
10442
10465
  const client = createClient2(config);
10443
- const result = await client.models.generateContent({
10444
- model,
10445
- contents: 'Say "ok"'
10446
- });
10466
+ const result = await withRetry(
10467
+ () => client.models.generateContent({
10468
+ model,
10469
+ contents: 'Say "ok"'
10470
+ })
10471
+ );
10447
10472
  const text2 = result.text ?? "";
10448
10473
  const backend = isVertexConfig(config) ? "vertex ai" : "api key";
10449
10474
  return {
@@ -10465,22 +10490,29 @@ async function executeTrackedQuery(input) {
10465
10490
  const model = resolveModel(input.config);
10466
10491
  const prompt = buildPrompt(input.keyword, input.location);
10467
10492
  const client = createClient2(input.config);
10468
- const result = await client.models.generateContent({
10469
- model,
10470
- contents: prompt,
10471
- config: {
10472
- tools: [{ googleSearch: {} }]
10473
- }
10474
- });
10475
- const groundingSources = extractGroundingMetadata(result);
10476
- const searchQueries = extractSearchQueries(result);
10477
- return {
10478
- provider: "gemini",
10479
- rawResponse: responseToRecord(result),
10480
- model,
10481
- groundingSources,
10482
- searchQueries
10483
- };
10493
+ try {
10494
+ const result = await withRetry(
10495
+ () => client.models.generateContent({
10496
+ model,
10497
+ contents: prompt,
10498
+ config: {
10499
+ tools: [{ googleSearch: {} }]
10500
+ }
10501
+ })
10502
+ );
10503
+ const groundingSources = extractGroundingMetadata(result);
10504
+ const searchQueries = extractSearchQueries(result);
10505
+ return {
10506
+ provider: "gemini",
10507
+ rawResponse: responseToRecord(result),
10508
+ model,
10509
+ groundingSources,
10510
+ searchQueries
10511
+ };
10512
+ } catch (err) {
10513
+ const msg = err instanceof Error ? err.message : String(err);
10514
+ throw new Error(`[provider-gemini] ${msg}`);
10515
+ }
10484
10516
  }
10485
10517
  function normalizeResult(raw) {
10486
10518
  const answerText = extractAnswerText(raw.rawResponse);
@@ -10582,10 +10614,12 @@ function extractDomainFromUri(uri) {
10582
10614
  async function generateText(prompt, config) {
10583
10615
  const model = resolveModel(config);
10584
10616
  const client = createClient2(config);
10585
- const result = await client.models.generateContent({
10586
- model,
10587
- contents: prompt
10588
- });
10617
+ const result = await withRetry(
10618
+ () => client.models.generateContent({
10619
+ model,
10620
+ contents: prompt
10621
+ })
10622
+ );
10589
10623
  return result.text ?? "";
10590
10624
  }
10591
10625
  function responseToRecord(response) {
@@ -10623,12 +10657,14 @@ var geminiAdapter = {
10623
10657
  displayName: "Gemini",
10624
10658
  mode: "api",
10625
10659
  keyUrl: "https://aistudio.google.com/apikey",
10660
+ // Upstream model list: https://ai.google.dev/gemini-api/docs/models
10626
10661
  modelRegistry: {
10627
10662
  defaultModel: "gemini-3-flash",
10628
- validationPattern: /^gemini-/,
10629
- validationHint: 'model name must start with "gemini-" (e.g. gemini-3-flash)',
10663
+ validationPattern: /./,
10664
+ validationHint: "any valid Google model name (e.g. gemini-3-flash, learnlm-1.5-pro-experimental)",
10630
10665
  knownModels: [
10631
10666
  { id: "gemini-3.1-pro-preview", displayName: "Gemini 3.1 Pro (Preview)", tier: "flagship" },
10667
+ { id: "gemini-3-flash", displayName: "Gemini 3 Flash", tier: "standard" },
10632
10668
  { id: "gemini-3-flash-preview", displayName: "Gemini 3 Flash (Preview)", tier: "standard" },
10633
10669
  { id: "gemini-3.1-flash-lite-preview", displayName: "Gemini 3.1 Flash-Lite (Preview)", tier: "economy" },
10634
10670
  { id: "gemini-2.5-flash", displayName: "Gemini 2.5 Flash", tier: "standard" }
@@ -10692,6 +10728,27 @@ var geminiAdapter = {
10692
10728
 
10693
10729
  // ../provider-openai/src/normalize.ts
10694
10730
  import OpenAI from "openai";
10731
+
10732
+ // ../provider-openai/src/utils.ts
10733
+ async function withRetry2(fn, options = {}) {
10734
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
10735
+ let lastError;
10736
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
10737
+ try {
10738
+ return await fn();
10739
+ } catch (err) {
10740
+ lastError = err;
10741
+ if (attempt < maxRetries) {
10742
+ const delay = initialDelay * Math.pow(2, attempt);
10743
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
10744
+ await new Promise((resolve) => setTimeout(resolve, delay));
10745
+ }
10746
+ }
10747
+ }
10748
+ throw lastError;
10749
+ }
10750
+
10751
+ // ../provider-openai/src/normalize.ts
10695
10752
  var DEFAULT_MODEL2 = "gpt-5.4";
10696
10753
  function validateConfig2(config) {
10697
10754
  if (!config.apiKey || config.apiKey.length === 0) {
@@ -10709,10 +10766,12 @@ async function healthcheck2(config) {
10709
10766
  if (!validation.ok) return validation;
10710
10767
  try {
10711
10768
  const client = new OpenAI({ apiKey: config.apiKey });
10712
- const response = await client.responses.create({
10713
- model: config.model ?? DEFAULT_MODEL2,
10714
- input: 'Say "ok"'
10715
- });
10769
+ const response = await withRetry2(
10770
+ () => client.responses.create({
10771
+ model: config.model ?? DEFAULT_MODEL2,
10772
+ input: 'Say "ok"'
10773
+ })
10774
+ );
10716
10775
  const text2 = extractResponseText(response);
10717
10776
  return {
10718
10777
  ok: text2.length > 0,
@@ -10742,21 +10801,28 @@ async function executeTrackedQuery2(input) {
10742
10801
  ...input.location.timezone ? { timezone: input.location.timezone } : {}
10743
10802
  };
10744
10803
  }
10745
- const response = await client.responses.create({
10746
- model,
10747
- tools: [webSearchTool],
10748
- tool_choice: "required",
10749
- input: buildPrompt2(input.keyword)
10750
- });
10751
- const groundingSources = extractGroundingSources(response);
10752
- const searchQueries = extractSearchQueries2(response);
10753
- return {
10754
- provider: "openai",
10755
- rawResponse: responseToRecord2(response),
10756
- model,
10757
- groundingSources,
10758
- searchQueries
10759
- };
10804
+ try {
10805
+ const response = await withRetry2(
10806
+ () => client.responses.create({
10807
+ model,
10808
+ tools: [webSearchTool],
10809
+ tool_choice: "required",
10810
+ input: buildPrompt2(input.keyword)
10811
+ })
10812
+ );
10813
+ const groundingSources = extractGroundingSources(response);
10814
+ const searchQueries = extractSearchQueries2(response);
10815
+ return {
10816
+ provider: "openai",
10817
+ rawResponse: responseToRecord2(response),
10818
+ model,
10819
+ groundingSources,
10820
+ searchQueries
10821
+ };
10822
+ } catch (err) {
10823
+ const msg = err instanceof Error ? err.message : String(err);
10824
+ throw new Error(`[provider-openai] ${msg}`);
10825
+ }
10760
10826
  }
10761
10827
  function normalizeResult2(raw) {
10762
10828
  const answerText = extractAnswerTextFromRaw(raw.rawResponse);
@@ -10791,13 +10857,15 @@ function extractResponseText(response) {
10791
10857
  }
10792
10858
  function extractGroundingSources(response) {
10793
10859
  const sources = [];
10860
+ const seen = /* @__PURE__ */ new Set();
10794
10861
  try {
10795
10862
  for (const item of response.output) {
10796
10863
  if (item.type === "message") {
10797
10864
  for (const content of item.content) {
10798
10865
  if (content.type === "output_text" && content.annotations) {
10799
10866
  for (const annotation of content.annotations) {
10800
- if (annotation.type === "url_citation") {
10867
+ if (annotation.type === "url_citation" && !seen.has(annotation.url)) {
10868
+ seen.add(annotation.url);
10801
10869
  sources.push({
10802
10870
  uri: annotation.url,
10803
10871
  title: annotation.title ?? ""
@@ -10817,7 +10885,10 @@ function extractSearchQueries2(response) {
10817
10885
  try {
10818
10886
  for (const item of response.output) {
10819
10887
  if (item.type === "web_search_call" && "query" in item) {
10820
- queries.push(item.query);
10888
+ const query = item.query;
10889
+ if (typeof query === "string" && query.length > 0) {
10890
+ queries.push(query);
10891
+ }
10821
10892
  }
10822
10893
  }
10823
10894
  } catch {
@@ -10862,10 +10933,12 @@ function extractDomainFromUri2(uri) {
10862
10933
  async function generateText2(prompt, config) {
10863
10934
  const model = config.model ?? DEFAULT_MODEL2;
10864
10935
  const client = new OpenAI({ apiKey: config.apiKey });
10865
- const response = await client.responses.create({
10866
- model,
10867
- input: prompt
10868
- });
10936
+ const response = await withRetry2(
10937
+ () => client.responses.create({
10938
+ model,
10939
+ input: prompt
10940
+ })
10941
+ );
10869
10942
  return extractResponseText(response);
10870
10943
  }
10871
10944
  function responseToRecord2(response) {
@@ -10889,10 +10962,11 @@ var openaiAdapter = {
10889
10962
  displayName: "OpenAI",
10890
10963
  mode: "api",
10891
10964
  keyUrl: "https://platform.openai.com/api-keys",
10965
+ // Upstream model list: https://platform.openai.com/docs/models
10892
10966
  modelRegistry: {
10893
10967
  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)",
10968
+ validationPattern: /./,
10969
+ validationHint: "any valid OpenAI model name (e.g. gpt-5.4, o3, chatgpt-4o-latest)",
10896
10970
  knownModels: [
10897
10971
  { id: "gpt-5.4", displayName: "GPT-5.4", tier: "flagship" },
10898
10972
  { id: "gpt-5.4-pro", displayName: "GPT-5.4 Pro", tier: "flagship" },
@@ -10960,46 +11034,82 @@ var openaiAdapter = {
10960
11034
 
10961
11035
  // ../provider-claude/src/normalize.ts
10962
11036
  import Anthropic from "@anthropic-ai/sdk";
11037
+
11038
+ // ../provider-claude/src/utils.ts
11039
+ async function withRetry3(fn, options = {}) {
11040
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
11041
+ let lastError;
11042
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
11043
+ try {
11044
+ return await fn();
11045
+ } catch (err) {
11046
+ lastError = err;
11047
+ if (attempt < maxRetries) {
11048
+ const delay = initialDelay * Math.pow(2, attempt);
11049
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
11050
+ await new Promise((resolve) => setTimeout(resolve, delay));
11051
+ }
11052
+ }
11053
+ }
11054
+ throw lastError;
11055
+ }
11056
+
11057
+ // ../provider-claude/src/normalize.ts
10963
11058
  var DEFAULT_MODEL3 = "claude-sonnet-4-6";
11059
+ var VALIDATION_PATTERN = /^claude-/;
11060
+ function resolveModel2(config) {
11061
+ const m = config.model;
11062
+ if (!m) return DEFAULT_MODEL3;
11063
+ if (VALIDATION_PATTERN.test(m)) return m;
11064
+ console.warn(
11065
+ `[provider-claude] Invalid model name "${m}" \u2014 this provider uses the Anthropic API which only accepts "claude-*" model names. Falling back to ${DEFAULT_MODEL3}.`
11066
+ );
11067
+ return DEFAULT_MODEL3;
11068
+ }
10964
11069
  function validateConfig3(config) {
10965
11070
  if (!config.apiKey || config.apiKey.length === 0) {
10966
11071
  return { ok: false, provider: "claude", message: "missing api key" };
10967
11072
  }
11073
+ const model = resolveModel2(config);
11074
+ const warning = config.model && !VALIDATION_PATTERN.test(config.model) ? ` (invalid model "${config.model}" replaced with default)` : "";
10968
11075
  return {
10969
11076
  ok: true,
10970
11077
  provider: "claude",
10971
- message: "config valid",
10972
- model: config.model ?? DEFAULT_MODEL3
11078
+ message: `config valid${warning}`,
11079
+ model
10973
11080
  };
10974
11081
  }
10975
11082
  async function healthcheck3(config) {
10976
11083
  const validation = validateConfig3(config);
10977
11084
  if (!validation.ok) return validation;
10978
11085
  try {
11086
+ const model = resolveModel2(config);
10979
11087
  const client = new Anthropic({ apiKey: config.apiKey });
10980
- const response = await client.messages.create({
10981
- model: config.model ?? DEFAULT_MODEL3,
10982
- max_tokens: 32,
10983
- messages: [{ role: "user", content: 'Say "ok"' }]
10984
- });
11088
+ const response = await withRetry3(
11089
+ () => client.messages.create({
11090
+ model,
11091
+ max_tokens: 32,
11092
+ messages: [{ role: "user", content: 'Say "ok"' }]
11093
+ })
11094
+ );
10985
11095
  const text2 = extractTextFromResponse(response);
10986
11096
  return {
10987
11097
  ok: text2.length > 0,
10988
11098
  provider: "claude",
10989
11099
  message: text2.length > 0 ? "claude api key verified" : "empty response from claude",
10990
- model: config.model ?? DEFAULT_MODEL3
11100
+ model
10991
11101
  };
10992
11102
  } catch (err) {
10993
11103
  return {
10994
11104
  ok: false,
10995
11105
  provider: "claude",
10996
11106
  message: err instanceof Error ? err.message : String(err),
10997
- model: config.model ?? DEFAULT_MODEL3
11107
+ model: resolveModel2(config)
10998
11108
  };
10999
11109
  }
11000
11110
  }
11001
11111
  async function executeTrackedQuery3(input) {
11002
- const model = input.config.model ?? DEFAULT_MODEL3;
11112
+ const model = resolveModel2(input.config);
11003
11113
  const client = new Anthropic({ apiKey: input.config.apiKey });
11004
11114
  const webSearchTool = {
11005
11115
  type: "web_search_20250305",
@@ -11015,21 +11125,28 @@ async function executeTrackedQuery3(input) {
11015
11125
  ...input.location.timezone ? { timezone: input.location.timezone } : {}
11016
11126
  };
11017
11127
  }
11018
- const response = await client.messages.create({
11019
- model,
11020
- max_tokens: 4096,
11021
- tools: [webSearchTool],
11022
- messages: [{ role: "user", content: input.keyword }]
11023
- });
11024
- const groundingSources = extractGroundingSources2(response);
11025
- const searchQueries = extractSearchQueries3(response);
11026
- return {
11027
- provider: "claude",
11028
- rawResponse: responseToRecord3(response),
11029
- model,
11030
- groundingSources,
11031
- searchQueries
11032
- };
11128
+ try {
11129
+ const response = await withRetry3(
11130
+ () => client.messages.create({
11131
+ model,
11132
+ max_tokens: 4096,
11133
+ tools: [webSearchTool],
11134
+ messages: [{ role: "user", content: input.keyword }]
11135
+ })
11136
+ );
11137
+ const groundingSources = extractGroundingSources2(response);
11138
+ const searchQueries = extractSearchQueries3(response);
11139
+ return {
11140
+ provider: "claude",
11141
+ rawResponse: responseToRecord3(response),
11142
+ model,
11143
+ groundingSources,
11144
+ searchQueries
11145
+ };
11146
+ } catch (err) {
11147
+ const msg = err instanceof Error ? err.message : String(err);
11148
+ throw new Error(`[provider-claude] ${msg}`);
11149
+ }
11033
11150
  }
11034
11151
  function normalizeResult3(raw) {
11035
11152
  const answerText = extractAnswerTextFromRaw2(raw.rawResponse);
@@ -11121,13 +11238,15 @@ function extractDomainFromUri3(uri) {
11121
11238
  }
11122
11239
  }
11123
11240
  async function generateText3(prompt, config) {
11124
- const model = config.model ?? DEFAULT_MODEL3;
11241
+ const model = resolveModel2(config);
11125
11242
  const client = new Anthropic({ apiKey: config.apiKey });
11126
- const response = await client.messages.create({
11127
- model,
11128
- max_tokens: 2048,
11129
- messages: [{ role: "user", content: prompt }]
11130
- });
11243
+ const response = await withRetry3(
11244
+ () => client.messages.create({
11245
+ model,
11246
+ max_tokens: 2048,
11247
+ messages: [{ role: "user", content: prompt }]
11248
+ })
11249
+ );
11131
11250
  return extractTextFromResponse(response);
11132
11251
  }
11133
11252
  function responseToRecord3(response) {
@@ -11151,6 +11270,7 @@ var claudeAdapter = {
11151
11270
  displayName: "Claude",
11152
11271
  mode: "api",
11153
11272
  keyUrl: "https://platform.claude.com/settings/keys",
11273
+ // Upstream model list: https://platform.claude.com/docs/en/about-claude/models/overview
11154
11274
  modelRegistry: {
11155
11275
  defaultModel: "claude-sonnet-4-6",
11156
11276
  validationPattern: /^claude-/,
@@ -11219,6 +11339,27 @@ var claudeAdapter = {
11219
11339
 
11220
11340
  // ../provider-local/src/normalize.ts
11221
11341
  import OpenAI2 from "openai";
11342
+
11343
+ // ../provider-local/src/utils.ts
11344
+ async function withRetry4(fn, options = {}) {
11345
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
11346
+ let lastError;
11347
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
11348
+ try {
11349
+ return await fn();
11350
+ } catch (err) {
11351
+ lastError = err;
11352
+ if (attempt < maxRetries) {
11353
+ const delay = initialDelay * Math.pow(2, attempt);
11354
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
11355
+ await new Promise((resolve) => setTimeout(resolve, delay));
11356
+ }
11357
+ }
11358
+ }
11359
+ throw lastError;
11360
+ }
11361
+
11362
+ // ../provider-local/src/normalize.ts
11222
11363
  var DEFAULT_MODEL4 = "llama3";
11223
11364
  function validateConfig4(config) {
11224
11365
  if (!config.baseUrl || config.baseUrl.length === 0) {
@@ -11239,16 +11380,19 @@ async function healthcheck4(config) {
11239
11380
  baseURL: config.baseUrl,
11240
11381
  apiKey: config.apiKey || "not-needed"
11241
11382
  });
11242
- const models = await client.models.list();
11243
- const modelList = [];
11244
- for await (const m of models) {
11245
- modelList.push(m.id);
11246
- if (modelList.length >= 5) break;
11247
- }
11383
+ const models = await withRetry4(async () => {
11384
+ const list = await client.models.list();
11385
+ const items = [];
11386
+ for await (const m of list) {
11387
+ items.push(m.id);
11388
+ if (items.length >= 5) break;
11389
+ }
11390
+ return items;
11391
+ });
11248
11392
  return {
11249
11393
  ok: true,
11250
11394
  provider: "local",
11251
- message: `connected, ${modelList.length} model(s) available`,
11395
+ message: `connected, ${models.length} model(s) available`,
11252
11396
  model: config.model ?? DEFAULT_MODEL4
11253
11397
  };
11254
11398
  } catch (err) {
@@ -11266,26 +11410,33 @@ async function executeTrackedQuery4(input) {
11266
11410
  baseURL: input.config.baseUrl,
11267
11411
  apiKey: input.config.apiKey || "not-needed"
11268
11412
  });
11269
- const response = await client.chat.completions.create({
11270
- model,
11271
- messages: [
11272
- {
11273
- role: "system",
11274
- content: "You are a helpful assistant. Provide comprehensive, factual answers. When mentioning websites or services, include their domain names."
11275
- },
11276
- {
11277
- role: "user",
11278
- content: buildPrompt3(input.keyword, input.location)
11279
- }
11280
- ]
11281
- });
11282
- return {
11283
- provider: "local",
11284
- rawResponse: responseToRecord4(response),
11285
- model,
11286
- groundingSources: [],
11287
- searchQueries: []
11288
- };
11413
+ try {
11414
+ const response = await withRetry4(
11415
+ () => client.chat.completions.create({
11416
+ model,
11417
+ messages: [
11418
+ {
11419
+ role: "system",
11420
+ content: "You are a helpful assistant. Provide comprehensive, factual answers. When mentioning websites or services, include their domain names."
11421
+ },
11422
+ {
11423
+ role: "user",
11424
+ content: buildPrompt3(input.keyword, input.location)
11425
+ }
11426
+ ]
11427
+ })
11428
+ );
11429
+ return {
11430
+ provider: "local",
11431
+ rawResponse: responseToRecord4(response),
11432
+ model,
11433
+ groundingSources: [],
11434
+ searchQueries: []
11435
+ };
11436
+ } catch (err) {
11437
+ const msg = err instanceof Error ? err.message : String(err);
11438
+ throw new Error(`[provider-local] ${msg}`);
11439
+ }
11289
11440
  }
11290
11441
  function normalizeResult4(raw) {
11291
11442
  const answerText = extractAnswerText2(raw.rawResponse);
@@ -11317,10 +11468,12 @@ async function generateText4(prompt, config) {
11317
11468
  baseURL: config.baseUrl,
11318
11469
  apiKey: config.apiKey || "not-needed"
11319
11470
  });
11320
- const response = await client.chat.completions.create({
11321
- model,
11322
- messages: [{ role: "user", content: prompt }]
11323
- });
11471
+ const response = await withRetry4(
11472
+ () => client.chat.completions.create({
11473
+ model,
11474
+ messages: [{ role: "user", content: prompt }]
11475
+ })
11476
+ );
11324
11477
  return response.choices[0]?.message?.content ?? "";
11325
11478
  }
11326
11479
  function extractDomainMentions(text2) {
@@ -11860,7 +12013,7 @@ function normalizeResult5(raw) {
11860
12013
  }
11861
12014
 
11862
12015
  // ../provider-cdp/src/adapter.ts
11863
- var sharedConnection = null;
12016
+ var connectionPool = /* @__PURE__ */ new Map();
11864
12017
  function getConnection(config) {
11865
12018
  if (!config.cdpEndpoint) {
11866
12019
  throw new CDPProviderError("CDP_CONNECTION_REFUSED", "CDP endpoint not configured");
@@ -11871,14 +12024,13 @@ function getConnection(config) {
11871
12024
  const parts = endpoint.split(":");
11872
12025
  if (parts.length >= 1 && parts[0]) host = parts[0];
11873
12026
  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);
12027
+ const key = `${host}:${port}`;
12028
+ let conn = connectionPool.get(key);
12029
+ if (!conn) {
12030
+ conn = new CDPConnectionManager(host, port);
12031
+ connectionPool.set(key, conn);
11880
12032
  }
11881
- return sharedConnection;
12033
+ return conn;
11882
12034
  }
11883
12035
  function getScreenshotDir2() {
11884
12036
  return path4.join(os3.homedir(), ".canonry", "screenshots");
@@ -11937,35 +12089,40 @@ var cdpChatgptAdapter = {
11937
12089
  async executeTrackedQuery(input, config) {
11938
12090
  const conn = getConnection(config);
11939
12091
  const target = chatgptTarget;
11940
- const client = await conn.prepareForQuery(target);
11941
- await target.submitQuery(client, input.keyword);
11942
- await target.waitForResponse(client);
11943
- const answerText = await target.extractAnswer(client);
11944
- const groundingSources = await target.extractCitations(client);
11945
- const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11946
- const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
11947
- let capturedScreenshotPath;
11948
12092
  try {
11949
- capturedScreenshotPath = await captureElementScreenshot(
11950
- client,
11951
- target.responseSelector,
11952
- screenshotPath
11953
- );
11954
- } catch {
11955
- }
11956
- return {
11957
- provider: "cdp:chatgpt",
11958
- rawResponse: {
11959
- answerText,
12093
+ const client = await conn.prepareForQuery(target);
12094
+ await target.submitQuery(client, input.keyword);
12095
+ await target.waitForResponse(client);
12096
+ const answerText = await target.extractAnswer(client);
12097
+ const groundingSources = await target.extractCitations(client);
12098
+ const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
12099
+ const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
12100
+ let capturedScreenshotPath;
12101
+ try {
12102
+ capturedScreenshotPath = await captureElementScreenshot(
12103
+ client,
12104
+ target.responseSelector,
12105
+ screenshotPath
12106
+ );
12107
+ } catch {
12108
+ }
12109
+ return {
12110
+ provider: "cdp:chatgpt",
12111
+ rawResponse: {
12112
+ answerText,
12113
+ groundingSources,
12114
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
12115
+ targetUrl: target.newConversationUrl
12116
+ },
12117
+ model: "chatgpt-web",
11960
12118
  groundingSources,
11961
- extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
11962
- targetUrl: target.newConversationUrl
11963
- },
11964
- model: "chatgpt-web",
11965
- groundingSources,
11966
- searchQueries: [input.keyword],
11967
- screenshotPath: capturedScreenshotPath
11968
- };
12119
+ searchQueries: [input.keyword],
12120
+ screenshotPath: capturedScreenshotPath
12121
+ };
12122
+ } catch (err) {
12123
+ const msg = err instanceof Error ? err.message : String(err);
12124
+ throw new Error(`[provider-cdp] ${msg}`);
12125
+ }
11969
12126
  },
11970
12127
  normalizeResult(raw) {
11971
12128
  return normalizeResult5(raw);
@@ -11977,6 +12134,27 @@ var cdpChatgptAdapter = {
11977
12134
 
11978
12135
  // ../provider-perplexity/src/normalize.ts
11979
12136
  import OpenAI3 from "openai";
12137
+
12138
+ // ../provider-perplexity/src/utils.ts
12139
+ async function withRetry5(fn, options = {}) {
12140
+ const { maxRetries = 3, initialDelay = 1e3 } = options;
12141
+ let lastError;
12142
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
12143
+ try {
12144
+ return await fn();
12145
+ } catch (err) {
12146
+ lastError = err;
12147
+ if (attempt < maxRetries) {
12148
+ const delay = initialDelay * Math.pow(2, attempt);
12149
+ console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err);
12150
+ await new Promise((resolve) => setTimeout(resolve, delay));
12151
+ }
12152
+ }
12153
+ }
12154
+ throw lastError;
12155
+ }
12156
+
12157
+ // ../provider-perplexity/src/normalize.ts
11980
12158
  var DEFAULT_MODEL5 = "sonar";
11981
12159
  var BASE_URL = "https://api.perplexity.ai";
11982
12160
  function validateConfig5(config) {
@@ -11995,10 +12173,12 @@ async function healthcheck5(config) {
11995
12173
  if (!validation.ok) return validation;
11996
12174
  try {
11997
12175
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
11998
- const response = await client.chat.completions.create({
11999
- model: config.model ?? DEFAULT_MODEL5,
12000
- messages: [{ role: "user", content: 'Say "ok"' }]
12001
- });
12176
+ const response = await withRetry5(
12177
+ () => client.chat.completions.create({
12178
+ model: config.model ?? DEFAULT_MODEL5,
12179
+ messages: [{ role: "user", content: 'Say "ok"' }]
12180
+ })
12181
+ );
12002
12182
  const text2 = response.choices[0]?.message?.content ?? "";
12003
12183
  return {
12004
12184
  ok: text2.length > 0,
@@ -12019,25 +12199,30 @@ async function executeTrackedQuery5(input) {
12019
12199
  const model = input.config.model ?? DEFAULT_MODEL5;
12020
12200
  const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
12021
12201
  const prompt = buildPrompt4(input.keyword, input.location);
12022
- const response = await client.chat.completions.create({
12023
- model,
12024
- messages: [
12025
- { role: "user", content: prompt }
12026
- ]
12027
- });
12028
- const rawResponse = responseToRecord5(response);
12029
- const citations = extractCitations(rawResponse);
12030
- const groundingSources = citations.map((url) => ({
12031
- uri: url,
12032
- title: ""
12033
- }));
12034
- return {
12035
- provider: "perplexity",
12036
- rawResponse,
12037
- model,
12038
- groundingSources,
12039
- searchQueries: [input.keyword]
12040
- };
12202
+ try {
12203
+ const response = await withRetry5(
12204
+ () => client.chat.completions.create({
12205
+ model,
12206
+ messages: [{ role: "user", content: prompt }]
12207
+ })
12208
+ );
12209
+ const rawResponse = responseToRecord5(response);
12210
+ const citations = extractCitations(rawResponse);
12211
+ const groundingSources = citations.map((url) => ({
12212
+ uri: url,
12213
+ title: ""
12214
+ }));
12215
+ return {
12216
+ provider: "perplexity",
12217
+ rawResponse,
12218
+ model,
12219
+ groundingSources,
12220
+ searchQueries: [input.keyword]
12221
+ };
12222
+ } catch (err) {
12223
+ const msg = err instanceof Error ? err.message : String(err);
12224
+ throw new Error(`[provider-perplexity] ${msg}`);
12225
+ }
12041
12226
  }
12042
12227
  function normalizeResult6(raw) {
12043
12228
  const answerText = extractAnswerText3(raw.rawResponse);
@@ -12097,10 +12282,12 @@ function extractDomainFromUri4(uri) {
12097
12282
  async function generateText5(prompt, config) {
12098
12283
  const model = config.model ?? DEFAULT_MODEL5;
12099
12284
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
12100
- const response = await client.chat.completions.create({
12101
- model,
12102
- messages: [{ role: "user", content: prompt }]
12103
- });
12285
+ const response = await withRetry5(
12286
+ () => client.chat.completions.create({
12287
+ model,
12288
+ messages: [{ role: "user", content: prompt }]
12289
+ })
12290
+ );
12104
12291
  return response.choices[0]?.message?.content ?? "";
12105
12292
  }
12106
12293
  function responseToRecord5(response) {
@@ -12124,6 +12311,7 @@ var perplexityAdapter = {
12124
12311
  displayName: "Perplexity",
12125
12312
  mode: "api",
12126
12313
  keyUrl: "https://www.perplexity.ai/settings/api",
12314
+ // Upstream model list: https://docs.perplexity.ai/guides/model-cards
12127
12315
  modelRegistry: {
12128
12316
  defaultModel: "sonar",
12129
12317
  validationPattern: /^sonar/,
@@ -12352,7 +12540,7 @@ import crypto18 from "crypto";
12352
12540
  import fs4 from "fs";
12353
12541
  import path5 from "path";
12354
12542
  import os4 from "os";
12355
- import { and as and6, eq as eq17, inArray as inArray3 } from "drizzle-orm";
12543
+ import { and as and6, eq as eq17, inArray as inArray3, sql as sql5 } from "drizzle-orm";
12356
12544
 
12357
12545
  // src/logger.ts
12358
12546
  var IS_TTY = process.stdout.isTTY === true;
@@ -12514,12 +12702,12 @@ var JobRunner = class {
12514
12702
  } else if (locationOverride) {
12515
12703
  runLocation = locationOverride;
12516
12704
  } else {
12517
- const projectLocations = JSON.parse(project.locations || "[]");
12705
+ const projectLocations = parseJsonColumn(project.locations, []);
12518
12706
  if (project.defaultLocation && projectLocations.length > 0) {
12519
12707
  runLocation = projectLocations.find((l) => l.label === project.defaultLocation);
12520
12708
  }
12521
12709
  }
12522
- const projectProviders = providerOverride ?? JSON.parse(project.providers || "[]");
12710
+ const projectProviders = providerOverride ?? parseJsonColumn(project.providers, []);
12523
12711
  activeProviders = this.registry.getForProject(projectProviders);
12524
12712
  if (activeProviders.length === 0) {
12525
12713
  throw new Error("No providers configured. Add at least one provider API key.");
@@ -12530,7 +12718,7 @@ var JobRunner = class {
12530
12718
  const competitorDomains = projectCompetitors.map((c) => c.domain);
12531
12719
  const allDomains = effectiveDomains({
12532
12720
  canonicalDomain: project.canonicalDomain,
12533
- ownedDomains: JSON.parse(project.ownedDomains || "[]")
12721
+ ownedDomains: parseJsonColumn(project.ownedDomains, [])
12534
12722
  });
12535
12723
  const executionContext = {
12536
12724
  providerCount: activeProviders.length,
@@ -12741,22 +12929,19 @@ var JobRunner = class {
12741
12929
  }
12742
12930
  }
12743
12931
  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
- }
12932
+ const now = (/* @__PURE__ */ new Date()).toISOString();
12933
+ const period = now.slice(0, 10);
12934
+ this.db.insert(usageCounters).values({
12935
+ id: crypto18.randomUUID(),
12936
+ scope,
12937
+ period,
12938
+ metric,
12939
+ count,
12940
+ updatedAt: now
12941
+ }).onConflictDoUpdate({
12942
+ target: [usageCounters.scope, usageCounters.period, usageCounters.metric],
12943
+ set: { count: sql5`${usageCounters.count} + ${count}`, updatedAt: now }
12944
+ }).run();
12760
12945
  }
12761
12946
  flushProviderUsage(projectId, providerDispatchCounts) {
12762
12947
  for (const [providerName, count] of providerDispatchCounts.entries()) {
@@ -12957,7 +13142,7 @@ function matchesBrandKey(candidateKey, brandKeys) {
12957
13142
 
12958
13143
  // src/gsc-sync.ts
12959
13144
  import crypto19 from "crypto";
12960
- import { eq as eq18, and as and7, sql as sql5 } from "drizzle-orm";
13145
+ import { eq as eq18, and as and7, sql as sql6 } from "drizzle-orm";
12961
13146
  var log2 = createLogger("GscSync");
12962
13147
  function formatDate2(d) {
12963
13148
  return d.toISOString().split("T")[0];
@@ -13011,8 +13196,8 @@ async function executeGscSync(db, runId, projectId, opts) {
13011
13196
  db.delete(gscSearchData).where(
13012
13197
  and7(
13013
13198
  eq18(gscSearchData.projectId, projectId),
13014
- sql5`${gscSearchData.date} >= ${startDate}`,
13015
- sql5`${gscSearchData.date} <= ${endDate}`
13199
+ sql6`${gscSearchData.date} >= ${startDate}`,
13200
+ sql6`${gscSearchData.date} <= ${endDate}`
13016
13201
  )
13017
13202
  ).run();
13018
13203
  const batchSize = 500;
@@ -13471,7 +13656,7 @@ var Scheduler = class {
13471
13656
  nextRunAt,
13472
13657
  updatedAt: now
13473
13658
  }).where(eq20(schedules.id, currentSchedule.id)).run();
13474
- const scheduleProviders = JSON.parse(currentSchedule.providers);
13659
+ const scheduleProviders = parseJsonColumn(currentSchedule.providers, []);
13475
13660
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
13476
13661
  log4.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
13477
13662
  this.callbacks.onRunCreated(runId, projectId, providers);