@ainyc/canonry 3.2.5 → 3.3.2

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.
package/assets/index.html CHANGED
@@ -12,8 +12,8 @@
12
12
  <link rel="icon" type="image/png" sizes="32x32" href="./favicon-32.png" />
13
13
  <link rel="apple-touch-icon" href="./apple-touch-icon.png" />
14
14
  <title>Canonry</title>
15
- <script type="module" crossorigin src="./assets/index-B8epngdo.js"></script>
16
- <link rel="stylesheet" crossorigin href="./assets/index-CQGbgHZY.css">
15
+ <script type="module" crossorigin src="./assets/index-C1kUp1aS.js"></script>
16
+ <link rel="stylesheet" crossorigin href="./assets/index-JG7aBJrz.css">
17
17
  </head>
18
18
  <body>
19
19
  <div id="root"></div>
@@ -641,6 +641,64 @@ function normalizeProjectDomain(input) {
641
641
  }
642
642
  return domain.replace(/^www\./, "");
643
643
  }
644
+ var MULTI_LABEL_PUBLIC_SUFFIXES = /* @__PURE__ */ new Set([
645
+ "ac.uk",
646
+ "co.id",
647
+ "co.il",
648
+ "co.in",
649
+ "co.jp",
650
+ "co.kr",
651
+ "co.nz",
652
+ "co.th",
653
+ "co.uk",
654
+ "co.za",
655
+ "com.au",
656
+ "com.br",
657
+ "com.cn",
658
+ "com.mx",
659
+ "com.ph",
660
+ "com.sg",
661
+ "com.tr",
662
+ "edu.au",
663
+ "edu.sg",
664
+ "gov.au",
665
+ "gov.uk",
666
+ "me.uk",
667
+ "ne.jp",
668
+ "net.au",
669
+ "net.br",
670
+ "net.cn",
671
+ "net.in",
672
+ "net.tr",
673
+ "or.jp",
674
+ "or.kr",
675
+ "org.au",
676
+ "org.br",
677
+ "org.in",
678
+ "org.nz",
679
+ "org.tr",
680
+ "org.uk",
681
+ "org.za"
682
+ ]);
683
+ function registrableDomain(input) {
684
+ const normalized = normalizeProjectDomain(input);
685
+ if (!normalized) return "";
686
+ const hostname = normalized.split("/")[0]?.split(":")[0] ?? "";
687
+ if (!hostname) return "";
688
+ const labels = hostname.split(".").filter(Boolean);
689
+ if (labels.length < 2) return "";
690
+ if (labels.length === 2) return labels.join(".");
691
+ const lastTwo = labels.slice(-2).join(".");
692
+ if (MULTI_LABEL_PUBLIC_SUFFIXES.has(lastTwo)) {
693
+ return labels.length >= 3 ? labels.slice(-3).join(".") : "";
694
+ }
695
+ return labels.slice(-2).join(".");
696
+ }
697
+ function brandLabelFromDomain(input) {
698
+ const reg = registrableDomain(input);
699
+ if (!reg) return "";
700
+ return reg.split(".")[0] ?? "";
701
+ }
644
702
  function effectiveDomains(project) {
645
703
  const all = [project.canonicalDomain, ...project.ownedDomains ?? []];
646
704
  const seen = /* @__PURE__ */ new Set();
@@ -1482,11 +1540,10 @@ function collectDistinctiveTokens(displayName, domains) {
1482
1540
  tokens.add(token);
1483
1541
  }
1484
1542
  for (const domain of domains) {
1485
- const hostname = normalizeProjectDomain(domain).split("/")[0] ?? "";
1486
- for (const label of hostname.split(".").filter(Boolean)) {
1487
- const token = label.replace(/[^a-z0-9]/gi, "").toLowerCase();
1488
- if (isDistinctiveToken(token)) tokens.add(token);
1489
- }
1543
+ const reg = registrableDomain(domain);
1544
+ const brand = reg ? brandLabelFromDomain(reg) : normalizeProjectDomain(domain).split("/")[0]?.split(".")[0] ?? "";
1545
+ const token = brand.replace(/[^a-z0-9]/gi, "").toLowerCase();
1546
+ if (isDistinctiveToken(token)) tokens.add(token);
1490
1547
  }
1491
1548
  return [...tokens];
1492
1549
  }
@@ -2010,6 +2067,13 @@ function citationStateToCited(state) {
2010
2067
  return state === "cited";
2011
2068
  }
2012
2069
 
