@ainyc/canonry 4.13.1 → 4.14.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.
@@ -4,7 +4,7 @@ import {
4
4
  configExists,
5
5
  loadConfig,
6
6
  saveConfigPatch
7
- } from "./chunk-LNRDWAG3.js";
7
+ } from "./chunk-5NYG5EC7.js";
8
8
  import {
9
9
  DEFAULT_RUN_HISTORY_LIMIT,
10
10
  IntelligenceService,
@@ -65,7 +65,7 @@ import {
65
65
  schedules,
66
66
  trafficSources,
67
67
  usageCounters
68
- } from "./chunk-DCE3B6KD.js";
68
+ } from "./chunk-7HBZCGRL.js";
69
69
  import {
70
70
  AGENT_MEMORY_VALUE_MAX_BYTES,
71
71
  AGENT_PROVIDER_IDS,
@@ -108,6 +108,11 @@ import {
108
108
  emptyCitationVisibility,
109
109
  extractAnswerMentions,
110
110
  findDuplicateLocationLabels,
111
+ formatDate,
112
+ formatDateRange,
113
+ formatIsoDate,
114
+ formatNumber,
115
+ formatRatio,
111
116
  getProviderLocationHandling,
112
117
  hasLocationLabel,
113
118
  internalError,
@@ -146,15 +151,18 @@ import {
146
151
  visibilityStateFromAnswerMentioned,
147
152
  windowCutoff,
148
153
  wordpressEnvSchema
149
- } from "./chunk-YDGT5CAY.js";
154
+ } from "./chunk-6QTH5NS5.js";
150
155
 
151
156
  // src/telemetry.ts
152
157
  import crypto from "crypto";
158
+ import os from "os";
153
159
  import { createRequire } from "module";
154
160
  var _require = createRequire(import.meta.url);
155
161
  var { version: VERSION } = _require("../package.json");
156
162
  var TELEMETRY_ENDPOINT = "https://ainyc.ai/api/telemetry";
157
163
  var TIMEOUT_MS = 3e3;
164
+ var ANON_ID_ENV_VAR = "CANONRY_ANONYMOUS_ID";
165
+ var ANON_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
158
166
  function isTelemetryEnabled() {
159
167
  if (process.env.CANONRY_TELEMETRY_DISABLED === "1") return false;
160
168
  if (process.env.DO_NOT_TRACK === "1") return false;
@@ -168,18 +176,66 @@ function isTelemetryEnabled() {
168
176
  }
169
177
  }
170
178
  function getOrCreateAnonymousId() {
171
- if (!configExists()) return void 0;
179
+ const fromEnv = readEnvAnonymousId();
180
+ if (fromEnv) return fromEnv;
181
+ if (configExists()) {
182
+ try {
183
+ const config = loadConfig();
184
+ if (config.anonymousId) return config.anonymousId;
185
+ const id = crypto.randomUUID();
186
+ config.anonymousId = id;
187
+ try {
188
+ saveConfigPatch(config);
189
+ } catch {
190
+ return getDeterministicAnonymousId();
191
+ }
192
+ return id;
193
+ } catch {
194
+ return getDeterministicAnonymousId();
195
+ }
196
+ }
197
+ return getDeterministicAnonymousId();
198
+ }
199
+ function readEnvAnonymousId() {
200
+ const raw = process.env[ANON_ID_ENV_VAR]?.trim();
201
+ if (!raw) return void 0;
202
+ if (!ANON_ID_PATTERN.test(raw)) {
203
+ return void 0;
204
+ }
205
+ return raw.toLowerCase();
206
+ }
207
+ function getDeterministicAnonymousId() {
172
208
  try {
173
- const config = loadConfig();
174
- if (config.anonymousId) return config.anonymousId;
175
- const id = crypto.randomUUID();
176
- config.anonymousId = id;
177
- saveConfigPatch(config);
178
- return id;
209
+ const hostname = os.hostname() || "";
210
+ const mac = firstNonInternalMac();
211
+ const seed = `canonry-anon:${hostname}:${mac}`;
212
+ const hex = crypto.createHash("sha256").update(seed).digest("hex");
213
+ const a = hex.slice(0, 8);
214
+ const b = hex.slice(8, 12);
215
+ const c = "5" + hex.slice(13, 16);
216
+ const dHi = (parseInt(hex.slice(16, 18), 16) & 63 | 128).toString(16).padStart(2, "0");
217
+ const d = dHi + hex.slice(18, 20);
218
+ const e = hex.slice(20, 32);
219
+ return `${a}-${b}-${c}-${d}-${e}`;
179
220
  } catch {
180
221
  return void 0;
181
222
  }
182
223
  }
224
+ function firstNonInternalMac() {
225
+ try {
226
+ const interfaces = os.networkInterfaces();
227
+ for (const ifaces of Object.values(interfaces)) {
228
+ if (!ifaces) continue;
229
+ for (const iface of ifaces) {
230
+ if (iface.internal) continue;
231
+ if (!iface.mac || iface.mac === "00:00:00:00:00:00") continue;
232
+ return iface.mac;
233
+ }
234
+ }
235
+ } catch {
236
+ }
237
+ return "no-mac";
238
+ }
183
239
  function isFirstRun() {
184
240
  if (!configExists()) return false;
185
241
  try {
@@ -2584,16 +2640,6 @@ var COLORS = {
2584
2640
  function escapeHtml(value) {
2585
2641
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
2586
2642
  }
2587
- function formatRatio(value) {
2588
- if (!Number.isFinite(value) || value === 0) return "0%";
2589
- return `${(value * 100).toFixed(1)}%`;
2590
- }
2591
- function formatNumber(value) {
2592
- if (!Number.isFinite(value)) return "\u2014";
2593
- if (Math.abs(value) >= 1e6) return `${(value / 1e6).toFixed(1)}M`;
2594
- if (Math.abs(value) >= 1e3) return `${(value / 1e3).toFixed(1)}K`;
2595
- return value.toLocaleString("en-US");
2596
- }
2597
2643
  function summarizeQueryParams(params) {
2598
2644
  const keys = Array.from(params.keys());
2599
2645
  const total = keys.length;
@@ -2636,23 +2682,6 @@ function formatLandingPageHtml(raw) {
2636
2682
  if (!summary) return pathHtml;
2637
2683
  return `${pathHtml}<span class="page-query" title="${escapeHtml(value)}">${escapeHtml(summary)}</span>`;
2638
2684
  }
2639
- function formatDate(iso) {
2640
- if (!iso) return "\u2014";
2641
- try {
2642
- const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
2643
- const options = { month: "short", day: "numeric", year: "numeric" };
2644
- const d = dateOnly && dateOnly[1] && dateOnly[2] && dateOnly[3] ? new Date(Date.UTC(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3]))) : new Date(iso);
2645
- if (Number.isNaN(d.getTime())) return iso;
2646
- return d.toLocaleDateString("en-US", dateOnly ? { ...options, timeZone: "UTC" } : options);
2647
- } catch {
2648
- return iso;
2649
- }
2650
- }
2651
- function formatDateRange(start, end) {
2652
- if (!start && !end) return "";
2653
- if (start && end) return `${formatDate(start)} \u2192 ${formatDate(end)}`;
2654
- return formatDate(start || end);
2655
- }
2656
2685
  function gscDateRange(report) {
2657
2686
  const summary = report.executiveSummary.gsc;
2658
2687
  const gsc = report.gsc;
@@ -3461,8 +3490,32 @@ table.report-table td .badge {
3461
3490
  .client-bar-row { grid-template-columns: 100px 1fr 100px; gap: 10px; }
3462
3491
  }
3463
3492
  @media print {
3464
- body { background: white; color: black; }
3465
- section.report-section { break-inside: avoid; }
3493
+ @page { margin: 0.5in; }
3494
+ html, body {
3495
+ background: ${COLORS.bg};
3496
+ color: ${COLORS.text};
3497
+ -webkit-print-color-adjust: exact;
3498
+ print-color-adjust: exact;
3499
+ }
3500
+ .container { max-width: none; padding: 0; }
3501
+ section.report-section,
3502
+ .executive-hero,
3503
+ .headline-card,
3504
+ .hero-proof,
3505
+ .client-hero,
3506
+ .client-metric-tile,
3507
+ .client-card,
3508
+ .client-note,
3509
+ .chart-card,
3510
+ .action-card,
3511
+ .insight-card,
3512
+ .source-bar-row,
3513
+ .client-bar-row,
3514
+ tr,
3515
+ table { break-inside: avoid; }
3516
+ h1, h2, h3, .eyebrow { break-after: avoid; }
3517
+ .footer { margin-top: 32px; }
3518
+ .footer a { color: ${COLORS.text}; }
3466
3519
  }
3467
3520
  `;
3468
3521
  function section(opts, body) {
@@ -4634,7 +4687,7 @@ function renderReportHtml(report, opts = {}) {
4634
4687
  <div class="subtitle">${escapeHtml(report.meta.project.canonicalDomain)} \xB7 ${escapeHtml(report.meta.project.country)} / ${escapeHtml(report.meta.project.language.toUpperCase())}${renderHeaderLocationFragment(report.meta.location)} \xB7 Generated ${formatDate(report.meta.generatedAt)}</div>
4635
4688
  </header>
4636
4689
  ${sections}
4637
- <footer class="footer">Generated by canonry \xB7 ${escapeHtml(report.meta.generatedAt)}</footer>
4690
+ <footer class="footer">Generated by <a href="https://canonry.ai">canonry</a> \xB7 ${escapeHtml(formatIsoDate(report.meta.generatedAt))}</footer>
4638
4691
  </div>
4639
4692
  <script type="application/json" id="canonry-report-data">${json}</script>
4640
4693
  </body>
@@ -11025,6 +11078,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
11025
11078
  { name: "date" },
11026
11079
  { name: sourceDim },
11027
11080
  { name: mediumDim },
11081
+ { name: "sessionDefaultChannelGroup" },
11028
11082
  { name: "landingPagePlusQueryString" }
11029
11083
  ],
11030
11084
  metrics: [
@@ -11050,7 +11104,8 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
11050
11104
  date: row.dimensionValues[0].value,
11051
11105
  source: row.dimensionValues[1].value,
11052
11106
  medium: row.dimensionValues[2].value,
11053
- landingPage: row.dimensionValues[3]?.value ?? "(not set)",
11107
+ channelGroup: row.dimensionValues[3]?.value ?? "(not set)",
11108
+ landingPage: row.dimensionValues[4]?.value ?? "(not set)",
11054
11109
  sessions: parseInt(row.metricValues[0].value, 10) || 0,
11055
11110
  users: parseInt(row.metricValues[1].value, 10) || 0,
11056
11111
  sourceDimension: dimLabel
@@ -11063,7 +11118,7 @@ async function fetchAiReferrals(accessToken, propertyId, days) {
11063
11118
  }
11064
11119
  const deduped = /* @__PURE__ */ new Map();
11065
11120
  for (const row of rows) {
11066
- const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}::${row.landingPage}`;
11121
+ const key = `${row.date}::${row.source}::${row.medium}::${row.sourceDimension}::${row.channelGroup}::${row.landingPage}`;
11067
11122
  const existing = deduped.get(key);
11068
11123
  if (!existing) {
11069
11124
  deduped.set(key, row);
@@ -12472,10 +12527,10 @@ async function bingRoutes(app, opts) {
12472
12527
  // ../api-routes/src/cdp.ts
12473
12528
  import fs from "fs";
12474
12529
  import path from "path";
12475
- import os from "os";
12530
+ import os2 from "os";
12476
12531
  import { eq as eq20, and as and8 } from "drizzle-orm";
12477
12532
  function getScreenshotDir() {
12478
- return path.join(os.homedir(), ".canonry", "screenshots");
12533
+ return path.join(os2.homedir(), ".canonry", "screenshots");
12479
12534
  }
12480
12535
  async function cdpRoutes(app, opts) {
12481
12536
  app.get("/screenshots/:snapshotId", async (request, reply) => {
@@ -12657,6 +12712,37 @@ function formatSharePct(numerator, total) {
12657
12712
  if (rounded === 0) return "<1%";
12658
12713
  return `${rounded}%`;
12659
12714
  }
12715
+ var SOCIAL_CHANNEL_GROUPS2 = /* @__PURE__ */ new Set(["Organic Social", "Paid Social"]);
12716
+ function buildChannelBreakdown(input) {
12717
+ const aiSessions = [...input.aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
12718
+ const aiOrganicOverlap = Math.min(input.organicSessions, input.aiSessionsByChannelGroup.get("Organic Search") ?? 0);
12719
+ const aiSocialOverlap = Math.min(
12720
+ input.socialSessions,
12721
+ [...input.aiSessionsByChannelGroup.entries()].filter(([channelGroup]) => SOCIAL_CHANNEL_GROUPS2.has(channelGroup)).reduce((sum, [, sessions]) => sum + sessions, 0)
12722
+ );
12723
+ const aiDirectOverlap = Math.min(input.directSessions, input.aiSessionsByChannelGroup.get("Direct") ?? 0);
12724
+ const organicSessions = Math.max(0, input.organicSessions - aiOrganicOverlap);
12725
+ const socialSessions = Math.max(0, input.socialSessions - aiSocialOverlap);
12726
+ const directSessions = Math.max(0, input.directSessions - aiDirectOverlap);
12727
+ const coveredSessions = organicSessions + socialSessions + directSessions + aiSessions;
12728
+ const otherSessions = Math.max(0, input.totalSessions - coveredSessions);
12729
+ const bucket = (sessions) => ({
12730
+ sessions,
12731
+ sharePct: input.totalSessions > 0 ? Math.round(sessions / input.totalSessions * 100) : 0,
12732
+ sharePctDisplay: formatSharePct(sessions, input.totalSessions)
12733
+ });
12734
+ return {
12735
+ organic: bucket(organicSessions),
12736
+ social: bucket(socialSessions),
12737
+ direct: bucket(directSessions),
12738
+ ai: bucket(aiSessions),
12739
+ other: {
12740
+ sessions: otherSessions,
12741
+ sharePct: input.totalSessions > 0 ? Math.round(otherSessions / input.totalSessions * 100) : 0,
12742
+ sharePctDisplay: input.totalSessions <= 0 && coveredSessions > 0 ? "\u2014" : formatSharePct(otherSessions, input.totalSessions)
12743
+ }
12744
+ };
12745
+ }
12660
12746
  function pickWinningDimension(rows, tupleKey) {
12661
12747
  const winners = /* @__PURE__ */ new Map();
12662
12748
  for (const row of rows) {
@@ -12945,6 +13031,7 @@ async function ga4Routes(app, opts) {
12945
13031
  source: row.source,
12946
13032
  medium: row.medium,
12947
13033
  sourceDimension: row.sourceDimension,
13034
+ channelGroup: row.channelGroup,
12948
13035
  landingPage: row.landingPage,
12949
13036
  landingPageNormalized: normalizeUrlPath(row.landingPage),
12950
13037
  sessions: row.sessions,
@@ -13136,10 +13223,18 @@ async function ga4Routes(app, opts) {
13136
13223
  GROUP BY date, source, medium
13137
13224
  )`
13138
13225
  ).get();
13139
- const aiBySession = app.db.select({
13226
+ const aiBySessionRows = app.db.select({
13227
+ channelGroup: gaAiReferrals.channelGroup,
13140
13228
  sessions: sql5`COALESCE(SUM(${gaAiReferrals.sessions}), 0)`,
13141
13229
  users: sql5`COALESCE(SUM(${gaAiReferrals.users}), 0)`
13142
- }).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).get();
13230
+ }).from(gaAiReferrals).where(and9(...aiConditions, eq21(gaAiReferrals.sourceDimension, "session"))).groupBy(gaAiReferrals.channelGroup).all();
13231
+ const aiSessionsByChannelGroup = /* @__PURE__ */ new Map();
13232
+ let aiBySessionUsers = 0;
13233
+ for (const row of aiBySessionRows) {
13234
+ aiSessionsByChannelGroup.set(row.channelGroup, row.sessions ?? 0);
13235
+ aiBySessionUsers += row.users ?? 0;
13236
+ }
13237
+ const aiBySessionSessions = [...aiSessionsByChannelGroup.values()].reduce((sum, sessions) => sum + sessions, 0);
13143
13238
  const socialReferrals = app.db.select({
13144
13239
  source: gaSocialReferrals.source,
13145
13240
  medium: gaSocialReferrals.medium,
@@ -13154,9 +13249,18 @@ async function ga4Routes(app, opts) {
13154
13249
  const latestSync = app.db.select({ syncedAt: gaTrafficSummaries.syncedAt }).from(gaTrafficSummaries).where(eq21(gaTrafficSummaries.projectId, project.id)).orderBy(desc10(gaTrafficSummaries.syncedAt)).limit(1).get();
13155
13250
  const total = summaryRow?.totalSessions ?? 0;
13156
13251
  const totalDirectSessions = directTotalRow?.totalDirectSessions ?? 0;
13252
+ const totalOrganicSessions = summaryRow?.totalOrganicSessions ?? 0;
13253
+ const socialSessions = socialTotals?.sessions ?? 0;
13254
+ const channelBreakdown = buildChannelBreakdown({
13255
+ totalSessions: total,
13256
+ organicSessions: totalOrganicSessions,
13257
+ socialSessions,
13258
+ directSessions: totalDirectSessions,
13259
+ aiSessionsByChannelGroup
13260
+ });
13157
13261
  return {
13158
13262
  totalSessions: total,
13159
- totalOrganicSessions: summaryRow?.totalOrganicSessions ?? 0,
13263
+ totalOrganicSessions,
13160
13264
  totalDirectSessions,
13161
13265
  totalUsers: summaryRow?.totalUsers ?? 0,
13162
13266
  topPages: rows.map((r) => ({
@@ -13183,8 +13287,8 @@ async function ga4Routes(app, opts) {
13183
13287
  })),
13184
13288
  aiSessionsDeduped: aiDeduped?.sessions ?? 0,
13185
13289
  aiUsersDeduped: aiDeduped?.users ?? 0,
13186
- aiSessionsBySession: aiBySession?.sessions ?? 0,
13187
- aiUsersBySession: aiBySession?.users ?? 0,
13290
+ aiSessionsBySession: aiBySessionSessions,
13291
+ aiUsersBySession: aiBySessionUsers,
13188
13292
  socialReferrals: socialReferrals.map((r) => ({
13189
13293
  source: r.source,
13190
13294
  medium: r.medium,
@@ -13192,18 +13296,22 @@ async function ga4Routes(app, opts) {
13192
13296
  sessions: r.sessions ?? 0,
13193
13297
  users: r.users ?? 0
13194
13298
  })),
13195
- socialSessions: socialTotals?.sessions ?? 0,
13299
+ socialSessions,
13196
13300
  socialUsers: socialTotals?.users ?? 0,
13197
- organicSharePct: total > 0 ? Math.round((summaryRow?.totalOrganicSessions ?? 0) / total * 100) : 0,
13301
+ channelBreakdown,
13302
+ organicSharePct: total > 0 ? Math.round(totalOrganicSessions / total * 100) : 0,
13198
13303
  aiSharePct: total > 0 ? Math.round((aiDeduped?.sessions ?? 0) / total * 100) : 0,
13199
- aiSharePctBySession: total > 0 ? Math.round((aiBySession?.sessions ?? 0) / total * 100) : 0,
13304
+ aiSharePctBySession: total > 0 ? Math.round(aiBySessionSessions / total * 100) : 0,
13200
13305
  directSharePct: total > 0 ? Math.round(totalDirectSessions / total * 100) : 0,
13201
- socialSharePct: total > 0 ? Math.round((socialTotals?.sessions ?? 0) / total * 100) : 0,
13202
- organicSharePctDisplay: formatSharePct(summaryRow?.totalOrganicSessions ?? 0, total),
13306
+ socialSharePct: total > 0 ? Math.round(socialSessions / total * 100) : 0,
13307
+ otherSessions: channelBreakdown.other.sessions,
13308
+ otherSharePct: channelBreakdown.other.sharePct,
13309
+ otherSharePctDisplay: channelBreakdown.other.sharePctDisplay,
13310
+ organicSharePctDisplay: formatSharePct(totalOrganicSessions, total),
13203
13311
  aiSharePctDisplay: formatSharePct(aiDeduped?.sessions ?? 0, total),
13204
- aiSharePctBySessionDisplay: formatSharePct(aiBySession?.sessions ?? 0, total),
13312
+ aiSharePctBySessionDisplay: formatSharePct(aiBySessionSessions, total),
13205
13313
  directSharePctDisplay: formatSharePct(totalDirectSessions, total),
13206
- socialSharePctDisplay: formatSharePct(socialTotals?.sessions ?? 0, total),
13314
+ socialSharePctDisplay: formatSharePct(socialSessions, total),
13207
13315
  lastSyncedAt: latestSync?.syncedAt ?? null,
13208
13316
  periodStart: (() => {
13209
13317
  const start = cutoffDate ?? summaryMeta?.periodStart ?? null;
@@ -15049,13 +15157,13 @@ import crypto18 from "crypto";
15049
15157
  import { and as and11, asc as asc2, desc as desc11, eq as eq22, sql as sql6 } from "drizzle-orm";
15050
15158
 
15051
15159
  // ../integration-commoncrawl/src/constants.ts
15052
- import os2 from "os";
15160
+ import os3 from "os";
15053
15161
  import path2 from "path";
15054
15162
  var CC_BASE_URL = "https://data.commoncrawl.org/projects/hyperlinkgraph";
15055
- var PLUGIN_DIR = path2.join(os2.homedir(), ".canonry", "plugins");
15163
+ var PLUGIN_DIR = path2.join(os3.homedir(), ".canonry", "plugins");
15056
15164
  var PLUGIN_PKG_JSON = path2.join(PLUGIN_DIR, "package.json");
15057
15165
  var DUCKDB_SPEC = process.env.CANONRY_DUCKDB_SPEC ?? "@duckdb/node-api@1.4.4-r.3";
15058
- var CC_CACHE_DIR = process.env.CANONRY_CC_CACHE_DIR ?? path2.join(os2.homedir(), ".canonry", "cache", "commoncrawl");
15166
+ var CC_CACHE_DIR = process.env.CANONRY_CC_CACHE_DIR ?? path2.join(os3.homedir(), ".canonry", "cache", "commoncrawl");
15059
15167
  var RELEASE_ID_REGEX = /^cc-main-(\d{4})-(jan-feb-mar|apr-may-jun|jul-aug-sep|oct-nov-dec)$/;
15060
15168
  function ccReleasePaths(release) {
15061
15169
  const base = `${CC_BASE_URL}/${release}/domain`;
@@ -17572,7 +17680,7 @@ async function apiRoutes(app, opts) {
17572
17680
  }
17573
17681
 
17574
17682
  // src/server.ts
17575
- import os5 from "os";
17683
+ import os6 from "os";
17576
17684
 
17577
17685
  // ../provider-gemini/src/normalize.ts
17578
17686
  import { GoogleGenAI } from "@google/genai";
@@ -18961,7 +19069,7 @@ var localAdapter = {
18961
19069
 
18962
19070
  // ../provider-cdp/src/adapter.ts
18963
19071
  import path8 from "path";
18964
- import os3 from "os";
19072
+ import os4 from "os";
18965
19073
 
18966
19074
  // ../provider-cdp/src/connection.ts
18967
19075
  import CDP from "chrome-remote-interface";
@@ -19425,7 +19533,7 @@ function getConnection(config) {
19425
19533
  return conn;
19426
19534
  }
19427
19535
  function getScreenshotDir2() {
19428
- return path8.join(os3.homedir(), ".canonry", "screenshots");
19536
+ return path8.join(os4.homedir(), ".canonry", "screenshots");
19429
19537
  }
19430
19538
  var cdpChatgptAdapter = {
19431
19539
  name: "cdp:chatgpt",
@@ -20060,7 +20168,7 @@ function removeWordpressConnection(config, projectName) {
20060
20168
  import crypto21 from "crypto";
20061
20169
  import fs7 from "fs";
20062
20170
  import path9 from "path";
20063
- import os4 from "os";
20171
+ import os5 from "os";
20064
20172
  import { and as and12, eq as eq24, inArray as inArray7, sql as sql8 } from "drizzle-orm";
20065
20173
 
20066
20174
  // src/citation-utils.ts
@@ -20309,6 +20417,50 @@ function resolveProviderFanout() {
20309
20417
  const parsed = Number.parseInt(raw, 10);
20310
20418
  return Number.isFinite(parsed) && parsed > 0 ? parsed : PROVIDER_FANOUT_DEFAULT;
20311
20419
  }
20420
+ function classifyRunAbortReason(message) {
20421
+ if (/^No providers configured\b/.test(message)) return "no_provider";
20422
+ if (/^Project [^ ]+ not found$/.test(message)) return "project_not_found";
20423
+ if (/^Daily quota exceeded\b/.test(message)) return "quota_exceeded";
20424
+ if (/^Run [^ ]+ not found$/.test(message)) return "run_not_found";
20425
+ if (/^Run [^ ]+ is not executable\b/.test(message)) return "run_not_executable";
20426
+ return void 0;
20427
+ }
20428
+ function classifyProviderErrors(errors) {
20429
+ const codes = /* @__PURE__ */ new Set();
20430
+ for (const message of errors.values()) {
20431
+ codes.add(classifyOneProviderError(message));
20432
+ }
20433
+ const priority = [
20434
+ "PROVIDER_AUTH",
20435
+ "RATE_LIMITED",
20436
+ "TIMEOUT",
20437
+ "NETWORK",
20438
+ "PARSE_ERROR",
20439
+ "UNKNOWN"
20440
+ ];
20441
+ for (const code of priority) {
20442
+ if (codes.has(code)) return code;
20443
+ }
20444
+ return "UNKNOWN";
20445
+ }
20446
+ function classifyOneProviderError(message) {
20447
+ if (/\b401\b|\b403\b|unauthorized|forbidden|invalid[_ -]?api[_ -]?key|missing[_ -]?api[_ -]?key|authentication/i.test(message)) {
20448
+ return "PROVIDER_AUTH";
20449
+ }
20450
+ if (/\b429\b|rate[_ -]?limit|too many requests|quota[_ -]?exceeded/i.test(message)) {
20451
+ return "RATE_LIMITED";
20452
+ }
20453
+ if (/timeout|timed out|ETIMEDOUT/i.test(message)) {
20454
+ return "TIMEOUT";
20455
+ }
20456
+ if (/ECONNRESET|ECONNREFUSED|ENOTFOUND|EAI_AGAIN|network|fetch failed|socket hang up/i.test(message)) {
20457
+ return "NETWORK";
20458
+ }
20459
+ if (/parse|unexpected token|invalid json|malformed|JSON\.parse/i.test(message)) {
20460
+ return "PARSE_ERROR";
20461
+ }
20462
+ return "UNKNOWN";
20463
+ }
20312
20464
  var JobRunner = class {
20313
20465
  db;
20314
20466
  registry;
@@ -20451,7 +20603,7 @@ var JobRunner = class {
20451
20603
  let screenshotRelPath = null;
20452
20604
  if (raw.screenshotPath && fs7.existsSync(raw.screenshotPath)) {
20453
20605
  const snapshotId = crypto21.randomUUID();
20454
- const screenshotDir = path9.join(os4.homedir(), ".canonry", "screenshots", runId);
20606
+ const screenshotDir = path9.join(os5.homedir(), ".canonry", "screenshots", runId);
20455
20607
  if (!fs7.existsSync(screenshotDir)) fs7.mkdirSync(screenshotDir, { recursive: true });
20456
20608
  const destPath = path9.join(screenshotDir, `${snapshotId}.png`);
20457
20609
  fs7.renameSync(raw.screenshotPath, destPath);
@@ -20540,12 +20692,14 @@ var JobRunner = class {
20540
20692
  }
20541
20693
  this.flushProviderUsage(projectId, providerDispatchCounts);
20542
20694
  const finalStatus = allFailed ? "failed" : someFailed ? "partial" : "completed";
20695
+ const failureCode = providerErrors.size > 0 ? classifyProviderErrors(providerErrors) : void 0;
20543
20696
  trackEvent("run.completed", {
20544
20697
  status: finalStatus,
20545
20698
  providerCount: executionContext.providerCount,
20546
20699
  providers: executionContext.providers,
20547
20700
  queryCount: executionContext.queryCount,
20548
20701
  durationMs: Date.now() - startTime,
20702
+ ...failureCode ? { errorCode: failureCode } : {},
20549
20703
  ...executionContext.location ? { location: executionContext.location } : {}
20550
20704
  });
20551
20705
  this.incrementUsage(projectId, "runs", 1);
@@ -20573,14 +20727,27 @@ var JobRunner = class {
20573
20727
  error: errorMessage
20574
20728
  }).where(eq24(runs.id, runId)).run();
20575
20729
  this.flushProviderUsage(projectId, providerDispatchCounts);
20576
- trackEvent("run.completed", {
20577
- status: "failed",
20578
- providerCount: executionContext.providerCount,
20579
- providers: executionContext.providers,
20580
- queryCount: executionContext.queryCount,
20581
- durationMs: Date.now() - startTime,
20582
- ...executionContext.location ? { location: executionContext.location } : {}
20583
- });
20730
+ const abortReason = classifyRunAbortReason(errorMessage);
20731
+ if (abortReason) {
20732
+ trackEvent("run.aborted", {
20733
+ reason: abortReason,
20734
+ providerCount: executionContext.providerCount,
20735
+ providers: executionContext.providers,
20736
+ queryCount: executionContext.queryCount,
20737
+ durationMs: Date.now() - startTime,
20738
+ ...executionContext.location ? { location: executionContext.location } : {}
20739
+ });
20740
+ } else {
20741
+ trackEvent("run.completed", {
20742
+ status: "failed",
20743
+ errorCode: "UNKNOWN",
20744
+ providerCount: executionContext.providerCount,
20745
+ providers: executionContext.providers,
20746
+ queryCount: executionContext.queryCount,
20747
+ durationMs: Date.now() - startTime,
20748
+ ...executionContext.location ? { location: executionContext.location } : {}
20749
+ });
20750
+ }
20584
20751
  if (this.onRunCompleted) {
20585
20752
  this.onRunCompleted(runId, projectId).catch((notifErr) => {
20586
20753
  log.error("notification.callback-failed", { runId, error: notifErr instanceof Error ? notifErr.message : String(notifErr) });
@@ -24072,7 +24239,7 @@ async function createServer(opts) {
24072
24239
  );
24073
24240
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
24074
24241
  const snapshotService = new SnapshotService(registry);
24075
- const orphanedOpenClawDir = path14.join(os5.homedir(), ".openclaw-aero");
24242
+ const orphanedOpenClawDir = path14.join(os6.homedir(), ".openclaw-aero");
24076
24243
  if (fs12.existsSync(orphanedOpenClawDir)) {
24077
24244
  app.log.warn(
24078
24245
  { path: orphanedOpenClawDir },