@ainyc/canonry 4.57.0 → 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 (26) hide show
  1. package/assets/agent-workspace/skills/canonry/SKILL.md +7 -0
  2. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +44 -0
  3. package/assets/agent-workspace/skills/canonry/references/google-business-profile.md +219 -0
  4. package/assets/assets/{BacklinksPage-CmeFZ8UJ.js → BacklinksPage-D_mc7c-b.js} +1 -1
  5. package/assets/assets/{ChartPrimitives-D7C1Cp8w.js → ChartPrimitives-BViWneKX.js} +1 -1
  6. package/assets/assets/{ProjectPage-Y6uCyjGb.js → ProjectPage-_hpYJAN1.js} +1 -1
  7. package/assets/assets/{RunRow-BntNdrgM.js → RunRow-DK69_0iD.js} +1 -1
  8. package/assets/assets/{RunsPage-Btp6qn10.js → RunsPage-DRu1peAA.js} +1 -1
  9. package/assets/assets/{SettingsPage-DkyNiU2i.js → SettingsPage-BrednApH.js} +1 -1
  10. package/assets/assets/{TrafficPage-CBl4Mwdc.js → TrafficPage-oFA65ZZc.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BZzuWCn-.js → TrafficSourceDetailPage-CUzzaYFC.js} +1 -1
  12. package/assets/assets/{extract-error-message-De8_qAzs.js → extract-error-message-Cv4MXGtB.js} +1 -1
  13. package/assets/assets/{index-XUKhruAg.js → index-BrCh3uvb.js} +90 -90
  14. package/assets/assets/{server-traffic-bn9LSZN9.js → server-traffic-rYE-NlE-.js} +1 -1
  15. package/assets/assets/{trash-2-B5clF2rU.js → trash-2-BgGGPjQf.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-4KWPOVIT.js → chunk-JW6TQFU7.js} +171 -1
  18. package/dist/{chunk-WFVUZVJD.js → chunk-LPPW7O26.js} +1421 -1190
  19. package/dist/{chunk-HL6JZUEW.js → chunk-NOQ4ZE3E.js} +2158 -624
  20. package/dist/{chunk-6X5TF73A.js → chunk-TFBPLY77.js} +414 -1
  21. package/dist/cli.js +610 -458
  22. package/dist/index.d.ts +2 -1
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-NY3MAVPB.js → intelligence-service-V4SWVKEQ.js} +2 -2
  25. package/dist/mcp.js +10 -8
  26. package/package.json +8 -7
@@ -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 crypto36 from "crypto";
581
- import fs15 from "fs";
582
- import path15 from "path";
583
- import { fileURLToPath as fileURLToPath2 } from "url";
584
- import { eq as eq43 } 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,16 +30251,16 @@ 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
 
29082
30260
  // src/coverage-refresh.ts
29083
- import crypto29 from "crypto";
29084
- import { and as and25, desc as desc16, eq as eq32, inArray as inArray11 } from "drizzle-orm";
29085
- var log6 = createLogger("CoverageRefresh");
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");
29086
30264
  var COVERAGE_REFRESH_MIN_INTERVAL_MS = 60 * 60 * 1e3;
