@ainyc/canonry 4.56.1 → 4.59.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.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/assets/agent-workspace/skills/aero/references/orchestration.md +1 -1
  3. package/assets/agent-workspace/skills/canonry/SKILL.md +8 -1
  4. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +44 -0
  5. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +219 -0
  6. package/assets/assets/{BacklinksPage-CmeFZ8UJ.js → BacklinksPage-D_mc7c-b.js} +1 -1
  7. package/assets/assets/{ChartPrimitives-D7C1Cp8w.js → ChartPrimitives-BViWneKX.js} +1 -1
  8. package/assets/assets/{ProjectPage-Y6uCyjGb.js → ProjectPage-_hpYJAN1.js} +1 -1
  9. package/assets/assets/{RunRow-BntNdrgM.js → RunRow-DK69_0iD.js} +1 -1
  10. package/assets/assets/{RunsPage-Btp6qn10.js → RunsPage-DRu1peAA.js} +1 -1
  11. package/assets/assets/{SettingsPage-DkyNiU2i.js → SettingsPage-BrednApH.js} +1 -1
  12. package/assets/assets/{TrafficPage-CBl4Mwdc.js → TrafficPage-oFA65ZZc.js} +1 -1
  13. package/assets/assets/{TrafficSourceDetailPage-BZzuWCn-.js → TrafficSourceDetailPage-CUzzaYFC.js} +1 -1
  14. package/assets/assets/{extract-error-message-De8_qAzs.js → extract-error-message-Cv4MXGtB.js} +1 -1
  15. package/assets/assets/{index-XUKhruAg.js → index-BrCh3uvb.js} +90 -90
  16. package/assets/assets/{server-traffic-bn9LSZN9.js → server-traffic-rYE-NlE-.js} +1 -1
  17. package/assets/assets/{trash-2-B5clF2rU.js → trash-2-BgGGPjQf.js} +1 -1
  18. package/assets/index.html +1 -1
  19. package/dist/{chunk-4KWPOVIT.js → chunk-JW6TQFU7.js} +171 -1
  20. package/dist/{chunk-WFVUZVJD.js → chunk-LPPW7O26.js} +1421 -1190
  21. package/dist/{chunk-I2LAM5IM.js → chunk-NOQ4ZE3E.js} +2210 -615
  22. package/dist/{chunk-6X5TF73A.js → chunk-TFBPLY77.js} +414 -1
  23. package/dist/cli.js +610 -458
  24. package/dist/index.d.ts +2 -1
  25. package/dist/index.js +4 -4
  26. package/dist/{intelligence-service-NY3MAVPB.js → intelligence-service-V4SWVKEQ.js} +2 -2
  27. package/dist/mcp.js +10 -8
  28. package/package.json +11 -10
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  ApiClient,
3
+ CliError,
4
+ PACKAGE_VERSION,
3
5
  canonryMcpTools,
4
6
  configExists,
5
7
  getConfigPath,
6
8
  loadConfig,
7
9
  loadConfigRaw,
8
10
  saveConfigPatch
