@canonry/canonry 4.136.1 → 4.137.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 (29) hide show
  1. package/README.md +2 -1
  2. package/assets/assets/{AuditHistoryPanel-B73VrWNb.js → AuditHistoryPanel-B6GEmlVM.js} +1 -1
  3. package/assets/assets/{BacklinksPage-CLVMg_qx.js → BacklinksPage-Cr3QegNo.js} +1 -1
  4. package/assets/assets/{ChartPrimitives-P81CbvWc.js → ChartPrimitives-rhsGzU1A.js} +1 -1
  5. package/assets/assets/{HistoryPage-IcDqWBYW.js → HistoryPage-B3HrxyPP.js} +1 -1
  6. package/assets/assets/ProjectPage-D3WiL3RI.js +8 -0
  7. package/assets/assets/{RunRow-BTH3-Tml.js → RunRow-MsV1RLPm.js} +1 -1
  8. package/assets/assets/{RunsPage-B0k5wjFN.js → RunsPage-BVlv0j3Y.js} +1 -1
  9. package/assets/assets/{SettingsPage-DGnT3Vdr.js → SettingsPage-Dfd-T4o9.js} +1 -1
  10. package/assets/assets/{TrafficPage-D79PC0bB.js → TrafficPage-Bl1jj73m.js} +1 -1
  11. package/assets/assets/{TrafficSourceDetailPage-BYEVjfFm.js → TrafficSourceDetailPage-CinQcKmY.js} +1 -1
  12. package/assets/assets/{arrow-left-DCdFYJK-.js → arrow-left-CHU6s_Xn.js} +1 -1
  13. package/assets/assets/extract-error-message-BDdyZgyf.js +1 -0
  14. package/assets/assets/index-BNulljlk.js +213 -0
  15. package/assets/assets/{trash-2-CEz2iXBv.js → trash-2-BE5oH1Tj.js} +1 -1
  16. package/assets/index.html +1 -1
  17. package/dist/{chunk-PIMWEIJ3.js → chunk-7RF5EDUO.js} +3141 -400
  18. package/dist/{chunk-YQ2RXMU2.js → chunk-PBTB3FCN.js} +207 -195
  19. package/dist/{chunk-OESGFFUO.js → chunk-UVWI6VPP.js} +1 -1
  20. package/dist/{chunk-YMF35LN5.js → chunk-XEX5CLQW.js} +163 -133
  21. package/dist/cli.js +147 -33
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +4 -4
  24. package/dist/{intelligence-service-BKLR57RQ.js → intelligence-service-JEFHUGVX.js} +2 -2
  25. package/dist/mcp.js +2 -2
  26. package/package.json +9 -9
  27. package/assets/assets/ProjectPage-DTivvVIl.js +0 -8
  28. package/assets/assets/extract-error-message-bM0aUBUI.js +0 -1
  29. package/assets/assets/index-CMuA-uEN.js +0 -210
@@ -42,7 +42,7 @@ import {
42
42
  trafficConnectWordpressRequestSchema,
43
43
  trafficEventKindSchema,
44
44
  trafficSeriesGranularitySchema
45
- } from "./chunk-PIMWEIJ3.js";
45
+ } from "./chunk-7RF5EDUO.js";
46
46
 
47
47
  // src/config.ts
48
48
  import fs from "fs";
