@canonry/canonry 7.44.1 → 7.45.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 (35) hide show
  1. package/assets/agent-workspace/skills/canonry/references/canonry-cli.md +10 -0
  2. package/assets/assets/{AuditHistoryPanel-CvANw3ds.js → AuditHistoryPanel-CZOskgSK.js} +1 -1
  3. package/assets/assets/{BacklinksPage-C6x9ELjk.js → BacklinksPage-CgyR9Ydh.js} +1 -1
  4. package/assets/assets/{HistoryPage-r36HX1cC.js → HistoryPage-luN-L4CJ.js} +1 -1
  5. package/assets/assets/{MeasurementPropertyPage-ComoCzR4.js → MeasurementPropertyPage-B0vFWHFx.js} +1 -1
  6. package/assets/assets/{ProjectPage-CIeU-buk.js → ProjectPage-BkLOWkz9.js} +1 -1
  7. package/assets/assets/{RunsPage-DI6eJATZ.js → RunsPage-CeW09PIZ.js} +1 -1
  8. package/assets/assets/{SettingsPage-Bw7GdXxs.js → SettingsPage-C4jww6eS.js} +1 -1
  9. package/assets/assets/{SiteHealthSection-C-JaNKpS.js → SiteHealthSection-DXxY03vO.js} +3 -3
  10. package/assets/assets/{TrafficPage-Cnsp276v.js → TrafficPage-DvUYZXne.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-CBIaJk7e.js → TrafficSourceDetailPage-DG-6Vn9Z.js} +1 -1
  12. package/assets/assets/{extract-error-message-C_Owa0n9.js → extract-error-message-1VWodBea.js} +1 -1
  13. package/assets/assets/{index-ZS9sSTMS.js → index-rBPSQ6B7.js} +3 -3
  14. package/assets/assets/{react-sigma_core.esm.min-ugyh-ooq.js → react-sigma_core.esm.min-Cm1_Mhdd.js} +1 -1
  15. package/assets/assets/{v2-overview-adapter-BLSrVQ7K.js → v2-overview-adapter-DSOkIdKV.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-GZ2KIONF.js → chunk-3MGCNSUL.js} +21 -0
  18. package/dist/{chunk-5LY7EG4U.js → chunk-3T3JQB2J.js} +2 -2
  19. package/dist/{chunk-G47XJVAS.js → chunk-6HUFI2G5.js} +80 -12
  20. package/dist/{chunk-MPWBFKGK.js → chunk-73YOKKHS.js} +1 -1
  21. package/dist/{chunk-LK7O7RM6.js → chunk-A2RC2Y24.js} +224 -38
  22. package/dist/{chunk-JVBI6VBT.js → chunk-D42JVABX.js} +2 -2
  23. package/dist/{chunk-45Z5RJZW.js → chunk-H4YKQAY7.js} +3 -3
  24. package/dist/{chunk-3JVF3ULD.js → chunk-JAQNHQGR.js} +1 -1
  25. package/dist/{chunk-SW54HXSZ.js → chunk-K3AEOEB7.js} +3 -3
  26. package/dist/{chunk-57MGI2UT.js → chunk-TH7WZBUX.js} +11 -9
  27. package/dist/{cli-catalog-PNJEQV5H.js → cli-catalog-6ABJHAAK.js} +3 -3
  28. package/dist/cli-commands-OLFYNI6A.js +15 -0
  29. package/dist/cli.js +10 -10
  30. package/dist/{demo-server-WRHR25HF.js → demo-server-ZNK2UDRJ.js} +4 -4
  31. package/dist/index.js +6 -6
  32. package/dist/{intelligence-service-QXXRUB2G.js → intelligence-service-MDXUBZPJ.js} +2 -2
  33. package/dist/mcp.js +4 -4
  34. package/package.json +9 -9
  35. package/dist/cli-commands-D3JIQ2ID.js +0 -15
@@ -21,19 +21,19 @@ import {
21
21
  saveConfig,
22
22
  saveConfigPatch,
23
23
  sentimentInstallReadiness
24
- } from "./chunk-5LY7EG4U.js";
24
+ } from "./chunk-3T3JQB2J.js";
25
25
  import {
26
26
  computeFactorAverages,
27
27
  deleteSiteCrawlGraphLayout,
28
28
  persistSiteCrawlGraphLayout,
29
29
  prepareSiteCrawlGraphLayout,
30
30
  recordedSharePct
31
- } from "./chunk-JVBI6VBT.js";
31
+ } from "./chunk-D42JVABX.js";
32
32
  import {
33
33
  CanonryMcpToolNames,
34
34
  PACKAGE_VERSION,
35
35
  canonryMcpTools
36
- } from "./chunk-MPWBFKGK.js";
36
+ } from "./chunk-73YOKKHS.js";
37
37
  import {
38
38
  CliError
39
39
  } from "./chunk-HGMYEDAQ.js";
