@ainyc/canonry 1.40.1 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/index.html CHANGED
@@ -12,7 +12,7 @@
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-DU7KOHur.js"></script>
15
+ <script type="module" crossorigin src="./assets/index-iy29Cmx8.js"></script>
16
16
  <link rel="stylesheet" crossorigin href="./assets/index-Djm1st6N.css">
17
17
  </head>
18
18
  <body>
@@ -110,7 +110,10 @@ var querySnapshots = sqliteTable("query_snapshots", {
110
110
  createdAt: text("created_at").notNull()
111
111
  }, (table) => [
112
112
  index("idx_snapshots_run").on(table.runId),
113
- index("idx_snapshots_keyword").on(table.keywordId)
113
+ index("idx_snapshots_keyword").on(table.keywordId),
114
+ index("idx_snapshots_citation_state").on(table.citationState),
115
+ index("idx_snapshots_provider_model").on(table.provider, table.model),
116
+ index("idx_snapshots_location").on(table.location)
114
117
  ]);
115
118
  var auditLog = sqliteTable("audit_log", {
116
119
  id: text("id").primaryKey(),
@@ -21,7 +21,7 @@ import {
21
21
  runs,
22
22
  schedules,
23
23
  usageCounters
24
- } from "./chunk-FOWWBLXD.js";
24
+ } from "./chunk-EUBC5EGC.js";
25
25
 
26
26
  // src/config.ts
27
27
  import fs from "fs";
@@ -978,6 +978,11 @@ var scheduleUpsertRequestSchema = z10.object({
978
978
  { message: 'Exactly one of "preset" or "cron" must be provided' }
979
979
  );
980
980
 
981
+ // ../contracts/src/analytics.ts
982
+ import { z as z11 } from "zod";
983
+ var visibilityMetricModeSchema = z11.enum(["answer", "citation"]);
984
+ var VisibilityMetricModes = visibilityMetricModeSchema.enum;
985
+
981
986
  // ../contracts/src/source-categories.ts
982
987
  var SOURCE_CATEGORY_RULES = [
983
988
  // Forums
@@ -1071,62 +1076,62 @@ function categoryLabel(category) {
1071
1076
  }
1072
1077
 
1073
1078
  // ../contracts/src/ga.ts
1074
- import { z as z11 } from "zod";
1075
- var ga4ConnectionDtoSchema = z11.object({
1076
- id: z11.string(),
1077
- projectId: z11.string(),
1078
- propertyId: z11.string(),
1079
- clientEmail: z11.string(),
1080
- connected: z11.boolean(),
1081
- createdAt: z11.string(),
1082
- updatedAt: z11.string()
1079
+ import { z as z12 } from "zod";
1080
+ var ga4ConnectionDtoSchema = z12.object({
1081
+ id: z12.string(),
1082
+ projectId: z12.string(),
1083
+ propertyId: z12.string(),
1084
+ clientEmail: z12.string(),
1085
+ connected: z12.boolean(),
1086
+ createdAt: z12.string(),
1087
+ updatedAt: z12.string()
1083
1088
  });
1084
- var ga4TrafficSnapshotDtoSchema = z11.object({
1085
- date: z11.string(),
1086
- landingPage: z11.string(),
1087
- sessions: z11.number(),
1088
- organicSessions: z11.number(),
1089
- users: z11.number()
1089
+ var ga4TrafficSnapshotDtoSchema = z12.object({
1090
+ date: z12.string(),
1091
+ landingPage: z12.string(),
1092
+ sessions: z12.number(),
1093
+ organicSessions: z12.number(),
1094
+ users: z12.number()
1090
1095
  });
1091
- var ga4SourceDimensionSchema = z11.enum(["session", "first_user", "manual_utm"]);
1092
- var ga4AiReferralDtoSchema = z11.object({
1093
- source: z11.string(),
1094
- medium: z11.string(),
1095
- sessions: z11.number(),
1096
- users: z11.number(),
1096
+ var ga4SourceDimensionSchema = z12.enum(["session", "first_user", "manual_utm"]);
1097
+ var ga4AiReferralDtoSchema = z12.object({
1098
+ source: z12.string(),
1099
+ medium: z12.string(),
1100
+ sessions: z12.number(),
1101
+ users: z12.number(),
1097
1102
  sourceDimension: ga4SourceDimensionSchema
1098
1103
  });
1099
- var ga4TrafficSummaryDtoSchema = z11.object({
1100
- totalSessions: z11.number(),
1101
- totalOrganicSessions: z11.number(),
1102
- totalUsers: z11.number(),
1103
- topPages: z11.array(z11.object({
1104
- landingPage: z11.string(),
1105
- sessions: z11.number(),
1106
- organicSessions: z11.number(),
1107
- users: z11.number()
1104
+ var ga4TrafficSummaryDtoSchema = z12.object({
1105
+ totalSessions: z12.number(),
1106
+ totalOrganicSessions: z12.number(),
1107
+ totalUsers: z12.number(),
1108
+ topPages: z12.array(z12.object({
1109
+ landingPage: z12.string(),
1110
+ sessions: z12.number(),
1111
+ organicSessions: z12.number(),
1112
+ users: z12.number()
1108
1113
  })),
1109
- aiReferrals: z11.array(ga4AiReferralDtoSchema),
1114
+ aiReferrals: z12.array(ga4AiReferralDtoSchema),
1110
1115
  /** Deduped AI session total: MAX(sessions) per date+source+medium across attribution dimensions, then summed. */
1111
- aiSessionsDeduped: z11.number(),
1116
+ aiSessionsDeduped: z12.number(),
1112
1117
  /** Deduped AI user total: MAX(users) per date+source+medium across attribution dimensions, then summed. */
1113
- aiUsersDeduped: z11.number(),
1114
- lastSyncedAt: z11.string().nullable()
1118
+ aiUsersDeduped: z12.number(),
1119
+ lastSyncedAt: z12.string().nullable()
1115
1120
  });
1116
- var ga4AiReferralHistoryEntrySchema = z11.object({
1117
- date: z11.string(),
1118
- source: z11.string(),
1119
- medium: z11.string(),
1120
- sessions: z11.number(),
1121
- users: z11.number(),
1121
+ var ga4AiReferralHistoryEntrySchema = z12.object({
1122
+ date: z12.string(),
1123
+ source: z12.string(),
1124
+ medium: z12.string(),
1125
+ sessions: z12.number(),
1126
+ users: z12.number(),
1122
1127
  /** Which GA4 dimension this row came from: session (sessionSource), first_user (firstUserSource), or manual_utm (utm_source parameter) */
1123
1128
  sourceDimension: ga4SourceDimensionSchema
1124
1129
  });
1125
- var ga4SessionHistoryEntrySchema = z11.object({
1126
- date: z11.string(),
1127
- sessions: z11.number(),
1128
- organicSessions: z11.number(),
1129
- users: z11.number()
1130
+ var ga4SessionHistoryEntrySchema = z12.object({
1131
+ date: z12.string(),
1132
+ sessions: z12.number(),
1133
+ organicSessions: z12.number(),
1134
+ users: z12.number()
1130
1135
  });
1131
1136
 
1132
1137
  // ../contracts/src/answer-visibility.ts
@@ -2877,20 +2882,27 @@ async function analyticsRoutes(app) {
2877
2882
  return reply.send({
2878
2883
  window,
2879
2884
  buckets: [],
2880
- overall: { citationRate: 0, cited: 0, total: 0 },
2885
+ overall: { citationRate: 0, cited: 0, total: 0, answerRate: 0, answerMentionedCount: 0 },
2881
2886
  byProvider: {},
2882
2887
  trend: "stable",
2888
+ answerTrend: "stable",
2883
2889
  keywordChanges: []
2884
2890
  });
2885
2891
  }
2886
2892
  const runIds = projectRuns.map((r) => r.id);
2887
- const allSnapshots = app.db.select({
2893
+ const rawSnapshots = app.db.select({
2888
2894
  runId: querySnapshots.runId,
2889
2895
  keywordId: querySnapshots.keywordId,
2890
2896
  provider: querySnapshots.provider,
2891
2897
  citationState: querySnapshots.citationState,
2898
+ answerMentioned: querySnapshots.answerMentioned,
2899
+ answerText: querySnapshots.answerText,
2892
2900
  createdAt: querySnapshots.createdAt
2893
2901
  }).from(querySnapshots).where(inArray2(querySnapshots.runId, runIds)).all();
2902
+ const allSnapshots = rawSnapshots.map((s) => ({
2903
+ ...s,
2904
+ resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
2905
+ }));
2894
2906
  const projectKeywords = app.db.select({ id: keywords.id, createdAt: keywords.createdAt }).from(keywords).where(eq10(keywords.projectId, project.id)).all();
2895
2907
  const keywordCreatedAt = new Map(projectKeywords.map((k) => [k.id, k.createdAt]));
2896
2908
  const overall = computeProviderMetric(allSnapshots);
@@ -2904,9 +2916,10 @@ async function analyticsRoutes(app) {
2904
2916
  const spanDays = Math.max(1, Math.ceil((latest.getTime() - earliest.getTime()) / 864e5));
2905
2917
  const bucketSize = bucketSizeForSpan(spanDays);
2906
2918
  const buckets = computeBuckets(allSnapshots, projectRuns, bucketSize, keywordCreatedAt);
2907
- const trend = computeTrend(buckets);
2919
+ const trend = computeTrend(buckets, "citationRate");
2920
+ const answerTrend = computeTrend(buckets, "answerRate");
2908
2921
  const keywordChanges = computeKeywordChanges(projectKeywords, cutoff);
2909
- return reply.send({ window, buckets, overall, byProvider, trend, keywordChanges });
2922
+ return reply.send({ window, buckets, overall, byProvider, trend, answerTrend, keywordChanges });
2910
2923
  });
2911
2924
  app.get("/projects/:name/analytics/gaps", async (request, reply) => {
2912
2925
  const project = resolveProject(app.db, request.params.name);
@@ -2914,7 +2927,7 @@ async function analyticsRoutes(app) {
2914
2927
  const cutoff = windowCutoff(window);
2915
2928
  const latestRun = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(desc3(runs.createdAt)).all().find((r) => r.status === "completed" || r.status === "partial");
2916
2929
  if (!latestRun) {
2917
- return reply.send({ cited: [], gap: [], uncited: [], runId: "", window });
2930
+ return reply.send({ cited: [], gap: [], uncited: [], mentionedKeywords: [], mentionGap: [], notMentioned: [], runId: "", window });
2918
2931
  }
2919
2932
  const windowRuns = app.db.select().from(runs).where(eq10(runs.projectId, project.id)).orderBy(runs.createdAt).all().filter((r) => r.status === "completed" || r.status === "partial").filter((r) => !cutoff || r.createdAt >= cutoff);
2920
2933
  const windowRunIds = windowRuns.map((r) => r.id);
@@ -2923,25 +2936,34 @@ async function analyticsRoutes(app) {
2923
2936
  const allWindowSnaps = app.db.select({
2924
2937
  keywordId: querySnapshots.keywordId,
2925
2938
  runId: querySnapshots.runId,
2926
- citationState: querySnapshots.citationState
2939
+ citationState: querySnapshots.citationState,
2940
+ answerMentioned: querySnapshots.answerMentioned,
2941
+ answerText: querySnapshots.answerText
2927
2942
  }).from(querySnapshots).where(inArray2(querySnapshots.runId, windowRunIds)).all();
2928
2943
  for (const s of allWindowSnaps) {
2929
2944
  let entry = consistencyMap.get(s.keywordId);
2930
2945
  if (!entry) {
2931
- entry = { citedRuns: /* @__PURE__ */ new Set(), totalRuns: /* @__PURE__ */ new Set() };
2946
+ entry = { citedRuns: /* @__PURE__ */ new Set(), totalRuns: /* @__PURE__ */ new Set(), mentionedRuns: /* @__PURE__ */ new Set() };
2932
2947
  consistencyMap.set(s.keywordId, entry);
2933
2948
  }
2934
2949
  entry.totalRuns.add(s.runId);
2935
2950
  if (s.citationState === "cited") entry.citedRuns.add(s.runId);
2951
+ if (resolveSnapshotAnswerMentioned(s, project)) entry.mentionedRuns.add(s.runId);
2936
2952
  }
2937
2953
  }
2938
- const snapshots = app.db.select({
2954
+ const rawSnapshots = app.db.select({
2939
2955
  keywordId: querySnapshots.keywordId,
2940
2956
  keyword: keywords.keyword,
2941
2957
  provider: querySnapshots.provider,
2942
2958
  citationState: querySnapshots.citationState,
2959
+ answerMentioned: querySnapshots.answerMentioned,
2960
+ answerText: querySnapshots.answerText,
2943
2961
  competitorOverlap: querySnapshots.competitorOverlap
2944
2962
  }).from(querySnapshots).leftJoin(keywords, eq10(querySnapshots.keywordId, keywords.id)).where(eq10(querySnapshots.runId, latestRun.id)).all();
2963
+ const snapshots = rawSnapshots.map((s) => ({
2964
+ ...s,
2965
+ resolvedMentioned: resolveSnapshotAnswerMentioned(s, project)
2966
+ }));
2945
2967
  const byKeyword = /* @__PURE__ */ new Map();
2946
2968
  for (const s of snapshots) {
2947
2969
  const key = s.keywordId;
@@ -2952,14 +2974,24 @@ async function analyticsRoutes(app) {
2952
2974
  const cited = [];
2953
2975
  const gap = [];
2954
2976
  const uncited = [];
2977
+ const mentionedKeywords = [];
2978
+ const mentionGap = [];
2979
+ const notMentioned = [];
2955
2980
  for (const [keywordId, kwSnapshots] of byKeyword) {
2956
2981
  const keyword = kwSnapshots[0]?.keyword ?? "";
2957
2982
  const citedProviders = kwSnapshots.filter((s) => s.citationState === "cited").map((s) => s.provider);
2983
+ const mentionedProviders = kwSnapshots.filter((s) => s.resolvedMentioned).map((s) => s.provider);
2958
2984
  const competitorsCiting = /* @__PURE__ */ new Set();
2959
2985
  for (const s of kwSnapshots) {
2960
2986
  const overlap = parseJsonColumn(s.competitorOverlap, []);
2961
2987
  for (const c of overlap) competitorsCiting.add(c);
2962
2988
  }
2989
+ const cons = consistencyMap.get(keywordId);
2990
+ const consistency = {
2991
+ citedRuns: cons?.citedRuns.size ?? 0,
2992
+ totalRuns: cons?.totalRuns.size ?? 0,
2993
+ mentionedRuns: cons?.mentionedRuns.size ?? 0
2994
+ };
2963
2995
  let category;
2964
2996
  if (citedProviders.length > 0) {
2965
2997
  category = "cited";
@@ -2968,26 +3000,44 @@ async function analyticsRoutes(app) {
2968
3000
  } else {
2969
3001
  category = "uncited";
2970
3002
  }
2971
- const cons = consistencyMap.get(keywordId);
2972
- const entry = {
3003
+ const citationEntry = {
2973
3004
  keyword,
2974
3005
  keywordId,
2975
3006
  category,
2976
3007
  providers: citedProviders,
2977
3008
  competitorsCiting: [...competitorsCiting],
2978
- consistency: {
2979
- citedRuns: cons?.citedRuns.size ?? 0,
2980
- totalRuns: cons?.totalRuns.size ?? 0
2981
- }
3009
+ consistency
3010
+ };
3011
+ if (category === "cited") cited.push(citationEntry);
3012
+ else if (category === "gap") gap.push(citationEntry);
3013
+ else uncited.push(citationEntry);
3014
+ let mentionCategory;
3015
+ if (mentionedProviders.length > 0) {
3016
+ mentionCategory = "cited";
3017
+ } else if (competitorsCiting.size > 0) {
3018
+ mentionCategory = "gap";
3019
+ } else {
3020
+ mentionCategory = "uncited";
3021
+ }
3022
+ const mentionEntry = {
3023
+ keyword,
3024
+ keywordId,
3025
+ category: mentionCategory,
3026
+ providers: mentionedProviders,
3027
+ competitorsCiting: [...competitorsCiting],
3028
+ consistency
2982
3029
  };
2983
- if (category === "cited") cited.push(entry);
2984
- else if (category === "gap") gap.push(entry);
2985
- else uncited.push(entry);
3030
+ if (mentionCategory === "cited") mentionedKeywords.push(mentionEntry);
3031
+ else if (mentionCategory === "gap") mentionGap.push(mentionEntry);
3032
+ else notMentioned.push(mentionEntry);
2986
3033
  }
2987
3034
  gap.sort((a, b) => b.competitorsCiting.length - a.competitorsCiting.length);
2988
3035
  cited.sort((a, b) => a.keyword.localeCompare(b.keyword));
2989
3036
  uncited.sort((a, b) => a.keyword.localeCompare(b.keyword));
2990
- return reply.send({ cited, gap, uncited, runId: latestRun.id, window });
3037
+ mentionGap.sort((a, b) => b.competitorsCiting.length - a.competitorsCiting.length);
3038
+ mentionedKeywords.sort((a, b) => a.keyword.localeCompare(b.keyword));
3039
+ notMentioned.sort((a, b) => a.keyword.localeCompare(b.keyword));
3040
+ return reply.send({ cited, gap, uncited, mentionedKeywords, mentionGap, notMentioned, runId: latestRun.id, window });
2991
3041
  });
2992
3042
  app.get("/projects/:name/analytics/sources", async (request, reply) => {
2993
3043
  const project = resolveProject(app.db, request.params.name);
@@ -3070,10 +3120,13 @@ function bucketSizeForSpan(spanDays) {
3070
3120
  function computeProviderMetric(snapshots) {
3071
3121
  const total = snapshots.length;
3072
3122
  const cited = snapshots.filter((s) => s.citationState === "cited").length;
3123
+ const answerMentionedCount = snapshots.filter((s) => s.resolvedMentioned).length;
3073
3124
  return {
3074
3125
  citationRate: total > 0 ? Math.round(cited / total * 1e4) / 1e4 : 0,
3075
3126
  cited,
3076
- total
3127
+ total,
3128
+ answerRate: total > 0 ? Math.round(answerMentionedCount / total * 1e4) / 1e4 : 0,
3129
+ answerMentionedCount
3077
3130
  };
3078
3131
  }
3079
3132
  function computeBuckets(snapshots, projectRuns, bucketDays, keywordCreatedAt) {
@@ -3106,7 +3159,9 @@ function computeBuckets(snapshots, projectRuns, bucketDays, keywordCreatedAt) {
3106
3159
  citationRate: metric.citationRate,
3107
3160
  cited: metric.cited,
3108
3161
  total: metric.total,
3109
- keywordCount
3162
+ keywordCount,
3163
+ answerRate: metric.answerRate,
3164
+ answerMentionedCount: metric.answerMentionedCount
3110
3165
  });
3111
3166
  }
3112
3167
  start = end;
@@ -3128,14 +3183,14 @@ function computeKeywordChanges(projectKeywords, cutoff) {
3128
3183
  label: `+${count} kp`
3129
3184
  }));
3130
3185
  }
3131
- function computeTrend(buckets) {
3186
+ function computeTrend(buckets, rateKey) {
3132
3187
  const nonEmpty = buckets.filter((b) => b.total > 0);
3133
3188
  if (nonEmpty.length < 2) return "stable";
3134
3189
  const mid = Math.floor(nonEmpty.length / 2);
3135
3190
  const firstHalf = nonEmpty.slice(0, mid);
3136
3191
  const secondHalf = nonEmpty.slice(mid);
3137
- const avgFirst = firstHalf.reduce((s, b) => s + b.citationRate, 0) / firstHalf.length;
3138
- const avgSecond = secondHalf.reduce((s, b) => s + b.citationRate, 0) / secondHalf.length;
3192
+ const avgFirst = firstHalf.reduce((s, b) => s + b[rateKey], 0) / firstHalf.length;
3193
+ const avgSecond = secondHalf.reduce((s, b) => s + b[rateKey], 0) / secondHalf.length;
3139
3194
  const diff = avgSecond - avgFirst;
3140
3195
  if (diff > 0.05) return "improving";
3141
3196
  if (diff < -0.05) return "declining";
@@ -6020,18 +6075,16 @@ async function gscFetch(accessToken, url, opts) {
6020
6075
  signal: AbortSignal.timeout(GOOGLE_REQUEST_TIMEOUT_MS)
6021
6076
  });
6022
6077
  if (res.status === 401) {
6023
- const body = await res.text().catch(() => "");
6024
- gscClientLog("error", "http.auth-expired", { url, method, httpStatus: 401, responseBody: body });
6078
+ gscClientLog("error", "http.auth-expired", { url, method, httpStatus: 401 });
6025
6079
  throw new GoogleApiError("Access token expired or revoked", 401);
6026
6080
  }
6027
6081
  if (res.status === 429) {
6028
- const body = await res.text().catch(() => "");
6029
- gscClientLog("error", "http.rate-limited", { url, method, httpStatus: 429, responseBody: body });
6082
+ gscClientLog("error", "http.rate-limited", { url, method, httpStatus: 429 });
6030
6083
  throw new GoogleApiError("Google API rate limit exceeded", 429);
6031
6084
  }
6032
6085
  if (!res.ok) {
6033
6086
  const body = await res.text();
6034
- gscClientLog("error", "http.error", { url, method, httpStatus: res.status, responseBody: body });
6087
+ gscClientLog("error", "http.error", { url, method, httpStatus: res.status });
6035
6088
  throw new GoogleApiError(`GSC API error (${res.status}): ${body}`, res.status);
6036
6089
  }
6037
6090
  return await res.json();
@@ -6214,7 +6267,7 @@ async function getAccessToken(clientEmail, privateKey) {
6214
6267
  });
6215
6268
  if (!res.ok) {
6216
6269
  const body = await res.text().catch(() => "");
6217
- ga4Log("error", "token.failed", { httpStatus: res.status, responseBody: body });
6270
+ ga4Log("error", "token.failed", { httpStatus: res.status });
6218
6271
  throw new GA4ApiError(`Failed to get access token: ${body}`, res.status);
6219
6272
  }
6220
6273
  const data = await res.json();
@@ -6244,7 +6297,7 @@ async function runReport(accessToken, propertyId, request) {
6244
6297
  } catch {
6245
6298
  if (body.length < 200) detail = ` ${body}`;
6246
6299
  }
6247
- ga4Log("error", "report.auth-failed", { propertyId, httpStatus: res.status, responseBody: body });
6300
+ ga4Log("error", "report.auth-failed", { propertyId, httpStatus: res.status });
6248
6301
  throw new GA4ApiError(
6249
6302
  `GA4 API authentication failed \u2014 check service account permissions.${detail}`,
6250
6303
  res.status
@@ -6256,7 +6309,7 @@ async function runReport(accessToken, propertyId, request) {
6256
6309
  }
6257
6310
  if (!res.ok) {
6258
6311
  const body = await res.text();
6259
- ga4Log("error", "report.error", { propertyId, httpStatus: res.status, responseBody: body });
6312
+ ga4Log("error", "report.error", { propertyId, httpStatus: res.status });
6260
6313
  throw new GA4ApiError(`GA4 API error (${res.status}): ${body}`, res.status);
6261
6314
  }
6262
6315
  return await res.json();
@@ -7317,18 +7370,16 @@ async function bingFetch(apiKey, endpoint, opts) {
7317
7370
  signal: AbortSignal.timeout(BING_REQUEST_TIMEOUT_MS)
7318
7371
  });
7319
7372
  if (res.status === 401 || res.status === 403) {
7320
- const body = await res.text().catch(() => "");
7321
- bingClientLog("error", "http.auth-failed", { endpoint, method, httpStatus: res.status, responseBody: body });
7373
+ bingClientLog("error", "http.auth-failed", { endpoint, method, httpStatus: res.status });
7322
7374
  throw new BingApiError("Bing API key is invalid or unauthorized", res.status);
7323
7375
  }
7324
7376
  if (res.status === 429) {
7325
- const body = await res.text().catch(() => "");
7326
- bingClientLog("error", "http.rate-limited", { endpoint, method, httpStatus: 429, responseBody: body });
7377
+ bingClientLog("error", "http.rate-limited", { endpoint, method, httpStatus: 429 });
7327
7378
  throw new BingApiError("Bing API rate limit exceeded", 429);
7328
7379
  }
7329
7380
  if (!res.ok) {
7330
7381
  const body = await res.text();
7331
- bingClientLog("error", "http.error", { endpoint, method, httpStatus: res.status, responseBody: body });
7382
+ bingClientLog("error", "http.error", { endpoint, method, httpStatus: res.status });
7332
7383
  throw new BingApiError(`Bing API error (${res.status}): ${body}`, res.status);
7333
7384
  }
7334
7385
  const text = await res.text();
@@ -10137,6 +10188,15 @@ async function apiRoutes(app, opts) {
10137
10188
  import { GoogleGenAI } from "@google/genai";
10138
10189
 
10139
10190
  // ../provider-gemini/src/utils.ts
10191
+ function isRetryableError(err) {
10192
+ if (err != null && typeof err === "object" && "status" in err) {
10193
+ const status = err.status;
10194
+ if (typeof status === "number") {
10195
+ return status >= 500 || status === 429;
10196
+ }
10197
+ }
10198
+ return true;
10199
+ }
10140
10200
  async function withRetry(fn, options = {}) {
10141
10201
  const { maxRetries = 3, initialDelay = 1e3 } = options;
10142
10202
  let lastError;
@@ -10145,10 +10205,12 @@ async function withRetry(fn, options = {}) {
10145
10205
  return await fn();
10146
10206
  } catch (err) {
10147
10207
  lastError = err;
10148
- if (attempt < maxRetries) {
10208
+ if (attempt < maxRetries && isRetryableError(err)) {
10149
10209
  const delay = initialDelay * Math.pow(2, attempt);
10150
10210
  console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
10151
10211
  await new Promise((resolve) => setTimeout(resolve, delay));
10212
+ } else {
10213
+ throw err;
10152
10214
  }
10153
10215
  }
10154
10216
  }
@@ -10510,6 +10572,15 @@ var geminiAdapter = {
10510
10572
  import OpenAI from "openai";
10511
10573
 
10512
10574
  // ../provider-openai/src/utils.ts
10575
+ function isRetryableError2(err) {
10576
+ if (err != null && typeof err === "object" && "status" in err) {
10577
+ const status = err.status;
10578
+ if (typeof status === "number") {
10579
+ return status >= 500 || status === 429;
10580
+ }
10581
+ }
10582
+ return true;
10583
+ }
10513
10584
  async function withRetry2(fn, options = {}) {
10514
10585
  const { maxRetries = 3, initialDelay = 1e3 } = options;
10515
10586
  let lastError;
@@ -10518,10 +10589,12 @@ async function withRetry2(fn, options = {}) {
10518
10589
  return await fn();
10519
10590
  } catch (err) {
10520
10591
  lastError = err;
10521
- if (attempt < maxRetries) {
10592
+ if (attempt < maxRetries && isRetryableError2(err)) {
10522
10593
  const delay = initialDelay * Math.pow(2, attempt);
10523
10594
  console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
10524
10595
  await new Promise((resolve) => setTimeout(resolve, delay));
10596
+ } else {
10597
+ throw err;
10525
10598
  }
10526
10599
  }
10527
10600
  }
@@ -10843,6 +10916,15 @@ var openaiAdapter = {
10843
10916
  import Anthropic from "@anthropic-ai/sdk";
10844
10917
 
10845
10918
  // ../provider-claude/src/utils.ts
10919
+ function isRetryableError3(err) {
10920
+ if (err != null && typeof err === "object" && "status" in err) {
10921
+ const status = err.status;
10922
+ if (typeof status === "number") {
10923
+ return status >= 500 || status === 429;
10924
+ }
10925
+ }
10926
+ return true;
10927
+ }
10846
10928
  async function withRetry3(fn, options = {}) {
10847
10929
  const { maxRetries = 3, initialDelay = 1e3 } = options;
10848
10930
  let lastError;
@@ -10851,10 +10933,12 @@ async function withRetry3(fn, options = {}) {
10851
10933
  return await fn();
10852
10934
  } catch (err) {
10853
10935
  lastError = err;
10854
- if (attempt < maxRetries) {
10936
+ if (attempt < maxRetries && isRetryableError3(err)) {
10855
10937
  const delay = initialDelay * Math.pow(2, attempt);
10856
10938
  console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
10857
10939
  await new Promise((resolve) => setTimeout(resolve, delay));
10940
+ } else {
10941
+ throw err;
10858
10942
  }
10859
10943
  }
10860
10944
  }
@@ -11199,6 +11283,15 @@ var claudeAdapter = {
11199
11283
  import OpenAI2 from "openai";
11200
11284
 
11201
11285
  // ../provider-local/src/utils.ts
11286
+ function isRetryableError4(err) {
11287
+ if (err != null && typeof err === "object" && "status" in err) {
11288
+ const status = err.status;
11289
+ if (typeof status === "number") {
11290
+ return status >= 500 || status === 429;
11291
+ }
11292
+ }
11293
+ return true;
11294
+ }
11202
11295
  async function withRetry4(fn, options = {}) {
11203
11296
  const { maxRetries = 3, initialDelay = 1e3 } = options;
11204
11297
  let lastError;
@@ -11207,10 +11300,12 @@ async function withRetry4(fn, options = {}) {
11207
11300
  return await fn();
11208
11301
  } catch (err) {
11209
11302
  lastError = err;
11210
- if (attempt < maxRetries) {
11303
+ if (attempt < maxRetries && isRetryableError4(err)) {
11211
11304
  const delay = initialDelay * Math.pow(2, attempt);
11212
11305
  console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
11213
11306
  await new Promise((resolve) => setTimeout(resolve, delay));
11307
+ } else {
11308
+ throw err;
11214
11309
  }
11215
11310
  }
11216
11311
  }
@@ -11994,6 +12089,15 @@ var cdpChatgptAdapter = {
11994
12089
  import OpenAI3 from "openai";
11995
12090
 
11996
12091
  // ../provider-perplexity/src/utils.ts
12092
+ function isRetryableError5(err) {
12093
+ if (err != null && typeof err === "object" && "status" in err) {
12094
+ const status = err.status;
12095
+ if (typeof status === "number") {
12096
+ return status >= 500 || status === 429;
12097
+ }
12098
+ }
12099
+ return true;
12100
+ }
11997
12101
  async function withRetry5(fn, options = {}) {
11998
12102
  const { maxRetries = 3, initialDelay = 1e3 } = options;
11999
12103
  let lastError;
@@ -12002,10 +12106,12 @@ async function withRetry5(fn, options = {}) {
12002
12106
  return await fn();
12003
12107
  } catch (err) {
12004
12108
  lastError = err;
12005
- if (attempt < maxRetries) {
12109
+ if (attempt < maxRetries && isRetryableError5(err)) {
12006
12110
  const delay = initialDelay * Math.pow(2, attempt);
12007
12111
  console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
12008
12112
  await new Promise((resolve) => setTimeout(resolve, delay));
12113
+ } else {
12114
+ throw err;
12009
12115
  }
12010
12116
  }
12011
12117
  }
package/dist/cli.js CHANGED
@@ -28,7 +28,7 @@ import {
28
28
  setGoogleAuthConfig,
29
29
  showFirstRunNotice,
30
30
  trackEvent
31
- } from "./chunk-FXHVGU5S.js";
31
+ } from "./chunk-TKMBOLZB.js";
32
32
  import {
33
33
  apiKeys,
34
34
  competitors,
@@ -38,7 +38,7 @@ import {
38
38
  projects,
39
39
  querySnapshots,
40
40
  runs
41
- } from "./chunk-FOWWBLXD.js";
41
+ } from "./chunk-EUBC5EGC.js";
42
42
 
43
43
  // src/cli.ts
44
44
  import { pathToFileURL } from "url";
@@ -355,7 +355,7 @@ async function backfillAnswerVisibilityCommand(opts) {
355
355
  console.log(` Errors: ${providerErrors}`);
356
356
  }
357
357
  async function backfillInsightsCommand(project, opts) {
358
- const { IntelligenceService } = await import("./intelligence-service-PDZOIB7L.js");
358
+ const { IntelligenceService } = await import("./intelligence-service-DXGTIRF5.js");
359
359
  const config = loadConfig();
360
360
  const db = createClient(config.database);
361
361
  migrate(db);
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  createServer,
3
3
  loadConfig
4
- } from "./chunk-FXHVGU5S.js";
5
- import "./chunk-FOWWBLXD.js";
4
+ } from "./chunk-TKMBOLZB.js";
5
+ import "./chunk-EUBC5EGC.js";
6
6
  export {
7
7
  createServer,
8
8
  loadConfig
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  IntelligenceService
3
- } from "./chunk-FOWWBLXD.js";
3
+ } from "./chunk-EUBC5EGC.js";
4
4
  export {
5
5
  IntelligenceService
6
6
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ainyc/canonry",
3
- "version": "1.40.1",
3
+ "version": "1.41.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",
@@ -57,17 +57,17 @@
57
57
  "@ainyc/canonry-api-routes": "0.0.0",
58
58
  "@ainyc/canonry-config": "0.0.0",
59
59
  "@ainyc/canonry-contracts": "0.0.0",
60
- "@ainyc/canonry-db": "0.0.0",
61
60
  "@ainyc/canonry-intelligence": "0.0.0",
62
- "@ainyc/canonry-integration-google": "0.0.0",
61
+ "@ainyc/canonry-integration-bing": "0.0.0",
63
62
  "@ainyc/canonry-integration-wordpress": "0.0.0",
63
+ "@ainyc/canonry-db": "0.0.0",
64
64
  "@ainyc/canonry-provider-cdp": "0.0.0",
65
- "@ainyc/canonry-integration-bing": "0.0.0",
66
- "@ainyc/canonry-provider-claude": "0.0.0",
65
+ "@ainyc/canonry-integration-google": "0.0.0",
67
66
  "@ainyc/canonry-provider-gemini": "0.0.0",
68
- "@ainyc/canonry-provider-perplexity": "0.0.0",
67
+ "@ainyc/canonry-provider-local": "0.0.0",
68
+ "@ainyc/canonry-provider-claude": "0.0.0",
69
69
  "@ainyc/canonry-provider-openai": "0.0.0",
70
- "@ainyc/canonry-provider-local": "0.0.0"
70
+ "@ainyc/canonry-provider-perplexity": "0.0.0"
71
71
  },
72
72
  "scripts": {
73
73
  "build": "tsup && tsx build-web.ts",