@@ -10,7 +10,7 @@ import {
10
10
  loadConfig,
11
11
  loadConfigRaw,
12
12
  saveConfigPatch
13
- } from "./chunk-OESGFFUO.js";
13
+ } from "./chunk-UVWI6VPP.js";
14
14
  import {
15
15
  CC_CACHE_DIR,
16
16
  DUCKDB_SPEC,
@@ -119,7 +119,7 @@ import {
119
119
  siteAuditPages,
120
120
  siteAuditSnapshots,
121
121
  usageCounters
122
- } from "./chunk-YQ2RXMU2.js";
122
+ } from "./chunk-PBTB3FCN.js";
123
123
  import {
124
124
  AGENT_MEMORY_VALUE_MAX_BYTES,
125
125
  AGENT_PROVIDER_IDS,
@@ -135,6 +135,7 @@ import {
135
135
  LLM_CAPABILITIES,
136
136
  LlmCapabilities,
137
137
  MemorySources,
138
+ ONBOARDING_FLOW_VERSION,
138
139
  ProviderNames,
139
140
  ResearchQueryStatuses,
140
141
  ResearchRunStatuses,
@@ -159,6 +160,7 @@ import {
159
160
  agentMemoryUpsertRequestSchema,
160
161
  authInvalid,
161
162
  authRequired,
163
+ bucketOnboardingCount,
162
164
  buildRunErrorFromMessages,
163
165
  classifySkillFile,
164
166
  coerceSkillManifest,
@@ -173,6 +175,8 @@ import {
173
175
  factorStatusFromScore,
174
176
  formatIsoDateInTimeZone,
175
177
  frameAncestorsHeaderValue,
178
+ hostMatchesAnyDomain,
179
+ hostMatchesDomain,
176
180
  hostOf,
177
181
  isAgentProviderId,
178
182
  isBrowserProvider,
@@ -185,16 +189,19 @@ import {
185
189
  notFound,
186
190
  parseOriginList,
187
191
  providerError,
192
+ registrableDomain,
188
193
  resolveSnapshotRequestQueries,
189
194
  serializeForInlineScript,
190
195
  serializeRunError,
191
196
  skillsClientSchema,
192
197
  splitList,
193
198
  startOfDayHourInTimeZone,
199
+ textContainsBrandAlias,
200
+ textContainsDomain,
194
201
  validationError,
195
202
  winnabilityClassLabel,
196
203
  withRetry
197
- } from "./chunk-PIMWEIJ3.js";
204
+ } from "./chunk-7RF5EDUO.js";
198
205
 
199
206
  // src/telemetry.ts
200
207
  import crypto from "crypto";
@@ -209,8 +216,42 @@ var ANON_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]
209
216
  function shouldDropTelemetryEvent(event, properties) {
210
217
  return isGhostTelemetryEvent(event, properties);
211
218
  }
219
+ function bucketCliCommandDuration(durationMs) {
220
+ const duration = Number.isFinite(durationMs) ? Math.max(0, durationMs) : 0;
221
+ if (duration < 1e3) return "under_1s";
222
+ if (duration < 1e4) return "1s_to_10s";
223
+ if (duration < 6e4) return "10s_to_1m";
224
+ if (duration < 3e5) return "1m_to_5m";
225
+ if (duration < 18e5) return "5m_to_30m";
226
+ return "30m_or_more";
227
+ }
228
+ function trackCliCommandFinished(input) {
229
+ trackEvent(
230
+ "cli.command.finished",
231
+ {
232
+ command: input.command,
233
+ success: input.success,
234
+ duration_bucket: bucketCliCommandDuration(input.durationMs),
235
+ ...input.setupState ? { setup_state: input.setupState } : {}
236
+ },
237
+ input.errorCode ? { errorCode: input.errorCode } : void 0
238
+ );
239
+ }
212
240
  var SESSION_ID = crypto.randomUUID();
213
241
  var CURRENT_SOURCE = "cli";
242
+ var TELEMETRY_SOURCE_VALUES = /* @__PURE__ */ new Set([
243
+ "cli",
244
+ "cli-server",
245
+ "api",
246
+ "mcp-server",
247
+ "wp-plugin",
248
+ "dashboard",
249
+ "agent-runtime"
250
+ ]);
251
+ function envSourceOverride() {
252
+ const raw = process.env.CANONRY_TELEMETRY_SOURCE?.trim();
253
+ return raw && TELEMETRY_SOURCE_VALUES.has(raw) ? raw : void 0;
254
+ }
214
255
  function setTelemetrySource(source) {
215
256
  CURRENT_SOURCE = source;
216
257
  }
@@ -326,9 +367,10 @@ function trackEvent(event, properties, options) {
326
367
  const anonymousId = getOrCreateAnonymousId();
327
368
  if (!anonymousId) return;
328
369
  const payload = {
370
+ eventId: options?.eventId ?? crypto.randomUUID(),
329
371
  anonymousId,
330
372
  sessionId: SESSION_ID,
331
- source: options?.source ?? CURRENT_SOURCE,
373
+ source: options?.source ?? envSourceOverride() ?? CURRENT_SOURCE,
332
374
  event,
333
375
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
334
376
  version: VERSION,
@@ -342,13 +384,17 @@ function trackEvent(event, properties, options) {
342
384
  const controller = new AbortController();
343
385
  const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
344
386
  timeout.unref();
345
- fetch(TELEMETRY_ENDPOINT, {
346
- method: "POST",
347
- headers: { "Content-Type": "application/json" },
348
- body: JSON.stringify(payload),
349
- signal: controller.signal
350
- }).catch(() => {
351
- }).finally(() => clearTimeout(timeout));
387
+ try {
388
+ void fetch(TELEMETRY_ENDPOINT, {
389
+ method: "POST",
390
+ headers: { "Content-Type": "application/json" },
391
+ body: JSON.stringify(payload),
392
+ signal: controller.signal
393
+ }).catch(() => {
394
+ }).finally(() => clearTimeout(timeout));
395
+ } catch {
396
+ clearTimeout(timeout);
397
+ }
352
398
  }
353
399
 
354
400
  // src/update-check.ts
@@ -728,17 +774,15 @@ function extractCitedDomainsFromSources(groundingSources) {
728
774
  return [...domains];
729
775
  }
730
776
  function extractDomainFromTitle(title) {
731
- const trimmed = title.trim().toLowerCase();
732
- if (/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?(?:\.[a-z]{2,})+$/.test(trimmed)) {
733
- return trimmed.replace(/^www\./, "");
734
- }
735
- return null;
777
+ const trimmed = title.trim();
778
+ return registrableDomain(trimmed) ? hostOf(trimmed) : null;
736
779
  }
737
780
  function extractDomainFromUri(uri) {
738
781
  try {
739
782
  const url = new URL(uri);
740
- const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
741
- if (AI_ENGINE_SELF_DOMAINS.chatgpt.some((self) => hostname.includes(self))) {
783
+ const hostname = hostOf(uri);
784
+ if (!hostname || !registrableDomain(hostname)) return null;
785
+ if (hostMatchesAnyDomain(hostname, AI_ENGINE_SELF_DOMAINS.chatgpt)) {
742
786
  return null;
743
787
  }
744
788
  if (hostname === VERTEX_AI_SEARCH_PROXY_DOMAIN) {
@@ -747,8 +791,7 @@ function extractDomainFromUri(uri) {
747
791
  try {
748
792
  const decoded = decodeURIComponent(redirectPath);
749
793
  if (decoded.startsWith("http")) {
750
- const realUrl = new URL(decoded);
751
- return realUrl.hostname.replace(/^www\./, "");
794
+ return hostOf(decoded);
752
795
  }
753
796
  } catch {
754
797
  }
@@ -1169,16 +1212,9 @@ function extractCitedDomainsFromSources2(groundingSources) {
1169
1212
  return [...domains];
1170
1213
  }
1171
1214
  function extractDomainFromUri2(uri) {
1172
- try {
1173
- const url = new URL(uri);
1174
- const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
1175
- if (AI_ENGINE_SELF_DOMAINS.chatgpt.some((self) => hostname.includes(self))) {
1176
- return null;
1177
- }
1178
- return hostname;
1179
- } catch {
1180
- return null;
1181
- }
1215
+ const hostname = hostOf(uri);
1216
+ if (!hostname || !registrableDomain(hostname) || hostMatchesAnyDomain(hostname, AI_ENGINE_SELF_DOMAINS.chatgpt)) return null;
1217
+ return hostname;
1182
1218
  }
1183
1219
  async function generateText2(prompt, config) {
1184
1220
  const model = config.model ?? DEFAULT_MODEL2;
@@ -1538,16 +1574,11 @@ function extractCitedDomainsFromSources3(groundingSources) {
1538
1574
  return [...domains];
1539
1575
  }
1540
1576
  function extractDomainFromUri3(uri) {
1541
- try {
1542
- const url = new URL(uri);
1543
- const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
1544
- if (AI_ENGINE_SELF_DOMAINS.chatgpt.some((self) => hostname.includes(self))) {
1545
- return null;
1546
- }
1547
- return hostname;
1548
- } catch {
1577
+ const hostname = hostOf(uri);
1578
+ if (!hostname || !registrableDomain(hostname) || hostMatchesAnyDomain(hostname, AI_ENGINE_SELF_DOMAINS.chatgpt)) {
1549
1579
  return null;
1550
1580
  }
1581
+ return hostname;
1551
1582
  }
1552
1583
  async function generateText3(prompt, config) {
1553
1584
  const model = resolveModel2(config);
@@ -1755,11 +1786,10 @@ async function executeTrackedQuery4(input) {
1755
1786
  }
1756
1787
  function normalizeResult4(raw) {
1757
1788
  const answerText = extractAnswerText2(raw.rawResponse);
1758
- const citedDomains = extractDomainMentions(answerText);
1759
- const groundingSources = citedDomains.map((domain) => ({
1760
- uri: `http://${domain}`,
1761
- title: domain
1762
- }));
1789
+ const groundingSources = raw.groundingSources;
1790
+ const citedDomains = [...new Set(
1791
+ groundingSources.map((source) => hostOf(source.uri)).filter((domain) => domain !== null && registrableDomain(domain).length > 0)
1792
+ )];
1763
1793
  return {
1764
1794
  provider: "local",
1765
1795
  answerText,
@@ -1795,19 +1825,6 @@ async function generateText4(prompt, config) {
1795
1825
  );
1796
1826
  return response.choices[0]?.message?.content ?? "";
1797
1827
  }
1798
- function extractDomainMentions(text) {
1799
- const domains = /* @__PURE__ */ new Set();
1800
- const urlPattern = /https?:\/\/([a-zA-Z0-9][-a-zA-Z0-9]*(?:\.[a-zA-Z0-9][-a-zA-Z0-9]*)+)/g;
1801
- let match;
1802
- while ((match = urlPattern.exec(text)) !== null) {
1803
- domains.add(match[1].replace(/^www\./, "").toLowerCase());
1804
- }
1805
- const domainPattern = /(?:^|[\s(["'])((?:[a-zA-Z0-9][-a-zA-Z0-9]*\.)+(?:com|org|net|io|co|dev|ai|app|edu|gov|biz|info|tech|health|dental|legal|law|med|uk|us|ca|au|de|fr|es|it|nl|se|no|dk|fi|jp|cn|kr|br|mx|ru|in|sg|nz|za)(?:\.[a-zA-Z]{2})?)(?:[\s).,;/"']|$)/g;
1806
- while ((match = domainPattern.exec(text)) !== null) {
1807
- domains.add(match[1].replace(/^www\./, "").toLowerCase());
1808
- }
1809
- return [...domains];
1810
- }
1811
1828
  function extractServedModel4(rawResponse) {
1812
1829
  return normalizeServedModel(rawResponse.model);
1813
1830
  }
@@ -2186,6 +2203,9 @@ var chatgptTarget = {
2186
2203
  const { result } = await client.Runtime.evaluate({
2187
2204
  expression: `(() => {
2188
2205
  const SELF_DOMAINS = ${selfDomainsLiteral};
2206
+ const isSelfDomain = hostname => SELF_DOMAINS.some(
2207
+ self => hostname === self || hostname.endsWith('.' + self)
2208
+ );
2189
2209
  const sources = [];
2190
2210
  const seen = new Set();
2191
2211
  const turns = document.querySelectorAll('[data-message-author-role="assistant"]');
@@ -2209,7 +2229,7 @@ var chatgptTarget = {
2209
2229
  }
2210
2230
  }
2211
2231
 
2212
- if (!seen.has(href) && !SELF_DOMAINS.includes(hostname)) {
2232
+ if (!seen.has(href) && !isSelfDomain(hostname)) {
2213
2233
  seen.add(href);
2214
2234
  sources.push({
2215
2235
  uri: href,
@@ -2226,7 +2246,7 @@ var chatgptTarget = {
2226
2246
  if (href && !seen.has(href)) {
2227
2247
  let hostname = '';
2228
2248
  try { hostname = new URL(href).hostname.replace(/^www\\./, ''); } catch {}
2229
- if (!SELF_DOMAINS.includes(hostname)) {
2249
+ if (!isSelfDomain(hostname)) {
2230
2250
  seen.add(href);
2231
2251
  sources.push({ uri: href, title: title || hostname || href });
2232
2252
  }
@@ -2313,13 +2333,10 @@ async function captureElementScreenshot(client, selector, outputPath) {
2313
2333
  function extractCitedDomains(groundingSources) {
2314
2334
  const domains = /* @__PURE__ */ new Set();
2315
2335
  for (const source of groundingSources) {
2316
- try {
2317
- const url = new URL(source.uri);
2318
- const domain = url.hostname.replace(/^www\./, "").toLowerCase();
2319
- if (!AI_ENGINE_SELF_DOMAINS.chatgpt.some((self) => domain.includes(self))) {
2320
- domains.add(domain);
2321
- }
2322
- } catch {
2336
+ const domain = hostOf(source.uri);
2337
+ if (domain && registrableDomain(domain)) {
2338
+ if (!hostMatchesAnyDomain(domain, AI_ENGINE_SELF_DOMAINS.chatgpt)) domains.add(domain);
2339
+ } else {
2323
2340
  const titleDomain = extractDomainFromTitle2(source.title);
2324
2341
  if (titleDomain) domains.add(titleDomain);
2325
2342
  }
@@ -2327,12 +2344,9 @@ function extractCitedDomains(groundingSources) {
2327
2344
  return [...domains];
2328
2345
  }
2329
2346
  function extractDomainFromTitle2(title) {
2330
- const domainPattern = /^(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i;
2331
2347
  const firstWord = title.split(/[\s\-–—|]/)[0]?.trim();
2332
- if (firstWord && domainPattern.test(firstWord)) {
2333
- return firstWord.replace(/^www\./, "").toLowerCase();
2334
- }
2335
- return void 0;
2348
+ if (!firstWord || !registrableDomain(firstWord)) return void 0;
2349
+ return hostOf(firstWord) ?? void 0;
2336
2350
  }
2337
2351
  function normalizeResult5(raw) {
2338
2352
  const answerText = typeof raw.rawResponse.answerText === "string" ? raw.rawResponse.answerText : "";
@@ -2685,16 +2699,9 @@ function extractCitedDomains2(groundingSources) {
2685
2699
  return [...domains];
2686
2700
  }
2687
2701
  function extractDomainFromUri4(uri) {
2688
- try {
2689
- const url = new URL(uri);
2690
- const hostname = url.hostname.replace(/^www\./, "").toLowerCase();
2691
- if (AI_ENGINE_SELF_DOMAINS.chatgpt.some((self) => hostname.includes(self))) {
2692
- return null;
2693
- }
2694
- return hostname;
2695
- } catch {
2696
- return null;
2697
- }
2702
+ const hostname = hostOf(uri);
2703
+ if (!hostname || !registrableDomain(hostname) || hostMatchesAnyDomain(hostname, AI_ENGINE_SELF_DOMAINS.chatgpt)) return null;
2704
+ return hostname;
2698
2705
  }
2699
2706
  async function generateText5(prompt, config) {
2700
2707
  const model = config.model ?? DEFAULT_MODEL5;
@@ -3105,7 +3112,7 @@ import crypto4 from "crypto";
3105
3112
  import fs2 from "fs";
3106
3113
  import path3 from "path";
3107
3114
  import os3 from "os";
3108
- import { and as and2, eq as eq2, inArray, sql as sql2 } from "drizzle-orm";
3115
+ import { and as and2, eq as eq2, inArray, ne, sql as sql2 } from "drizzle-orm";
3109
3116
 
3110
3117
  // src/run-telemetry.ts
3111
3118
  import crypto2 from "crypto";
@@ -3312,10 +3319,18 @@ function classifyOneProviderError(message) {
3312
3319
  return "UNKNOWN";
3313
3320
  }
3314
3321
  var JobRunner = class {
3322
+ /**
3323
+ * Invoked exactly when `activation.completed` is emitted: the project's
3324
+ * first non-empty, non-probe answer-visibility result. The serve process
3325
+ * uses it to thank the operator once; it is a UX hook, not telemetry, so it
3326
+ * fires (and is given) independently of whether telemetry is enabled.
3327
+ */
3328
+ onFirstActivation;
3315
3329
  db;
3316
3330
  registry;
3317
3331
  onRunCompleted;
3318
- constructor(db, registry) {
3332
+ constructor(db, registry, opts) {
3333
+ this.onFirstActivation = opts?.onFirstActivation;
3319
3334
  this.db = db;
3320
3335
  this.registry = registry;
3321
3336
  }
@@ -3587,6 +3602,19 @@ var JobRunner = class {
3587
3602
  }),
3588
3603
  failureCode ? { errorCode: failureCode } : void 0
3589
3604
  );
3605
+ if (existingRun.kind === "answer-visibility" && runTrigger !== "probe" && totalSnapshotsInserted > 0 && !this.hasPriorActivation(projectId, runId)) {
3606
+ trackEvent("activation.completed", {
3607
+ flowVersion: ONBOARDING_FLOW_VERSION,
3608
+ status: finalStatus,
3609
+ providerCountBucket: bucketOnboardingCount(executionContext.providerCount),
3610
+ queryCountBucket: bucketOnboardingCount(executionContext.queryCount),
3611
+ snapshotCountBucket: bucketOnboardingCount(totalSnapshotsInserted)
3612
+ });
3613
+ try {
3614
+ this.onFirstActivation?.();
3615
+ } catch {
3616
+ }
3617
+ }
3590
3618
  this.incrementUsage(projectId, "runs", 1);
3591
3619
  if (this.onRunCompleted) {
3592
3620
  this.onRunCompleted(runId, projectId).catch((err) => {
@@ -3675,8 +3703,18 @@ var JobRunner = class {
3675
3703
  }
3676
3704
  providerReservations.clear();
3677
3705
  }
3706
+ hasPriorActivation(projectId, currentRunId) {
3707
+ return this.db.select({ id: querySnapshots.id }).from(querySnapshots).innerJoin(runs, eq2(querySnapshots.runId, runs.id)).where(and2(
3708
+ eq2(runs.projectId, projectId),
3709
+ eq2(runs.kind, "answer-visibility"),
3710
+ inArray(runs.status, ["completed", "partial"]),
3711
+ ne(runs.trigger, "probe"),
3712
+ ne(runs.id, currentRunId)
3713
+ )).limit(1).get() !== void 0;
3714
+ }
3678
3715
  getRunState(runId) {
3679
3716
  return this.db.select({
3717
+ kind: runs.kind,
3680
3718
  status: runs.status,
3681
3719
  finishedAt: runs.finishedAt,
3682
3720
  error: runs.error,
@@ -3731,6 +3769,21 @@ function buildPhases(input) {
3731
3769
  return { setup_ms, provider_call_ms, total_ms };
3732
3770
  }
3733
3771
 
3772
+ // src/activation-notice.ts
3773
+ var ACTIVATION_NOTICE = "\n\u2605 First sweep finished. Your AI visibility baseline is in the dashboard.\n If Canonry is useful, a GitHub star helps others find it:\n https://github.com/Canonry/canonry\n\n";
3774
+ function maybeShowActivationNotice(io = {}) {
3775
+ const isTTY = io.isTTY ?? Boolean(process.stderr.isTTY);
3776
+ const write = io.write ?? ((line) => process.stderr.write(line));
3777
+ try {
3778
+ if (!isTTY) return;
3779
+ const raw = loadConfigRaw();
3780
+ if (!raw || raw.activationNoticeShown === true) return;
3781
+ saveConfigPatch({ activationNoticeShown: true });
3782
+ write(ACTIVATION_NOTICE);
3783
+ } catch {
3784
+ }
3785
+ }
3786
+
3734
3787
  // src/gsc-sync.ts
3735
3788
  import crypto5 from "crypto";
3736
3789
  import { eq as eq3, and as and3, sql as sql3 } from "drizzle-orm";
@@ -5930,7 +5983,7 @@ function aggregateInboundLinksByDomain(links, targetDomain) {
5930
5983
  for (const link of links) {
5931
5984
  const host = hostOf(link.Url);
5932
5985
  if (!host) continue;
5933
- if (target && (host === target || host.endsWith(`.${target}`))) continue;
5986
+ if (target && hostMatchesDomain(host, target)) continue;
5934
5987
  let urls = byHost.get(host);
5935
5988
  if (!urls) {
5936
5989
  urls = /* @__PURE__ */ new Set();
@@ -7066,7 +7119,7 @@ function readStoredGroundingSources(rawResponse) {
7066
7119
  return result;
7067
7120
  }
7068
7121
  async function backfillInsightsCommand(project, opts) {
7069
- const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-BKLR57RQ.js");
7122
+ const { IntelligenceService: IntelligenceService2 } = await import("./intelligence-service-JEFHUGVX.js");
7070
7123
  const config = loadConfig();
7071
7124
  const db = createClient(config.database);
7072
7125
  migrate(db);
@@ -10899,16 +10952,6 @@ var FETCH_TIMEOUT_MS2 = 1e4;
10899
10952
  var MAX_TEXT_LENGTH = 4e3;
10900
10953
  var MAX_BODY_BYTES = 512e3;
10901
10954
  var USER_AGENT = "Canonry/1.0 (site-analysis)";
10902
- function extractHostname(domain) {
10903
- let hostname = domain;
10904
- try {
10905
- if (hostname.includes("://")) {
10906
- hostname = new URL(hostname).hostname;
10907
- }
10908
- } catch {
10909
- }
10910
- return hostname.replace(/^www\./, "");
10911
- }
10912
10955
  function fetchWithPinnedAddress(target) {
10913
10956
  return new Promise((resolve) => {
10914
10957
  const port = target.url.port ? Number(target.url.port) : 443;
@@ -10965,7 +11008,8 @@ function fetchWithPinnedAddress(target) {
10965
11008
  });
10966
11009
  }
10967
11010
  async function fetchSiteText(domain) {
10968
- const hostname = extractHostname(domain);
11011
+ const hostname = hostOf(domain);
11012
+ if (!hostname) return "";
10969
11013
  const url = `https://${hostname}`;
10970
11014
  const targetCheck = await resolveWebhookTarget(url);
10971
11015
  if (!targetCheck.ok) return "";
@@ -11084,7 +11128,7 @@ var SnapshotService = class {
11084
11128
  registry;
11085
11129
  async createReport(input) {
11086
11130
  const companyName = input.companyName.trim();
11087
- const domain = normalizeDomain(input.domain);
11131
+ const domain = hostOf(input.domain) ?? input.domain.trim();
11088
11132
  const manualQueries = normalizeStringList(resolveSnapshotRequestQueries(input));
11089
11133
  const manualCompetitors = normalizeStringList(input.competitors ?? []);
11090
11134
  const providers = this.registry.getAll();
@@ -11092,14 +11136,14 @@ var SnapshotService = class {
11092
11136
  throw new Error("No providers configured. Add at least one provider API key before running canonry snapshot.");
11093
11137
  }
11094
11138
  const analysisProvider = pickAnalysisProvider(this.registry.getApiProviders());
11095
- const homepageUrl = `https://${extractHostname2(domain)}`;
11139
+ const homepageUrl = `https://${domain}`;
11096
11140
  const [siteText, audit] = await Promise.all([
11097
11141
  fetchSiteText(domain),
11098
11142
  this.runAudit(homepageUrl)
11099
11143
  ]);
11100
11144
  if (manualQueries.length === 0 && !siteText) {
11101
11145
  throw new Error(
11102
- `Could not analyze https://${extractHostname2(domain)}. Try again with a reachable homepage or pass manual category queries via --queries.`
11146
+ `Could not analyze https://${domain}. Try again with a reachable homepage or pass manual category queries via --queries.`
11103
11147
  );
11104
11148
  }
11105
11149
  const profile = await this.buildProfile({
@@ -11501,36 +11545,30 @@ function buildFallbackRecommendedActions(audit) {
11501
11545
  return uniqueStrings([...weakestFactors, ...defaults]).slice(0, 4);
11502
11546
  }
11503
11547
  function citesTargetDomain(citedDomains, groundingSources, targetDomain) {
11504
- const normalizedTarget = extractHostname2(targetDomain);
11505
11548
  for (const domain of citedDomains) {
11506
- if (domainMatches(domain, normalizedTarget)) {
11549
+ if (hostMatchesDomain(domain, targetDomain)) {
11507
11550
  return true;
11508
11551
  }
11509
11552
  }
11510
11553
  for (const source of groundingSources) {
11511
- if (source.uri && source.uri.toLowerCase().includes(normalizedTarget.toLowerCase())) {
11512
- return true;
11513
- }
11514
- if (source.title && source.title.toLowerCase().includes(normalizedTarget.toLowerCase())) {
11515
- return true;
11516
- }
11554
+ if (hostMatchesDomain(source.uri, targetDomain)) return true;
11555
+ if (source.title && hostMatchesDomain(source.title, targetDomain)) return true;
11517
11556
  }
11518
11557
  return false;
11519
11558
  }
11520
11559
  function extractCompetitorsFromResponse(ctx) {
11521
11560
  const competitors2 = /* @__PURE__ */ new Set();
11522
- const lowerAnswer = ctx.answerText.toLowerCase();
11523
- const targetDomain = extractHostname2(ctx.targetDomain);
11561
+ const targetDomain = hostOf(ctx.targetDomain) ?? ctx.targetDomain;
11524
11562
  for (const hint of ctx.manualCompetitors) {
11525
11563
  if (isDomainLike(hint)) {
11526
- const normalizedHint = normalizeDomain(hint);
11527
- if (domainMatches(normalizedHint, targetDomain)) continue;
11528
- if (ctx.citedDomains.some((domain) => domainMatches(domain, normalizedHint)) || lowerAnswer.includes(normalizedHint.toLowerCase())) {
11564
+ const normalizedHint = hostOf(hint) ?? hint;
11565
+ if (hostMatchesDomain(normalizedHint, targetDomain)) continue;
11566
+ if (ctx.citedDomains.some((domain) => hostMatchesDomain(domain, normalizedHint)) || textContainsDomain(ctx.answerText, normalizedHint)) {
11529
11567
  competitors2.add(normalizedHint);
11530
11568
  }
11531
11569
  continue;
11532
11570
  }
11533
- if (hint.length >= 3 && lowerAnswer.includes(hint.toLowerCase())) {
11571
+ if (hint.length >= 3 && textContainsBrandAlias(ctx.answerText, hint)) {
11534
11572
  competitors2.add(hint);
11535
11573
  }
11536
11574
  }
@@ -11579,27 +11617,8 @@ function uniqueStrings(values) {
11579
11617
  values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)
11580
11618
  )];
11581
11619
  }
11582
- function normalizeDomain(value) {
11583
- const trimmed = value.trim();
11584
- if (!trimmed) return trimmed;
11585
- try {
11586
- const url = trimmed.includes("://") ? new URL(trimmed) : new URL(`https://${trimmed}`);
11587
- return url.hostname.replace(/^www\./, "").toLowerCase();
11588
- } catch {
11589
- return trimmed.replace(/^https?:\/\//, "").replace(/\/.*$/, "").replace(/^www\./, "").toLowerCase();
11590
- }
11591
- }
11592
- function extractHostname2(value) {
11593
- return normalizeDomain(value);
11594
- }
11595
- function domainMatches(candidate, target) {
11596
- const normalizedCandidate = normalizeDomain(candidate);
11597
- const normalizedTarget = normalizeDomain(target);
11598
- return normalizedCandidate === normalizedTarget || normalizedCandidate.endsWith(`.${normalizedTarget}`);
11599
- }
11600
11620
  function isDomainLike(value) {
11601
- const normalized = normalizeDomain(value);
11602
- return normalized.includes(".") && !normalized.includes(" ");
11621
+ return registrableDomain(value).length > 0;
11603
11622
  }
11604
11623
  function clipText(value, length) {
11605
11624
  if (value.length <= length) return value;
@@ -11998,7 +12017,12 @@ async function createServer(opts) {
11998
12017
  }
11999
12018
  const port = opts.config.port ?? 4100;
12000
12019
  const serverUrl = `http://localhost:${port}`;
12001
- const jobRunner = new JobRunner(opts.db, registry);
12020
+ const jobRunner = new JobRunner(opts.db, registry, {
12021
+ // The one-time first-sweep thank-you. Lives on the serve console because
12022
+ // runs execute here, including the foreground serve that init hands off
12023
+ // to. TTY-gated inside, so supervised deployments never see it.
12024
+ onFirstActivation: () => maybeShowActivationNotice()
12025
+ });
12002
12026
  jobRunner.recoverStaleRuns();
12003
12027
  const notifier = new Notifier(opts.db, serverUrl);
12004
12028
  const intelligenceService = new IntelligenceService(opts.db);
@@ -13127,6 +13151,7 @@ async function createServer(opts) {
13127
13151
  app.log.error({ runId, err }, "Job runner failed");
13128
13152
  });
13129
13153
  },
13154
+ getRunnableProviderNames: () => registry.getAll().map((provider) => provider.adapter.name),
13130
13155
  onProviderUpdate: (providerName, apiKey, model, baseUrl, incomingQuota) => {
13131
13156
  const name = providerName;
13132
13157
  if (!adapterMap[name]) return null;
@@ -13264,6 +13289,10 @@ async function createServer(opts) {
13264
13289
  saveConfigPatch(config);
13265
13290
  opts.config.telemetry = enabled;
13266
13291
  },
13292
+ recordOnboardingEvent: (event) => {
13293
+ const { event: eventName, eventId, ...properties } = event;
13294
+ trackEvent(eventName, properties, { source: "dashboard", eventId });
13295
+ },
13267
13296
  onCdpConfigure: async (host, port2) => {
13268
13297
  if (!opts.config.cdp) opts.config.cdp = {};
13269
13298
  opts.config.cdp.host = host;
@@ -13502,6 +13531,7 @@ function parseQueryResponse(raw, count) {
13502
13531
  }
13503
13532
 
13504
13533
  export {
13534
+ trackCliCommandFinished,
13505
13535
  setTelemetrySource,
13506
13536
  isTelemetryEnabled,
13507
13537
  getOrCreateAnonymousId,