29087
30265
  var ACTIVE_OR_DONE_STATUSES = [
29088
30266
  RunStatuses.queued,
@@ -29092,27 +30270,27 @@ var ACTIVE_OR_DONE_STATUSES = [
29092
30270
  ];
29093
30271
  var defaultDeps = { executeInspectSitemap };
29094
30272
  async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps, nowMs = Date.now()) {
29095
- const project = db.select({ canonicalDomain: projects.canonicalDomain }).from(projects).where(eq32(projects.id, projectId)).get();
30273
+ const project = db.select({ canonicalDomain: projects.canonicalDomain }).from(projects).where(eq33(projects.id, projectId)).get();
29096
30274
  if (!project) return null;
29097
30275
  const { clientId, clientSecret } = getGoogleAuthConfig(config);
29098
30276
  if (!clientId || !clientSecret) return null;
29099
30277
  const conn = getGoogleConnection(config, project.canonicalDomain, "gsc");
29100
30278
  if (!conn?.refreshToken || !conn.propertyId) return null;
29101
30279
  const recent = db.select({ createdAt: runs.createdAt }).from(runs).where(
29102
- and25(
29103
- eq32(runs.projectId, projectId),
29104
- eq32(runs.kind, RunKinds["inspect-sitemap"]),
29105
- inArray11(runs.status, ACTIVE_OR_DONE_STATUSES)
30280
+ and26(
30281
+ eq33(runs.projectId, projectId),
30282
+ eq33(runs.kind, RunKinds["inspect-sitemap"]),
30283
+ inArray12(runs.status, ACTIVE_OR_DONE_STATUSES)
29106
30284
  )
29107
- ).orderBy(desc16(runs.createdAt)).limit(1).get();
30285
+ ).orderBy(desc17(runs.createdAt)).limit(1).get();
29108
30286
  if (recent) {
29109
30287
  const ageMs = nowMs - Date.parse(recent.createdAt);
29110
30288
  if (Number.isFinite(ageMs) && ageMs < COVERAGE_REFRESH_MIN_INTERVAL_MS) {
29111
- log6.info("skip.recent", { projectId, ageMs });
30289
+ log7.info("skip.recent", { projectId, ageMs });
29112
30290
  return null;
29113
30291
  }
29114
30292
  }
29115
- const runId = crypto29.randomUUID();
30293
+ const runId = crypto32.randomUUID();
29116
30294
  db.insert(runs).values({
29117
30295
  id: runId,
29118
30296
  projectId,
@@ -29121,11 +30299,11 @@ async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps
29121
30299
  trigger: RunTriggers.scheduled,
29122
30300
  createdAt: new Date(nowMs).toISOString()
29123
30301
  }).run();
29124
- log6.info("refresh.start", { projectId, runId });
30302
+ log7.info("refresh.start", { projectId, runId });
29125
30303
  try {
29126
30304
  await deps.executeInspectSitemap(db, runId, projectId, { config });
29127
30305
  } catch (err) {
29128
- log6.error("refresh.failed", {
30306
+ log7.error("refresh.failed", {
29129
30307
  projectId,
29130
30308
  runId,
29131
30309
  error: err instanceof Error ? err.message : String(err)
@@ -29135,10 +30313,10 @@ async function maybeRefreshGscCoverage(db, config, projectId, deps = defaultDeps
29135
30313
  }
29136
30314
 
29137
30315
  // src/commoncrawl-sync.ts
29138
- import crypto30 from "crypto";
30316
+ import crypto33 from "crypto";
29139
30317
  import path11 from "path";
29140
- import { and as and26, eq as eq33, sql as sql14 } from "drizzle-orm";
29141
- var log7 = createLogger("CommonCrawlSync");
30318
+ import { and as and27, eq as eq34, sql as sql14 } from "drizzle-orm";
30319
+ var log8 = createLogger("CommonCrawlSync");
29142
30320
  var INSERT_CHUNK_SIZE = 1e4;
29143
30321
  function defaultDeps2() {
29144
30322
  return {
@@ -29163,7 +30341,7 @@ async function executeReleaseSync(db, syncId, opts) {
29163
30341
  phaseDetail: "downloading vertices + edges",
29164
30342
  updatedAt: downloadStartedAt,
29165
30343
  error: null
29166
- }).where(eq33(ccReleaseSyncs.id, syncId)).run();
30344
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
29167
30345
  const paths = ccReleasePaths(release);
29168
30346
  const releaseCacheDir = path11.join(deps.cacheDir, release);
29169
30347
  const vertexPath = path11.join(releaseCacheDir, paths.vertexFilename);
@@ -29186,7 +30364,7 @@ async function executeReleaseSync(db, syncId, opts) {
29186
30364
  vertexSha256: vertex.sha256,
29187
30365
  edgesSha256: edges.sha256,
29188
30366
  updatedAt: downloadFinishedAt
29189
- }).where(eq33(ccReleaseSyncs.id, syncId)).run();
30367
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
29190
30368
  const allProjects = db.select().from(projects).all();
29191
30369
  const targets = Array.from(new Set(allProjects.map((p) => p.canonicalDomain)));
29192
30370
  let rows = [];
@@ -29202,15 +30380,15 @@ async function executeReleaseSync(db, syncId, opts) {
29202
30380
  }
29203
30381
  const queriedAt = deps.now().toISOString();
29204
30382
  db.transaction((tx) => {
29205
- tx.delete(backlinkDomains).where(eq33(backlinkDomains.releaseSyncId, syncId)).run();
29206
- tx.delete(backlinkSummaries).where(eq33(backlinkSummaries.releaseSyncId, syncId)).run();
30383
+ tx.delete(backlinkDomains).where(eq34(backlinkDomains.releaseSyncId, syncId)).run();
30384
+ tx.delete(backlinkSummaries).where(eq34(backlinkSummaries.releaseSyncId, syncId)).run();
29207
30385
  const expanded = [];
29208
30386
  for (const r of rows) {
29209
30387
  const projectIds = projectsByDomain.get(r.targetDomain);
29210
30388
  if (!projectIds) continue;
29211
30389
  for (const projectId of projectIds) {
29212
30390
  expanded.push({
29213
- id: crypto30.randomUUID(),
30391
+ id: crypto33.randomUUID(),
29214
30392
  projectId,
29215
30393
  releaseSyncId: syncId,
29216
30394
  release,
@@ -29230,7 +30408,7 @@ async function executeReleaseSync(db, syncId, opts) {
29230
30408
  const projectRows = rowsByProject.get(p.id) ?? [];
29231
30409
  const summary = computeSummary(projectRows);
29232
30410
  tx.insert(backlinkSummaries).values({
29233
- id: crypto30.randomUUID(),
30411
+ id: crypto33.randomUUID(),
29234
30412
  projectId: p.id,
29235
30413
  releaseSyncId: syncId,
29236
30414
  release,
@@ -29262,8 +30440,8 @@ async function executeReleaseSync(db, syncId, opts) {
29262
30440
  domainsDiscovered: rows.length,
29263
30441
  updatedAt: finishedAt,
29264
30442
  error: null
29265
- }).where(eq33(ccReleaseSyncs.id, syncId)).run();
29266
- log7.info("sync.completed", {
30443
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
30444
+ log8.info("sync.completed", {
29267
30445
  syncId,
29268
30446
  release,
29269
30447
  projectsProcessed: allProjects.length,
@@ -29275,7 +30453,7 @@ async function executeReleaseSync(db, syncId, opts) {
29275
30453
  try {
29276
30454
  deps.enqueueAutoExtract({ projectId: p.id, release });
29277
30455
  } catch (err) {
29278
- log7.error("auto-extract.enqueue-failed", {
30456
+ log8.error("auto-extract.enqueue-failed", {
29279
30457
  syncId,
29280
30458
  release,
29281
30459
  projectId: p.id,
@@ -29292,8 +30470,8 @@ async function executeReleaseSync(db, syncId, opts) {
29292
30470
  error: errorMsg,
29293
30471
  phaseDetail: null,
29294
30472
  updatedAt: finishedAt
29295
- }).where(eq33(ccReleaseSyncs.id, syncId)).run();
29296
- log7.error("sync.failed", { syncId, release, error: errorMsg });
30473
+ }).where(eq34(ccReleaseSyncs.id, syncId)).run();
30474
+ log8.error("sync.failed", { syncId, release, error: errorMsg });
29297
30475
  throw err;
29298
30476
  }
29299
30477
  }
@@ -29326,10 +30504,10 @@ function computeSummary(rows) {
29326
30504
  }
29327
30505
 
29328
30506
  // src/backlink-extract.ts
29329
- import crypto31 from "crypto";
30507
+ import crypto34 from "crypto";
29330
30508
  import fs11 from "fs";
29331
- import { and as and27, desc as desc17, eq as eq34 } from "drizzle-orm";
29332
- var log8 = createLogger("BacklinkExtract");
30509
+ import { and as and28, desc as desc18, eq as eq35 } from "drizzle-orm";
30510
+ var log9 = createLogger("BacklinkExtract");
29333
30511
  function defaultDeps3() {
29334
30512
  return {
29335
30513
  queryBacklinks,
@@ -29340,13 +30518,13 @@ function defaultDeps3() {
29340
30518
  async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29341
30519
  const deps = { ...defaultDeps3(), ...opts.deps };
29342
30520
  const startedAt = deps.now().toISOString();
29343
- db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq34(runs.id, runId)).run();
30521
+ db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq35(runs.id, runId)).run();
29344
30522
  try {
29345
- const project = db.select().from(projects).where(eq34(projects.id, projectId)).get();
30523
+ const project = db.select().from(projects).where(eq35(projects.id, projectId)).get();
29346
30524
  if (!project) {
29347
30525
  throw new Error(`Project not found: ${projectId}`);
29348
30526
  }
29349
- const sync = opts.release ? db.select().from(ccReleaseSyncs).where(eq34(ccReleaseSyncs.release, opts.release)).get() : db.select().from(ccReleaseSyncs).where(eq34(ccReleaseSyncs.status, CcReleaseSyncStatuses.ready)).orderBy(desc17(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();
29350
30528
  if (!sync) {
29351
30529
  throw new Error("No ready release sync available \u2014 run `canonry backlinks sync` first");
29352
30530
  }
@@ -29374,11 +30552,11 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29374
30552
  const targetDomain = project.canonicalDomain;
29375
30553
  db.transaction((tx) => {
29376
30554
  tx.delete(backlinkDomains).where(
29377
- and27(eq34(backlinkDomains.projectId, projectId), eq34(backlinkDomains.release, release))
30555
+ and28(eq35(backlinkDomains.projectId, projectId), eq35(backlinkDomains.release, release))
29378
30556
  ).run();
29379
30557
  if (rows.length > 0) {
29380
30558
  const values = rows.map((r) => ({
29381
- id: crypto31.randomUUID(),
30559
+ id: crypto34.randomUUID(),
29382
30560
  projectId,
29383
30561
  releaseSyncId: syncId,
29384
30562
  release,
@@ -29391,7 +30569,7 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29391
30569
  }
29392
30570
  const summary = computeSummary2(rows);
29393
30571
  tx.insert(backlinkSummaries).values({
29394
- id: crypto31.randomUUID(),
30572
+ id: crypto34.randomUUID(),
29395
30573
  projectId,
29396
30574
  releaseSyncId: syncId,
29397
30575
  release,
@@ -29414,8 +30592,8 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29414
30592
  }).run();
29415
30593
  });
29416
30594
  const finishedAt = deps.now().toISOString();
29417
- db.update(runs).set({ status: RunStatuses.completed, finishedAt }).where(eq34(runs.id, runId)).run();
29418
- log8.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 });
29419
30597
  } catch (err) {
29420
30598
  const errorMsg = err instanceof Error ? err.message : String(err);
29421
30599
  const finishedAt = deps.now().toISOString();
@@ -29423,8 +30601,8 @@ async function executeBacklinkExtract(db, runId, projectId, opts = {}) {
29423
30601
  status: RunStatuses.failed,
29424
30602
  error: errorMsg,
29425
30603
  finishedAt
29426
- }).where(eq34(runs.id, runId)).run();
29427
- log8.error("extract.failed", { runId, projectId, error: errorMsg });
30604
+ }).where(eq35(runs.id, runId)).run();
30605
+ log9.error("extract.failed", { runId, projectId, error: errorMsg });
29428
30606
  throw err;
29429
30607
  }
29430
30608
  }
@@ -29444,18 +30622,18 @@ function computeSummary2(rows) {
29444
30622
  }
29445
30623
 
29446
30624
  // src/discovery-run.ts
29447
- import crypto32 from "crypto";
29448
- import { and as and28, eq as eq35 } from "drizzle-orm";
29449
- var log9 = createLogger("DiscoveryRun");
30625
+ import crypto35 from "crypto";
30626
+ import { and as and29, eq as eq36 } from "drizzle-orm";
30627
+ var log10 = createLogger("DiscoveryRun");
29450
30628
  var DEFAULT_SEED_COUNT = 30;
29451
30629
  var QUERIES_PER_INTENT_BUCKET = 6;
29452
30630
  async function executeDiscoveryRun(opts) {
29453
30631
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
29454
- opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq35(runs.id, opts.runId)).run();
30632
+ opts.db.update(runs).set({ status: RunStatuses.running, startedAt }).where(eq36(runs.id, opts.runId)).run();
29455
30633
  try {
29456
- const projectRow = opts.db.select().from(projects).where(eq35(projects.id, opts.projectId)).get();
30634
+ const projectRow = opts.db.select().from(projects).where(eq36(projects.id, opts.projectId)).get();
29457
30635
  if (!projectRow) throw new Error(`Project ${opts.projectId} not found`);
29458
- const projectCompetitors = opts.db.select({ domain: competitors.domain }).from(competitors).where(eq35(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());
29459
30637
  const canonicalDomains = effectiveDomains({
29460
30638
  canonicalDomain: projectRow.canonicalDomain,
29461
30639
  ownedDomains: projectRow.ownedDomains
@@ -29485,8 +30663,8 @@ async function executeDiscoveryRun(opts) {
29485
30663
  seedProvider: result.seedProvider,
29486
30664
  result
29487
30665
  });
29488
- opts.db.update(runs).set({ status: RunStatuses.completed, finishedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq35(runs.id, opts.runId)).run();
29489
- log9.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", {
29490
30668
  runId: opts.runId,
29491
30669
  sessionId: opts.sessionId,
29492
30670
  buckets: result.buckets,
@@ -29494,13 +30672,13 @@ async function executeDiscoveryRun(opts) {
29494
30672
  });
29495
30673
  } catch (err) {
29496
30674
  const errorMsg = err instanceof Error ? err.message : String(err);
29497
- log9.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
30675
+ log10.error("discovery.failed", { runId: opts.runId, sessionId: opts.sessionId, error: errorMsg });
29498
30676
  markSessionFailed(opts.db, opts.sessionId, errorMsg);
29499
30677
  opts.db.update(runs).set({
29500
30678
  status: RunStatuses.failed,
29501
30679
  finishedAt: (/* @__PURE__ */ new Date()).toISOString(),
29502
30680
  error: errorMsg
29503
- }).where(eq35(runs.id, opts.runId)).run();
30681
+ }).where(eq36(runs.id, opts.runId)).run();
29504
30682
  }
29505
30683
  }
29506
30684
  function buildDefaultDeps(registry) {
@@ -29705,13 +30883,13 @@ function writeDiscoveryInsight(db, input) {
29705
30883
  totalProbes
29706
30884
  });
29707
30885
  db.transaction((tx) => {
29708
- tx.update(insights).set({ dismissed: true }).where(and28(
29709
- eq35(insights.projectId, input.projectId),
29710
- eq35(insights.type, "discovery.basket-divergence"),
29711
- eq35(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)
29712
30890
  )).run();
29713
30891
  tx.insert(insights).values({
29714
- id: crypto32.randomUUID(),
30892
+ id: crypto35.randomUUID(),
29715
30893
  projectId: input.projectId,
29716
30894
  runId: input.runId,
29717
30895
  type: "discovery.basket-divergence",
@@ -29747,7 +30925,7 @@ function buildDiscoveryInsightTitle(input) {
29747
30925
  }
29748
30926
 
29749
30927
  // src/commands/backfill.ts
29750
- import { and as and29, eq as eq36, inArray as inArray12, 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";
29751
30929
  var SNAPSHOT_BATCH_SIZE = 500;
29752
30930
  async function backfillAnswerVisibilityCommand(opts) {
29753
30931
  const config = loadConfig();
@@ -29755,7 +30933,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29755
30933
  migrate(db);
29756
30934
  const projectFilter = opts?.project?.trim();
29757
30935
  const isDryRun = opts?.dryRun === true;
29758
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq36(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();
29759
30937
  let examined = 0;
29760
30938
  let updated = 0;
29761
30939
  let wouldUpdate = 0;
@@ -29763,10 +30941,10 @@ async function backfillAnswerVisibilityCommand(opts) {
29763
30941
  let reparsed = 0;
29764
30942
  let providerErrors = 0;
29765
30943
  if (scopedProjects.length > 0) {
29766
- const runRows = projectFilter ? db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(and29(
29767
- eq36(runs.kind, RunKinds["answer-visibility"]),
29768
- inArray12(runs.projectId, scopedProjects.map((project) => project.id))
29769
- )).all() : db.select({ id: runs.id, projectId: runs.projectId }).from(runs).where(eq36(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();
29770
30948
  const runIdsByProject = /* @__PURE__ */ new Map();
29771
30949
  for (const run of runRows) {
29772
30950
  const existing = runIdsByProject.get(run.projectId);
@@ -29774,7 +30952,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29774
30952
  else runIdsByProject.set(run.projectId, [run.id]);
29775
30953
  }
29776
30954
  for (const project of scopedProjects) {
29777
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq36(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);
29778
30956
  const runIds = runIdsByProject.get(project.id) ?? [];
29779
30957
  if (runIds.length === 0) continue;
29780
30958
  const projectDomains = effectiveDomains({
@@ -29797,7 +30975,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29797
30975
  competitorOverlap: querySnapshots.competitorOverlap,
29798
30976
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
29799
30977
  rawResponse: querySnapshots.rawResponse
29800
- }).from(querySnapshots).where(inArray12(querySnapshots.runId, batchRunIds)).all();
30978
+ }).from(querySnapshots).where(inArray13(querySnapshots.runId, batchRunIds)).all();
29801
30979
  const pendingUpdates = [];
29802
30980
  for (const snapshot of snapshotRows) {
29803
30981
  examined++;
@@ -29862,7 +31040,7 @@ async function backfillAnswerVisibilityCommand(opts) {
29862
31040
  } else {
29863
31041
  db.transaction((tx) => {
29864
31042
  for (const update of pendingUpdates) {
29865
- tx.update(querySnapshots).set(update.patch).where(eq36(querySnapshots.id, update.id)).run();
31043
+ tx.update(querySnapshots).set(update.patch).where(eq37(querySnapshots.id, update.id)).run();
29866
31044
  }
29867
31045
  });
29868
31046
  updated += pendingUpdates.length;
@@ -29911,13 +31089,13 @@ No DB writes performed. Re-run without --dry-run to apply.`);
29911
31089
  function backfillNormalizedPaths(db, opts) {
29912
31090
  const baseConditions = [];
29913
31091
  if (opts?.projectId) {
29914
- baseConditions.push(eq36(gaTrafficSnapshots.projectId, opts.projectId));
31092
+ baseConditions.push(eq37(gaTrafficSnapshots.projectId, opts.projectId));
29915
31093
  }
29916
31094
  const rows = db.select({
29917
31095
  id: gaTrafficSnapshots.id,
29918
31096
  landingPage: gaTrafficSnapshots.landingPage,
29919
31097
  landingPageNormalized: gaTrafficSnapshots.landingPageNormalized
29920
- }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and29(...baseConditions) : void 0).all();
31098
+ }).from(gaTrafficSnapshots).where(baseConditions.length > 0 ? and30(...baseConditions) : void 0).all();
29921
31099
  let updated = 0;
29922
31100
  let unchanged = 0;
29923
31101
  if (rows.length > 0) {
@@ -29932,7 +31110,7 @@ function backfillNormalizedPaths(db, opts) {
29932
31110
  unchanged++;
29933
31111
  continue;
29934
31112
  }
29935
- tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq36(gaTrafficSnapshots.id, row.id)).run();
31113
+ tx.update(gaTrafficSnapshots).set({ landingPageNormalized: next }).where(eq37(gaTrafficSnapshots.id, row.id)).run();
29936
31114
  updated++;
29937
31115
  }
29938
31116
  });
@@ -29946,7 +31124,7 @@ async function backfillNormalizedPathsCommand(opts) {
29946
31124
  const projectFilter = opts?.project?.trim();
29947
31125
  let projectId;
29948
31126
  if (projectFilter) {
29949
- const project = db.select({ id: projects.id }).from(projects).where(eq36(projects.name, projectFilter)).get();
31127
+ const project = db.select({ id: projects.id }).from(projects).where(eq37(projects.name, projectFilter)).get();
29950
31128
  if (!project) {
29951
31129
  const result2 = {
29952
31130
  project: projectFilter,
@@ -29983,13 +31161,13 @@ async function backfillNormalizedPathsCommand(opts) {
29983
31161
  function backfillAiReferralPaths(db, opts) {
29984
31162
  const baseConditions = [];
29985
31163
  if (opts?.projectId) {
29986
- baseConditions.push(eq36(gaAiReferrals.projectId, opts.projectId));
31164
+ baseConditions.push(eq37(gaAiReferrals.projectId, opts.projectId));
29987
31165
  }
29988
31166
  const rows = db.select({
29989
31167
  id: gaAiReferrals.id,
29990
31168
  landingPage: gaAiReferrals.landingPage,
29991
31169
  landingPageNormalized: gaAiReferrals.landingPageNormalized
29992
- }).from(gaAiReferrals).where(baseConditions.length > 0 ? and29(...baseConditions) : void 0).all();
31170
+ }).from(gaAiReferrals).where(baseConditions.length > 0 ? and30(...baseConditions) : void 0).all();
29993
31171
  let updated = 0;
29994
31172
  let unchanged = 0;
29995
31173
  if (rows.length > 0) {
@@ -30004,7 +31182,7 @@ function backfillAiReferralPaths(db, opts) {
30004
31182
  unchanged++;
30005
31183
  continue;
30006
31184
  }
30007
- tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq36(gaAiReferrals.id, row.id)).run();
31185
+ tx.update(gaAiReferrals).set({ landingPageNormalized: next }).where(eq37(gaAiReferrals.id, row.id)).run();
30008
31186
  updated++;
30009
31187
  }
30010
31188
  });
@@ -30018,7 +31196,7 @@ async function backfillAiReferralPathsCommand(opts) {
30018
31196
  const projectFilter = opts?.project?.trim();
30019
31197
  let projectId;
30020
31198
  if (projectFilter) {
30021
- const project = db.select({ id: projects.id }).from(projects).where(eq36(projects.name, projectFilter)).get();
31199
+ const project = db.select({ id: projects.id }).from(projects).where(eq37(projects.name, projectFilter)).get();
30022
31200
  if (!project) {
30023
31201
  const result2 = {
30024
31202
  project: projectFilter,
@@ -30054,10 +31232,10 @@ async function backfillAiReferralPathsCommand(opts) {
30054
31232
  }
30055
31233
  function backfillProjectAnswerMentions(db, projectId, opts) {
30056
31234
  const isDryRun = opts?.dryRun === true;
30057
- const project = db.select().from(projects).where(eq36(projects.id, projectId)).get();
31235
+ const project = db.select().from(projects).where(eq37(projects.id, projectId)).get();
30058
31236
  if (!project) return { examined: 0, updated: 0, mentioned: 0 };
30059
- const competitorDomains = db.select({ domain: competitors.domain }).from(competitors).where(eq36(competitors.projectId, projectId)).all().map((row) => row.domain);
30060
- const runRows = db.select({ id: runs.id }).from(runs).where(and29(eq36(runs.kind, RunKinds["answer-visibility"]), eq36(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();
30061
31239
  const runIds = runRows.map((r) => r.id);
30062
31240
  let examined = 0;
30063
31241
  let updated = 0;
@@ -30085,7 +31263,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
30085
31263
  competitorOverlap: querySnapshots.competitorOverlap,
30086
31264
  recommendedCompetitors: querySnapshots.recommendedCompetitors,
30087
31265
  rawResponse: querySnapshots.rawResponse
30088
- }).from(querySnapshots).where(inArray12(querySnapshots.runId, batchRunIds)).all();
31266
+ }).from(querySnapshots).where(inArray13(querySnapshots.runId, batchRunIds)).all();
30089
31267
  const pendingUpdates = [];
30090
31268
  for (const snapshot of snapshotRows) {
30091
31269
  examined++;
@@ -30129,7 +31307,7 @@ function backfillProjectAnswerMentions(db, projectId, opts) {
30129
31307
  } else {
30130
31308
  db.transaction((tx) => {
30131
31309
  for (const update of pendingUpdates) {
30132
- tx.update(querySnapshots).set(update.patch).where(eq36(querySnapshots.id, update.id)).run();
31310
+ tx.update(querySnapshots).set(update.patch).where(eq37(querySnapshots.id, update.id)).run();
30133
31311
  }
30134
31312
  });
30135
31313
  updated += pendingUpdates.length;
@@ -30144,7 +31322,7 @@ async function backfillAnswerMentionsCommand(opts) {
30144
31322
  migrate(db);
30145
31323
  const projectFilter = opts?.project?.trim();
30146
31324
  const isDryRun = opts?.dryRun === true;
30147
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq36(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();
30148
31326
  let examined = 0;
30149
31327
  let updated = 0;
30150
31328
  let wouldUpdate = 0;
@@ -30204,7 +31382,7 @@ function readStoredGroundingSources(rawResponse) {
30204
31382
  return result;
30205
31383
  }
30206
31384
  async function backfillInsightsCommand(project, opts) {
30207
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-NY3MAVPB.js");
31385
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-V4SWVKEQ.js");
30208
31386
  const config = loadConfig();
30209
31387
  const db = createClient(config.database);
30210
31388
  migrate(db);
@@ -30363,7 +31541,7 @@ async function backfillSnapshotAttributionCommand(opts) {
30363
31541
  const config = loadConfig();
30364
31542
  const db = createClient(config.database);
30365
31543
  migrate(db);
30366
- const project = db.select().from(projects).where(eq36(projects.name, opts.project)).get();
31544
+ const project = db.select().from(projects).where(eq37(projects.name, opts.project)).get();
30367
31545
  if (!project) {
30368
31546
  throw new Error(`Project "${opts.project}" not found`);
30369
31547
  }
@@ -30374,17 +31552,17 @@ async function backfillSnapshotAttributionCommand(opts) {
30374
31552
  process.stderr.write(`Recovering orphan snapshot attribution for "${project.name}"${mode}...
30375
31553
  `);
30376
31554
  }
30377
- const events = db.select({ createdAt: auditLog.createdAt, action: auditLog.action, diff: auditLog.diff }).from(auditLog).where(and29(
30378
- eq36(auditLog.projectId, project.id),
30379
- inArray12(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"])
30380
31558
  )).orderBy(auditLog.createdAt).all();
30381
31559
  const history = replayQueryAuditLog(events);
30382
31560
  const orphanRuns = db.select({
30383
31561
  runId: runs.id,
30384
31562
  createdAt: runs.createdAt,
30385
31563
  location: runs.location
30386
- }).from(runs).innerJoin(querySnapshots, eq36(querySnapshots.runId, runs.id)).where(and29(
30387
- eq36(runs.projectId, project.id),
31564
+ }).from(runs).innerJoin(querySnapshots, eq37(querySnapshots.runId, runs.id)).where(and30(
31565
+ eq37(runs.projectId, project.id),
30388
31566
  isNull(querySnapshots.queryId),
30389
31567
  isNull(querySnapshots.queryText)
30390
31568
  )).groupBy(runs.id).orderBy(runs.createdAt).all();
@@ -30406,8 +31584,8 @@ async function backfillSnapshotAttributionCommand(opts) {
30406
31584
  provider: querySnapshots.provider,
30407
31585
  createdAt: querySnapshots.createdAt,
30408
31586
  answerText: querySnapshots.answerText
30409
- }).from(querySnapshots).where(and29(
30410
- eq36(querySnapshots.runId, run.runId),
31587
+ }).from(querySnapshots).where(and30(
31588
+ eq37(querySnapshots.runId, run.runId),
30411
31589
  isNull(querySnapshots.queryId),
30412
31590
  isNull(querySnapshots.queryText)
30413
31591
  )).orderBy(querySnapshots.provider, querySnapshots.createdAt).all();
@@ -30473,7 +31651,7 @@ async function backfillSnapshotAttributionCommand(opts) {
30473
31651
  if (!isDryRun && updates.length > 0) {
30474
31652
  db.transaction((tx) => {
30475
31653
  for (const u of updates) {
30476
- tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq36(querySnapshots.id, u.id)).run();
31654
+ tx.update(querySnapshots).set({ queryText: u.queryText }).where(eq37(querySnapshots.id, u.id)).run();
30477
31655
  }
30478
31656
  });
30479
31657
  }
@@ -30547,7 +31725,7 @@ async function backfillTrafficClassificationCommand(opts) {
30547
31725
  const projectFilter = opts?.project?.trim();
30548
31726
  const isDryRun = opts?.dryRun === true;
30549
31727
  const isJson = opts?.format === "json";
30550
- const scopedProjects = projectFilter ? db.select().from(projects).where(eq36(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();
30551
31729
  if (scopedProjects.length === 0) {
30552
31730
  if (projectFilter && !isJson) {
30553
31731
  process.stderr.write(`No project named "${projectFilter}".
@@ -30572,9 +31750,9 @@ async function backfillTrafficClassificationCommand(opts) {
30572
31750
  dryRun: isDryRun,
30573
31751
  byBot: {}
30574
31752
  };
30575
- const unknownCountRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and29(
30576
- eq36(rawEventSamples.eventType, "unknown"),
30577
- inArray12(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)
30578
31756
  )).get();
30579
31757
  result.unknownBefore = Number(unknownCountRow?.n ?? 0);
30580
31758
  const unknownSamples = db.select({
@@ -30585,9 +31763,9 @@ async function backfillTrafficClassificationCommand(opts) {
30585
31763
  userAgent: rawEventSamples.userAgent,
30586
31764
  pathNormalized: rawEventSamples.pathNormalized,
30587
31765
  status: rawEventSamples.status
30588
- }).from(rawEventSamples).where(and29(
30589
- eq36(rawEventSamples.eventType, "unknown"),
30590
- inArray12(rawEventSamples.projectId, projectIds)
31766
+ }).from(rawEventSamples).where(and30(
31767
+ eq37(rawEventSamples.eventType, "unknown"),
31768
+ inArray13(rawEventSamples.projectId, projectIds)
30591
31769
  )).all();
30592
31770
  result.examined = unknownSamples.length;
30593
31771
  if (unknownSamples.length === 0) {
@@ -30625,7 +31803,7 @@ async function backfillTrafficClassificationCommand(opts) {
30625
31803
  result.reclassified++;
30626
31804
  result.byBot[classified.botId] = (result.byBot[classified.botId] ?? 0) + 1;
30627
31805
  if (isDryRun) continue;
30628
- db.update(rawEventSamples).set({ eventType: userFetch ? TrafficEventKinds["ai-user-fetch"] : TrafficEventKinds.crawler }).where(eq36(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();
30629
31807
  const tsHour = new Date(snap.ts);
30630
31808
  tsHour.setUTCMinutes(0, 0, 0);
30631
31809
  if (userFetch) {
@@ -30689,9 +31867,9 @@ async function backfillTrafficClassificationCommand(opts) {
30689
31867
  }
30690
31868
  }
30691
31869
  if (!isDryRun) {
30692
- const afterRow = db.select({ n: sql15`count(*)` }).from(rawEventSamples).where(and29(
30693
- eq36(rawEventSamples.eventType, "unknown"),
30694
- inArray12(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)
30695
31873
  )).get();
30696
31874
  result.unknownAfter = Number(afterRow?.n ?? 0);
30697
31875
  } else {
@@ -30724,6 +31902,333 @@ No DB writes performed. Re-run without --dry-run to apply.`);
30724
31902
  }
30725
31903
  }
30726
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
+
30727
32232
  // src/provider-registry.ts
30728
32233
  var ProviderRegistry = class {
30729
32234
  providers = /* @__PURE__ */ new Map();
@@ -30777,8 +32282,8 @@ var ProviderRegistry = class {
30777
32282
 
30778
32283
  // src/scheduler.ts
30779
32284
  import cron from "node-cron";
30780
- import { and as and30, eq as eq37 } from "drizzle-orm";
30781
- var log10 = createLogger("Scheduler");
32285
+ import { and as and31, eq as eq38 } from "drizzle-orm";
32286
+ var log11 = createLogger("Scheduler");
30782
32287
  function taskKey(projectId, kind) {
30783
32288
  return `${projectId}::${kind}`;
30784
32289
  }
@@ -30792,16 +32297,16 @@ var Scheduler = class {
30792
32297
  }
30793
32298
  /** Load all enabled schedules from DB and register cron jobs. */
30794
32299
  start() {
30795
- const allSchedules = this.db.select().from(schedules).where(eq37(schedules.enabled, true)).all();
32300
+ const allSchedules = this.db.select().from(schedules).where(eq38(schedules.enabled, true)).all();
30796
32301
  for (const schedule of allSchedules) {
30797
32302
  const missedRunAt = schedule.nextRunAt;
30798
32303
  this.registerCronTask(schedule);
30799
32304
  if (missedRunAt && new Date(missedRunAt) < /* @__PURE__ */ new Date()) {
30800
- log10.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
32305
+ log11.info("run.catch-up", { projectId: schedule.projectId, kind: schedule.kind, missedRunAt });
30801
32306
  this.triggerRun(schedule.id, schedule.projectId, schedule.kind);
30802
32307
  }
30803
32308
  }
30804
- log10.info("started", { scheduleCount: allSchedules.length });
32309
+ log11.info("started", { scheduleCount: allSchedules.length });
30805
32310
  }
30806
32311
  /** Stop all cron tasks for graceful shutdown. */
30807
32312
  stop() {
@@ -30822,7 +32327,7 @@ var Scheduler = class {
30822
32327
  this.stopTask(key, existing, "Stopped");
30823
32328
  this.tasks.delete(key);
30824
32329
  }
30825
- const schedule = this.db.select().from(schedules).where(and30(eq37(schedules.projectId, projectId), eq37(schedules.kind, kind))).get();
32330
+ const schedule = this.db.select().from(schedules).where(and31(eq38(schedules.projectId, projectId), eq38(schedules.kind, kind))).get();
30826
32331
  if (schedule && schedule.enabled) {
30827
32332
  this.registerCronTask(schedule);
30828
32333
  }
@@ -30845,13 +32350,13 @@ var Scheduler = class {
30845
32350
  stopTask(key, task, verb) {
30846
32351
  void task.stop();
30847
32352
  void task.destroy();
30848
- log10.info(`task.${verb.toLowerCase()}`, { key });
32353
+ log11.info(`task.${verb.toLowerCase()}`, { key });
30849
32354
  }
30850
32355
  registerCronTask(schedule) {
30851
32356
  const { id: scheduleId, projectId, cronExpr, timezone } = schedule;
30852
32357
  const kind = schedule.kind;
30853
32358
  if (!cron.validate(cronExpr)) {
30854
- log10.error("cron.invalid", { projectId, kind, cronExpr });
32359
+ log11.error("cron.invalid", { projectId, kind, cronExpr });
30855
32360
  return;
30856
32361
  }
30857
32362
  const task = cron.schedule(cronExpr, () => {
@@ -30863,43 +32368,43 @@ var Scheduler = class {
30863
32368
  this.db.update(schedules).set({
30864
32369
  nextRunAt: task.getNextRun()?.toISOString() ?? null,
30865
32370
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
30866
- }).where(eq37(schedules.id, scheduleId)).run();
32371
+ }).where(eq38(schedules.id, scheduleId)).run();
30867
32372
  const label = schedule.preset ?? cronExpr;
30868
- log10.info("cron.registered", { projectId, kind, schedule: label, timezone });
32373
+ log11.info("cron.registered", { projectId, kind, schedule: label, timezone });
30869
32374
  }
30870
32375
  triggerRun(scheduleId, projectId, kind) {
30871
32376
  try {
30872
32377
  const now = (/* @__PURE__ */ new Date()).toISOString();
30873
- const currentSchedule = this.db.select().from(schedules).where(eq37(schedules.id, scheduleId)).get();
32378
+ const currentSchedule = this.db.select().from(schedules).where(eq38(schedules.id, scheduleId)).get();
30874
32379
  if (!currentSchedule || !currentSchedule.enabled) {
30875
- log10.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" });
30876
32381
  this.remove(projectId, kind);
30877
32382
  return;
30878
32383
  }
30879
32384
  const task = this.tasks.get(taskKey(projectId, kind));
30880
32385
  const nextRunAt = task?.getNextRun()?.toISOString() ?? null;
30881
- const project = this.db.select().from(projects).where(eq37(projects.id, projectId)).get();
32386
+ const project = this.db.select().from(projects).where(eq38(projects.id, projectId)).get();
30882
32387
  if (!project) {
30883
- log10.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
32388
+ log11.error("project.not-found", { projectId, kind, msg: "skipping scheduled run" });
30884
32389
  this.remove(projectId, kind);
30885
32390
  return;
30886
32391
  }
30887
32392
  if (kind === SchedulableRunKinds["traffic-sync"]) {
30888
32393
  const sourceId = currentSchedule.sourceId;
30889
32394
  if (!sourceId) {
30890
- log10.warn("traffic-sync.missing-source", { scheduleId, projectId });
32395
+ log11.warn("traffic-sync.missing-source", { scheduleId, projectId });
30891
32396
  return;
30892
32397
  }
30893
32398
  if (!this.callbacks.onTrafficSyncRequested) {
30894
- log10.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" });
30895
32400
  return;
30896
32401
  }
30897
32402
  this.db.update(schedules).set({
30898
32403
  lastRunAt: now,
30899
32404
  nextRunAt,
30900
32405
  updatedAt: now
30901
- }).where(eq37(schedules.id, currentSchedule.id)).run();
30902
- log10.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 });
30903
32408
  this.callbacks.onTrafficSyncRequested(project.name, sourceId);
30904
32409
  return;
30905
32410
  }
@@ -30908,7 +32413,7 @@ var Scheduler = class {
30908
32413
  if (project.defaultLocation) {
30909
32414
  const loc = projectLocations.find((l) => l.label === project.defaultLocation);
30910
32415
  if (!loc) {
30911
- log10.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
32416
+ log11.warn("default-location.stale", { scheduleId, projectId, label: project.defaultLocation });
30912
32417
  return;
30913
32418
  }
30914
32419
  resolvedLocation = loc;
@@ -30922,11 +32427,11 @@ var Scheduler = class {
30922
32427
  location: locationLabel
30923
32428
  });
30924
32429
  if (queueResult.conflict) {
30925
- log10.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
32430
+ log11.info("run.skipped-active", { projectName: project.name, activeRunId: queueResult.activeRunId });
30926
32431
  this.db.update(schedules).set({
30927
32432
  nextRunAt,
30928
32433
  updatedAt: now
30929
- }).where(eq37(schedules.id, currentSchedule.id)).run();
32434
+ }).where(eq38(schedules.id, currentSchedule.id)).run();
30930
32435
  return;
30931
32436
  }
30932
32437
  const runId = queueResult.runId;
@@ -30934,21 +32439,21 @@ var Scheduler = class {
30934
32439
  lastRunAt: now,
30935
32440
  nextRunAt,
30936
32441
  updatedAt: now
30937
- }).where(eq37(schedules.id, currentSchedule.id)).run();
32442
+ }).where(eq38(schedules.id, currentSchedule.id)).run();
30938
32443
  const scheduleProviders = currentSchedule.providers;
30939
32444
  const providers = scheduleProviders.length > 0 ? scheduleProviders : void 0;
30940
- log10.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
32445
+ log11.info("run.triggered", { runId, projectName: project.name, providers: providers ?? "all" });
30941
32446
  this.callbacks.onRunCreated(runId, projectId, providers, resolvedLocation);
30942
32447
  } catch (err) {
30943
- log10.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) });
30944
32449
  }
30945
32450
  }
30946
32451
  };
30947
32452
 
30948
32453
  // src/notifier.ts
30949
- import { eq as eq38, desc as desc18, and as and31, inArray as inArray13, or as or5 } from "drizzle-orm";
30950
- import crypto33 from "crypto";
30951
- var log11 = 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");
30952
32457
  var Notifier = class {
30953
32458
  db;
30954
32459
  serverUrl;
@@ -30958,26 +32463,26 @@ var Notifier = class {
30958
32463
  }
30959
32464
  /** Called after a run completes (success, partial, or failed). */
30960
32465
  async onRunCompleted(runId, projectId) {
30961
- log11.info("run.completed", { runId, projectId });
30962
- const notifs = this.db.select().from(notifications).where(eq38(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);
30963
32468
  if (notifs.length === 0) {
30964
- log11.info("notifications.none-enabled", { projectId });
32469
+ log12.info("notifications.none-enabled", { projectId });
30965
32470
  return;
30966
32471
  }
30967
- log11.info("notifications.found", { projectId, count: notifs.length });
30968
- const run = this.db.select().from(runs).where(eq38(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();
30969
32474
  if (!run) {
30970
- log11.error("run.not-found", { runId, msg: "skipping notification dispatch" });
32475
+ log12.error("run.not-found", { runId, msg: "skipping notification dispatch" });
30971
32476
  return;
30972
32477
  }
30973
- const project = this.db.select().from(projects).where(eq38(projects.id, projectId)).get();
32478
+ const project = this.db.select().from(projects).where(eq39(projects.id, projectId)).get();
30974
32479
  if (!project) {
30975
- log11.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
32480
+ log12.error("project.not-found", { projectId, msg: "skipping notification dispatch" });
30976
32481
  return;
30977
32482
  }
30978
32483
  const transitions = this.computeTransitions(runId, projectId);
30979
32484
  const events = [];
30980
- log11.info("run.status", { runId: run.id, status: run.status, projectId });
32485
+ log12.info("run.status", { runId: run.id, status: run.status, projectId });
30981
32486
  if (run.status === "completed" || run.status === "partial") {
30982
32487
  events.push("run.completed");
30983
32488
  }
@@ -30993,7 +32498,7 @@ var Notifier = class {
30993
32498
  if (!config.url) continue;
30994
32499
  const subscribedEvents = config.events;
30995
32500
  const matchingEvents = events.filter((e) => subscribedEvents.includes(e));
30996
- log11.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
32501
+ log12.info("notification.match", { notificationId: notif.id, subscribedEvents, matchedEvents: matchingEvents });
30997
32502
  if (matchingEvents.length === 0) continue;
30998
32503
  for (const event of matchingEvents) {
30999
32504
  const relevantTransitions = event === "citation.lost" ? lostTransitions : event === "citation.gained" ? gainedTransitions : transitions;
@@ -31017,11 +32522,11 @@ var Notifier = class {
31017
32522
  if (criticalInsights.length > 0) insightEvents.push("insight.critical");
31018
32523
  if (highInsights.length > 0) insightEvents.push("insight.high");
31019
32524
  if (insightEvents.length === 0) return;
31020
- const notifs = this.db.select().from(notifications).where(eq38(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);
31021
32526
  if (notifs.length === 0) return;
31022
- const run = this.db.select().from(runs).where(eq38(runs.id, runId)).get();
32527
+ const run = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
31023
32528
  if (!run) return;
31024
- const project = this.db.select().from(projects).where(eq38(projects.id, projectId)).get();
32529
+ const project = this.db.select().from(projects).where(eq39(projects.id, projectId)).get();
31025
32530
  if (!project) return;
31026
32531
  for (const notif of notifs) {
31027
32532
  const config = notif.config;
@@ -31051,12 +32556,12 @@ var Notifier = class {
31051
32556
  }
31052
32557
  }
31053
32558
  computeTransitions(runId, projectId) {
31054
- const thisRun = this.db.select().from(runs).where(eq38(runs.id, runId)).get();
32559
+ const thisRun = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
31055
32560
  if (!thisRun) return [];
31056
- const groupSiblings = this.db.select().from(runs).where(and31(
31057
- eq38(runs.projectId, projectId),
31058
- eq38(runs.kind, thisRun.kind),
31059
- eq38(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)
31060
32565
  )).all();
31061
32566
  const stillPending = groupSiblings.some((r) => r.status === "queued" || r.status === "running");
31062
32567
  if (stillPending) return [];
@@ -31072,19 +32577,19 @@ var Notifier = class {
31072
32577
  return candidate.id > best.id ? candidate : best;
31073
32578
  });
31074
32579
  if (winner.id !== runId) return [];
31075
- const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq38(projects.id, projectId)).get();
32580
+ const projectLocations = this.db.select({ locations: projects.locations }).from(projects).where(eq39(projects.id, projectId)).get();
31076
32581
  const locationCount = Math.max(
31077
32582
  1,
31078
32583
  (projectLocations?.locations ?? []).length
31079
32584
  );
31080
32585
  const RECENT_FETCH_LIMIT = Math.max(8, locationCount * 4);
31081
32586
  const recentRuns = this.db.select().from(runs).where(
31082
- and31(
31083
- eq38(runs.projectId, projectId),
31084
- eq38(runs.kind, thisRun.kind),
31085
- or5(eq38(runs.status, "completed"), eq38(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"))
31086
32591
  )
31087
- ).orderBy(desc18(runs.createdAt), desc18(runs.id)).limit(RECENT_FETCH_LIMIT).all();
32592
+ ).orderBy(desc19(runs.createdAt), desc19(runs.id)).limit(RECENT_FETCH_LIMIT).all();
31088
32593
  const groups = groupRunsByCreatedAt(recentRuns);
31089
32594
  const currentGroupIdx = groups.findIndex((g) => g[0]?.createdAt === thisRun.createdAt);
31090
32595
  if (currentGroupIdx < 0) return [];
@@ -31099,13 +32604,13 @@ var Notifier = class {
31099
32604
  provider: querySnapshots.provider,
31100
32605
  location: querySnapshots.location,
31101
32606
  citationState: querySnapshots.citationState
31102
- }).from(querySnapshots).leftJoin(queries, eq38(querySnapshots.queryId, queries.id)).where(inArray13(querySnapshots.runId, currentRunIds)).all();
32607
+ }).from(querySnapshots).leftJoin(queries, eq39(querySnapshots.queryId, queries.id)).where(inArray14(querySnapshots.runId, currentRunIds)).all();
31103
32608
  const previousSnapshots = this.db.select({
31104
32609
  queryId: querySnapshots.queryId,
31105
32610
  provider: querySnapshots.provider,
31106
32611
  location: querySnapshots.location,
31107
32612
  citationState: querySnapshots.citationState
31108
- }).from(querySnapshots).where(inArray13(querySnapshots.runId, previousRunIds)).all();
32613
+ }).from(querySnapshots).where(inArray14(querySnapshots.runId, previousRunIds)).all();
31109
32614
  const prevMap = /* @__PURE__ */ new Map();
31110
32615
  for (const s of previousSnapshots) {
31111
32616
  if (s.queryId == null) continue;
@@ -31132,23 +32637,23 @@ var Notifier = class {
31132
32637
  const targetLabel = redactNotificationUrl(url).urlDisplay;
31133
32638
  const targetCheck = await resolveWebhookTarget(url);
31134
32639
  if (!targetCheck.ok) {
31135
- log11.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
32640
+ log12.error("webhook.ssrf-blocked", { url: targetLabel, reason: targetCheck.message });
31136
32641
  this.logDelivery(projectId, notificationId, payload.event, "failed", `SSRF: ${targetCheck.message}`);
31137
32642
  return;
31138
32643
  }
31139
- log11.info("webhook.send", { event: payload.event, url: targetLabel });
32644
+ log12.info("webhook.send", { event: payload.event, url: targetLabel });
31140
32645
  const maxRetries = 3;
31141
32646
  const delays = [1e3, 4e3, 16e3];
31142
32647
  for (let attempt = 0; attempt < maxRetries; attempt++) {
31143
32648
  try {
31144
32649
  const response = await deliverWebhook(targetCheck.target, payload, webhookSecret);
31145
32650
  if (response.status >= 200 && response.status < 300) {
31146
- log11.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
32651
+ log12.info("webhook.delivered", { event: payload.event, url: targetLabel, httpStatus: response.status });
31147
32652
  this.logDelivery(projectId, notificationId, payload.event, "sent", null);
31148
32653
  return;
31149
32654
  }
31150
32655
  const errorDetail = response.error ?? `HTTP ${response.status}`;
31151
- log11.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 });
31152
32657
  if (attempt === maxRetries - 1) {
31153
32658
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
31154
32659
  }
@@ -31156,7 +32661,7 @@ var Notifier = class {
31156
32661
  const errorDetail = err instanceof Error ? err.message : String(err);
31157
32662
  if (attempt === maxRetries - 1) {
31158
32663
  this.logDelivery(projectId, notificationId, payload.event, "failed", errorDetail);
31159
- log11.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
32664
+ log12.error("webhook.exhausted", { event: payload.event, url: targetLabel, maxRetries, error: errorDetail });
31160
32665
  }
31161
32666
  }
31162
32667
  if (attempt < maxRetries - 1) {
@@ -31166,7 +32671,7 @@ var Notifier = class {
31166
32671
  }
31167
32672
  logDelivery(projectId, notificationId, event, status, error) {
31168
32673
  this.db.insert(auditLog).values({
31169
- id: crypto33.randomUUID(),
32674
+ id: crypto37.randomUUID(),
31170
32675
  projectId,
31171
32676
  actor: "scheduler",
31172
32677
  action: `notification.${status}`,
@@ -31179,8 +32684,8 @@ var Notifier = class {
31179
32684
  };
31180
32685
 
31181
32686
  // src/run-coordinator.ts
31182
- import { eq as eq39 } from "drizzle-orm";
31183
- var log12 = createLogger("RunCoordinator");
32687
+ import { eq as eq40 } from "drizzle-orm";
32688
+ var log13 = createLogger("RunCoordinator");
31184
32689
  var RunCoordinator = class {
31185
32690
  constructor(db, notifier, intelligenceService, onInsightsGenerated, onAeroEvent) {
31186
32691
  this.db = db;
@@ -31190,10 +32695,10 @@ var RunCoordinator = class {
31190
32695
  this.onAeroEvent = onAeroEvent;
31191
32696
  }
31192
32697
  async onRunCompleted(runId, projectId) {
31193
- const runRow = this.db.select().from(runs).where(eq39(runs.id, runId)).get();
32698
+ const runRow = this.db.select().from(runs).where(eq40(runs.id, runId)).get();
31194
32699
  const kind = runRow?.kind ?? RunKinds["answer-visibility"];
31195
32700
  if (runRow?.trigger === RunTriggers.probe) {
31196
- log12.info("probe.skip-side-effects", { runId, projectId, kind });
32701
+ log13.info("probe.skip-side-effects", { runId, projectId, kind });
31197
32702
  return;
31198
32703
  }
31199
32704
  let insightCount = 0;
@@ -31210,18 +32715,18 @@ var RunCoordinator = class {
31210
32715
  try {
31211
32716
  await this.onInsightsGenerated(runId, projectId, result);
31212
32717
  } catch (err) {
31213
- log12.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) });
31214
32719
  }
31215
32720
  }
31216
32721
  }
31217
32722
  } catch (err) {
31218
- log12.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) });
31219
32724
  }
31220
32725
  }
31221
32726
  try {
31222
32727
  await this.notifier.onRunCompleted(runId, projectId);
31223
32728
  } catch (err) {
31224
- log12.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) });
31225
32730
  }
31226
32731
  if (this.onAeroEvent) {
31227
32732
  try {
@@ -31234,7 +32739,7 @@ var RunCoordinator = class {
31234
32739
  };
31235
32740
  await this.onAeroEvent(ctx);
31236
32741
  } catch (err) {
31237
- log12.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) });
31238
32743
  }
31239
32744
  }
31240
32745
  }
@@ -31249,7 +32754,7 @@ var RunCoordinator = class {
31249
32754
  * so the Aero queue is never starved of a follow-up.
31250
32755
  */
31251
32756
  buildDiscoveryAeroContext(runId, projectId, status, error) {
31252
- const session = this.db.select().from(discoverySessions).where(eq39(discoverySessions.runId, runId)).get();
32757
+ const session = this.db.select().from(discoverySessions).where(eq40(discoverySessions.runId, runId)).get();
31253
32758
  const competitorMap = session ? session.competitorMap : [];
31254
32759
  return {
31255
32760
  kind: RunKinds["aeo-discover-probe"],
@@ -31271,12 +32776,12 @@ var RunCoordinator = class {
31271
32776
  };
31272
32777
 
31273
32778
  // src/agent/session-registry.ts
31274
- import crypto35 from "crypto";
31275
- import { eq as eq41 } from "drizzle-orm";
32779
+ import crypto39 from "crypto";
32780
+ import { eq as eq42 } from "drizzle-orm";
31276
32781
 
31277
32782
  // src/agent/session.ts
31278
- import fs14 from "fs";
31279
- import path14 from "path";
32783
+ import fs15 from "fs";
32784
+ import path15 from "path";
31280
32785
  import { Agent } from "@mariozechner/pi-agent-core";
31281
32786
  import { registerBuiltInApiProviders } from "@mariozechner/pi-ai";
31282
32787
 
@@ -31416,26 +32921,26 @@ function buildAgentProvidersResponse(config) {
31416
32921
  }
31417
32922
 
31418
32923
  // src/agent/skill-paths.ts
31419
- import fs12 from "fs";
31420
- import path12 from "path";
31421
- import { fileURLToPath } from "url";
32924
+ import fs13 from "fs";
32925
+ import path13 from "path";
32926
+ import { fileURLToPath as fileURLToPath2 } from "url";
31422
32927
  function resolveAeroSkillDir(pkgDir) {
31423
- const here = pkgDir ?? path12.dirname(fileURLToPath(import.meta.url));
32928
+ const here = pkgDir ?? path13.dirname(fileURLToPath2(import.meta.url));
31424
32929
  const candidates = [
31425
- path12.join(here, "../assets/agent-workspace/skills/aero"),
31426
- path12.join(here, "../../assets/agent-workspace/skills/aero"),
31427
- 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")
31428
32933
  ];
31429
32934
  for (const candidate of candidates) {
31430
- if (fs12.existsSync(path12.join(candidate, "SKILL.md"))) return candidate;
32935
+ if (fs13.existsSync(path13.join(candidate, "SKILL.md"))) return candidate;
31431
32936
  }
31432
32937
  throw new Error(`Aero skill not found. Searched:
31433
32938
  ${candidates.join("\n ")}`);
31434
32939
  }
31435
32940
 
31436
32941
  // src/agent/skill-tools.ts
31437
- import fs13 from "fs";
31438
- import path13 from "path";
32942
+ import fs14 from "fs";
32943
+ import path14 from "path";
31439
32944
  import { Type } from "@sinclair/typebox";
31440
32945
  var MAX_DOC_CHARS = 2e4;
31441
32946
  function textResult(details) {
@@ -31444,7 +32949,7 @@ function textResult(details) {
31444
32949
  details
31445
32950
  };
31446
32951
  }
31447
- function parseDescription(body) {
32952
+ function parseDescription2(body) {
31448
32953
  if (!body.startsWith("---")) return "(no description)";
31449
32954
  const end = body.indexOf("\n---", 3);
31450
32955
  if (end === -1) return "(no description)";
@@ -31456,16 +32961,16 @@ function parseDescription(body) {
31456
32961
  return "(no description)";
31457
32962
  }
31458
32963
  function scanSkillDocs(skillDir) {
31459
- const refsDir = path13.join(skillDir ?? resolveAeroSkillDir(), "references");
31460
- if (!fs13.existsSync(refsDir)) return [];
32964
+ const refsDir = path14.join(skillDir ?? resolveAeroSkillDir(), "references");
32965
+ if (!fs14.existsSync(refsDir)) return [];
31461
32966
  const entries = [];
31462
- for (const file of fs13.readdirSync(refsDir)) {
32967
+ for (const file of fs14.readdirSync(refsDir)) {
31463
32968
  if (!file.endsWith(".md")) continue;
31464
- const filePath = path13.join(refsDir, file);
31465
- const body = fs13.readFileSync(filePath, "utf-8");
32969
+ const filePath = path14.join(refsDir, file);
32970
+ const body = fs14.readFileSync(filePath, "utf-8");
31466
32971
  entries.push({
31467
32972
  slug: file.replace(/\.md$/, ""),
31468
- description: parseDescription(body),
32973
+ description: parseDescription2(body),
31469
32974
  bytes: Buffer.byteLength(body, "utf-8")
31470
32975
  });
31471
32976
  }
@@ -31505,8 +33010,8 @@ function buildReadSkillDocTool() {
31505
33010
  availableSlugs: docs.map((d) => d.slug)
31506
33011
  });
31507
33012
  }
31508
- const filePath = path13.join(skillDir, "references", `${match.slug}.md`);
31509
- 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");
31510
33015
  if (content.length > MAX_DOC_CHARS) {
31511
33016
  return textResult({
31512
33017
  slug: match.slug,
@@ -31600,10 +33105,10 @@ function ensureBuiltinsRegistered() {
31600
33105
  }
31601
33106
  function loadAeroSystemPrompt(pkgDir) {
31602
33107
  const skillDir = resolveAeroSkillDir(pkgDir);
31603
- const skillBody = fs14.readFileSync(path14.join(skillDir, "SKILL.md"), "utf-8");
31604
- const soulPath = path14.join(skillDir, "soul.md");
31605
- if (!fs14.existsSync(soulPath)) return skillBody;
31606
- 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");
31607
33112
  return `${soulBody.trimEnd()}
31608
33113
 
31609
33114
  ---
@@ -31660,8 +33165,8 @@ function resolveSessionProviderAndModel(config, opts) {
31660
33165
  }
31661
33166
 
31662
33167
  // src/agent/memory-store.ts
31663
- import crypto34 from "crypto";
31664
- import { and as and32, desc as desc19, eq as eq40, 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";
31665
33170
  var COMPACTION_KEY_PREFIX = "compaction:";
31666
33171
  var COMPACTION_NOTES_PER_SESSION = 3;
31667
33172
  function rowToDto2(row) {
@@ -31675,7 +33180,7 @@ function rowToDto2(row) {
31675
33180
  };
31676
33181
  }
31677
33182
  function listMemoryEntries(db, projectId, opts = {}) {
31678
- const query = db.select().from(agentMemory).where(eq40(agentMemory.projectId, projectId)).orderBy(desc19(agentMemory.updatedAt));
33183
+ const query = db.select().from(agentMemory).where(eq41(agentMemory.projectId, projectId)).orderBy(desc20(agentMemory.updatedAt));
31679
33184
  const rows = opts.limit === void 0 ? query.all() : query.limit(opts.limit).all();
31680
33185
  return rows.map(rowToDto2);
31681
33186
  }
@@ -31689,7 +33194,7 @@ function upsertMemoryEntry(db, args) {
31689
33194
  throw new Error(`memory key prefix "${COMPACTION_KEY_PREFIX}" is reserved for compaction notes`);
31690
33195
  }
31691
33196
  const now = (/* @__PURE__ */ new Date()).toISOString();
31692
- const id = crypto34.randomUUID();
33197
+ const id = crypto38.randomUUID();
31693
33198
  db.insert(agentMemory).values({
31694
33199
  id,
31695
33200
  projectId: args.projectId,
@@ -31706,12 +33211,12 @@ function upsertMemoryEntry(db, args) {
31706
33211
  updatedAt: now
31707
33212
  }
31708
33213
  }).run();
31709
- const row = db.select().from(agentMemory).where(and32(eq40(agentMemory.projectId, args.projectId), eq40(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();
31710
33215
  if (!row) throw new Error("memory upsert produced no row");
31711
33216
  return rowToDto2(row);
31712
33217
  }
31713
33218
  function deleteMemoryEntry(db, projectId, key) {
31714
- const result = db.delete(agentMemory).where(and32(eq40(agentMemory.projectId, projectId), eq40(agentMemory.key, key))).run();
33219
+ const result = db.delete(agentMemory).where(and33(eq41(agentMemory.projectId, projectId), eq41(agentMemory.key, key))).run();
31715
33220
  const changes = result.changes ?? 0;
31716
33221
  return changes > 0;
31717
33222
  }
@@ -31726,7 +33231,7 @@ function writeCompactionNote(db, args) {
31726
33231
  }
31727
33232
  const now = (/* @__PURE__ */ new Date()).toISOString();
31728
33233
  const key = `${COMPACTION_KEY_PREFIX}${args.sessionId}:${now}`;
31729
- const id = crypto34.randomUUID();
33234
+ const id = crypto38.randomUUID();
31730
33235
  let inserted;
31731
33236
  db.transaction((tx) => {
31732
33237
  tx.insert(agentMemory).values({
@@ -31740,16 +33245,16 @@ function writeCompactionNote(db, args) {
31740
33245
  }).run();
31741
33246
  const sessionPrefix = `${COMPACTION_KEY_PREFIX}${args.sessionId}:`;
31742
33247
  const existing = tx.select({ id: agentMemory.id, updatedAt: agentMemory.updatedAt }).from(agentMemory).where(
31743
- and32(
31744
- eq40(agentMemory.projectId, args.projectId),
33248
+ and33(
33249
+ eq41(agentMemory.projectId, args.projectId),
31745
33250
  like2(agentMemory.key, `${sessionPrefix}%`)
31746
33251
  )
31747
- ).orderBy(desc19(agentMemory.updatedAt)).all();
33252
+ ).orderBy(desc20(agentMemory.updatedAt)).all();
31748
33253
  const stale = existing.slice(COMPACTION_NOTES_PER_SESSION).map((r) => r.id);
31749
33254
  if (stale.length > 0) {
31750
33255
  tx.delete(agentMemory).where(sql16`${agentMemory.id} IN (${sql16.join(stale.map((s) => sql16`${s}`), sql16`, `)})`).run();
31751
33256
  }
31752
- const row = tx.select().from(agentMemory).where(and32(eq40(agentMemory.projectId, args.projectId), eq40(agentMemory.key, key))).get();
33257
+ const row = tx.select().from(agentMemory).where(and33(eq41(agentMemory.projectId, args.projectId), eq41(agentMemory.key, key))).get();
31753
33258
  if (row) inserted = rowToDto2(row);
31754
33259
  });
31755
33260
  if (!inserted) throw new Error("compaction note write produced no row");
@@ -31882,7 +33387,7 @@ async function compactMessages(args) {
31882
33387
  }
31883
33388
 
31884
33389
  // src/agent/session-registry.ts
31885
- var log13 = createLogger("SessionRegistry");
33390
+ var log14 = createLogger("SessionRegistry");
31886
33391
  var MAX_HYDRATE_NOTES = 20;
31887
33392
  var MAX_HYDRATE_BYTES = 32 * 1024;
31888
33393
  function escapeMemoryFragment(value) {
@@ -31931,7 +33436,7 @@ var SessionRegistry = class {
31931
33436
  modelProvider: effectiveProvider,
31932
33437
  modelId: effectiveModelId,
31933
33438
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
31934
- }).where(eq41(agentSessions.projectId, projectId)).run();
33439
+ }).where(eq42(agentSessions.projectId, projectId)).run();
31935
33440
  }
31936
33441
  const agent2 = createAeroSession({
31937
33442
  projectName,
@@ -32109,13 +33614,13 @@ ${lines.join("\n")}
32109
33614
  agent.state.messages = result.messages;
32110
33615
  agent.state.systemPrompt = this.buildHydratedSystemPrompt(projectId, row.systemPrompt);
32111
33616
  this.save(projectName);
32112
- log13.info("compaction.completed", {
33617
+ log14.info("compaction.completed", {
32113
33618
  projectName,
32114
33619
  removedCount: result.removedCount,
32115
33620
  summaryBytes: Buffer.byteLength(result.summary, "utf8")
32116
33621
  });
32117
33622
  } catch (err) {
32118
- log13.error("compaction.failed", {
33623
+ log14.error("compaction.failed", {
32119
33624
  projectName,
32120
33625
  error: err instanceof Error ? err.message : String(err)
32121
33626
  });
@@ -32145,7 +33650,7 @@ ${lines.join("\n")}
32145
33650
  modelProvider: nextProvider,
32146
33651
  modelId: nextModelId,
32147
33652
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
32148
- }).where(eq41(agentSessions.projectId, projectId)).run();
33653
+ }).where(eq42(agentSessions.projectId, projectId)).run();
32149
33654
  }
32150
33655
  /** Persist a session's transcript back to the DB. Call after any run settles. */
32151
33656
  save(projectName) {
@@ -32212,7 +33717,7 @@ ${lines.join("\n")}
32212
33717
  await agent.prompt(msgs);
32213
33718
  this.save(projectName);
32214
33719
  } catch (err) {
32215
- log13.error("drain.failed", {
33720
+ log14.error("drain.failed", {
32216
33721
  projectName,
32217
33722
  error: err instanceof Error ? err.message : String(err)
32218
33723
  });
@@ -32307,17 +33812,17 @@ ${lines.join("\n")}
32307
33812
  return id;
32308
33813
  }
32309
33814
  tryResolveProjectId(projectName) {
32310
- const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq41(projects.name, projectName)).get();
33815
+ const row = this.opts.db.select({ id: projects.id }).from(projects).where(eq42(projects.name, projectName)).get();
32311
33816
  return row?.id;
32312
33817
  }
32313
33818
  loadRow(projectId) {
32314
- const row = this.opts.db.select().from(agentSessions).where(eq41(agentSessions.projectId, projectId)).get();
33819
+ const row = this.opts.db.select().from(agentSessions).where(eq42(agentSessions.projectId, projectId)).get();
32315
33820
  return row ?? null;
32316
33821
  }
32317
33822
  insertRow(params) {
32318
33823
  const now = (/* @__PURE__ */ new Date()).toISOString();
32319
33824
  this.opts.db.insert(agentSessions).values({
32320
- id: crypto35.randomUUID(),
33825
+ id: crypto39.randomUUID(),
32321
33826
  projectId: params.projectId,
32322
33827
  systemPrompt: params.systemPrompt,
32323
33828
  modelProvider: params.provider ?? params.modelProvider ?? AgentProviderIds.claude,
@@ -32330,14 +33835,14 @@ ${lines.join("\n")}
32330
33835
  }
32331
33836
  updateRow(projectId, patch) {
32332
33837
  const now = (/* @__PURE__ */ new Date()).toISOString();
32333
- this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq41(agentSessions.projectId, projectId)).run();
33838
+ this.opts.db.update(agentSessions).set({ ...patch, updatedAt: now }).where(eq42(agentSessions.projectId, projectId)).run();
32334
33839
  }
32335
33840
  };
32336
33841
 
32337
33842
  // src/agent/agent-routes.ts
32338
- import { eq as eq42 } from "drizzle-orm";
33843
+ import { eq as eq43 } from "drizzle-orm";
32339
33844
  function resolveProject2(db, name) {
32340
- const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq42(projects.name, name)).get();
33845
+ const row = db.select({ id: projects.id, name: projects.name }).from(projects).where(eq43(projects.name, name)).get();
32341
33846
  if (!row) throw notFound("project", name);
32342
33847
  return row;
32343
33848
  }
@@ -32346,7 +33851,7 @@ function registerAgentRoutes(app, opts) {
32346
33851
  "/projects/:name/agent/transcript",
32347
33852
  async (request) => {
32348
33853
  const project = resolveProject2(opts.db, request.params.name);
32349
- const row = opts.db.select().from(agentSessions).where(eq42(agentSessions.projectId, project.id)).get();
33854
+ const row = opts.db.select().from(agentSessions).where(eq43(agentSessions.projectId, project.id)).get();
32350
33855
  if (!row) {
32351
33856
  return { messages: [], modelProvider: null, modelId: null, updatedAt: null };
32352
33857
  }
@@ -32370,7 +33875,7 @@ function registerAgentRoutes(app, opts) {
32370
33875
  async (request) => {
32371
33876
  const project = resolveProject2(opts.db, request.params.name);
32372
33877
  opts.sessionRegistry.reset(project.name);
32373
- opts.db.update(agentSessions).set({ messages: "[]", followUpQueue: "[]", updatedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq42(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();
32374
33879
  return { status: "reset" };
32375
33880
  }
32376
33881
  );
@@ -32629,13 +34134,13 @@ function extractHostname(domain) {
32629
34134
  function fetchWithPinnedAddress(target) {
32630
34135
  return new Promise((resolve) => {
32631
34136
  const port = target.url.port ? Number(target.url.port) : 443;
32632
- const path16 = target.url.pathname + target.url.search;
34137
+ const path17 = target.url.pathname + target.url.search;
32633
34138
  const req = https2.request(
32634
34139
  {
32635
34140
  hostname: target.address,
32636
34141
  family: target.family,
32637
34142
  port,
32638
- path: path16,
34143
+ path: path17,
32639
34144
  method: "GET",
32640
34145
  timeout: FETCH_TIMEOUT_MS2,
32641
34146
  servername: target.url.hostname,
@@ -32727,7 +34232,7 @@ function formatAuditFactorScore(factor) {
32727
34232
  }
32728
34233
 
32729
34234
  // src/snapshot-service.ts
32730
- var log14 = createLogger("Snapshot");
34235
+ var log15 = createLogger("Snapshot");
32731
34236
  var ANALYSIS_PROVIDER_PRIORITY = ["openai", "claude", "gemini", "perplexity", "local"];
32732
34237
  var SNAPSHOT_QUERY_COUNT = 6;
32733
34238
  var ProviderExecutionGate2 = class {
@@ -32870,7 +34375,7 @@ var SnapshotService = class {
32870
34375
  return mapAuditReport(report);
32871
34376
  } catch (err) {
32872
34377
  const message = err instanceof Error ? err.message : String(err);
32873
- log14.warn("audit.failed", { homepageUrl, error: message });
34378
+ log15.warn("audit.failed", { homepageUrl, error: message });
32874
34379
  return {
32875
34380
  url: homepageUrl,
32876
34381
  finalUrl: homepageUrl,
@@ -32900,7 +34405,7 @@ var SnapshotService = class {
32900
34405
  queries: parsedQueries
32901
34406
  };
32902
34407
  } catch (err) {
32903
- log14.warn("profile.generation-failed", {
34408
+ log15.warn("profile.generation-failed", {
32904
34409
  domain: ctx.domain,
32905
34410
  provider: ctx.analysisProvider.adapter.name,
32906
34411
  error: err instanceof Error ? err.message : String(err)
@@ -33042,7 +34547,7 @@ var SnapshotService = class {
33042
34547
  recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
33043
34548
  };
33044
34549
  } catch (err) {
33045
- log14.warn("response.analysis-failed", {
34550
+ log15.warn("response.analysis-failed", {
33046
34551
  provider: ctx.analysisProvider.adapter.name,
33047
34552
  error: err instanceof Error ? err.message : String(err)
33048
34553
  });
@@ -33327,7 +34832,7 @@ function clipText(value, length) {
33327
34832
  // src/server.ts
33328
34833
  var _require3 = createRequire4(import.meta.url);
33329
34834
  var { version: PKG_VERSION2 } = _require3("../package.json");
33330
- var log15 = createLogger("Server");
34835
+ var log16 = createLogger("Server");
33331
34836
  var DEFAULT_QUOTA = {
33332
34837
  maxConcurrency: 2,
33333
34838
  maxRequestsPerMinute: 10,
@@ -33357,14 +34862,14 @@ function summarizeProviderConfig(provider, config) {
33357
34862
  };
33358
34863
  }
33359
34864
  function hashApiKey(key) {
33360
- return crypto36.createHash("sha256").update(key).digest("hex");
34865
+ return crypto40.createHash("sha256").update(key).digest("hex");
33361
34866
  }
33362
34867
  var DASHBOARD_SCRYPT_KEYLEN = 64;
33363
34868
  var DASHBOARD_SCRYPT_COST = 1 << 15;
33364
34869
  var DASHBOARD_SCRYPT_MAXMEM = 64 * 1024 * 1024;
33365
34870
  function hashDashboardPassword(password) {
33366
- const salt = crypto36.randomBytes(16);
33367
- const derived = crypto36.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
34871
+ const salt = crypto40.randomBytes(16);
34872
+ const derived = crypto40.scryptSync(password, salt, DASHBOARD_SCRYPT_KEYLEN, {
33368
34873
  N: DASHBOARD_SCRYPT_COST,
33369
34874
  maxmem: DASHBOARD_SCRYPT_MAXMEM
33370
34875
  });
@@ -33385,18 +34890,18 @@ function verifyDashboardPassword(password, storedHash) {
33385
34890
  } catch {
33386
34891
  return { ok: false, needsRehash: false };
33387
34892
  }
33388
- const derived = crypto36.scryptSync(password, salt, expected.length, {
34893
+ const derived = crypto40.scryptSync(password, salt, expected.length, {
33389
34894
  N: DASHBOARD_SCRYPT_COST,
33390
34895
  maxmem: DASHBOARD_SCRYPT_MAXMEM
33391
34896
  });
33392
34897
  if (derived.length !== expected.length) return { ok: false, needsRehash: false };
33393
- return { ok: crypto36.timingSafeEqual(derived, expected), needsRehash: false };
34898
+ return { ok: crypto40.timingSafeEqual(derived, expected), needsRehash: false };
33394
34899
  }
33395
34900
  if (/^[a-f0-9]{64}$/i.test(storedHash)) {
33396
34901
  const candidate = Buffer.from(hashApiKey(password), "hex");
33397
34902
  const expected = Buffer.from(storedHash, "hex");
33398
34903
  if (candidate.length !== expected.length) return { ok: false, needsRehash: false };
33399
- const ok = crypto36.timingSafeEqual(candidate, expected);
34904
+ const ok = crypto40.timingSafeEqual(candidate, expected);
33400
34905
  return { ok, needsRehash: ok };
33401
34906
  }
33402
34907
  return { ok: false, needsRehash: false };
@@ -33455,7 +34960,7 @@ function applyLegacyCredentials(rows, config) {
33455
34960
  }
33456
34961
  if (migratedGoogle > 0) {
33457
34962
  saveConfigPatch({ google: config.google });
33458
- log15.info("credentials.migrated", { type: "google", count: migratedGoogle });
34963
+ log16.info("credentials.migrated", { type: "google", count: migratedGoogle });
33459
34964
  }
33460
34965
  let migratedGa4 = 0;
33461
34966
  for (const row of rows.ga4) {
@@ -33473,7 +34978,7 @@ function applyLegacyCredentials(rows, config) {
33473
34978
  }
33474
34979
  if (migratedGa4 > 0) {
33475
34980
  saveConfigPatch({ ga4: config.ga4 });
33476
- log15.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
34981
+ log16.info("credentials.migrated", { type: "ga4", count: migratedGa4 });
33477
34982
  }
33478
34983
  }
33479
34984
  async function createServer(opts) {
@@ -33505,11 +35010,11 @@ async function createServer(opts) {
33505
35010
  applyLegacyCredentials(legacyRows, opts.config);
33506
35011
  dropLegacyCredentialColumns(opts.db);
33507
35012
  } catch (err) {
33508
- log15.warn("credentials.migration.failed", {
35013
+ log16.warn("credentials.migration.failed", {
33509
35014
  error: err instanceof Error ? err.message : String(err)
33510
35015
  });
33511
35016
  }
33512
- log15.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
35017
+ log16.info("providers.configured", { providers: Object.keys(providers).filter((k) => {
33513
35018
  const p = providers[k];
33514
35019
  return p?.apiKey || p?.baseUrl || p?.vertexProject;
33515
35020
  }) });
@@ -33558,7 +35063,7 @@ async function createServer(opts) {
33558
35063
  intelligenceService,
33559
35064
  (runId, projectId, result) => notifier.dispatchInsightWebhooks(runId, projectId, result),
33560
35065
  async (ctx) => {
33561
- const project = opts.db.select({ name: projects.name }).from(projects).where(eq43(projects.id, ctx.projectId)).get();
35066
+ const project = opts.db.select({ name: projects.name }).from(projects).where(eq44(projects.id, ctx.projectId)).get();
33562
35067
  if (!project) return;
33563
35068
  let content;
33564
35069
  if (ctx.kind === RunKinds["aeo-discover-probe"]) {
@@ -33581,8 +35086,8 @@ async function createServer(opts) {
33581
35086
  );
33582
35087
  jobRunner.onRunCompleted = (runId, projectId) => runCoordinator.onRunCompleted(runId, projectId);
33583
35088
  const snapshotService = new SnapshotService(registry);
33584
- const orphanedOpenClawDir = path15.join(os6.homedir(), ".openclaw-aero");
33585
- if (fs15.existsSync(orphanedOpenClawDir)) {
35089
+ const orphanedOpenClawDir = path16.join(os7.homedir(), ".openclaw-aero");
35090
+ if (fs16.existsSync(orphanedOpenClawDir)) {
33586
35091
  app.log.warn(
33587
35092
  { path: orphanedOpenClawDir },
33588
35093
  "OpenClaw gateway is no longer used. Remove ~/.openclaw-aero/ manually to reclaim the directory."
@@ -33715,7 +35220,7 @@ async function createServer(opts) {
33715
35220
  return removed;
33716
35221
  }
33717
35222
  };
33718
- const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto36.randomBytes(32).toString("hex");
35223
+ const googleStateSecret = process.env.GOOGLE_STATE_SECRET ?? crypto40.randomBytes(32).toString("hex");
33719
35224
  const googleConnectionStore = {
33720
35225
  listConnections: (domain) => listGoogleConnections(opts.config, domain),
33721
35226
  getConnection: (domain, connectionType) => getGoogleConnection(opts.config, domain, connectionType),
@@ -33761,11 +35266,11 @@ async function createServer(opts) {
33761
35266
  const apiPrefix = basePath ? `${basePath}api/v1` : "/api/v1";
33762
35267
  if (opts.config.apiKey) {
33763
35268
  const keyHash = hashApiKey(opts.config.apiKey);
33764
- const existing = opts.db.select().from(apiKeys).where(eq43(apiKeys.keyHash, keyHash)).get();
35269
+ const existing = opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, keyHash)).get();
33765
35270
  if (!existing) {
33766
35271
  const prefix = opts.config.apiKey.slice(0, 12);
33767
35272
  opts.db.insert(apiKeys).values({
33768
- id: `key_${crypto36.randomBytes(8).toString("hex")}`,
35273
+ id: `key_${crypto40.randomBytes(8).toString("hex")}`,
33769
35274
  name: "default",
33770
35275
  keyHash,
33771
35276
  keyPrefix: prefix,
@@ -33789,7 +35294,7 @@ async function createServer(opts) {
33789
35294
  };
33790
35295
  const createSession = (apiKeyId) => {
33791
35296
  pruneExpiredSessions();
33792
- const sessionId = crypto36.randomBytes(32).toString("hex");
35297
+ const sessionId = crypto40.randomBytes(32).toString("hex");
33793
35298
  sessions.set(sessionId, {
33794
35299
  apiKeyId,
33795
35300
  expiresAt: Date.now() + SESSION_TTL_MS
@@ -33813,7 +35318,7 @@ async function createServer(opts) {
33813
35318
  };
33814
35319
  const getDefaultApiKey = () => {
33815
35320
  if (!opts.config.apiKey) return void 0;
33816
- return opts.db.select().from(apiKeys).where(eq43(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
35321
+ return opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, hashApiKey(opts.config.apiKey))).get();
33817
35322
  };
33818
35323
  const createPasswordSession = (reply) => {
33819
35324
  const key = getDefaultApiKey();
@@ -33875,12 +35380,12 @@ async function createServer(opts) {
33875
35380
  return reply.send({ authenticated: true });
33876
35381
  }
33877
35382
  if (apiKey) {
33878
- const key = opts.db.select().from(apiKeys).where(eq43(apiKeys.keyHash, hashApiKey(apiKey))).get();
35383
+ const key = opts.db.select().from(apiKeys).where(eq44(apiKeys.keyHash, hashApiKey(apiKey))).get();
33879
35384
  if (!key || key.revokedAt) {
33880
35385
  const err2 = authInvalid();
33881
35386
  return reply.status(err2.statusCode).send(err2.toJSON());
33882
35387
  }
33883
- opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq43(apiKeys.id, key.id)).run();
35388
+ opts.db.update(apiKeys).set({ lastUsedAt: (/* @__PURE__ */ new Date()).toISOString() }).where(eq44(apiKeys.id, key.id)).run();
33884
35389
  const sessionId = createSession(key.id);
33885
35390
  reply.header("set-cookie", serializeSessionCookie({
33886
35391
  name: SESSION_COOKIE_NAME,
@@ -33951,9 +35456,20 @@ async function createServer(opts) {
33951
35456
  const configPath = getConfigPath();
33952
35457
  return {
33953
35458
  databasePath: opts.config.database,
33954
- configPath: fs15.existsSync(configPath) ? configPath : null
35459
+ configPath: fs16.existsSync(configPath) ? configPath : null
33955
35460
  };
33956
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
+ })(),
33957
35473
  // Local canonry serve runs on the operator's machine, where pointing a
33958
35474
  // webhook at localhost (Discord test container, Pipedream-mock dev server,
33959
35475
  // etc.) is a legitimate workflow. Default to allowing it for the local
@@ -33995,6 +35511,19 @@ async function createServer(opts) {
33995
35511
  app.log.error({ runId, err }, "Inspect sitemap failed");
33996
35512
  });
33997
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
+ },
33998
35527
  getBacklinksStatus: () => ({
33999
35528
  duckdbInstalled: isDuckdbInstalled(),
34000
35529
  duckdbVersion: readInstalledVersion() ?? void 0,
@@ -34016,7 +35545,7 @@ async function createServer(opts) {
34016
35545
  deps: {
34017
35546
  enqueueAutoExtract: ({ projectId, release: r }) => {
34018
35547
  const now = (/* @__PURE__ */ new Date()).toISOString();
34019
- const runId = crypto36.randomUUID();
35548
+ const runId = crypto40.randomUUID();
34020
35549
  opts.db.insert(runs).values({
34021
35550
  id: runId,
34022
35551
  projectId,
@@ -34099,7 +35628,7 @@ async function createServer(opts) {
34099
35628
  ...inspectOpts,
34100
35629
  config: opts.config
34101
35630
  }).then(() => {
34102
- const finished = opts.db.select({ status: runs.status }).from(runs).where(eq43(runs.id, runId)).get();
35631
+ const finished = opts.db.select({ status: runs.status }).from(runs).where(eq44(runs.id, runId)).get();
34103
35632
  if (finished?.status === RunStatuses.completed || finished?.status === RunStatuses.partial) {
34104
35633
  return maybeRefreshGscCoverage(opts.db, opts.config, projectId);
34105
35634
  }
@@ -34186,7 +35715,7 @@ async function createServer(opts) {
34186
35715
  const targetProjectIds = affectedProjectIds.length > 0 ? affectedProjectIds : [null];
34187
35716
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
34188
35717
  opts.db.insert(auditLog).values(targetProjectIds.map((projectId) => ({
34189
- id: crypto36.randomUUID(),
35718
+ id: crypto40.randomUUID(),
34190
35719
  projectId,
34191
35720
  actor: "api",
34192
35721
  action: existing ? "provider.updated" : "provider.created",
@@ -34330,10 +35859,10 @@ async function createServer(opts) {
34330
35859
  return snapshotService.createReport(input);
34331
35860
  }
34332
35861
  });
34333
- const dirname = path15.dirname(fileURLToPath2(import.meta.url));
34334
- const assetsDir = path15.join(dirname, "..", "assets");
34335
- if (fs15.existsSync(assetsDir)) {
34336
- 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");
34337
35866
  const injectConfig = (html) => {
34338
35867
  const clientConfig = {};
34339
35868
  if (basePath) clientConfig.basePath = basePath;
@@ -34365,8 +35894,8 @@ async function createServer(opts) {
34365
35894
  }
34366
35895
  });
34367
35896
  const serveIndex = (_request, reply) => {
34368
- if (fs15.existsSync(indexPath)) {
34369
- const html = fs15.readFileSync(indexPath, "utf-8");
35897
+ if (fs16.existsSync(indexPath)) {
35898
+ const html = fs16.readFileSync(indexPath, "utf-8");
34370
35899
  return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
34371
35900
  }
34372
35901
  return reply.status(404).send({ error: "Dashboard not built" });
@@ -34386,8 +35915,8 @@ async function createServer(opts) {
34386
35915
  if (basePath && !url.startsWith(basePath)) {
34387
35916
  return reply.status(404).send({ error: "Not found", path: request.url });
34388
35917
  }
34389
- if (fs15.existsSync(indexPath)) {
34390
- const html = fs15.readFileSync(indexPath, "utf-8");
35918
+ if (fs16.existsSync(indexPath)) {
35919
+ const html = fs16.readFileSync(indexPath, "utf-8");
34391
35920
  return reply.header("Cache-Control", "no-cache, must-revalidate").type("text/html").send(injectConfig(html));
34392
35921
  }
34393
35922
  return reply.status(404).send({ error: "Not found" });
@@ -34479,6 +36008,11 @@ export {
34479
36008
  backfillTrafficClassificationCommand,
34480
36009
  renderReportHtml,
34481
36010
  setGoogleAuthConfig,
36011
+ installSkills,
36012
+ listSkills,
36013
+ emitInstallSummary,
36014
+ getMissingUserSkillsNudge,
36015
+ parseSkillsClient,
34482
36016
  formatAuditFactorScore,
34483
36017
  checkLatestVersionForCli,
34484
36018
  listAgentProviders,