@ainyc/canonry 4.55.1 → 4.56.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 (30) hide show
  1. package/assets/agent-workspace/skills/aero/SKILL.md +2 -0
  2. package/assets/agent-workspace/skills/canonry/SKILL.md +2 -0
  3. package/assets/agent-workspace/skills/canonry/references/server-side-traffic.md +55 -7
  4. package/assets/assets/{BacklinksPage-DVmaM864.js → BacklinksPage-yWx_BBLG.js} +1 -1
  5. package/assets/assets/ChartPrimitives-gqioJ07n.js +1 -0
  6. package/assets/assets/ProjectPage-D1tnXJ2L.js +6 -0
  7. package/assets/assets/{RunRow-BRqiLxj2.js → RunRow-DEr_yQLw.js} +1 -1
  8. package/assets/assets/{RunsPage-UxZ93-cg.js → RunsPage-DMrl5Fhn.js} +1 -1
  9. package/assets/assets/{SettingsPage-Cr5_EGbk.js → SettingsPage-Dp0TXf34.js} +1 -1
  10. package/assets/assets/{TrafficPage-CUC_lfTe.js → TrafficPage-POy4iHHt.js} +1 -1
  11. package/assets/assets/TrafficSourceDetailPage-BewjZ53n.js +1 -0
  12. package/assets/assets/{extract-error-message-DD5MibWI.js → extract-error-message-C0GGpK4T.js} +1 -1
  13. package/assets/assets/{index-nnF1LnyK.js → index-D0A-UvNH.js} +79 -79
  14. package/assets/assets/index-_jdnW4nh.css +1 -0
  15. package/assets/assets/{server-traffic-DjRISEZ-.js → server-traffic-B5rtrB-q.js} +1 -1
  16. package/assets/assets/{trash-2-CJ5M--Le.js → trash-2-CUczQ2Yl.js} +1 -1
  17. package/assets/index.html +2 -2
  18. package/dist/{chunk-UTM3FPAJ.js → chunk-4KWPOVIT.js} +181 -3
  19. package/dist/{chunk-ZY3EDW3S.js → chunk-6X5TF73A.js} +49 -3
  20. package/dist/{chunk-2OI7HFAB.js → chunk-I2LAM5IM.js} +309 -222
  21. package/dist/{chunk-OFY3Z2F7.js → chunk-WFVUZVJD.js} +368 -361
  22. package/dist/cli.js +66 -11
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-NKAEHHJ5.js → intelligence-service-NY3MAVPB.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +10 -10
  27. package/assets/assets/ChartPrimitives-9Kx3gzQL.js +0 -1
  28. package/assets/assets/ProjectPage-DtL3LFne.js +0 -6
  29. package/assets/assets/TrafficSourceDetailPage-DARPL2TU.js +0 -1
  30. package/assets/assets/index-Bm3JQsW0.css +0 -1
