@ainyc/canonry 1.37.1 → 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,6 +10394,27 @@ 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
10419
  function isVertexConfig(config) {
10391
10420
  return !!config.vertexProject;
@@ -10434,10 +10463,12 @@ async function healthcheck(config) {
10434
10463
  try {
10435
10464
  const model = resolveModel(config);
10436
10465
  const client = createClient2(config);
10437
- const result = await client.models.generateContent({
10438
- model,
10439
- contents: 'Say "ok"'
10440
- });
10466
+ const result = await withRetry(
10467
+ () => client.models.generateContent({
10468
+ model,
10469
+ contents: 'Say "ok"'
10470
+ })
10471
+ );
10441
10472
  const text2 = result.text ?? "";
10442
10473
  const backend = isVertexConfig(config) ? "vertex ai" : "api key";
10443
10474
  return {
@@ -10459,22 +10490,29 @@ async function executeTrackedQuery(input) {
10459
10490
  const model = resolveModel(input.config);
10460
10491
  const prompt = buildPrompt(input.keyword, input.location);
10461
10492
  const client = createClient2(input.config);
10462
- const result = await client.models.generateContent({
10463
- model,
10464
- contents: prompt,
10465
- config: {
10466
- tools: [{ googleSearch: {} }]
10467
- }
10468
- });
10469
- const groundingSources = extractGroundingMetadata(result);
10470
- const searchQueries = extractSearchQueries(result);
10471
- return {
10472
- provider: "gemini",
10473
- rawResponse: responseToRecord(result),
10474
- model,
10475
- groundingSources,
10476
- searchQueries
10477
- };
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
+ }
10478
10516
  }
10479
10517
  function normalizeResult(raw) {
10480
10518
  const answerText = extractAnswerText(raw.rawResponse);
@@ -10576,10 +10614,12 @@ function extractDomainFromUri(uri) {
10576
10614
  async function generateText(prompt, config) {
10577
10615
  const model = resolveModel(config);
10578
10616
  const client = createClient2(config);
10579
- const result = await client.models.generateContent({
10580
- model,
10581
- contents: prompt
10582
- });
10617
+ const result = await withRetry(
10618
+ () => client.models.generateContent({
10619
+ model,
10620
+ contents: prompt
10621
+ })
10622
+ );
10583
10623
  return result.text ?? "";
10584
10624
  }
10585
10625
  function responseToRecord(response) {
@@ -10688,6 +10728,27 @@ var geminiAdapter = {
10688
10728
 
10689
10729
  // ../provider-openai/src/normalize.ts
10690
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
10691
10752
  var DEFAULT_MODEL2 = "gpt-5.4";
10692
10753
  function validateConfig2(config) {
10693
10754
  if (!config.apiKey || config.apiKey.length === 0) {
@@ -10705,10 +10766,12 @@ async function healthcheck2(config) {
10705
10766
  if (!validation.ok) return validation;
10706
10767
  try {
10707
10768
  const client = new OpenAI({ apiKey: config.apiKey });
10708
- const response = await client.responses.create({
10709
- model: config.model ?? DEFAULT_MODEL2,
10710
- input: 'Say "ok"'
10711
- });
10769
+ const response = await withRetry2(
10770
+ () => client.responses.create({
10771
+ model: config.model ?? DEFAULT_MODEL2,
10772
+ input: 'Say "ok"'
10773
+ })
10774
+ );
10712
10775
  const text2 = extractResponseText(response);
10713
10776
  return {
10714
10777
  ok: text2.length > 0,
@@ -10738,21 +10801,28 @@ async function executeTrackedQuery2(input) {
10738
10801
  ...input.location.timezone ? { timezone: input.location.timezone } : {}
10739
10802
  };
10740
10803
  }
10741
- const response = await client.responses.create({
10742
- model,
10743
- tools: [webSearchTool],
10744
- tool_choice: "required",
10745
- input: buildPrompt2(input.keyword)
10746
- });
10747
- const groundingSources = extractGroundingSources(response);
10748
- const searchQueries = extractSearchQueries2(response);
10749
- return {
10750
- provider: "openai",
10751
- rawResponse: responseToRecord2(response),
10752
- model,
10753
- groundingSources,
10754
- searchQueries
10755
- };
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
+ }
10756
10826
  }
10757
10827
  function normalizeResult2(raw) {
10758
10828
  const answerText = extractAnswerTextFromRaw(raw.rawResponse);
@@ -10863,10 +10933,12 @@ function extractDomainFromUri2(uri) {
10863
10933
  async function generateText2(prompt, config) {
10864
10934
  const model = config.model ?? DEFAULT_MODEL2;
10865
10935
  const client = new OpenAI({ apiKey: config.apiKey });
10866
- const response = await client.responses.create({
10867
- model,
10868
- input: prompt
10869
- });
10936
+ const response = await withRetry2(
10937
+ () => client.responses.create({
10938
+ model,
10939
+ input: prompt
10940
+ })
10941
+ );
10870
10942
  return extractResponseText(response);
10871
10943
  }
10872
10944
  function responseToRecord2(response) {
@@ -10962,6 +11034,27 @@ var openaiAdapter = {
10962
11034
 
10963
11035
  // ../provider-claude/src/normalize.ts
10964
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
10965
11058
  var DEFAULT_MODEL3 = "claude-sonnet-4-6";
10966
11059
  var VALIDATION_PATTERN = /^claude-/;
10967
11060
  function resolveModel2(config) {
@@ -10992,11 +11085,13 @@ async function healthcheck3(config) {
10992
11085
  try {
10993
11086
  const model = resolveModel2(config);
10994
11087
  const client = new Anthropic({ apiKey: config.apiKey });
10995
- const response = await client.messages.create({
10996
- model,
10997
- max_tokens: 32,
10998
- messages: [{ role: "user", content: 'Say "ok"' }]
10999
- });
11088
+ const response = await withRetry3(
11089
+ () => client.messages.create({
11090
+ model,
11091
+ max_tokens: 32,
11092
+ messages: [{ role: "user", content: 'Say "ok"' }]
11093
+ })
11094
+ );
11000
11095
  const text2 = extractTextFromResponse(response);
11001
11096
  return {
11002
11097
  ok: text2.length > 0,
@@ -11030,21 +11125,28 @@ async function executeTrackedQuery3(input) {
11030
11125
  ...input.location.timezone ? { timezone: input.location.timezone } : {}
11031
11126
  };
11032
11127
  }
11033
- const response = await client.messages.create({
11034
- model,
11035
- max_tokens: 4096,
11036
- tools: [webSearchTool],
11037
- messages: [{ role: "user", content: input.keyword }]
11038
- });
11039
- const groundingSources = extractGroundingSources2(response);
11040
- const searchQueries = extractSearchQueries3(response);
11041
- return {
11042
- provider: "claude",
11043
- rawResponse: responseToRecord3(response),
11044
- model,
11045
- groundingSources,
11046
- searchQueries
11047
- };
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
+ }
11048
11150
  }
11049
11151
  function normalizeResult3(raw) {
11050
11152
  const answerText = extractAnswerTextFromRaw2(raw.rawResponse);
@@ -11138,11 +11240,13 @@ function extractDomainFromUri3(uri) {
11138
11240
  async function generateText3(prompt, config) {
11139
11241
  const model = resolveModel2(config);
11140
11242
  const client = new Anthropic({ apiKey: config.apiKey });
11141
- const response = await client.messages.create({
11142
- model,
11143
- max_tokens: 2048,
11144
- messages: [{ role: "user", content: prompt }]
11145
- });
11243
+ const response = await withRetry3(
11244
+ () => client.messages.create({
11245
+ model,
11246
+ max_tokens: 2048,
11247
+ messages: [{ role: "user", content: prompt }]
11248
+ })
11249
+ );
11146
11250
  return extractTextFromResponse(response);
11147
11251
  }
11148
11252
  function responseToRecord3(response) {
@@ -11235,6 +11339,27 @@ var claudeAdapter = {
11235
11339
 
11236
11340
  // ../provider-local/src/normalize.ts
11237
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
11238
11363
  var DEFAULT_MODEL4 = "llama3";
11239
11364
  function validateConfig4(config) {
11240
11365
  if (!config.baseUrl || config.baseUrl.length === 0) {
@@ -11255,16 +11380,19 @@ async function healthcheck4(config) {
11255
11380
  baseURL: config.baseUrl,
11256
11381
  apiKey: config.apiKey || "not-needed"
11257
11382
  });
11258
- const models = await client.models.list();
11259
- const modelList = [];
11260
- for await (const m of models) {
11261
- modelList.push(m.id);
11262
- if (modelList.length >= 5) break;
11263
- }
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
+ });
11264
11392
  return {
11265
11393
  ok: true,
11266
11394
  provider: "local",
11267
- message: `connected, ${modelList.length} model(s) available`,
11395
+ message: `connected, ${models.length} model(s) available`,
11268
11396
  model: config.model ?? DEFAULT_MODEL4
11269
11397
  };
11270
11398
  } catch (err) {
@@ -11282,26 +11410,33 @@ async function executeTrackedQuery4(input) {
11282
11410
  baseURL: input.config.baseUrl,
11283
11411
  apiKey: input.config.apiKey || "not-needed"
11284
11412
  });
11285
- const response = await client.chat.completions.create({
11286
- model,
11287
- messages: [
11288
- {
11289
- role: "system",
11290
- content: "You are a helpful assistant. Provide comprehensive, factual answers. When mentioning websites or services, include their domain names."
11291
- },
11292
- {
11293
- role: "user",
11294
- content: buildPrompt3(input.keyword, input.location)
11295
- }
11296
- ]
11297
- });
11298
- return {
11299
- provider: "local",
11300
- rawResponse: responseToRecord4(response),
11301
- model,
11302
- groundingSources: [],
11303
- searchQueries: []
11304
- };
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
+ }
11305
11440
  }
11306
11441
  function normalizeResult4(raw) {
11307
11442
  const answerText = extractAnswerText2(raw.rawResponse);
@@ -11333,10 +11468,12 @@ async function generateText4(prompt, config) {
11333
11468
  baseURL: config.baseUrl,
11334
11469
  apiKey: config.apiKey || "not-needed"
11335
11470
  });
11336
- const response = await client.chat.completions.create({
11337
- model,
11338
- messages: [{ role: "user", content: prompt }]
11339
- });
11471
+ const response = await withRetry4(
11472
+ () => client.chat.completions.create({
11473
+ model,
11474
+ messages: [{ role: "user", content: prompt }]
11475
+ })
11476
+ );
11340
11477
  return response.choices[0]?.message?.content ?? "";
11341
11478
  }
11342
11479
  function extractDomainMentions(text2) {
@@ -11952,35 +12089,40 @@ var cdpChatgptAdapter = {
11952
12089
  async executeTrackedQuery(input, config) {
11953
12090
  const conn = getConnection(config);
11954
12091
  const target = chatgptTarget;
11955
- const client = await conn.prepareForQuery(target);
11956
- await target.submitQuery(client, input.keyword);
11957
- await target.waitForResponse(client);
11958
- const answerText = await target.extractAnswer(client);
11959
- const groundingSources = await target.extractCitations(client);
11960
- const screenshotId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
11961
- const screenshotPath = path4.join(getScreenshotDir2(), `${screenshotId}.png`);
11962
- let capturedScreenshotPath;
11963
12092
  try {
11964
- capturedScreenshotPath = await captureElementScreenshot(
11965
- client,
11966
- target.responseSelector,
11967
- screenshotPath
11968
- );
11969
- } catch {
11970
- }
11971
- return {
11972
- provider: "cdp:chatgpt",
11973
- rawResponse: {
11974
- 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",
11975
12118
  groundingSources,
11976
- extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
11977
- targetUrl: target.newConversationUrl
11978
- },
11979
- model: "chatgpt-web",
11980
- groundingSources,
11981
- searchQueries: [input.keyword],
11982
- screenshotPath: capturedScreenshotPath
11983
- };
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
+ }
11984
12126
  },
11985
12127
  normalizeResult(raw) {
11986
12128
  return normalizeResult5(raw);
@@ -11992,6 +12134,27 @@ var cdpChatgptAdapter = {
11992
12134
 
11993
12135
  // ../provider-perplexity/src/normalize.ts
11994
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
11995
12158
  var DEFAULT_MODEL5 = "sonar";
11996
12159
  var BASE_URL = "https://api.perplexity.ai";
11997
12160
  function validateConfig5(config) {
@@ -12010,10 +12173,12 @@ async function healthcheck5(config) {
12010
12173
  if (!validation.ok) return validation;
12011
12174
  try {
12012
12175
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
12013
- const response = await client.chat.completions.create({
12014
- model: config.model ?? DEFAULT_MODEL5,
12015
- messages: [{ role: "user", content: 'Say "ok"' }]
12016
- });
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
+ );
12017
12182
  const text2 = response.choices[0]?.message?.content ?? "";
12018
12183
  return {
12019
12184
  ok: text2.length > 0,
@@ -12034,25 +12199,30 @@ async function executeTrackedQuery5(input) {
12034
12199
  const model = input.config.model ?? DEFAULT_MODEL5;
12035
12200
  const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
12036
12201
  const prompt = buildPrompt4(input.keyword, input.location);
12037
- const response = await client.chat.completions.create({
12038
- model,
12039
- messages: [
12040
- { role: "user", content: prompt }
12041
- ]
12042
- });
12043
- const rawResponse = responseToRecord5(response);
12044
- const citations = extractCitations(rawResponse);
12045
- const groundingSources = citations.map((url) => ({
12046
- uri: url,
12047
- title: ""
12048
- }));
12049
- return {
12050
- provider: "perplexity",
12051
- rawResponse,
12052
- model,
12053
- groundingSources,
12054
- searchQueries: [input.keyword]
12055
- };
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
+ }
12056
12226
  }
12057
12227
  function normalizeResult6(raw) {
12058
12228
  const answerText = extractAnswerText3(raw.rawResponse);
@@ -12112,10 +12282,12 @@ function extractDomainFromUri4(uri) {
12112
12282
  async function generateText5(prompt, config) {
12113
12283
  const model = config.model ?? DEFAULT_MODEL5;
12114
12284
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
12115
- const response = await client.chat.completions.create({
12116
- model,
12117
- messages: [{ role: "user", content: prompt }]
12118
- });
12285
+ const response = await withRetry5(
12286
+ () => client.chat.completions.create({
12287
+ model,
12288
+ messages: [{ role: "user", content: prompt }]
12289
+ })
12290
+ );
12119
12291
  return response.choices[0]?.message?.content ?? "";
12120
12292
  }
12121
12293
  function responseToRecord5(response) {
package/dist/cli.js CHANGED
@@ -26,22 +26,26 @@ import {
26
26
  setGoogleAuthConfig,
27
27
  showFirstRunNotice,
28
28
  trackEvent
29
- } from "./chunk-U2ZNKIWK.js";
29
+ } from "./chunk-SAVCFB5B.js";
30
30
 
31
31
  // src/cli.ts
32
32
  import { pathToFileURL } from "url";
33
33
 
34
34
  // src/cli-error.ts
35
+ var EXIT_USER_ERROR = 1;
36
+ var EXIT_SYSTEM_ERROR = 2;
35
37
  var CliError = class extends Error {
36
38
  code;
37
39
  displayMessage;
38
40
  details;
41
+ exitCode;
39
42
  constructor(options) {
40
43
  super(options.message);
41
44
  this.name = "CliError";
42
45
  this.code = options.code;
43
46
  this.displayMessage = options.displayMessage;
44
47
  this.details = options.details;
48
+ this.exitCode = options.exitCode ?? EXIT_USER_ERROR;
45
49
  }
46
50
  };
47
51
  function usageError(displayMessage, options) {
@@ -398,13 +402,16 @@ var ApiClient = class {
398
402
  body: serializedBody
399
403
  });
400
404
  } catch (err) {
405
+ if (err instanceof CliError) throw err;
401
406
  const msg = err instanceof Error ? err.message : String(err);
402
407
  if (msg.includes("fetch failed") || msg.includes("ECONNREFUSED") || msg.includes("connect ECONNREFUSED")) {
403
- throw new Error(
404
- `Could not connect to canonry server at ${this.baseUrl.replace("/api/v1", "")}. Start it with "canonry serve" (or "canonry serve &" to run in background).`
405
- );
408
+ throw new CliError({
409
+ code: "CONNECTION_ERROR",
410
+ message: `Could not connect to canonry server at ${this.baseUrl.replace("/api/v1", "")}. Start it with "canonry serve" (or "canonry serve &" to run in background).`,
411
+ exitCode: EXIT_SYSTEM_ERROR
412
+ });
406
413
  }
407
- throw err;
414
+ throw new CliError({ code: "CONNECTION_ERROR", message: msg, exitCode: EXIT_SYSTEM_ERROR });
408
415
  }
409
416
  if (!res.ok) {
410
417
  let errorBody;
@@ -413,8 +420,11 @@ var ApiClient = class {
413
420
  } catch {
414
421
  errorBody = { error: { code: "UNKNOWN", message: res.statusText } };
415
422
  }
416
- const msg = errorBody && typeof errorBody === "object" && "error" in errorBody && errorBody.error && typeof errorBody.error === "object" && "message" in errorBody.error ? String(errorBody.error.message) : `HTTP ${res.status}: ${res.statusText}`;
417
- throw new Error(msg);
423
+ const errorObj = errorBody && typeof errorBody === "object" && "error" in errorBody && errorBody.error && typeof errorBody.error === "object" ? errorBody.error : null;
424
+ const msg = errorObj?.message ? String(errorObj.message) : `HTTP ${res.status}: ${res.statusText}`;
425
+ const code = errorObj?.code ? String(errorObj.code) : "API_ERROR";
426
+ const exitCode = res.status >= 500 ? EXIT_SYSTEM_ERROR : EXIT_USER_ERROR;
427
+ throw new CliError({ code, message: msg, exitCode });
418
428
  }
419
429
  if (res.status === 204) {
420
430
  return void 0;
@@ -751,22 +761,12 @@ function getClient() {
751
761
  return createApiClient();
752
762
  }
753
763
  async function bingConnect(project, opts) {
754
- let apiKey = opts?.apiKey;
755
- if (!apiKey) {
756
- const readline2 = await import("readline");
757
- const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
758
- apiKey = await new Promise((resolve) => {
759
- rl.question("Bing Webmaster Tools API key: ", (answer) => {
760
- rl.close();
761
- resolve(answer.trim());
762
- });
763
- });
764
- }
764
+ const apiKey = opts?.apiKey;
765
765
  if (!apiKey) {
766
766
  throw new CliError({
767
767
  code: "BING_API_KEY_REQUIRED",
768
- message: "API key is required (pass --api-key or enter interactively)",
769
- displayMessage: "Error: API key is required (pass --api-key or enter interactively)",
768
+ message: "API key is required. Pass --api-key <key>.",
769
+ displayMessage: "Error: API key is required. Pass --api-key <key>.",
770
770
  details: {
771
771
  project
772
772
  }
@@ -3287,13 +3287,13 @@ async function showStatus(project, format) {
3287
3287
  console.log(JSON.stringify({ project: projectData, runs: runs2 }, null, 2));
3288
3288
  return;
3289
3289
  }
3290
- console.log(`Status: ${projectData.displayName} (${projectData.name})
3290
+ console.log(`Status: ${projectData.displayName ?? projectData.name} (${projectData.name})
3291
3291
  `);
3292
3292
  console.log(` Domain: ${projectData.canonicalDomain}`);
3293
3293
  console.log(` Country: ${projectData.country}`);
3294
3294
  console.log(` Language: ${projectData.language}`);
3295
3295
  if (runs2.length > 0) {
3296
- const latest = runs2[runs2.length - 1];
3296
+ const latest = runs2[0];
3297
3297
  console.log(`
3298
3298
  Latest run:`);
3299
3299
  console.log(` ID: ${latest.id}`);
@@ -3313,25 +3313,25 @@ async function showStatus(project, format) {
3313
3313
  var OPERATOR_CLI_COMMANDS = [
3314
3314
  {
3315
3315
  path: ["status"],
3316
- usage: "canonry status <project>",
3316
+ usage: "canonry status <project> [--format json]",
3317
3317
  run: async (input) => {
3318
- const project = requireProject(input, "status", "canonry status <project>");
3318
+ const project = requireProject(input, "status", "canonry status <project> [--format json]");
3319
3319
  await showStatus(project, input.format);
3320
3320
  }
3321
3321
  },
3322
3322
  {
3323
3323
  path: ["evidence"],
3324
- usage: "canonry evidence <project>",
3324
+ usage: "canonry evidence <project> [--format json]",
3325
3325
  run: async (input) => {
3326
- const project = requireProject(input, "evidence", "canonry evidence <project>");
3326
+ const project = requireProject(input, "evidence", "canonry evidence <project> [--format json]");
3327
3327
  await showEvidence(project, input.format);
3328
3328
  }
3329
3329
  },
3330
3330
  {
3331
3331
  path: ["history"],
3332
- usage: "canonry history <project>",
3332
+ usage: "canonry history <project> [--format json]",
3333
3333
  run: async (input) => {
3334
- const project = requireProject(input, "history", "canonry history <project>");
3334
+ const project = requireProject(input, "history", "canonry history <project> [--format json]");
3335
3335
  await showHistory(project, input.format);
3336
3336
  }
3337
3337
  },
@@ -3437,7 +3437,7 @@ async function showProject(name, format) {
3437
3437
  console.log(JSON.stringify(project, null, 2));
3438
3438
  return;
3439
3439
  }
3440
- console.log(`Project: ${project.displayName}
3440
+ console.log(`Project: ${project.displayName ?? project.name}
3441
3441
  `);
3442
3442
  console.log(` Name: ${project.name}`);
3443
3443
  console.log(` ID: ${project.id}`);
@@ -3453,8 +3453,8 @@ async function showProject(name, format) {
3453
3453
  console.log(` Tags: ${project.tags.length > 0 ? project.tags.join(", ") : "(none)"}`);
3454
3454
  const labelEntries = Object.entries(project.labels);
3455
3455
  console.log(` Labels: ${labelEntries.length > 0 ? labelEntries.map(([k, v]) => `${k}=${v}`).join(", ") : "(none)"}`);
3456
- console.log(` Created: ${project.createdAt}`);
3457
- console.log(` Updated: ${project.updatedAt}`);
3456
+ if (project.createdAt) console.log(` Created: ${project.createdAt}`);
3457
+ if (project.updatedAt) console.log(` Updated: ${project.updatedAt}`);
3458
3458
  }
3459
3459
  async function updateProjectSettings(name, opts) {
3460
3460
  const client = getClient12();
@@ -3469,7 +3469,7 @@ async function updateProjectSettings(name, opts) {
3469
3469
  ownedDomains = ownedDomains.filter((d) => !toRemove.has(d));
3470
3470
  }
3471
3471
  const result = await client.putProject(name, {
3472
- displayName: opts.displayName ?? project.displayName,
3472
+ displayName: opts.displayName ?? project.displayName ?? project.name,
3473
3473
  canonicalDomain: opts.domain ?? project.canonicalDomain,
3474
3474
  ownedDomains,
3475
3475
  country: opts.country ?? project.country,
@@ -5454,6 +5454,15 @@ async function serveCommand(format = "text") {
5454
5454
  const db = createClient(config.database);
5455
5455
  migrate(db);
5456
5456
  const app = await createServer({ config, db });
5457
+ const shutdown = () => {
5458
+ app.close().then(() => {
5459
+ process.exit(0);
5460
+ }).catch(() => {
5461
+ process.exit(1);
5462
+ });
5463
+ };
5464
+ process.on("SIGTERM", shutdown);
5465
+ process.on("SIGINT", shutdown);
5457
5466
  try {
5458
5467
  await app.listen({ host, port });
5459
5468
  const url = `http://${host === "0.0.0.0" ? "localhost" : host}:${port}`;
@@ -5758,16 +5767,6 @@ import fs6 from "fs";
5758
5767
  function getClient17() {
5759
5768
  return createApiClient();
5760
5769
  }
5761
- async function promptForAppPassword() {
5762
- const readline2 = await import("readline");
5763
- const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
5764
- return new Promise((resolve) => {
5765
- rl.question("WordPress Application Password: ", (answer) => {
5766
- rl.close();
5767
- resolve(answer.trim());
5768
- });
5769
- });
5770
- }
5771
5770
  function printJson(value) {
5772
5771
  console.log(JSON.stringify(value, null, 2));
5773
5772
  }
@@ -5903,12 +5902,12 @@ Staging:`);
5903
5902
  console.log(` Snippet: ${diff.staging.contentSnippet || "(empty)"}`);
5904
5903
  }
5905
5904
  async function wordpressConnect(project, opts) {
5906
- const appPassword = opts.appPassword ?? await promptForAppPassword();
5905
+ const appPassword = opts.appPassword;
5907
5906
  if (!appPassword) {
5908
5907
  throw new CliError({
5909
5908
  code: "WORDPRESS_APP_PASSWORD_REQUIRED",
5910
5909
  message: "WordPress Application Password is required",
5911
- displayMessage: "Error: WordPress Application Password is required (pass --app-password or enter interactively).",
5910
+ displayMessage: "Error: WordPress Application Password is required. Pass --app-password <password>.",
5912
5911
  details: { project }
5913
5912
  });
5914
5913
  }
@@ -6200,12 +6199,12 @@ async function wordpressSchemaStatus(project, opts) {
6200
6199
  }
6201
6200
  }
6202
6201
  async function wordpressOnboard(project, opts) {
6203
- const appPassword = opts.appPassword ?? await promptForAppPassword();
6202
+ const appPassword = opts.appPassword;
6204
6203
  if (!appPassword) {
6205
6204
  throw new CliError({
6206
6205
  code: "WORDPRESS_APP_PASSWORD_REQUIRED",
6207
6206
  message: "WordPress Application Password is required",
6208
- displayMessage: "Error: WordPress Application Password is required (pass --app-password or enter interactively).",
6207
+ displayMessage: "Error: WordPress Application Password is required. Pass --app-password <password>.",
6209
6208
  details: { project }
6210
6209
  });
6211
6210
  }
@@ -6857,10 +6856,10 @@ Usage:
6857
6856
  canonry run --all Trigger runs for all projects
6858
6857
  canonry run show <id> Show run details and snapshots
6859
6858
  canonry runs <project> List runs for a project (--limit <n>)
6860
- canonry status <project> Show project summary
6861
- canonry evidence <project> Show per-phrase results
6859
+ canonry status <project> Show project summary [--format json]
6860
+ canonry evidence <project> Show per-phrase results [--format json]
6862
6861
  canonry analytics <project> Show analytics (--feature metrics|gaps|sources, --window 7d|30d|90d|all)
6863
- canonry history <project> Show audit trail
6862
+ canonry history <project> Show audit trail [--format json]
6864
6863
  canonry export <project> Export project as YAML
6865
6864
  canonry apply <file...> Apply declarative config (multi-doc YAML supported)
6866
6865
  canonry schedule set <project> Set schedule (--preset or --cron)
@@ -7019,7 +7018,7 @@ Run "canonry --help" for usage.`, {
7019
7018
  });
7020
7019
  } catch (err) {
7021
7020
  printCliError(err, format);
7022
- return 1;
7021
+ return err instanceof CliError ? err.exitCode : EXIT_SYSTEM_ERROR;
7023
7022
  }
7024
7023
  }
7025
7024
  async function main(args = process.argv.slice(2)) {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-U2ZNKIWK.js";
4
+ } from "./chunk-SAVCFB5B.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.1",
3
+ "version": "1.38.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-contracts": "0.0.0",
59
58
  "@ainyc/canonry-integration-google": "0.0.0",
60
59
  "@ainyc/canonry-db": "0.0.0",
61
- "@ainyc/canonry-config": "0.0.0",
62
- "@ainyc/canonry-integration-bing": "0.0.0",
63
60
  "@ainyc/canonry-integration-wordpress": "0.0.0",
64
- "@ainyc/canonry-provider-cdp": "0.0.0",
65
- "@ainyc/canonry-provider-local": "0.0.0",
66
- "@ainyc/canonry-provider-openai": "0.0.0",
61
+ "@ainyc/canonry-integration-bing": "0.0.0",
62
+ "@ainyc/canonry-contracts": "0.0.0",
63
+ "@ainyc/canonry-config": "0.0.0",
67
64
  "@ainyc/canonry-provider-claude": "0.0.0",
65
+ "@ainyc/canonry-provider-cdp": "0.0.0",
68
66
  "@ainyc/canonry-provider-gemini": "0.0.0",
69
- "@ainyc/canonry-provider-perplexity": "0.0.0"
67
+ "@ainyc/canonry-provider-openai": "0.0.0",
68
+ "@ainyc/canonry-provider-perplexity": "0.0.0",
69
+ "@ainyc/canonry-provider-local": "0.0.0"
70
70
  },
71
71
  "scripts": {
72
72
  "build": "tsup && tsx build-web.ts",