@@ -240,7 +240,7 @@ import {
240
240
  webhookResponseReason,
241
241
  webhookTargetRefusalReason,
242
242
  writeAuditLog
243
- } from "./chunk-G47XJVAS.js";
243
+ } from "./chunk-6HUFI2G5.js";
244
244
  import {
245
245
  AGENT_MEMORY_VALUE_MAX_BYTES,
246
246
  AGENT_NONE,
@@ -255,6 +255,7 @@ import {
255
255
  AppError,
256
256
  BacklinkSources,
257
257
  CANONRY_NPM_PACKAGE_URL,
258
+ CHECK_RUNTIME_ERROR_SUFFIX,
258
259
  CITED_URL_CAPTURE_VERSION,
259
260
  CcReleaseSyncStatuses,
260
261
  CheckNotificationPolicies,
@@ -487,7 +488,7 @@ import {
487
488
  winnabilityClassLabel,
488
489
  withRetry,
489
490
  withSkippedProviders
490
- } from "./chunk-GZ2KIONF.js";
491
+ } from "./chunk-3MGCNSUL.js";
491
492
 
492
493
  // src/runtime-context.ts
493
494
  function cliRuntimeContext(env = process.env, stdio = process) {
@@ -8368,7 +8369,7 @@ function nonBlank(value) {
8368
8369
  return trimmed ? trimmed : void 0;
8369
8370
  }
8370
8371
  function resolveBuildCommit(env = process.env) {
8371
- const embedded = true ? "e030cb7a90ea9f96ff105df0274d1490664614f1" : void 0;
8372
+ const embedded = true ? "1c908111fca7364bee14ee96e5018bc3a5ef740b" : void 0;
8372
8373
  return nonBlank(embedded) ?? nonBlank(env.CANONRY_COMMIT);
8373
8374
  }
8374
8375
  function resolveInstanceIdentity(env = process.env) {
@@ -18945,6 +18946,39 @@ function isNews(now, was) {
18945
18946
  return was === void 0 || now.rank > was.rank || now.rank === was.rank && now.code !== was.code;
18946
18947
  }
18947
18948
  var breachCodes = (breaches) => [...new Set([...breaches].map((b) => b.code))];
18949
+ var URGENT_HEALTH_EVENTS = /* @__PURE__ */ new Set(["health.urgent", "health.urgent-resolved"]);
18950
+ function urgentReceiptGroups(subscribers, receipts) {
18951
+ const groups = /* @__PURE__ */ new Map();
18952
+ for (const subscriber of subscribers) {
18953
+ const { url } = subscriber.config;
18954
+ const receipt = receipts?.[subscriber.id];
18955
+ const notified = receipt?.url === url ? receipt.notified : null;
18956
+ const group = groups.get(notified);
18957
+ if (group) group.subscribers.push(subscriber);
18958
+ else groups.set(notified, { notified, subscribers: [subscriber] });
18959
+ }
18960
+ return [...groups.values()];
18961
+ }
18962
+ function recordUrgentReceipts(receipts, subscribers, notified) {
18963
+ for (const subscriber of subscribers) receipts[subscriber.id] = { url: subscriber.config.url, notified };
18964
+ }
18965
+ function doctorUrgentSummary(receipts) {
18966
+ const breaches = Object.values(receipts).flatMap((receipt) => receipt.notified ? parseBreaches(receipt.notified) : []);
18967
+ const summary = formatUrgentBreaches(indexBreaches(breaches, "id").values());
18968
+ return summary || null;
18969
+ }
18970
+ function formatUrgentBreaches(breaches) {
18971
+ return formatBreaches([...breaches].sort((a, b) => b.rank - a.rank || compareText(a.id ?? a.code, b.id ?? b.code) || compareText(a.code, b.code)));
18972
+ }
18973
+ function siteUrgentSummary(receipts) {
18974
+ return Object.values(receipts).find((receipt) => receipt.notified !== null)?.notified ?? null;
18975
+ }
18976
+ var HEALTH_URGENT_RESOLVED_CODE = "health.urgent.resolved";
18977
+ function receivesHealthEvent(listed, event) {
18978
+ const events = listed ?? [];
18979
+ if (URGENT_HEALTH_EVENTS.has(event)) return events.includes(event);
18980
+ return events.includes(event) || !events.some((e) => URGENT_HEALTH_EVENTS.has(e));
18981
+ }
18948
18982
  var SITE_LIVENESS_FAILURES_TO_PAGE = 2;
18949
18983
  var SITE_LIVENESS_MIN_PASS_GAP_MS = 4 * 6e4;
18950
18984
  var SITE_LIVENESS_MAX_PASS_GAP_MS = 45 * 6e4;
@@ -19041,9 +19075,19 @@ var Notifier = class {
19041
19075
  * the next pass, because a quiet clearing pass would otherwise never
19042
19076
  * surface it.
19043
19077
  *
19044
- * Returns the event emitted, or null when nothing was sent. The return value
19045
- * is what tests assert against, so "stayed quiet" is observable rather than
19046
- * inferred from the absence of a webhook.
19078
+ * Urgent problems are tracked beside all of this, not inside it. A check
19079
+ * marks its own output `urgent`; when one starts, `health.urgent` goes to
19080
+ * the webhooks that list it, and `health.urgent-resolved` when the ones it
19081
+ * reported have cleared and none is known to be open. They keep their own
19082
+ * record of what a subscriber was told (`urgent_notified`) because they go
19083
+ * to different webhooks than the alert above and are accepted
19084
+ * independently: a refused urgent alert is owed again whether or not the
19085
+ * general one was delivered. The record exists only while a webhook
19086
+ * subscribes to urgent events.
19087
+ *
19088
+ * Returns the general event emitted, or null when none was sent. The return
19089
+ * value is what tests assert against, so "stayed quiet" is observable rather
19090
+ * than inferred from the absence of a webhook.
19047
19091
  */
19048
19092
  async onHealthChecked(projectId, report) {
19049
19093
  const project = this.db.select().from(projects).where(eq21(projects.id, projectId)).get();
@@ -19132,7 +19176,46 @@ var Notifier = class {
19132
19176
  event = "health.degraded";
19133
19177
  }
19134
19178
  const cleared = before === null || noSignal ? null : goneFrom(new Map([...before, ...notified ?? []]));
19135
- const notifs = event ? this.db.select().from(notifications).where(eq21(notifications.projectId, projectId)).all().filter((n) => n.enabled) : [];
19179
+ const urgentItems = [];
19180
+ for (const c of failing) {
19181
+ if (c.urgent !== true) continue;
19182
+ urgentItems.push({ id: c.id, code: c.code, rank: rank(c.status), status: c.status, summary: c.summary, remediation: c.remediation ?? null });
19183
+ }
19184
+ for (const c of healthChecks) {
19185
+ const supersededCode = c.details?.supersededCode;
19186
+ if (c.status !== CheckStatuses.skipped || c.urgent !== true || typeof supersededCode !== "string") continue;
19187
+ const causeIds = Array.isArray(c.details?.supersededBy) ? c.details.supersededBy : [];
19188
+ const cause = failing.find((f) => causeIds.includes(f.id));
19189
+ const status2 = c.details?.supersededStatus === "warn" ? "warn" : "fail";
19190
+ urgentItems.push({ id: c.id, code: supersededCode, rank: rank(status2), status: status2, summary: cause?.summary ?? c.summary, remediation: cause?.remediation ?? null });
19191
+ }
19192
+ urgentItems.sort((a, b) => b.rank - a.rank);
19193
+ const urgentById = new Map(urgentItems.map((item) => [item.id, item]));
19194
+ const generalEvent = event;
19195
+ const urgentRecord = previous?.urgentNotified ?? null;
19196
+ const enabled = generalEvent || urgentItems.length > 0 || urgentRecord ? this.db.select().from(notifications).where(eq21(notifications.projectId, projectId)).all().filter((n) => n.enabled) : [];
19197
+ const listedEvents = (notif) => notif.config.events;
19198
+ const hasUrl = (notif) => Boolean(notif.config.url);
19199
+ const urgentSubscribers = enabled.filter((n) => hasUrl(n) && (listedEvents(n) ?? []).some((e) => URGENT_HEALTH_EVENTS.has(e)));
19200
+ const conclusive = new Set(graded.filter((c) => !c.code.endsWith(CHECK_RUNTIME_ERROR_SUFFIX)).map((c) => c.id));
19201
+ const urgentReceipts = {};
19202
+ const recordedReceipts = previous?.urgentReceipts;
19203
+ const validReceipts = recordedReceipts && doctorUrgentSummary(recordedReceipts) === urgentRecord ? recordedReceipts : null;
19204
+ const urgentGroups = urgentReceiptGroups(urgentSubscribers, validReceipts).map((group) => {
19205
+ const urgentTold = group.notified ? parseBreaches(group.notified).filter((b) => b.id !== null) : [];
19206
+ const urgentCleared = urgentTold.filter((b) => conclusive.has(b.id) && !urgentById.has(b.id));
19207
+ const urgentUnknown = urgentTold.filter((b) => !conclusive.has(b.id) && !urgentById.has(b.id));
19208
+ const urgentOpen = urgentTold.filter((b) => urgentById.has(b.id) || hidden.has(b.id));
19209
+ const toldIds = new Set(urgentTold.map((b) => b.id));
19210
+ const urgentNew = urgentItems.filter((item) => !toldIds.has(item.id));
19211
+ const urgentEvent = urgentNew.length > 0 ? "health.urgent" : urgentCleared.length > 0 && urgentOpen.length === 0 ? "health.urgent-resolved" : null;
19212
+ const urgentNotifs = urgentEvent ? group.subscribers.filter((n) => receivesHealthEvent(listedEvents(n), urgentEvent)) : [];
19213
+ const urgentSettled = urgentEvent === "health.urgent-resolved" ? [] : [...urgentUnknown, ...urgentItems];
19214
+ const urgentBefore = urgentEvent === null ? [...urgentUnknown, ...urgentItems.filter((item) => toldIds.has(item.id))] : urgentNotifs.length > 0 ? urgentTold : urgentSettled;
19215
+ recordUrgentReceipts(urgentReceipts, group.subscribers, formatUrgentBreaches(urgentBefore) || null);
19216
+ return { ...group, urgentTold, urgentCleared, urgentNew, urgentEvent, urgentNotifs, urgentSettled };
19217
+ });
19218
+ const notifs = generalEvent ? enabled.filter((n) => receivesHealthEvent(listedEvents(n), generalEvent)) : [];
19136
19219
  const willAttempt = notifs.some((n) => Boolean(n.config.url));
19137
19220
  const triggerIds = new Set([
19138
19221
  ...newBreaches ?? failingNow.filter((b) => noSignal || b.id === worst?.id),
@@ -19149,6 +19232,8 @@ var Notifier = class {
19149
19232
  failingSignature,
19150
19233
  failingChecks: formatBreaches(failingNow),
19151
19234
  pendingSignature: pendingNow === null ? null : formatBreaches(pendingNow),
19235
+ urgentNotified: doctorUrgentSummary(urgentReceipts),
19236
+ urgentReceipts,
19152
19237
  // A delivered set an older release has written past is no longer what
19153
19238
  // the operator was last told.
19154
19239
  ...idsKnown ? {} : { notifiedSignature: null }
@@ -19158,6 +19243,60 @@ var Notifier = class {
19158
19243
  } else {
19159
19244
  this.db.update(doctorHealthState).set(observation).where(eq21(doctorHealthState.projectId, projectId)).run();
19160
19245
  }
19246
+ for (const group of urgentGroups) {
19247
+ const { urgentEvent, urgentTold, urgentNew, urgentCleared, urgentNotifs, urgentSettled } = group;
19248
+ if (!urgentEvent) continue;
19249
+ const toldRank = urgentTold.reduce((worstRank, b) => Math.max(worstRank, b.rank), 0);
19250
+ const previousUrgent = toldRank === 0 ? "ok" : toldRank === 2 ? "fail" : "warn";
19251
+ const lead = urgentNew[0];
19252
+ const onlyCleared = urgentCleared.length === 1 ? graded.find((c) => c.id === urgentCleared[0].id) : void 0;
19253
+ const resolvedSummary = onlyCleared ? onlyCleared.status === "ok" ? onlyCleared.summary : `No longer urgent. ${onlyCleared.summary}` : `${urgentCleared.length} urgent problems are resolved.`;
19254
+ const urgentHealth = urgentEvent === "health.urgent" && lead ? {
19255
+ status: lead.status,
19256
+ code: lead.code,
19257
+ summary: lead.summary,
19258
+ remediation: lead.remediation,
19259
+ checkedAt: now,
19260
+ previousStatus: previousUrgent,
19261
+ failing: urgentItems.map((item) => ({ id: item.id, status: item.status, code: item.code, summary: item.summary })),
19262
+ added: breachCodes(urgentNew),
19263
+ ...urgentCleared.length > 0 ? { cleared: breachCodes(urgentCleared) } : {}
19264
+ } : {
19265
+ status: "ok",
19266
+ code: HEALTH_URGENT_RESOLVED_CODE,
19267
+ summary: resolvedSummary,
19268
+ remediation: null,
19269
+ checkedAt: now,
19270
+ previousStatus: previousUrgent,
19271
+ failing: [],
19272
+ cleared: breachCodes(urgentCleared)
19273
+ };
19274
+ const urgentPayload = {
19275
+ source: "canonry",
19276
+ event: urgentEvent,
19277
+ project: { name: project.name, canonicalDomain: project.canonicalDomain },
19278
+ health: urgentHealth,
19279
+ dashboardUrl: `${this.serverUrl}/projects/${project.name}`
19280
+ };
19281
+ let accepted = 0;
19282
+ for (const notif of urgentNotifs) {
19283
+ const config = notif.config;
19284
+ if (await this.sendWebhook(config.url, urgentPayload, notif.id, projectId, notif.webhookSecret ?? null)) accepted += 1;
19285
+ }
19286
+ if (accepted > 0) {
19287
+ recordUrgentReceipts(urgentReceipts, group.subscribers, formatUrgentBreaches(urgentSettled) || null);
19288
+ this.db.update(doctorHealthState).set({ urgentNotified: doctorUrgentSummary(urgentReceipts), urgentReceipts }).where(eq21(doctorHealthState.projectId, projectId)).run();
19289
+ }
19290
+ if (urgentNotifs.length > 0) {
19291
+ log21.info("health.urgent-notified", {
19292
+ projectId,
19293
+ event: urgentEvent,
19294
+ codes: urgentEvent === "health.urgent" ? breachCodes(urgentNew) : breachCodes(urgentCleared),
19295
+ subscribers: urgentNotifs.length,
19296
+ delivered: accepted
19297
+ });
19298
+ }
19299
+ }
19161
19300
  if (!event) {
19162
19301
  if (clearedThisPass.length > 0 || easedThisPass.length > 0) {
19163
19302
  log21.info("health.improved", { projectId, status, code, cleared: clearedThisPass, eased: easedThisPass });
@@ -19233,6 +19372,27 @@ var Notifier = class {
19233
19372
  let event = null;
19234
19373
  if (check.status === "fail" && failures >= SITE_LIVENESS_FAILURES_TO_PAGE && !outagePaged) event = "health.degraded";
19235
19374
  if (check.status === "ok" && outagePaged) event = "health.recovered";
19375
+ const confirmed = check.status === "fail" && failures >= SITE_LIVENESS_FAILURES_TO_PAGE;
19376
+ const urgentRecord = previous?.urgentNotifiedAt ?? null;
19377
+ const enabled = event !== null || confirmed || urgentRecord !== null ? this.db.select().from(notifications).where(eq21(notifications.projectId, projectId)).all().filter((n) => n.enabled) : [];
19378
+ const listedEvents = (notif) => notif.config.events;
19379
+ const hasUrl = (notif) => Boolean(notif.config.url);
19380
+ const scoped = enabled.filter((n) => hasUrl(n) && (listedEvents(n) ?? []).some((e) => URGENT_HEALTH_EVENTS.has(e)));
19381
+ const urgentReceipts = {};
19382
+ const recordedReceipts = previous?.urgentReceipts;
19383
+ const validReceipts = recordedReceipts && siteUrgentSummary(recordedReceipts) === urgentRecord ? recordedReceipts : null;
19384
+ const urgentGroups = urgentReceiptGroups(scoped, validReceipts).map((group) => {
19385
+ const urgentEvent = confirmed && group.notified === null ? "health.urgent" : check.status === "ok" && group.notified !== null ? "health.urgent-resolved" : null;
19386
+ const recipients = urgentEvent ? group.subscribers.filter((n) => receivesHealthEvent(listedEvents(n), urgentEvent)) : [];
19387
+ const generalName = urgentEvent === "health.urgent-resolved" ? "health.recovered" : "health.degraded";
19388
+ const generalRecipients = recipients.filter((n) => event === generalName && receivesHealthEvent(listedEvents(n), generalName));
19389
+ const generalIds = new Set(generalRecipients.map((n) => n.id));
19390
+ const urgentNotifs = recipients.filter((n) => !generalIds.has(n.id));
19391
+ const settled = urgentEvent === "health.urgent" ? checkedAt : null;
19392
+ const before = urgentEvent && recipients.length === 0 ? settled : group.notified;
19393
+ recordUrgentReceipts(urgentReceipts, group.subscribers, before);
19394
+ return { ...group, urgentEvent, urgentNotifs, generalIds, settled };
19395
+ });
19236
19396
  const observation = {
19237
19397
  projectId,
19238
19398
  status: check.status,
@@ -19240,43 +19400,69 @@ var Notifier = class {
19240
19400
  summary: check.summary,
19241
19401
  consecutiveFailures: failures,
19242
19402
  checkedAt,
19243
- notifiedAt: check.status === "ok" ? null : previous?.notifiedAt ?? null
19403
+ notifiedAt: check.status === "ok" ? null : previous?.notifiedAt ?? null,
19404
+ urgentNotifiedAt: siteUrgentSummary(urgentReceipts),
19405
+ urgentReceipts
19244
19406
  };
19245
19407
  if (previous === void 0) {
19246
19408
  this.db.insert(siteLivenessState).values(observation).run();
19247
19409
  } else {
19248
19410
  this.db.update(siteLivenessState).set(observation).where(eq21(siteLivenessState.projectId, projectId)).run();
19249
19411
  }
19250
- if (!event) return null;
19251
- const notifs = this.db.select().from(notifications).where(eq21(notifications.projectId, projectId)).all().filter((n) => n.enabled);
19252
- const payload = {
19253
- source: "canonry",
19254
- event,
19255
- project: { name: project.name, canonicalDomain: project.canonicalDomain },
19256
- health: {
19257
- status: check.status,
19258
- code: check.code,
19259
- summary: check.summary,
19260
- remediation: check.remediation ?? null,
19261
- checkedAt,
19262
- // The counter resets on every ok, so a page always follows a site that was
19263
- // answering. Echoing the row the first failed pass wrote would report
19264
- // "fail -> fail" and show the operator no transition at all.
19265
- previousStatus: event === "health.degraded" ? "ok" : "fail",
19266
- failing: event === "health.degraded" ? [{ id: check.id, status: check.status, code: check.code, summary: check.summary }] : []
19267
- },
19268
- dashboardUrl: `${this.serverUrl}/projects/${project.name}`
19412
+ if (!event && urgentGroups.every((group) => group.urgentEvent === null)) return null;
19413
+ const payloadFor = (name) => {
19414
+ const outage = name === "health.degraded" || name === "health.urgent";
19415
+ return {
19416
+ source: "canonry",
19417
+ event: name,
19418
+ project: { name: project.name, canonicalDomain: project.canonicalDomain },
19419
+ health: {
19420
+ status: check.status,
19421
+ code: check.code,
19422
+ summary: check.summary,
19423
+ remediation: check.remediation ?? null,
19424
+ checkedAt,
19425
+ // The counter resets on every ok, so a page always follows a site that was
19426
+ // answering. Echoing the row the first failed pass wrote would report
19427
+ // "fail -> fail" and show the operator no transition at all.
19428
+ previousStatus: outage ? "ok" : "fail",
19429
+ failing: outage ? [{ id: check.id, status: check.status, code: check.code, summary: check.summary }] : []
19430
+ },
19431
+ dashboardUrl: `${this.serverUrl}/projects/${project.name}`
19432
+ };
19269
19433
  };
19270
- let delivered = 0;
19271
- for (const notif of notifs) {
19272
- const config = notif.config;
19273
- if (!config.url) continue;
19274
- if (await this.sendWebhook(config.url, payload, notif.id, projectId, notif.webhookSecret ?? null)) delivered += 1;
19434
+ const generalAccepted = /* @__PURE__ */ new Set();
19435
+ if (event) {
19436
+ const notifs = enabled.filter((n) => hasUrl(n) && receivesHealthEvent(listedEvents(n), event));
19437
+ let delivered = 0;
19438
+ for (const notif of notifs) {
19439
+ const config = notif.config;
19440
+ if (await this.sendWebhook(config.url, payloadFor(event), notif.id, projectId, notif.webhookSecret ?? null)) {
19441
+ delivered += 1;
19442
+ generalAccepted.add(notif.id);
19443
+ }
19444
+ }
19445
+ if (event === "health.degraded" && delivered > 0) {
19446
+ this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq21(siteLivenessState.projectId, projectId)).run();
19447
+ }
19448
+ log21.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
19275
19449
  }
19276
- if (event === "health.degraded" && delivered > 0) {
19277
- this.db.update(siteLivenessState).set({ notifiedAt: checkedAt }).where(eq21(siteLivenessState.projectId, projectId)).run();
19450
+ for (const group of urgentGroups) {
19451
+ const { urgentEvent, urgentNotifs, generalIds } = group;
19452
+ if (!urgentEvent) continue;
19453
+ let accepted = [...generalIds].filter((id) => generalAccepted.has(id)).length;
19454
+ for (const notif of urgentNotifs) {
19455
+ const config = notif.config;
19456
+ if (await this.sendWebhook(config.url, payloadFor(urgentEvent), notif.id, projectId, notif.webhookSecret ?? null)) accepted += 1;
19457
+ }
19458
+ if (accepted > 0) {
19459
+ recordUrgentReceipts(urgentReceipts, group.subscribers, group.settled);
19460
+ this.db.update(siteLivenessState).set({ urgentNotifiedAt: siteUrgentSummary(urgentReceipts), urgentReceipts }).where(eq21(siteLivenessState.projectId, projectId)).run();
19461
+ }
19462
+ if (urgentNotifs.length > 0 || generalIds.size > 0) {
19463
+ log21.info("site-liveness.urgent-notified", { projectId, event: urgentEvent, code: check.code, subscribers: urgentNotifs.length + generalIds.size, delivered: accepted });
19464
+ }
19278
19465
  }
19279
- log21.info("site-liveness.notified", { projectId, event, code: check.code, subscribers: notifs.length, delivered });
19280
19466
  return event;
19281
19467
  }
19282
19468
  /**
@@ -5,7 +5,7 @@ import {
5
5
  siteCrawlGraphNodes,
6
6
  siteCrawlPages,
7
7
  siteCrawlSnapshots
8
- } from "./chunk-G47XJVAS.js";
8
+ } from "./chunk-6HUFI2G5.js";
9
9
  import {
10
10
  SITE_CRAWL_GRAPH_MAX_EDGES,
11
11
  SITE_CRAWL_GRAPH_MAX_NODES,
@@ -15,7 +15,7 @@ import {
15
15
  factorStatusFromDistribution,
16
16
  factorStatusFromScore,
17
17
  roundPreservingTotal
18
- } from "./chunk-GZ2KIONF.js";
18
+ } from "./chunk-3MGCNSUL.js";
19
19
 
20
20
  // src/site-audit-factors.ts
21
21
  function recordedSharePct(factor) {
@@ -4,7 +4,7 @@ import {
4
4
  resolveCliAccess,
5
5
  resolveCliAccessForInvocation,
6
6
  windowOptions
7
- } from "./chunk-3JVF3ULD.js";
7
+ } from "./chunk-JAQNHQGR.js";
8
8
  import {
9
9
  CliError,
10
10
  EXIT_USER_ERROR,
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-HGMYEDAQ.js";
13
13
  import {
14
14
  OperationAccesses
15
- } from "./chunk-GZ2KIONF.js";
15
+ } from "./chunk-3MGCNSUL.js";
16
16
 
17
17
  // src/cli-catalog.ts
18
18
  import { createRequire } from "module";
@@ -109,7 +109,7 @@ async function dispatchRegisteredCommand(args, fallbackFormat, specs) {
109
109
  const accessValue = accessFilterFromArgs(argsWithoutHelp);
110
110
  validateAccessFilter(accessValue);
111
111
  if (fallbackFormat !== "text") {
112
- const { printCliCatalog: printCliCatalog2 } = await import("./cli-catalog-PNJEQV5H.js");
112
+ const { printCliCatalog: printCliCatalog2 } = await import("./cli-catalog-6ABJHAAK.js");
113
113
  const group2 = argsWithoutHelp.find((arg) => !arg.startsWith("--"));
114
114
  printCliCatalog2(specs, fallbackFormat, group2, accessValue);
115
115
  return true;
@@ -5,7 +5,7 @@ import {
5
5
  AppError,
6
6
  parseWindowIn,
7
7
  resolveDateRange
8
- } from "./chunk-GZ2KIONF.js";
8
+ } from "./chunk-3MGCNSUL.js";
9
9
 
10
10
  // src/cli-access.ts
11
11
  var CLI_ACCESS_BY_ID = Object.freeze({
@@ -4,14 +4,14 @@ import {
4
4
  installSkills,
5
5
  loadConfigRaw,
6
6
  saveConfigPatch
7
- } from "./chunk-5LY7EG4U.js";
7
+ } from "./chunk-3T3JQB2J.js";
8
8
  import {
9
9
  PACKAGE_VERSION
10
- } from "./chunk-MPWBFKGK.js";
10
+ } from "./chunk-73YOKKHS.js";
11
11
  import {
12
12
  SKILL_MANIFEST_FILENAME,
13
13
  SkillsClients
14
- } from "./chunk-GZ2KIONF.js";
14
+ } from "./chunk-3MGCNSUL.js";
15
15
 
16
16
  // src/skills-autosync.ts
17
17
  import fs from "fs";
@@ -31,7 +31,7 @@ import {
31
31
  trackFeatureCompleted,
32
32
  trackIntegrationConnection,
33
33
  waitForServerRuntimeStartup
34
- } from "./chunk-LK7O7RM6.js";
34
+ } from "./chunk-A2RC2Y24.js";
35
35
  import {
36
36
  clearDashboardPassword,
37
37
  configExists,
@@ -50,7 +50,7 @@ import {
50
50
  saveConfigPatch,
51
51
  shouldPrintServeSkillsNudge,
52
52
  withBootstrapProviderDefaults
53
- } from "./chunk-5LY7EG4U.js";
53
+ } from "./chunk-3T3JQB2J.js";
54
54
  import {
55
55
  emitJsonl,
56
56
  getBoolean,
@@ -64,7 +64,7 @@ import {
64
64
  resolveCliAccess,
65
65
  stringOption,
66
66
  unknownSubcommand
67
- } from "./chunk-3JVF3ULD.js";
67
+ } from "./chunk-JAQNHQGR.js";
68
68
  import {
69
69
  DEFAULT_DEMO_TRUSTED_PROXIES,
70
70
  trustedProxiesProblem,
@@ -73,7 +73,7 @@ import {
73
73
  import {
74
74
  measurementDraftOperationSchema,
75
75
  runMeasurementDraftAction
76
- } from "./chunk-MPWBFKGK.js";
76
+ } from "./chunk-73YOKKHS.js";
77
77
  import {
78
78
  CANONRY_DISCORD_URL,
79
79
  CANONRY_ISSUES_URL,
@@ -118,7 +118,7 @@ import {
118
118
  rawEventSamples,
119
119
  readMarketCompetitorNames,
120
120
  runs
121
- } from "./chunk-G47XJVAS.js";
121
+ } from "./chunk-6HUFI2G5.js";
122
122
  import {
123
123
  AdsDeliverySnapshotStatuses,
124
124
  AdsHistoricalCampaignRollupStatuses,
@@ -274,7 +274,7 @@ import {
274
274
  visibilityCompareSelectionSchema,
275
275
  visibilityReportRequestSchema,
276
276
  winnabilityClassSchema
277
- } from "./chunk-GZ2KIONF.js";
277
+ } from "./chunk-3MGCNSUL.js";
278
278
 
279
279
  // src/cli-commands/sentiment.ts
280
280
  import "zod";
@@ -582,7 +582,7 @@ function parseDemoTrustedProxies(values) {
582
582
  async function demoCommand(options) {
583
583
  const { host, port } = parseDemoListenOptions(options);
584
584
  const trustProxy = parseDemoTrustedProxies(options.trustProxy);
585
- const { createDemoServer } = await import("./demo-server-WRHR25HF.js");
585
+ const { createDemoServer } = await import("./demo-server-ZNK2UDRJ.js");
586
586
  const app = await createDemoServer({ trustProxy });
587
587
  let stopping = false;
588
588
  const stop = () => {
@@ -996,7 +996,7 @@ No DB writes performed. Re-run without --dry-run to apply.`);
996
996
  }
997
997
  }
998
998
  async function backfillInsightsCommand(project2, opts) {
999
- const { IntelligenceService } = await import("./intelligence-service-QXXRUB2G.js");
999
+ const { IntelligenceService } = await import("./intelligence-service-MDXUBZPJ.js");
1000
1000
  const config = loadConfig();
1001
1001
  const db = createClient(config.database);
1002
1002
  migrate(db);
@@ -13215,6 +13215,8 @@ var EVENT_DESCRIPTIONS = {
13215
13215
  "insight.high": "A high-severity insight was generated",
13216
13216
  "health.degraded": "A scheduled health check found the measurement degraded, daily data stopped arriving, or the website stopped answering (sent when a check starts failing or gets worse, not every pass)",
13217
13217
  "health.recovered": "A degraded health check is passing again, or a website that was paged as down answers again",
13218
+ "health.urgent": "An urgent problem started: the website is down, GA4 or Search Console data is stale, or a sync keeps failing. A webhook that lists this receives only the health events it lists, so it is not sent routine warnings",
13219
+ "health.urgent-resolved": "The urgent problems that were reported have cleared (the website check and the scheduled health pass each send their own)",
13218
13220
  "review.negative": "A new or edited Google review at or below the project's negative-review threshold, 3 stars by default (Business Profile reviews, or the public listing when Google has not enabled review access)",
13219
13221
  "review.rating-dropped": "A location's public Google rating fell (sent for locations without Business Profile review access)"
13220
13222
  };
@@ -19436,7 +19438,7 @@ var SYSTEM_CLI_COMMANDS = [
19436
19438
  options: { access: stringOption() },
19437
19439
  run: async (input) => {
19438
19440
  if (input.positionals.length > 1) throw usageError("Expected at most one command group");
19439
- const [{ printCliCatalog }, { REGISTERED_CLI_COMMANDS: REGISTERED_CLI_COMMANDS2 }] = await Promise.all([import("./cli-catalog-PNJEQV5H.js"), import("./cli-commands-D3JIQ2ID.js")]);
19441
+ const [{ printCliCatalog }, { REGISTERED_CLI_COMMANDS: REGISTERED_CLI_COMMANDS2 }] = await Promise.all([import("./cli-catalog-6ABJHAAK.js"), import("./cli-commands-OLFYNI6A.js")]);
19440
19442
  printCliCatalog(REGISTERED_CLI_COMMANDS2, input.format, input.positionals[0], getString(input.values, "access"));
19441
19443
  }
19442
19444
  },
@@ -3,10 +3,10 @@ import {
3
3
  buildCliCatalog,
4
4
  printCliCatalog,
5
5
  renderTopLevelHelp
6
- } from "./chunk-45Z5RJZW.js";
7
- import "./chunk-3JVF3ULD.js";
6
+ } from "./chunk-H4YKQAY7.js";
7
+ import "./chunk-JAQNHQGR.js";
8
8
  import "./chunk-HGMYEDAQ.js";
9
- import "./chunk-GZ2KIONF.js";
9
+ import "./chunk-3MGCNSUL.js";
10
10
  export {
11
11
  CLI_ROOT_SUMMARIES,
12
12
  buildCliCatalog,
@@ -0,0 +1,15 @@
1
+ import {
2
+ REGISTERED_CLI_COMMANDS
3
+ } from "./chunk-TH7WZBUX.js";
4
+ import "./chunk-A2RC2Y24.js";
5
+ import "./chunk-3T3JQB2J.js";
6
+ import "./chunk-JAQNHQGR.js";
7
+ import "./chunk-RB2F4D3Z.js";
8
+ import "./chunk-D42JVABX.js";
9
+ import "./chunk-73YOKKHS.js";
10
+ import "./chunk-HGMYEDAQ.js";
11
+ import "./chunk-6HUFI2G5.js";
12
+ import "./chunk-3MGCNSUL.js";
13
+ export {
14
+ REGISTERED_CLI_COMMANDS
15
+ };
package/dist/cli.js CHANGED
@@ -2,18 +2,18 @@
2
2
  import {
3
3
  autoSyncSkills,
4
4
  formatAutoSyncNotice
5
- } from "./chunk-SW54HXSZ.js";
5
+ } from "./chunk-K3AEOEB7.js";
6
6
  import {
7
7
  accessFilterFromArgs,
8
8
  dispatchRegisteredCommand,
9
9
  printCliCatalog
10
- } from "./chunk-45Z5RJZW.js";
10
+ } from "./chunk-H4YKQAY7.js";
11
11
  import {
12
12
  REGISTERED_CLI_COMMANDS,
13
13
  buildSetupState,
14
14
  consumePendingServeHandoff,
15
15
  serveCommand
16
- } from "./chunk-57MGI2UT.js";
16
+ } from "./chunk-TH7WZBUX.js";
17
17
  import {
18
18
  checkLatestVersionForCli,
19
19
  cliRuntimeContext,
@@ -27,14 +27,14 @@ import {
27
27
  showFirstRunNotice,
28
28
  trackCliCommandFinished,
29
29
  trackEvent
30
- } from "./chunk-LK7O7RM6.js";
31
- import "./chunk-5LY7EG4U.js";
30
+ } from "./chunk-A2RC2Y24.js";
31
+ import "./chunk-3T3JQB2J.js";
32
32
  import {
33
33
  resolveCliAccess
34
- } from "./chunk-3JVF3ULD.js";
34
+ } from "./chunk-JAQNHQGR.js";
35
35
  import "./chunk-RB2F4D3Z.js";
36
- import "./chunk-JVBI6VBT.js";
37
- import "./chunk-MPWBFKGK.js";
36
+ import "./chunk-D42JVABX.js";
37
+ import "./chunk-73YOKKHS.js";
38
38
  import {
39
39
  CliError,
40
40
  EXIT_SYSTEM_ERROR,
@@ -42,10 +42,10 @@ import {
42
42
  printCliError,
43
43
  usageError
44
44
  } from "./chunk-HGMYEDAQ.js";
45
- import "./chunk-G47XJVAS.js";
45
+ import "./chunk-6HUFI2G5.js";
46
46
  import {
47
47
  OperationAccesses
48
- } from "./chunk-GZ2KIONF.js";
48
+ } from "./chunk-3MGCNSUL.js";
49
49
 
50
50
  // src/cli.ts
51
51
  import { pathToFileURL } from "url";
@@ -5,11 +5,11 @@ import {
5
5
  computeFactorAverages,
6
6
  layoutSiteCrawlGraphInput,
7
7
  persistSiteCrawlGraphLayout
8
- } from "./chunk-JVBI6VBT.js";
8
+ } from "./chunk-D42JVABX.js";
9
9
  import {
10
10
  PACKAGE_VERSION,
11
11
  canonryMcpTools
12
- } from "./chunk-MPWBFKGK.js";
12
+ } from "./chunk-73YOKKHS.js";
13
13
  import "./chunk-HGMYEDAQ.js";
14
14
  import {
15
15
  HISTORY_WINDOW_RUNS,
@@ -86,7 +86,7 @@ import {
86
86
  siteCrawlPages,
87
87
  siteCrawlSnapshots,
88
88
  trafficSources
89
- } from "./chunk-G47XJVAS.js";
89
+ } from "./chunk-6HUFI2G5.js";
90
90
  import {
91
91
  RunKinds,
92
92
  SiteCrawlFetchStates,
@@ -116,7 +116,7 @@ import {
116
116
  roundPreservingTotal,
117
117
  siteAuditPageFactorSchema,
118
118
  wilsonInterval
119
- } from "./chunk-GZ2KIONF.js";
119
+ } from "./chunk-3MGCNSUL.js";
120
120
 
121
121
  // src/demo-server.ts
122
122
  import { dirname, join as join2 } from "path";