@@ -6,7 +6,7 @@ import {
6
6
  loadConfig,
7
7
  loadConfigRaw,
8
8
  saveConfigPatch
9
- } from "./chunk-ZY3EDW3S.js";
9
+ } from "./chunk-6X5TF73A.js";
10
10
  import {
11
11
  DEFAULT_RUN_HISTORY_LIMIT,
12
12
  IntelligenceService,
@@ -45,14 +45,17 @@ import {
45
45
  categorizeQueryByIntent,
46
46
  ccReleaseSyncs,
47
47
  competitors,
48
+ computeCompetitorOverlap,
48
49
  contentTargetDismissals,
49
50
  crawlerEventsHourly,
50
51
  createClient,
51
52
  createLogger,
53
+ determineCitationState,
52
54
  discoveryProbes,
53
55
  discoverySessions,
54
56
  dropLegacyCredentialColumns,
55
57
  extractLegacyCredentials,
58
+ extractRecommendedCompetitors,
56
59
  filterTrackedSnapshots,
57
60
  gaAiReferrals,
58
61
  gaSocialReferrals,
@@ -84,7 +87,7 @@ import {
84
87
  smoothedRunDelta,
85
88
  trafficSources,
86
89
  usageCounters
87
- } from "./chunk-UTM3FPAJ.js";
90
+ } from "./chunk-4KWPOVIT.js";
88
91
  import {
89
92
  AGENT_MEMORY_VALUE_MAX_BYTES,
90
93
  AGENT_PROVIDER_IDS,
@@ -144,7 +147,6 @@ import {
144
147
  bingSitesResponseDtoSchema,
145
148
  bingStatusDtoSchema,
146
149
  bingUrlInspectionDtoSchema,
147
- brandKeyFromText,
148
150
  brandLabelFromDomain,
149
151
  buildRunErrorFromMessages,
150
152
  categorizeSource,
@@ -265,6 +267,7 @@ import {
265
267
  trafficConnectVercelRequestSchema,
266
268
  trafficConnectWordpressRequestSchema,
267
269
  trafficEventsResponseSchema,
270
+ trafficResetRequestSchema,
268
271
  trafficSourceDetailDtoSchema,
269
272
  trafficSourceDtoSchema,
270
273
  trafficSourceListResponseSchema,
@@ -286,7 +289,7 @@ import {
286
289
  wordpressSchemaDeployResultDtoSchema,
287
290
  wordpressSchemaStatusResultDtoSchema,
288
291
  wordpressStatusDtoSchema
289
- } from "./chunk-OFY3Z2F7.js";
292
+ } from "./chunk-WFVUZVJD.js";
290
293
 
291
294
  // src/telemetry.ts
292
295
  import crypto from "crypto";
@@ -1657,7 +1660,9 @@ async function runRoutes(app, opts) {
1657
1660
  const project = resolveProject(app.db, request.params.name);
1658
1661
  const parsedLimit = parseInt(request.query.limit ?? "", 10);
1659
1662
  const limit = Number.isNaN(parsedLimit) || parsedLimit <= 0 ? void 0 : parsedLimit;
1660
- const rows = limit == null ? app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(eq7(runs.projectId, project.id)).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
1663
+ const kind = parseListKind(request.query.kind);
1664
+ const where = kind ? and2(eq7(runs.projectId, project.id), eq7(runs.kind, kind)) : eq7(runs.projectId, project.id);
1665
+ const rows = limit == null ? app.db.select().from(runs).where(where).orderBy(asc(runs.createdAt)).all() : app.db.select().from(runs).where(where).orderBy(desc(runs.createdAt)).limit(limit).all().reverse();
1661
1666
  return reply.send(rows.map(formatRun));
1662
1667
  });
1663
1668
  app.get("/projects/:name/runs/latest", async (request, reply) => {
@@ -9118,7 +9123,7 @@ var routeCatalog = [
9118
9123
  path: "/api/v1/projects/{name}/runs",
9119
9124
  summary: "List project runs",
9120
9125
  tags: ["runs"],
9121
- parameters: [nameParameter, limitQueryParameter],
9126
+ parameters: [nameParameter, limitQueryParameter, runsListKindQueryParameter],
9122
9127
  responses: {
9123
9128
  200: jsonArrayResponse("Runs returned.", "RunDto")
9124
9129
  }
@@ -11498,6 +11503,36 @@ var routeCatalog = [
11498
11503
  404: errorResponse("Project or traffic source not found.")
11499
11504
  }
11500
11505
  },
11506
+ {
11507
+ method: "post",
11508
+ path: "/api/v1/projects/{name}/traffic/sources/{id}/reset",
11509
+ summary: "Advance lastSyncedAt to NOW and clear the error state",
11510
+ description: "Operator recovery: advances `lastSyncedAt` to NOW, sets `status` back to `connected`, and clears `last_error`. Accepts any non-archived source \u2014 the `lastSyncedAt` advance determines the next sync window for time-windowed sources (Vercel, Cloud Run) and is informational for cursor-based sources (WordPress, where `last_cursor` governs the next drain and is preserved). Common trigger: an idle Vercel/Cloud Run source whose `lastSyncedAt` has aged past the upstream retention window (`request-logs` ~14d, Cloud Logging 30d) and now throws on every sync. Any pre-existing rollup history stays in place; the skipped history is the explicit trade-off \u2014 run `traffic backfill` separately to recover any of it. `advanceToNow: true` is required (no implicit reset). Archived sources are rejected with 400 \u2014 re-connect them via the appropriate `traffic/connect/*` endpoint instead.",
11511
+ tags: ["traffic"],
11512
+ parameters: [
11513
+ nameParameter,
11514
+ { name: "id", in: "path", required: true, description: "Traffic source ID.", schema: stringSchema }
11515
+ ],
11516
+ requestBody: {
11517
+ required: true,
11518
+ content: {
11519
+ "application/json": {
11520
+ schema: {
11521
+ type: "object",
11522
+ required: ["advanceToNow"],
11523
+ properties: {
11524
+ advanceToNow: { type: "boolean", enum: [true], description: "Must be `true` \u2014 explicit gate against accidental resets." }
11525
+ }
11526
+ }
11527
+ }
11528
+ }
11529
+ },
11530
+ responses: {
11531
+ 200: jsonResponse("Source reset; lastSyncedAt advanced to NOW.", "TrafficSourceDetailDto"),
11532
+ 400: errorResponse("Missing or invalid `advanceToNow` flag, or the source is archived."),
11533
+ 404: errorResponse("Project or traffic source not found.")
11534
+ }
11535
+ },
11501
11536
  {
11502
11537
  method: "get",
11503
11538
  path: "/api/v1/projects/{name}/traffic/sources",
@@ -21586,14 +21621,34 @@ var VERCEL_REQUEST_LOGS_URL = "https://vercel.com/api/logs/request-logs";
21586
21621
  var DEFAULT_ENVIRONMENT = "production";
21587
21622
  var DEFAULT_MAX_PAGES3 = 1;
21588
21623
  var DEFAULT_TIMEOUT_MS3 = 3e4;
21624
+ var DEFAULT_MAX_RETRIES = 3;
21625
+ var DEFAULT_INITIAL_RETRY_DELAY_MS = 1e3;
21589
21626
  var VercelLogsApiError = class extends Error {
21590
- constructor(message, status, body) {
21627
+ constructor(message, status, body, retryAfterSeconds) {
21591
21628
  super(message);
21592
21629
  this.status = status;
21593
21630
  this.body = body;
21631
+ this.retryAfterSeconds = retryAfterSeconds;
21594
21632
  this.name = "VercelLogsApiError";
21595
21633
  }
21596
21634
  };
21635
+ function parseRetryAfter2(headerValue) {
21636
+ if (!headerValue) return void 0;
21637
+ const trimmed = headerValue.trim();
21638
+ const asNum = Number(trimmed);
21639
+ if (Number.isFinite(asNum) && asNum >= 0) return asNum;
21640
+ const asDate = Date.parse(trimmed);
21641
+ if (!Number.isNaN(asDate)) {
21642
+ return Math.max(0, (asDate - Date.now()) / 1e3);
21643
+ }
21644
+ return void 0;
21645
+ }
21646
+ function isRetryableVercelError(error) {
21647
+ if (error instanceof VercelLogsApiError) {
21648
+ return error.status === 429 || error.status >= 500;
21649
+ }
21650
+ return true;
21651
+ }
21597
21652
  function trimRequired2(name, value) {
21598
21653
  const trimmed = value.trim();
21599
21654
  if (!trimmed) {
@@ -21620,6 +21675,24 @@ async function readErrorBody3(response) {
21620
21675
  if (!text) return void 0;
21621
21676
  return text.length <= 500 ? text : `${text.slice(0, 500)}... [truncated]`;
21622
21677
  }
21678
+ async function withVercelRetry(attempt, maxRetries, initialDelayMs) {
21679
+ let lastError;
21680
+ for (let attemptNumber = 0; attemptNumber <= maxRetries; attemptNumber += 1) {
21681
+ try {
21682
+ return await attempt();
21683
+ } catch (error) {
21684
+ lastError = error;
21685
+ if (attemptNumber >= maxRetries || !isRetryableVercelError(error)) throw error;
21686
+ const retryAfterSeconds = error instanceof VercelLogsApiError ? error.retryAfterSeconds : void 0;
21687
+ const computedDelayMs = initialDelayMs * Math.pow(2, attemptNumber);
21688
+ const delayMs = retryAfterSeconds !== void 0 ? Math.max(0, retryAfterSeconds * 1e3) : computedDelayMs;
21689
+ if (delayMs > 0) {
21690
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
21691
+ }
21692
+ }
21693
+ }
21694
+ throw lastError;
21695
+ }
21623
21696
  async function listVercelTrafficEvents(options) {
21624
21697
  const token = trimRequired2("token", options.token);
21625
21698
  const projectId = trimRequired2("projectId", options.projectId);
@@ -21629,6 +21702,8 @@ async function listVercelTrafficEvents(options) {
21629
21702
  const endDate = toEpochMs("endDate", options.endDate);
21630
21703
  const maxPages = normalizeMaxPages3(options.maxPages);
21631
21704
  const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
21705
+ const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
21706
+ const initialRetryDelayMs = options.initialRetryDelayMs ?? DEFAULT_INITIAL_RETRY_DELAY_MS;
21632
21707
  let rawEntryCount = 0;
21633
21708
  let skippedEntryCount = 0;
21634
21709
  let hasMore = false;
@@ -21642,23 +21717,27 @@ async function listVercelTrafficEvents(options) {
21642
21717
  url.searchParams.set("startDate", startDate);
21643
21718
  url.searchParams.set("endDate", endDate);
21644
21719
  url.searchParams.set("environment", environment);
21645
- const response = await fetch(url, {
21646
- method: "GET",
21647
- headers: {
21648
- Authorization: `Bearer ${token}`,
21649
- Accept: "application/json"
21650
- },
21651
- signal: AbortSignal.timeout(timeoutMs)
21652
- });
21653
- if (!response.ok) {
21654
- const body2 = await readErrorBody3(response);
21655
- throw new VercelLogsApiError(
21656
- `Vercel request-logs endpoint returned HTTP ${response.status}`,
21657
- response.status,
21658
- body2
21659
- );
21660
- }
21661
- const body = await response.json();
21720
+ const body = await withVercelRetry(async () => {
21721
+ const response = await fetch(url, {
21722
+ method: "GET",
21723
+ headers: {
21724
+ Authorization: `Bearer ${token}`,
21725
+ Accept: "application/json"
21726
+ },
21727
+ signal: AbortSignal.timeout(timeoutMs)
21728
+ });
21729
+ if (!response.ok) {
21730
+ const errorBody = await readErrorBody3(response);
21731
+ const retryAfterSeconds = parseRetryAfter2(response.headers.get("retry-after"));
21732
+ throw new VercelLogsApiError(
21733
+ `Vercel request-logs endpoint returned HTTP ${response.status}`,
21734
+ response.status,
21735
+ errorBody,
21736
+ retryAfterSeconds
21737
+ );
21738
+ }
21739
+ return await response.json();
21740
+ }, maxRetries, initialRetryDelayMs);
21662
21741
  const rows = body.rows ?? [];
21663
21742
  rawEntryCount += rows.length;
21664
21743
  for (const row of rows) {
@@ -21682,19 +21761,67 @@ async function listVercelTrafficEvents(options) {
21682
21761
  }
21683
21762
 
21684
21763
  // ../integration-vercel/src/drain.ts
21685
- var MIN_SUB_WINDOW_MS = 6e4;
21764
+ var MIN_SUB_WINDOW_MS = 1e3;
21765
+ var FLOOR_SLICE_MAX_PAGES = 1e3;
21766
+ var FLOOR_CONGESTION_PROBE_INTERVAL = 60;
21767
+ var RETENTION_PROBE_WINDOW_MS = 6e4;
21768
+ var RETENTION_BOUNDARY_TOLERANCE_MS = 60 * 6e4;
21686
21769
  function toMs(value) {
21687
21770
  return typeof value === "number" ? value : value.getTime();
21688
21771
  }
21772
+ function isRetentionError(error) {
21773
+ return error instanceof VercelLogsApiError && error.status === 400 && (error.body ?? "").includes("ExceedsBillingLimitError");
21774
+ }
21775
+ async function isServable(options, windowStartMs, windowEndMs) {
21776
+ try {
21777
+ await options.pull({
21778
+ token: options.token,
21779
+ projectId: options.projectId,
21780
+ teamId: options.teamId,
21781
+ environment: options.environment,
21782
+ startDate: windowStartMs,
21783
+ endDate: windowEndMs,
21784
+ maxPages: 1
21785
+ });
21786
+ return true;
21787
+ } catch (error) {
21788
+ if (isRetentionError(error)) return false;
21789
+ throw error;
21790
+ }
21791
+ }
21792
+ async function resolveRetainedStart(options, unservableStartMs, endMs) {
21793
+ const tailStartMs = Math.max(unservableStartMs, endMs - RETENTION_PROBE_WINDOW_MS);
21794
+ if (!await isServable(options, tailStartMs, endMs)) {
21795
+ return endMs;
21796
+ }
21797
+ let lo = unservableStartMs;
21798
+ let hi = tailStartMs;
21799
+ while (hi - lo > RETENTION_BOUNDARY_TOLERANCE_MS) {
21800
+ const mid = lo + Math.floor((hi - lo) / 2);
21801
+ if (await isServable(options, mid, endMs)) {
21802
+ hi = mid;
21803
+ } else {
21804
+ lo = mid;
21805
+ }
21806
+ }
21807
+ return hi;
21808
+ }
21689
21809
  async function drainVercelTrafficEvents(options) {
21690
21810
  const startMs = toMs(options.startDate);
21691
21811
  const endMs = toMs(options.endDate);
21692
21812
  const events = [];
21693
21813
  const seenEventIds = /* @__PURE__ */ new Set();
21694
- if (endMs <= startMs) return { events, subWindowCount: 0 };
21814
+ if (endMs <= startMs) {
21815
+ return { events, subWindowCount: 0, effectiveStartMs: startMs, retentionClamped: false };
21816
+ }
21695
21817
  let cursorMs = startMs;
21696
21818
  let spanMs = endMs - startMs;
21697
21819
  let subWindowCount = 0;
21820
+ let effectiveStartMs = startMs;
21821
+ let retentionClamped = false;
21822
+ let retentionResolved = false;
21823
+ let floorSpanProbeCountdown = 0;
21824
+ let floorPageBudgetCountdown = 0;
21698
21825
  while (cursorMs < endMs) {
21699
21826
  if (subWindowCount >= options.maxSubWindows) {
21700
21827
  throw new Error(
@@ -21702,25 +21829,63 @@ async function drainVercelTrafficEvents(options) {
21702
21829
  );
21703
21830
  }
21704
21831
  const subEndMs = Math.min(cursorMs + spanMs, endMs);
21705
- const page = await options.pull({
21706
- token: options.token,
21707
- projectId: options.projectId,
21708
- teamId: options.teamId,
21709
- environment: options.environment,
21710
- startDate: cursorMs,
21711
- endDate: subEndMs,
21712
- maxPages: options.pagesPerSubWindow
21713
- });
21832
+ const subSpanMs = subEndMs - cursorMs;
21833
+ const useFloorPageBudget = subSpanMs <= MIN_SUB_WINDOW_MS && floorPageBudgetCountdown > 0;
21834
+ const pageBudget = useFloorPageBudget ? FLOOR_SLICE_MAX_PAGES : options.pagesPerSubWindow;
21835
+ let page;
21836
+ try {
21837
+ page = await options.pull({
21838
+ token: options.token,
21839
+ projectId: options.projectId,
21840
+ teamId: options.teamId,
21841
+ environment: options.environment,
21842
+ startDate: cursorMs,
21843
+ endDate: subEndMs,
21844
+ maxPages: pageBudget
21845
+ });
21846
+ } catch (error) {
21847
+ if (isRetentionError(error) && !retentionResolved) {
21848
+ retentionResolved = true;
21849
+ const retainedStartMs = await resolveRetainedStart(options, cursorMs, endMs);
21850
+ retentionClamped = retainedStartMs > cursorMs;
21851
+ cursorMs = retainedStartMs;
21852
+ effectiveStartMs = retainedStartMs;
21853
+ spanMs = Math.max(endMs - cursorMs, MIN_SUB_WINDOW_MS);
21854
+ continue;
21855
+ }
21856
+ throw error;
21857
+ }
21714
21858
  subWindowCount += 1;
21715
21859
  if (page.hasMore) {
21716
- const subSpanMs = subEndMs - cursorMs;
21717
- if (subSpanMs <= MIN_SUB_WINDOW_MS) {
21860
+ if (subSpanMs > MIN_SUB_WINDOW_MS) {
21861
+ spanMs = Math.max(Math.floor(subSpanMs / 2), MIN_SUB_WINDOW_MS);
21862
+ if (spanMs === MIN_SUB_WINDOW_MS) {
21863
+ floorSpanProbeCountdown = FLOOR_CONGESTION_PROBE_INTERVAL;
21864
+ }
21865
+ continue;
21866
+ }
21867
+ if (pageBudget >= FLOOR_SLICE_MAX_PAGES) {
21718
21868
  throw new Error(
21719
- `Vercel window holds more than ${options.pagesPerSubWindow} pages within a ${MIN_SUB_WINDOW_MS / 6e4}-minute slice \u2014 cannot subdivide further`
21869
+ `Vercel ${MIN_SUB_WINDOW_MS / 1e3}-second slice holds more than ${FLOOR_SLICE_MAX_PAGES} pages and cannot be drained further`
21870
+ );
21871
+ }
21872
+ page = await options.pull({
21873
+ token: options.token,
21874
+ projectId: options.projectId,
21875
+ teamId: options.teamId,
21876
+ environment: options.environment,
21877
+ startDate: cursorMs,
21878
+ endDate: subEndMs,
21879
+ maxPages: FLOOR_SLICE_MAX_PAGES
21880
+ });
21881
+ subWindowCount += 1;
21882
+ floorSpanProbeCountdown = FLOOR_CONGESTION_PROBE_INTERVAL;
21883
+ floorPageBudgetCountdown = FLOOR_CONGESTION_PROBE_INTERVAL;
21884
+ if (page.hasMore) {
21885
+ throw new Error(
21886
+ `Vercel ${MIN_SUB_WINDOW_MS / 1e3}-second slice holds more than ${FLOOR_SLICE_MAX_PAGES} pages and cannot be drained further`
21720
21887
  );
21721
21888
  }
21722
- spanMs = Math.max(Math.floor(subSpanMs / 2), MIN_SUB_WINDOW_MS);
21723
- continue;
21724
21889
  }
21725
21890
  for (const event of page.events) {
21726
21891
  if (!seenEventIds.has(event.eventId)) {
@@ -21731,10 +21896,19 @@ async function drainVercelTrafficEvents(options) {
21731
21896
  cursorMs = subEndMs;
21732
21897
  const remainingMs = endMs - cursorMs;
21733
21898
  if (remainingMs > 0) {
21734
- spanMs = Math.min(spanMs * 2, remainingMs);
21899
+ if (spanMs === MIN_SUB_WINDOW_MS && floorSpanProbeCountdown > 0) {
21900
+ floorSpanProbeCountdown -= 1;
21901
+ if (floorPageBudgetCountdown > 0) floorPageBudgetCountdown -= 1;
21902
+ spanMs = Math.min(MIN_SUB_WINDOW_MS, remainingMs);
21903
+ } else {
21904
+ spanMs = Math.min(spanMs * 2, remainingMs);
21905
+ if (spanMs > MIN_SUB_WINDOW_MS) {
21906
+ floorPageBudgetCountdown = 0;
21907
+ }
21908
+ }
21735
21909
  }
21736
21910
  }
21737
- return { events, subWindowCount };
21911
+ return { events, subWindowCount, effectiveStartMs, retentionClamped };
21738
21912
  }
21739
21913
 
21740
21914
  // ../api-routes/src/traffic.ts
@@ -21746,6 +21920,7 @@ var DEFAULT_WP_PAGE_SIZE = 500;
21746
21920
  var DEFAULT_WP_MAX_PAGES = 20;
21747
21921
  var DEFAULT_VERCEL_MAX_PAGES = 50;
21748
21922
  var VERCEL_MAX_SUB_WINDOWS = 5e3;
21923
+ var VERCEL_BACKFILL_CHUNK_MS = 60 * 6e4;
21749
21924
  var MAX_TRACKED_EVENT_IDS = 1e3;
21750
21925
  var DEFAULT_BACKFILL_DAYS = 30;
21751
21926
  var MAX_BACKFILL_DAYS = 90;
@@ -21781,6 +21956,11 @@ async function defaultResolveAccessToken(record) {
21781
21956
  "OAuth-mode Cloud Run sync is not yet supported in v1. Provide a service-account key file."
21782
21957
  );
21783
21958
  }
21959
+ function vercelRetentionClampError(requestedStartMs, effectiveStartMs) {
21960
+ return new Error(
21961
+ `Vercel request-logs retention starts at ${new Date(effectiveStartMs).toISOString()}, after requested start ${new Date(requestedStartMs).toISOString()}; refusing to advance because historical traffic would be skipped`
21962
+ );
21963
+ }
21784
21964
  async function runBackfillTask(options) {
21785
21965
  const {
21786
21966
  app,
@@ -21960,8 +22140,12 @@ async function trafficRoutes(app, opts) {
21960
22140
  const { address, family } = check.target;
21961
22141
  return new UndiciAgent({
21962
22142
  connect: {
21963
- lookup: (_hostname, _options, cb) => {
21964
- cb(null, address, family === 6 ? 6 : 4);
22143
+ lookup: (_hostname, options, cb) => {
22144
+ if (options?.all) {
22145
+ cb(null, [{ address, family: family === 6 ? 6 : 4 }]);
22146
+ } else {
22147
+ cb(null, address, family === 6 ? 6 : 4);
22148
+ }
21965
22149
  }
21966
22150
  }
21967
22151
  });
@@ -22207,7 +22391,15 @@ async function trafficRoutes(app, opts) {
22207
22391
  sourceType: TrafficSourceTypes.vercel,
22208
22392
  displayName: fallbackName,
22209
22393
  status: TrafficSourceStatuses.connected,
22210
- lastSyncedAt: null,
22394
+ // Seed lastSyncedAt to NOW so the first sync uses a tight window.
22395
+ // Leaving this null would make the first sync fall back to
22396
+ // DEFAULT_SYNC_WINDOW_MINUTES (30 days) — which exceeds Vercel's
22397
+ // request-logs retention (~14 days), causing the first sync to
22398
+ // throw a retention error and leaving the source permanently
22399
+ // stuck before it ever drained an event. New users opt into
22400
+ // historical recovery via the explicit `traffic backfill` command;
22401
+ // they do not silently inherit a 30-day pull on connect.
22402
+ lastSyncedAt: now,
22211
22403
  lastCursor: null,
22212
22404
  lastError: null,
22213
22405
  archivedAt: null,
@@ -22416,6 +22608,9 @@ async function trafficRoutes(app, opts) {
22416
22608
  pagesPerSubWindow: vercelMaxPages,
22417
22609
  maxSubWindows: VERCEL_MAX_SUB_WINDOWS
22418
22610
  });
22611
+ if (drained.retentionClamped) {
22612
+ throw vercelRetentionClampError(windowStart.getTime(), drained.effectiveStartMs);
22613
+ }
22419
22614
  allEvents = drained.events;
22420
22615
  } catch (e) {
22421
22616
  const msg = e instanceof Error ? e.message : String(e);
@@ -22762,18 +22957,32 @@ async function trafficRoutes(app, opts) {
22762
22957
  const vercelEnvironment = config.environment ?? credential.environment;
22763
22958
  pullErrorPrefix = "Vercel pull failed";
22764
22959
  pullForBackfill = async () => {
22765
- const drained = await drainVercelTrafficEvents({
22766
- pull: pullVercelEvents,
22767
- token: credential.token,
22768
- projectId: vercelProjectId,
22769
- teamId: vercelTeamId,
22770
- environment: vercelEnvironment,
22771
- startDate: windowStart.getTime(),
22772
- endDate: windowEnd.getTime(),
22773
- pagesPerSubWindow: vercelMaxPages,
22774
- maxSubWindows: VERCEL_MAX_SUB_WINDOWS
22775
- });
22776
- return drained.events;
22960
+ const collected = [];
22961
+ const seenEventIds = /* @__PURE__ */ new Set();
22962
+ const backfillEndMs = windowEnd.getTime();
22963
+ for (let chunkStartMs = windowStart.getTime(); chunkStartMs < backfillEndMs; chunkStartMs += VERCEL_BACKFILL_CHUNK_MS) {
22964
+ const chunkEndMs = Math.min(chunkStartMs + VERCEL_BACKFILL_CHUNK_MS, backfillEndMs);
22965
+ const drained = await drainVercelTrafficEvents({
22966
+ pull: pullVercelEvents,
22967
+ token: credential.token,
22968
+ projectId: vercelProjectId,
22969
+ teamId: vercelTeamId,
22970
+ environment: vercelEnvironment,
22971
+ startDate: chunkStartMs,
22972
+ endDate: chunkEndMs,
22973
+ pagesPerSubWindow: BACKFILL_MAX_PAGES,
22974
+ maxSubWindows: VERCEL_MAX_SUB_WINDOWS
22975
+ });
22976
+ if (drained.retentionClamped) {
22977
+ throw vercelRetentionClampError(chunkStartMs, drained.effectiveStartMs);
22978
+ }
22979
+ for (const event of drained.events) {
22980
+ if (seenEventIds.has(event.eventId)) continue;
22981
+ seenEventIds.add(event.eventId);
22982
+ collected.push(event);
22983
+ }
22984
+ }
22985
+ return collected;
22777
22986
  };
22778
22987
  }
22779
22988
  const startedAt = windowEnd.toISOString();
@@ -22859,6 +23068,43 @@ async function trafficRoutes(app, opts) {
22859
23068
  } : null
22860
23069
  };
22861
23070
  }
23071
+ app.post("/projects/:name/traffic/sources/:id/reset", async (request) => {
23072
+ const project = resolveProject(app.db, request.params.name);
23073
+ const parsed = trafficResetRequestSchema.safeParse(request.body ?? {});
23074
+ if (!parsed.success) {
23075
+ throw validationError(
23076
+ "`advanceToNow` must be `true`. There is no implicit reset."
23077
+ );
23078
+ }
23079
+ const sourceRow = app.db.select().from(trafficSources).where(and19(eq24(trafficSources.projectId, project.id), eq24(trafficSources.id, request.params.id))).get();
23080
+ if (!sourceRow) {
23081
+ throw notFound("traffic source", request.params.id);
23082
+ }
23083
+ if (sourceRow.status === TrafficSourceStatuses.archived) {
23084
+ throw validationError(
23085
+ `Traffic source "${sourceRow.id}" is archived. Re-connect via "canonry traffic connect ..." to start tracking it again.`
23086
+ );
23087
+ }
23088
+ const now = (/* @__PURE__ */ new Date()).toISOString();
23089
+ let updatedRow;
23090
+ app.db.transaction((tx) => {
23091
+ tx.update(trafficSources).set({
23092
+ lastSyncedAt: now,
23093
+ status: TrafficSourceStatuses.connected,
23094
+ lastError: null,
23095
+ updatedAt: now
23096
+ }).where(eq24(trafficSources.id, sourceRow.id)).run();
23097
+ writeAuditLog(tx, auditFromRequest(request, {
23098
+ projectId: project.id,
23099
+ actor: "api",
23100
+ action: "traffic.source.reset",
23101
+ entityType: "traffic_source",
23102
+ entityId: sourceRow.id
23103
+ }));
23104
+ updatedRow = tx.select().from(trafficSources).where(eq24(trafficSources.id, sourceRow.id)).get();
23105
+ });
23106
+ return buildSourceDetail(project.id, updatedRow, new Date(Date.now() - 24 * 60 * 6e4).toISOString());
23107
+ });
22862
23108
  app.get("/projects/:name/traffic/sources", async (request) => {
22863
23109
  const project = resolveProject(app.db, request.params.name);
22864
23110
  const rows = app.db.select().from(trafficSources).where(eq24(trafficSources.projectId, project.id)).orderBy(desc13(trafficSources.createdAt)).all();
@@ -27694,166 +27940,6 @@ function buildRunCompletedProps(input) {
27694
27940
  return props;
27695
27941
  }
27696
27942
 
27697
- // src/citation-utils.ts
27698
- function domainMatches(domain, canonicalDomain) {
27699
- const normalized = normalizeProjectDomain(canonicalDomain);
27700
- const d = normalizeProjectDomain(domain);
27701
- return d === normalized || d.endsWith(`.${normalized}`);
27702
- }
27703
- function determineCitationState(normalized, domains) {
27704
- for (const canonicalDomain of domains) {
27705
- const bareDomain = normalizeProjectDomain(canonicalDomain);
27706
- if (normalized.citedDomains.some((d) => domainMatches(d, bareDomain))) {
27707
- return "cited";
27708
- }
27709
- const lowerDomain = bareDomain.toLowerCase();
27710
- for (const source of normalized.groundingSources) {
27711
- try {
27712
- const uri = source.uri.toLowerCase();
27713
- if (lowerDomain.includes(".") && uri.includes(lowerDomain)) {
27714
- return "cited";
27715
- }
27716
- } catch {
27717
- }
27718
- if (source.title) {
27719
- const titleLower = source.title.toLowerCase().replace(/^www\./, "");
27720
- if (titleLower === lowerDomain || titleLower.endsWith(`.${lowerDomain}`)) {
27721
- return "cited";
27722
- }
27723
- }
27724
- }
27725
- }
27726
- return "not-cited";
27727
- }
27728
- function computeCompetitorOverlap(normalized, competitorDomains) {
27729
- const overlapSet = /* @__PURE__ */ new Set();
27730
- for (const d of normalized.citedDomains) {
27731
- for (const cd of competitorDomains) {
27732
- if (domainMatches(d, cd)) {
27733
- overlapSet.add(cd);
27734
- }
27735
- }
27736
- }
27737
- for (const source of normalized.groundingSources) {
27738
- const uri = source.uri.toLowerCase();
27739
- for (const cd of competitorDomains) {
27740
- if (uri.includes(cd.toLowerCase())) {
27741
- overlapSet.add(cd);
27742
- }
27743
- }
27744
- }
27745
- if (normalized.answerText) {
27746
- const lowerAnswer = normalized.answerText.toLowerCase();
27747
- for (const cd of competitorDomains) {
27748
- if (lowerAnswer.includes(cd.toLowerCase())) {
27749
- overlapSet.add(cd);
27750
- }
27751
- const brand = brandLabelFromDomain(cd);
27752
- if (brand.length >= 4 && new RegExp(`\\b${escapeRegExp5(brand)}\\b`, "i").test(lowerAnswer)) {
27753
- overlapSet.add(cd);
27754
- }
27755
- }
27756
- }
27757
- return [...overlapSet];
27758
- }
27759
- function escapeRegExp5(value) {
27760
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27761
- }
27762
- function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains, ownBrandNames = []) {
27763
- if (!answerText || answerText.length < 20) return [];
27764
- const ownBrandKeys = new Set(
27765
- ownDomains.flatMap((domain) => collectBrandKeysFromDomain(domain))
27766
- );
27767
- for (const name of ownBrandNames) {
27768
- const key = brandKeyFromText(name);
27769
- if (key.length >= 4) ownBrandKeys.add(key);
27770
- }
27771
- const knownCompetitorKeys = new Set(
27772
- [...citedDomains, ...competitorDomains].flatMap((domain) => collectBrandKeysFromDomain(domain)).filter((key) => !ownBrandKeys.has(key))
27773
- );
27774
- if (knownCompetitorKeys.size === 0) return [];
27775
- const candidatePatterns = [
27776
- /^\s*(?:[-*]|\d+\.)\s+(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?\s*[:\u2014\u2013-]/gm,
27777
- /\*\*([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\*\*/g,
27778
- /^#{1,4}\s+(?:\d+\.\s+)?(?:\*\*)?([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50}?)(?:\*\*)?$/gm,
27779
- /\[([A-Z0-9][A-Za-z0-9][\w\s.&',/()-]{1,50})\]\(https?:\/\/[^\s)]+\)/g
27780
- ];
27781
- const genericKeys = /* @__PURE__ */ new Set([
27782
- "additional",
27783
- "best",
27784
- "benefits",
27785
- "bottomline",
27786
- "comparison",
27787
- "conclusion",
27788
- "directorylisting",
27789
- "example",
27790
- "expertise",
27791
- "features",
27792
- "finalthoughts",
27793
- "howitworks",
27794
- "important",
27795
- "keybenefits",
27796
- "keyfeatures",
27797
- "major",
27798
- "note",
27799
- "notable",
27800
- "option",
27801
- "other",
27802
- "overview",
27803
- "pricing",
27804
- "pros",
27805
- "reviews",
27806
- "step",
27807
- "summary",
27808
- "top",
27809
- "verdict",
27810
- "whattolookfor",
27811
- "whyitmatters",
27812
- "whyitstandsout",
27813
- "whywechoseit"
27814
- ]);
27815
- const seen = /* @__PURE__ */ new Map();
27816
- for (const pattern of candidatePatterns) {
27817
- let match;
27818
- while ((match = pattern.exec(answerText)) !== null) {
27819
- const candidate = cleanCandidateName(match[1] ?? "");
27820
- const candidateKey = brandKeyFromText(candidate);
27821
- if (!candidateKey) continue;
27822
- if (genericKeys.has(candidateKey)) continue;
27823
- if (candidate.split(/\s+/).length > 6) continue;
27824
- if (matchesBrandKey(candidateKey, ownBrandKeys)) continue;
27825
- if (!matchesBrandKey(candidateKey, knownCompetitorKeys)) continue;
27826
- if (!seen.has(candidateKey)) seen.set(candidateKey, candidate);
27827
- }
27828
- }
27829
- return [...seen.values()].slice(0, 10);
27830
- }
27831
- function cleanCandidateName(candidate) {
27832
- return candidate.replace(/^[\s"'`]+|[\s"'`.,:;!?]+$/g, "").replace(/\s+/g, " ").trim();
27833
- }
27834
- function collectBrandKeysFromDomain(domain) {
27835
- const reg = registrableDomain(domain);
27836
- if (!reg) {
27837
- const hostname = normalizeProjectDomain(domain).split("/")[0] ?? "";
27838
- const fallback = hostname.replace(/[^a-z0-9]/gi, "").toLowerCase();
27839
- return fallback.length >= 4 ? [fallback] : [];
27840
- }
27841
- const keys = /* @__PURE__ */ new Set();
27842
- const fullKey = reg.replace(/[^a-z0-9]/gi, "").toLowerCase();
27843
- if (fullKey.length >= 4) keys.add(fullKey);
27844
- const brand = brandLabelFromDomain(reg).replace(/[^a-z0-9]/gi, "").toLowerCase();
27845
- if (brand.length >= 4) keys.add(brand);
27846
- return [...keys];
27847
- }
27848
- function matchesBrandKey(candidateKey, brandKeys) {
27849
- for (const brandKey of brandKeys) {
27850
- if (candidateKey === brandKey) return true;
27851
- if (candidateKey.startsWith(brandKey) || candidateKey.endsWith(brandKey)) return true;
27852
- if (brandKey.startsWith(candidateKey) || brandKey.endsWith(candidateKey)) return true;
27853
- }
27854
- return false;
27855
- }
27856
-
27857
27943
  // src/job-runner.ts
27858
27944
  var log = createLogger("JobRunner");
27859
27945
  var RunCancelledError = class extends Error {
@@ -30063,7 +30149,7 @@ function readStoredGroundingSources(rawResponse) {
30063
30149
  return result;
30064
30150
  }
30065
30151
  async function backfillInsightsCommand(project, opts) {
30066
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-NKAEHHJ5.js");
30152
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-NY3MAVPB.js");
30067
30153
  const config = loadConfig();
30068
30154
  const db = createClient(config.database);
30069
30155
  migrate(db);
@@ -33077,7 +33163,7 @@ function buildFallbackRecommendedActions(audit) {
33077
33163
  function citesTargetDomain(citedDomains, groundingSources, targetDomain) {
33078
33164
  const normalizedTarget = extractHostname2(targetDomain);
33079
33165
  for (const domain of citedDomains) {
33080
- if (domainMatches2(domain, normalizedTarget)) {
33166
+ if (domainMatches(domain, normalizedTarget)) {
33081
33167
  return true;
33082
33168
  }
33083
33169
  }
@@ -33098,8 +33184,8 @@ function extractCompetitorsFromResponse(ctx) {
33098
33184
  for (const hint of ctx.manualCompetitors) {
33099
33185
  if (isDomainLike(hint)) {
33100
33186
  const normalizedHint = normalizeDomain3(hint);
33101
- if (domainMatches2(normalizedHint, targetDomain)) continue;
33102
- if (ctx.citedDomains.some((domain) => domainMatches2(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
33187
+ if (domainMatches(normalizedHint, targetDomain)) continue;
33188
+ if (ctx.citedDomains.some((domain) => domainMatches(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
33103
33189
  competitors2.add(normalizedHint);
33104
33190
  }
33105
33191
  continue;
@@ -33169,7 +33255,7 @@ function normalizeDomain3(value) {
33169
33255
  function extractHostname2(value) {
33170
33256
  return normalizeDomain3(value);
33171
33257
  }
33172
- function domainMatches2(candidate, target) {
33258
+ function domainMatches(candidate, target) {
33173
33259
  const normalizedCandidate = normalizeDomain3(candidate);
33174
33260
  const normalizedTarget = normalizeDomain3(target);
33175
33261
  return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
@@ -33465,6 +33551,7 @@ async function createServer(opts) {
33465
33551
  keyUrl: adapter.keyUrl,
33466
33552
  modelHint: `e.g. ${adapter.modelRegistry.defaultModel}`,
33467
33553
  model: registry.get(adapter.name)?.config.model,
33554
+ defaultModel: adapter.modelRegistry.defaultModel,
33468
33555
  configured: !!registry.get(adapter.name),
33469
33556
  quota: registry.get(adapter.name)?.config.quotaPolicy,
33470
33557
  vertexConfigured: adapter.name === "gemini" ? !!opts.config.providers?.gemini?.vertexProject : void 0