2070
+ // ../contracts/src/skills.ts
2071
+ import { z as z19 } from "zod";
2072
+ var codingAgentSchema = z19.enum(["claude", "codex"]);
2073
+ var CodingAgents = codingAgentSchema.enum;
2074
+ var skillsClientSchema = z19.enum(["claude", "codex", "all"]);
2075
+ var SkillsClients = skillsClientSchema.enum;
2076
+
2013
2077
  // src/client.ts
2014
2078
  function createApiClient() {
2015
2079
  const config = loadConfig();
@@ -2677,21 +2741,21 @@ var ApiClient = class {
2677
2741
  };
2678
2742
 
2679
2743
  // src/mcp/tool-registry.ts
2680
- import { z as z20 } from "zod";
2744
+ import { z as z21 } from "zod";
2681
2745
 
2682
2746
  // src/mcp/schema.ts
2683
- import { z as z19 } from "zod";
2684
- var projectNameSchema = z19.string().min(1).describe("Canonry project name.");
2685
- var runIdSchema = z19.string().min(1).describe("Canonry run ID.");
2686
- var insightIdSchema = z19.string().min(1).describe("Canonry insight ID.");
2687
- var analyticsWindowSchema = z19.enum(["7d", "30d", "90d", "all"]).describe("Analytics time window.");
2688
- var emptyInputSchema = z19.object({});
2689
- var projectInputSchema = z19.object({
2747
+ import { z as z20 } from "zod";
2748
+ var projectNameSchema = z20.string().min(1).describe("Canonry project name.");
2749
+ var runIdSchema = z20.string().min(1).describe("Canonry run ID.");
2750
+ var insightIdSchema = z20.string().min(1).describe("Canonry insight ID.");
2751
+ var analyticsWindowSchema = z20.enum(["7d", "30d", "90d", "all"]).describe("Analytics time window.");
2752
+ var emptyInputSchema = z20.object({});
2753
+ var projectInputSchema = z20.object({
2690
2754
  project: projectNameSchema
2691
2755
  });
2692
2756
  function toJsonSchema(schema, name) {
2693
2757
  return {
2694
- ...z19.toJSONSchema(schema, { target: "draft-7" }),
2758
+ ...z20.toJSONSchema(schema, { target: "draft-7" }),
2695
2759
  title: name
2696
2760
  };
2697
2761
  }
@@ -2733,119 +2797,119 @@ function defineTool(tool) {
2733
2797
  inputJsonSchema: toJsonSchema(tool.inputSchema, tool.name)
2734
2798
  };
2735
2799
  }
2736
- var runTriggerInputSchema = z20.object({
2800
+ var runTriggerInputSchema = z21.object({
2737
2801
  project: projectNameSchema,
2738
2802
  request: runTriggerRequestSchema.optional()
2739
2803
  });
2740
- var runsListInputSchema = z20.object({
2804
+ var runsListInputSchema = z21.object({
2741
2805
  project: projectNameSchema,
2742
- limit: z20.number().int().positive().max(500).optional()
2806
+ limit: z21.number().int().positive().max(500).optional()
2743
2807
  });
2744
- var runGetInputSchema = z20.object({
2808
+ var runGetInputSchema = z21.object({
2745
2809
  runId: runIdSchema
2746
2810
  });
2747
- var timelineInputSchema = z20.object({
2811
+ var timelineInputSchema = z21.object({
2748
2812
  project: projectNameSchema,
2749
- location: z20.string().optional().describe("Location label. Use an empty string for locationless results.")
2813
+ location: z21.string().optional().describe("Location label. Use an empty string for locationless results.")
2750
2814
  });
2751
- var snapshotsListInputSchema = z20.object({
2815
+ var snapshotsListInputSchema = z21.object({
2752
2816
  project: projectNameSchema,
2753
- limit: z20.number().int().positive().max(500).optional(),
2754
- offset: z20.number().int().nonnegative().optional(),
2755
- location: z20.string().optional().describe("Location label. Use an empty string for locationless results.")
2817
+ limit: z21.number().int().positive().max(500).optional(),
2818
+ offset: z21.number().int().nonnegative().optional(),
2819
+ location: z21.string().optional().describe("Location label. Use an empty string for locationless results.")
2756
2820
  });
2757
- var snapshotsDiffInputSchema = z20.object({
2821
+ var snapshotsDiffInputSchema = z21.object({
2758
2822
  project: projectNameSchema,
2759
2823
  run1: runIdSchema,
2760
2824
  run2: runIdSchema
2761
2825
  });
2762
- var insightsListInputSchema = z20.object({
2826
+ var insightsListInputSchema = z21.object({
2763
2827
  project: projectNameSchema,
2764
- dismissed: z20.boolean().optional(),
2828
+ dismissed: z21.boolean().optional(),
2765
2829
  runId: runIdSchema.optional()
2766
2830
  });
2767
- var insightInputSchema = z20.object({
2831
+ var insightInputSchema = z21.object({
2768
2832
  project: projectNameSchema,
2769
2833
  insightId: insightIdSchema
2770
2834
  });
2771
- var healthHistoryInputSchema = z20.object({
2835
+ var healthHistoryInputSchema = z21.object({
2772
2836
  project: projectNameSchema,
2773
- limit: z20.number().int().positive().max(100).optional()
2837
+ limit: z21.number().int().positive().max(100).optional()
2774
2838
  });
2775
- var gscPerformanceInputSchema = z20.object({
2839
+ var gscPerformanceInputSchema = z21.object({
2776
2840
  project: projectNameSchema,
2777
- startDate: z20.string().optional(),
2778
- endDate: z20.string().optional(),
2779
- query: z20.string().optional(),
2780
- page: z20.string().optional(),
2781
- limit: z20.number().int().positive().max(500).optional(),
2841
+ startDate: z21.string().optional(),
2842
+ endDate: z21.string().optional(),
2843
+ query: z21.string().optional(),
2844
+ page: z21.string().optional(),
2845
+ limit: z21.number().int().positive().max(500).optional(),
2782
2846
  window: analyticsWindowSchema.optional()
2783
2847
  });
2784
- var gscInspectionsInputSchema = z20.object({
2848
+ var gscInspectionsInputSchema = z21.object({
2785
2849
  project: projectNameSchema,
2786
- url: z20.string().optional(),
2787
- limit: z20.number().int().positive().max(500).optional()
2850
+ url: z21.string().optional(),
2851
+ limit: z21.number().int().positive().max(500).optional()
2788
2852
  });
2789
- var gscCoverageHistoryInputSchema = z20.object({
2853
+ var gscCoverageHistoryInputSchema = z21.object({
2790
2854
  project: projectNameSchema,
2791
- limit: z20.number().int().positive().max(500).optional()
2855
+ limit: z21.number().int().positive().max(500).optional()
2792
2856
  });
2793
- var gaWindowInputSchema = z20.object({
2857
+ var gaWindowInputSchema = z21.object({
2794
2858
  project: projectNameSchema,
2795
2859
  window: analyticsWindowSchema.optional()
2796
2860
  });
2797
2861
  var gaTrafficInputSchema = gaWindowInputSchema.extend({
2798
- limit: z20.number().int().positive().max(500).optional()
2862
+ limit: z21.number().int().positive().max(500).optional()
2799
2863
  });
2800
- var keywordsInputSchema = z20.object({
2864
+ var keywordsInputSchema = z21.object({
2801
2865
  project: projectNameSchema,
2802
2866
  request: keywordBatchRequestSchema
2803
2867
  });
2804
- var keywordGenerateInputSchema = z20.object({
2868
+ var keywordGenerateInputSchema = z21.object({
2805
2869
  project: projectNameSchema,
2806
2870
  request: keywordGenerateRequestSchema
2807
2871
  });
2808
- var competitorsInputSchema = z20.object({
2872
+ var competitorsInputSchema = z21.object({
2809
2873
  project: projectNameSchema,
2810
2874
  request: competitorBatchRequestSchema
2811
2875
  });
2812
- var projectUpsertInputSchema = z20.object({
2876
+ var projectUpsertInputSchema = z21.object({
2813
2877
  project: projectNameSchema,
2814
2878
  request: projectUpsertRequestSchema
2815
2879
  });
2816
- var applyConfigInputSchema = z20.object({
2880
+ var applyConfigInputSchema = z21.object({
2817
2881
  config: projectConfigSchema
2818
2882
  });
2819
- var scheduleSetInputSchema = z20.object({
2883
+ var scheduleSetInputSchema = z21.object({
2820
2884
  project: projectNameSchema,
2821
2885
  schedule: scheduleUpsertRequestSchema
2822
2886
  });
2823
- var agentWebhookAttachInputSchema = z20.object({
2887
+ var agentWebhookAttachInputSchema = z21.object({
2824
2888
  project: projectNameSchema,
2825
- url: z20.string().url()
2889
+ url: z21.string().url()
2826
2890
  });
2827
- var doctorInputSchema = z20.object({
2891
+ var doctorInputSchema = z21.object({
2828
2892
  project: projectNameSchema.optional().describe("Project name to scope project-level checks. Omit to run global checks (provider keys, config, etc.)."),
2829
- checks: z20.array(z20.string().min(1)).optional().describe('Optional check IDs or wildcard prefixes (e.g. "google.auth.*", "config.providers"). Empty/omitted runs all matching checks for the chosen scope.')
2893
+ checks: z21.array(z21.string().min(1)).optional().describe('Optional check IDs or wildcard prefixes (e.g. "google.auth.*", "config.providers"). Empty/omitted runs all matching checks for the chosen scope.')
2830
2894
  });
2831
- var contentTargetsInputSchema = z20.object({
2895
+ var contentTargetsInputSchema = z21.object({
2832
2896
  project: projectNameSchema,
2833
- limit: z20.number().int().positive().max(500).optional().describe("Max rows. Defaults to all. Use a small number (3-10) when summarizing for the user."),
2834
- includeInProgress: z20.boolean().optional().describe("Include rows that already have an in-flight tracked action. Default false.")
2897
+ limit: z21.number().int().positive().max(500).optional().describe("Max rows. Defaults to all. Use a small number (3-10) when summarizing for the user."),
2898
+ includeInProgress: z21.boolean().optional().describe("Include rows that already have an in-flight tracked action. Default false.")
2835
2899
  });
2836
- var backlinksDomainsInputSchema = z20.object({
2900
+ var backlinksDomainsInputSchema = z21.object({
2837
2901
  project: projectNameSchema,
2838
- limit: z20.number().int().positive().max(200).optional().describe("Max linking-domain rows. Default 50, max 200."),
2839
- release: z20.string().optional().describe("Common Crawl release id (e.g., cc-main-2026-jan-feb-mar). Omit for the most recent release with data.")
2902
+ limit: z21.number().int().positive().max(200).optional().describe("Max linking-domain rows. Default 50, max 200."),
2903
+ release: z21.string().optional().describe("Common Crawl release id (e.g., cc-main-2026-jan-feb-mar). Omit for the most recent release with data.")
2840
2904
  });
2841
- var memoryUpsertInputSchema = z20.object({
2905
+ var memoryUpsertInputSchema = z21.object({
2842
2906
  project: projectNameSchema,
2843
- key: z20.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH).describe(`Stable identifier for the note (max ${AGENT_MEMORY_KEY_MAX_LENGTH} chars). Writing the same key overwrites the prior value.`),
2844
- value: z20.string().min(1).describe(`Plain-text note body (max ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes). Use for durable operator preferences, migration context, or non-obvious reasoning that should survive future sessions.`)
2907
+ key: z21.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH).describe(`Stable identifier for the note (max ${AGENT_MEMORY_KEY_MAX_LENGTH} chars). Writing the same key overwrites the prior value.`),
2908
+ value: z21.string().min(1).describe(`Plain-text note body (max ${AGENT_MEMORY_VALUE_MAX_BYTES} bytes). Use for durable operator preferences, migration context, or non-obvious reasoning that should survive future sessions.`)
2845
2909
  });
2846
- var memoryForgetInputSchema = z20.object({
2910
+ var memoryForgetInputSchema = z21.object({
2847
2911
  project: projectNameSchema,
2848
- key: z20.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH).describe("Exact key of the note to remove. No-op (status=missing) when no note exists for that key.")
2912
+ key: z21.string().min(1).max(AGENT_MEMORY_KEY_MAX_LENGTH).describe("Exact key of the note to remove. No-op (status=missing) when no note exists for that key.")
2849
2913
  });
2850
2914
  var AGENT_WEBHOOK_EVENTS = [
2851
2915
  notificationEventSchema.enum["run.completed"],
@@ -2893,10 +2957,10 @@ var canonryMcpTools = [
2893
2957
  description: "Search query snapshots and intelligence insights for the given text. Looks at snapshot answer text, cited domains, raw provider responses, and insight title/keyword/recommendation/cause. Returns ranked hits with snippets \u2014 use it instead of paginating snapshots when you need to find a competitor mention or term.",
2894
2958
  access: "read",
2895
2959
  tier: "core",
2896
- inputSchema: z20.object({
2960
+ inputSchema: z21.object({
2897
2961
  project: projectNameSchema,
2898
- q: z20.string().min(2).describe("Search term, at least 2 characters."),
2899
- limit: z20.number().int().positive().max(50).optional().describe("Max combined hits (1-50, default 25).")
2962
+ q: z21.string().min(2).describe("Search term, at least 2 characters."),
2963
+ limit: z21.number().int().positive().max(50).optional().describe("Max combined hits (1-50, default 25).")
2900
2964
  }),
2901
2965
  annotations: readAnnotations(),
2902
2966
  openApiOperations: ["GET /api/v1/projects/{name}/search"],
@@ -3607,6 +3671,8 @@ export {
3607
3671
  keywordGenerateRequestSchema,
3608
3672
  competitorBatchRequestSchema,
3609
3673
  normalizeProjectDomain,
3674
+ registrableDomain,
3675
+ brandLabelFromDomain,
3610
3676
  effectiveDomains,
3611
3677
  projectConfigSchema,
3612
3678
  AppError,
@@ -3658,6 +3724,9 @@ export {
3658
3724
  normalizeUrlPath,
3659
3725
  emptyCitationVisibility,
3660
3726
  citationStateToCited,
3727
+ CodingAgents,
3728
+ skillsClientSchema,
3729
+ SkillsClients,
3661
3730
  createApiClient,
3662
3731
  ApiClient,
3663
3732
  canonryMcpTools
@@ -19,6 +19,7 @@ import {
19
19
  authInvalid,
20
20
  authRequired,
21
21
  brandKeyFromText,
22
+ brandLabelFromDomain,
22
23
  buildRunErrorFromMessages,
23
24
  canonryMcpTools,
24
25
  categorizeSource,
@@ -49,6 +50,7 @@ import {
49
50
  projectConfigSchema,
50
51
  projectUpsertRequestSchema,
51
52
  providerError,
53
+ registrableDomain,
52
54
  runInProgress,
53
55
  runNotCancellable,
54
56
  runTriggerRequestSchema,
@@ -62,7 +64,7 @@ import {
62
64
  visibilityStateFromAnswerMentioned,
63
65
  windowCutoff,
64
66
  wordpressEnvSchema
65
- } from "./chunk-PS7JRDL3.js";
67
+ } from "./chunk-ALMP3NBQ.js";
66
68
  import {
67
69
  IntelligenceService,
68
70
  agentMemory,
@@ -721,6 +723,24 @@ async function keywordRoutes(app, opts) {
721
723
  // ../api-routes/src/competitors.ts
722
724
  import crypto6 from "crypto";
723
725
  import { eq as eq5 } from "drizzle-orm";
726
+ function normalizeCompetitor(domain) {
727
+ const reg = registrableDomain(domain);
728
+ if (reg) return reg;
729
+ return normalizeProjectDomain(domain);
730
+ }
731
+ function normalizeCompetitorList(domains) {
732
+ const seen = /* @__PURE__ */ new Set();
733
+ const result = [];
734
+ for (const raw of domains) {
735
+ const trimmed = raw?.trim();
736
+ if (!trimmed) continue;
737
+ const normalized = normalizeCompetitor(trimmed);
738
+ if (!normalized || seen.has(normalized)) continue;
739
+ seen.add(normalized);
740
+ result.push(normalized);
741
+ }
742
+ return result;
743
+ }
724
744
  async function competitorRoutes(app) {
725
745
  app.get("/projects/:name/competitors", async (request, reply) => {
726
746
  const project = resolveProject(app.db, request.params.name);
@@ -734,9 +754,10 @@ async function competitorRoutes(app) {
734
754
  throw validationError('Body must contain a "competitors" array');
735
755
  }
736
756
  const now = (/* @__PURE__ */ new Date()).toISOString();
757
+ const normalizedCompetitors = normalizeCompetitorList(body.competitors);
737
758
  app.db.transaction((tx) => {
738
759
  tx.delete(competitors).where(eq5(competitors.projectId, project.id)).run();
739
- for (const domain of body.competitors) {
760
+ for (const domain of normalizedCompetitors) {
740
761
  tx.insert(competitors).values({
741
762
  id: crypto6.randomUUID(),
742
763
  projectId: project.id,
@@ -749,7 +770,7 @@ async function competitorRoutes(app) {
749
770
  actor: "api",
750
771
  action: "competitors.replaced",
751
772
  entityType: "competitor",
752
- diff: { competitors: body.competitors }
773
+ diff: { competitors: normalizedCompetitors }
753
774
  });
754
775
  });
755
776
  const rows = app.db.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
@@ -759,7 +780,7 @@ async function competitorRoutes(app) {
759
780
  const project = resolveProject(app.db, request.params.name);
760
781
  const body = parseCompetitorBatch(request.body);
761
782
  const now = (/* @__PURE__ */ new Date()).toISOString();
762
- const requested = uniqueStrings(body.competitors);
783
+ const requested = normalizeCompetitorList(body.competitors);
763
784
  app.db.transaction((tx) => {
764
785
  const existing = tx.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
765
786
  const existingSet = new Set(existing.map((c) => c.domain));
@@ -789,7 +810,7 @@ async function competitorRoutes(app) {
789
810
  app.delete("/projects/:name/competitors", async (request, reply) => {
790
811
  const project = resolveProject(app.db, request.params.name);
791
812
  const body = parseCompetitorBatch(request.body);
792
- const requested = new Set(uniqueStrings(body.competitors));
813
+ const requested = new Set(normalizeCompetitorList(body.competitors));
793
814
  app.db.transaction((tx) => {
794
815
  const existing = tx.select().from(competitors).where(eq5(competitors.projectId, project.id)).all();
795
816
  const rowsToDelete = existing.filter((c) => requested.has(c.domain));
@@ -819,16 +840,6 @@ function parseCompetitorBatch(value) {
819
840
  }))
820
841
  });
821
842
  }
822
- function uniqueStrings(values) {
823
- const seen = /* @__PURE__ */ new Set();
824
- const result = [];
825
- for (const value of values) {
826
- if (seen.has(value)) continue;
827
- seen.add(value);
828
- result.push(value);
829
- }
830
- return result;
831
- }
832
843
 
833
844
  // ../api-routes/src/runs.ts
834
845
  import crypto8 from "crypto";
@@ -1550,7 +1561,8 @@ async function applyRoutes(app, opts) {
1550
1561
  diff: { keywords: config.spec.keywords }
1551
1562
  });
1552
1563
  tx.delete(competitors).where(eq8(competitors.projectId, projectId)).run();
1553
- for (const domain of config.spec.competitors) {
1564
+ const normalizedCompetitors = normalizeCompetitorList2(config.spec.competitors);
1565
+ for (const domain of normalizedCompetitors) {
1554
1566
  tx.insert(competitors).values({
1555
1567
  id: crypto10.randomUUID(),
1556
1568
  projectId,
@@ -1563,7 +1575,7 @@ async function applyRoutes(app, opts) {
1563
1575
  actor: "api",
1564
1576
  action: "competitors.replaced",
1565
1577
  entityType: "competitor",
1566
- diff: { competitors: config.spec.competitors }
1578
+ diff: { competitors: normalizedCompetitors }
1567
1579
  });
1568
1580
  if (resolvedSchedule) {
1569
1581
  const existingSched = tx.select().from(schedules).where(eq8(schedules.projectId, projectId)).get();
@@ -1651,6 +1663,19 @@ async function applyRoutes(app, opts) {
1651
1663
  });
1652
1664
  });
1653
1665
  }
1666
+ function normalizeCompetitorList2(domains) {
1667
+ const seen = /* @__PURE__ */ new Set();
1668
+ const result = [];
1669
+ for (const raw of domains) {
1670
+ const trimmed = raw?.trim();
1671
+ if (!trimmed) continue;
1672
+ const normalized = registrableDomain(trimmed) || normalizeProjectDomain(trimmed);
1673
+ if (!normalized || seen.has(normalized)) continue;
1674
+ seen.add(normalized);
1675
+ result.push(normalized);
1676
+ }
1677
+ return result;
1678
+ }
1654
1679
 
1655
1680
  // ../api-routes/src/history.ts
1656
1681
  import { eq as eq9, desc as desc2, inArray } from "drizzle-orm";
@@ -15134,14 +15159,17 @@ function computeCompetitorOverlap(normalized, competitorDomains) {
15134
15159
  if (lowerAnswer.includes(cd.toLowerCase())) {
15135
15160
  overlapSet.add(cd);
15136
15161
  }
15137
- const brand = cd.split(".")[0];
15138
- if (brand && brand.length >= 4 && new RegExp(`\\b${brand}\\b`, "i").test(lowerAnswer)) {
15162
+ const brand = brandLabelFromDomain(cd);
15163
+ if (brand.length >= 4 && new RegExp(`\\b${escapeRegExp5(brand)}\\b`, "i").test(lowerAnswer)) {
15139
15164
  overlapSet.add(cd);
15140
15165
  }
15141
15166
  }
15142
15167
  }
15143
15168
  return [...overlapSet];
15144
15169
  }
15170
+ function escapeRegExp5(value) {
15171
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
15172
+ }
15145
15173
  function extractRecommendedCompetitors(answerText, ownDomains, citedDomains, competitorDomains) {
15146
15174
  if (!answerText || answerText.length < 20) return [];
15147
15175
  const ownBrandKeys = new Set(
@@ -15211,15 +15239,17 @@ function cleanCandidateName(candidate) {
15211
15239
  return candidate.replace(/^[\s"'`]+|[\s"'`.,:;!?]+$/g, "").replace(/\s+/g, " ").trim();
15212
15240
  }
15213
15241
  function collectBrandKeysFromDomain(domain) {
15214
- const hostname = normalizeProjectDomain(domain).split("/")[0] ?? "";
15215
- const labels = hostname.split(".").filter(Boolean);
15216
- const keys = /* @__PURE__ */ new Set();
15217
- const hostnameKey = hostname.replace(/[^a-z0-9]/gi, "").toLowerCase();
15218
- if (hostnameKey.length >= 4) keys.add(hostnameKey);
15219
- for (const label of labels) {
15220
- const key = label.replace(/[^a-z0-9]/gi, "").toLowerCase();
15221
- if (key.length >= 4) keys.add(key);
15242
+ const reg = registrableDomain(domain);
15243
+ if (!reg) {
15244
+ const hostname = normalizeProjectDomain(domain).split("/")[0] ?? "";
15245
+ const fallback = hostname.replace(/[^a-z0-9]/gi, "").toLowerCase();
15246
+ return fallback.length >= 4 ? [fallback] : [];
15222
15247
  }
15248
+ const keys = /* @__PURE__ */ new Set();
15249
+ const fullKey = reg.replace(/[^a-z0-9]/gi, "").toLowerCase();
15250
+ if (fullKey.length >= 4) keys.add(fullKey);
15251
+ const brand = brandLabelFromDomain(reg).replace(/[^a-z0-9]/gi, "").toLowerCase();
15252
+ if (brand.length >= 4) keys.add(brand);
15223
15253
  return [...keys];
15224
15254
  }
15225
15255
  function matchesBrandKey(candidateKey, brandKeys) {
@@ -18398,7 +18428,7 @@ var SnapshotService = class {
18398
18428
  });
18399
18429
  const enrichedResults = applyBatchAssessment(queryResults, batchAssessment);
18400
18430
  const summary = buildSnapshotSummary(companyName, profile.phrases, providers, enrichedResults, audit, batchAssessment);
18401
- const reportCompetitors = uniqueStrings2([
18431
+ const reportCompetitors = uniqueStrings([
18402
18432
  ...manualCompetitors,
18403
18433
  ...summary.topCompetitors.map((entry) => entry.name)
18404
18434
  ]);
@@ -18451,8 +18481,8 @@ var SnapshotService = class {
18451
18481
  return {
18452
18482
  industry: parsed.industry?.trim() || "Unknown",
18453
18483
  summary: parsed.summary?.trim() || ctx.audit.summary,
18454
- services: uniqueStrings2(parsed.services ?? []).slice(0, 6),
18455
- categoryTerms: uniqueStrings2(parsed.categoryTerms ?? []).slice(0, 8),
18484
+ services: uniqueStrings(parsed.services ?? []).slice(0, 6),
18485
+ categoryTerms: uniqueStrings(parsed.categoryTerms ?? []).slice(0, 8),
18456
18486
  phrases: parsedPhrases
18457
18487
  };
18458
18488
  } catch (err) {
@@ -18520,9 +18550,9 @@ var SnapshotService = class {
18520
18550
  accuracyNotes: null,
18521
18551
  incorrectClaims: [],
18522
18552
  recommendedCompetitors: preliminaryCompetitors,
18523
- citedDomains: uniqueStrings2(normalized.citedDomains),
18553
+ citedDomains: uniqueStrings(normalized.citedDomains),
18524
18554
  groundingSources: normalized.groundingSources,
18525
- searchQueries: uniqueStrings2(normalized.searchQueries),
18555
+ searchQueries: uniqueStrings(normalized.searchQueries),
18526
18556
  answerText: normalized.answerText,
18527
18557
  error: null
18528
18558
  };
@@ -18588,14 +18618,14 @@ var SnapshotService = class {
18588
18618
  mentioned: assessment.mentioned,
18589
18619
  describedAccurately: assessment.describedAccurately,
18590
18620
  accuracyNotes: assessment.accuracyNotes ?? null,
18591
- incorrectClaims: uniqueStrings2(assessment.incorrectClaims ?? []).slice(0, 5),
18621
+ incorrectClaims: uniqueStrings(assessment.incorrectClaims ?? []).slice(0, 5),
18592
18622
  ...hasReviewedCompetitors ? {
18593
- recommendedCompetitors: uniqueStrings2(assessment.recommendedCompetitors ?? []).slice(0, 10)
18623
+ recommendedCompetitors: uniqueStrings(assessment.recommendedCompetitors ?? []).slice(0, 10)
18594
18624
  } : {}
18595
18625
  };
18596
18626
  }),
18597
- whatThisMeans: uniqueStrings2(parsed.whatThisMeans ?? []).slice(0, 4),
18598
- recommendedActions: uniqueStrings2(parsed.recommendedActions ?? []).slice(0, 4)
18627
+ whatThisMeans: uniqueStrings(parsed.whatThisMeans ?? []).slice(0, 4),
18628
+ recommendedActions: uniqueStrings(parsed.recommendedActions ?? []).slice(0, 4)
18599
18629
  };
18600
18630
  } catch (err) {
18601
18631
  log12.warn("response.analysis-failed", {
@@ -18690,13 +18720,13 @@ function applyBatchAssessment(queryResults, batchAssessment) {
18690
18720
  };
18691
18721
  }
18692
18722
  const reviewedCompetitors = assessment.recommendedCompetitors;
18693
- const recommendedCompetitors = reviewedCompetitors !== void 0 ? uniqueStrings2(reviewedCompetitors) : result.recommendedCompetitors;
18723
+ const recommendedCompetitors = reviewedCompetitors !== void 0 ? uniqueStrings(reviewedCompetitors) : result.recommendedCompetitors;
18694
18724
  return {
18695
18725
  ...result,
18696
18726
  mentioned: result.mentioned || assessment.mentioned === true,
18697
18727
  describedAccurately: assessment.describedAccurately ?? (result.mentioned ? "unknown" : "not-mentioned"),
18698
18728
  accuracyNotes: assessment.accuracyNotes ?? result.accuracyNotes ?? null,
18699
- incorrectClaims: uniqueStrings2([
18729
+ incorrectClaims: uniqueStrings([
18700
18730
  ...result.incorrectClaims,
18701
18731
  ...assessment.incorrectClaims ?? []
18702
18732
  ]),
@@ -18726,7 +18756,7 @@ function buildSnapshotSummary(companyName, phrases, providers, queryResults, aud
18726
18756
  const whatThisMeans = batchAssessment.whatThisMeans.length > 0 ? batchAssessment.whatThisMeans : [
18727
18757
  defaultMeaning
18728
18758
  ];
18729
- const combinedWhatThisMeans = uniqueStrings2([...whatThisMeans, ...failureNote]).slice(0, 5);
18759
+ const combinedWhatThisMeans = uniqueStrings([...whatThisMeans, ...failureNote]).slice(0, 5);
18730
18760
  const recommendedActions = batchAssessment.recommendedActions.length > 0 ? batchAssessment.recommendedActions : buildFallbackRecommendedActions(audit);
18731
18761
  return {
18732
18762
  totalQueries: phrases.length,
@@ -18769,7 +18799,7 @@ function buildFallbackRecommendedActions(audit) {
18769
18799
  "Add machine-readable trust signals such as schema, FAQs, and llms.txt support.",
18770
18800
  "Build comparison and proof content that makes the category fit unmistakable."
18771
18801
  ];
18772
- return uniqueStrings2([...weakestFactors, ...defaults]).slice(0, 4);
18802
+ return uniqueStrings([...weakestFactors, ...defaults]).slice(0, 4);
18773
18803
  }
18774
18804
  function citesTargetDomain(citedDomains, groundingSources, targetDomain) {
18775
18805
  const normalizedTarget = extractHostname2(targetDomain);
@@ -18843,11 +18873,11 @@ function parseJsonObject(input) {
18843
18873
  }
18844
18874
  function normalizeStringList(values) {
18845
18875
  const items = values.flatMap((value) => value.split(","));
18846
- return uniqueStrings2(
18876
+ return uniqueStrings(
18847
18877
  items.map((value) => value.trim()).filter(Boolean)
18848
18878
  );
18849
18879
  }
18850
- function uniqueStrings2(values) {
18880
+ function uniqueStrings(values) {
18851
18881
  if (!Array.isArray(values)) return [];
18852
18882
  return [...new Set(
18853
18883
  values.filter((value) => typeof value === "string").map((value) => value.trim()).filter(Boolean)