9
- } from "./chunk-6X5TF73A.js";
11
+ } from "./chunk-TFBPLY77.js";
10
12
  import {
11
13
  DEFAULT_RUN_HISTORY_LIMIT,
12
14
  IntelligenceService,
@@ -62,6 +64,11 @@ import {
62
64
  gaTrafficSnapshots,
63
65
  gaTrafficSummaries,
64
66
  gaTrafficWindowSummaries,
67
+ gbpDailyMetrics,
68
+ gbpKeywordImpressions,
69
+ gbpLocations,
70
+ gbpLodgingSnapshots,
71
+ gbpPlaceActions,
65
72
  groupInsights,
66
73
  groupRunsByCreatedAt,
67
74
  gscCoverageSnapshots,
@@ -87,7 +94,7 @@ import {
87
94
  smoothedRunDelta,
88
95
  trafficSources,
89
96
  usageCounters
90
- } from "./chunk-4KWPOVIT.js";
97
+ } from "./chunk-JW6TQFU7.js";
91
98
  import {
92
99
  AGENT_MEMORY_VALUE_MAX_BYTES,
93
100
  AGENT_PROVIDER_IDS,
@@ -101,6 +108,7 @@ import {
101
108
  CheckScopes,
102
109
  CheckStatuses,
103
110
  CitationStates,
111
+ CodingAgents,
104
112
  DEFAULT_DISCOVERY_PROMOTE_BUCKETS,
105
113
  DEFAULT_DISCOVERY_PROMOTE_COMPETITOR_TYPES,
106
114
  DISCOVERY_PROMOTE_COMPETITOR_CAP,
@@ -115,7 +123,9 @@ import {
115
123
  RunKinds,
116
124
  RunStatuses,
117
125
  RunTriggers,
126
+ SKILL_MANIFEST_FILENAME,
118
127
  SchedulableRunKinds,
128
+ SkillsClients,
119
129
  TrafficEventConfidences,
120
130
  TrafficEventKinds,
121
131
  TrafficEvidenceKinds,
@@ -158,7 +168,9 @@ import {
158
168
  citationStateSchema,
159
169
  citationStateToCited,
160
170
  citationVisibilityResponseSchema,
171
+ classifySkillFile,
161
172
  clusterByCosine,
173
+ coerceSkillManifest,
162
174
  competitorBatchRequestSchema,
163
175
  competitorDtoSchema,
164
176
  contentActionLabel,
@@ -199,6 +211,18 @@ import {
199
211
  ga4SocialReferralHistoryEntrySchema,
200
212
  ga4StatusDtoSchema,
201
213
  ga4SyncResponseDtoSchema,
214
+ gbpAccountListResponseSchema,
215
+ gbpDailyMetricListResponseSchema,
216
+ gbpDiscoverRequestSchema,
217
+ gbpKeywordImpressionListResponseSchema,
218
+ gbpLocationDtoSchema,
219
+ gbpLocationListResponseSchema,
220
+ gbpLocationSelectionRequestSchema,
221
+ gbpLodgingListResponseSchema,
222
+ gbpPlaceActionListResponseSchema,
223
+ gbpSummaryDtoSchema,
224
+ gbpSyncRequestSchema,
225
+ gbpSyncResponseSchema,
202
226
  getProviderLocationHandling,
203
227
  googleConnectionDtoSchema,
204
228
  gscCoverageSnapshotDtoSchema,
@@ -214,6 +238,7 @@ import {
214
238
  internalError,
215
239
  isAgentProviderId,
216
240
  isBrowserProvider,
241
+ isRetryableHttpError,
217
242
  keywordDtoSchema,
218
243
  keywordGenerateRequestSchema,
219
244
  latestProjectRunDtoSchema,
@@ -236,6 +261,7 @@ import {
236
261
  providerError,
237
262
  queryDtoSchema,
238
263
  queryGenerateRequestSchema,
264
+ quotaExceeded,
239
265
  recommendationExplainRequestSchema,
240
266
  recommendationExplanationDtoSchema,
241
267
  registrableDomain,
@@ -258,6 +284,7 @@ import {
258
284
  scheduleUpsertRequestSchema,
259
285
  serializeRunError,
260
286
  settingsDtoSchema,
287
+ skillsClientSchema,
261
288
  snapshotDiffResponseSchema,
262
289
  snapshotListResponseSchema,
263
290
  snapshotReportSchema,
@@ -277,6 +304,7 @@ import {
277
304
  validationError,
278
305
  visibilityStateFromAnswerMentioned,
279
306
  windowCutoff,
307
+ withRetry,
280
308
  wordpressAuditPageDtoSchema,
281
309
  wordpressBulkMetaResultDtoSchema,
282
310
  wordpressDiffDtoSchema,
@@ -289,7 +317,7 @@ import {
289
317
  wordpressSchemaDeployResultDtoSchema,
290
318
  wordpressSchemaStatusResultDtoSchema,
291
319
  wordpressStatusDtoSchema
292
- } from "./chunk-WFVUZVJD.js";
320
+ } from "./chunk-LPPW7O26.js";
293
321
 
294
322
  // src/telemetry.ts
295
323
  import crypto from "crypto";
@@ -577,11 +605,11 @@ function checkLatestVersionForServer(opts) {
577
605
 
578
606
  // src/server.ts
579
607
  import { createRequire as createRequire4 } from "module";
580
- import crypto35 from "crypto";
581
- import fs15 from "fs";
582
- import path15 from "path";
583
- import { fileURLToPath as fileURLToPath2 } from "url";
584
- import { eq as eq42 } from "drizzle-orm";
608
+ import crypto40 from "crypto";
609
+ import fs16 from "fs";
610
+ import path16 from "path";
611
+ import { fileURLToPath as fileURLToPath3 } from "url";
612
+ import { eq as eq44 } from "drizzle-orm";
585
613
  import Fastify from "fastify";
586
614
 
587
615
  // ../api-routes/src/index.ts
@@ -2029,7 +2057,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2029
2057
  const body = JSON.stringify(payload);
2030
2058
  const isHttps = target.url.protocol === "https:";
2031
2059
  const port = target.url.port ? Number(target.url.port) : isHttps ? 443 : 80;
2032
- const path16 = `${target.url.pathname}${target.url.search}`;
2060
+ const path17 = `${target.url.pathname}${target.url.search}`;
2033
2061
  const headers = {
2034
2062
  "Content-Length": String(Buffer.byteLength(body)),
2035
2063
  "Content-Type": "application/json",
@@ -2045,7 +2073,7 @@ async function deliverWebhook(target, payload, webhookSecret) {
2045
2073
  headers,
2046
2074
  hostname: target.address,
2047
2075
  method: "POST",
2048
- path: path16,
2076
+ path: path17,
2049
2077
  port,
2050
2078
  timeout: REQUEST_TIMEOUT_MS
2051
2079
  };
@@ -3392,9 +3420,9 @@ function buildGaTrafficByPage(db, projectId) {
3392
3420
  }).from(gaTrafficSnapshots).where(eq12(gaTrafficSnapshots.projectId, projectId)).all();
3393
3421
  const map = /* @__PURE__ */ new Map();
3394
3422
  for (const row of rows) {
3395
- const path16 = extractPath(row.landingPage);
3396
- if (!path16) continue;
3397
- map.set(path16, (map.get(path16) ?? 0) + (row.sessions ?? 0));
3423
+ const path17 = extractPath(row.landingPage);
3424
+ if (!path17) continue;
3425
+ map.set(path17, (map.get(path17) ?? 0) + (row.sessions ?? 0));
3398
3426
  }
3399
3427
  return map;
3400
3428
  }
@@ -3589,13 +3617,13 @@ function normalizeDomain(domain) {
3589
3617
  function extractPath(url) {
3590
3618
  if (!url) return "";
3591
3619
  const trimmed = url.trim();
3592
- let path16;
3620
+ let path17;
3593
3621
  try {
3594
- path16 = new URL(trimmed).pathname;
3622
+ path17 = new URL(trimmed).pathname;
3595
3623
  } catch {
3596
- path16 = trimmed;
3624
+ path17 = trimmed;
3597
3625
  }
3598
- const stripped = path16.replace(/\/+$/, "");
3626
+ const stripped = path17.replace(/\/+$/, "");
3599
3627
  return stripped || "/";
3600
3628
  }
3601
3629
 
@@ -3867,9 +3895,9 @@ function inferAdSource(params) {
3867
3895
  function formatLandingPageHtml(raw) {
3868
3896
  const value = raw ?? "";
3869
3897
  const queryIdx = value.indexOf("?");
3870
- const path16 = queryIdx === -1 ? value : value.slice(0, queryIdx);
3898
+ const path17 = queryIdx === -1 ? value : value.slice(0, queryIdx);
3871
3899
  const query = queryIdx === -1 ? "" : value.slice(queryIdx + 1);
3872
- const pathHtml = `<span class="page-path">${escapeHtml(path16 || "/")}</span>`;
3900
+ const pathHtml = `<span class="page-path">${escapeHtml(path17 || "/")}</span>`;
3873
3901
  if (!query) return pathHtml;
3874
3902
  let summary = "";
3875
3903
  try {
@@ -5227,7 +5255,7 @@ function renderLineChart(points, color, title, height = 200) {
5227
5255
  y: padY + usableH - p.y / max * usableH,
5228
5256
  raw: p
5229
5257
  }));
5230
- const path16 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
5258
+ const path17 = xy.map((p, i) => `${i === 0 ? "M" : "L"} ${p.x.toFixed(1)} ${p.y.toFixed(1)}`).join(" ");
5231
5259
  const dots = xy.map((p) => `<circle cx="${p.x.toFixed(1)}" cy="${p.y.toFixed(1)}" r="3" fill="${color}" />`).join("");
5232
5260
  const xLabels = xy.map((p, i) => {
5233
5261
  if (points.length > 8 && i % Math.ceil(points.length / 6) !== 0 && i !== points.length - 1) return "";
@@ -5239,7 +5267,7 @@ function renderLineChart(points, color, title, height = 200) {
5239
5267
  <line x1="${padX}" y1="${padY + usableH}" x2="${padX + usableW}" y2="${padY + usableH}" stroke="${COLORS.border}" stroke-width="1" />
5240
5268
  <text x="${padX - 6}" y="${(padY + 4).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">${formatNumber(max)}</text>
5241
5269
  <text x="${padX - 6}" y="${(padY + usableH).toFixed(1)}" fill="${COLORS.textFaint}" font-size="9" text-anchor="end">0</text>
5242
- <path d="${path16}" stroke="${color}" stroke-width="2" fill="none" />
5270
+ <path d="${path17}" stroke="${color}" stroke-width="2" fill="none" />
5243
5271
  ${dots}
5244
5272
  ${xLabels}
5245
5273
  </svg>
@@ -8332,6 +8360,15 @@ var SCHEMA_TABLE = {
8332
8360
  GA4SocialReferralHistoryEntry: ga4SocialReferralHistoryEntrySchema,
8333
8361
  GA4StatusDto: ga4StatusDtoSchema,
8334
8362
  GA4SyncResponseDto: ga4SyncResponseDtoSchema,
8363
+ GbpAccountListResponse: gbpAccountListResponseSchema,
8364
+ GbpDailyMetricListResponse: gbpDailyMetricListResponseSchema,
8365
+ GbpKeywordImpressionListResponse: gbpKeywordImpressionListResponseSchema,
8366
+ GbpLocationDto: gbpLocationDtoSchema,
8367
+ GbpLocationListResponse: gbpLocationListResponseSchema,
8368
+ GbpLodgingListResponse: gbpLodgingListResponseSchema,
8369
+ GbpPlaceActionListResponse: gbpPlaceActionListResponseSchema,
8370
+ GbpSummaryDto: gbpSummaryDtoSchema,
8371
+ GbpSyncResponse: gbpSyncResponseSchema,
8335
8372
  GoogleConnectionDto: googleConnectionDtoSchema,
8336
8373
  GscCoverageSnapshotDto: gscCoverageSnapshotDtoSchema,
8337
8374
  GscCoverageSummaryDto: gscCoverageSummaryDtoSchema,
@@ -8435,7 +8472,7 @@ var booleanSchema = { type: "boolean" };
8435
8472
  var integerSchema = { type: "integer" };
8436
8473
  var objectSchema = { type: "object", additionalProperties: true };
8437
8474
  var stringArraySchema = { type: "array", items: stringSchema };
8438
- var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4"] };
8475
+ var googleConnectionTypeSchema = { type: "string", enum: ["gsc", "ga4", "gbp"] };
8439
8476
  var locationSchema = {
8440
8477
  type: "object",
8441
8478
  required: ["label", "city", "region", "country"],
@@ -10051,6 +10088,196 @@ var routeCatalog = [
10051
10088
  404: errorResponse("Project or connection not found.")
10052
10089
  }
10053
10090
  },
10091
+ {
10092
+ method: "post",
10093
+ path: "/api/v1/projects/{name}/gbp/locations/discover",
10094
+ summary: "Discover Google Business Profile locations and persist selection state",
10095
+ tags: ["gbp"],
10096
+ parameters: [nameParameter],
10097
+ requestBody: {
10098
+ content: {
10099
+ "application/json": {
10100
+ schema: {
10101
+ type: "object",
10102
+ properties: {
10103
+ selectAllNew: booleanSchema,
10104
+ accountName: stringSchema,
10105
+ switchAccount: booleanSchema
10106
+ }
10107
+ }
10108
+ }
10109
+ }
10110
+ },
10111
+ responses: {
10112
+ 200: jsonResponse("List of discovered locations and selection summary returned.", "GbpLocationListResponse"),
10113
+ 400: errorResponse("Invalid discover request, unknown account, account-switch not opted into, or scope/API problem."),
10114
+ 404: errorResponse("Project not found."),
10115
+ 429: errorResponse("GBP API quota exceeded (access form may not be approved).")
10116
+ }
10117
+ },
10118
+ {
10119
+ method: "get",
10120
+ path: "/api/v1/projects/{name}/gbp/accounts",
10121
+ summary: "List Google Business Profile accounts the connected user can access",
10122
+ tags: ["gbp"],
10123
+ parameters: [nameParameter],
10124
+ responses: {
10125
+ 200: jsonResponse("Accounts the OAuth user manages or owns.", "GbpAccountListResponse"),
10126
+ 400: errorResponse("No GBP connection or scope/API problem."),
10127
+ 404: errorResponse("Project not found."),
10128
+ 429: errorResponse("GBP API quota exceeded (access form may not be approved).")
10129
+ }
10130
+ },
10131
+ {
10132
+ method: "get",
10133
+ path: "/api/v1/projects/{name}/gbp/locations",
10134
+ summary: "List Google Business Profile locations + selection state",
10135
+ tags: ["gbp"],
10136
+ parameters: [
10137
+ nameParameter,
10138
+ { in: "query", name: "selected", required: false, description: "Filter to selected=true or selected=false", schema: { type: "string", enum: ["true", "false"] } }
10139
+ ],
10140
+ responses: {
10141
+ 200: jsonResponse("List of locations returned.", "GbpLocationListResponse"),
10142
+ 404: errorResponse("Project not found.")
10143
+ }
10144
+ },
10145
+ {
10146
+ method: "put",
10147
+ path: "/api/v1/projects/{name}/gbp/locations/{locationName}/selection",
10148
+ summary: "Toggle a Google Business Profile location's sync selection",
10149
+ tags: ["gbp"],
10150
+ parameters: [
10151
+ nameParameter,
10152
+ { in: "path", name: "locationName", required: true, schema: stringSchema, description: 'URL-encoded "locations/{n}" resource name' }
10153
+ ],
10154
+ requestBody: {
10155
+ required: true,
10156
+ content: {
10157
+ "application/json": {
10158
+ schema: {
10159
+ type: "object",
10160
+ required: ["selected"],
10161
+ properties: { selected: booleanSchema }
10162
+ }
10163
+ }
10164
+ }
10165
+ },
10166
+ responses: {
10167
+ 200: jsonResponse("Updated location returned.", "GbpLocationDto"),
10168
+ 400: errorResponse("Invalid selection request."),
10169
+ 404: errorResponse("Project or location not found.")
10170
+ }
10171
+ },
10172
+ {
10173
+ method: "delete",
10174
+ path: "/api/v1/projects/{name}/gbp/connection",
10175
+ summary: "Disconnect Google Business Profile and remove discovered locations",
10176
+ tags: ["gbp"],
10177
+ parameters: [nameParameter],
10178
+ responses: {
10179
+ 204: { description: "Disconnected." },
10180
+ 404: errorResponse("Project not found.")
10181
+ }
10182
+ },
10183
+ {
10184
+ method: "post",
10185
+ path: "/api/v1/projects/{name}/gbp/sync",
10186
+ summary: "Trigger a Google Business Profile performance sync (daily metrics + monthly keywords)",
10187
+ tags: ["gbp"],
10188
+ parameters: [nameParameter],
10189
+ requestBody: {
10190
+ content: {
10191
+ "application/json": {
10192
+ schema: {
10193
+ type: "object",
10194
+ properties: {
10195
+ locationNames: stringArraySchema,
10196
+ daysOfMetrics: integerSchema,
10197
+ monthsOfKeywords: integerSchema
10198
+ }
10199
+ }
10200
+ }
10201
+ }
10202
+ },
10203
+ responses: {
10204
+ 200: jsonResponse("Sync run queued.", "GbpSyncResponse"),
10205
+ 400: errorResponse("Invalid sync request or no GBP connection."),
10206
+ 404: errorResponse("Project not found.")
10207
+ }
10208
+ },
10209
+ {
10210
+ method: "get",
10211
+ path: "/api/v1/projects/{name}/gbp/metrics",
10212
+ summary: "List stored Google Business Profile daily performance metrics",
10213
+ tags: ["gbp"],
10214
+ parameters: [
10215
+ nameParameter,
10216
+ { in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema },
10217
+ { in: "query", name: "metric", required: false, description: "Filter to one DailyMetric", schema: stringSchema }
10218
+ ],
10219
+ responses: {
10220
+ 200: jsonResponse("Daily metrics returned.", "GbpDailyMetricListResponse"),
10221
+ 404: errorResponse("Project not found.")
10222
+ }
10223
+ },
10224
+ {
10225
+ method: "get",
10226
+ path: "/api/v1/projects/{name}/gbp/keywords",
10227
+ summary: "List stored Google Business Profile monthly keyword impressions",
10228
+ tags: ["gbp"],
10229
+ parameters: [
10230
+ nameParameter,
10231
+ { in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema },
10232
+ { in: "query", name: "month", required: false, description: "Filter to one YYYY-MM month", schema: stringSchema }
10233
+ ],
10234
+ responses: {
10235
+ 200: jsonResponse("Keyword impressions returned.", "GbpKeywordImpressionListResponse"),
10236
+ 404: errorResponse("Project not found.")
10237
+ }
10238
+ },
10239
+ {
10240
+ method: "get",
10241
+ path: "/api/v1/projects/{name}/gbp/place-actions",
10242
+ summary: "List stored Google Business Profile place action links (booking CTAs)",
10243
+ tags: ["gbp"],
10244
+ parameters: [
10245
+ nameParameter,
10246
+ { in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema }
10247
+ ],
10248
+ responses: {
10249
+ 200: jsonResponse("Place actions returned.", "GbpPlaceActionListResponse"),
10250
+ 404: errorResponse("Project not found.")
10251
+ }
10252
+ },
10253
+ {
10254
+ method: "get",
10255
+ path: "/api/v1/projects/{name}/gbp/lodging",
10256
+ summary: "List latest Google Business Profile lodging snapshots per location",
10257
+ tags: ["gbp"],
10258
+ parameters: [
10259
+ nameParameter,
10260
+ { in: "query", name: "locationName", required: false, description: "Filter to one location resource name", schema: stringSchema }
10261
+ ],
10262
+ responses: {
10263
+ 200: jsonResponse("Lodging snapshots returned.", "GbpLodgingListResponse"),
10264
+ 404: errorResponse("Project not found.")
10265
+ }
10266
+ },
10267
+ {
10268
+ method: "get",
10269
+ path: "/api/v1/projects/{name}/gbp/summary",
10270
+ summary: "Composite Google Business Profile local-AEO summary (all derived metrics)",
10271
+ tags: ["gbp"],
10272
+ parameters: [
10273
+ nameParameter,
10274
+ { in: "query", name: "locationName", required: false, description: "Scope to one location (omit = aggregate across selected)", schema: stringSchema }
10275
+ ],
10276
+ responses: {
10277
+ 200: jsonResponse("Summary returned.", "GbpSummaryDto"),
10278
+ 404: errorResponse("Project not found.")
10279
+ }
10280
+ },
10054
10281
  {
10055
10282
  method: "post",
10056
10283
  path: "/api/v1/projects/{name}/bing/connect",
@@ -11928,8 +12155,8 @@ async function openApiRoutes(app, opts = {}) {
11928
12155
  return reply.type("application/json").send(buildOpenApiDocument(opts));
11929
12156
  });
11930
12157
  }
11931
- function buildOperationId(method, path16) {
11932
- const parts = path16.split("/").filter(Boolean).map((part) => {
12158
+ function buildOperationId(method, path17) {
12159
+ const parts = path17.split("/").filter(Boolean).map((part) => {
11933
12160
  if (part.startsWith("{") && part.endsWith("}")) {
11934
12161
  return `by-${part.slice(1, -1)}`;
11935
12162
  }
@@ -12368,8 +12595,89 @@ function formatNotification(row) {
12368
12595
  }
12369
12596
 
12370
12597
  // ../api-routes/src/google.ts
12371
- import crypto15 from "crypto";
12372
- import { eq as eq19, and as and13, desc as desc9, sql as sql7 } from "drizzle-orm";
12598
+ import crypto16 from "crypto";
12599
+ import { eq as eq19, and as and13, desc as desc9, sql as sql7, inArray as inArray9 } from "drizzle-orm";
12600
+
12601
+ // ../api-routes/src/gbp-summary.ts
12602
+ function computeMetricTotals(rows) {
12603
+ const totals = {};
12604
+ for (const row of rows) {
12605
+ totals[row.metric] = (totals[row.metric] ?? 0) + row.value;
12606
+ }
12607
+ return totals;
12608
+ }
12609
+ function shiftDate(date, days) {
12610
+ const [y, m, d] = date.split("-").map(Number);
12611
+ const dt = new Date(Date.UTC(y, m - 1, d));
12612
+ dt.setUTCDate(dt.getUTCDate() + days);
12613
+ const yy = dt.getUTCFullYear();
12614
+ const mm = String(dt.getUTCMonth() + 1).padStart(2, "0");
12615
+ const dd = String(dt.getUTCDate()).padStart(2, "0");
12616
+ return `${yy}-${mm}-${dd}`;
12617
+ }
12618
+ function roundPct(numerator, denominator) {
12619
+ if (denominator === 0) return 0;
12620
+ return Math.round(numerator / denominator * 100);
12621
+ }
12622
+ function computeWindowDelta(rows, referenceDate) {
12623
+ const recentLow = shiftDate(referenceDate, -7);
12624
+ const priorLow = shiftDate(referenceDate, -14);
12625
+ const recent7d = {};
12626
+ const prior7d = {};
12627
+ for (const row of rows) {
12628
+ if (row.date > recentLow && row.date <= referenceDate) {
12629
+ recent7d[row.metric] = (recent7d[row.metric] ?? 0) + row.value;
12630
+ } else if (row.date > priorLow && row.date <= recentLow) {
12631
+ prior7d[row.metric] = (prior7d[row.metric] ?? 0) + row.value;
12632
+ }
12633
+ }
12634
+ const deltaPct = {};
12635
+ const metrics = /* @__PURE__ */ new Set([...Object.keys(recent7d), ...Object.keys(prior7d)]);
12636
+ for (const metric of metrics) {
12637
+ const recent = recent7d[metric] ?? 0;
12638
+ const prior = prior7d[metric] ?? 0;
12639
+ recent7d[metric] = recent;
12640
+ prior7d[metric] = prior;
12641
+ deltaPct[metric] = prior === 0 ? null : Math.round((recent - prior) / prior * 100);
12642
+ }
12643
+ return { recent7d, prior7d, deltaPct };
12644
+ }
12645
+ function computeKeywordCoverage(rows) {
12646
+ const total = rows.length;
12647
+ const thresholdedCount = rows.filter((r) => r.valueCount === null).length;
12648
+ return { total, thresholdedCount, thresholdedPct: roundPct(thresholdedCount, total) };
12649
+ }
12650
+ function summarizePlaceActions(rows) {
12651
+ return {
12652
+ total: rows.length,
12653
+ hasReservationCta: rows.some((r) => r.placeActionType === "RESERVATION"),
12654
+ hasBookingCta: rows.some((r) => r.placeActionType === "BOOK"),
12655
+ hasDirectMerchantCta: rows.some((r) => r.providerType === "MERCHANT")
12656
+ };
12657
+ }
12658
+ function summarizeLodging(rows) {
12659
+ const populatedLodgingCount = rows.filter((r) => r.populatedGroupCount > 0).length;
12660
+ return {
12661
+ lodgingLocationCount: rows.length,
12662
+ populatedLodgingCount,
12663
+ emptyLodgingCount: rows.length - populatedLodgingCount
12664
+ };
12665
+ }
12666
+ function buildGbpSummary(input) {
12667
+ const window = computeWindowDelta(input.dailyMetrics, input.referenceDate);
12668
+ return {
12669
+ scope: { locationName: input.locationName, locationCount: input.locationCount },
12670
+ performance: {
12671
+ totals: computeMetricTotals(input.dailyMetrics),
12672
+ recent7d: window.recent7d,
12673
+ prior7d: window.prior7d,
12674
+ deltaPct: window.deltaPct
12675
+ },
12676
+ keywords: computeKeywordCoverage(input.keywords),
12677
+ placeActions: summarizePlaceActions(input.placeActions),
12678
+ lodging: summarizeLodging(input.lodging)
12679
+ };
12680
+ }
12373
12681
 
12374
12682
  // ../integration-google/src/constants.ts
12375
12683
  var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
@@ -12923,27 +13231,34 @@ function isRetryableGa4Error(err) {
12923
13231
  return false;
12924
13232
  }
12925
13233
  async function withGa4Retry(fn, errLabel) {
12926
- let lastError;
12927
- for (let attempt = 0; attempt <= GA4_MAX_RETRIES; attempt++) {
12928
- try {
12929
- return await fn();
12930
- } catch (err) {
12931
- lastError = err;
12932
- if (attempt >= GA4_MAX_RETRIES || !isRetryableGa4Error(err)) throw err;
13234
+ return withRetry(fn, {
13235
+ maxRetries: GA4_MAX_RETRIES,
13236
+ baseDelayMs: GA4_INITIAL_RETRY_DELAY_MS,
13237
+ // Retry loop predates jittered backoff; preserve the deterministic
13238
+ // schedule operators have been seeing in production until we have a
13239
+ // reason to introduce variance.
13240
+ jitter: false,
13241
+ isRetryable: isRetryableGa4Error,
13242
+ // Honor `Retry-After` when the server supplies it; fall back to the
13243
+ // computed exponential delay otherwise.
13244
+ computeDelayMs: (_attempt, err, defaultMs) => {
13245
+ const ga4Err = err;
13246
+ if (ga4Err?.retryAfterSeconds !== void 0) {
13247
+ return Math.max(0, ga4Err.retryAfterSeconds * 1e3);
13248
+ }
13249
+ return defaultMs;
13250
+ },
13251
+ onRetry: ({ attempt, err, delayMs }) => {
12933
13252
  const ga4Err = err;
12934
- const computedDelayMs = GA4_INITIAL_RETRY_DELAY_MS * Math.pow(2, attempt);
12935
- const delayMs = ga4Err.retryAfterSeconds !== void 0 ? Math.max(0, ga4Err.retryAfterSeconds * 1e3) : computedDelayMs;
12936
13253
  ga4Log("info", `${errLabel}.retry`, {
12937
13254
  attempt: attempt + 1,
12938
13255
  maxAttempts: GA4_MAX_RETRIES + 1,
12939
- status: ga4Err.status,
13256
+ status: ga4Err?.status,
12940
13257
  delayMs,
12941
- usedRetryAfter: ga4Err.retryAfterSeconds !== void 0
13258
+ usedRetryAfter: ga4Err?.retryAfterSeconds !== void 0
12942
13259
  });
12943
- await new Promise((resolve) => setTimeout(resolve, delayMs));
12944
13260
  }
12945
- }
12946
- throw lastError;
13261
+ });
12947
13262
  }
12948
13263
  async function runReport(accessToken, propertyId, request) {
12949
13264
  validatePropertyId(propertyId);
@@ -13430,9 +13745,307 @@ async function fetchSocialReferrals(accessToken, propertyId, days) {
13430
13745
  return rows;
13431
13746
  }
13432
13747
 
13748
+ // ../integration-google-business-profile/src/constants.ts
13749
+ var GBP_SCOPE = "https://www.googleapis.com/auth/business.manage";
13750
+ var GBP_ACCOUNT_MANAGEMENT_BASE = "https://mybusinessaccountmanagement.googleapis.com/v1";
13751
+ var GBP_BUSINESS_INFO_BASE = "https://mybusinessbusinessinformation.googleapis.com/v1";
13752
+ var GBP_PERFORMANCE_BASE = "https://businessprofileperformance.googleapis.com/v1";
13753
+ var GBP_LODGING_BASE = "https://mybusinesslodging.googleapis.com/v1";
13754
+ var GBP_DAILY_METRICS = [
13755
+ "BUSINESS_IMPRESSIONS_DESKTOP_MAPS",
13756
+ "BUSINESS_IMPRESSIONS_DESKTOP_SEARCH",
13757
+ "BUSINESS_IMPRESSIONS_MOBILE_MAPS",
13758
+ "BUSINESS_IMPRESSIONS_MOBILE_SEARCH",
13759
+ "BUSINESS_CONVERSATIONS",
13760
+ "BUSINESS_DIRECTION_REQUESTS",
13761
+ "CALL_CLICKS",
13762
+ "WEBSITE_CLICKS",
13763
+ "BUSINESS_BOOKINGS",
13764
+ "BUSINESS_FOOD_ORDERS",
13765
+ "BUSINESS_FOOD_MENU_CLICKS"
13766
+ ];
13767
+ var GBP_REQUEST_TIMEOUT_MS = 3e4;
13768
+ var GBP_DEFAULT_PAGE_SIZE = 100;
13769
+ var GBP_MAX_PAGES = 200;
13770
+ var GBP_LOCATIONS_DEFAULT_READ_MASK = [
13771
+ "name",
13772
+ "title",
13773
+ "storefrontAddress",
13774
+ "websiteUri",
13775
+ "categories.primaryCategory.displayName"
13776
+ ].join(",");
13777
+
13778
+ // ../integration-google-business-profile/src/types.ts
13779
+ var GbpApiError = class extends Error {
13780
+ status;
13781
+ reason;
13782
+ body;
13783
+ quotaLimitValue;
13784
+ constructor(message, status, reason, body, quotaLimitValue = null) {
13785
+ super(message);
13786
+ this.name = "GbpApiError";
13787
+ this.status = status;
13788
+ this.reason = reason;
13789
+ this.body = body;
13790
+ this.quotaLimitValue = quotaLimitValue;
13791
+ }
13792
+ };
13793
+
13794
+ // ../integration-google-business-profile/src/http.ts
13795
+ function extractReason(body) {
13796
+ if (!body || typeof body !== "object") return null;
13797
+ const payload = body;
13798
+ const details = payload.error?.details;
13799
+ if (!Array.isArray(details)) return null;
13800
+ for (const detail of details) {
13801
+ if (detail && typeof detail.reason === "string") return detail.reason;
13802
+ }
13803
+ return null;
13804
+ }
13805
+ function extractQuotaLimitValue(body) {
13806
+ if (!body || typeof body !== "object") return null;
13807
+ const payload = body;
13808
+ const details = payload.error?.details;
13809
+ if (!Array.isArray(details)) return null;
13810
+ for (const detail of details) {
13811
+ const raw = detail?.metadata?.quota_limit_value;
13812
+ if (typeof raw === "string") {
13813
+ const parsed = Number(raw);
13814
+ if (Number.isFinite(parsed)) return parsed;
13815
+ }
13816
+ }
13817
+ return null;
13818
+ }
13819
+ function isRetryable(err) {
13820
+ if (!(err instanceof GbpApiError)) return false;
13821
+ if (err.status === 429) {
13822
+ return err.quotaLimitValue !== 0;
13823
+ }
13824
+ if (err.status === 503) return true;
13825
+ return false;
13826
+ }
13827
+ async function gbpFetchOnce(url, accessToken, opts) {
13828
+ const controller = new AbortController();
13829
+ const timeout = setTimeout(() => controller.abort(), GBP_REQUEST_TIMEOUT_MS);
13830
+ try {
13831
+ const headers = {
13832
+ authorization: `Bearer ${accessToken}`,
13833
+ accept: "application/json"
13834
+ };
13835
+ if (opts.quotaProject) headers["x-goog-user-project"] = opts.quotaProject;
13836
+ const res = await fetch(url, { method: "GET", headers, signal: controller.signal });
13837
+ const text = await res.text();
13838
+ let body;
13839
+ try {
13840
+ body = text ? JSON.parse(text) : void 0;
13841
+ } catch {
13842
+ body = text;
13843
+ }
13844
+ if (!res.ok) {
13845
+ const payload = body;
13846
+ const message = typeof payload === "object" && payload?.error?.message ? payload.error.message : typeof payload === "string" ? payload : `HTTP ${res.status}`;
13847
+ throw new GbpApiError(message, res.status, extractReason(body), body, extractQuotaLimitValue(body));
13848
+ }
13849
+ return body;
13850
+ } finally {
13851
+ clearTimeout(timeout);
13852
+ }
13853
+ }
13854
+ async function gbpFetchGet(url, accessToken, opts = {}) {
13855
+ return withRetry(() => gbpFetchOnce(url, accessToken, opts), {
13856
+ maxRetries: opts.retry?.maxRetries ?? 5,
13857
+ baseDelayMs: opts.retry?.baseDelayMs ?? 1e3,
13858
+ jitter: true,
13859
+ isRetryable,
13860
+ sleep: opts.retry?.sleep
13861
+ });
13862
+ }
13863
+
13864
+ // ../integration-google-business-profile/src/accounts-client.ts
13865
+ async function listAccounts(accessToken, opts = {}) {
13866
+ const collected = [];
13867
+ let pageToken;
13868
+ let page = 0;
13869
+ do {
13870
+ const url = new URL(`${GBP_ACCOUNT_MANAGEMENT_BASE}/accounts`);
13871
+ url.searchParams.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
13872
+ if (pageToken) url.searchParams.set("pageToken", pageToken);
13873
+ const res = await gbpFetchGet(url.toString(), accessToken, opts);
13874
+ if (res.accounts) collected.push(...res.accounts);
13875
+ pageToken = res.nextPageToken;
13876
+ page++;
13877
+ } while (pageToken && page < GBP_MAX_PAGES);
13878
+ return collected;
13879
+ }
13880
+
13881
+ // ../integration-google-business-profile/src/locations-client.ts
13882
+ async function listLocations(accessToken, accountName, opts = {}) {
13883
+ const readMask = opts.readMask ?? GBP_LOCATIONS_DEFAULT_READ_MASK;
13884
+ const collected = [];
13885
+ let pageToken;
13886
+ let page = 0;
13887
+ do {
13888
+ const url = new URL(`${GBP_BUSINESS_INFO_BASE}/${accountName}/locations`);
13889
+ url.searchParams.set("readMask", readMask);
13890
+ url.searchParams.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
13891
+ if (pageToken) url.searchParams.set("pageToken", pageToken);
13892
+ const res = await gbpFetchGet(url.toString(), accessToken, opts);
13893
+ if (res.locations) collected.push(...res.locations);
13894
+ pageToken = res.nextPageToken;
13895
+ page++;
13896
+ } while (pageToken && page < GBP_MAX_PAGES);
13897
+ return collected;
13898
+ }
13899
+ function formatStorefrontAddress(loc) {
13900
+ const addr = loc.storefrontAddress;
13901
+ if (!addr) return null;
13902
+ const parts = [
13903
+ ...addr.addressLines ?? [],
13904
+ addr.locality,
13905
+ addr.administrativeArea,
13906
+ addr.postalCode,
13907
+ addr.regionCode
13908
+ ].filter((p) => Boolean(p));
13909
+ return parts.length ? parts.join(", ") : null;
13910
+ }
13911
+
13912
+ // ../integration-google-business-profile/src/performance-client.ts
13913
+ function pad2(n) {
13914
+ return String(n).padStart(2, "0");
13915
+ }
13916
+ function formatGoogleDate(d) {
13917
+ return `${d.year}-${pad2(d.month)}-${pad2(d.day)}`;
13918
+ }
13919
+ async function fetchDailyMetrics(accessToken, locationName, opts) {
13920
+ const params = new URLSearchParams();
13921
+ for (const m of opts.metrics) params.append("dailyMetrics", m);
13922
+ params.set("dailyRange.startDate.year", String(opts.startDate.getUTCFullYear()));
13923
+ params.set("dailyRange.startDate.month", String(opts.startDate.getUTCMonth() + 1));
13924
+ params.set("dailyRange.startDate.day", String(opts.startDate.getUTCDate()));
13925
+ params.set("dailyRange.endDate.year", String(opts.endDate.getUTCFullYear()));
13926
+ params.set("dailyRange.endDate.month", String(opts.endDate.getUTCMonth() + 1));
13927
+ params.set("dailyRange.endDate.day", String(opts.endDate.getUTCDate()));
13928
+ const url = `${GBP_PERFORMANCE_BASE}/${locationName}:fetchMultiDailyMetricsTimeSeries?${params.toString()}`;
13929
+ const res = await gbpFetchGet(url, accessToken, opts);
13930
+ const rows = [];
13931
+ const series = res.multiDailyMetricTimeSeries?.[0]?.dailyMetricTimeSeries ?? [];
13932
+ for (const s of series) {
13933
+ for (const dv of s.timeSeries?.datedValues ?? []) {
13934
+ rows.push({
13935
+ metric: s.dailyMetric,
13936
+ date: formatGoogleDate(dv.date),
13937
+ // Omitted value = zero traffic that day.
13938
+ value: dv.value !== void 0 ? Number(dv.value) : 0
13939
+ });
13940
+ }
13941
+ }
13942
+ return rows;
13943
+ }
13944
+ async function listMonthlyKeywords(accessToken, locationName, opts) {
13945
+ const collected = [];
13946
+ let pageToken;
13947
+ let page = 0;
13948
+ do {
13949
+ const params = new URLSearchParams();
13950
+ params.set("monthlyRange.startMonth.year", String(opts.startMonth.year));
13951
+ params.set("monthlyRange.startMonth.month", String(opts.startMonth.month));
13952
+ params.set("monthlyRange.endMonth.year", String(opts.endMonth.year));
13953
+ params.set("monthlyRange.endMonth.month", String(opts.endMonth.month));
13954
+ params.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
13955
+ if (pageToken) params.set("pageToken", pageToken);
13956
+ const url = `${GBP_PERFORMANCE_BASE}/${locationName}/searchkeywords/impressions/monthly?${params.toString()}`;
13957
+ const res = await gbpFetchGet(url, accessToken, opts);
13958
+ for (const c of res.searchKeywordsCounts ?? []) {
13959
+ const value = c.insightsValue?.value;
13960
+ const threshold = c.insightsValue?.threshold;
13961
+ collected.push({
13962
+ keyword: c.searchKeyword,
13963
+ valueCount: value !== void 0 ? Number(value) : null,
13964
+ valueThreshold: value === void 0 && threshold !== void 0 ? Number(threshold) : null
13965
+ });
13966
+ }
13967
+ pageToken = res.nextPageToken;
13968
+ page++;
13969
+ } while (pageToken && page < GBP_MAX_PAGES);
13970
+ return collected;
13971
+ }
13972
+
13973
+ // ../integration-google-business-profile/src/place-actions-client.ts
13974
+ async function listPlaceActionLinks(accessToken, locationName, opts = {}) {
13975
+ const collected = [];
13976
+ let pageToken;
13977
+ let page = 0;
13978
+ do {
13979
+ const url = new URL(`${GBP_BUSINESS_INFO_BASE}/${locationName}/placeActionLinks`);
13980
+ url.searchParams.set("pageSize", String(GBP_DEFAULT_PAGE_SIZE));
13981
+ if (pageToken) url.searchParams.set("pageToken", pageToken);
13982
+ const res = await gbpFetchGet(url.toString(), accessToken, opts);
13983
+ for (const link of res.placeActionLinks ?? []) {
13984
+ collected.push({
13985
+ placeActionLinkName: link.name,
13986
+ placeActionType: link.placeActionType,
13987
+ uri: link.uri ?? null,
13988
+ isPreferred: link.isPreferred ?? false,
13989
+ providerType: link.providerType ?? null
13990
+ });
13991
+ }
13992
+ pageToken = res.nextPageToken;
13993
+ page++;
13994
+ } while (pageToken && page < GBP_MAX_PAGES);
13995
+ return collected;
13996
+ }
13997
+
13998
+ // ../integration-google-business-profile/src/lodging-client.ts
13999
+ import crypto15 from "crypto";
14000
+ async function getLodging(accessToken, locationName, opts = {}) {
14001
+ const url = `${GBP_LODGING_BASE}/${locationName}/lodging?readMask=*`;
14002
+ try {
14003
+ return await gbpFetchGet(url, accessToken, opts);
14004
+ } catch (err) {
14005
+ if (err instanceof GbpApiError && err.status === 400) {
14006
+ return null;
14007
+ }
14008
+ throw err;
14009
+ }
14010
+ }
14011
+ function countPopulatedGroups(lodging) {
14012
+ let count = 0;
14013
+ for (const [key, value] of Object.entries(lodging)) {
14014
+ if (key === "name" || key === "metadata") continue;
14015
+ if (isPopulated(value)) count++;
14016
+ }
14017
+ return count;
14018
+ }
14019
+ function isPopulated(value) {
14020
+ if (value === null || value === void 0) return false;
14021
+ if (Array.isArray(value)) return value.length > 0;
14022
+ if (typeof value === "object") return Object.keys(value).length > 0;
14023
+ return true;
14024
+ }
14025
+ function hashLodging(lodging) {
14026
+ return crypto15.createHash("sha256").update(stableStringify(lodging)).digest("hex");
14027
+ }
14028
+ function stableStringify(value) {
14029
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
14030
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
14031
+ const obj = value;
14032
+ const keys = Object.keys(obj).sort();
14033
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
14034
+ }
14035
+
13433
14036
  // ../api-routes/src/google.ts
14037
+ function scopesForConnectionType(type) {
14038
+ switch (type) {
14039
+ case "gsc":
14040
+ return [GSC_SCOPE, INDEXING_SCOPE];
14041
+ case "ga4":
14042
+ return [GA4_SCOPE];
14043
+ case "gbp":
14044
+ return [GBP_SCOPE];
14045
+ }
14046
+ }
13434
14047
  function signState(payload, secret) {
13435
- return crypto15.createHmac("sha256", secret).update(payload).digest("hex");
14048
+ return crypto16.createHmac("sha256", secret).update(payload).digest("hex");
13436
14049
  }
13437
14050
  function buildSignedState(data, secret) {
13438
14051
  const payload = JSON.stringify(data);
@@ -13443,7 +14056,7 @@ function verifySignedState(encoded, secret) {
13443
14056
  try {
13444
14057
  const { payload, sig } = JSON.parse(Buffer.from(encoded, "base64url").toString());
13445
14058
  const expected = signState(payload, secret);
13446
- if (!crypto15.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
14059
+ if (!crypto16.timingSafeEqual(Buffer.from(sig, "hex"), Buffer.from(expected, "hex"))) return null;
13447
14060
  return JSON.parse(payload);
13448
14061
  } catch {
13449
14062
  return null;
@@ -13524,8 +14137,8 @@ async function googleRoutes(app, opts) {
13524
14137
  throw validationError("Google OAuth is not configured. Set Google OAuth credentials in the local Canonry config.");
13525
14138
  }
13526
14139
  const { type, propertyId, publicUrl } = request.body ?? {};
13527
- if (!type || type !== "gsc" && type !== "ga4") {
13528
- throw validationError('type must be "gsc" or "ga4"');
14140
+ if (!type || type !== "gsc" && type !== "ga4" && type !== "gbp") {
14141
+ throw validationError('type must be "gsc", "ga4", or "gbp"');
13529
14142
  }
13530
14143
  const project = resolveProject(app.db, request.params.name);
13531
14144
  let redirectUri;
@@ -13538,7 +14151,7 @@ async function googleRoutes(app, opts) {
13538
14151
  const host = request.headers.host ?? "localhost:4100";
13539
14152
  redirectUri = `${proto}://${host}${opts.routePrefix ?? "/api/v1"}/projects/${encodeURIComponent(request.params.name)}/google/callback`;
13540
14153
  }
13541
- const scopes = type === "gsc" ? [GSC_SCOPE, INDEXING_SCOPE] : [GA4_SCOPE];
14154
+ const scopes = scopesForConnectionType(type);
13542
14155
  const stateEncoded = buildSignedState(
13543
14156
  {
13544
14157
  projectId: project.id,
@@ -13713,7 +14326,7 @@ async function googleRoutes(app, opts) {
13713
14326
  throw validationError('No GSC connection found for this domain. Run "canonry google connect" first.');
13714
14327
  }
13715
14328
  const now = (/* @__PURE__ */ new Date()).toISOString();
13716
- const runId = crypto15.randomUUID();
14329
+ const runId = crypto16.randomUUID();
13717
14330
  app.db.insert(runs).values({
13718
14331
  id: runId,
13719
14332
  projectId: project.id,
@@ -13806,7 +14419,7 @@ async function googleRoutes(app, opts) {
13806
14419
  const mob = ir.mobileUsabilityResult;
13807
14420
  const rich = ir.richResultsResult;
13808
14421
  const now = (/* @__PURE__ */ new Date()).toISOString();
13809
- const id = crypto15.randomUUID();
14422
+ const id = crypto16.randomUUID();
13810
14423
  app.db.insert(gscUrlInspections).values({
13811
14424
  id,
13812
14425
  projectId: project.id,
@@ -14041,7 +14654,7 @@ async function googleRoutes(app, opts) {
14041
14654
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14042
14655
  });
14043
14656
  const now = (/* @__PURE__ */ new Date()).toISOString();
14044
- const runId = crypto15.randomUUID();
14657
+ const runId = crypto16.randomUUID();
14045
14658
  app.db.insert(runs).values({
14046
14659
  id: runId,
14047
14660
  projectId: project.id,
@@ -14067,7 +14680,7 @@ async function googleRoutes(app, opts) {
14067
14680
  throw validationError("No GSC property configured for this connection");
14068
14681
  }
14069
14682
  const now = (/* @__PURE__ */ new Date()).toISOString();
14070
- const runId = crypto15.randomUUID();
14683
+ const runId = crypto16.randomUUID();
14071
14684
  app.db.insert(runs).values({
14072
14685
  id: runId,
14073
14686
  projectId: project.id,
@@ -14194,10 +14807,385 @@ async function googleRoutes(app, opts) {
14194
14807
  results
14195
14808
  };
14196
14809
  });
14810
+ function gbpErrorToAppError(err, context) {
14811
+ if (err.reason === "ACCESS_TOKEN_SCOPE_INSUFFICIENT") {
14812
+ return validationError(
14813
+ `${context}: OAuth token is missing the business.manage scope. Reconnect with "canonry gbp connect".`
14814
+ );
14815
+ }
14816
+ if (err.reason === "RATE_LIMIT_EXCEEDED" || /quota/i.test(err.message)) {
14817
+ if (err.quotaLimitValue === 0) {
14818
+ return quotaExceeded(
14819
+ "Google Business Profile API (0 QPM \u2014 access form pending approval). See https://support.google.com/business/contact/api_default"
14820
+ );
14821
+ }
14822
+ return quotaExceeded(
14823
+ `Google Business Profile API rate limit exceeded${err.quotaLimitValue ? ` (${err.quotaLimitValue} QPM cap)` : ""}. Retries exhausted; try again shortly.`
14824
+ );
14825
+ }
14826
+ if (err.reason === "API_DISABLED" || err.reason === "CONSUMER_INVALID") {
14827
+ return providerError(
14828
+ `${context}: required Business Profile API is not enabled on the configured GCP project.`,
14829
+ { reason: err.reason, body: err.body }
14830
+ );
14831
+ }
14832
+ if (err.status === 401) return authRequired();
14833
+ return providerError(`${context}: ${err.message}`, { reason: err.reason ?? void 0, status: err.status });
14834
+ }
14835
+ function rowToDto3(row) {
14836
+ return {
14837
+ id: row.id,
14838
+ projectId: row.projectId,
14839
+ accountName: row.accountName,
14840
+ locationName: row.locationName,
14841
+ displayName: row.displayName,
14842
+ primaryCategoryDisplayName: row.primaryCategoryDisplayName ?? null,
14843
+ storefrontAddress: row.storefrontAddress ?? null,
14844
+ websiteUri: row.websiteUri ?? null,
14845
+ selected: Boolean(row.selected),
14846
+ syncedAt: row.syncedAt ?? null,
14847
+ createdAt: row.createdAt,
14848
+ updatedAt: row.updatedAt
14849
+ };
14850
+ }
14851
+ function listSelectionResponse(projectId) {
14852
+ const rows = app.db.select().from(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).all();
14853
+ const dtos = rows.map(rowToDto3);
14854
+ return {
14855
+ locations: dtos,
14856
+ totalDiscovered: dtos.length,
14857
+ totalSelected: dtos.filter((d) => d.selected).length
14858
+ };
14859
+ }
14860
+ function clearGbpProjectData(tx, projectId) {
14861
+ tx.delete(gbpDailyMetrics).where(eq19(gbpDailyMetrics.projectId, projectId)).run();
14862
+ tx.delete(gbpKeywordImpressions).where(eq19(gbpKeywordImpressions.projectId, projectId)).run();
14863
+ tx.delete(gbpPlaceActions).where(eq19(gbpPlaceActions.projectId, projectId)).run();
14864
+ tx.delete(gbpLodgingSnapshots).where(eq19(gbpLodgingSnapshots.projectId, projectId)).run();
14865
+ tx.delete(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).run();
14866
+ }
14867
+ function currentProjectAccount(projectId) {
14868
+ const row = app.db.select({ accountName: gbpLocations.accountName }).from(gbpLocations).where(eq19(gbpLocations.projectId, projectId)).limit(1).get();
14869
+ return row?.accountName ?? null;
14870
+ }
14871
+ app.post("/projects/:name/gbp/locations/discover", async (request) => {
14872
+ const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
14873
+ if (!googleClientId || !googleClientSecret) {
14874
+ throw validationError("Google OAuth is not configured");
14875
+ }
14876
+ const project = resolveProject(app.db, request.params.name);
14877
+ const store = requireConnectionStore();
14878
+ const parsed = gbpDiscoverRequestSchema.safeParse(request.body ?? {});
14879
+ if (!parsed.success) {
14880
+ throw validationError(parsed.error.issues[0]?.message ?? "Invalid discover request");
14881
+ }
14882
+ const { selectAllNew, accountName: requestedAccount, switchAccount } = parsed.data;
14883
+ const { accessToken } = await getValidToken(
14884
+ store,
14885
+ project.canonicalDomain,
14886
+ "gbp",
14887
+ googleClientId,
14888
+ googleClientSecret
14889
+ );
14890
+ const fetchAccounts = async () => {
14891
+ try {
14892
+ return await listAccounts(accessToken);
14893
+ } catch (err) {
14894
+ if (err instanceof GbpApiError) throw gbpErrorToAppError(err, "list accounts");
14895
+ throw err;
14896
+ }
14897
+ };
14898
+ const conn = store.getConnection(project.canonicalDomain, "gbp");
14899
+ const current = currentProjectAccount(project.id);
14900
+ let accountName;
14901
+ if (requestedAccount) {
14902
+ const accounts = await fetchAccounts();
14903
+ if (!accounts.some((a) => a.name === requestedAccount)) {
14904
+ throw validationError(`GBP account "${requestedAccount}" is not accessible to this connection. Run "canonry gbp accounts <project>" to list available accounts.`);
14905
+ }
14906
+ accountName = requestedAccount;
14907
+ } else {
14908
+ const remembered = current ?? conn?.gbpAccountName ?? null;
14909
+ if (remembered) {
14910
+ accountName = remembered;
14911
+ } else {
14912
+ const accounts = await fetchAccounts();
14913
+ if (accounts.length === 0) {
14914
+ throw validationError("No GBP accounts are visible to this OAuth user. Confirm the user has manager/owner access on the target Business Profile.");
14915
+ }
14916
+ accountName = accounts[0].name;
14917
+ }
14918
+ }
14919
+ const switching = current !== null && current !== accountName;
14920
+ if (switching && !switchAccount) {
14921
+ throw validationError(`This project currently tracks GBP account "${current}". Re-pointing it at "${accountName}" would replace its locations and all synced data. Pass switchAccount=true (CLI: --switch-account) to confirm, or run "canonry gbp disconnect <project>" first.`);
14922
+ }
14923
+ let remoteLocations;
14924
+ try {
14925
+ remoteLocations = await listLocations(accessToken, accountName);
14926
+ } catch (err) {
14927
+ if (err instanceof GbpApiError) throw gbpErrorToAppError(err, "list locations");
14928
+ throw err;
14929
+ }
14930
+ store.updateConnection(project.canonicalDomain, "gbp", {
14931
+ gbpAccountName: accountName,
14932
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
14933
+ });
14934
+ const now = (/* @__PURE__ */ new Date()).toISOString();
14935
+ app.db.transaction((tx) => {
14936
+ if (switching) clearGbpProjectData(tx, project.id);
14937
+ for (const remote of remoteLocations) {
14938
+ const existing = tx.select().from(gbpLocations).where(and13(eq19(gbpLocations.projectId, project.id), eq19(gbpLocations.locationName, remote.name))).get();
14939
+ if (existing) {
14940
+ tx.update(gbpLocations).set({
14941
+ accountName,
14942
+ displayName: remote.title ?? existing.displayName,
14943
+ primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
14944
+ storefrontAddress: formatStorefrontAddress(remote),
14945
+ websiteUri: remote.websiteUri ?? null,
14946
+ updatedAt: now
14947
+ }).where(eq19(gbpLocations.id, existing.id)).run();
14948
+ } else {
14949
+ tx.insert(gbpLocations).values({
14950
+ id: crypto16.randomUUID(),
14951
+ projectId: project.id,
14952
+ accountName,
14953
+ locationName: remote.name,
14954
+ displayName: remote.title ?? remote.name,
14955
+ primaryCategoryDisplayName: remote.categories?.primaryCategory?.displayName ?? null,
14956
+ storefrontAddress: formatStorefrontAddress(remote),
14957
+ websiteUri: remote.websiteUri ?? null,
14958
+ selected: selectAllNew,
14959
+ createdAt: now,
14960
+ updatedAt: now
14961
+ }).run();
14962
+ }
14963
+ }
14964
+ writeAuditLog(tx, {
14965
+ projectId: project.id,
14966
+ actor: "api",
14967
+ action: switching ? "gbp.account.switched" : "gbp.locations.discovered",
14968
+ entityType: "gbp_locations",
14969
+ diff: { account: accountName, switchedFrom: switching ? current : null, count: remoteLocations.length, selectAllNew }
14970
+ });
14971
+ });
14972
+ return listSelectionResponse(project.id);
14973
+ });
14974
+ app.get("/projects/:name/gbp/accounts", async (request) => {
14975
+ const { clientId: googleClientId, clientSecret: googleClientSecret } = getAuthConfig();
14976
+ if (!googleClientId || !googleClientSecret) {
14977
+ throw validationError("Google OAuth is not configured");
14978
+ }
14979
+ const project = resolveProject(app.db, request.params.name);
14980
+ const store = requireConnectionStore();
14981
+ const conn = store.getConnection(project.canonicalDomain, "gbp");
14982
+ if (!conn) {
14983
+ throw validationError('No GBP connection found for this project. Run "canonry gbp connect" first.');
14984
+ }
14985
+ const { accessToken } = await getValidToken(
14986
+ store,
14987
+ project.canonicalDomain,
14988
+ "gbp",
14989
+ googleClientId,
14990
+ googleClientSecret
14991
+ );
14992
+ let accounts;
14993
+ try {
14994
+ accounts = await listAccounts(accessToken);
14995
+ } catch (err) {
14996
+ if (err instanceof GbpApiError) throw gbpErrorToAppError(err, "list accounts");
14997
+ throw err;
14998
+ }
14999
+ const response = {
15000
+ accounts: accounts.map((a) => ({
15001
+ name: a.name,
15002
+ accountName: a.accountName ?? null,
15003
+ type: a.type ?? null,
15004
+ role: a.role ?? null
15005
+ })),
15006
+ total: accounts.length
15007
+ };
15008
+ return response;
15009
+ });
15010
+ app.get("/projects/:name/gbp/locations", async (request) => {
15011
+ const project = resolveProject(app.db, request.params.name);
15012
+ const response = listSelectionResponse(project.id);
15013
+ const filter = request.query.selected;
15014
+ if (filter === "true" || filter === "false") {
15015
+ const want = filter === "true";
15016
+ response.locations = response.locations.filter((l) => l.selected === want);
15017
+ }
15018
+ return response;
15019
+ });
15020
+ app.put("/projects/:name/gbp/locations/:locationName/selection", async (request) => {
15021
+ const project = resolveProject(app.db, request.params.name);
15022
+ const locationName = decodeURIComponent(request.params.locationName);
15023
+ const parsed = gbpLocationSelectionRequestSchema.safeParse(request.body ?? {});
15024
+ if (!parsed.success) {
15025
+ throw validationError(parsed.error.issues[0]?.message ?? "Invalid selection request");
15026
+ }
15027
+ const { selected } = parsed.data;
15028
+ const existing = app.db.select().from(gbpLocations).where(and13(eq19(gbpLocations.projectId, project.id), eq19(gbpLocations.locationName, locationName))).get();
15029
+ if (!existing) throw notFound("GBP location", locationName);
15030
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15031
+ app.db.transaction((tx) => {
15032
+ tx.update(gbpLocations).set({ selected, updatedAt: now }).where(eq19(gbpLocations.id, existing.id)).run();
15033
+ writeAuditLog(tx, {
15034
+ projectId: project.id,
15035
+ actor: "api",
15036
+ action: selected ? "gbp.location.selected" : "gbp.location.deselected",
15037
+ entityType: "gbp_location",
15038
+ entityId: locationName
15039
+ });
15040
+ });
15041
+ const refreshed = app.db.select().from(gbpLocations).where(eq19(gbpLocations.id, existing.id)).get();
15042
+ return rowToDto3(refreshed);
15043
+ });
15044
+ app.delete("/projects/:name/gbp/connection", async (request, reply) => {
15045
+ const project = resolveProject(app.db, request.params.name);
15046
+ const store = requireConnectionStore();
15047
+ app.db.transaction((tx) => {
15048
+ clearGbpProjectData(tx, project.id);
15049
+ writeAuditLog(tx, {
15050
+ projectId: project.id,
15051
+ actor: "api",
15052
+ action: "gbp.disconnected",
15053
+ entityType: "gbp_connection"
15054
+ });
15055
+ });
15056
+ store.deleteConnection(project.canonicalDomain, "gbp");
15057
+ return reply.status(204).send();
15058
+ });
15059
+ app.post("/projects/:name/gbp/sync", async (request) => {
15060
+ const project = resolveProject(app.db, request.params.name);
15061
+ const store = requireConnectionStore();
15062
+ const conn = store.getConnection(project.canonicalDomain, "gbp");
15063
+ if (!conn) {
15064
+ throw validationError('No GBP connection found for this project. Run "canonry gbp connect" first.');
15065
+ }
15066
+ const parsed = gbpSyncRequestSchema.safeParse(request.body ?? {});
15067
+ if (!parsed.success) {
15068
+ throw validationError(parsed.error.issues[0]?.message ?? "Invalid sync request");
15069
+ }
15070
+ const now = (/* @__PURE__ */ new Date()).toISOString();
15071
+ const runId = crypto16.randomUUID();
15072
+ app.db.insert(runs).values({
15073
+ id: runId,
15074
+ projectId: project.id,
15075
+ kind: "gbp-sync",
15076
+ status: "queued",
15077
+ trigger: "manual",
15078
+ createdAt: now
15079
+ }).run();
15080
+ opts.onGbpSyncRequested?.(runId, project.id, parsed.data);
15081
+ return { runId, status: "running" };
15082
+ });
15083
+ app.get("/projects/:name/gbp/metrics", async (request) => {
15084
+ const project = resolveProject(app.db, request.params.name);
15085
+ const conditions = [eq19(gbpDailyMetrics.projectId, project.id)];
15086
+ if (request.query.locationName) conditions.push(eq19(gbpDailyMetrics.locationName, request.query.locationName));
15087
+ if (request.query.metric) conditions.push(eq19(gbpDailyMetrics.metric, request.query.metric));
15088
+ const rows = app.db.select().from(gbpDailyMetrics).where(and13(...conditions)).orderBy(desc9(gbpDailyMetrics.date)).all();
15089
+ return {
15090
+ metrics: rows.map((r) => ({ locationName: r.locationName, date: r.date, metric: r.metric, value: r.value })),
15091
+ total: rows.length
15092
+ };
15093
+ });
15094
+ app.get("/projects/:name/gbp/keywords", async (request) => {
15095
+ const project = resolveProject(app.db, request.params.name);
15096
+ const conditions = [eq19(gbpKeywordImpressions.projectId, project.id)];
15097
+ if (request.query.locationName) conditions.push(eq19(gbpKeywordImpressions.locationName, request.query.locationName));
15098
+ const rows = app.db.select().from(gbpKeywordImpressions).where(and13(...conditions)).all();
15099
+ rows.sort((a, b) => (b.valueCount ?? -1) - (a.valueCount ?? -1));
15100
+ const thresholded = rows.filter((r) => r.valueThreshold !== null).length;
15101
+ return {
15102
+ keywords: rows.map((r) => ({
15103
+ locationName: r.locationName,
15104
+ periodStart: r.periodStart,
15105
+ periodEnd: r.periodEnd,
15106
+ keyword: r.keyword,
15107
+ valueCount: r.valueCount ?? null,
15108
+ valueThreshold: r.valueThreshold ?? null
15109
+ })),
15110
+ total: rows.length,
15111
+ thresholdedPct: rows.length ? Math.round(thresholded / rows.length * 100) : 0
15112
+ };
15113
+ });
15114
+ app.get("/projects/:name/gbp/place-actions", async (request) => {
15115
+ const project = resolveProject(app.db, request.params.name);
15116
+ const conditions = [eq19(gbpPlaceActions.projectId, project.id)];
15117
+ if (request.query.locationName) conditions.push(eq19(gbpPlaceActions.locationName, request.query.locationName));
15118
+ const rows = app.db.select().from(gbpPlaceActions).where(and13(...conditions)).all();
15119
+ return {
15120
+ placeActions: rows.map((r) => ({
15121
+ locationName: r.locationName,
15122
+ placeActionLinkName: r.placeActionLinkName,
15123
+ placeActionType: r.placeActionType,
15124
+ uri: r.uri ?? null,
15125
+ isPreferred: Boolean(r.isPreferred),
15126
+ providerType: r.providerType ?? null
15127
+ })),
15128
+ total: rows.length
15129
+ };
15130
+ });
15131
+ app.get("/projects/:name/gbp/lodging", async (request) => {
15132
+ const project = resolveProject(app.db, request.params.name);
15133
+ const conditions = [eq19(gbpLodgingSnapshots.projectId, project.id)];
15134
+ if (request.query.locationName) conditions.push(eq19(gbpLodgingSnapshots.locationName, request.query.locationName));
15135
+ const rows = app.db.select().from(gbpLodgingSnapshots).where(and13(...conditions)).orderBy(desc9(gbpLodgingSnapshots.syncedAt)).all();
15136
+ const latestByLocation = /* @__PURE__ */ new Map();
15137
+ for (const row of rows) {
15138
+ if (!latestByLocation.has(row.locationName)) latestByLocation.set(row.locationName, row);
15139
+ }
15140
+ const lodging = [...latestByLocation.values()].map((r) => ({
15141
+ locationName: r.locationName,
15142
+ populatedGroupCount: r.populatedGroupCount,
15143
+ syncedAt: r.syncedAt,
15144
+ attributes: r.attributes
15145
+ }));
15146
+ return { lodging, total: lodging.length };
15147
+ });
15148
+ app.get("/projects/:name/gbp/summary", async (request) => {
15149
+ const project = resolveProject(app.db, request.params.name);
15150
+ const locationName = request.query.locationName ?? null;
15151
+ const locationNames = locationName ? [locationName] : app.db.select({ n: gbpLocations.locationName }).from(gbpLocations).where(and13(eq19(gbpLocations.projectId, project.id), eq19(gbpLocations.selected, true))).all().map((r) => r.n);
15152
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
15153
+ if (locationNames.length === 0) {
15154
+ return buildGbpSummary({
15155
+ locationName,
15156
+ locationCount: 0,
15157
+ referenceDate: today,
15158
+ dailyMetrics: [],
15159
+ keywords: [],
15160
+ placeActions: [],
15161
+ lodging: []
15162
+ });
15163
+ }
15164
+ const metricRows = app.db.select().from(gbpDailyMetrics).where(and13(eq19(gbpDailyMetrics.projectId, project.id), inArray9(gbpDailyMetrics.locationName, locationNames))).all();
15165
+ const keywordRows = app.db.select().from(gbpKeywordImpressions).where(and13(eq19(gbpKeywordImpressions.projectId, project.id), inArray9(gbpKeywordImpressions.locationName, locationNames))).all();
15166
+ const placeActionRows = app.db.select().from(gbpPlaceActions).where(and13(eq19(gbpPlaceActions.projectId, project.id), inArray9(gbpPlaceActions.locationName, locationNames))).all();
15167
+ const lodgingRows = app.db.select().from(gbpLodgingSnapshots).where(and13(eq19(gbpLodgingSnapshots.projectId, project.id), inArray9(gbpLodgingSnapshots.locationName, locationNames))).orderBy(desc9(gbpLodgingSnapshots.syncedAt)).all();
15168
+ const latestLodgingByLocation = /* @__PURE__ */ new Map();
15169
+ for (const row of lodgingRows) {
15170
+ if (!latestLodgingByLocation.has(row.locationName)) {
15171
+ latestLodgingByLocation.set(row.locationName, { locationName: row.locationName, populatedGroupCount: row.populatedGroupCount });
15172
+ }
15173
+ }
15174
+ const referenceDate = metricRows.reduce((max, r) => r.date > max ? r.date : max, "") || today;
15175
+ return buildGbpSummary({
15176
+ locationName,
15177
+ locationCount: locationNames.length,
15178
+ referenceDate,
15179
+ dailyMetrics: metricRows.map((r) => ({ metric: r.metric, date: r.date, value: r.value })),
15180
+ keywords: keywordRows.map((r) => ({ valueCount: r.valueCount ?? null, valueThreshold: r.valueThreshold ?? null })),
15181
+ placeActions: placeActionRows.map((r) => ({ placeActionType: r.placeActionType, providerType: r.providerType ?? null })),
15182
+ lodging: [...latestLodgingByLocation.values()]
15183
+ });
15184
+ });
14197
15185
  }
14198
15186
 
14199
15187
  // ../api-routes/src/bing.ts
14200
- import crypto16 from "crypto";
15188
+ import crypto17 from "crypto";
14201
15189
  import { eq as eq20, and as and14, desc as desc10 } from "drizzle-orm";
14202
15190
 
14203
15191
  // ../integration-bing/src/constants.ts
@@ -14583,7 +15571,7 @@ async function bingRoutes(app, opts) {
14583
15571
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
14584
15572
  const now = (/* @__PURE__ */ new Date()).toISOString();
14585
15573
  app.db.insert(bingCoverageSnapshots).values({
14586
- id: crypto16.randomUUID(),
15574
+ id: crypto17.randomUUID(),
14587
15575
  projectId: project.id,
14588
15576
  syncRunId: snapshotRunId,
14589
15577
  date: snapshotDate,
@@ -14654,7 +15642,7 @@ async function bingRoutes(app, opts) {
14654
15642
  throw validationError("url is required");
14655
15643
  }
14656
15644
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
14657
- const runId = crypto16.randomUUID();
15645
+ const runId = crypto17.randomUUID();
14658
15646
  app.db.insert(runs).values({
14659
15647
  id: runId,
14660
15648
  projectId: project.id,
@@ -14675,7 +15663,7 @@ async function bingRoutes(app, opts) {
14675
15663
  discoveryDate: result.DiscoveryDate ?? null
14676
15664
  });
14677
15665
  const now = (/* @__PURE__ */ new Date()).toISOString();
14678
- const id = crypto16.randomUUID();
15666
+ const id = crypto17.randomUUID();
14679
15667
  const httpCode = result.HttpStatus ?? result.HttpCode ?? null;
14680
15668
  const lastCrawledDate = parseBingDate(result.LastCrawledDate);
14681
15669
  const inIndexDate = parseBingDate(result.InIndexDate);
@@ -14745,7 +15733,7 @@ async function bingRoutes(app, opts) {
14745
15733
  throw validationError('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
14746
15734
  }
14747
15735
  const now = (/* @__PURE__ */ new Date()).toISOString();
14748
- const runId = crypto16.randomUUID();
15736
+ const runId = crypto17.randomUUID();
14749
15737
  app.db.insert(runs).values({
14750
15738
  id: runId,
14751
15739
  projectId: project.id,
@@ -15029,7 +16017,7 @@ async function cdpRoutes(app, opts) {
15029
16017
  }
15030
16018
 
15031
16019
  // ../api-routes/src/ga.ts
15032
- import crypto17 from "crypto";
16020
+ import crypto18 from "crypto";
15033
16021
  import { eq as eq22, desc as desc11, and as and16, sql as sql8 } from "drizzle-orm";
15034
16022
  function gaLog(level, action, ctx) {
15035
16023
  const entry = { ts: (/* @__PURE__ */ new Date()).toISOString(), level, module: "GA4Routes", action, ...ctx };
@@ -15285,7 +16273,7 @@ async function ga4Routes(app, opts) {
15285
16273
  const syncAi = !only || only === "ai";
15286
16274
  const syncSocial = !only || only === "social";
15287
16275
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
15288
- const runId = crypto17.randomUUID();
16276
+ const runId = crypto18.randomUUID();
15289
16277
  app.db.insert(runs).values({
15290
16278
  id: runId,
15291
16279
  projectId: project.id,
@@ -15333,7 +16321,7 @@ async function ga4Routes(app, opts) {
15333
16321
  ).run();
15334
16322
  for (const row of rows) {
15335
16323
  tx.insert(gaTrafficSnapshots).values({
15336
- id: crypto17.randomUUID(),
16324
+ id: crypto18.randomUUID(),
15337
16325
  projectId: project.id,
15338
16326
  date: row.date,
15339
16327
  landingPage: row.landingPage,
@@ -15357,7 +16345,7 @@ async function ga4Routes(app, opts) {
15357
16345
  ).run();
15358
16346
  for (const row of aiReferrals) {
15359
16347
  tx.insert(gaAiReferrals).values({
15360
- id: crypto17.randomUUID(),
16348
+ id: crypto18.randomUUID(),
15361
16349
  projectId: project.id,
15362
16350
  date: row.date,
15363
16351
  source: row.source,
@@ -15383,7 +16371,7 @@ async function ga4Routes(app, opts) {
15383
16371
  ).run();
15384
16372
  for (const row of socialReferrals) {
15385
16373
  tx.insert(gaSocialReferrals).values({
15386
- id: crypto17.randomUUID(),
16374
+ id: crypto18.randomUUID(),
15387
16375
  projectId: project.id,
15388
16376
  date: row.date,
15389
16377
  source: row.source,
@@ -15399,7 +16387,7 @@ async function ga4Routes(app, opts) {
15399
16387
  if (syncSummary) {
15400
16388
  tx.delete(gaTrafficSummaries).where(eq22(gaTrafficSummaries.projectId, project.id)).run();
15401
16389
  tx.insert(gaTrafficSummaries).values({
15402
- id: crypto17.randomUUID(),
16390
+ id: crypto18.randomUUID(),
15403
16391
  projectId: project.id,
15404
16392
  periodStart: summary.periodStart,
15405
16393
  periodEnd: summary.periodEnd,
@@ -15412,7 +16400,7 @@ async function ga4Routes(app, opts) {
15412
16400
  tx.delete(gaTrafficWindowSummaries).where(eq22(gaTrafficWindowSummaries.projectId, project.id)).run();
15413
16401
  for (const ws of windowSummaries) {
15414
16402
  tx.insert(gaTrafficWindowSummaries).values({
15415
- id: crypto17.randomUUID(),
16403
+ id: crypto18.randomUUID(),
15416
16404
  projectId: project.id,
15417
16405
  windowKey: ws.windowKey,
15418
16406
  periodStart: ws.periodStart,
@@ -15698,7 +16686,7 @@ async function ga4Routes(app, opts) {
15698
16686
  requireGa4Connection(opts, project.name, project.canonicalDomain);
15699
16687
  const today = /* @__PURE__ */ new Date();
15700
16688
  const fmt = (d) => d.toISOString().split("T")[0];
15701
- const daysAgo2 = (n) => {
16689
+ const daysAgo3 = (n) => {
15702
16690
  const d = new Date(today);
15703
16691
  d.setDate(d.getDate() - n);
15704
16692
  return fmt(d);
@@ -15708,17 +16696,17 @@ async function ga4Routes(app, opts) {
15708
16696
  sql8`${gaSocialReferrals.date} >= ${from}`,
15709
16697
  sql8`${gaSocialReferrals.date} < ${to}`
15710
16698
  )).get();
15711
- const current7d = sumSocial(daysAgo2(7), fmt(today));
15712
- const prev7d = sumSocial(daysAgo2(14), daysAgo2(7));
15713
- const current30d = sumSocial(daysAgo2(30), fmt(today));
15714
- const prev30d = sumSocial(daysAgo2(60), daysAgo2(30));
16699
+ const current7d = sumSocial(daysAgo3(7), fmt(today));
16700
+ const prev7d = sumSocial(daysAgo3(14), daysAgo3(7));
16701
+ const current30d = sumSocial(daysAgo3(30), fmt(today));
16702
+ const prev30d = sumSocial(daysAgo3(60), daysAgo3(30));
15715
16703
  const pct = (cur, prev) => prev === 0 ? null : Math.round((cur - prev) / prev * 100);
15716
16704
  const sourceCurrent = app.db.select({
15717
16705
  source: gaSocialReferrals.source,
15718
16706
  sessions: sql8`SUM(${gaSocialReferrals.sessions})`
15719
16707
  }).from(gaSocialReferrals).where(and16(
15720
16708
  eq22(gaSocialReferrals.projectId, project.id),
15721
- sql8`${gaSocialReferrals.date} >= ${daysAgo2(7)}`,
16709
+ sql8`${gaSocialReferrals.date} >= ${daysAgo3(7)}`,
15722
16710
  sql8`${gaSocialReferrals.date} < ${fmt(today)}`
15723
16711
  )).groupBy(gaSocialReferrals.source).all();
15724
16712
  const sourcePrev = app.db.select({
@@ -15726,8 +16714,8 @@ async function ga4Routes(app, opts) {
15726
16714
  sessions: sql8`SUM(${gaSocialReferrals.sessions})`
15727
16715
  }).from(gaSocialReferrals).where(and16(
15728
16716
  eq22(gaSocialReferrals.projectId, project.id),
15729
- sql8`${gaSocialReferrals.date} >= ${daysAgo2(14)}`,
15730
- sql8`${gaSocialReferrals.date} < ${daysAgo2(7)}`
16717
+ sql8`${gaSocialReferrals.date} >= ${daysAgo3(14)}`,
16718
+ sql8`${gaSocialReferrals.date} < ${daysAgo3(7)}`
15731
16719
  )).groupBy(gaSocialReferrals.source).all();
15732
16720
  const prevMap = new Map(sourcePrev.map((r) => [r.source, r.sessions]));
15733
16721
  let biggestMover = null;
@@ -15760,7 +16748,7 @@ async function ga4Routes(app, opts) {
15760
16748
  requireGa4Connection(opts, project.name, project.canonicalDomain);
15761
16749
  const today = /* @__PURE__ */ new Date();
15762
16750
  const fmt = (d) => d.toISOString().split("T")[0];
15763
- const daysAgo2 = (n) => {
16751
+ const daysAgo3 = (n) => {
15764
16752
  const d = new Date(today);
15765
16753
  d.setDate(d.getDate() - n);
15766
16754
  return fmt(d);
@@ -15778,22 +16766,22 @@ async function ga4Routes(app, opts) {
15778
16766
  const sumSocial = (from, to) => app.db.select({ sessions: sql8`COALESCE(SUM(${gaSocialReferrals.sessions}), 0)` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${from}`, sql8`${gaSocialReferrals.date} < ${to}`)).get();
15779
16767
  const todayStr = fmt(today);
15780
16768
  const buildTrend = (sum) => {
15781
- const c7 = sum(daysAgo2(7), todayStr)?.sessions ?? 0;
15782
- const p7 = sum(daysAgo2(14), daysAgo2(7))?.sessions ?? 0;
15783
- const c30 = sum(daysAgo2(30), todayStr)?.sessions ?? 0;
15784
- const p30 = sum(daysAgo2(60), daysAgo2(30))?.sessions ?? 0;
16769
+ const c7 = sum(daysAgo3(7), todayStr)?.sessions ?? 0;
16770
+ const p7 = sum(daysAgo3(14), daysAgo3(7))?.sessions ?? 0;
16771
+ const c30 = sum(daysAgo3(30), todayStr)?.sessions ?? 0;
16772
+ const p30 = sum(daysAgo3(60), daysAgo3(30))?.sessions ?? 0;
15785
16773
  return { sessions7d: c7, sessionsPrev7d: p7, trend7dPct: pct(c7, p7), sessions30d: c30, sessionsPrev30d: p30, trend30dPct: pct(c30, p30) };
15786
16774
  };
15787
16775
  const aiSourceCurrent = app.db.select({ source: gaAiReferrals.source, sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and16(
15788
16776
  eq22(gaAiReferrals.projectId, project.id),
15789
- sql8`${gaAiReferrals.date} >= ${daysAgo2(7)}`,
16777
+ sql8`${gaAiReferrals.date} >= ${daysAgo3(7)}`,
15790
16778
  sql8`${gaAiReferrals.date} < ${todayStr}`,
15791
16779
  eq22(gaAiReferrals.sourceDimension, "session")
15792
16780
  )).groupBy(gaAiReferrals.source).all();
15793
16781
  const aiSourcePrev = app.db.select({ source: gaAiReferrals.source, sessions: sql8`COALESCE(SUM(${gaAiReferrals.sessions}), 0)` }).from(gaAiReferrals).where(and16(
15794
16782
  eq22(gaAiReferrals.projectId, project.id),
15795
- sql8`${gaAiReferrals.date} >= ${daysAgo2(14)}`,
15796
- sql8`${gaAiReferrals.date} < ${daysAgo2(7)}`,
16783
+ sql8`${gaAiReferrals.date} >= ${daysAgo3(14)}`,
16784
+ sql8`${gaAiReferrals.date} < ${daysAgo3(7)}`,
15797
16785
  eq22(gaAiReferrals.sourceDimension, "session")
15798
16786
  )).groupBy(gaAiReferrals.source).all();
15799
16787
  const findBiggestMover = (current, prev) => {
@@ -15810,8 +16798,8 @@ async function ga4Routes(app, opts) {
15810
16798
  }
15811
16799
  return mover;
15812
16800
  };
15813
- const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo2(7)}`, sql8`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
15814
- const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo2(14)}`, sql8`${gaSocialReferrals.date} < ${daysAgo2(7)}`)).groupBy(gaSocialReferrals.source).all();
16801
+ const socialSourceCurrent = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo3(7)}`, sql8`${gaSocialReferrals.date} < ${todayStr}`)).groupBy(gaSocialReferrals.source).all();
16802
+ const socialSourcePrev = app.db.select({ source: gaSocialReferrals.source, sessions: sql8`SUM(${gaSocialReferrals.sessions})` }).from(gaSocialReferrals).where(and16(eq22(gaSocialReferrals.projectId, project.id), sql8`${gaSocialReferrals.date} >= ${daysAgo3(14)}`, sql8`${gaSocialReferrals.date} < ${daysAgo3(7)}`)).groupBy(gaSocialReferrals.source).all();
15815
16803
  return {
15816
16804
  total: buildTrend(sumTotal),
15817
16805
  organic: buildTrend(sumOrganic),
@@ -15983,7 +16971,7 @@ function parseSchemaPageEntry(entry) {
15983
16971
  }
15984
16972
 
15985
16973
  // ../integration-wordpress/src/wordpress-client.ts
15986
- import crypto18 from "crypto";
16974
+ import crypto19 from "crypto";
15987
16975
  function validateUsername(username) {
15988
16976
  if (!username || typeof username !== "string" || username.trim().length === 0) {
15989
16977
  throw new WordpressApiError("AUTH_INVALID", "Username is required and must be a non-empty string", 400);
@@ -16065,10 +17053,10 @@ function buildAuthErrorMessage(res, responseText) {
16065
17053
  }
16066
17054
  return "WordPress credentials are invalid or lack permission for this action";
16067
17055
  }
16068
- async function fetchJson(connection, siteUrl, path16, init) {
17056
+ async function fetchJson(connection, siteUrl, path17, init) {
16069
17057
  if (siteUrl.startsWith("http:")) {
16070
17058
  }
16071
- const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path16}`, {
17059
+ const res = await fetch(`${normalizeSiteUrl(siteUrl)}${path17}`, {
16072
17060
  ...init,
16073
17061
  headers: {
16074
17062
  "Authorization": `Basic ${encodeBasicAuth(connection.username, connection.appPassword)}`,
@@ -16196,7 +17184,7 @@ function buildSnippet(content) {
16196
17184
  return `${text.slice(0, 157)}...`;
16197
17185
  }
16198
17186
  function contentHash(content) {
16199
- return crypto18.createHash("sha256").update(content).digest("hex");
17187
+ return crypto19.createHash("sha256").update(content).digest("hex");
16200
17188
  }
16201
17189
  function buildAmbiguousSlugMessage(slug, pages) {
16202
17190
  const candidates = pages.map((page) => {
@@ -17485,7 +18473,7 @@ async function wordpressRoutes(app, opts) {
17485
18473
  }
17486
18474
 
17487
18475
  // ../api-routes/src/backlinks.ts
17488
- import crypto19 from "crypto";
18476
+ import crypto20 from "crypto";
17489
18477
  import { and as and18, asc as asc2, desc as desc12, eq as eq23, sql as sql9 } from "drizzle-orm";
17490
18478
 
17491
18479
  // ../integration-commoncrawl/src/constants.ts
@@ -18065,7 +19053,7 @@ async function backlinksRoutes(app, opts) {
18065
19053
  const refreshed = app.db.select().from(ccReleaseSyncs).where(eq23(ccReleaseSyncs.id, existing.id)).get();
18066
19054
  return reply.status(200).send(mapSyncRow(refreshed));
18067
19055
  }
18068
- const id = crypto19.randomUUID();
19056
+ const id = crypto20.randomUUID();
18069
19057
  app.db.insert(ccReleaseSyncs).values({
18070
19058
  id,
18071
19059
  release,
@@ -18122,7 +19110,7 @@ async function backlinksRoutes(app, opts) {
18122
19110
  throw validationError("Invalid release id");
18123
19111
  }
18124
19112
  const now = (/* @__PURE__ */ new Date()).toISOString();
18125
- const runId = crypto19.randomUUID();
19113
+ const runId = crypto20.randomUUID();
18126
19114
  app.db.insert(runs).values({
18127
19115
  id: runId,
18128
19116
  projectId: project.id,
@@ -18195,12 +19183,12 @@ async function backlinksRoutes(app, opts) {
18195
19183
  }
18196
19184
 
18197
19185
  // ../api-routes/src/traffic.ts
18198
- import crypto21 from "crypto";
19186
+ import crypto22 from "crypto";
18199
19187
  import { Agent as UndiciAgent } from "undici";
18200
19188
  import { and as and19, desc as desc13, eq as eq24, gte as gte3, lte as lte2, sql as sql10 } from "drizzle-orm";
18201
19189
 
18202
19190
  // ../integration-cloud-run/src/auth.ts
18203
- import crypto20 from "crypto";
19191
+ import crypto21 from "crypto";
18204
19192
  var GOOGLE_TOKEN_URL3 = "https://oauth2.googleapis.com/token";
18205
19193
  var CLOUD_LOGGING_READ_SCOPE = "https://www.googleapis.com/auth/logging.read";
18206
19194
  var TOKEN_REQUEST_TIMEOUT_MS = 3e4;
@@ -18229,7 +19217,7 @@ function createServiceAccountJwt2(clientEmail, privateKey, scope) {
18229
19217
  const headerB64 = encode(header);
18230
19218
  const payloadB64 = encode(payload);
18231
19219
  const signingInput = `${headerB64}.${payloadB64}`;
18232
- const sign = crypto20.createSign("RSA-SHA256");
19220
+ const sign = crypto21.createSign("RSA-SHA256");
18233
19221
  sign.update(signingInput);
18234
19222
  const signature = sign.sign(privateKey, "base64url");
18235
19223
  return `${signingInput}.${signature}`;
@@ -21103,8 +22091,8 @@ var ASSET_PATH_PREFIXES = [
21103
22091
  "/img/",
21104
22092
  "/static/"
21105
22093
  ];
21106
- function normalizeTrafficPathPattern(path16) {
21107
- const cleanPath = path16.trim() || "/";
22094
+ function normalizeTrafficPathPattern(path17) {
22095
+ const cleanPath = path17.trim() || "/";
21108
22096
  const pathOnly = cleanPath.split("?")[0] || "/";
21109
22097
  const segments = pathOnly.split("/").map((segment) => {
21110
22098
  if (!segment) return segment;
@@ -21153,8 +22141,8 @@ function resolveAiReferralLandingPath(event, evidenceType) {
21153
22141
  }
21154
22142
  return normalizeTrafficPathPattern(event.path);
21155
22143
  }
21156
- function isLikelySubresourcePath(path16) {
21157
- const cleanPath = path16.split("?")[0] || "/";
22144
+ function isLikelySubresourcePath(path17) {
22145
+ const cleanPath = path17.split("?")[0] || "/";
21158
22146
  return ASSET_PATH_PREFIXES.some((prefix) => cleanPath.startsWith(prefix)) || ASSET_EXTENSION_PATTERN.test(cleanPath);
21159
22147
  }
21160
22148
  function actorKey(event) {
@@ -21387,11 +22375,11 @@ function buildEventId2(event) {
21387
22375
  function normalizeWordpressTrafficEvent(event) {
21388
22376
  if (!event.observed_at) return null;
21389
22377
  if (typeof event.id !== "number" || !Number.isFinite(event.id)) return null;
21390
- const path16 = event.path?.trim();
21391
- if (!path16) return null;
22378
+ const path17 = event.path?.trim();
22379
+ if (!path17) return null;
21392
22380
  const queryString = trimOrNull(event.query_string);
21393
22381
  const host = trimOrNull(event.host);
21394
- const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : `${path16}${queryString ? `?${queryString}` : ""}`;
22382
+ const requestUrl = host ? `https://${host}${path17}${queryString ? `?${queryString}` : ""}` : `${path17}${queryString ? `?${queryString}` : ""}`;
21395
22383
  return {
21396
22384
  sourceType: TrafficSourceTypes.wordpress,
21397
22385
  evidenceKind: TrafficEvidenceKinds["raw-request"],
@@ -21401,7 +22389,7 @@ function normalizeWordpressTrafficEvent(event) {
21401
22389
  method: trimOrNull(event.method),
21402
22390
  requestUrl,
21403
22391
  host,
21404
- path: path16,
22392
+ path: path17,
21405
22393
  queryString,
21406
22394
  status: typeof event.status === "number" && Number.isFinite(event.status) ? event.status : null,
21407
22395
  userAgent: trimOrNull(event.user_agent),
@@ -21572,15 +22560,15 @@ function stringLabels(input) {
21572
22560
  );
21573
22561
  }
21574
22562
  function normalizeVercelLogRow(row) {
21575
- const path16 = row.requestPath;
21576
- if (!path16) return null;
22563
+ const path17 = row.requestPath;
22564
+ if (!path17) return null;
21577
22565
  const observedAt = row.timestamp;
21578
22566
  if (!observedAt) return null;
21579
22567
  const requestId = row.requestId;
21580
22568
  if (!requestId) return null;
21581
22569
  const host = emptyToNull(row.domain);
21582
22570
  const queryString = serializeSearchParams(row.requestSearchParams);
21583
- const requestUrl = host ? `https://${host}${path16}${queryString ? `?${queryString}` : ""}` : null;
22571
+ const requestUrl = host ? `https://${host}${path17}${queryString ? `?${queryString}` : ""}` : null;
21584
22572
  return {
21585
22573
  sourceType: TrafficSourceTypes.vercel,
21586
22574
  evidenceKind: TrafficEvidenceKinds["raw-request"],
@@ -21590,7 +22578,7 @@ function normalizeVercelLogRow(row) {
21590
22578
  method: row.requestMethod ?? null,
21591
22579
  requestUrl,
21592
22580
  host,
21593
- path: path16,
22581
+ path: path17,
21594
22582
  queryString,
21595
22583
  status: resolveStatus(row),
21596
22584
  userAgent: emptyToNull(row.clientUserAgent),
@@ -22095,7 +23083,7 @@ async function runBackfillTask(options) {
22095
23083
  }
22096
23084
  })();
22097
23085
  tx.insert(rawEventSamples).values({
22098
- id: crypto21.randomUUID(),
23086
+ id: crypto22.randomUUID(),
22099
23087
  projectId: project.id,
22100
23088
  sourceId: sourceRow.id,
22101
23089
  ts: sample.observedAt,
@@ -22220,7 +23208,7 @@ async function trafficRoutes(app, opts) {
22220
23208
  }).where(eq24(trafficSources.id, activeSource.id)).run();
22221
23209
  sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
22222
23210
  } else {
22223
- const newId = crypto21.randomUUID();
23211
+ const newId = crypto22.randomUUID();
22224
23212
  app.db.insert(trafficSources).values({
22225
23213
  id: newId,
22226
23214
  projectId: project.id,
@@ -22301,7 +23289,7 @@ async function trafficRoutes(app, opts) {
22301
23289
  }).where(eq24(trafficSources.id, activeSource.id)).run();
22302
23290
  sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
22303
23291
  } else {
22304
- const newId = crypto21.randomUUID();
23292
+ const newId = crypto22.randomUUID();
22305
23293
  app.db.insert(trafficSources).values({
22306
23294
  id: newId,
22307
23295
  projectId: project.id,
@@ -22384,7 +23372,7 @@ async function trafficRoutes(app, opts) {
22384
23372
  }).where(eq24(trafficSources.id, activeSource.id)).run();
22385
23373
  sourceRow = app.db.select().from(trafficSources).where(eq24(trafficSources.id, activeSource.id)).get();
22386
23374
  } else {
22387
- const newId = crypto21.randomUUID();
23375
+ const newId = crypto22.randomUUID();
22388
23376
  app.db.insert(trafficSources).values({
22389
23377
  id: newId,
22390
23378
  projectId: project.id,
@@ -22432,7 +23420,7 @@ async function trafficRoutes(app, opts) {
22432
23420
  const windowEnd = /* @__PURE__ */ new Date();
22433
23421
  const startedAt = windowEnd.toISOString();
22434
23422
  const syncStartedAtMs = windowEnd.getTime();
22435
- const runId = crypto21.randomUUID();
23423
+ const runId = crypto22.randomUUID();
22436
23424
  app.db.insert(runs).values({
22437
23425
  id: runId,
22438
23426
  projectId: project.id,
@@ -22761,7 +23749,7 @@ async function trafficRoutes(app, opts) {
22761
23749
  }
22762
23750
  })();
22763
23751
  tx.insert(rawEventSamples).values({
22764
- id: crypto21.randomUUID(),
23752
+ id: crypto22.randomUUID(),
22765
23753
  projectId: project.id,
22766
23754
  sourceId: sourceRow.id,
22767
23755
  ts: sample.observedAt,
@@ -22986,7 +23974,7 @@ async function trafficRoutes(app, opts) {
22986
23974
  };
22987
23975
  }
22988
23976
  const startedAt = windowEnd.toISOString();
22989
- const runId = crypto21.randomUUID();
23977
+ const runId = crypto22.randomUUID();
22990
23978
  app.db.insert(runs).values({
22991
23979
  id: runId,
22992
23980
  projectId: project.id,
@@ -23266,6 +24254,7 @@ async function trafficRoutes(app, opts) {
23266
24254
  }
23267
24255
 
23268
24256
  // ../api-routes/src/doctor/checks/agent.ts
24257
+ import crypto23 from "crypto";
23269
24258
  import fs6 from "fs";
23270
24259
  import path7 from "path";
23271
24260
  var REQUIRED_SKILLS = ["canonry", "aero"];
@@ -23324,6 +24313,90 @@ var skillsInstalledCheck = {
23324
24313
  };
23325
24314
  }
23326
24315
  };
24316
+ var skillsCurrentCheck = {
24317
+ id: "agent.skills.current",
24318
+ category: CheckCategories.agent,
24319
+ scope: CheckScopes.global,
24320
+ title: "Agent skills up to date (~/.claude/skills/)",
24321
+ run: (ctx) => {
24322
+ const bundled = ctx.bundledSkills;
24323
+ if (!bundled || bundled.length === 0) {
24324
+ return {
24325
+ status: CheckStatuses.skipped,
24326
+ code: "agent.skills.bundle-unavailable",
24327
+ summary: "Bundled skill snapshot is not available in this deployment \u2014 cannot assess skill drift.",
24328
+ remediation: null
24329
+ };
24330
+ }
24331
+ const home = process.env.HOME;
24332
+ if (!home) {
24333
+ return {
24334
+ status: CheckStatuses.skipped,
24335
+ code: "agent.skills.no-home",
24336
+ summary: "Cannot determine $HOME \u2014 skip skills staleness check.",
24337
+ remediation: null
24338
+ };
24339
+ }
24340
+ const skillsBase = path7.join(home, ".claude", "skills");
24341
+ const bundledVersion = bundled[0].version;
24342
+ const skills = bundled.map((snap) => {
24343
+ const skillDir = path7.join(skillsBase, snap.name);
24344
+ if (!isInstalled(skillDir)) {
24345
+ return { name: snap.name, installed: false, installedVersion: null, missing: [], stale: [], edited: [], behind: false };
24346
+ }
24347
+ const manifest = readInstalledManifest(skillDir);
24348
+ const missing = [];
24349
+ const stale = [];
24350
+ const edited = [];
24351
+ for (const [rel, bundledHash] of Object.entries(snap.files)) {
24352
+ const installedHash = hashInstalledFile(path7.join(skillDir, rel));
24353
+ const state = classifySkillFile({ bundledHash, installedHash, manifestHash: manifest?.files[rel] });
24354
+ if (state === "missing") missing.push(rel);
24355
+ else if (state === "stale") stale.push(rel);
24356
+ else if (state === "edited") edited.push(rel);
24357
+ }
24358
+ return {
24359
+ name: snap.name,
24360
+ installed: true,
24361
+ installedVersion: manifest?.version ?? null,
24362
+ missing,
24363
+ stale,
24364
+ edited,
24365
+ behind: missing.length > 0 || stale.length > 0
24366
+ };
24367
+ });
24368
+ const details = { checkedPath: skillsBase, bundledVersion, skills };
24369
+ const installedSkills = skills.filter((s) => s.installed);
24370
+ if (installedSkills.length === 0) {
24371
+ return {
24372
+ status: CheckStatuses.skipped,
24373
+ code: "agent.skills.none-installed",
24374
+ summary: "No agent skills are installed under ~/.claude/skills/ \u2014 see agent.skills.installed.",
24375
+ remediation: null,
24376
+ details
24377
+ };
24378
+ }
24379
+ const behind = installedSkills.filter((s) => s.behind);
24380
+ if (behind.length === 0) {
24381
+ return {
24382
+ status: CheckStatuses.ok,
24383
+ code: "agent.skills.current",
24384
+ summary: `Installed agent skills are up to date with the bundled version (v${bundledVersion}).`,
24385
+ remediation: null,
24386
+ details
24387
+ };
24388
+ }
24389
+ const newFiles = behind.reduce((n, s) => n + s.missing.length, 0);
24390
+ const updatedFiles = behind.reduce((n, s) => n + s.stale.length, 0);
24391
+ return {
24392
+ status: CheckStatuses.warn,
24393
+ code: "agent.skills.behind",
24394
+ summary: `${behind.map((s) => s.name).join(", ")} ${behind.length === 1 ? "is" : "are"} behind the bundled skill version (v${bundledVersion}): ${newFiles} new file(s), ${updatedFiles} updated file(s) not yet installed.`,
24395
+ remediation: "Run `canonry skills install --user` to additively refresh \u2014 new and upstream-updated files are copied; your local edits are preserved.",
24396
+ details
24397
+ };
24398
+ }
24399
+ };
23327
24400
  function isInstalled(dir) {
23328
24401
  try {
23329
24402
  if (!fs6.existsSync(dir)) return false;
@@ -23332,7 +24405,21 @@ function isInstalled(dir) {
23332
24405
  return false;
23333
24406
  }
23334
24407
  }
23335
- var AGENT_CHECKS = [skillsInstalledCheck];
24408
+ function hashInstalledFile(filePath) {
24409
+ try {
24410
+ return crypto23.createHash("sha256").update(fs6.readFileSync(filePath)).digest("hex");
24411
+ } catch {
24412
+ return void 0;
24413
+ }
24414
+ }
24415
+ function readInstalledManifest(skillDir) {
24416
+ try {
24417
+ return coerceSkillManifest(JSON.parse(fs6.readFileSync(path7.join(skillDir, SKILL_MANIFEST_FILENAME), "utf-8")));
24418
+ } catch {
24419
+ return null;
24420
+ }
24421
+ }
24422
+ var AGENT_CHECKS = [skillsInstalledCheck, skillsCurrentCheck];
23336
24423
 
23337
24424
  // ../api-routes/src/doctor/checks/bing-auth.ts
23338
24425
  var BING_AUTH_CHECKS = [
@@ -23962,8 +25049,8 @@ var dbFilePresentCheck = {
23962
25049
  scope: CheckScopes.global,
23963
25050
  title: "Database file present",
23964
25051
  run: (ctx) => {
23965
- const path16 = ctx.runtimeStatePaths?.databasePath;
23966
- if (!path16) {
25052
+ const path17 = ctx.runtimeStatePaths?.databasePath;
25053
+ if (!path17) {
23967
25054
  return {
23968
25055
  status: CheckStatuses.skipped,
23969
25056
  code: "db.file.path-not-wired",
@@ -23971,21 +25058,21 @@ var dbFilePresentCheck = {
23971
25058
  remediation: null
23972
25059
  };
23973
25060
  }
23974
- if (!fs7.existsSync(path16)) {
25061
+ if (!fs7.existsSync(path17)) {
23975
25062
  return {
23976
25063
  status: CheckStatuses.fail,
23977
25064
  code: "db.file.missing",
23978
- summary: `Database file at \`${path16}\` has been deleted while the daemon is running.`,
25065
+ summary: `Database file at \`${path17}\` has been deleted while the daemon is running.`,
23979
25066
  remediation: "Restart `canonry serve` so a fresh database is created and migrations re-run. Until you do, the daemon will keep serving stale data from a deleted-but-open file handle and writes will be lost.",
23980
- details: { path: path16 }
25067
+ details: { path: path17 }
23981
25068
  };
23982
25069
  }
23983
25070
  return {
23984
25071
  status: CheckStatuses.ok,
23985
25072
  code: "db.file.present",
23986
- summary: `Database file present at \`${path16}\`.`,
25073
+ summary: `Database file present at \`${path17}\`.`,
23987
25074
  remediation: null,
23988
- details: { path: path16 }
25075
+ details: { path: path17 }
23989
25076
  };
23990
25077
  }
23991
25078
  };
@@ -23995,8 +25082,8 @@ var configFilePresentCheck = {
23995
25082
  scope: CheckScopes.global,
23996
25083
  title: "Config file present",
23997
25084
  run: (ctx) => {
23998
- const path16 = ctx.runtimeStatePaths?.configPath;
23999
- if (!path16) {
25085
+ const path17 = ctx.runtimeStatePaths?.configPath;
25086
+ if (!path17) {
24000
25087
  return {
24001
25088
  status: CheckStatuses.skipped,
24002
25089
  code: "config.file.path-not-wired",
@@ -24004,21 +25091,21 @@ var configFilePresentCheck = {
24004
25091
  remediation: null
24005
25092
  };
24006
25093
  }
24007
- if (!fs7.existsSync(path16)) {
25094
+ if (!fs7.existsSync(path17)) {
24008
25095
  return {
24009
25096
  status: CheckStatuses.fail,
24010
25097
  code: "config.file.missing",
24011
- summary: `Config file at \`${path16}\` has been deleted while the daemon is running.`,
25098
+ summary: `Config file at \`${path17}\` has been deleted while the daemon is running.`,
24012
25099
  remediation: "Restart `canonry serve` after the file is restored (provider keys, OAuth tokens, and integration credentials live in this file; the in-memory copy is read-only until restart).",
24013
- details: { path: path16 }
25100
+ details: { path: path17 }
24014
25101
  };
24015
25102
  }
24016
25103
  return {
24017
25104
  status: CheckStatuses.ok,
24018
25105
  code: "config.file.present",
24019
- summary: `Config file present at \`${path16}\`.`,
25106
+ summary: `Config file present at \`${path17}\`.`,
24020
25107
  remediation: null,
24021
- details: { path: path16 }
25108
+ details: { path: path17 }
24022
25109
  };
24023
25110
  }
24024
25111
  };
@@ -24498,7 +25585,8 @@ async function doctorRoutes(app, opts) {
24498
25585
  redirectUri,
24499
25586
  providerSummary: opts.providerSummary,
24500
25587
  trafficSourceValidators: opts.trafficSourceValidators,
24501
- runtimeStatePaths: opts.runtimeStatePaths
25588
+ runtimeStatePaths: opts.runtimeStatePaths,
25589
+ bundledSkills: opts.bundledSkills
24502
25590
  };
24503
25591
  return runChecks(ctx, ALL_CHECKS, { checkIds });
24504
25592
  });
@@ -24521,15 +25609,16 @@ async function doctorRoutes(app, opts) {
24521
25609
  redirectUri,
24522
25610
  providerSummary: opts.providerSummary,
24523
25611
  trafficSourceValidators: opts.trafficSourceValidators,
24524
- runtimeStatePaths: opts.runtimeStatePaths
25612
+ runtimeStatePaths: opts.runtimeStatePaths,
25613
+ bundledSkills: opts.bundledSkills
24525
25614
  };
24526
25615
  return runChecks(ctx, ALL_CHECKS, { checkIds });
24527
25616
  });
24528
25617
  }
24529
25618
 
24530
25619
  // ../api-routes/src/discovery/routes.ts
24531
- import crypto22 from "crypto";
24532
- import { and as and21, desc as desc14, eq as eq26, gte as gte5, inArray as inArray9 } from "drizzle-orm";
25620
+ import crypto24 from "crypto";
25621
+ import { and as and21, desc as desc14, eq as eq26, gte as gte5, inArray as inArray10 } from "drizzle-orm";
24533
25622
  var MAX_INFLIGHT_DISCOVERY_AGE_MS = 2 * 60 * 60 * 1e3;
24534
25623
  async function discoveryRoutes(app, opts) {
24535
25624
  app.post("/projects/:name/discover/run", async (request, reply) => {
@@ -24564,7 +25653,7 @@ async function discoveryRoutes(app, opts) {
24564
25653
  const existing = tx.select({ id: discoverySessions.id, runId: discoverySessions.runId }).from(discoverySessions).where(and21(
24565
25654
  eq26(discoverySessions.projectId, project.id),
24566
25655
  eq26(discoverySessions.icpDescription, icpDescription),
24567
- inArray9(discoverySessions.status, [
25656
+ inArray10(discoverySessions.status, [
24568
25657
  DiscoverySessionStatuses.queued,
24569
25658
  DiscoverySessionStatuses.seeding,
24570
25659
  DiscoverySessionStatuses.probing
@@ -24574,8 +25663,8 @@ async function discoveryRoutes(app, opts) {
24574
25663
  if (existing && existing.runId) {
24575
25664
  return { reused: true, sessionId: existing.id, runId: existing.runId };
24576
25665
  }
24577
- const sessionId = crypto22.randomUUID();
24578
- const runId = crypto22.randomUUID();
25666
+ const sessionId = crypto24.randomUUID();
25667
+ const runId = crypto24.randomUUID();
24579
25668
  tx.insert(discoverySessions).values({
24580
25669
  id: sessionId,
24581
25670
  projectId: project.id,
@@ -24757,7 +25846,7 @@ async function discoveryRoutes(app, opts) {
24757
25846
  app.db.transaction((tx) => {
24758
25847
  for (const query of promotedQueries) {
24759
25848
  tx.insert(queries).values({
24760
- id: crypto22.randomUUID(),
25849
+ id: crypto24.randomUUID(),
24761
25850
  projectId: project.id,
24762
25851
  query,
24763
25852
  provenance,
@@ -24766,7 +25855,7 @@ async function discoveryRoutes(app, opts) {
24766
25855
  }
24767
25856
  for (const domain of promotedCompetitors) {
24768
25857
  tx.insert(competitors).values({
24769
- id: crypto22.randomUUID(),
25858
+ id: crypto24.randomUUID(),
24770
25859
  projectId: project.id,
24771
25860
  domain,
24772
25861
  provenance,
@@ -24840,7 +25929,7 @@ function selectEligibleCompetitors(competitorMap, competitorTypes) {
24840
25929
  }
24841
25930
 
24842
25931
  // ../api-routes/src/discovery/orchestrate.ts
24843
- import crypto23 from "crypto";
25932
+ import crypto25 from "crypto";
24844
25933
  import { eq as eq27 } from "drizzle-orm";
24845
25934
  var DEFAULT_DEDUP_THRESHOLD = 0.85;
24846
25935
  var DEFAULT_MAX_PROBES = 100;
@@ -24929,7 +26018,7 @@ async function executeDiscovery(opts) {
24929
26018
  probeRows.push({ citedDomains: probe.citedDomains, bucket });
24930
26019
  buckets[bucket]++;
24931
26020
  opts.db.insert(discoveryProbes).values({
24932
- id: crypto23.randomUUID(),
26021
+ id: crypto25.randomUUID(),
24933
26022
  sessionId: opts.sessionId,
24934
26023
  projectId: opts.project.id,
24935
26024
  query,
@@ -25102,7 +26191,8 @@ async function apiRoutes(app, opts) {
25102
26191
  publicUrl: opts.publicUrl,
25103
26192
  routePrefix: opts.routePrefix,
25104
26193
  onGscSyncRequested: opts.onGscSyncRequested,
25105
- onInspectSitemapRequested: opts.onInspectSitemapRequested
26194
+ onInspectSitemapRequested: opts.onInspectSitemapRequested,
26195
+ onGbpSyncRequested: opts.onGbpSyncRequested
25106
26196
  });
25107
26197
  await api.register(wordpressRoutes, {
25108
26198
  wordpressConnectionStore: opts.wordpressConnectionStore,
@@ -25151,7 +26241,8 @@ async function apiRoutes(app, opts) {
25151
26241
  publicUrl: opts.publicUrl,
25152
26242
  providerSummary: opts.providerSummary,
25153
26243
  trafficSourceValidators: buildTrafficSourceValidators(opts),
25154
- runtimeStatePaths: opts.runtimeStatePaths
26244
+ runtimeStatePaths: opts.runtimeStatePaths,
26245
+ bundledSkills: opts.bundledSkills
25155
26246
  });
25156
26247
  if (opts.registerAuthenticatedRoutes) {
25157
26248
  await opts.registerAuthenticatedRoutes(api);
@@ -25295,45 +26386,28 @@ function buildTrafficSourceValidators(opts) {
25295
26386
  }
25296
26387
 
25297
26388
  // src/server.ts
25298
- import os6 from "os";
26389
+ import os7 from "os";
25299
26390
 
25300
26391
  // ../provider-gemini/src/normalize.ts
25301
26392
  import { GoogleGenAI } from "@google/genai";
25302
26393
 
25303
26394
  // ../provider-gemini/src/utils.ts
25304
- function isRetryableError(err) {
25305
- if (err != null && typeof err === "object" && "status" in err) {
25306
- const status = err.status;
25307
- if (typeof status === "number") {
25308
- return status >= 500 || status === 429;
25309
- }
25310
- }
25311
- if (err instanceof Error) {
25312
- const msg = err.message.toLowerCase();
25313
- if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
25314
- return true;
25315
- }
25316
- }
25317
- return true;
25318
- }
25319
- async function withRetry(fn, options = {}) {
25320
- const { maxRetries = 3, initialDelay = 1e3 } = options;
25321
- let lastError;
25322
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
25323
- try {
25324
- return await fn();
25325
- } catch (err) {
25326
- lastError = err;
25327
- if (attempt < maxRetries && isRetryableError(err)) {
25328
- const delay = initialDelay * Math.pow(2, attempt);
25329
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
25330
- await new Promise((resolve) => setTimeout(resolve, delay));
25331
- } else {
25332
- throw err;
25333
- }
26395
+ async function withRetry2(fn, options = {}) {
26396
+ return withRetry(fn, {
26397
+ maxRetries: options.maxRetries ?? 3,
26398
+ baseDelayMs: options.initialDelay ?? 1e3,
26399
+ // Jitter off preserves the historical deterministic backoff that every
26400
+ // provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
26401
+ // if you need it.
26402
+ jitter: false,
26403
+ isRetryable: isRetryableHttpError,
26404
+ onRetry: ({ attempt, err, delayMs }) => {
26405
+ console.warn(
26406
+ `[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
26407
+ err instanceof Error ? err.message : String(err)
26408
+ );
25334
26409
  }
25335
- }
25336
- throw lastError;
26410
+ });
25337
26411
  }
25338
26412
 
25339
26413
  // ../provider-gemini/src/normalize.ts
@@ -25385,7 +26459,7 @@ async function healthcheck(config) {
25385
26459
  try {
25386
26460
  const model = resolveModel(config);
25387
26461
  const client = createClient2(config);
25388
- const result = await withRetry(
26462
+ const result = await withRetry2(
25389
26463
  () => client.models.generateContent({
25390
26464
  model,
25391
26465
  contents: 'Say "ok"'
@@ -25413,7 +26487,7 @@ async function executeTrackedQuery(input) {
25413
26487
  const prompt = buildPrompt(input.query, input.location);
25414
26488
  const client = createClient2(input.config);
25415
26489
  try {
25416
- const result = await withRetry(
26490
+ const result = await withRetry2(
25417
26491
  () => client.models.generateContent({
25418
26492
  model,
25419
26493
  contents: prompt,
@@ -25577,7 +26651,7 @@ function extractDomainFromUri(uri) {
25577
26651
  async function generateText(prompt, config) {
25578
26652
  const model = resolveModel(config);
25579
26653
  const client = createClient2(config);
25580
- const result = await withRetry(
26654
+ const result = await withRetry2(
25581
26655
  () => client.models.generateContent({
25582
26656
  model,
25583
26657
  contents: prompt
@@ -25744,39 +26818,22 @@ var geminiAdapter = {
25744
26818
  import OpenAI from "openai";
25745
26819
 
25746
26820
  // ../provider-openai/src/utils.ts
25747
- function isRetryableError2(err) {
25748
- if (err != null && typeof err === "object" && "status" in err) {
25749
- const status = err.status;
25750
- if (typeof status === "number") {
25751
- return status >= 500 || status === 429;
25752
- }
25753
- }
25754
- if (err instanceof Error) {
25755
- const msg = err.message.toLowerCase();
25756
- if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
25757
- return true;
25758
- }
25759
- }
25760
- return true;
25761
- }
25762
- async function withRetry2(fn, options = {}) {
25763
- const { maxRetries = 3, initialDelay = 1e3 } = options;
25764
- let lastError;
25765
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
25766
- try {
25767
- return await fn();
25768
- } catch (err) {
25769
- lastError = err;
25770
- if (attempt < maxRetries && isRetryableError2(err)) {
25771
- const delay = initialDelay * Math.pow(2, attempt);
25772
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
25773
- await new Promise((resolve) => setTimeout(resolve, delay));
25774
- } else {
25775
- throw err;
25776
- }
26821
+ async function withRetry3(fn, options = {}) {
26822
+ return withRetry(fn, {
26823
+ maxRetries: options.maxRetries ?? 3,
26824
+ baseDelayMs: options.initialDelay ?? 1e3,
26825
+ // Jitter off preserves the historical deterministic backoff that every
26826
+ // provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
26827
+ // if you need it.
26828
+ jitter: false,
26829
+ isRetryable: isRetryableHttpError,
26830
+ onRetry: ({ attempt, err, delayMs }) => {
26831
+ console.warn(
26832
+ `[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
26833
+ err instanceof Error ? err.message : String(err)
26834
+ );
25777
26835
  }
25778
- }
25779
- throw lastError;
26836
+ });
25780
26837
  }
25781
26838
 
25782
26839
  // ../provider-openai/src/normalize.ts
@@ -25797,7 +26854,7 @@ async function healthcheck2(config) {
25797
26854
  if (!validation.ok) return validation;
25798
26855
  try {
25799
26856
  const client = new OpenAI({ apiKey: config.apiKey });
25800
- const response = await withRetry2(
26857
+ const response = await withRetry3(
25801
26858
  () => client.responses.create({
25802
26859
  model: config.model ?? DEFAULT_MODEL2,
25803
26860
  input: 'Say "ok"'
@@ -25833,7 +26890,7 @@ async function executeTrackedQuery2(input) {
25833
26890
  };
25834
26891
  }
25835
26892
  try {
25836
- const response = await withRetry2(
26893
+ const response = await withRetry3(
25837
26894
  () => client.responses.create({
25838
26895
  model,
25839
26896
  tools: [webSearchTool],
@@ -25995,7 +27052,7 @@ function extractDomainFromUri2(uri) {
25995
27052
  async function generateText2(prompt, config) {
25996
27053
  const model = config.model ?? DEFAULT_MODEL2;
25997
27054
  const client = new OpenAI({ apiKey: config.apiKey });
25998
- const response = await withRetry2(
27055
+ const response = await withRetry3(
25999
27056
  () => client.responses.create({
26000
27057
  model,
26001
27058
  input: prompt
@@ -26098,39 +27155,22 @@ var openaiAdapter = {
26098
27155
  import Anthropic from "@anthropic-ai/sdk";
26099
27156
 
26100
27157
  // ../provider-claude/src/utils.ts
26101
- function isRetryableError3(err) {
26102
- if (err != null && typeof err === "object" && "status" in err) {
26103
- const status = err.status;
26104
- if (typeof status === "number") {
26105
- return status >= 500 || status === 429;
26106
- }
26107
- }
26108
- if (err instanceof Error) {
26109
- const msg = err.message.toLowerCase();
26110
- if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
26111
- return true;
26112
- }
26113
- }
26114
- return true;
26115
- }
26116
- async function withRetry3(fn, options = {}) {
26117
- const { maxRetries = 3, initialDelay = 1e3 } = options;
26118
- let lastError;
26119
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
26120
- try {
26121
- return await fn();
26122
- } catch (err) {
26123
- lastError = err;
26124
- if (attempt < maxRetries && isRetryableError3(err)) {
26125
- const delay = initialDelay * Math.pow(2, attempt);
26126
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
26127
- await new Promise((resolve) => setTimeout(resolve, delay));
26128
- } else {
26129
- throw err;
26130
- }
27158
+ async function withRetry4(fn, options = {}) {
27159
+ return withRetry(fn, {
27160
+ maxRetries: options.maxRetries ?? 3,
27161
+ baseDelayMs: options.initialDelay ?? 1e3,
27162
+ // Jitter off preserves the historical deterministic backoff that every
27163
+ // provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
27164
+ // if you need it.
27165
+ jitter: false,
27166
+ isRetryable: isRetryableHttpError,
27167
+ onRetry: ({ attempt, err, delayMs }) => {
27168
+ console.warn(
27169
+ `[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
27170
+ err instanceof Error ? err.message : String(err)
27171
+ );
26131
27172
  }
26132
- }
26133
- throw lastError;
27173
+ });
26134
27174
  }
26135
27175
 
26136
27176
  // ../provider-claude/src/normalize.ts
@@ -26164,7 +27204,7 @@ async function healthcheck3(config) {
26164
27204
  try {
26165
27205
  const model = resolveModel2(config);
26166
27206
  const client = new Anthropic({ apiKey: config.apiKey });
26167
- const response = await withRetry3(
27207
+ const response = await withRetry4(
26168
27208
  () => client.messages.create({
26169
27209
  model,
26170
27210
  max_tokens: 32,
@@ -26205,7 +27245,7 @@ async function executeTrackedQuery3(input) {
26205
27245
  };
26206
27246
  }
26207
27247
  try {
26208
- const response = await withRetry3(
27248
+ const response = await withRetry4(
26209
27249
  () => client.messages.create({
26210
27250
  model,
26211
27251
  max_tokens: 4096,
@@ -26374,7 +27414,7 @@ function extractDomainFromUri3(uri) {
26374
27414
  async function generateText3(prompt, config) {
26375
27415
  const model = resolveModel2(config);
26376
27416
  const client = new Anthropic({ apiKey: config.apiKey });
26377
- const response = await withRetry3(
27417
+ const response = await withRetry4(
26378
27418
  () => client.messages.create({
26379
27419
  model,
26380
27420
  max_tokens: 2048,
@@ -26475,39 +27515,22 @@ var claudeAdapter = {
26475
27515
  import OpenAI2 from "openai";
26476
27516
 
26477
27517
  // ../provider-local/src/utils.ts
26478
- function isRetryableError4(err) {
26479
- if (err != null && typeof err === "object" && "status" in err) {
26480
- const status = err.status;
26481
- if (typeof status === "number") {
26482
- return status >= 500 || status === 429;
26483
- }
26484
- }
26485
- if (err instanceof Error) {
26486
- const msg = err.message.toLowerCase();
26487
- if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
26488
- return true;
26489
- }
26490
- }
26491
- return true;
26492
- }
26493
- async function withRetry4(fn, options = {}) {
26494
- const { maxRetries = 3, initialDelay = 1e3 } = options;
26495
- let lastError;
26496
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
26497
- try {
26498
- return await fn();
26499
- } catch (err) {
26500
- lastError = err;
26501
- if (attempt < maxRetries && isRetryableError4(err)) {
26502
- const delay = initialDelay * Math.pow(2, attempt);
26503
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
26504
- await new Promise((resolve) => setTimeout(resolve, delay));
26505
- } else {
26506
- throw err;
26507
- }
27518
+ async function withRetry5(fn, options = {}) {
27519
+ return withRetry(fn, {
27520
+ maxRetries: options.maxRetries ?? 3,
27521
+ baseDelayMs: options.initialDelay ?? 1e3,
27522
+ // Jitter off preserves the historical deterministic backoff that every
27523
+ // provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
27524
+ // if you need it.
27525
+ jitter: false,
27526
+ isRetryable: isRetryableHttpError,
27527
+ onRetry: ({ attempt, err, delayMs }) => {
27528
+ console.warn(
27529
+ `[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
27530
+ err instanceof Error ? err.message : String(err)
27531
+ );
26508
27532
  }
26509
- }
26510
- throw lastError;
27533
+ });
26511
27534
  }
26512
27535
 
26513
27536
  // ../provider-local/src/normalize.ts
@@ -26531,7 +27554,7 @@ async function healthcheck4(config) {
26531
27554
  baseURL: config.baseUrl,
26532
27555
  apiKey: config.apiKey || "not-needed"
26533
27556
  });
26534
- const models = await withRetry4(async () => {
27557
+ const models = await withRetry5(async () => {
26535
27558
  const list = await client.models.list();
26536
27559
  const items = [];
26537
27560
  for await (const m of list) {
@@ -26562,7 +27585,7 @@ async function executeTrackedQuery4(input) {
26562
27585
  apiKey: input.config.apiKey || "not-needed"
26563
27586
  });
26564
27587
  try {
26565
- const response = await withRetry4(
27588
+ const response = await withRetry5(
26566
27589
  () => client.chat.completions.create({
26567
27590
  model,
26568
27591
  messages: [
@@ -26623,7 +27646,7 @@ async function generateText4(prompt, config) {
26623
27646
  baseURL: config.baseUrl,
26624
27647
  apiKey: config.apiKey || "not-needed"
26625
27648
  });
26626
- const response = await withRetry4(
27649
+ const response = await withRetry5(
26627
27650
  () => client.chat.completions.create({
26628
27651
  model,
26629
27652
  messages: [{ role: "user", content: prompt }]
@@ -27301,39 +28324,22 @@ var cdpChatgptAdapter = {
27301
28324
  import OpenAI3 from "openai";
27302
28325
 
27303
28326
  // ../provider-perplexity/src/utils.ts
27304
- function isRetryableError5(err) {
27305
- if (err != null && typeof err === "object" && "status" in err) {
27306
- const status = err.status;
27307
- if (typeof status === "number") {
27308
- return status >= 500 || status === 429;
27309
- }
27310
- }
27311
- if (err instanceof Error) {
27312
- const msg = err.message.toLowerCase();
27313
- if (msg.includes("fetch failed") || msg.includes("econnreset") || msg.includes("etimedout") || msg.includes("enotfound") || msg.includes("econnrefused") || msg.includes("network error")) {
27314
- return true;
27315
- }
27316
- }
27317
- return true;
27318
- }
27319
- async function withRetry5(fn, options = {}) {
27320
- const { maxRetries = 3, initialDelay = 1e3 } = options;
27321
- let lastError;
27322
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
27323
- try {
27324
- return await fn();
27325
- } catch (err) {
27326
- lastError = err;
27327
- if (attempt < maxRetries && isRetryableError5(err)) {
27328
- const delay = initialDelay * Math.pow(2, attempt);
27329
- console.warn(`[provider] Attempt ${attempt + 1} failed, retrying in ${delay}ms...`, err instanceof Error ? err.message : String(err));
27330
- await new Promise((resolve) => setTimeout(resolve, delay));
27331
- } else {
27332
- throw err;
27333
- }
28327
+ async function withRetry6(fn, options = {}) {
28328
+ return withRetry(fn, {
28329
+ maxRetries: options.maxRetries ?? 3,
28330
+ baseDelayMs: options.initialDelay ?? 1e3,
28331
+ // Jitter off preserves the historical deterministic backoff that every
28332
+ // provider has shipped to date (1000, 2000, 4000ms). Enable per-caller
28333
+ // if you need it.
28334
+ jitter: false,
28335
+ isRetryable: isRetryableHttpError,
28336
+ onRetry: ({ attempt, err, delayMs }) => {
28337
+ console.warn(
28338
+ `[provider] Attempt ${attempt + 1} failed, retrying in ${delayMs}ms...`,
28339
+ err instanceof Error ? err.message : String(err)
28340
+ );
27334
28341
  }
27335
- }
27336
- throw lastError;
28342
+ });
27337
28343
  }
27338
28344
 
27339
28345
  // ../provider-perplexity/src/normalize.ts
@@ -27355,7 +28361,7 @@ async function healthcheck5(config) {
27355
28361
  if (!validation.ok) return validation;
27356
28362
  try {
27357
28363
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
27358
- const response = await withRetry5(
28364
+ const response = await withRetry6(
27359
28365
  () => client.chat.completions.create({
27360
28366
  model: config.model ?? DEFAULT_MODEL5,
27361
28367
  messages: [{ role: "user", content: 'Say "ok"' }]
@@ -27382,7 +28388,7 @@ async function executeTrackedQuery5(input) {
27382
28388
  const client = new OpenAI3({ apiKey: input.config.apiKey, baseURL: BASE_URL });
27383
28389
  const prompt = buildPrompt4(input.query, input.location);
27384
28390
  try {
27385
- const response = await withRetry5(
28391
+ const response = await withRetry6(
27386
28392
  () => client.chat.completions.create({
27387
28393
  model,
27388
28394
  messages: [{ role: "user", content: prompt }]
@@ -27543,7 +28549,7 @@ function extractDomainFromUri4(uri) {
27543
28549
  async function generateText5(prompt, config) {
27544
28550
  const model = config.model ?? DEFAULT_MODEL5;
27545
28551
  const client = new OpenAI3({ apiKey: config.apiKey, baseURL: BASE_URL });
27546
- const response = await withRetry5(
28552
+ const response = await withRetry6(
27547
28553
  () => client.chat.completions.create({
27548
28554
  model,
27549
28555
  messages: [{ role: "user", content: prompt }]
@@ -27894,14 +28900,14 @@ function removeWordpressConnection(config, projectName) {
27894
28900
  }
27895
28901
 
27896
28902
  // src/job-runner.ts
27897
- import crypto25 from "crypto";
28903
+ import crypto27 from "crypto";
27898
28904
  import fs10 from "fs";
27899
28905
  import path10 from "path";
27900
28906
  import os5 from "os";
27901
- import { and as and22, eq as eq28, inArray as inArray10, sql as sql12 } from "drizzle-orm";
28907
+ import { and as and22, eq as eq28, inArray as inArray11, sql as sql12 } from "drizzle-orm";
27902
28908
 
27903
28909
  // src/run-telemetry.ts
27904
- import crypto24 from "crypto";
28910
+ import crypto26 from "crypto";
27905
28911
  function extractRegistrableHost(input) {
27906
28912
  if (!input) return null;
27907
28913
  const trimmed = input.trim();
@@ -27921,7 +28927,7 @@ function extractRegistrableHost(input) {
27921
28927
  function hashDomain(input) {
27922
28928
  const host = extractRegistrableHost(input);
27923
28929
  if (!host) return null;
27924
- return crypto24.createHash("sha256").update(host).digest("hex");
28930
+ return crypto26.createHash("sha256").update(host).digest("hex");
27925
28931
  }
27926
28932
  function buildRunCompletedProps(input) {
27927
28933
  const totalMs = input.phases?.total_ms ?? Date.now() - input.startTime;
@@ -28083,7 +29089,7 @@ var JobRunner = class {
28083
29089
  this.registry = registry;
28084
29090
  }
28085
29091
  recoverStaleRuns() {
28086
- const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray10(runs.status, ["running", "queued"])).all();
29092
+ const stale = this.db.select({ id: runs.id, status: runs.status }).from(runs).where(inArray11(runs.status, ["running", "queued"])).all();
28087
29093
  if (stale.length === 0) return;
28088
29094
  const now = (/* @__PURE__ */ new Date()).toISOString();
28089
29095
  for (const run of stale) {
@@ -28146,7 +29152,7 @@ var JobRunner = class {
28146
29152
  }
28147
29153
  log.info("run.dispatch", { runId, providerCount: activeProviders.length, providers: activeProviders.map((p) => p.adapter.name) });
28148
29154
  const scopedQueryNames = existingRun.queries;
28149
- projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and22(eq28(queries.projectId, projectId), inArray10(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq28(queries.projectId, projectId)).all();
29155
+ projectQueries = scopedQueryNames ? this.db.select().from(queries).where(and22(eq28(queries.projectId, projectId), inArray11(queries.query, scopedQueryNames))).all() : this.db.select().from(queries).where(eq28(queries.projectId, projectId)).all();
28150
29156
  const projectCompetitors = this.db.select().from(competitors).where(eq28(competitors.projectId, projectId)).all();
28151
29157
  const competitorDomains = projectCompetitors.map((c) => c.domain);
28152
29158
  const allDomains = effectiveDomains({
@@ -28230,7 +29236,7 @@ var JobRunner = class {
28230
29236
  );
28231
29237
  let screenshotRelPath = null;
28232
29238
  if (raw.screenshotPath && fs10.existsSync(raw.screenshotPath)) {
28233
- const snapshotId = crypto25.randomUUID();
29239
+ const snapshotId = crypto27.randomUUID();
28234
29240
  const screenshotDir = path10.join(os5.homedir(), ".canonry", "screenshots", runId);
28235
29241
  if (!fs10.existsSync(screenshotDir)) fs10.mkdirSync(screenshotDir, { recursive: true });
28236
29242
  const destPath = path10.join(screenshotDir, `${snapshotId}.png`);
@@ -28261,7 +29267,7 @@ var JobRunner = class {
28261
29267
  }).run();
28262
29268
  } else {
28263
29269
  this.db.insert(querySnapshots).values({
28264
- id: crypto25.randomUUID(),
29270
+ id: crypto27.randomUUID(),
28265
29271
  runId,
28266
29272
  queryId: q.id,
28267
29273
  queryText: q.query,
@@ -28411,7 +29417,7 @@ var JobRunner = class {
28411
29417
  const now = (/* @__PURE__ */ new Date()).toISOString();
28412
29418
  const period = now.slice(0, 10);
28413
29419
  this.db.insert(usageCounters).values({
28414
- id: crypto25.randomUUID(),
29420
+ id: crypto27.randomUUID(),
28415
29421
  scope,
28416
29422
  period,
28417
29423
  metric,
@@ -28488,7 +29494,7 @@ function buildPhases(input) {
28488
29494
  }
28489
29495
 
28490
29496
  // src/gsc-sync.ts
28491
- import crypto26 from "crypto";
29497
+ import crypto28 from "crypto";
28492
29498
  import { eq as eq29, and as and23, sql as sql13 } from "drizzle-orm";
28493
29499
  var log2 = createLogger("GscSync");
28494
29500
  function formatDate3(d) {
@@ -28554,7 +29560,7 @@ async function executeGscSync(db, runId, projectId, opts) {
28554
29560
  for (const row of batch) {
28555
29561
  const [query, page, country, device, date] = row.keys;
28556
29562
  db.insert(gscSearchData).values({
28557
- id: crypto26.randomUUID(),
29563
+ id: crypto28.randomUUID(),
28558
29564
  projectId,
28559
29565
  syncRunId: runId,
28560
29566
  date: date ?? "",
@@ -28588,7 +29594,7 @@ async function executeGscSync(db, runId, projectId, opts) {
28588
29594
  const rich = ir.richResultsResult;
28589
29595
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
28590
29596
  db.insert(gscUrlInspections).values({
28591
- id: crypto26.randomUUID(),
29597
+ id: crypto28.randomUUID(),
28592
29598
  projectId,
28593
29599
  syncRunId: runId,
28594
29600
  url: pageUrl,
@@ -28632,7 +29638,7 @@ async function executeGscSync(db, runId, projectId, opts) {
28632
29638
  const snapshotDate = formatDate3(/* @__PURE__ */ new Date());
28633
29639
  db.delete(gscCoverageSnapshots).where(and23(eq29(gscCoverageSnapshots.projectId, projectId), eq29(gscCoverageSnapshots.date, snapshotDate))).run();
28634
29640
  db.insert(gscCoverageSnapshots).values({
28635
- id: crypto26.randomUUID(),
29641
+ id: crypto28.randomUUID(),
28636
29642
  projectId,
28637
29643
  syncRunId: runId,
28638
29644
  date: snapshotDate,
@@ -28651,12 +29657,184 @@ async function executeGscSync(db, runId, projectId, opts) {
28651
29657
  }
28652
29658
  }
28653
29659
 
29660
+ // src/gbp-sync.ts
29661
+ import crypto29 from "crypto";
29662
+ import { eq as eq30, and as and24, desc as desc15 } from "drizzle-orm";
29663
+ var log3 = createLogger("GbpSync");
29664
+ var LOCATION_CONCURRENCY = 4;
29665
+ var DEFAULT_DAYS_OF_METRICS = 30;
29666
+ var DEFAULT_MONTHS_OF_KEYWORDS = 12;
29667
+ function daysAgo2(n) {
29668
+ const d = /* @__PURE__ */ new Date();
29669
+ d.setUTCDate(d.getUTCDate() - n);
29670
+ return d;
29671
+ }
29672
+ function monthMinus(n) {
29673
+ const d = /* @__PURE__ */ new Date();
29674
+ d.setUTCMonth(d.getUTCMonth() - n);
29675
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth() + 1 };
29676
+ }
29677
+ async function executeGbpSync(db, runId, projectId, opts) {
29678
+ const now = (/* @__PURE__ */ new Date()).toISOString();
29679
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq30(runs.id, runId)).run();
29680
+ try {
29681
+ const { clientId, clientSecret } = getGoogleAuthConfig(opts.config);
29682
+ if (!clientId || !clientSecret) {
29683
+ throw new Error("Google OAuth is not configured in the local Canonry config");
29684
+ }
29685
+ const project = db.select().from(projects).where(eq30(projects.id, projectId)).get();
29686
+ if (!project) throw new Error(`Project not found: ${projectId}`);
29687
+ const conn = getGoogleConnection(opts.config, project.canonicalDomain, "gbp");
29688
+ if (!conn || !conn.refreshToken) {
29689
+ throw new Error('No GBP connection found or connection is incomplete. Run "canonry gbp connect" first.');
29690
+ }
29691
+ let accessToken = conn.accessToken;
29692
+ const expiresAt = conn.tokenExpiresAt ? new Date(conn.tokenExpiresAt).getTime() : 0;
29693
+ if (Date.now() > expiresAt - 5 * 60 * 1e3) {
29694
+ const tokens = await refreshAccessToken(clientId, clientSecret, conn.refreshToken);
29695
+ accessToken = tokens.access_token;
29696
+ patchGoogleConnection(opts.config, project.canonicalDomain, "gbp", {
29697
+ accessToken: tokens.access_token,
29698
+ tokenExpiresAt: new Date(Date.now() + tokens.expires_in * 1e3).toISOString(),
29699
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
29700
+ });
29701
+ saveConfigPatch(opts.config);
29702
+ }
29703
+ let locationRows = db.select().from(gbpLocations).where(and24(eq30(gbpLocations.projectId, projectId), eq30(gbpLocations.selected, true))).all();
29704
+ if (opts.locationNames?.length) {
29705
+ const wanted = new Set(opts.locationNames);
29706
+ locationRows = locationRows.filter((l) => wanted.has(l.locationName));
29707
+ }
29708
+ if (locationRows.length === 0) {
29709
+ throw new Error("No selected GBP locations to sync. Discover and select locations first.");
29710
+ }
29711
+ const daysOfMetrics = opts.daysOfMetrics ?? DEFAULT_DAYS_OF_METRICS;
29712
+ const monthsOfKeywords = opts.monthsOfKeywords ?? DEFAULT_MONTHS_OF_KEYWORDS;
29713
+ const metricsStart = daysAgo2(daysOfMetrics);
29714
+ const metricsEnd = daysAgo2(1);
29715
+ const keywordsStart = monthMinus(monthsOfKeywords);
29716
+ const keywordsEnd = monthMinus(0);
29717
+ log3.info("sync.start", { runId, projectId, locations: locationRows.length, daysOfMetrics, monthsOfKeywords });
29718
+ const errors = /* @__PURE__ */ new Map();
29719
+ let okCount = 0;
29720
+ for (let i = 0; i < locationRows.length; i += LOCATION_CONCURRENCY) {
29721
+ const batch = locationRows.slice(i, i + LOCATION_CONCURRENCY);
29722
+ await Promise.all(batch.map(async (loc) => {
29723
+ try {
29724
+ const [metricRows, keywordRows, placeActionRows, lodging] = await Promise.all([
29725
+ fetchDailyMetrics(accessToken, loc.locationName, {
29726
+ metrics: GBP_DAILY_METRICS,
29727
+ startDate: metricsStart,
29728
+ endDate: metricsEnd
29729
+ }),
29730
+ listMonthlyKeywords(accessToken, loc.locationName, {
29731
+ startMonth: keywordsStart,
29732
+ endMonth: keywordsEnd
29733
+ }),
29734
+ listPlaceActionLinks(accessToken, loc.locationName),
29735
+ // null when the location is not a lodging-category property.
29736
+ getLodging(accessToken, loc.locationName)
29737
+ ]);
29738
+ const lodgingHash = lodging ? hashLodging(lodging) : null;
29739
+ const latestLodging = lodging ? db.select().from(gbpLodgingSnapshots).where(and24(eq30(gbpLodgingSnapshots.projectId, projectId), eq30(gbpLodgingSnapshots.locationName, loc.locationName))).orderBy(desc15(gbpLodgingSnapshots.syncedAt)).limit(1).get() : void 0;
29740
+ const lodgingChanged = lodging !== null && latestLodging?.contentHash !== lodgingHash;
29741
+ const insertNow = (/* @__PURE__ */ new Date()).toISOString();
29742
+ db.transaction((tx) => {
29743
+ tx.delete(gbpDailyMetrics).where(and24(eq30(gbpDailyMetrics.projectId, projectId), eq30(gbpDailyMetrics.locationName, loc.locationName))).run();
29744
+ for (const row of metricRows) {
29745
+ tx.insert(gbpDailyMetrics).values({
29746
+ id: crypto29.randomUUID(),
29747
+ projectId,
29748
+ locationName: loc.locationName,
29749
+ date: row.date,
29750
+ metric: row.metric,
29751
+ value: row.value,
29752
+ syncRunId: runId
29753
+ }).run();
29754
+ }
29755
+ tx.delete(gbpKeywordImpressions).where(and24(eq30(gbpKeywordImpressions.projectId, projectId), eq30(gbpKeywordImpressions.locationName, loc.locationName))).run();
29756
+ for (const row of keywordRows) {
29757
+ tx.insert(gbpKeywordImpressions).values({
29758
+ id: crypto29.randomUUID(),
29759
+ projectId,
29760
+ locationName: loc.locationName,
29761
+ periodStart: monthKey(keywordsStart),
29762
+ periodEnd: monthKey(keywordsEnd),
29763
+ keyword: row.keyword,
29764
+ valueCount: row.valueCount,
29765
+ valueThreshold: row.valueThreshold,
29766
+ syncRunId: runId
29767
+ }).run();
29768
+ }
29769
+ tx.delete(gbpPlaceActions).where(and24(eq30(gbpPlaceActions.projectId, projectId), eq30(gbpPlaceActions.locationName, loc.locationName))).run();
29770
+ for (const row of placeActionRows) {
29771
+ tx.insert(gbpPlaceActions).values({
29772
+ id: crypto29.randomUUID(),
29773
+ projectId,
29774
+ locationName: loc.locationName,
29775
+ placeActionLinkName: row.placeActionLinkName,
29776
+ placeActionType: row.placeActionType,
29777
+ uri: row.uri,
29778
+ isPreferred: row.isPreferred,
29779
+ providerType: row.providerType,
29780
+ syncRunId: runId
29781
+ }).run();
29782
+ }
29783
+ if (lodging !== null && lodgingChanged) {
29784
+ tx.insert(gbpLodgingSnapshots).values({
29785
+ id: crypto29.randomUUID(),
29786
+ projectId,
29787
+ locationName: loc.locationName,
29788
+ contentHash: lodgingHash,
29789
+ attributes: lodging,
29790
+ populatedGroupCount: countPopulatedGroups(lodging),
29791
+ syncedAt: insertNow,
29792
+ syncRunId: runId
29793
+ }).run();
29794
+ }
29795
+ tx.update(gbpLocations).set({ syncedAt: insertNow, updatedAt: insertNow }).where(eq30(gbpLocations.id, loc.id)).run();
29796
+ });
29797
+ okCount++;
29798
+ } catch (err) {
29799
+ errors.set(loc.locationName, err instanceof Error ? err.message : String(err));
29800
+ log3.error("location.failed", { runId, location: loc.locationName, error: err instanceof Error ? err.message : String(err) });
29801
+ }
29802
+ }));
29803
+ }
29804
+ const finishedAt = (/* @__PURE__ */ new Date()).toISOString();
29805
+ if (errors.size === 0) {
29806
+ db.update(runs).set({ status: "completed", finishedAt }).where(eq30(runs.id, runId)).run();
29807
+ } else if (okCount > 0) {
29808
+ db.update(runs).set({
29809
+ status: "partial",
29810
+ error: serializeRunError(buildRunErrorFromMessages(errors)),
29811
+ finishedAt
29812
+ }).where(eq30(runs.id, runId)).run();
29813
+ } else {
29814
+ db.update(runs).set({
29815
+ status: "failed",
29816
+ error: serializeRunError(buildRunErrorFromMessages(errors)),
29817
+ finishedAt
29818
+ }).where(eq30(runs.id, runId)).run();
29819
+ }
29820
+ log3.info("sync.done", { runId, projectId, ok: okCount, failed: errors.size });
29821
+ } catch (err) {
29822
+ const errorMsg = err instanceof Error ? err.message : String(err);
29823
+ db.update(runs).set({ status: "failed", error: serializeRunError({ message: errorMsg }), finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(runs.id, runId)).run();
29824
+ log3.error("sync.failed", { runId, projectId, error: errorMsg });
29825
+ throw err;
29826
+ }
29827
+ }
29828
+ function monthKey(m) {
29829
+ return `${m.year}-${String(m.month).padStart(2, "0")}`;
29830
+ }
29831
+
28654
29832
  // src/gsc-inspect-sitemap.ts
28655
- import crypto27 from "crypto";
28656
- import { eq as eq30, and as and24 } from "drizzle-orm";
29833
+ import crypto30 from "crypto";
29834
+ import { eq as eq31, and as and25 } from "drizzle-orm";
28657
29835
 
28658
29836
  // src/sitemap-parser.ts
28659
- var log3 = createLogger("SitemapParser");
29837
+ var log4 = createLogger("SitemapParser");
28660
29838
  var LOC_REGEX = /<loc>([^<]+)<\/loc>/gi;
28661
29839
  var SITEMAP_TAG_REGEX = /<sitemap>[\s\S]*?<\/sitemap>/gi;
28662
29840
  var PRIVATE_IP_PATTERNS = [
@@ -28719,7 +29897,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
28719
29897
  res = await fetch(url);
28720
29898
  } catch (err) {
28721
29899
  if (!isChild) throw err;
28722
- log3.warn("child-sitemap.fetch-failed", {
29900
+ log4.warn("child-sitemap.fetch-failed", {
28723
29901
  url,
28724
29902
  error: err instanceof Error ? err.message : String(err)
28725
29903
  });
@@ -28729,7 +29907,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
28729
29907
  if (!isChild) {
28730
29908
  throw new Error(`Failed to fetch sitemap at ${url}: ${res.status} ${res.statusText}`);
28731
29909
  }
28732
- log3.warn("child-sitemap.http-error", { url, status: res.status, statusText: res.statusText });
29910
+ log4.warn("child-sitemap.http-error", { url, status: res.status, statusText: res.statusText });
28733
29911
  return;
28734
29912
  }
28735
29913
  let xml;
@@ -28737,7 +29915,7 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
28737
29915
  xml = await readSitemapBody(res);
28738
29916
  } catch (err) {
28739
29917
  if (!isChild) throw err;
28740
- log3.warn("child-sitemap.parse-failed", {
29918
+ log4.warn("child-sitemap.parse-failed", {
28741
29919
  url,
28742
29920
  error: err instanceof Error ? err.message : String(err)
28743
29921
  });
@@ -28773,16 +29951,16 @@ async function parseSitemapRecursive(url, urls, visited, depth, isChild) {
28773
29951
  }
28774
29952
 
28775
29953
  // src/gsc-inspect-sitemap.ts
28776
- var log4 = createLogger("InspectSitemap");
29954
+ var log5 = createLogger("InspectSitemap");
28777
29955
  async function executeInspectSitemap(db, runId, projectId, opts) {
28778
29956
  const now = (/* @__PURE__ */ new Date()).toISOString();
28779
- db.update(runs).set({ status: "running", startedAt: now }).where(eq30(runs.id, runId)).run();
29957
+ db.update(runs).set({ status: "running", startedAt: now }).where(eq31(runs.id, runId)).run();
28780
29958
  try {
28781
29959
  const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
28782
29960
  if (!googleClientId || !googleClientSecret) {
28783
29961
  throw new Error("Google OAuth is not configured in the local Canonry config");
28784
29962
  }
28785
- const project = db.select().from(projects).where(eq30(projects.id, projectId)).get();
29963
+ const project = db.select().from(projects).where(eq31(projects.id, projectId)).get();
28786
29964
  if (!project) {
28787
29965
  throw new Error(`Project not found: ${projectId}`);
28788
29966
  }
@@ -28806,9 +29984,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
28806
29984
  saveConfigPatch(opts.config);
28807
29985
  }
28808
29986
  const sitemapUrl = opts.sitemapUrl || conn.sitemapUrl || `https://${project.canonicalDomain}/sitemap.xml`;
28809
- log4.info("sitemap.fetch", { runId, projectId, sitemapUrl });
29987
+ log5.info("sitemap.fetch", { runId, projectId, sitemapUrl });
28810
29988
  const urls = await fetchAndParseSitemap(sitemapUrl);
28811
- log4.info("sitemap.parsed", { runId, projectId, urlCount: urls.length, sitemapUrl });
29989
+ log5.info("sitemap.parsed", { runId, projectId, urlCount: urls.length, sitemapUrl });
28812
29990
  if (urls.length === 0) {
28813
29991
  throw new Error("No URLs found in sitemap");
28814
29992
  }
@@ -28823,7 +30001,7 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
28823
30001
  const rich = ir.richResultsResult;
28824
30002
  const inspectedAt = (/* @__PURE__ */ new Date()).toISOString();
28825
30003
  db.insert(gscUrlInspections).values({
28826
- id: crypto27.randomUUID(),
30004
+ id: crypto30.randomUUID(),
28827
30005
  projectId,
28828
30006
  syncRunId: runId,
28829
30007
  url: pageUrl,
@@ -28841,16 +30019,16 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
28841
30019
  createdAt: inspectedAt
28842
30020
  }).run();
28843
30021
  inspected++;
28844
- log4.info("inspect.url-done", { runId, projectId, url: pageUrl, progress: `${inspected}/${urls.length}` });
30022
+ log5.info("inspect.url-done", { runId, projectId, url: pageUrl, progress: `${inspected}/${urls.length}` });
28845
30023
  } catch (err) {
28846
30024
  errors++;
28847
- log4.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
30025
+ log5.error("inspect.url-failed", { runId, projectId, url: pageUrl, error: err instanceof Error ? err.message : String(err) });
28848
30026
  }
28849
30027
  if (inspected + errors < urls.length) {
28850
30028
  await new Promise((r) => setTimeout(r, 1e3));
28851
30029
  }
28852
30030
  }
28853
- const allInspections = db.select().from(gscUrlInspections).where(eq30(gscUrlInspections.projectId, projectId)).all();
30031
+ const allInspections = db.select().from(gscUrlInspections).where(eq31(gscUrlInspections.projectId, projectId)).all();
28854
30032
  const latestByUrl = /* @__PURE__ */ new Map();
28855
30033
  for (const row of allInspections) {
28856
30034
  const existing = latestByUrl.get(row.url);
@@ -28871,9 +30049,9 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
28871
30049
  }
28872
30050
  }
28873
30051
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
28874
- db.delete(gscCoverageSnapshots).where(and24(eq30(gscCoverageSnapshots.projectId, projectId), eq30(gscCoverageSnapshots.date, snapshotDate))).run();
30052
+ db.delete(gscCoverageSnapshots).where(and25(eq31(gscCoverageSnapshots.projectId, projectId), eq31(gscCoverageSnapshots.date, snapshotDate))).run();
28875
30053
  db.insert(gscCoverageSnapshots).values({
28876
- id: crypto27.randomUUID(),
30054
+ id: crypto30.randomUUID(),
28877
30055
  projectId,
28878
30056
  syncRunId: runId,
28879
30057
  date: snapshotDate,
@@ -28883,20 +30061,20 @@ async function executeInspectSitemap(db, runId, projectId, opts) {
28883
30061
  createdAt: (/* @__PURE__ */ new Date()).toISOString()
28884
30062
  }).run();
28885
30063
  const status = errors > 0 && inspected > 0 ? "partial" : errors === urls.length ? "failed" : "completed";
28886
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(runs.id, runId)).run();
28887
- log4.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
30064
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(runs.id, runId)).run();
30065
+ log5.info("inspect.completed", { runId, projectId, inspected, errors, total: urls.length, indexed: snapIndexed, notIndexed: snapNotIndexed });
28888
30066
  } catch (err) {
28889
30067
  const errorMsg = err instanceof Error ? err.message : String(err);
28890
- db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq30(runs.id, runId)).run();
28891
- log4.error("inspect.failed", { runId, projectId, error: errorMsg });
30068
+ db.update(runs).set({ status: "failed", error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(runs.id, runId)).run();
30069
+ log5.error("inspect.failed", { runId, projectId, error: errorMsg });
28892
30070
  throw err;
28893
30071
  }
28894
30072
  }
28895
30073
 
28896
30074
  // src/bing-inspect-sitemap.ts
28897
- import crypto28 from "crypto";
28898
- import { eq as eq31, desc as desc15 } from "drizzle-orm";
28899
- var log5 = createLogger("BingInspectSitemap");
30075
+ import crypto31 from "crypto";
30076
+ import { eq as eq32, desc as desc16 } from "drizzle-orm";
30077
+ var log6 = createLogger("BingInspectSitemap");
28900
30078
  function parseBingDate2(value) {
28901
30079
  if (!value) return null;
28902
30080
  const match = /\/Date\((-?\d+)(?:[-+]\d+)?\)\//.exec(value);
@@ -28913,9 +30091,9 @@ function isBlockingIssueType2(issueType) {
28913
30091
  }
28914
30092
  async function executeBingInspectSitemap(db, runId, projectId, opts) {
28915
30093
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
28916
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq31(runs.id, runId)).run();
30094
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq32(runs.id, runId)).run();
28917
30095
  try {
28918
- const project = db.select().from(projects).where(eq31(projects.id, projectId)).get();
30096
+ const project = db.select().from(projects).where(eq32(projects.id, projectId)).get();
28919
30097
  if (!project) {
28920
30098
  throw new Error(`Project not found: ${projectId}`);
28921
30099
  }
@@ -28927,16 +30105,16 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
28927
30105
  throw new Error('No Bing site configured. Run "canonry bing set-site <project> <url>" first.');
28928
30106
  }
28929
30107
  const sitemapUrl = opts.sitemapUrl ?? `https://${project.canonicalDomain}/sitemap.xml`;
28930
- log5.info("sitemap.fetch", { runId, projectId, sitemapUrl });
30108
+ log6.info("sitemap.fetch", { runId, projectId, sitemapUrl });
28931
30109
  const sitemapUrls = await fetchAndParseSitemap(sitemapUrl);
28932
- log5.info("sitemap.parsed", { runId, projectId, urlCount: sitemapUrls.length, sitemapUrl });
30110
+ log6.info("sitemap.parsed", { runId, projectId, urlCount: sitemapUrls.length, sitemapUrl });
28933
30111
  if (sitemapUrls.length === 0) {
28934
30112
  throw new Error("No URLs found in sitemap");
28935
30113
  }
28936
- const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq31(bingUrlInspections.projectId, projectId)).all();
30114
+ const trackedRows = db.select({ url: bingUrlInspections.url }).from(bingUrlInspections).where(eq32(bingUrlInspections.projectId, projectId)).all();
28937
30115
  const trackedUrls = new Set(trackedRows.map((r) => r.url));
28938
30116
  const discovered = sitemapUrls.filter((u) => !trackedUrls.has(u));
28939
- log5.info("sitemap.diff", {
30117
+ log6.info("sitemap.diff", {
28940
30118
  runId,
28941
30119
  projectId,
28942
30120
  sitemapTotal: sitemapUrls.length,
@@ -28951,9 +30129,9 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
28951
30129
  blockedUrls.add(issue.Url);
28952
30130
  }
28953
30131
  }
28954
- log5.info("crawl-issues.loaded", { runId, projectId, blockedCount: blockedUrls.size });
30132
+ log6.info("crawl-issues.loaded", { runId, projectId, blockedCount: blockedUrls.size });
28955
30133
  } catch (err) {
28956
- log5.warn("crawl-issues.lookup-failed", {
30134
+ log6.warn("crawl-issues.lookup-failed", {
28957
30135
  runId,
28958
30136
  projectId,
28959
30137
  error: err instanceof Error ? err.message : String(err)
@@ -28982,7 +30160,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
28982
30160
  derivedInIndex = false;
28983
30161
  }
28984
30162
  db.insert(bingUrlInspections).values({
28985
- id: crypto28.randomUUID(),
30163
+ id: crypto31.randomUUID(),
28986
30164
  projectId,
28987
30165
  url: pageUrl,
28988
30166
  httpCode,
@@ -28997,7 +30175,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
28997
30175
  discoveryDate
28998
30176
  }).run();
28999
30177
  inspected++;
29000
- log5.info("inspect.url-done", {
30178
+ log6.info("inspect.url-done", {
29001
30179
  runId,
29002
30180
  projectId,
29003
30181
  url: pageUrl,
@@ -29005,7 +30183,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
29005
30183
  });
29006
30184
  } catch (err) {
29007
30185
  errors++;
29008
- log5.error("inspect.url-failed", {
30186
+ log6.error("inspect.url-failed", {
29009
30187
  runId,
29010
30188
  projectId,
29011
30189
  url: pageUrl,
@@ -29016,7 +30194,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
29016
30194
  await new Promise((r) => setTimeout(r, 1e3));
29017
30195
  }
29018
30196
  }
29019
- const allInspections = db.select().from(bingUrlInspections).where(eq31(bingUrlInspections.projectId, projectId)).orderBy(desc15(bingUrlInspections.inspectedAt)).all();
30197
+ const allInspections = db.select().from(bingUrlInspections).where(eq32(bingUrlInspections.projectId, projectId)).orderBy(desc16(bingUrlInspections.inspectedAt)).all();
29020
30198
  const latestByUrl = /* @__PURE__ */ new Map();
29021
30199
  const definitiveByUrl = /* @__PURE__ */ new Map();
29022
30200
  for (const row of allInspections) {
@@ -29040,7 +30218,7 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
29040
30218
  const snapshotDate = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
29041
30219
  const snapNow = (/* @__PURE__ */ new Date()).toISOString();
29042
30220
  db.insert(bingCoverageSnapshots).values({
29043
- id: crypto28.randomUUID(),
30221
+ id: crypto31.randomUUID(),
29044
30222
  projectId,
29045
30223
  syncRunId: runId,
29046
30224
  date: snapshotDate,
@@ -29059,8 +30237,8 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
29059
30237
  }
29060
30238
  }).run();
29061
30239
  const status = errors === sitemapUrls.length ? RunStatuses.failed : errors > 0 ? RunStatuses.partial : RunStatuses.completed;
29062
- db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(runs.id, runId)).run();
29063
- log5.info("inspect.completed", {
30240
+ db.update(runs).set({ status, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(runs.id, runId)).run();
30241
+ log6.info("inspect.completed", {
29064
30242
  runId,
29065
30243
  projectId,
29066
30244
  inspected,
@@ -29073,19 +30251,74 @@ async function executeBingInspectSitemap(db, runId, projectId, opts) {
29073
30251
  });
29074
30252
  } catch (err) {
29075
30253
  const errorMsg = err instanceof Error ? err.message : String(err);
29076
- db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq31(runs.id, runId)).run();
29077
- log5.error("inspect.failed", { runId, projectId, error: errorMsg });
30254
+ db.update(runs).set({ status: RunStatuses.failed, error: errorMsg, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq32(runs.id, runId)).run();
30255
+ log6.error("inspect.failed", { runId, projectId, error: errorMsg });
29078
30256
  throw err;
29079
30257
  }
29080
30258
  }
29081
30259
 
30260
+ // src/coverage-refresh.ts
30261
+ import crypto32 from "crypto";
30262
+ import { and as and26, desc as desc17, eq as eq33, inArray as inArray12 } from "drizzle-orm";
30263
+ var log7 = createLogger("CoverageRefresh");
30264
+ var COVERAGE_REFRESH_MIN_INTERVAL_MS = 60 * 60 * 1e3;
30265
+ var ACTIVE_OR_DONE_STATUSES = [
30266
+ RunStatuses.queued,
30267
+ RunStatuses.running,
30268
+ RunStatuses.completed,
30269
+ RunStatuses.partial
30270
+ ];
30271
+ var defaultDeps = { executeInspectSitemap };
30272
+ async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps, nowMs = Date.now()) {
30273
+ const project = db.select({ canonicalDomain: projects.canonicalDomain }).from(projects).where(eq33(projects.id, projectId)).get();
30274
+ if (!project) return null;
30275
+ const { clientId, clientSecret } = getGoogleAuthConfig(config);
30276
+ if (!clientId || !clientSecret) return null;
30277
+ const conn = getGoogleConnection(config, project.canonicalDomain, "gsc");
30278
+ if (!conn?.refreshToken || !conn.propertyId) return null;
30279
+ const recent = db.select({ createdAt: runs.createdAt }).from(runs).where(
30280
+ and26(
30281
+ eq33(runs.projectId, projectId),
30282
+ eq33(runs.kind, RunKinds["inspect-sitemap"]),
30283
+ inArray12(runs.status, ACTIVE_OR_DONE_STATUSES)
30284
+ )
30285
+ ).orderBy(desc17(runs.createdAt)).limit(1).get();
30286
+ if (recent) {
30287
+ const ageMs = nowMs - Date.parse(recent.createdAt);
30288
+ if (Number.isFinite(ageMs) && ageMs < COVERAGE_REFRESH_MIN_INTERVAL_MS) {
30289
+ log7.info("skip.recent", { projectId, ageMs });
30290
+ return null;
30291
+ }
30292
+ }
30293
+ const runId = crypto32.randomUUID();
30294
+ db.insert(runs).values({
30295
+ id: runId,
30296
+ projectId,
30297
+ kind: RunKinds["inspect-sitemap"],
30298
+ status: RunStatuses.queued,
30299
+ trigger: RunTriggers.scheduled,
30300
+ createdAt: new Date(nowMs).toISOString()
30301
+ }).run();
30302
+ log7.info("refresh.start", { projectId, runId });
30303
+ try {
30304
+ await deps.executeInspectSitemap(db, runId, projectId, { config });
30305
+ } catch (err) {
30306
+ log7.error("refresh.failed", {
30307
+ projectId,
30308
+ runId,
30309
+ error: err instanceof Error ? err.message : String(err)
30310
+ });
30311
+ }
30312
+ return runId;
30313
+ }
30314
+
29082
30315
  // src/commoncrawl-sync.ts
29083
- import crypto29 from "crypto";
30316
+ import crypto33 from "crypto";
29084
30317
  import path11 from "path";
29085
- import { and as and25, eq as eq32, sql as sql14 } from "drizzle-orm";
29086
- var log6 = createLogger("CommonCrawlSync");
30318
+ import { and as and27, eq as eq34, sql as sql14 } from "drizzle-orm";
30319
+ var log8 = createLogger("CommonCrawlSync");
29087
30320
  var INSERT_CHUNK_SIZE = 1e4;
29088
- function defaultDeps() {
30321
+ function defaultDeps2() {
29089
30322
  return {
29090
30323
  downloadFile,
29091
30324
  queryBacklinks,
@@ -29095,7 +30328,7 @@ function defaultDeps() {
29095
30328
  };
29096
30329
  }
29097
30330
  async function executeReleaseSync(db, syncId, opts) {
29098
- const deps = { ...defaultDeps(), ...opts.deps };
30331
+ const deps = { ...defaultDeps2(), ...opts.deps };
29099
30332
  const release = opts.release;
29100
30333
  try {
29101
30334
  if (!isValidReleaseId(release)) {
@@ -29108,7 +30341,7 @@ async function executeReleaseSync(db, syncId, opts) {
29108
30341
  phaseDetail: "downloading vertices + edges",
29109
30342
  updatedAt: downloadStartedAt,
29110
30343
  error: null
29111
- }).where(eq32(ccReleaseSyncs.id, syncId)).run();
30344
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
29112
30345
  const paths = ccReleasePaths(release);
29113
30346
  const releaseCacheDir = path11.join(deps.cacheDir, release);
29114
30347
  const vertexPath = path11.join(releaseCacheDir, paths.vertexFilename);
@@ -29131,7 +30364,7 @@ async function executeReleaseSync(db, syncId, opts) {
29131
30364
  vertexSha256: vertex.sha256,
29132
30365
  edgesSha256: edges.sha256,
29133
30366
  updatedAt: downloadFinishedAt
29134
- }).where(eq32(ccReleaseSyncs.id, syncId)).run();
30367
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
29135
30368
  const allProjects = db.select().from(projects).all();
29136
30369
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
29137
30370
  let rows = [];
@@ -29147,15 +30380,15 @@ async function executeReleaseSync(db, syncId, opts) {
29147
30380
  }
29148
30381
  const queriedAt = deps.now().toISOString();
29149
30382
  db.transaction((tx) => {
29150
- tx.delete(backlinkDomains).where(eq32(backlinkDomains.releaseSyncId, syncId)).run();
29151
- tx.delete(backlinkSummaries).where(eq32(backlinkSummaries.releaseSyncId, syncId)).run();
30383
+ tx.delete(backlinkDomains).where(eq34(backlinkDomains.releaseSyncId, syncId)).run();
30384
+ tx.delete(backlinkSummaries).where(eq34(backlinkSummaries.releaseSyncId, syncId)).run();
29152
30385
  const expanded = [];
29153
30386
  for (const r of rows) {
29154
30387
  const projectIds = projectsByDomain.get(r.targetDomain);
29155
30388
  if (!projectIds) continue;
29156
30389
  for (const projectId of projectIds) {
29157
30390
  expanded.push({
29158
- id: crypto29.randomUUID(),
30391
+ id: crypto33.randomUUID(),
29159
30392
  projectId,
29160
30393
  releaseSyncId: syncId,
29161
30394
  release,
@@ -29175,7 +30408,7 @@ async function executeReleaseSync(db, syncId, opts) {
29175
30408
  const projectRows = rowsByProject.get(p.id) ?? [];
29176
30409
  const summary = computeSummary(projectRows);
29177
30410
  tx.insert(backlinkSummaries).values({
29178
- id: crypto29.randomUUID(),
30411
+ id: crypto33.randomUUID(),
29179
30412
  projectId: p.id,
29180
30413
  releaseSyncId: syncId,
29181
30414
  release,
@@ -29207,8 +30440,8 @@ async function executeReleaseSync(db, syncId, opts) {
29207
30440
  domainsDiscovered: rows.length,
29208
30441
  updatedAt: finishedAt,
29209
30442
  error: null
29210
- }).where(eq32(ccReleaseSyncs.id, syncId)).run();
29211
- log6.info("sync.completed", {
30443
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
30444
+ log8.info("sync.completed", {
29212
30445
  syncId,
29213
30446
  release,
29214
30447
  projectsProcessed: allProjects.length,
@@ -29220,7 +30453,7 @@ async function executeReleaseSync(db, syncId, opts) {
29220
30453
  try {
29221
30454
  deps.enqueueAutoExtract({ projectId: p.id, release });
29222
30455
  } catch (err) {
29223
- log6.error("auto-extract.enqueue-failed", {
30456
+ log8.error("auto-extract.enqueue-failed", {
29224
30457
  syncId,
29225
30458
  release,
29226
30459
  projectId: p.id,
@@ -29237,8 +30470,8 @@ async function executeReleaseSync(db, syncId, opts) {
29237
30470
  error: errorMsg,
29238
30471
  phaseDetail: null,
29239
30472
  updatedAt: finishedAt
29240
- }).where(eq32(ccReleaseSyncs.id, syncId)).run();
29241
- log6.error("sync.failed", { syncId, release, error: errorMsg });
30473
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
30474
+ log8.error("sync.failed", { syncId, release, error: errorMsg });
29242
30475
  throw err;
29243
30476
  }
29244
30477
  }
@@ -29271,11 +30504,11 @@ function computeSummary(rows) {
29271
30504
  }
29272
30505
 
29273
30506
  // src/backlink-extract.ts
29274
- import crypto30 from "crypto";
30507
+ import crypto34 from "crypto";
29275
30508
  import fs11 from "fs";
29276
- import { and as and26, desc as desc16, eq as eq33 } from "drizzle-orm";
29277
- var log7 = createLogger("BacklinkExtract");
29278
- function defaultDeps2() {
30509
+ import { and as and28, desc as desc18, eq as eq35 } from "drizzle-orm";
30510
+ var log9 = createLogger("BacklinkExtract");
30511
+ function defaultDeps3() {
29279
30512
  return {
29280
30513
  queryBacklinks,
29281
30514
  loadDuckdb,
@@ -29283,15 +30516,15 @@ function defaultDeps2() {
29283
30516
  };
29284
30517
  }
29285
30518
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29286
- const deps = { ...defaultDeps2(), ...opts.deps };
30519
+ const deps = { ...defaultDeps3(), ...opts.deps };
29287
30520
  const startedAt = deps.now().toISOString();
29288
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq33(runs.id, runId)).run();
30521
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq35(runs.id, runId)).run();
29289
30522
  try {
29290
- const project = db.select().from(projects).where(eq33(projects.id, projectId)).get();
30523
+ const project = db.select().from(projects).where(eq35(projects.id, projectId)).get();
29291
30524
  if (!project) {
29292
30525
  throw new Error(`Project not found: ${projectId}`);
29293
30526
  }
29294
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq33(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq33(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc16(ccReleaseSyncs.createdAt)).limit(1).get();
30527
+ const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq35(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq35(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc18(ccReleaseSyncs.createdAt)).limit(1).get();
29295
30528
  if (!sync) {
29296
30529
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
29297
30530
  }
@@ -29319,11 +30552,11 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29319
30552
  const targetDomain = project.canonicalDomain;
29320
30553
  db.transaction((tx) => {
29321
30554
  tx.delete(backlinkDomains).where(
29322
- and26(eq33(backlinkDomains.projectId, projectId), eq33(backlinkDomains.release, release))
30555
+ and28(eq35(backlinkDomains.projectId, projectId), eq35(backlinkDomains.release, release))
29323
30556
  ).run();
29324
30557
  if (rows.length > 0) {
29325
30558
  const values = rows.map((r) => ({
29326
- id: crypto30.randomUUID(),
30559
+ id: crypto34.randomUUID(),
29327
30560
  projectId,
29328
30561
  releaseSyncId: syncId,
29329
30562
  release,
@@ -29336,7 +30569,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29336
30569
  }
29337
30570
  const summary = computeSummary2(rows);
29338
30571
  tx.insert(backlinkSummaries).values({
29339
- id: crypto30.randomUUID(),
30572
+ id: crypto34.randomUUID(),
29340
30573
  projectId,
29341
30574
  releaseSyncId: syncId,
29342
30575
  release,
@@ -29359,8 +30592,8 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29359
30592
  }).run();
29360
30593
  });
29361
30594
  const finishedAt = deps.now().toISOString();
29362
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq33(runs.id, runId)).run();
29363
- log7.info("extract.completed", { runId, projectId, release, rows: rows.length });
30595
+ db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq35(runs.id, runId)).run();
30596
+ log9.info("extract.completed", { runId, projectId, release, rows: rows.length });
29364
30597
  } catch (err) {
29365
30598
  const errorMsg = err instanceof Error ? err.message : String(err);
29366
30599
  const finishedAt = deps.now().toISOString();
@@ -29368,8 +30601,8 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29368
30601
  status: RunStatuses.failed,
29369
30602
  error: errorMsg,
29370
30603
  finishedAt
29371
- }).where(eq33(runs.id, runId)).run();
29372
- log7.error("extract.failed", { runId, projectId, error: errorMsg });
30604
+ }).where(eq35(runs.id, runId)).run();
30605
+ log9.error("extract.failed", { runId, projectId, error: errorMsg });
29373
30606
  throw err;
29374
30607
  }
29375
30608
  }
@@ -29389,18 +30622,18 @@ function computeSummary2(rows) {
29389
30622
  }
29390
30623
 
29391
30624
  // src/discovery-run.ts
29392
- import crypto31 from "crypto";
29393
- import { and as and27, eq as eq34 } from "drizzle-orm";
29394
- var log8 = createLogger("DiscoveryRun");
30625
+ import crypto35 from "crypto";
30626
+ import { and as and29, eq as eq36 } from "drizzle-orm";
30627
+ var log10 = createLogger("DiscoveryRun");
29395
30628
  var DEFAULT_SEED_COUNT = 30;
29396
30629
  var QUERIES_PER_INTENT_BUCKET = 6;
29397
30630
  async function executeDiscoveryRun(opts) {
29398
30631
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
29399
- opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq34(runs.id, opts.runId)).run();
30632
+ opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq36(runs.id, opts.runId)).run();
29400
30633
  try {
29401
- const projectRow = opts.db.select().from(projects).where(eq34(projects.id, opts.projectId)).get();
30634
+ const projectRow = opts.db.select().from(projects).where(eq36(projects.id, opts.projectId)).get();
29402
30635
  if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
29403
- const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq34(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
30636
+ const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, opts.projectId)).all().map((r) => r.domain.toLowerCase());
29404
30637
  const canonicalDomains = effectiveDomains({
29405
30638
  canonicalDomain: projectRow.canonicalDomain,
29406
30639
  ownedDomains: projectRow.ownedDomains
@@ -29430,8 +30663,8 @@ async function executeDiscoveryRun(opts) {
29430
30663
  seedProvider: result.seedProvider,
29431
30664
  result
29432
30665
  });
29433
- opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq34(runs.id, opts.runId)).run();
29434
- log8.info("discovery.completed", {
30666
+ opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq36(runs.id, opts.runId)).run();
30667
+ log10.info("discovery.completed", {
29435
30668
  runId: opts.runId,
29436
30669
  sessionId: opts.sessionId,
29437
30670
  buckets: result.buckets,
@@ -29439,13 +30672,13 @@ async function executeDiscoveryRun(opts) {
29439
30672
  });
29440
30673
  } catch (err) {
29441
30674
  const errorMsg = err instanceof Error ? err.message : String(err);
29442
- log8.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
30675
+ log10.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
29443
30676
  markSessionFailed(opts.db, opts.sessionId, errorMsg);
29444
30677
  opts.db.update(runs).set({
29445
30678
  status: RunStatuses.failed,
29446
30679
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
29447
30680
  error: errorMsg
29448
- }).where(eq34(runs.id, opts.runId)).run();
30681
+ }).where(eq36(runs.id, opts.runId)).run();
29449
30682
  }
29450
30683
  }
29451
30684
  function buildDefaultDeps(registry) {
@@ -29650,13 +30883,13 @@ function writeDiscoveryInsight(db, input) {
29650
30883
  totalProbes
29651
30884
  });
29652
30885
  db.transaction((tx) => {
29653
- tx.update(insights).set({ dismissed: true }).where(and27(
29654
- eq34(insights.projectId, input.projectId),
29655
- eq34(insights.type, "discovery.basket-divergence"),
29656
- eq34(insights.dismissed, false)
30886
+ tx.update(insights).set({ dismissed: true }).where(and29(
30887
+ eq36(insights.projectId, input.projectId),
30888
+ eq36(insights.type, "discovery.basket-divergence"),
30889
+ eq36(insights.dismissed, false)
29657
30890
  )).run();
29658
30891
  tx.insert(insights).values({
29659
- id: crypto31.randomUUID(),
30892
+ id: crypto35.randomUUID(),
29660
30893
  projectId: input.projectId,
29661
30894
  runId: input.runId,
29662
30895
  type: "discovery.basket-divergence",
@@ -29692,7 +30925,7 @@ function buildDiscoveryInsightTitle(input) {
29692
30925
  }
29693
30926
 
29694
30927
  // src/commands/backfill.ts
29695
- import { and as and28, eq as eq35, inArray as inArray11, isNull, sql as sql15 } from "drizzle-orm";
30928
+ import { and as and30, eq as eq37, inArray as inArray13, isNull, sql as sql15 } from "drizzle-orm";
29696
30929
  var SNAPSHOT_BATCH_SIZE = 500;
29697
30930
  async function backfillAnswerVisibilityCommand(opts) {
29698
30931
  const config = loadConfig();
@@ -29700,7 +30933,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29700
30933
  migrate(db);
29701
30934
  const projectFilter = opts?.project?.trim();
29702
30935
  const isDryRun = opts?.dryRun === true;
29703
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq35(projects.name, projectFilter)).all() : db.select().from(projects).all();
30936
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq37(projects.name, projectFilter)).all() : db.select().from(projects).all();
29704
30937
  let examined = 0;
29705
30938
  let updated = 0;
29706
30939
  let wouldUpdate = 0;
@@ -29708,10 +30941,10 @@ async function backfillAnswerVisibilityCommand(opts) {
29708
30941
  let reparsed = 0;
29709
30942
  let providerErrors = 0;
29710
30943
  if (scopedProjects.length > 0) {
29711
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and28(
29712
- eq35(runs.kind, RunKinds["answer-visibility"]),
29713
- inArray11(runs.projectId, scopedProjects.map((project) => project.id))
29714
- )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq35(runs.kind, RunKinds["answer-visibility"])).all();
30944
+ const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and30(
30945
+ eq37(runs.kind, RunKinds["answer-visibility"]),
30946
+ inArray13(runs.projectId, scopedProjects.map((project) => project.id))
30947
+ )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq37(runs.kind, RunKinds["answer-visibility"])).all();
29715
30948
  const runIdsByProject = /* @__PURE__ */ new Map();
29716
30949
  for (const run of runRows) {
29717
30950
  const existing = runIdsByProject.get(run.projectId);
@@ -29719,7 +30952,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29719
30952
  else runIdsByProject.set(run.projectId, [run.id]);
29720
30953
  }
29721
30954
  for (const project of scopedProjects) {
29722
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, project.id)).all().map((row) => row.domain);
30955
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, project.id)).all().map((row) => row.domain);
29723
30956
  const runIds = runIdsByProject.get(project.id) ?? [];
29724
30957
  if (runIds.length === 0) continue;
29725
30958
  const projectDomains = effectiveDomains({
@@ -29742,7 +30975,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29742
30975
  competitorOverlap: querySnapshots.competitorOverlap,
29743
30976
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
29744
30977
  rawResponse: querySnapshots.rawResponse
29745
- }).from(querySnapshots).where(inArray11(querySnapshots.runId, batchRunIds)).all();
30978
+ }).from(querySnapshots).where(inArray13(querySnapshots.runId, batchRunIds)).all();
29746
30979
  const pendingUpdates = [];
29747
30980
  for (const snapshot of snapshotRows) {
29748
30981
  examined++;
@@ -29807,7 +31040,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29807
31040
  } else {
29808
31041
  db.transaction((tx) => {
29809
31042
  for (const update of pendingUpdates) {
29810
- tx.update(querySnapshots).set(update.patch).where(eq35(querySnapshots.id, update.id)).run();
31043
+ tx.update(querySnapshots).set(update.patch).where(eq37(querySnapshots.id, update.id)).run();
29811
31044
  }
29812
31045
  });
29813
31046
  updated += pendingUpdates.length;
@@ -29856,13 +31089,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
29856
31089
  function backfillNormalizedPaths(db, opts) {
29857
31090
  const baseConditions = [];
29858
31091
  if (opts?.projectId) {
29859
- baseConditions.push(eq35(gaTrafficSnapshots.projectId, opts.projectId));
31092
+ baseConditions.push(eq37(gaTrafficSnapshots.projectId, opts.projectId));
29860
31093
  }
29861
31094
  const rows = db.select({
29862
31095
  id: gaTrafficSnapshots.id,
29863
31096
  landingPage: gaTrafficSnapshots.landingPage,
29864
31097
  landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
29865
- }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and28(...baseConditions) : void 0).all();
31098
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and30(...baseConditions) : void 0).all();
29866
31099
  let updated = 0;
29867
31100
  let unchanged = 0;
29868
31101
  if (rows.length > 0) {
@@ -29877,7 +31110,7 @@ function backfillNormalizedPaths(db, opts) {
29877
31110
  unchanged++;
29878
31111
  continue;
29879
31112
  }
29880
- tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq35(gaTrafficSnapshots.id, row.id)).run();
31113
+ tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq37(gaTrafficSnapshots.id, row.id)).run();
29881
31114
  updated++;
29882
31115
  }
29883
31116
  });
@@ -29891,7 +31124,7 @@ async function backfillNormalizedPathsCommand(opts) {
29891
31124
  const projectFilter = opts?.project?.trim();
29892
31125
  let projectId;
29893
31126
  if (projectFilter) {
29894
- const project = db.select({ id: projects.id }).from(projects).where(eq35(projects.name, projectFilter)).get();
31127
+ const project = db.select({ id: projects.id }).from(projects).where(eq37(projects.name, projectFilter)).get();
29895
31128
  if (!project) {
29896
31129
  const result2 = {
29897
31130
  project: projectFilter,
@@ -29928,13 +31161,13 @@ async function backfillNormalizedPathsCommand(opts) {
29928
31161
  function backfillAiReferralPaths(db, opts) {
29929
31162
  const baseConditions = [];
29930
31163
  if (opts?.projectId) {
29931
- baseConditions.push(eq35(gaAiReferrals.projectId, opts.projectId));
31164
+ baseConditions.push(eq37(gaAiReferrals.projectId, opts.projectId));
29932
31165
  }
29933
31166
  const rows = db.select({
29934
31167
  id: gaAiReferrals.id,
29935
31168
  landingPage: gaAiReferrals.landingPage,
29936
31169
  landingPageNormalized: gaAiReferrals.landingPageNormalized
29937
- }).from(gaAiReferrals).where(baseConditions.length > 0 ? and28(...baseConditions) : void 0).all();
31170
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and30(...baseConditions) : void 0).all();
29938
31171
  let updated = 0;
29939
31172
  let unchanged = 0;
29940
31173
  if (rows.length > 0) {
@@ -29949,7 +31182,7 @@ function backfillAiReferralPaths(db, opts) {
29949
31182
  unchanged++;
29950
31183
  continue;
29951
31184
  }
29952
- tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq35(gaAiReferrals.id, row.id)).run();
31185
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq37(gaAiReferrals.id, row.id)).run();
29953
31186
  updated++;
29954
31187
  }
29955
31188
  });
@@ -29963,7 +31196,7 @@ async function backfillAiReferralPathsCommand(opts) {
29963
31196
  const projectFilter = opts?.project?.trim();
29964
31197
  let projectId;
29965
31198
  if (projectFilter) {
29966
- const project = db.select({ id: projects.id }).from(projects).where(eq35(projects.name, projectFilter)).get();
31199
+ const project = db.select({ id: projects.id }).from(projects).where(eq37(projects.name, projectFilter)).get();
29967
31200
  if (!project) {
29968
31201
  const result2 = {
29969
31202
  project: projectFilter,
@@ -29999,10 +31232,10 @@ async function backfillAiReferralPathsCommand(opts) {
29999
31232
  }
30000
31233
  function backfillProjectAnswerMentions(db, projectId, opts) {
30001
31234
  const isDryRun = opts?.dryRun === true;
30002
- const project = db.select().from(projects).where(eq35(projects.id, projectId)).get();
31235
+ const project = db.select().from(projects).where(eq37(projects.id, projectId)).get();
30003
31236
  if (!project) return { examined: 0, updated: 0, mentioned: 0 };
30004
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq35(competitors.projectId, projectId)).all().map((row) => row.domain);
30005
- const runRows = db.select({ id: runs.id }).from(runs).where(and28(eq35(runs.kind, RunKinds["answer-visibility"]), eq35(runs.projectId, projectId))).all();
31237
+ const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq37(competitors.projectId, projectId)).all().map((row) => row.domain);
31238
+ const runRows = db.select({ id: runs.id }).from(runs).where(and30(eq37(runs.kind, RunKinds["answer-visibility"]), eq37(runs.projectId, projectId))).all();
30006
31239
  const runIds = runRows.map((r) => r.id);
30007
31240
  let examined = 0;
30008
31241
  let updated = 0;
@@ -30030,7 +31263,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
30030
31263
  competitorOverlap: querySnapshots.competitorOverlap,
30031
31264
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
30032
31265
  rawResponse: querySnapshots.rawResponse
30033
- }).from(querySnapshots).where(inArray11(querySnapshots.runId, batchRunIds)).all();
31266
+ }).from(querySnapshots).where(inArray13(querySnapshots.runId, batchRunIds)).all();
30034
31267
  const pendingUpdates = [];
30035
31268
  for (const snapshot of snapshotRows) {
30036
31269
  examined++;
@@ -30074,7 +31307,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
30074
31307
  } else {
30075
31308
  db.transaction((tx) => {
30076
31309
  for (const update of pendingUpdates) {
30077
- tx.update(querySnapshots).set(update.patch).where(eq35(querySnapshots.id, update.id)).run();
31310
+ tx.update(querySnapshots).set(update.patch).where(eq37(querySnapshots.id, update.id)).run();
30078
31311
  }
30079
31312
  });
30080
31313
  updated += pendingUpdates.length;
@@ -30089,7 +31322,7 @@ async function backfillAnswerMentionsCommand(opts) {
30089
31322
  migrate(db);
30090
31323
  const projectFilter = opts?.project?.trim();
30091
31324
  const isDryRun = opts?.dryRun === true;
30092
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq35(projects.name, projectFilter)).all() : db.select().from(projects).all();
31325
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq37(projects.name, projectFilter)).all() : db.select().from(projects).all();
30093
31326
  let examined = 0;
30094
31327
  let updated = 0;
30095
31328
  let wouldUpdate = 0;
@@ -30149,7 +31382,7 @@ function readStoredGroundingSources(rawResponse) {
30149
31382
  return result;
30150
31383
  }
30151
31384
  async function backfillInsightsCommand(project, opts) {
30152
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-NY3MAVPB.js");
31385
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-V4SWVKEQ.js");
30153
31386
  const config = loadConfig();
30154
31387
  const db = createClient(config.database);
30155
31388
  migrate(db);
@@ -30308,7 +31541,7 @@ async function backfillSnapshotAttributionCommand(opts) {
30308
31541
  const config = loadConfig();
30309
31542
  const db = createClient(config.database);
30310
31543
  migrate(db);
30311
- const project = db.select().from(projects).where(eq35(projects.name, opts.project)).get();
31544
+ const project = db.select().from(projects).where(eq37(projects.name, opts.project)).get();
30312
31545
  if (!project) {
30313
31546
  throw new Error(`Project "${opts.project}" not found`);
30314
31547
  }
@@ -30319,17 +31552,17 @@ async function backfillSnapshotAttributionCommand(opts) {
30319
31552
  process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
30320
31553
  `);
30321
31554
  }
30322
- const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and28(
30323
- eq35(auditLog.projectId, project.id),
30324
- inArray11(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
31555
+ const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and30(
31556
+ eq37(auditLog.projectId, project.id),
31557
+ inArray13(auditLog.action, ["keywords.appended", "keywords.deleted", "queries.appended", "queries.deleted", "queries.replaced"])
30325
31558
  )).orderBy(auditLog.createdAt).all();
30326
31559
  const history = replayQueryAuditLog(events);
30327
31560
  const orphanRuns = db.select({
30328
31561
  runId: runs.id,
30329
31562
  createdAt: runs.createdAt,
30330
31563
  location: runs.location
30331
- }).from(runs).innerJoin(querySnapshots, eq35(querySnapshots.runId, runs.id)).where(and28(
30332
- eq35(runs.projectId, project.id),
31564
+ }).from(runs).innerJoin(querySnapshots, eq37(querySnapshots.runId, runs.id)).where(and30(
31565
+ eq37(runs.projectId, project.id),
30333
31566
  isNull(querySnapshots.queryId),
30334
31567
  isNull(querySnapshots.queryText)
30335
31568
  )).groupBy(runs.id).orderBy(runs.createdAt).all();
@@ -30351,8 +31584,8 @@ async function backfillSnapshotAttributionCommand(opts) {
30351
31584
  provider: querySnapshots.provider,
30352
31585
  createdAt: querySnapshots.createdAt,
30353
31586
  answerText: querySnapshots.answerText
30354
- }).from(querySnapshots).where(and28(
30355
- eq35(querySnapshots.runId, run.runId),
31587
+ }).from(querySnapshots).where(and30(
31588
+ eq37(querySnapshots.runId, run.runId),
30356
31589
  isNull(querySnapshots.queryId),
30357
31590
  isNull(querySnapshots.queryText)
30358
31591
  )).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
@@ -30418,7 +31651,7 @@ async function backfillSnapshotAttributionCommand(opts) {
30418
31651
  if (!isDryRun && updates.length > 0) {
30419
31652
  db.transaction((tx) => {
30420
31653
  for (const u of updates) {
30421
- tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq35(querySnapshots.id, u.id)).run();
31654
+ tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq37(querySnapshots.id, u.id)).run();
30422
31655
  }
30423
31656
  });
30424
31657
  }
@@ -30492,7 +31725,7 @@ async function backfillTrafficClassificationCommand(opts) {
30492
31725
  const projectFilter = opts?.project?.trim();
30493
31726
  const isDryRun = opts?.dryRun === true;
30494
31727
  const isJson = opts?.format === "json";
30495
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq35(projects.name, projectFilter)).all() : db.select().from(projects).all();
31728
+ const scopedProjects = projectFilter ? db.select().from(projects).where(eq37(projects.name, projectFilter)).all() : db.select().from(projects).all();
30496
31729
  if (scopedProjects.length === 0) {
30497
31730
  if (projectFilter && !isJson) {
30498
31731
  process.stderr.write(`No project named "${projectFilter}".
@@ -30517,9 +31750,9 @@ async function backfillTrafficClassificationCommand(opts) {
30517
31750
  dryRun: isDryRun,
30518
31751
  byBot: {}
30519
31752
  };
30520
- const unknownCountRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and28(
30521
- eq35(rawEventSamples.eventType, "unknown"),
30522
- inArray11(rawEventSamples.projectId, projectIds)
31753
+ const unknownCountRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and30(
31754
+ eq37(rawEventSamples.eventType, "unknown"),
31755
+ inArray13(rawEventSamples.projectId, projectIds)
30523
31756
  )).get();
30524
31757
  result.unknownBefore = Number(unknownCountRow?.n ?? 0);
30525
31758
  const unknownSamples = db.select({
@@ -30530,9 +31763,9 @@ async function backfillTrafficClassificationCommand(opts) {
30530
31763
  userAgent: rawEventSamples.userAgent,
30531
31764
  pathNormalized: rawEventSamples.pathNormalized,
30532
31765
  status: rawEventSamples.status
30533
- }).from(rawEventSamples).where(and28(
30534
- eq35(rawEventSamples.eventType, "unknown"),
30535
- inArray11(rawEventSamples.projectId, projectIds)
31766
+ }).from(rawEventSamples).where(and30(
31767
+ eq37(rawEventSamples.eventType, "unknown"),
31768
+ inArray13(rawEventSamples.projectId, projectIds)
30536
31769
  )).all();
30537
31770
  result.examined = unknownSamples.length;
30538
31771
  if (unknownSamples.length === 0) {
@@ -30570,7 +31803,7 @@ async function backfillTrafficClassificationCommand(opts) {
30570
31803
  result.reclassified++;
30571
31804
  result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
30572
31805
  if (isDryRun) continue;
30573
- db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq35(rawEventSamples.id, snap.id)).run();
31806
+ db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq37(rawEventSamples.id, snap.id)).run();
30574
31807
  const tsHour = new Date(snap.ts);
30575
31808
  tsHour.setUTCMinutes(0, 0, 0);
30576
31809
  if (userFetch) {
@@ -30634,9 +31867,9 @@ async function backfillTrafficClassificationCommand(opts) {
30634
31867
  }
30635
31868
  }
30636
31869
  if (!isDryRun) {
30637
- const afterRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and28(
30638
- eq35(rawEventSamples.eventType, "unknown"),
30639
- inArray11(rawEventSamples.projectId, projectIds)
31870
+ const afterRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and30(
31871
+ eq37(rawEventSamples.eventType, "unknown"),
31872
+ inArray13(rawEventSamples.projectId, projectIds)
30640
31873
  )).get();
30641
31874
  result.unknownAfter = Number(afterRow?.n ?? 0);
30642
31875
  } else {
@@ -30669,6 +31902,333 @@ No DB writes performed. Re-run without --dry-run to apply.`);
30669
31902
  }
30670
31903
  }
30671
31904
 
31905
+ // src/commands/skills.ts
31906
+ import crypto36 from "crypto";
31907
+ import fs12 from "fs";
31908
+ import os6 from "os";
31909
+ import path12 from "path";
31910
+ import { fileURLToPath } from "url";
31911
+ var BUNDLED_SKILL_NAMES = ["canonry", "aero"];
31912
+ function resolveBundledSkillsRoot(pkgDir) {
31913
+ const here = pkgDir ?? path12.dirname(fileURLToPath(import.meta.url));
31914
+ const candidates = [
31915
+ path12.join(here, "../assets/agent-workspace/skills"),
31916
+ path12.join(here, "../../assets/agent-workspace/skills"),
31917
+ path12.join(here, "../../../../skills")
31918
+ ];
31919
+ for (const candidate of candidates) {
31920
+ if (BUNDLED_SKILL_NAMES.every((name) => fs12.existsSync(path12.join(candidate, name, "SKILL.md")))) {
31921
+ return candidate;
31922
+ }
31923
+ }
31924
+ throw new CliError({
31925
+ code: "INTERNAL_ERROR",
31926
+ message: `Bundled skills not found. Searched:
31927
+ ${candidates.join("\n ")}`,
31928
+ exitCode: 2
31929
+ });
31930
+ }
31931
+ function parseDescription(content) {
31932
+ const fmMatch = /^---\n([\s\S]*?)\n---/.exec(content);
31933
+ if (!fmMatch) return "";
31934
+ const descMatch = /^description:\s*(\S.*)$/m.exec(fmMatch[1]);
31935
+ if (!descMatch) return "";
31936
+ return descMatch[1].replace(/^["']|["']$/g, "").trim();
31937
+ }
31938
+ function getBundledSkills(pkgDir) {
31939
+ const root = resolveBundledSkillsRoot(pkgDir);
31940
+ return BUNDLED_SKILL_NAMES.map((name) => {
31941
+ const skillDir = path12.join(root, name);
31942
+ const skillFile = path12.join(skillDir, "SKILL.md");
31943
+ const content = fs12.readFileSync(skillFile, "utf-8");
31944
+ return { name, description: parseDescription(content), bundledPath: skillDir };
31945
+ });
31946
+ }
31947
+ function walkRelative(dir, prefix = "") {
31948
+ const out = [];
31949
+ for (const entry of fs12.readdirSync(dir, { withFileTypes: true })) {
31950
+ const rel = prefix ? path12.join(prefix, entry.name) : entry.name;
31951
+ const full = path12.join(dir, entry.name);
31952
+ if (entry.isDirectory()) {
31953
+ out.push(...walkRelative(full, rel));
31954
+ } else if (entry.isFile()) {
31955
+ out.push(rel);
31956
+ }
31957
+ }
31958
+ return out.sort();
31959
+ }
31960
+ function sha256File(filePath) {
31961
+ return crypto36.createHash("sha256").update(fs12.readFileSync(filePath)).digest("hex");
31962
+ }
31963
+ function readSkillManifest(skillDir) {
31964
+ try {
31965
+ return coerceSkillManifest(JSON.parse(fs12.readFileSync(path12.join(skillDir, SKILL_MANIFEST_FILENAME), "utf-8")));
31966
+ } catch {
31967
+ return null;
31968
+ }
31969
+ }
31970
+ function writeSkillManifest(skillDir, manifest) {
31971
+ fs12.writeFileSync(path12.join(skillDir, SKILL_MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}
31972
+ `, "utf-8");
31973
+ }
31974
+ function reconcileSkillTree(srcDir, destDir, manifest, force) {
31975
+ const result = { added: [], updated: [], unchanged: [], conflicts: [], bundledHashes: /* @__PURE__ */ new Map() };
31976
+ for (const rel of walkRelative(srcDir)) {
31977
+ const srcPath = path12.join(srcDir, rel);
31978
+ const destPath = path12.join(destDir, rel);
31979
+ const bundledHash = sha256File(srcPath);
31980
+ result.bundledHashes.set(rel, bundledHash);
31981
+ const installedHash = fs12.existsSync(destPath) ? sha256File(destPath) : void 0;
31982
+ const state = classifySkillFile({ bundledHash, installedHash, manifestHash: manifest?.files[rel] });
31983
+ switch (state) {
31984
+ case "missing":
31985
+ fs12.mkdirSync(path12.dirname(destPath), { recursive: true });
31986
+ fs12.copyFileSync(srcPath, destPath);
31987
+ result.added.push(rel);
31988
+ break;
31989
+ case "unchanged":
31990
+ result.unchanged.push(rel);
31991
+ break;
31992
+ case "stale":
31993
+ fs12.copyFileSync(srcPath, destPath);
31994
+ result.updated.push(rel);
31995
+ break;
31996
+ case "edited":
31997
+ if (force) {
31998
+ fs12.copyFileSync(srcPath, destPath);
31999
+ result.updated.push(rel);
32000
+ } else {
32001
+ result.conflicts.push(rel);
32002
+ }
32003
+ break;
32004
+ }
32005
+ }
32006
+ return result;
32007
+ }
32008
+ function buildManifest(skillName, recon, prior) {
32009
+ const conflicts = new Set(recon.conflicts);
32010
+ const files = {};
32011
+ for (const [rel, bundledHash] of recon.bundledHashes) {
32012
+ files[rel] = conflicts.has(rel) ? prior?.files[rel] ?? bundledHash : bundledHash;
32013
+ }
32014
+ return { skill: skillName, version: PACKAGE_VERSION, files };
32015
+ }
32016
+ function describeChanges(recon) {
32017
+ const parts = [];
32018
+ if (recon.added.length > 0) parts.push(`${recon.added.length} added`);
32019
+ if (recon.updated.length > 0) parts.push(`${recon.updated.length} refreshed`);
32020
+ return parts.length > 0 ? parts.join(", ") : "no changes";
32021
+ }
32022
+ function buildClaudeMessage(name, status, recon) {
32023
+ const rel = `.claude/skills/${name}`;
32024
+ let message = status === "installed" ? `Installed ${rel}` : status === "updated" ? `Updated ${rel} (${describeChanges(recon)})` : `Already installed: ${rel}`;
32025
+ if (recon.conflicts.length > 0) {
32026
+ message += ` \u2014 ${recon.conflicts.length} file(s) differ from the bundle (local edits kept; pass --force to overwrite)`;
32027
+ }
32028
+ return message;
32029
+ }
32030
+ function installClaudeSkill(skill, targetDir, force) {
32031
+ const targetPath = path12.join(targetDir, ".claude", "skills", skill.name);
32032
+ const existedBefore = fs12.existsSync(path12.join(targetPath, "SKILL.md"));
32033
+ const priorManifest = readSkillManifest(targetPath);
32034
+ fs12.mkdirSync(targetPath, { recursive: true });
32035
+ const recon = reconcileSkillTree(skill.bundledPath, targetPath, priorManifest, force);
32036
+ writeSkillManifest(targetPath, buildManifest(skill.name, recon, priorManifest));
32037
+ const changed = recon.added.length + recon.updated.length;
32038
+ const status = !existedBefore ? "installed" : changed > 0 ? "updated" : "already-installed";
32039
+ return {
32040
+ skill: skill.name,
32041
+ client: CodingAgents.claude,
32042
+ targetPath,
32043
+ status,
32044
+ message: buildClaudeMessage(skill.name, status, recon),
32045
+ added: recon.added,
32046
+ updated: recon.updated,
32047
+ unchanged: recon.unchanged,
32048
+ conflicts: recon.conflicts
32049
+ };
32050
+ }
32051
+ function installCodexSymlink(skill, targetDir, force) {
32052
+ const codexPath = path12.join(targetDir, ".codex", "skills", skill.name);
32053
+ const claudePath = path12.join(targetDir, ".claude", "skills", skill.name);
32054
+ const linkTarget = path12.relative(path12.dirname(codexPath), claudePath);
32055
+ fs12.mkdirSync(path12.dirname(codexPath), { recursive: true });
32056
+ let stat;
32057
+ try {
32058
+ stat = fs12.lstatSync(codexPath);
32059
+ } catch {
32060
+ stat = void 0;
32061
+ }
32062
+ if (stat?.isSymbolicLink()) {
32063
+ const existing = fs12.readlinkSync(codexPath);
32064
+ if (existing === linkTarget) {
32065
+ return {
32066
+ skill: skill.name,
32067
+ client: CodingAgents.codex,
32068
+ targetPath: codexPath,
32069
+ status: "already-linked",
32070
+ message: `Already linked: .codex/skills/${skill.name}`
32071
+ };
32072
+ }
32073
+ if (!force) {
32074
+ throw new CliError({
32075
+ code: "VALIDATION_ERROR",
32076
+ message: `.codex/skills/${skill.name} is a symlink pointing elsewhere (${existing}). Pass --force to relink.`,
32077
+ details: { skill: skill.name, targetPath: codexPath, existingTarget: existing },
32078
+ exitCode: 1
32079
+ });
32080
+ }
32081
+ fs12.unlinkSync(codexPath);
32082
+ fs12.symlinkSync(linkTarget, codexPath);
32083
+ return {
32084
+ skill: skill.name,
32085
+ client: CodingAgents.codex,
32086
+ targetPath: codexPath,
32087
+ status: "relinked",
32088
+ message: `Relinked .codex/skills/${skill.name} \u2192 ${linkTarget}`
32089
+ };
32090
+ }
32091
+ if (stat) {
32092
+ if (!force) {
32093
+ throw new CliError({
32094
+ code: "VALIDATION_ERROR",
32095
+ message: `.codex/skills/${skill.name} exists but is not a symlink. Pass --force to replace.`,
32096
+ details: { skill: skill.name, targetPath: codexPath },
32097
+ exitCode: 1
32098
+ });
32099
+ }
32100
+ fs12.rmSync(codexPath, { recursive: true, force: true });
32101
+ }
32102
+ fs12.symlinkSync(linkTarget, codexPath);
32103
+ return {
32104
+ skill: skill.name,
32105
+ client: CodingAgents.codex,
32106
+ targetPath: codexPath,
32107
+ status: stat ? "relinked" : "linked",
32108
+ message: stat ? `Replaced and linked .codex/skills/${skill.name} \u2192 ${linkTarget}` : `Linked .codex/skills/${skill.name} \u2192 ${linkTarget}`
32109
+ };
32110
+ }
32111
+ function buildSummaryMessage(results) {
32112
+ const counts = {};
32113
+ for (const r of results) counts[r.status] = (counts[r.status] ?? 0) + 1;
32114
+ const parts = Object.entries(counts).map(([status, n]) => `${n} ${status}`);
32115
+ let message = `Skills install summary: ${parts.join(", ")}.`;
32116
+ const totalConflicts = results.reduce((sum, r) => sum + (r.conflicts?.length ?? 0), 0);
32117
+ if (totalConflicts > 0) {
32118
+ message += ` ${totalConflicts} file(s) differ from the bundle and were kept \u2014 pass --force to overwrite local edits.`;
32119
+ }
32120
+ return message;
32121
+ }
32122
+ function getBundledSkillSnapshots(pkgDir) {
32123
+ return getBundledSkills(pkgDir).map((skill) => {
32124
+ const files = {};
32125
+ for (const rel of walkRelative(skill.bundledPath)) {
32126
+ files[rel] = sha256File(path12.join(skill.bundledPath, rel));
32127
+ }
32128
+ return { name: skill.name, version: PACKAGE_VERSION, files };
32129
+ });
32130
+ }
32131
+ async function installSkills(opts = {}) {
32132
+ const targetDir = opts.user ? os6.homedir() : path12.resolve(opts.dir ?? process.cwd());
32133
+ const client = opts.client ?? SkillsClients.all;
32134
+ const force = opts.force ?? false;
32135
+ const allSkills = getBundledSkills();
32136
+ const requestedNames = opts.skills && opts.skills.length > 0 ? opts.skills : allSkills.map((s) => s.name);
32137
+ const knownNames = new Set(allSkills.map((s) => s.name));
32138
+ const unknown = requestedNames.filter((n) => !knownNames.has(n));
32139
+ if (unknown.length > 0) {
32140
+ throw new CliError({
32141
+ code: "VALIDATION_ERROR",
32142
+ message: `Unknown skill(s): ${unknown.join(", ")}. Available: ${[...knownNames].join(", ")}`,
32143
+ details: { unknownSkills: unknown, availableSkills: [...knownNames] },
32144
+ exitCode: 1
32145
+ });
32146
+ }
32147
+ const skillsToInstall = allSkills.filter((s) => requestedNames.includes(s.name));
32148
+ fs12.mkdirSync(targetDir, { recursive: true });
32149
+ const results = [];
32150
+ for (const skill of skillsToInstall) {
32151
+ results.push(installClaudeSkill(skill, targetDir, force));
32152
+ if (client !== SkillsClients.claude) {
32153
+ results.push(installCodexSymlink(skill, targetDir, force));
32154
+ }
32155
+ }
32156
+ return {
32157
+ targetDir,
32158
+ results,
32159
+ message: buildSummaryMessage(results)
32160
+ };
32161
+ }
32162
+ async function listSkills(opts = {}) {
32163
+ const skills = getBundledSkills();
32164
+ if (opts.format === "json") {
32165
+ console.log(JSON.stringify({
32166
+ skills: skills.map((s) => ({
32167
+ name: s.name,
32168
+ description: s.description,
32169
+ claudePath: `.claude/skills/${s.name}`,
32170
+ codexPath: `.codex/skills/${s.name}`
32171
+ }))
32172
+ }, null, 2));
32173
+ return;
32174
+ }
32175
+ console.log("Bundled canonry skills:\n");
32176
+ for (const skill of skills) {
32177
+ console.log(` ${skill.name}`);
32178
+ if (skill.description) console.log(` ${skill.description}`);
32179
+ console.log(` Claude: .claude/skills/${skill.name}/`);
32180
+ console.log(` Codex: .codex/skills/${skill.name} (symlink \u2192 ../../.claude/skills/${skill.name})`);
32181
+ console.log();
32182
+ }
32183
+ }
32184
+ function emitInstallSummary(summary, format) {
32185
+ if (format === "json") {
32186
+ console.log(JSON.stringify(summary, null, 2));
32187
+ return;
32188
+ }
32189
+ for (const r of summary.results) console.log(r.message);
32190
+ console.log(`
32191
+ Target: ${summary.targetDir}`);
32192
+ console.log(summary.message);
32193
+ }
32194
+ function getMissingUserSkillsNudge(home) {
32195
+ if (!home) return null;
32196
+ const skillsBase = path12.join(home, ".claude", "skills");
32197
+ const installed = [];
32198
+ const missing = [];
32199
+ for (const name of BUNDLED_SKILL_NAMES) {
32200
+ const skillFile = path12.join(skillsBase, name, "SKILL.md");
32201
+ if (existsSafe(skillFile)) installed.push(name);
32202
+ else missing.push(name);
32203
+ }
32204
+ if (missing.length === 0) return null;
32205
+ const fix = missing.length === BUNDLED_SKILL_NAMES.length ? "canonry skills install --user" : `canonry skills install ${missing.join(" ")} --user`;
32206
+ return {
32207
+ message: `Tip: ${missing.join(" + ")} skill${missing.length === 1 ? "" : "s"} not installed in ~/.claude/skills/. Run \`${fix}\` so Claude/Codex sessions on this host auto-load the canonry reference docs.`,
32208
+ missing,
32209
+ installed
32210
+ };
32211
+ }
32212
+ function existsSafe(p) {
32213
+ try {
32214
+ return fs12.existsSync(p);
32215
+ } catch {
32216
+ return false;
32217
+ }
32218
+ }
32219
+ function parseSkillsClient(value) {
32220
+ if (!value) return SkillsClients.all;
32221
+ const parsed = skillsClientSchema.safeParse(value);
32222
+ if (parsed.success) return parsed.data;
32223
+ const allowed = skillsClientSchema.options;
32224
+ throw new CliError({
32225
+ code: "VALIDATION_ERROR",
32226
+ message: `Invalid --client value "${value}". Must be one of: ${allowed.join(", ")}`,
32227
+ details: { flag: "client", value, allowed },
32228
+ exitCode: 1
32229
+ });
32230
+ }
32231
+
30672
32232
  // src/provider-registry.ts
30673
32233
  var ProviderRegistry = class {
30674
32234
  providers = /* @__PURE__ */ new Map();
@@ -30722,8 +32282,8 @@ var ProviderRegistry = class {
30722
32282
 
30723
32283
  // src/scheduler.ts
30724
32284
  import cron from "node-cron";
30725
- import { and as and29, eq as eq36 } from "drizzle-orm";
30726
- var log9 = createLogger("Scheduler");
32285
+ import { and as and31, eq as eq38 } from "drizzle-orm";
32286
+ var log11 = createLogger("Scheduler");
30727
32287
  function taskKey(projectId, kind) {
30728
32288
  return `${projectId}::${kind}`;
30729
32289
  }
@@ -30737,16 +32297,16 @@ var Scheduler = class {
30737
32297
  }
30738
32298
  /** Load all enabled schedules from DB and register cron jobs. */
30739
32299
  start() {
30740
- const allSchedules = this.db.select().from(schedules).where(eq36(schedules.enabled, true)).all();
32300
+ const allSchedules = this.db.select().from(schedules).where(eq38(schedules.enabled, true)).all();
30741
32301
  for (const schedule of allSchedules) {
30742
32302
  const missedRunAt = schedule.nextRunAt;
30743
32303
  this.registerCronTask(schedule);
30744
32304
  if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
30745
- log9.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
32305
+ log11.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
30746
32306
  this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
30747
32307
  }
30748
32308
  }
30749
- log9.info("started", { scheduleCount: allSchedules.length });
32309
+ log11.info("started", { scheduleCount: allSchedules.length });
30750
32310
  }
30751
32311
  /** Stop all cron tasks for graceful shutdown. */
30752
32312
  stop() {
@@ -30767,7 +32327,7 @@ var Scheduler = class {
30767
32327
  this.stopTask(key, existing, "Stopped");
30768
32328
  this.tasks.delete(key);
30769
32329
  }
30770
- const schedule = this.db.select().from(schedules).where(and29(eq36(schedules.projectId, projectId), eq36(schedules.kind, kind))).get();
32330
+ const schedule = this.db.select().from(schedules).where(and31(eq38(schedules.projectId, projectId), eq38(schedules.kind, kind))).get();
30771
32331
  if (schedule && schedule.enabled) {
30772
32332
  this.registerCronTask(schedule);
30773
32333
  }
@@ -30790,13 +32350,13 @@ var Scheduler = class {
30790
32350
  stopTask(key, task, verb) {
30791
32351
  void task.stop();
30792
32352
  void task.destroy();
30793
- log9.info(`task.${verb.toLowerCase()}`, { key });
32353
+ log11.info(`task.${verb.toLowerCase()}`, { key });
30794
32354
  }
30795
32355
  registerCronTask(schedule) {
30796
32356
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
30797
32357
  const kind = schedule.kind;
30798
32358
  if (!cron.validate(cronExpr)) {
30799
- log9.error("cron.invalid", { projectId, kind, cronExpr });
32359
+ log11.error("cron.invalid", { projectId, kind, cronExpr });
30800
32360
  return;
30801
32361
  }
30802
32362
  const task = cron.schedule(cronExpr, () => {
@@ -30808,43 +32368,43 @@ var Scheduler = class {
30808
32368
  this.db.update(schedules).set({
30809
32369
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
30810
32370
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
30811
- }).where(eq36(schedules.id, scheduleId)).run();
32371
+ }).where(eq38(schedules.id, scheduleId)).run();
30812
32372
  const label = schedule.preset ?? cronExpr;
30813
- log9.info("cron.registered", { projectId, kind, schedule: label, timezone });
32373
+ log11.info("cron.registered", { projectId, kind, schedule: label, timezone });
30814
32374
  }
30815
32375
  triggerRun(scheduleId, projectId, kind) {
30816
32376
  try {
30817
32377
  const now = (/* @__PURE__ */ new Date()).toISOString();
30818
- const currentSchedule = this.db.select().from(schedules).where(eq36(schedules.id, scheduleId)).get();
32378
+ const currentSchedule = this.db.select().from(schedules).where(eq38(schedules.id, scheduleId)).get();
30819
32379
  if (!currentSchedule || !currentSchedule.enabled) {
30820
- log9.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
32380
+ log11.warn("schedule.stale", { scheduleId, projectId, kind, msg: "schedule no longer exists or is disabled" });
30821
32381
  this.remove(projectId, kind);
30822
32382
  return;
30823
32383
  }
30824
32384
  const task = this.tasks.get(taskKey(projectId, kind));
30825
32385
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
30826
- const project = this.db.select().from(projects).where(eq36(projects.id, projectId)).get();
32386
+ const project = this.db.select().from(projects).where(eq38(projects.id, projectId)).get();
30827
32387
  if (!project) {
30828
- log9.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
32388
+ log11.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
30829
32389
  this.remove(projectId, kind);
30830
32390
  return;
30831
32391
  }
30832
32392
  if (kind === SchedulableRunKinds["traffic-sync"]) {
30833
32393
  const sourceId = currentSchedule.sourceId;
30834
32394
  if (!sourceId) {
30835
- log9.warn("traffic-sync.missing-source", { scheduleId, projectId });
32395
+ log11.warn("traffic-sync.missing-source", { scheduleId, projectId });
30836
32396
  return;
30837
32397
  }
30838
32398
  if (!this.callbacks.onTrafficSyncRequested) {
30839
- log9.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
32399
+ log11.warn("traffic-sync.no-callback", { scheduleId, projectId, msg: "host did not register onTrafficSyncRequested" });
30840
32400
  return;
30841
32401
  }
30842
32402
  this.db.update(schedules).set({
30843
32403
  lastRunAt: now,
30844
32404
  nextRunAt,
30845
32405
  updatedAt: now
30846
- }).where(eq36(schedules.id, currentSchedule.id)).run();
30847
- log9.info("traffic-sync.triggered", { projectName: project.name, sourceId });
32406
+ }).where(eq38(schedules.id, currentSchedule.id)).run();
32407
+ log11.info("traffic-sync.triggered", { projectName: project.name, sourceId });
30848
32408
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
30849
32409
  return;
30850
32410
  }
@@ -30853,7 +32413,7 @@ var Scheduler = class {
30853
32413
  if (project.defaultLocation) {
30854
32414
  const loc = projectLocations.find((l) => l.label === project.defaultLocation);
30855
32415
  if (!loc) {
30856
- log9.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
32416
+ log11.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
30857
32417
  return;
30858
32418
  }
30859
32419
  resolvedLocation = loc;
@@ -30867,11 +32427,11 @@ var Scheduler = class {
30867
32427
  location: locationLabel
30868
32428
  });
30869
32429
  if (queueResult.conflict) {
30870
- log9.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
32430
+ log11.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
30871
32431
  this.db.update(schedules).set({
30872
32432
  nextRunAt,
30873
32433
  updatedAt: now
30874
- }).where(eq36(schedules.id, currentSchedule.id)).run();
32434
+ }).where(eq38(schedules.id, currentSchedule.id)).run();
30875
32435
  return;
30876
32436
  }
30877
32437
  const runId = queueResult.runId;
@@ -30879,21 +32439,21 @@ var Scheduler = class {
30879
32439
  lastRunAt: now,
30880
32440
  nextRunAt,
30881
32441
  updatedAt: now
30882
- }).where(eq36(schedules.id, currentSchedule.id)).run();
32442
+ }).where(eq38(schedules.id, currentSchedule.id)).run();
30883
32443
  const scheduleProviders = currentSchedule.providers;
30884
32444
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
30885
- log9.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
32445
+ log11.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
30886
32446
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
30887
32447
  } catch (err) {
30888
- log9.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
32448
+ log11.error("trigger.error", { scheduleId, projectId, kind, error: err instanceof Error ? err.message : String(err) });
30889
32449
  }
30890
32450
  }
30891
32451
  };
30892
32452
 
30893
32453
  // src/notifier.ts
30894
- import { eq as eq37, desc as desc17, and as and30, inArray as inArray12, or as or5 } from "drizzle-orm";
30895
- import crypto32 from "crypto";
30896
- var log10 = createLogger("Notifier");
32454
+ import { eq as eq39, desc as desc19, and as and32, inArray as inArray14, or as or5 } from "drizzle-orm";
32455
+ import crypto37 from "crypto";
32456
+ var log12 = createLogger("Notifier");
30897
32457
  var Notifier = class {
30898
32458
  db;
30899
32459
  serverUrl;
@@ -30903,26 +32463,26 @@ var Notifier = class {
30903
32463
  }
30904
32464
  /** Called after a run completes (success, partial, or failed). */
30905
32465
  async onRunCompleted(runId, projectId) {
30906
- log10.info("run.completed", { runId, projectId });
30907
- const notifs = this.db.select().from(notifications).where(eq37(notifications.projectId, projectId)).all().filter((n) => n.enabled);
32466
+ log12.info("run.completed", { runId, projectId });
32467
+ const notifs = this.db.select().from(notifications).where(eq39(notifications.projectId, projectId)).all().filter((n) => n.enabled);
30908
32468
  if (notifs.length === 0) {
30909
- log10.info("notifications.none-enabled", { projectId });
32469
+ log12.info("notifications.none-enabled", { projectId });
30910
32470
  return;
30911
32471
  }
30912
- log10.info("notifications.found", { projectId, count: notifs.length });
30913
- const run = this.db.select().from(runs).where(eq37(runs.id, runId)).get();
32472
+ log12.info("notifications.found", { projectId, count: notifs.length });
32473
+ const run = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
30914
32474
  if (!run) {
30915
- log10.error("run.not-found", { runId, msg: "skipping notification dispatch" });
32475
+ log12.error("run.not-found", { runId, msg: "skipping notification dispatch" });
30916
32476
  return;
30917
32477
  }
30918
- const project = this.db.select().from(projects).where(eq37(projects.id, projectId)).get();
32478
+ const project = this.db.select().from(projects).where(eq39(projects.id, projectId)).get();
30919
32479
  if (!project) {
30920
- log10.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
32480
+ log12.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
30921
32481
  return;
30922
32482
  }
30923
32483
  const transitions = this.computeTransitions(runId, projectId);
30924
32484
  const events = [];
30925
- log10.info("run.status", { runId: run.id, status: run.status, projectId });
32485
+ log12.info("run.status", { runId: run.id, status: run.status, projectId });
30926
32486
  if (run.status === "completed" || run.status === "partial") {
30927
32487
  events.push("run.completed");
30928
32488
  }
@@ -30938,7 +32498,7 @@ var Notifier = class {
30938
32498
  if (!config.url) continue;
30939
32499
  const subscribedEvents = config.events;
30940
32500
  const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
30941
- log10.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
32501
+ log12.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
30942
32502
  if (matchingEvents.length === 0) continue;
30943
32503
  for (const event of matchingEvents) {
30944
32504
  const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
@@ -30962,11 +32522,11 @@ var Notifier = class {
30962
32522
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
30963
32523
  if (highInsights.length > 0) insightEvents.push("insight.high");
30964
32524
  if (insightEvents.length === 0) return;
30965
- const notifs = this.db.select().from(notifications).where(eq37(notifications.projectId, projectId)).all().filter((n) => n.enabled);
32525
+ const notifs = this.db.select().from(notifications).where(eq39(notifications.projectId, projectId)).all().filter((n) => n.enabled);
30966
32526
  if (notifs.length === 0) return;
30967
- const run = this.db.select().from(runs).where(eq37(runs.id, runId)).get();
32527
+ const run = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
30968
32528
  if (!run) return;
30969
- const project = this.db.select().from(projects).where(eq37(projects.id, projectId)).get();
32529
+ const project = this.db.select().from(projects).where(eq39(projects.id, projectId)).get();
30970
32530
  if (!project) return;
30971
32531
  for (const notif of notifs) {
30972
32532
  const config = notif.config;
@@ -30996,12 +32556,12 @@ var Notifier = class {
30996
32556
  }
30997
32557
  }
30998
32558
  computeTransitions(runId, projectId) {
30999
- const thisRun = this.db.select().from(runs).where(eq37(runs.id, runId)).get();
32559
+ const thisRun = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
31000
32560
  if (!thisRun) return [];
31001
- const groupSiblings = this.db.select().from(runs).where(and30(
31002
- eq37(runs.projectId, projectId),
31003
- eq37(runs.kind, thisRun.kind),
31004
- eq37(runs.createdAt, thisRun.createdAt)
32561
+ const groupSiblings = this.db.select().from(runs).where(and32(
32562
+ eq39(runs.projectId, projectId),
32563
+ eq39(runs.kind, thisRun.kind),
32564
+ eq39(runs.createdAt, thisRun.createdAt)
31005
32565
  )).all();
31006
32566
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
31007
32567
  if (stillPending) return [];
@@ -31017,19 +32577,19 @@ var Notifier = class {
31017
32577
  return candidate.id > best.id ? candidate : best;
31018
32578
  });
31019
32579
  if (winner.id !== runId) return [];
31020
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq37(projects.id, projectId)).get();
32580
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq39(projects.id, projectId)).get();
31021
32581
  const locationCount = Math.max(
31022
32582
  1,
31023
32583
  (projectLocations?.locations ?? []).length
31024
32584
  );
31025
32585
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
31026
32586
  const recentRuns = this.db.select().from(runs).where(
31027
- and30(
31028
- eq37(runs.projectId, projectId),
31029
- eq37(runs.kind, thisRun.kind),
31030
- or5(eq37(runs.status, "completed"), eq37(runs.status, "partial"))
32587
+ and32(
32588
+ eq39(runs.projectId, projectId),
32589
+ eq39(runs.kind, thisRun.kind),
32590
+ or5(eq39(runs.status, "completed"), eq39(runs.status, "partial"))
31031
32591
  )
31032
- ).orderBy(desc17(runs.createdAt), desc17(runs.id)).limit(RECENT_FETCH_LIMIT).all();
32592
+ ).orderBy(desc19(runs.createdAt), desc19(runs.id)).limit(RECENT_FETCH_LIMIT).all();
31033
32593
  const groups = groupRunsByCreatedAt(recentRuns);
31034
32594
  const currentGroupIdx = groups.findIndex((g) => g[0]?.createdAt === thisRun.createdAt);
31035
32595
  if (currentGroupIdx < 0) return [];
@@ -31044,13 +32604,13 @@ var Notifier = class {
31044
32604
  provider: querySnapshots.provider,
31045
32605
  location: querySnapshots.location,
31046
32606
  citationState: querySnapshots.citationState
31047
- }).from(querySnapshots).leftJoin(queries, eq37(querySnapshots.queryId, queries.id)).where(inArray12(querySnapshots.runId, currentRunIds)).all();
32607
+ }).from(querySnapshots).leftJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(inArray14(querySnapshots.runId, currentRunIds)).all();
31048
32608
  const previousSnapshots = this.db.select({
31049
32609
  queryId: querySnapshots.queryId,
31050
32610
  provider: querySnapshots.provider,
31051
32611
  location: querySnapshots.location,
31052
32612
  citationState: querySnapshots.citationState
31053
- }).from(querySnapshots).where(inArray12(querySnapshots.runId, previousRunIds)).all();
32613
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, previousRunIds)).all();
31054
32614
  const prevMap = /* @__PURE__ */ new Map();
31055
32615
  for (const s of previousSnapshots) {
31056
32616
  if (s.queryId == null) continue;
@@ -31077,23 +32637,23 @@ var Notifier = class {
31077
32637
  const targetLabel = redactNotificationUrl(url).urlDisplay;
31078
32638
  const targetCheck = await resolveWebhookTarget(url);
31079
32639
  if (!targetCheck.ok) {
31080
- log10.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
32640
+ log12.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
31081
32641
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
31082
32642
  return;
31083
32643
  }
31084
- log10.info("webhook.send", { event: payload.event, url: targetLabel });
32644
+ log12.info("webhook.send", { event: payload.event, url: targetLabel });
31085
32645
  const maxRetries = 3;
31086
32646
  const delays = [1e3, 4e3, 16e3];
31087
32647
  for (let attempt = 0; attempt < maxRetries; attempt++) {
31088
32648
  try {
31089
32649
  const response = await deliverWebhook(targetCheck.target, payload, webhookSecret);
31090
32650
  if (response.status >= 200 && response.status < 300) {
31091
- log10.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
32651
+ log12.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
31092
32652
  this.logDelivery(projectId, notificationId, payload.event, "sent", null);
31093
32653
  return;
31094
32654
  }
31095
32655
  const errorDetail = response.error ?? `HTTP ${response.status}`;
31096
- log10.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
32656
+ log12.warn("webhook.attempt-failed", { event: payload.event, url: targetLabel, attempt: attempt + 1, maxRetries, httpStatus: response.status, error: errorDetail });
31097
32657
  if (attempt === maxRetries - 1) {
31098
32658
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
31099
32659
  }
@@ -31101,7 +32661,7 @@ var Notifier = class {
31101
32661
  const errorDetail = err instanceof Error ? err.message : String(err);
31102
32662
  if (attempt === maxRetries - 1) {
31103
32663
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
31104
- log10.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
32664
+ log12.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
31105
32665
  }
31106
32666
  }
31107
32667
  if (attempt < maxRetries - 1) {
@@ -31111,7 +32671,7 @@ var Notifier = class {
31111
32671
  }
31112
32672
  logDelivery(projectId, notificationId, event, status, error) {
31113
32673
  this.db.insert(auditLog).values({
31114
- id: crypto32.randomUUID(),
32674
+ id: crypto37.randomUUID(),
31115
32675
  projectId,
31116
32676
  actor: "scheduler",
31117
32677
  action: `notification.${status}`,
@@ -31124,8 +32684,8 @@ var Notifier = class {
31124
32684
  };
31125
32685
 
31126
32686
  // src/run-coordinator.ts
31127
- import { eq as eq38 } from "drizzle-orm";
31128
- var log11 = createLogger("RunCoordinator");
32687
+ import { eq as eq40 } from "drizzle-orm";
32688
+ var log13 = createLogger("RunCoordinator");
31129
32689
  var RunCoordinator = class {
31130
32690
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
31131
32691
  this.db = db;
@@ -31135,10 +32695,10 @@ var RunCoordinator = class {
31135
32695
  this.onAeroEvent = onAeroEvent;
31136
32696
  }
31137
32697
  async onRunCompleted(runId, projectId) {
31138
- const runRow = this.db.select().from(runs).where(eq38(runs.id, runId)).get();
32698
+ const runRow = this.db.select().from(runs).where(eq40(runs.id, runId)).get();
31139
32699
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
31140
32700
  if (runRow?.trigger === RunTriggers.probe) {
31141
- log11.info("probe.skip-side-effects", { runId, projectId, kind });
32701
+ log13.info("probe.skip-side-effects", { runId, projectId, kind });
31142
32702
  return;
31143
32703
  }
31144
32704
  let insightCount = 0;
@@ -31155,18 +32715,18 @@ var RunCoordinator = class {
31155
32715
  try {
31156
32716
  await this.onInsightsGenerated(runId, projectId, result);
31157
32717
  } catch (err) {
31158
- log11.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
32718
+ log13.error("insight-webhook.failed", { runId, error: err instanceof Error ? err.message : String(err) });
31159
32719
  }
31160
32720
  }
31161
32721
  }
31162
32722
  } catch (err) {
31163
- log11.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
32723
+ log13.error("intelligence.failed", { runId, error: err instanceof Error ? err.message : String(err) });
31164
32724
  }
31165
32725
  }
31166
32726
  try {
31167
32727
  await this.notifier.onRunCompleted(runId, projectId);
31168
32728
  } catch (err) {
31169
- log11.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
32729
+ log13.error("notifier.failed", { runId, error: err instanceof Error ? err.message : String(err) });
31170
32730
  }
31171
32731
  if (this.onAeroEvent) {
31172
32732
  try {
@@ -31179,7 +32739,7 @@ var RunCoordinator = class {
31179
32739
  };
31180
32740
  await this.onAeroEvent(ctx);
31181
32741
  } catch (err) {
31182
- log11.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
32742
+ log13.error("aero.failed", { runId, error: err instanceof Error ? err.message : String(err) });
31183
32743
  }
31184
32744
  }
31185
32745
  }
@@ -31194,7 +32754,7 @@ var RunCoordinator = class {
31194
32754
  * so the Aero queue is never starved of a follow-up.
31195
32755
  */
31196
32756
  buildDiscoveryAeroContext(runId, projectId, status, error) {
31197
- const session = this.db.select().from(discoverySessions).where(eq38(discoverySessions.runId, runId)).get();
32757
+ const session = this.db.select().from(discoverySessions).where(eq40(discoverySessions.runId, runId)).get();
31198
32758
  const competitorMap = session ? session.competitorMap : [];
31199
32759
  return {
31200
32760
  kind: RunKinds["aeo-discover-probe"],
@@ -31216,12 +32776,12 @@ var RunCoordinator = class {
31216
32776
  };
31217
32777
 
31218
32778
  // src/agent/session-registry.ts
31219
- import crypto34 from "crypto";
31220
- import { eq as eq40 } from "drizzle-orm";
32779
+ import crypto39 from "crypto";
32780
+ import { eq as eq42 } from "drizzle-orm";
31221
32781
 
31222
32782
  // src/agent/session.ts
31223
- import fs14 from "fs";
31224
- import path14 from "path";
32783
+ import fs15 from "fs";
32784
+ import path15 from "path";
31225
32785
  import { Agent } from "@mariozechner/pi-agent-core";
31226
32786
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
31227
32787
 
@@ -31361,26 +32921,26 @@ function buildAgentProvidersResponse(config) {
31361
32921
  }
31362
32922
 
31363
32923
  // src/agent/skill-paths.ts
31364
- import fs12 from "fs";
31365
- import path12 from "path";
31366
- import { fileURLToPath } from "url";
32924
+ import fs13 from "fs";
32925
+ import path13 from "path";
32926
+ import { fileURLToPath as fileURLToPath2 } from "url";
31367
32927
  function resolveAeroSkillDir(pkgDir) {
31368
- const here = pkgDir ?? path12.dirname(fileURLToPath(import.meta.url));
32928
+ const here = pkgDir ?? path13.dirname(fileURLToPath2(import.meta.url));
31369
32929
  const candidates = [
31370
- path12.join(here, "../assets/agent-workspace/skills/aero"),
31371
- path12.join(here, "../../assets/agent-workspace/skills/aero"),
31372
- path12.join(here, "../../../../skills/aero")
32930
+ path13.join(here, "../assets/agent-workspace/skills/aero"),
32931
+ path13.join(here, "../../assets/agent-workspace/skills/aero"),
32932
+ path13.join(here, "../../../../skills/aero")
31373
32933
  ];
31374
32934
  for (const candidate of candidates) {
31375
- if (fs12.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
32935
+ if (fs13.existsSync(path13.join(candidate, "SKILL.md"))) return candidate;
31376
32936
  }
31377
32937
  throw new Error(`Aero skill not found. Searched:
31378
32938
  ${candidates.join("\n ")}`);
31379
32939
  }
31380
32940
 
31381
32941
  // src/agent/skill-tools.ts
31382
- import fs13 from "fs";
31383
- import path13 from "path";
32942
+ import fs14 from "fs";
32943
+ import path14 from "path";
31384
32944
  import { Type } from "@sinclair/typebox";
31385
32945
  var MAX_DOC_CHARS = 2e4;
31386
32946
  function textResult(details) {
@@ -31389,7 +32949,7 @@ function textResult(details) {
31389
32949
  details
31390
32950
  };
31391
32951
  }
31392
- function parseDescription(body) {
32952
+ function parseDescription2(body) {
31393
32953
  if (!body.startsWith("---")) return "(no description)";
31394
32954
  const end = body.indexOf("\n---", 3);
31395
32955
  if (end === -1) return "(no description)";
@@ -31401,16 +32961,16 @@ function parseDescription(body) {
31401
32961
  return "(no description)";
31402
32962
  }
31403
32963
  function scanSkillDocs(skillDir) {
31404
- const refsDir = path13.join(skillDir ?? resolveAeroSkillDir(), "references");
31405
- if (!fs13.existsSync(refsDir)) return [];
32964
+ const refsDir = path14.join(skillDir ?? resolveAeroSkillDir(), "references");
32965
+ if (!fs14.existsSync(refsDir)) return [];
31406
32966
  const entries = [];
31407
- for (const file of fs13.readdirSync(refsDir)) {
32967
+ for (const file of fs14.readdirSync(refsDir)) {
31408
32968
  if (!file.endsWith(".md")) continue;
31409
- const filePath = path13.join(refsDir, file);
31410
- const body = fs13.readFileSync(filePath, "utf-8");
32969
+ const filePath = path14.join(refsDir, file);
32970
+ const body = fs14.readFileSync(filePath, "utf-8");
31411
32971
  entries.push({
31412
32972
  slug: file.replace(/\.md$/, ""),
31413
- description: parseDescription(body),
32973
+ description: parseDescription2(body),
31414
32974
  bytes: Buffer.byteLength(body, "utf-8")
31415
32975
  });
31416
32976
  }
@@ -31450,8 +33010,8 @@ function buildReadSkillDocTool() {
31450
33010
  availableSlugs: docs.map((d) => d.slug)
31451
33011
  });
31452
33012
  }
31453
- const filePath = path13.join(skillDir, "references", `${match.slug}.md`);
31454
- const content = fs13.readFileSync(filePath, "utf-8");
33013
+ const filePath = path14.join(skillDir, "references", `${match.slug}.md`);
33014
+ const content = fs14.readFileSync(filePath, "utf-8");
31455
33015
  if (content.length > MAX_DOC_CHARS) {
31456
33016
  return textResult({
31457
33017
  slug: match.slug,
@@ -31545,10 +33105,10 @@ function ensureBuiltinsRegistered() {
31545
33105
  }
31546
33106
  function loadAeroSystemPrompt(pkgDir) {
31547
33107
  const skillDir = resolveAeroSkillDir(pkgDir);
31548
- const skillBody = fs14.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
31549
- const soulPath = path14.join(skillDir, "soul.md");
31550
- if (!fs14.existsSync(soulPath)) return skillBody;
31551
- const soulBody = fs14.readFileSync(soulPath, "utf-8");
33108
+ const skillBody = fs15.readFileSync(path15.join(skillDir, "SKILL.md"), "utf-8");
33109
+ const soulPath = path15.join(skillDir, "soul.md");
33110
+ if (!fs15.existsSync(soulPath)) return skillBody;
33111
+ const soulBody = fs15.readFileSync(soulPath, "utf-8");
31552
33112
  return `${soulBody.trimEnd()}
31553
33113
 
31554
33114
  ---
@@ -31605,8 +33165,8 @@ function resolveSessionProviderAndModel(config, opts) {
31605
33165
  }
31606
33166
 
31607
33167
  // src/agent/memory-store.ts
31608
- import crypto33 from "crypto";
31609
- import { and as and31, desc as desc18, eq as eq39, like as like2, sql as sql16 } from "drizzle-orm";
33168
+ import crypto38 from "crypto";
33169
+ import { and as and33, desc as desc20, eq as eq41, like as like2, sql as sql16 } from "drizzle-orm";
31610
33170
  var COMPACTION_KEY_PREFIX = "compaction:";
31611
33171
  var COMPACTION_NOTES_PER_SESSION = 3;
31612
33172
  function rowToDto2(row) {
@@ -31620,7 +33180,7 @@ function rowToDto2(row) {
31620
33180
  };
31621
33181
  }
31622
33182
  function listMemoryEntries(db, projectId, opts = {}) {
31623
- const query = db.select().from(agentMemory).where(eq39(agentMemory.projectId, projectId)).orderBy(desc18(agentMemory.updatedAt));
33183
+ const query = db.select().from(agentMemory).where(eq41(agentMemory.projectId, projectId)).orderBy(desc20(agentMemory.updatedAt));
31624
33184
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
31625
33185
  return rows.map(rowToDto2);
31626
33186
  }
@@ -31634,7 +33194,7 @@ function upsertMemoryEntry(db, args) {
31634
33194
  throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
31635
33195
  }
31636
33196
  const now = (/* @__PURE__ */ new Date()).toISOString();
31637
- const id = crypto33.randomUUID();
33197
+ const id = crypto38.randomUUID();
31638
33198
  db.insert(agentMemory).values({
31639
33199
  id,
31640
33200
  projectId: args.projectId,
@@ -31651,12 +33211,12 @@ function upsertMemoryEntry(db, args) {
31651
33211
  updatedAt: now
31652
33212
  }
31653
33213
  }).run();
31654
- const row = db.select().from(agentMemory).where(and31(eq39(agentMemory.projectId, args.projectId), eq39(agentMemory.key, args.key))).get();
33214
+ const row = db.select().from(agentMemory).where(and33(eq41(agentMemory.projectId, args.projectId), eq41(agentMemory.key, args.key))).get();
31655
33215
  if (!row) throw new Error("memory upsert produced no row");
31656
33216
  return rowToDto2(row);
31657
33217
  }
31658
33218
  function deleteMemoryEntry(db, projectId, key) {
31659
- const result = db.delete(agentMemory).where(and31(eq39(agentMemory.projectId, projectId), eq39(agentMemory.key, key))).run();
33219
+ const result = db.delete(agentMemory).where(and33(eq41(agentMemory.projectId, projectId), eq41(agentMemory.key, key))).run();
31660
33220
  const changes = result.changes ?? 0;
31661
33221
  return changes > 0;
31662
33222
  }
@@ -31671,7 +33231,7 @@ function writeCompactionNote(db, args) {
31671
33231
  }
31672
33232
  const now = (/* @__PURE__ */ new Date()).toISOString();
31673
33233
  const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
31674
- const id = crypto33.randomUUID();
33234
+ const id = crypto38.randomUUID();
31675
33235
  let inserted;
31676
33236
  db.transaction((tx) => {
31677
33237
  tx.insert(agentMemory).values({
@@ -31685,16 +33245,16 @@ function writeCompactionNote(db, args) {
31685
33245
  }).run();
31686
33246
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
31687
33247
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
31688
- and31(
31689
- eq39(agentMemory.projectId, args.projectId),
33248
+ and33(
33249
+ eq41(agentMemory.projectId, args.projectId),
31690
33250
  like2(agentMemory.key, `${sessionPrefix}%`)
31691
33251
  )
31692
- ).orderBy(desc18(agentMemory.updatedAt)).all();
33252
+ ).orderBy(desc20(agentMemory.updatedAt)).all();
31693
33253
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
31694
33254
  if (stale.length > 0) {
31695
33255
  tx.delete(agentMemory).where(sql16`${agentMemory.id} IN (${sql16.join(stale.map((s) => sql16`${s}`), sql16`, `)})`).run();
31696
33256
  }
31697
- const row = tx.select().from(agentMemory).where(and31(eq39(agentMemory.projectId, args.projectId), eq39(agentMemory.key, key))).get();
33257
+ const row = tx.select().from(agentMemory).where(and33(eq41(agentMemory.projectId, args.projectId), eq41(agentMemory.key, key))).get();
31698
33258
  if (row) inserted = rowToDto2(row);
31699
33259
  });
31700
33260
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -31827,7 +33387,7 @@ async function compactMessages(args) {
31827
33387
  }
31828
33388
 
31829
33389
  // src/agent/session-registry.ts
31830
- var log12 = createLogger("SessionRegistry");
33390
+ var log14 = createLogger("SessionRegistry");
31831
33391
  var MAX_HYDRATE_NOTES = 20;
31832
33392
  var MAX_HYDRATE_BYTES = 32 * 1024;
31833
33393
  function escapeMemoryFragment(value) {
@@ -31876,7 +33436,7 @@ var SessionRegistry = class {
31876
33436
  modelProvider: effectiveProvider,
31877
33437
  modelId: effectiveModelId,
31878
33438
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
31879
- }).where(eq40(agentSessions.projectId, projectId)).run();
33439
+ }).where(eq42(agentSessions.projectId, projectId)).run();
31880
33440
  }
31881
33441
  const agent2 = createAeroSession({
31882
33442
  projectName,
@@ -32054,13 +33614,13 @@ ${lines.join("\n")}
32054
33614
  agent.state.messages = result.messages;
32055
33615
  agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
32056
33616
  this.save(projectName);
32057
- log12.info("compaction.completed", {
33617
+ log14.info("compaction.completed", {
32058
33618
  projectName,
32059
33619
  removedCount: result.removedCount,
32060
33620
  summaryBytes: Buffer.byteLength(result.summary, "utf8")
32061
33621
  });
32062
33622
  } catch (err) {
32063
- log12.error("compaction.failed", {
33623
+ log14.error("compaction.failed", {
32064
33624
  projectName,
32065
33625
  error: err instanceof Error ? err.message : String(err)
32066
33626
  });
@@ -32090,7 +33650,7 @@ ${lines.join("\n")}
32090
33650
  modelProvider: nextProvider,
32091
33651
  modelId: nextModelId,
32092
33652
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
32093
- }).where(eq40(agentSessions.projectId, projectId)).run();
33653
+ }).where(eq42(agentSessions.projectId, projectId)).run();
32094
33654
  }
32095
33655
  /** Persist a session's transcript back to the DB. Call after any run settles. */
32096
33656
  save(projectName) {
@@ -32157,7 +33717,7 @@ ${lines.join("\n")}
32157
33717
  await agent.prompt(msgs);
32158
33718
  this.save(projectName);
32159
33719
  } catch (err) {
32160
- log12.error("drain.failed", {
33720
+ log14.error("drain.failed", {
32161
33721
  projectName,
32162
33722
  error: err instanceof Error ? err.message : String(err)
32163
33723
  });
@@ -32252,17 +33812,17 @@ ${lines.join("\n")}
32252
33812
  return id;
32253
33813
  }
32254
33814
  tryResolveProjectId(projectName) {
32255
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq40(projects.name, projectName)).get();
33815
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq42(projects.name, projectName)).get();
32256
33816
  return row?.id;
32257
33817
  }
32258
33818
  loadRow(projectId) {
32259
- const row = this.opts.db.select().from(agentSessions).where(eq40(agentSessions.projectId, projectId)).get();
33819
+ const row = this.opts.db.select().from(agentSessions).where(eq42(agentSessions.projectId, projectId)).get();
32260
33820
  return row ?? null;
32261
33821
  }
32262
33822
  insertRow(params) {
32263
33823
  const now = (/* @__PURE__ */ new Date()).toISOString();
32264
33824
  this.opts.db.insert(agentSessions).values({
32265
- id: crypto34.randomUUID(),
33825
+ id: crypto39.randomUUID(),
32266
33826
  projectId: params.projectId,
32267
33827
  systemPrompt: params.systemPrompt,
32268
33828
  modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
@@ -32275,14 +33835,14 @@ ${lines.join("\n")}
32275
33835
  }
32276
33836
  updateRow(projectId, patch) {
32277
33837
  const now = (/* @__PURE__ */ new Date()).toISOString();
32278
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq40(agentSessions.projectId, projectId)).run();
33838
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq42(agentSessions.projectId, projectId)).run();
32279
33839
  }
32280
33840
  };
32281
33841
 
32282
33842
  // src/agent/agent-routes.ts
32283
- import { eq as eq41 } from "drizzle-orm";
33843
+ import { eq as eq43 } from "drizzle-orm";
32284
33844
  function resolveProject2(db, name) {
32285
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq41(projects.name, name)).get();
33845
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq43(projects.name, name)).get();
32286
33846
  if (!row) throw notFound("project", name);
32287
33847
  return row;
32288
33848
  }
@@ -32291,7 +33851,7 @@ function registerAgentRoutes(app, opts) {
32291
33851
  "/projects/:name/agent/transcript",
32292
33852
  async (request) => {
32293
33853
  const project = resolveProject2(opts.db, request.params.name);
32294
- const row = opts.db.select().from(agentSessions).where(eq41(agentSessions.projectId, project.id)).get();
33854
+ const row = opts.db.select().from(agentSessions).where(eq43(agentSessions.projectId, project.id)).get();
32295
33855
  if (!row) {
32296
33856
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
32297
33857
  }
@@ -32315,7 +33875,7 @@ function registerAgentRoutes(app, opts) {
32315
33875
  async (request) => {
32316
33876
  const project = resolveProject2(opts.db, request.params.name);
32317
33877
  opts.sessionRegistry.reset(project.name);
32318
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq41(agentSessions.projectId, project.id)).run();
33878
+ opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq43(agentSessions.projectId, project.id)).run();
32319
33879
  return { status: "reset" };
32320
33880
  }
32321
33881
  );
@@ -32574,13 +34134,13 @@ function extractHostname(domain) {
32574
34134
  function fetchWithPinnedAddress(target) {
32575
34135
  return new Promise((resolve) => {
32576
34136
  const port = target.url.port ? Number(target.url.port) : 443;
32577
- const path16 = target.url.pathname + target.url.search;
34137
+ const path17 = target.url.pathname + target.url.search;
32578
34138
  const req = https2.request(
32579
34139
  {
32580
34140
  hostname: target.address,
32581
34141
  family: target.family,
32582
34142
  port,
32583
- path: path16,
34143
+ path: path17,
32584
34144
  method: "GET",
32585
34145
  timeout: FETCH_TIMEOUT_MS2,
32586
34146
  servername: target.url.hostname,
@@ -32672,7 +34232,7 @@ function formatAuditFactorScore(factor) {
32672
34232
  }
32673
34233
 
32674
34234
  // src/snapshot-service.ts
32675
- var log13 = createLogger("Snapshot");
34235
+ var log15 = createLogger("Snapshot");
32676
34236
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
32677
34237
  var SNAPSHOT_QUERY_COUNT = 6;
32678
34238
  var ProviderExecutionGate2 = class {
@@ -32815,7 +34375,7 @@ var SnapshotService = class {
32815
34375
  return mapAuditReport(report);
32816
34376
  } catch (err) {
32817
34377
  const message = err instanceof Error ? err.message : String(err);
32818
- log13.warn("audit.failed", { homepageUrl, error: message });
34378
+ log15.warn("audit.failed", { homepageUrl, error: message });
32819
34379
  return {
32820
34380
  url: homepageUrl,
32821
34381
  finalUrl: homepageUrl,
@@ -32845,7 +34405,7 @@ var SnapshotService = class {
32845
34405
  queries: parsedQueries
32846
34406
  };
32847
34407
  } catch (err) {
32848
- log13.warn("profile.generation-failed", {
34408
+ log15.warn("profile.generation-failed", {
32849
34409
  domain: ctx.domain,
32850
34410
  provider: ctx.analysisProvider.adapter.name,
32851
34411
  error: err instanceof Error ? err.message : String(err)
@@ -32987,7 +34547,7 @@ var SnapshotService = class {
32987
34547
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
32988
34548
  };
32989
34549
  } catch (err) {
32990
- log13.warn("response.analysis-failed", {
34550
+ log15.warn("response.analysis-failed", {
32991
34551
  provider: ctx.analysisProvider.adapter.name,
32992
34552
  error: err instanceof Error ? err.message : String(err)
32993
34553
  });
@@ -33272,7 +34832,7 @@ function clipText(value, length) {
33272
34832
  // src/server.ts
33273
34833
  var _require3 = createRequire4(import.meta.url);
33274
34834
  var { version: PKG_VERSION2 } = _require3("../package.json");
33275
- var log14 = createLogger("Server");
34835
+ var log16 = createLogger("Server");
33276
34836
  var DEFAULT_QUOTA = {
33277
34837
  maxConcurrency: 2,
33278
34838
  maxRequestsPerMinute: 10,
@@ -33302,14 +34862,14 @@ function summarizeProviderConfig(provider, config) {
33302
34862
  };
33303
34863
  }
33304
34864
  function hashApiKey(key) {
33305
- return crypto35.createHash("sha256").update(key).digest("hex");
34865
+ return crypto40.createHash("sha256").update(key).digest("hex");
33306
34866
  }
33307
34867
  var DASHBOARD_SCRYPT_KEYLEN = 64;
33308
34868
  var DASHBOARD_SCRYPT_COST = 1 << 15;
33309
34869
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
33310
34870
  function hashDashboardPassword(password) {
33311
- const salt = crypto35.randomBytes(16);
33312
- const derived = crypto35.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
34871
+ const salt = crypto40.randomBytes(16);
34872
+ const derived = crypto40.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
33313
34873
  N: DASHBOARD_SCRYPT_COST,
33314
34874
  maxmem: DASHBOARD_SCRYPT_MAXMEM
33315
34875
  });
@@ -33330,18 +34890,18 @@ function verifyDashboardPassword(password, storedHash) {
33330
34890
  } catch {
33331
34891
  return { ok: false, needsRehash: false };
33332
34892
  }
33333
- const derived = crypto35.scryptSync(password, salt, expected.length, {
34893
+ const derived = crypto40.scryptSync(password, salt, expected.length, {
33334
34894
  N: DASHBOARD_SCRYPT_COST,
33335
34895
  maxmem: DASHBOARD_SCRYPT_MAXMEM
33336
34896
  });
33337
34897
  if (derived.length !== expected.length) return { ok: false, needsRehash: false };
33338
- return { ok: crypto35.timingSafeEqual(derived, expected), needsRehash: false };
34898
+ return { ok: crypto40.timingSafeEqual(derived, expected), needsRehash: false };
33339
34899
  }
33340
34900
  if (/^[a-f0-9]{64}$/i.test(storedHash)) {
33341
34901
  const candidate = Buffer.from(hashApiKey(password), "hex");
33342
34902
  const expected = Buffer.from(storedHash, "hex");
33343
34903
  if (candidate.length !== expected.length) return { ok: false, needsRehash: false };
33344
- const ok = crypto35.timingSafeEqual(candidate, expected);
34904
+ const ok = crypto40.timingSafeEqual(candidate, expected);
33345
34905
  return { ok, needsRehash: ok };
33346
34906
  }
33347
34907
  return { ok: false, needsRehash: false };
@@ -33400,7 +34960,7 @@ function applyLegacyCredentials(rows, config) {
33400
34960
  }
33401
34961
  if (migratedGoogle > 0) {
33402
34962
  saveConfigPatch({ google: config.google });
33403
- log14.info("credentials.migrated", { type: "google", count: migratedGoogle });
34963
+ log16.info("credentials.migrated", { type: "google", count: migratedGoogle });
33404
34964
  }
33405
34965
  let migratedGa4 = 0;
33406
34966
  for (const row of rows.ga4) {
@@ -33418,7 +34978,7 @@ function applyLegacyCredentials(rows, config) {
33418
34978
  }
33419
34979
  if (migratedGa4 > 0) {
33420
34980
  saveConfigPatch({ ga4: config.ga4 });
33421
- log14.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
34981
+ log16.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
33422
34982
  }
33423
34983
  }
33424
34984
  async function createServer(opts) {
@@ -33450,11 +35010,11 @@ async function createServer(opts) {
33450
35010
  applyLegacyCredentials(legacyRows, opts.config);
33451
35011
  dropLegacyCredentialColumns(opts.db);
33452
35012
  } catch (err) {
33453
- log14.warn("credentials.migration.failed", {
35013
+ log16.warn("credentials.migration.failed", {
33454
35014
  error: err instanceof Error ? err.message : String(err)
33455
35015
  });
33456
35016
  }
33457
- log14.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
35017
+ log16.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
33458
35018
  const p = providers[k];
33459
35019
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
33460
35020
  }) });
@@ -33503,7 +35063,7 @@ async function createServer(opts) {
33503
35063
  intelligenceService,
33504
35064
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
33505
35065
  async (ctx) => {
33506
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq42(projects.id, ctx.projectId)).get();
35066
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq44(projects.id, ctx.projectId)).get();
33507
35067
  if (!project) return;
33508
35068
  let content;
33509
35069
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -33526,8 +35086,8 @@ async function createServer(opts) {
33526
35086
  );
33527
35087
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
33528
35088
  const snapshotService = new SnapshotService(registry);
33529
- const orphanedOpenClawDir = path15.join(os6.homedir(), ".openclaw-aero");
33530
- if (fs15.existsSync(orphanedOpenClawDir)) {
35089
+ const orphanedOpenClawDir = path16.join(os7.homedir(), ".openclaw-aero");
35090
+ if (fs16.existsSync(orphanedOpenClawDir)) {
33531
35091
  app.log.warn(
33532
35092
  { path: orphanedOpenClawDir },
33533
35093
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -33660,7 +35220,7 @@ async function createServer(opts) {
33660
35220
  return removed;
33661
35221
  }
33662
35222
  };
33663
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto35.randomBytes(32).toString("hex");
35223
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto40.randomBytes(32).toString("hex");
33664
35224
  const googleConnectionStore = {
33665
35225
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
33666
35226
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -33706,11 +35266,11 @@ async function createServer(opts) {
33706
35266
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
33707
35267
  if (opts.config.apiKey) {
33708
35268
  const keyHash = hashApiKey(opts.config.apiKey);
33709
- const existing = opts.db.select().from(apiKeys).where(eq42(apiKeys.keyHash, keyHash)).get();
35269
+ const existing = opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, keyHash)).get();
33710
35270
  if (!existing) {
33711
35271
  const prefix = opts.config.apiKey.slice(0, 12);
33712
35272
  opts.db.insert(apiKeys).values({
33713
- id: `key_${crypto35.randomBytes(8).toString("hex")}`,
35273
+ id: `key_${crypto40.randomBytes(8).toString("hex")}`,
33714
35274
  name: "default",
33715
35275
  keyHash,
33716
35276
  keyPrefix: prefix,
@@ -33734,7 +35294,7 @@ async function createServer(opts) {
33734
35294
  };
33735
35295
  const createSession = (apiKeyId) => {
33736
35296
  pruneExpiredSessions();
33737
- const sessionId = crypto35.randomBytes(32).toString("hex");
35297
+ const sessionId = crypto40.randomBytes(32).toString("hex");
33738
35298
  sessions.set(sessionId, {
33739
35299
  apiKeyId,
33740
35300
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -33758,7 +35318,7 @@ async function createServer(opts) {
33758
35318
  };
33759
35319
  const getDefaultApiKey = () => {
33760
35320
  if (!opts.config.apiKey) return void 0;
33761
- return opts.db.select().from(apiKeys).where(eq42(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
35321
+ return opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
33762
35322
  };
33763
35323
  const createPasswordSession = (reply) => {
33764
35324
  const key = getDefaultApiKey();
@@ -33820,12 +35380,12 @@ async function createServer(opts) {
33820
35380
  return reply.send({ authenticated: true });
33821
35381
  }
33822
35382
  if (apiKey) {
33823
- const key = opts.db.select().from(apiKeys).where(eq42(apiKeys.keyHash, hashApiKey(apiKey))).get();
35383
+ const key = opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, hashApiKey(apiKey))).get();
33824
35384
  if (!key || key.revokedAt) {
33825
35385
  const err2 = authInvalid();
33826
35386
  return reply.status(err2.statusCode).send(err2.toJSON());
33827
35387
  }
33828
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq42(apiKeys.id, key.id)).run();
35388
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq44(apiKeys.id, key.id)).run();
33829
35389
  const sessionId = createSession(key.id);
33830
35390
  reply.header("set-cookie", serializeSessionCookie({
33831
35391
  name: SESSION_COOKIE_NAME,
@@ -33896,9 +35456,20 @@ async function createServer(opts) {
33896
35456
  const configPath = getConfigPath();
33897
35457
  return {
33898
35458
  databasePath: opts.config.database,
33899
- configPath: fs15.existsSync(configPath) ? configPath : null
35459
+ configPath: fs16.existsSync(configPath) ? configPath : null
33900
35460
  };
33901
35461
  })(),
35462
+ // Snapshot the bundled skill trees (version + file hashes) so the
35463
+ // `agent.skills.current` doctor check can flag a `~/.claude/skills/` install
35464
+ // that has drifted behind this build. Best-effort: if the bundled assets
35465
+ // can't be resolved the check simply skips rather than failing boot.
35466
+ bundledSkills: (() => {
35467
+ try {
35468
+ return getBundledSkillSnapshots();
35469
+ } catch {
35470
+ return void 0;
35471
+ }
35472
+ })(),
33902
35473
  // Local canonry serve runs on the operator's machine, where pointing a
33903
35474
  // webhook at localhost (Discord test container, Pipedream-mock dev server,
33904
35475
  // etc.) is a legitimate workflow. Default to allowing it for the local
@@ -33923,7 +35494,7 @@ async function createServer(opts) {
33923
35494
  executeGscSync(opts.db, runId, projectId, {
33924
35495
  ...syncOpts,
33925
35496
  config: opts.config
33926
- }).catch((err) => {
35497
+ }).then(() => maybeRefreshGscCoverage(opts.db, opts.config, projectId)).catch((err) => {
33927
35498
  app.log.error({ runId, err }, "GSC sync failed");
33928
35499
  });
33929
35500
  },
@@ -33940,6 +35511,19 @@ async function createServer(opts) {
33940
35511
  app.log.error({ runId, err }, "Inspect sitemap failed");
33941
35512
  });
33942
35513
  },
35514
+ onGbpSyncRequested: (runId, projectId, syncOpts) => {
35515
+ const { clientId: googleClientId, clientSecret: googleClientSecret } = getGoogleAuthConfig(opts.config);
35516
+ if (!googleClientId || !googleClientSecret) {
35517
+ app.log.error("GBP sync requested but Google OAuth credentials are not configured in the local config");
35518
+ return;
35519
+ }
35520
+ executeGbpSync(opts.db, runId, projectId, {
35521
+ ...syncOpts,
35522
+ config: opts.config
35523
+ }).catch((err) => {
35524
+ app.log.error({ runId, err }, "GBP sync failed");
35525
+ });
35526
+ },
33943
35527
  getBacklinksStatus: () => ({
33944
35528
  duckdbInstalled: isDuckdbInstalled(),
33945
35529
  duckdbVersion: readInstalledVersion() ?? void 0,
@@ -33961,7 +35545,7 @@ async function createServer(opts) {
33961
35545
  deps: {
33962
35546
  enqueueAutoExtract: ({ projectId, release: r }) => {
33963
35547
  const now = (/* @__PURE__ */ new Date()).toISOString();
33964
- const runId = crypto35.randomUUID();
35548
+ const runId = crypto40.randomUUID();
33965
35549
  opts.db.insert(runs).values({
33966
35550
  id: runId,
33967
35551
  projectId,
@@ -34043,6 +35627,12 @@ async function createServer(opts) {
34043
35627
  executeBingInspectSitemap(opts.db, runId, projectId, {
34044
35628
  ...inspectOpts,
34045
35629
  config: opts.config
35630
+ }).then(() => {
35631
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq44(runs.id, runId)).get();
35632
+ if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
35633
+ return maybeRefreshGscCoverage(opts.db, opts.config, projectId);
35634
+ }
35635
+ return null;
34046
35636
  }).catch((err) => {
34047
35637
  app.log.error({ runId, err }, "Bing inspect sitemap failed");
34048
35638
  });
@@ -34125,7 +35715,7 @@ async function createServer(opts) {
34125
35715
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
34126
35716
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
34127
35717
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
34128
- id: crypto35.randomUUID(),
35718
+ id: crypto40.randomUUID(),
34129
35719
  projectId,
34130
35720
  actor: "api",
34131
35721
  action: existing ? "provider.updated" : "provider.created",
@@ -34269,10 +35859,10 @@ async function createServer(opts) {
34269
35859
  return snapshotService.createReport(input);
34270
35860
  }
34271
35861
  });
34272
- const dirname = path15.dirname(fileURLToPath2(import.meta.url));
34273
- const assetsDir = path15.join(dirname, "..", "assets");
34274
- if (fs15.existsSync(assetsDir)) {
34275
- const indexPath = path15.join(assetsDir, "index.html");
35862
+ const dirname = path16.dirname(fileURLToPath3(import.meta.url));
35863
+ const assetsDir = path16.join(dirname, "..", "assets");
35864
+ if (fs16.existsSync(assetsDir)) {
35865
+ const indexPath = path16.join(assetsDir, "index.html");
34276
35866
  const injectConfig = (html) => {
34277
35867
  const clientConfig = {};
34278
35868
  if (basePath) clientConfig.basePath = basePath;
@@ -34304,8 +35894,8 @@ async function createServer(opts) {
34304
35894
  }
34305
35895
  });
34306
35896
  const serveIndex = (_request, reply) => {
34307
- if (fs15.existsSync(indexPath)) {
34308
- const html = fs15.readFileSync(indexPath, "utf-8");
35897
+ if (fs16.existsSync(indexPath)) {
35898
+ const html = fs16.readFileSync(indexPath, "utf-8");
34309
35899
  return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
34310
35900
  }
34311
35901
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -34325,8 +35915,8 @@ async function createServer(opts) {
34325
35915
  if (basePath && !url.startsWith(basePath)) {
34326
35916
  return reply.status(404).send({ error: "Not found", path: request.url });
34327
35917
  }
34328
- if (fs15.existsSync(indexPath)) {
34329
- const html = fs15.readFileSync(indexPath, "utf-8");
35918
+ if (fs16.existsSync(indexPath)) {
35919
+ const html = fs16.readFileSync(indexPath, "utf-8");
34330
35920
  return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
34331
35921
  }
34332
35922
  return reply.status(404).send({ error: "Not found" });
@@ -34418,6 +36008,11 @@ export {
34418
36008
  backfillTrafficClassificationCommand,
34419
36009
  renderReportHtml,
34420
36010
  setGoogleAuthConfig,
36011
+ installSkills,
36012
+ listSkills,
36013
+ emitInstallSummary,
36014
+ getMissingUserSkillsNudge,
36015
+ parseSkillsClient,
34421
36016
  formatAuditFactorScore,
34422
36017
  checkLatestVersionForCli,
34423
36018
  listAgentProviders,