@skill-map/cli 0.31.0 → 0.33.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.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // cli/entry.ts
2
- import { existsSync as existsSync30 } from "fs";
2
+ import { existsSync as existsSync33 } from "fs";
3
3
  import { Builtins, Cli as Cli2 } from "clipanion";
4
4
 
5
5
  // kernel/adapters/in-memory-progress.ts
@@ -494,6 +494,31 @@ var claudeProvider = {
494
494
  colorDark: "#34d399",
495
495
  icon: { kind: "pi", id: "pi-bolt" }
496
496
  }
497
+ },
498
+ /**
499
+ * Phase 5 of the active-lens migration: MCP servers surface as
500
+ * synthetic / virtual nodes (no filesystem path; identifier is
501
+ * `mcp://<name>`) derived from a config file (`settings.json`
502
+ * for Claude). Per-skill / per-agent references appear as
503
+ * `tools: [mcp__<server>__<tool>, ...]` entries in frontmatter;
504
+ * the `core/mcp-tools` extractor turns each match into one
505
+ * MCP node (idempotent dedup by path) plus a `references` link
506
+ * from the source skill / agent to that node.
507
+ *
508
+ * `schema` is provider-agnostic enough that we reuse the skill
509
+ * schema for now (mcp nodes have `name` + `description` at most,
510
+ * which the base skill schema accepts). A dedicated schema lands
511
+ * if MCP nodes grow Claude-specific metadata.
512
+ */
513
+ mcp: {
514
+ schema: "./schemas/skill.schema.json",
515
+ schemaJson: skill_schema_default,
516
+ ui: {
517
+ label: "MCP servers",
518
+ color: "#8b5cf6",
519
+ colorDark: "#a78bfa",
520
+ icon: { kind: "pi", id: "pi-server" }
521
+ }
497
522
  }
498
523
  },
499
524
  // Auxiliary schemas the per-kind schemas $ref by $id. AJV needs them
@@ -511,6 +536,166 @@ var claudeProvider = {
511
536
  }
512
537
  };
513
538
 
539
+ // kernel/util/strip-code-blocks.ts
540
+ var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
541
+ function stripCodeBlocks(input) {
542
+ if (!input) return input;
543
+ const fenceless = stripFences(input);
544
+ return stripInline(fenceless);
545
+ }
546
+ function stripFences(input) {
547
+ const out = [];
548
+ const lines = input.split("\n");
549
+ let openFence = null;
550
+ for (const line of lines) {
551
+ if (openFence) {
552
+ const closer = matchClosingFence(line, openFence);
553
+ if (closer) {
554
+ out.push(blank(line));
555
+ openFence = null;
556
+ } else {
557
+ out.push(blank(line));
558
+ }
559
+ continue;
560
+ }
561
+ const open = FENCE_RE.exec(line);
562
+ if (open?.groups) {
563
+ openFence = open.groups["fence"];
564
+ out.push(blank(line));
565
+ continue;
566
+ }
567
+ out.push(line);
568
+ }
569
+ return out.join("\n");
570
+ }
571
+ function matchClosingFence(line, openFence) {
572
+ const m = FENCE_RE.exec(line);
573
+ if (!m?.groups) return false;
574
+ const fence = m.groups["fence"];
575
+ return fence[0] === openFence[0] && fence.length >= openFence.length;
576
+ }
577
+ function stripInline(input) {
578
+ return input.replace(/(`+)([\s\S]*?)\1/g, (_full, ticks, body) => {
579
+ return ticks.replace(/`/g, " ") + blank(body) + ticks.replace(/`/g, " ");
580
+ });
581
+ }
582
+ function blank(s) {
583
+ return s.replace(/[^\s]/g, " ");
584
+ }
585
+
586
+ // kernel/trigger-normalize.ts
587
+ function normalizeTrigger(source) {
588
+ let out = source.normalize("NFD");
589
+ out = out.replace(new RegExp("\\p{Mn}+", "gu"), "");
590
+ out = out.toLowerCase();
591
+ out = out.replace(/[-_\s]+/g, " ");
592
+ out = out.replace(/ +/g, " ");
593
+ return out.trim();
594
+ }
595
+
596
+ // plugins/claude/extractors/at-directive/index.ts
597
+ var ID = "at-directive";
598
+ var AT_RE = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
599
+ var FILE_EXT_RE = /\.(md|mdx|js|jsx|ts|tsx|json|yml|yaml|toml|txt|html|css|scss|less|py|rb|go|rs|java|c|cpp|h|hpp|sh|sql|svg|png|jpg|jpeg|gif|webp|pdf)$/i;
600
+ var atDirectiveExtractor = {
601
+ id: ID,
602
+ pluginId: "claude",
603
+ kind: "extractor",
604
+ version: "1.0.0",
605
+ description: "Detects `@<token>` directives in a node's body using Claude Code interpretation rules. A bare handle (e.g. `@team`) becomes a `mentions` link; a file-flavoured token (e.g. `@docs/api.md`, `@./readme.md`) becomes a `references` link. Gated by `precondition.provider: ['claude']` so Gemini / Cursor / Codex apply their own at-directive flavours via their own extractors.",
606
+ scope: "body",
607
+ precondition: { provider: ["claude"] },
608
+ extract(ctx) {
609
+ const seenMentions = /* @__PURE__ */ new Set();
610
+ const seenReferences = /* @__PURE__ */ new Set();
611
+ const body = stripCodeBlocks(ctx.body);
612
+ for (const match of body.matchAll(AT_RE)) {
613
+ const original = match[1];
614
+ const bare = original.slice(1);
615
+ const isReference = bare.startsWith("./") || bare.startsWith("../") || bare.startsWith("/") || FILE_EXT_RE.test(bare);
616
+ if (isReference) {
617
+ const target = bare.replace(/^\.\//, "");
618
+ if (seenReferences.has(target)) continue;
619
+ seenReferences.add(target);
620
+ ctx.emitLink({
621
+ source: ctx.node.path,
622
+ target,
623
+ kind: "references",
624
+ // 0.85: strong file signal (path prefix `./` / `../` / `/` OR
625
+ // a known file extension on the tail). One degree of inference
626
+ // (the runtime still resolves the path).
627
+ confidence: 0.85,
628
+ sources: [ID],
629
+ trigger: {
630
+ originalTrigger: original,
631
+ normalizedTrigger: target.toLowerCase()
632
+ }
633
+ });
634
+ continue;
635
+ }
636
+ const normalized = normalizeTrigger(original);
637
+ if (seenMentions.has(normalized)) continue;
638
+ seenMentions.add(normalized);
639
+ ctx.emitLink({
640
+ source: ctx.node.path,
641
+ target: original,
642
+ kind: "mentions",
643
+ // 0.5: genuine ambiguity. A bare `@handle` (no extension, no
644
+ // path prefix) could be an agent, a handle, or generic prose.
645
+ // The runtime decides at invocation time; the extractor leaves
646
+ // the question open.
647
+ confidence: 0.5,
648
+ sources: [ID],
649
+ trigger: {
650
+ originalTrigger: original,
651
+ normalizedTrigger: normalized
652
+ }
653
+ });
654
+ }
655
+ }
656
+ };
657
+
658
+ // plugins/claude/extractors/slash/index.ts
659
+ var ID2 = "slash";
660
+ var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
661
+ var slashExtractor = {
662
+ id: ID2,
663
+ pluginId: "claude",
664
+ kind: "extractor",
665
+ version: "1.0.0",
666
+ description: "Detects `/command` invocations in a node's body using Claude Code routing rules and turns each one into an arrow between nodes in the graph. Gated by `precondition.provider: ['claude']` so Gemini / Cursor / Codex apply their own slash flavours (Gemini has 4 routing separators, Codex deprecated user slash commands, etc.) via their own extractors.",
667
+ scope: "body",
668
+ precondition: { provider: ["claude"] },
669
+ extract(ctx) {
670
+ const seen = /* @__PURE__ */ new Set();
671
+ const body = stripCodeBlocks(ctx.body);
672
+ for (const match of body.matchAll(SLASH_RE)) {
673
+ const original = match[1];
674
+ const endIdx = (match.index ?? 0) + match[0].length;
675
+ const nextChar = body[endIdx];
676
+ if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
677
+ const normalized = normalizeTrigger(original);
678
+ if (seen.has(normalized)) continue;
679
+ seen.add(normalized);
680
+ ctx.emitLink({
681
+ source: ctx.node.path,
682
+ target: original,
683
+ kind: "invokes",
684
+ // 0.8: clean `/command` match after code-block strip. The
685
+ // post-match path guard above filters URL / file-path noise,
686
+ // so a hit is unambiguous syntax. Resolution against the live
687
+ // skill / command catalog happens downstream.
688
+ confidence: 0.8,
689
+ sources: [ID2],
690
+ trigger: {
691
+ originalTrigger: original,
692
+ normalizedTrigger: normalized
693
+ }
694
+ });
695
+ }
696
+ }
697
+ };
698
+
514
699
  // plugins/gemini/providers/gemini/schemas/agent.schema.json
515
700
  var agent_schema_default2 = {
516
701
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -629,6 +814,86 @@ var geminiProvider = {
629
814
  }
630
815
  };
631
816
 
817
+ // plugins/openai/providers/openai/schemas/agent.schema.json
818
+ var agent_schema_default3 = {
819
+ $schema: "https://json-schema.org/draft/2020-12/schema",
820
+ $id: "https://skill-map.dev/providers/openai/v1/frontmatter/agent.schema.json",
821
+ title: "FrontmatterCodexAgent",
822
+ description: "Frontmatter shape for nodes classified as `agent` by the OpenAI Codex Provider. Codex sub-agents live as TOML files under `.codex/agents/<name>.toml`; the entire file IS the agent definition (no markdown body). The TOML parser feeds the parsed root object into `frontmatter`, so this schema validates the same shape skill-map's other providers carry on per-kind frontmatter. Mirrors Codex's documented sub-agent fields (https://github.com/openai/codex) with `additionalProperties: true` so future additions flow through unchanged.",
823
+ allOf: [
824
+ { $ref: "https://skill-map.dev/spec/v0/frontmatter/base.schema.json" }
825
+ ],
826
+ type: "object",
827
+ additionalProperties: true,
828
+ properties: {
829
+ name: {
830
+ type: "string",
831
+ minLength: 1,
832
+ description: "Sub-agent identifier. Conventionally matches the filename stem."
833
+ },
834
+ description: {
835
+ type: "string",
836
+ description: "Short description of when this sub-agent applies. Codex surfaces this in the agent picker; skill-map mirrors it in the card."
837
+ },
838
+ model: {
839
+ type: "string",
840
+ description: "Model identifier (`gpt-4o`, `o3-mini`, etc.) the sub-agent runs against."
841
+ },
842
+ instructions: {
843
+ type: "string",
844
+ description: "Multi-line prompt body (TOML triple-quoted string)."
845
+ },
846
+ tools: {
847
+ type: "array",
848
+ items: { type: "string" },
849
+ description: "Tool ids this sub-agent is allowed to call."
850
+ },
851
+ mcp_servers: {
852
+ type: "array",
853
+ items: { type: "string" },
854
+ description: "MCP server ids attached to this sub-agent (`tools: [mcp__<server>__*]` follows the same pattern as Claude)."
855
+ },
856
+ approval_policy: {
857
+ type: "string",
858
+ enum: ["never", "on-request", "untrusted"],
859
+ description: "Codex approval policy for this sub-agent's destructive operations."
860
+ },
861
+ sandbox_mode: {
862
+ type: "string",
863
+ enum: ["read-only", "workspace-write", "danger-full-access"],
864
+ description: "Codex sandbox mode the sub-agent runs under."
865
+ }
866
+ }
867
+ };
868
+
869
+ // plugins/openai/providers/openai/index.ts
870
+ var openaiProvider = {
871
+ id: "openai",
872
+ pluginId: "openai",
873
+ kind: "provider",
874
+ version: "1.0.0",
875
+ description: "Walks OpenAI Codex CLI scope conventions (.codex/agents/*.toml).",
876
+ read: { extensions: [".toml"], parser: "toml" },
877
+ kinds: {
878
+ agent: {
879
+ schema: "./schemas/agent.schema.json",
880
+ schemaJson: agent_schema_default3,
881
+ ui: {
882
+ label: "Codex agents",
883
+ // Codex green; distinct from claude / gemini palettes.
884
+ color: "#22c55e",
885
+ colorDark: "#4ade80",
886
+ icon: { kind: "pi", id: "pi-bolt" }
887
+ }
888
+ }
889
+ },
890
+ classify(path) {
891
+ const lower = path.toLowerCase();
892
+ if (lower.startsWith(".codex/agents/") && lower.endsWith(".toml")) return "agent";
893
+ return null;
894
+ }
895
+ };
896
+
632
897
  // plugins/agent-skills/providers/agent-skills/schemas/skill.schema.json
633
898
  var skill_schema_default3 = {
634
899
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -722,9 +987,9 @@ var coreMarkdownProvider = {
722
987
  };
723
988
 
724
989
  // plugins/core/extractors/annotations/index.ts
725
- var ID = "annotations";
990
+ var ID3 = "annotations";
726
991
  var annotationsExtractor = {
727
- id: ID,
992
+ id: ID3,
728
993
  pluginId: "core",
729
994
  kind: "extractor",
730
995
  version: "1.0.0",
@@ -751,147 +1016,36 @@ function processBlock(block, sourcePath, emit) {
751
1016
  if (typeof supersededBy === "string" && supersededBy.length > 0) {
752
1017
  emit(supersededBy, sourcePath);
753
1018
  }
754
- }
755
- function pickAnnotations(node) {
756
- const sidecar = node.sidecar;
757
- if (!sidecar || sidecar.present !== true) return null;
758
- const ann = sidecar.annotations;
759
- if (ann && typeof ann === "object" && !Array.isArray(ann)) {
760
- return ann;
761
- }
762
- return null;
763
- }
764
- function stringArray(value) {
765
- if (!Array.isArray(value)) return [];
766
- return value.filter((v) => typeof v === "string" && v.length > 0);
767
- }
768
- function link(source, target) {
769
- return {
770
- source,
771
- target,
772
- kind: "supersedes",
773
- confidence: "high",
774
- sources: [ID]
775
- };
776
- }
777
-
778
- // kernel/util/strip-code-blocks.ts
779
- var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
780
- function stripCodeBlocks(input) {
781
- if (!input) return input;
782
- const fenceless = stripFences(input);
783
- return stripInline(fenceless);
784
- }
785
- function stripFences(input) {
786
- const out = [];
787
- const lines = input.split("\n");
788
- let openFence = null;
789
- for (const line of lines) {
790
- if (openFence) {
791
- const closer = matchClosingFence(line, openFence);
792
- if (closer) {
793
- out.push(blank(line));
794
- openFence = null;
795
- } else {
796
- out.push(blank(line));
797
- }
798
- continue;
799
- }
800
- const open = FENCE_RE.exec(line);
801
- if (open?.groups) {
802
- openFence = open.groups["fence"];
803
- out.push(blank(line));
804
- continue;
805
- }
806
- out.push(line);
807
- }
808
- return out.join("\n");
809
- }
810
- function matchClosingFence(line, openFence) {
811
- const m = FENCE_RE.exec(line);
812
- if (!m?.groups) return false;
813
- const fence = m.groups["fence"];
814
- return fence[0] === openFence[0] && fence.length >= openFence.length;
815
- }
816
- function stripInline(input) {
817
- return input.replace(/(`+)([\s\S]*?)\1/g, (_full, ticks, body) => {
818
- return ticks.replace(/`/g, " ") + blank(body) + ticks.replace(/`/g, " ");
819
- });
820
- }
821
- function blank(s) {
822
- return s.replace(/[^\s]/g, " ");
823
- }
824
-
825
- // kernel/trigger-normalize.ts
826
- function normalizeTrigger(source) {
827
- let out = source.normalize("NFD");
828
- out = out.replace(new RegExp("\\p{Mn}+", "gu"), "");
829
- out = out.toLowerCase();
830
- out = out.replace(/[-_\s]+/g, " ");
831
- out = out.replace(/ +/g, " ");
832
- return out.trim();
833
- }
834
-
835
- // plugins/core/extractors/at-directive/index.ts
836
- var ID2 = "at-directive";
837
- var AT_RE = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
838
- var FILE_EXT_RE = /\.(md|mdx|js|jsx|ts|tsx|json|yml|yaml|toml|txt|html|css|scss|less|py|rb|go|rs|java|c|cpp|h|hpp|sh|sql|svg|png|jpg|jpeg|gif|webp|pdf)$/i;
839
- var atDirectiveExtractor = {
840
- id: ID2,
841
- pluginId: "core",
842
- kind: "extractor",
843
- version: "1.0.0",
844
- description: "Detects `@<token>` directives in a node's body. A bare handle (e.g. `@team`) becomes a `mentions` link; a file-flavoured token (e.g. `@docs/api.md`, `@./readme.md`) becomes a `references` link, matching how Claude Code / Gemini CLI / Cursor read the same syntax.",
845
- scope: "body",
846
- extract(ctx) {
847
- const seenMentions = /* @__PURE__ */ new Set();
848
- const seenReferences = /* @__PURE__ */ new Set();
849
- const body = stripCodeBlocks(ctx.body);
850
- for (const match of body.matchAll(AT_RE)) {
851
- const original = match[1];
852
- const bare = original.slice(1);
853
- const isReference = bare.startsWith("./") || bare.startsWith("../") || bare.startsWith("/") || FILE_EXT_RE.test(bare);
854
- if (isReference) {
855
- const target = bare.replace(/^\.\//, "");
856
- if (seenReferences.has(target)) continue;
857
- seenReferences.add(target);
858
- ctx.emitLink({
859
- source: ctx.node.path,
860
- target,
861
- kind: "references",
862
- confidence: "medium",
863
- sources: [ID2],
864
- trigger: {
865
- originalTrigger: original,
866
- normalizedTrigger: target.toLowerCase()
867
- }
868
- });
869
- continue;
870
- }
871
- const normalized = normalizeTrigger(original);
872
- if (seenMentions.has(normalized)) continue;
873
- seenMentions.add(normalized);
874
- ctx.emitLink({
875
- source: ctx.node.path,
876
- target: original,
877
- kind: "mentions",
878
- confidence: "medium",
879
- sources: [ID2],
880
- trigger: {
881
- originalTrigger: original,
882
- normalizedTrigger: normalized
883
- }
884
- });
885
- }
886
- }
887
- };
1019
+ }
1020
+ function pickAnnotations(node) {
1021
+ const sidecar = node.sidecar;
1022
+ if (!sidecar || sidecar.present !== true) return null;
1023
+ const ann = sidecar.annotations;
1024
+ if (ann && typeof ann === "object" && !Array.isArray(ann)) {
1025
+ return ann;
1026
+ }
1027
+ return null;
1028
+ }
1029
+ function stringArray(value) {
1030
+ if (!Array.isArray(value)) return [];
1031
+ return value.filter((v) => typeof v === "string" && v.length > 0);
1032
+ }
1033
+ function link(source, target) {
1034
+ return {
1035
+ source,
1036
+ target,
1037
+ kind: "supersedes",
1038
+ confidence: 1,
1039
+ sources: [ID3]
1040
+ };
1041
+ }
888
1042
 
889
1043
  // plugins/core/extractors/external-url-counter/index.ts
890
- var ID3 = "external-url-counter";
1044
+ var ID4 = "external-url-counter";
891
1045
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
892
1046
  var TRAILING_PUNCT = /[.,;:!?]+$/;
893
1047
  var externalUrlCounterExtractor = {
894
- id: ID3,
1048
+ id: ID4,
895
1049
  pluginId: "core",
896
1050
  kind: "extractor",
897
1051
  version: "1.0.0",
@@ -937,8 +1091,8 @@ var externalUrlCounterExtractor = {
937
1091
  source: ctx.node.path,
938
1092
  target: normalized,
939
1093
  kind: "references",
940
- confidence: "low",
941
- sources: [ID3],
1094
+ confidence: 0.3,
1095
+ sources: [ID4],
942
1096
  trigger: {
943
1097
  originalTrigger: original,
944
1098
  normalizedTrigger: normalized
@@ -985,11 +1139,11 @@ function lineFor(lineStarts, offset) {
985
1139
 
986
1140
  // plugins/core/extractors/markdown-link/index.ts
987
1141
  import { posix as pathPosix } from "path";
988
- var ID4 = "markdown-link";
1142
+ var ID5 = "markdown-link";
989
1143
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
990
1144
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
991
1145
  var markdownLinkExtractor = {
992
- id: ID4,
1146
+ id: ID5,
993
1147
  pluginId: "core",
994
1148
  kind: "extractor",
995
1149
  version: "1.0.0",
@@ -1010,8 +1164,8 @@ var markdownLinkExtractor = {
1010
1164
  source: ctx.node.path,
1011
1165
  target: resolved,
1012
1166
  kind: "references",
1013
- confidence: "high",
1014
- sources: [ID4],
1167
+ confidence: 0.95,
1168
+ sources: [ID5],
1015
1169
  trigger: {
1016
1170
  originalTrigger: original,
1017
1171
  normalizedTrigger: resolved
@@ -1050,47 +1204,61 @@ function lineFor2(lineStarts, offset) {
1050
1204
  return lo + 1;
1051
1205
  }
1052
1206
 
1053
- // plugins/core/extractors/slash/index.ts
1054
- var ID5 = "slash";
1055
- var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
1056
- var slashExtractor = {
1057
- id: ID5,
1207
+ // plugins/core/extractors/mcp-tools/index.ts
1208
+ var ID6 = "mcp-tools";
1209
+ var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
1210
+ var mcpToolsExtractor = {
1211
+ id: ID6,
1058
1212
  pluginId: "core",
1059
1213
  kind: "extractor",
1060
1214
  version: "1.0.0",
1061
- description: "Detects `/command` invocations in a node's body and turns each one into an arrow between nodes in the graph.",
1062
- scope: "body",
1215
+ description: "Detects `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter and turns each unique server into an MCP node + a reference edge from the source.",
1216
+ scope: "frontmatter",
1063
1217
  extract(ctx) {
1064
- const seen = /* @__PURE__ */ new Set();
1065
- const body = stripCodeBlocks(ctx.body);
1066
- for (const match of body.matchAll(SLASH_RE)) {
1067
- const original = match[1];
1068
- const endIdx = (match.index ?? 0) + match[0].length;
1069
- const nextChar = body[endIdx];
1070
- if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
1071
- const normalized = normalizeTrigger(original);
1072
- if (seen.has(normalized)) continue;
1073
- seen.add(normalized);
1218
+ const raw = ctx.frontmatter["tools"];
1219
+ if (!Array.isArray(raw)) return;
1220
+ const servers = collectMcpServers(raw);
1221
+ if (servers.size === 0) return;
1222
+ for (const server of servers) {
1223
+ const mcpPath = `mcp://${server}`;
1224
+ ctx.emitNode({
1225
+ path: mcpPath,
1226
+ kind: "mcp",
1227
+ virtual: true,
1228
+ provider: ctx.node.provider,
1229
+ derivedFrom: [ctx.node.path],
1230
+ frontmatter: { name: server }
1231
+ });
1074
1232
  ctx.emitLink({
1075
1233
  source: ctx.node.path,
1076
- target: original,
1077
- kind: "invokes",
1078
- confidence: "medium",
1079
- sources: [ID5],
1234
+ target: mcpPath,
1235
+ kind: "references",
1236
+ confidence: 0.85,
1237
+ sources: [ID6],
1080
1238
  trigger: {
1081
- originalTrigger: original,
1082
- normalizedTrigger: normalized
1239
+ originalTrigger: `mcp__${server}__*`,
1240
+ normalizedTrigger: mcpPath
1083
1241
  }
1084
1242
  });
1085
1243
  }
1086
1244
  }
1087
1245
  };
1246
+ function collectMcpServers(tools) {
1247
+ const out = /* @__PURE__ */ new Set();
1248
+ for (const t of tools) {
1249
+ if (typeof t !== "string" || t.length === 0) continue;
1250
+ const match = MCP_PATTERN.exec(t);
1251
+ if (!match) continue;
1252
+ out.add(match[1].toLowerCase());
1253
+ }
1254
+ return out;
1255
+ }
1088
1256
 
1089
1257
  // plugins/core/extractors/tools-count/index.ts
1090
- var ID6 = "tools-count";
1258
+ var ID7 = "tools-count";
1091
1259
  var TOOLTIP_MAX = 255;
1092
1260
  var toolsCountExtractor = {
1093
- id: ID6,
1261
+ id: ID7,
1094
1262
  pluginId: "core",
1095
1263
  kind: "extractor",
1096
1264
  version: "1.0.0",
@@ -1133,9 +1301,9 @@ var ANNOTATION_ORPHAN_TEXTS = {
1133
1301
  };
1134
1302
 
1135
1303
  // plugins/core/analyzers/annotation-orphan/index.ts
1136
- var ID7 = "annotation-orphan";
1304
+ var ID8 = "annotation-orphan";
1137
1305
  var annotationOrphanAnalyzer = {
1138
- id: ID7,
1306
+ id: ID8,
1139
1307
  pluginId: "core",
1140
1308
  kind: "analyzer",
1141
1309
  version: "1.0.0",
@@ -1148,7 +1316,7 @@ var annotationOrphanAnalyzer = {
1148
1316
  for (const orphan of orphans) {
1149
1317
  const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1150
1318
  issues.push({
1151
- analyzerId: ID7,
1319
+ analyzerId: ID8,
1152
1320
  severity: "warn",
1153
1321
  nodeIds: [expectedMdRelative],
1154
1322
  message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
@@ -1185,9 +1353,9 @@ var ANNOTATION_STALE_TEXTS = {
1185
1353
  };
1186
1354
 
1187
1355
  // plugins/core/analyzers/annotation-stale/index.ts
1188
- var ID8 = "annotation-stale";
1356
+ var ID9 = "annotation-stale";
1189
1357
  var annotationStaleAnalyzer = {
1190
- id: ID8,
1358
+ id: ID9,
1191
1359
  pluginId: "core",
1192
1360
  kind: "analyzer",
1193
1361
  version: "1.0.0",
@@ -1222,7 +1390,7 @@ var annotationStaleAnalyzer = {
1222
1390
  if (status === "fresh") continue;
1223
1391
  const message = status === "stale-body" ? tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path: node.path }) : status === "stale-frontmatter" ? tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path: node.path }) : tx(ANNOTATION_STALE_TEXTS.bothDrift, { path: node.path });
1224
1392
  issues.push({
1225
- analyzerId: ID8,
1393
+ analyzerId: ID9,
1226
1394
  severity: "warn",
1227
1395
  nodeIds: [node.path],
1228
1396
  message,
@@ -1268,9 +1436,9 @@ var BROKEN_REF_TEXTS = {
1268
1436
  };
1269
1437
 
1270
1438
  // plugins/core/analyzers/broken-ref/index.ts
1271
- var ID9 = "broken-ref";
1439
+ var ID10 = "broken-ref";
1272
1440
  var brokenRefAnalyzer = {
1273
- id: ID9,
1441
+ id: ID10,
1274
1442
  pluginId: "core",
1275
1443
  kind: "analyzer",
1276
1444
  version: "1.0.0",
@@ -1339,7 +1507,7 @@ function buildIssue(link2, hintCandidates = []) {
1339
1507
  trigger: link2.trigger?.normalizedTrigger ?? null
1340
1508
  };
1341
1509
  const issue = {
1342
- analyzerId: ID9,
1510
+ analyzerId: ID10,
1343
1511
  severity: "warn",
1344
1512
  nodeIds: [link2.source],
1345
1513
  message: tx(BROKEN_REF_TEXTS.message, {
@@ -1433,9 +1601,9 @@ function isPathStyleLink(link2) {
1433
1601
  }
1434
1602
 
1435
1603
  // plugins/core/analyzers/contribution-orphan/index.ts
1436
- var ID10 = "contribution-orphan";
1604
+ var ID11 = "contribution-orphan";
1437
1605
  var contributionOrphanAnalyzer = {
1438
- id: ID10,
1606
+ id: ID11,
1439
1607
  pluginId: "core",
1440
1608
  kind: "analyzer",
1441
1609
  version: "0.0.0",
@@ -1456,9 +1624,9 @@ var JOB_ORPHAN_FILE_TEXTS = {
1456
1624
  };
1457
1625
 
1458
1626
  // plugins/core/analyzers/job-orphan-file/index.ts
1459
- var ID11 = "job-orphan-file";
1627
+ var ID12 = "job-orphan-file";
1460
1628
  var jobOrphanFileAnalyzer = {
1461
- id: ID11,
1629
+ id: ID12,
1462
1630
  pluginId: "core",
1463
1631
  kind: "analyzer",
1464
1632
  version: "1.0.0",
@@ -1470,7 +1638,7 @@ var jobOrphanFileAnalyzer = {
1470
1638
  const issues = [];
1471
1639
  for (const filePath of orphans) {
1472
1640
  issues.push({
1473
- analyzerId: ID11,
1641
+ analyzerId: ID12,
1474
1642
  severity: "warn",
1475
1643
  nodeIds: [filePath],
1476
1644
  message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
@@ -1488,9 +1656,9 @@ var LINK_CONFLICT_TEXTS = {
1488
1656
  };
1489
1657
 
1490
1658
  // plugins/core/analyzers/link-conflict/index.ts
1491
- var ID12 = "link-conflict";
1659
+ var ID13 = "link-conflict";
1492
1660
  var linkConflictAnalyzer = {
1493
- id: ID12,
1661
+ id: ID13,
1494
1662
  pluginId: "core",
1495
1663
  kind: "analyzer",
1496
1664
  version: "1.0.0",
@@ -1538,7 +1706,7 @@ var linkConflictAnalyzer = {
1538
1706
  const [source, target] = key.split("\0");
1539
1707
  const kindList = variants.map((v) => v.kind).join(" / ");
1540
1708
  issues.push({
1541
- analyzerId: ID12,
1709
+ analyzerId: ID13,
1542
1710
  severity: "warn",
1543
1711
  nodeIds: [source, target],
1544
1712
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -1553,14 +1721,7 @@ var linkConflictAnalyzer = {
1553
1721
  }
1554
1722
  };
1555
1723
  function rankConfidence(c) {
1556
- switch (c) {
1557
- case "high":
1558
- return 2;
1559
- case "medium":
1560
- return 1;
1561
- case "low":
1562
- return 0;
1563
- }
1724
+ return c;
1564
1725
  }
1565
1726
 
1566
1727
  // kernel/util/trigger-resolve.ts
@@ -1612,9 +1773,9 @@ function resolveLinkTargetToPath(link2, nameIndex) {
1612
1773
  }
1613
1774
 
1614
1775
  // plugins/core/analyzers/link-counts/index.ts
1615
- var ID13 = "link-counts";
1776
+ var ID14 = "link-counts";
1616
1777
  var linkCountsAnalyzer = {
1617
- id: ID13,
1778
+ id: ID14,
1618
1779
  pluginId: "core",
1619
1780
  kind: "analyzer",
1620
1781
  version: "1.0.0",
@@ -1678,11 +1839,11 @@ function formatBreakdown(byKind, direction) {
1678
1839
  }
1679
1840
 
1680
1841
  // plugins/core/analyzers/stability/index.ts
1681
- var ID14 = "stability";
1842
+ var ID15 = "stability";
1682
1843
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
1683
1844
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
1684
1845
  var stabilityAnalyzer = {
1685
- id: ID14,
1846
+ id: ID15,
1686
1847
  pluginId: "core",
1687
1848
  kind: "analyzer",
1688
1849
  version: "1.0.0",
@@ -1714,7 +1875,7 @@ var stabilityAnalyzer = {
1714
1875
  tooltip: EXPERIMENTAL_TOOLTIP
1715
1876
  });
1716
1877
  issues.push({
1717
- analyzerId: ID14,
1878
+ analyzerId: ID15,
1718
1879
  severity: "info",
1719
1880
  nodeIds: [node.path],
1720
1881
  message: `Node '${node.path}' is marked experimental: API may change.`,
@@ -1727,7 +1888,7 @@ var stabilityAnalyzer = {
1727
1888
  severity: "warn"
1728
1889
  });
1729
1890
  issues.push({
1730
- analyzerId: ID14,
1891
+ analyzerId: ID15,
1731
1892
  severity: "warn",
1732
1893
  nodeIds: [node.path],
1733
1894
  message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
@@ -1761,9 +1922,9 @@ var SUPERSEDED_TEXTS = {
1761
1922
  };
1762
1923
 
1763
1924
  // plugins/core/analyzers/superseded/index.ts
1764
- var ID15 = "superseded";
1925
+ var ID16 = "superseded";
1765
1926
  var supersededAnalyzer = {
1766
- id: ID15,
1927
+ id: ID16,
1767
1928
  pluginId: "core",
1768
1929
  kind: "analyzer",
1769
1930
  version: "1.0.0",
@@ -1775,7 +1936,7 @@ var supersededAnalyzer = {
1775
1936
  const supersededBy = pickSupersededBy(node);
1776
1937
  if (supersededBy === null) continue;
1777
1938
  issues.push({
1778
- analyzerId: ID15,
1939
+ analyzerId: ID16,
1779
1940
  severity: "info",
1780
1941
  nodeIds: [node.path],
1781
1942
  message: tx(SUPERSEDED_TEXTS.message, {
@@ -1824,14 +1985,14 @@ var TRIGGER_COLLISION_TEXTS = {
1824
1985
  };
1825
1986
 
1826
1987
  // plugins/core/analyzers/trigger-collision/index.ts
1827
- var ID16 = "trigger-collision";
1988
+ var ID17 = "trigger-collision";
1828
1989
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
1829
1990
  "command",
1830
1991
  "skill",
1831
1992
  "agent"
1832
1993
  ]);
1833
1994
  var triggerCollisionAnalyzer = {
1834
- id: ID16,
1995
+ id: ID17,
1835
1996
  pluginId: "core",
1836
1997
  kind: "analyzer",
1837
1998
  mode: "deterministic",
@@ -1930,7 +2091,7 @@ function analyzeTriggerBucket(normalized, claims) {
1930
2091
  part: parts[0]
1931
2092
  });
1932
2093
  return {
1933
- analyzerId: ID16,
2094
+ analyzerId: ID17,
1934
2095
  severity: "error",
1935
2096
  nodeIds,
1936
2097
  message,
@@ -1970,10 +2131,10 @@ var UNKNOWN_FIELD_TEXTS = {
1970
2131
  };
1971
2132
 
1972
2133
  // plugins/core/analyzers/unknown-field/index.ts
1973
- var ID17 = "unknown-field";
2134
+ var ID18 = "unknown-field";
1974
2135
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1975
2136
  var unknownFieldAnalyzer = {
1976
- id: ID17,
2137
+ id: ID18,
1977
2138
  pluginId: "core",
1978
2139
  kind: "analyzer",
1979
2140
  version: "1.0.0",
@@ -2031,7 +2192,7 @@ var unknownFieldAnalyzer = {
2031
2192
  for (const key of Object.keys(annotations)) {
2032
2193
  if (!knownAnnotationKeys.has(key)) {
2033
2194
  issues.push({
2034
- analyzerId: ID17,
2195
+ analyzerId: ID18,
2035
2196
  severity: "warn",
2036
2197
  nodeIds: [node.path],
2037
2198
  message: tx(UNKNOWN_FIELD_TEXTS.unknownAnnotationKey, {
@@ -2058,7 +2219,7 @@ var unknownFieldAnalyzer = {
2058
2219
  if (validator(value)) continue;
2059
2220
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
2060
2221
  issues.push({
2061
- analyzerId: ID17,
2222
+ analyzerId: ID18,
2062
2223
  severity: "warn",
2063
2224
  nodeIds: [node.path],
2064
2225
  message: tx(UNKNOWN_FIELD_TEXTS.pluginNamespaceInvalid, {
@@ -2074,7 +2235,7 @@ var unknownFieldAnalyzer = {
2074
2235
  continue;
2075
2236
  }
2076
2237
  issues.push({
2077
- analyzerId: ID17,
2238
+ analyzerId: ID18,
2078
2239
  severity: "warn",
2079
2240
  nodeIds: [node.path],
2080
2241
  message: tx(UNKNOWN_FIELD_TEXTS.unknownRootKey, {
@@ -2367,9 +2528,9 @@ var VALIDATE_ALL_TEXTS = {
2367
2528
  };
2368
2529
 
2369
2530
  // plugins/core/analyzers/validate-all/index.ts
2370
- var ID18 = "validate-all";
2531
+ var ID19 = "validate-all";
2371
2532
  var validateAllAnalyzer = {
2372
- id: ID18,
2533
+ id: ID19,
2373
2534
  pluginId: "core",
2374
2535
  kind: "analyzer",
2375
2536
  version: "1.0.0",
@@ -2432,7 +2593,7 @@ function collectNodeFindings(v, node, out) {
2432
2593
  const result = v.validate("node", toNodeForSchema(node));
2433
2594
  if (result.ok) return;
2434
2595
  out.push({
2435
- analyzerId: ID18,
2596
+ analyzerId: ID19,
2436
2597
  severity: "error",
2437
2598
  nodeIds: [node.path],
2438
2599
  message: tx(VALIDATE_ALL_TEXTS.nodeFailure, {
@@ -2451,7 +2612,7 @@ function collectFrontmatterBaseFindings(node, out) {
2451
2612
  if (isMissingStringField(fm, "description")) missing.push("description");
2452
2613
  if (missing.length === 0) return;
2453
2614
  out.push({
2454
- analyzerId: ID18,
2615
+ analyzerId: ID19,
2455
2616
  // `warn` (not `error`) so the default `sm scan` exit code stays
2456
2617
  // 0 even when nodes are missing frontmatter base fields. Strict
2457
2618
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -2473,7 +2634,7 @@ function collectLinkFindings(v, link2, out) {
2473
2634
  const result = v.validate("link", toLinkForSchema(link2));
2474
2635
  if (result.ok) return;
2475
2636
  out.push({
2476
- analyzerId: ID18,
2637
+ analyzerId: ID19,
2477
2638
  severity: "error",
2478
2639
  nodeIds: [link2.source],
2479
2640
  message: tx(VALIDATE_ALL_TEXTS.linkFailure, {
@@ -2541,13 +2702,13 @@ var ASCII_FORMATTER_TEXTS = {
2541
2702
  };
2542
2703
 
2543
2704
  // plugins/core/formatters/ascii/index.ts
2544
- var ID19 = "ascii";
2705
+ var ID20 = "ascii";
2545
2706
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
2546
2707
  var asciiFormatter = {
2547
- id: ID19,
2708
+ id: ID20,
2548
2709
  pluginId: "core",
2549
2710
  kind: "formatter",
2550
- formatId: ID19,
2711
+ formatId: ID20,
2551
2712
  version: "1.0.0",
2552
2713
  description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
2553
2714
  // ASCII tree formatter, header + per-kind sections + per-issue
@@ -2642,14 +2803,14 @@ function renderSection(out, kind, group) {
2642
2803
  }
2643
2804
 
2644
2805
  // plugins/core/formatters/json/index.ts
2645
- var ID20 = "json";
2806
+ var ID21 = "json";
2646
2807
  var jsonFormatter = {
2647
- id: ID20,
2808
+ id: ID21,
2648
2809
  pluginId: "core",
2649
2810
  kind: "formatter",
2650
2811
  version: "1.0.0",
2651
2812
  description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json` when the full ScanResult is available). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
2652
- formatId: ID20,
2813
+ formatId: ID21,
2653
2814
  format(ctx) {
2654
2815
  if (ctx.scanResult !== void 0) {
2655
2816
  return JSON.stringify(ctx.scanResult);
@@ -2788,10 +2949,10 @@ function resolveSpecRoot2() {
2788
2949
  }
2789
2950
 
2790
2951
  // plugins/core/actions/bump/index.ts
2791
- var ID21 = "bump";
2952
+ var ID22 = "bump";
2792
2953
  var PLUGIN_ID = "core";
2793
2954
  var bumpAction = {
2794
- id: ID21,
2955
+ id: ID22,
2795
2956
  pluginId: PLUGIN_ID,
2796
2957
  kind: "action",
2797
2958
  version: "1.0.0",
@@ -2850,10 +3011,10 @@ function pickCurrentVersion(overlay) {
2850
3011
  }
2851
3012
 
2852
3013
  // plugins/core/actions/mark-superseded/index.ts
2853
- var ID22 = "mark-superseded";
3014
+ var ID23 = "mark-superseded";
2854
3015
  var PLUGIN_ID2 = "core";
2855
3016
  var markSupersededAction = {
2856
- id: ID22,
3017
+ id: ID23,
2857
3018
  pluginId: PLUGIN_ID2,
2858
3019
  kind: "action",
2859
3020
  version: "0.0.0",
@@ -2963,7 +3124,7 @@ var UPDATE_CHECK_TEXTS = {
2963
3124
  // package.json
2964
3125
  var package_default = {
2965
3126
  name: "@skill-map/cli",
2966
- version: "0.31.0",
3127
+ version: "0.33.0",
2967
3128
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2968
3129
  license: "MIT",
2969
3130
  type: "module",
@@ -3044,6 +3205,7 @@ var package_default = {
3044
3205
  "js-yaml": "4.1.1",
3045
3206
  kysely: "0.28.17",
3046
3207
  semver: "7.7.4",
3208
+ "smol-toml": "1.6.1",
3047
3209
  typanion: "3.14.0",
3048
3210
  ws: "8.20.0"
3049
3211
  },
@@ -3339,14 +3501,16 @@ var updateCheckHook = {
3339
3501
 
3340
3502
  // plugins/built-ins.ts
3341
3503
  var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
3504
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude" };
3505
+ var slashExtractor2 = { ...slashExtractor, pluginId: "claude" };
3342
3506
  var geminiProvider2 = { ...geminiProvider, pluginId: "gemini" };
3507
+ var openaiProvider2 = { ...openaiProvider, pluginId: "openai" };
3343
3508
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
3344
3509
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core" };
3345
3510
  var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
3346
- var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "core" };
3347
3511
  var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
3348
3512
  var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
3349
- var slashExtractor2 = { ...slashExtractor, pluginId: "core" };
3513
+ var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core" };
3350
3514
  var toolsCountExtractor2 = { ...toolsCountExtractor, pluginId: "core" };
3351
3515
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core" };
3352
3516
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core" };
@@ -3371,7 +3535,9 @@ var builtInBundles = [
3371
3535
  granularity: "bundle",
3372
3536
  description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and parses Claude-flavored frontmatter.",
3373
3537
  extensions: [
3374
- claudeProvider2
3538
+ claudeProvider2,
3539
+ atDirectiveExtractor2,
3540
+ slashExtractor2
3375
3541
  ]
3376
3542
  },
3377
3543
  {
@@ -3382,6 +3548,14 @@ var builtInBundles = [
3382
3548
  geminiProvider2
3383
3549
  ]
3384
3550
  },
3551
+ {
3552
+ id: "openai",
3553
+ granularity: "bundle",
3554
+ description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml` and (future) walks the hierarchical AGENTS.md cascade. Provider for the active-lens `openai` runtime.",
3555
+ extensions: [
3556
+ openaiProvider2
3557
+ ]
3558
+ },
3385
3559
  {
3386
3560
  id: "agent-skills",
3387
3561
  granularity: "bundle",
@@ -3397,10 +3571,9 @@ var builtInBundles = [
3397
3571
  extensions: [
3398
3572
  coreMarkdownProvider2,
3399
3573
  annotationsExtractor2,
3400
- atDirectiveExtractor2,
3401
3574
  externalUrlCounterExtractor2,
3402
3575
  markdownLinkExtractor2,
3403
- slashExtractor2,
3576
+ mcpToolsExtractor2,
3404
3577
  toolsCountExtractor2,
3405
3578
  annotationOrphanAnalyzer2,
3406
3579
  annotationStaleAnalyzer2,
@@ -3893,7 +4066,6 @@ var defaults_default = {
3893
4066
  watch: {
3894
4067
  debounceMs: 300
3895
4068
  },
3896
- extraFolders: [],
3897
4069
  referencePaths: []
3898
4070
  },
3899
4071
  plugins: {},
@@ -3919,7 +4091,6 @@ var defaults_default = {
3919
4091
  // kernel/config/loader.ts
3920
4092
  var PROJECT_LOCAL_ONLY_KEYS = /* @__PURE__ */ new Set([
3921
4093
  "allowEditSmFiles",
3922
- "scan.extraFolders",
3923
4094
  "scan.referencePaths"
3924
4095
  ]);
3925
4096
  var DEFAULTS = defaults_default;
@@ -4258,7 +4429,6 @@ function writeJsonAtomic(path, content) {
4258
4429
 
4259
4430
  // core/config/helper.ts
4260
4431
  var PRIVACY_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
4261
- "scan.extraFolders",
4262
4432
  "scan.referencePaths"
4263
4433
  ]);
4264
4434
  var ProjectLocalOnlyKeyError = class extends Error {
@@ -4330,19 +4500,17 @@ var ConfigValidationError = class extends Error {
4330
4500
  function projectPathExposure(inputs) {
4331
4501
  const empty = { expandsSurface: false, exposedPaths: [] };
4332
4502
  if (!PRIVACY_SENSITIVE_KEYS.has(inputs.key)) return empty;
4333
- if (inputs.key === "scan.extraFolders" || inputs.key === "scan.referencePaths") {
4334
- if (!Array.isArray(inputs.value)) return empty;
4335
- const before = readConfigValue(inputs.key, {
4336
- cwd: inputs.cwd,
4337
- default: []
4338
- }) ?? [];
4339
- const beforeSet = new Set(before);
4340
- const added = inputs.value.filter((entry) => typeof entry === "string").filter((entry) => !beforeSet.has(entry));
4341
- const exposed = added.map((entry) => resolveScanPathForExposure(entry, inputs.cwd)).filter((abs) => abs !== null && !isUnderProject(abs, inputs.cwd));
4342
- if (exposed.length === 0) return empty;
4343
- return { expandsSurface: true, exposedPaths: exposed };
4344
- }
4345
- return empty;
4503
+ if (inputs.key !== "scan.referencePaths") return empty;
4504
+ if (!Array.isArray(inputs.value)) return empty;
4505
+ const before = readConfigValue(inputs.key, {
4506
+ cwd: inputs.cwd,
4507
+ default: []
4508
+ }) ?? [];
4509
+ const beforeSet = new Set(before);
4510
+ const added = inputs.value.filter((entry) => typeof entry === "string").filter((entry) => !beforeSet.has(entry));
4511
+ const exposed = added.map((entry) => resolveScanPathForExposure(entry, inputs.cwd)).filter((abs) => abs !== null && !isUnderProject(abs, inputs.cwd));
4512
+ if (exposed.length === 0) return empty;
4513
+ return { expandsSurface: true, exposedPaths: exposed };
4346
4514
  }
4347
4515
  function resolveScanPathForExposure(raw, cwd) {
4348
4516
  if (raw.startsWith("~/")) return resolve6(join4(osHomedir(), raw.slice(2)));
@@ -4890,7 +5058,7 @@ var AsyncMutex = class {
4890
5058
  this.#locked = true;
4891
5059
  return;
4892
5060
  }
4893
- await new Promise((resolve37) => this.#waiters.push(resolve37));
5061
+ await new Promise((resolve38) => this.#waiters.push(resolve38));
4894
5062
  this.#locked = true;
4895
5063
  }
4896
5064
  unlock() {
@@ -5950,11 +6118,6 @@ var LINK_KIND_VALUES = Object.freeze([
5950
6118
  "mentions",
5951
6119
  "supersedes"
5952
6120
  ]);
5953
- var CONFIDENCE_VALUES = Object.freeze([
5954
- "high",
5955
- "medium",
5956
- "low"
5957
- ]);
5958
6121
  var SEVERITY_VALUES = Object.freeze([
5959
6122
  "error",
5960
6123
  "warn",
@@ -5986,7 +6149,7 @@ function isLinkKind(s) {
5986
6149
  return typeof s === "string" && LINK_KIND_VALUES.includes(s);
5987
6150
  }
5988
6151
  function isConfidence(s) {
5989
- return typeof s === "string" && CONFIDENCE_VALUES.includes(s);
6152
+ return typeof s === "number" && Number.isFinite(s) && s >= 0 && s <= 1;
5990
6153
  }
5991
6154
  function isSeverity(s) {
5992
6155
  return typeof s === "string" && SEVERITY_VALUES.includes(s);
@@ -6000,7 +6163,7 @@ function parseLinkKind(s, ctx) {
6000
6163
  function parseConfidence(s, ctx) {
6001
6164
  if (isConfidence(s)) return s;
6002
6165
  throw new Error(
6003
- `Invalid Confidence value ${formatValue(s)} at ${ctx}. Allowed: ${CONFIDENCE_VALUES.join(" | ")}.`
6166
+ `Invalid Confidence value ${formatValue(s)} at ${ctx}. Expected a finite number in [0..1].`
6004
6167
  );
6005
6168
  }
6006
6169
  function parseSeverity(s, ctx) {
@@ -8081,9 +8244,9 @@ function providerKindFailure(opts, status, fileName, errDescription) {
8081
8244
  }
8082
8245
  };
8083
8246
  }
8084
- function isDirectorySafe(path, statSync12) {
8247
+ function isDirectorySafe(path, statSync13) {
8085
8248
  try {
8086
- return statSync12(path).isDirectory();
8249
+ return statSync13(path).isDirectory();
8087
8250
  } catch {
8088
8251
  return false;
8089
8252
  }
@@ -8799,10 +8962,45 @@ var plainParser = {
8799
8962
  }
8800
8963
  };
8801
8964
 
8965
+ // plugins/core/parsers/toml/index.ts
8966
+ import { parse as parseToml } from "smol-toml";
8967
+ var tomlParser = {
8968
+ id: "toml",
8969
+ parse(raw, _path) {
8970
+ let parsed = {};
8971
+ const issues = [];
8972
+ try {
8973
+ const doc = parseToml(raw);
8974
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
8975
+ parsed = stripPrototypePollution(doc);
8976
+ }
8977
+ } catch (err) {
8978
+ issues.push({
8979
+ code: "frontmatter-parse-error",
8980
+ message: sanitiseParseErrorMessage2(err)
8981
+ });
8982
+ }
8983
+ const out = {
8984
+ frontmatterRaw: raw,
8985
+ frontmatter: parsed,
8986
+ body: ""
8987
+ };
8988
+ if (issues.length > 0) {
8989
+ return { ...out, issues };
8990
+ }
8991
+ return out;
8992
+ }
8993
+ };
8994
+ function sanitiseParseErrorMessage2(err) {
8995
+ const raw = err instanceof Error ? err.message : String(err);
8996
+ return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
8997
+ }
8998
+
8802
8999
  // kernel/scan/parsers/index.ts
8803
9000
  var REGISTRY = /* @__PURE__ */ new Map([
8804
9001
  [frontmatterYamlParser.id, frontmatterYamlParser],
8805
- [plainParser.id, plainParser]
9002
+ [plainParser.id, plainParser],
9003
+ [tomlParser.id, tomlParser]
8806
9004
  ]);
8807
9005
  var FROZEN_IDS = new Set(REGISTRY.keys());
8808
9006
  function getParser(id) {
@@ -9474,6 +9672,42 @@ function relativeIfBelow(path, cwd) {
9474
9672
  return rel;
9475
9673
  }
9476
9674
 
9675
+ // cli/util/scan-zone-drop.ts
9676
+ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
9677
+
9678
+ // cli/commands/db/shared.ts
9679
+ var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
9680
+ function assertSafeIdentifier(name) {
9681
+ if (!SAFE_SQL_IDENTIFIER_RE.test(name)) {
9682
+ throw new Error(`refusing to operate on non-identifier table name: ${JSON.stringify(name)}`);
9683
+ }
9684
+ }
9685
+
9686
+ // cli/util/scan-zone-drop.ts
9687
+ function dropScanZone(dbPath) {
9688
+ const db = new DatabaseSync4(dbPath);
9689
+ try {
9690
+ const rows = db.prepare(
9691
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'scan\\_%' ESCAPE '\\'"
9692
+ ).all();
9693
+ for (const r of rows) assertSafeIdentifier(r.name);
9694
+ if (rows.length === 0) {
9695
+ return { tableCount: 0, droppedTables: [] };
9696
+ }
9697
+ db.exec("BEGIN");
9698
+ for (const { name } of rows) {
9699
+ db.exec(`DELETE FROM "${name}"`);
9700
+ }
9701
+ db.exec("COMMIT");
9702
+ return {
9703
+ tableCount: rows.length,
9704
+ droppedTables: rows.map((r) => r.name)
9705
+ };
9706
+ } finally {
9707
+ db.close();
9708
+ }
9709
+ }
9710
+
9477
9711
  // cli/i18n/config.texts.ts
9478
9712
  var CONFIG_TEXTS = {
9479
9713
  unknownKey: "{{glyph}} Unknown config key: {{key}}\n",
@@ -9516,6 +9750,20 @@ var CONFIG_TEXTS = {
9516
9750
  * screen what they just opted into.
9517
9751
  */
9518
9752
  privacyGateConfirmed: '{{glyph}} Opening disk access for "{{key}}":\n{{paths}}\n',
9753
+ /**
9754
+ * Confirmation printed after `sm config set activeProvider <id>`
9755
+ * succeeds. The lens change atomically drops the scan_* zone (per
9756
+ * `architecture.md` §Active Provider Lens) so the persisted graph
9757
+ * never carries stale node / link rows from the previous lens. We
9758
+ * surface what was cleared so the operator knows their state was
9759
+ * touched and what to do next.
9760
+ */
9761
+ lensSwitchedCleared: "{{glyph}} Lens switched. Cleared {{tableCount}} scan table(s): {{tableNames}}.\n {{hint}}\n",
9762
+ lensSwitchedClearedHint: "Run `sm scan` to repopulate the graph under the new lens.",
9763
+ /** Same lens-switch announcement when the DB was empty (no rows to clear). */
9764
+ lensSwitchedEmpty: "{{glyph}} Lens switched. Scan zone was already empty.\n {{hint}}\n",
9765
+ /** Lens switch happened before any `sm scan` ran (no DB file on disk yet). */
9766
+ lensSwitchedNoDb: "{{glyph}} Lens switched. Run `sm scan` to populate the graph under the new lens.\n",
9519
9767
  // --- list verb (sectioned human renderer) ----------------------------
9520
9768
  /** Section heading: ` General`, ` Scan`, … rendered before its rows. */
9521
9769
  listSectionHeader: " {{title}}\n",
@@ -9879,7 +10127,7 @@ var ConfigSetCommand = class extends SmCommand {
9879
10127
  key = Option4.String({ required: true });
9880
10128
  value = Option4.String({ required: true });
9881
10129
  yes = Option4.Boolean("--yes", false, {
9882
- description: "Confirm a privacy-sensitive write that opens disk access outside the project (scan.extraFolders / scan.referencePaths)."
10130
+ description: "Confirm a privacy-sensitive write that opens disk access outside the project (scan.referencePaths)."
9883
10131
  });
9884
10132
  // CLI orchestrator: each branch is one validation gate (forbidden
9885
10133
  // segment / privacy guard / schema violation) or output dispatch.
@@ -9967,8 +10215,44 @@ var ConfigSetCommand = class extends SmCommand {
9967
10215
  )
9968
10216
  })
9969
10217
  );
10218
+ if (this.key === "activeProvider") {
10219
+ this.announceLensSwitch(ctx.cwd, ansi);
10220
+ }
9970
10221
  return ExitCode.Ok;
9971
10222
  }
10223
+ /**
10224
+ * Side effect of `sm config set activeProvider <id>`, atomically
10225
+ * drops the `scan_*` zone so the persisted graph never reflects the
10226
+ * wrong lens (see `architecture.md` §Active Provider Lens). The drop
10227
+ * is non-destructive of `state_*` / `config_*` rows; the operator
10228
+ * runs `sm scan` next to repopulate.
10229
+ *
10230
+ * Silent when no DB file exists on disk yet (fresh project that has
10231
+ * never run `sm scan`), the lens just gets set and the next scan
10232
+ * uses it.
10233
+ */
10234
+ announceLensSwitch(cwd, ansi) {
10235
+ const dbPath = resolveDbPath({ db: void 0, cwd });
10236
+ const okGlyph = ansi.green("\u2713");
10237
+ if (!existsSync14(dbPath)) {
10238
+ this.printer.info(tx(CONFIG_TEXTS.lensSwitchedNoDb, { glyph: okGlyph }));
10239
+ return;
10240
+ }
10241
+ const result = dropScanZone(dbPath);
10242
+ const hint = ansi.dim(CONFIG_TEXTS.lensSwitchedClearedHint);
10243
+ if (result.tableCount === 0) {
10244
+ this.printer.info(tx(CONFIG_TEXTS.lensSwitchedEmpty, { glyph: okGlyph, hint }));
10245
+ return;
10246
+ }
10247
+ this.printer.info(
10248
+ tx(CONFIG_TEXTS.lensSwitchedCleared, {
10249
+ glyph: okGlyph,
10250
+ tableCount: result.tableCount,
10251
+ tableNames: result.droppedTables.join(", "),
10252
+ hint
10253
+ })
10254
+ );
10255
+ }
9972
10256
  };
9973
10257
  var ConfigResetCommand = class extends SmCommand {
9974
10258
  static paths = [["config", "reset"]];
@@ -11006,18 +11290,8 @@ var DbRestoreCommand = class extends SmCommand {
11006
11290
 
11007
11291
  // cli/commands/db/reset.ts
11008
11292
  import { rm as rm2 } from "fs/promises";
11009
- import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
11293
+ import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
11010
11294
  import { Command as Command8, Option as Option8 } from "clipanion";
11011
-
11012
- // cli/commands/db/shared.ts
11013
- var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
11014
- function assertSafeIdentifier(name) {
11015
- if (!SAFE_SQL_IDENTIFIER_RE.test(name)) {
11016
- throw new Error(`refusing to operate on non-identifier table name: ${JSON.stringify(name)}`);
11017
- }
11018
- }
11019
-
11020
- // cli/commands/db/reset.ts
11021
11295
  var DbResetCommand = class extends SmCommand {
11022
11296
  static paths = [["db", "reset"]];
11023
11297
  static usage = Command8.Usage({
@@ -11099,7 +11373,7 @@ var DbResetCommand = class extends SmCommand {
11099
11373
  return ExitCode.Error;
11100
11374
  }
11101
11375
  }
11102
- const db = new DatabaseSync4(path);
11376
+ const db = new DatabaseSync5(path);
11103
11377
  try {
11104
11378
  const rows = db.prepare(
11105
11379
  "SELECT name FROM sqlite_master WHERE type='table' AND (name LIKE 'scan\\_%' ESCAPE '\\'" + (this.state ? " OR name LIKE 'state\\_%' ESCAPE '\\'" : "") + ")"
@@ -11242,7 +11516,7 @@ var DbBrowserCommand = class extends SmCommand {
11242
11516
  };
11243
11517
 
11244
11518
  // cli/commands/db/dump.ts
11245
- import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
11519
+ import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
11246
11520
  import { Command as Command11, Option as Option10 } from "clipanion";
11247
11521
  var DbDumpCommand = class extends SmCommand {
11248
11522
  static paths = [["db", "dump"]];
@@ -11284,7 +11558,7 @@ var DbDumpCommand = class extends SmCommand {
11284
11558
  }
11285
11559
  };
11286
11560
  function dumpDatabaseToStream(dbPath, out, tables) {
11287
- const db = new DatabaseSync5(dbPath, { readOnly: true });
11561
+ const db = new DatabaseSync6(dbPath, { readOnly: true });
11288
11562
  try {
11289
11563
  out.write("PRAGMA foreign_keys=OFF;\n");
11290
11564
  out.write("BEGIN TRANSACTION;\n");
@@ -12750,12 +13024,22 @@ var ORCHESTRATOR_TEXTS = {
12750
13024
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
12751
13025
  };
12752
13026
 
13027
+ // kernel/types.ts
13028
+ var ConfidenceTier = Object.freeze({
13029
+ HIGH: 0.9,
13030
+ MEDIUM: 0.6,
13031
+ LOW: 0.3
13032
+ });
13033
+
12753
13034
  // kernel/orchestrator/extractors.ts
12754
13035
  async function runExtractorsForNode(opts) {
12755
13036
  const internalLinks = [];
12756
13037
  const externalLinks = [];
12757
13038
  const enrichmentBuffer = /* @__PURE__ */ new Map();
12758
13039
  const contributions = [];
13040
+ const signals = [];
13041
+ const virtualNodes = [];
13042
+ const virtualNodePaths = /* @__PURE__ */ new Set();
12759
13043
  const validators = loadSchemaValidators();
12760
13044
  for (const extractor of opts.extractors) {
12761
13045
  const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
@@ -12827,6 +13111,18 @@ async function runExtractorsForNode(opts) {
12827
13111
  emittedAt: Date.now()
12828
13112
  });
12829
13113
  };
13114
+ const emitSignal = (signal) => {
13115
+ const validated = validateSignal(extractor, signal, opts.emitter);
13116
+ if (!validated) return;
13117
+ signals.push(validated);
13118
+ };
13119
+ const emitNode = (emitted) => {
13120
+ if (virtualNodePaths.has(emitted.path)) return;
13121
+ const node = buildVirtualNode(extractor, emitted, opts.emitter);
13122
+ if (!node) return;
13123
+ virtualNodePaths.add(node.path);
13124
+ virtualNodes.push(node);
13125
+ };
12830
13126
  const store = opts.pluginStores?.get(extractor.pluginId);
12831
13127
  const ctx = buildExtractorContext(
12832
13128
  extractor,
@@ -12836,6 +13132,8 @@ async function runExtractorsForNode(opts) {
12836
13132
  emitLink,
12837
13133
  enrichNode,
12838
13134
  emitContribution,
13135
+ emitSignal,
13136
+ emitNode,
12839
13137
  store
12840
13138
  );
12841
13139
  await extractor.extract(ctx);
@@ -12844,7 +13142,9 @@ async function runExtractorsForNode(opts) {
12844
13142
  internalLinks,
12845
13143
  externalLinks,
12846
13144
  enrichments: Array.from(enrichmentBuffer.values()),
12847
- contributions
13145
+ contributions,
13146
+ signals,
13147
+ virtualNodes
12848
13148
  };
12849
13149
  }
12850
13150
  function readDeclaredContributions(extension) {
@@ -12869,7 +13169,7 @@ function emitExtensionError(emitter, qualifiedId2, nodePath, data) {
12869
13169
  })
12870
13170
  );
12871
13171
  }
12872
- function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
13172
+ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, emitSignal, emitNode, store) {
12873
13173
  const scope = extractor.scope ?? "both";
12874
13174
  const settings = extractor.resolvedSettings ?? {};
12875
13175
  return {
@@ -12880,9 +13180,62 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
12880
13180
  emitLink,
12881
13181
  enrichNode,
12882
13182
  emitContribution,
13183
+ emitSignal,
13184
+ emitNode,
12883
13185
  ...store !== void 0 ? { store } : {}
12884
13186
  };
12885
13187
  }
13188
+ var VIRTUAL_NODE_PLACEHOLDER_HASH = "0".repeat(64);
13189
+ function buildVirtualNode(extractor, emitted, emitter) {
13190
+ const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
13191
+ if (typeof emitted.path !== "string" || emitted.path.length === 0) {
13192
+ emitter.emit(
13193
+ makeEvent("extension.error", {
13194
+ kind: "virtual-node-missing-path",
13195
+ extensionId: qualifiedId2,
13196
+ message: `Extractor ${qualifiedId2} emitted a virtual node with no path; dropped.`
13197
+ })
13198
+ );
13199
+ return null;
13200
+ }
13201
+ if (typeof emitted.kind !== "string" || emitted.kind.length === 0) {
13202
+ emitter.emit(
13203
+ makeEvent("extension.error", {
13204
+ kind: "virtual-node-missing-kind",
13205
+ extensionId: qualifiedId2,
13206
+ virtualPath: emitted.path,
13207
+ message: `Extractor ${qualifiedId2} emitted a virtual node at '${emitted.path}' with no kind; dropped.`
13208
+ })
13209
+ );
13210
+ return null;
13211
+ }
13212
+ if (!Array.isArray(emitted.derivedFrom) || emitted.derivedFrom.length === 0) {
13213
+ emitter.emit(
13214
+ makeEvent("extension.error", {
13215
+ kind: "virtual-node-missing-derived-from",
13216
+ extensionId: qualifiedId2,
13217
+ virtualPath: emitted.path,
13218
+ message: `Extractor ${qualifiedId2} emitted a virtual node at '${emitted.path}' with empty derivedFrom; dropped.`
13219
+ })
13220
+ );
13221
+ return null;
13222
+ }
13223
+ const node = {
13224
+ path: emitted.path,
13225
+ kind: emitted.kind,
13226
+ provider: emitted.provider,
13227
+ bodyHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
13228
+ frontmatterHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
13229
+ bytes: { frontmatter: 0, body: 0, total: 0 },
13230
+ linksOutCount: 0,
13231
+ linksInCount: 0,
13232
+ externalRefsCount: 0,
13233
+ virtual: true,
13234
+ derivedFrom: [...emitted.derivedFrom]
13235
+ };
13236
+ if (emitted.frontmatter) node.frontmatter = emitted.frontmatter;
13237
+ return node;
13238
+ }
12886
13239
  function validateLink(extractor, link2, emitter) {
12887
13240
  const knownKinds = ["invokes", "references", "mentions", "supersedes"];
12888
13241
  if (!knownKinds.includes(link2.kind)) {
@@ -12903,9 +13256,68 @@ function validateLink(extractor, link2, emitter) {
12903
13256
  );
12904
13257
  return null;
12905
13258
  }
12906
- const confidence = link2.confidence ?? "medium";
13259
+ const c = link2.confidence;
13260
+ if (c !== void 0 && (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1)) {
13261
+ const qualifiedId2 = `${extractor.pluginId}/${extractor.id}`;
13262
+ emitter.emit(
13263
+ makeEvent("extension.error", {
13264
+ kind: "link-confidence-out-of-range",
13265
+ extensionId: qualifiedId2,
13266
+ confidence: c,
13267
+ message: `Extractor ${qualifiedId2} emitted a Link with confidence ${String(c)} outside [0..1]; dropped.`
13268
+ })
13269
+ );
13270
+ return null;
13271
+ }
13272
+ const confidence = c ?? ConfidenceTier.MEDIUM;
12907
13273
  return { ...link2, confidence };
12908
13274
  }
13275
+ var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes"];
13276
+ function validateSignal(extractor, signal, emitter) {
13277
+ const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
13278
+ if (!Array.isArray(signal.candidates) || signal.candidates.length === 0) {
13279
+ emitter.emit(
13280
+ makeEvent("extension.error", {
13281
+ kind: "signal-no-candidates",
13282
+ extensionId: qualifiedId2,
13283
+ signal: { source: signal.source, scope: signal.scope },
13284
+ message: `Extractor ${qualifiedId2} emitted a Signal with no candidates; dropped.`
13285
+ })
13286
+ );
13287
+ return null;
13288
+ }
13289
+ for (const candidate of signal.candidates) {
13290
+ if (!isValidSignalCandidate(qualifiedId2, candidate, emitter)) return null;
13291
+ }
13292
+ return signal;
13293
+ }
13294
+ function isValidSignalCandidate(qualifiedId2, candidate, emitter) {
13295
+ if (!KNOWN_LINK_KINDS.includes(candidate.kind)) {
13296
+ emitter.emit(
13297
+ makeEvent("extension.error", {
13298
+ kind: "signal-candidate-kind-not-declared",
13299
+ extensionId: qualifiedId2,
13300
+ candidateKind: candidate.kind,
13301
+ declaredKinds: KNOWN_LINK_KINDS,
13302
+ message: `Extractor ${qualifiedId2} emitted a Signal candidate with off-enum kind '${String(candidate.kind)}'; dropped.`
13303
+ })
13304
+ );
13305
+ return false;
13306
+ }
13307
+ const c = candidate.confidence;
13308
+ if (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1) {
13309
+ emitter.emit(
13310
+ makeEvent("extension.error", {
13311
+ kind: "signal-candidate-confidence-out-of-range",
13312
+ extensionId: qualifiedId2,
13313
+ confidence: candidate.confidence,
13314
+ message: `Extractor ${qualifiedId2} emitted a Signal candidate with confidence ${String(c)} outside [0..1]; dropped.`
13315
+ })
13316
+ );
13317
+ return false;
13318
+ }
13319
+ return true;
13320
+ }
12909
13321
  function dedupeLinks(links) {
12910
13322
  const out = /* @__PURE__ */ new Map();
12911
13323
  for (const link2 of links) {
@@ -12952,7 +13364,9 @@ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
12952
13364
  }
12953
13365
  }
12954
13366
  var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
13367
+ var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
12955
13368
  function isExternalUrlLink(link2) {
13369
+ if (VIRTUAL_NODE_SCHEME_RE.test(link2.target)) return false;
12956
13370
  return EXTERNAL_URL_SCHEME_RE.test(link2.target);
12957
13371
  }
12958
13372
 
@@ -13111,13 +13525,9 @@ function originatingNodeOf(link2, priorNodePaths) {
13111
13525
  }
13112
13526
  function computeCacheDecision(opts) {
13113
13527
  const applicableExtractors = opts.extractors.filter((ex) => {
13114
- const kinds = ex.precondition?.kind;
13115
- if (!kinds || kinds.length === 0) return true;
13116
- return kinds.some((qualified) => {
13117
- const slashIdx = qualified.indexOf("/");
13118
- const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
13119
- return kindOnly === opts.kind;
13120
- });
13528
+ if (!matchesKindPrecondition(ex, opts.kind)) return false;
13529
+ if (!matchesProviderPrecondition(ex, opts.provider)) return false;
13530
+ return true;
13121
13531
  });
13122
13532
  const applicableQualifiedIds = new Set(
13123
13533
  applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
@@ -13131,6 +13541,20 @@ function computeCacheDecision(opts) {
13131
13541
  fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0
13132
13542
  };
13133
13543
  }
13544
+ function matchesKindPrecondition(ex, kind) {
13545
+ const kinds = ex.precondition?.kind;
13546
+ if (!kinds || kinds.length === 0) return true;
13547
+ return kinds.some((qualified) => {
13548
+ const slashIdx = qualified.indexOf("/");
13549
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
13550
+ return kindOnly === kind;
13551
+ });
13552
+ }
13553
+ function matchesProviderPrecondition(ex, provider) {
13554
+ const providers = ex.precondition?.provider;
13555
+ if (!providers || providers.length === 0) return true;
13556
+ return providers.includes(provider);
13557
+ }
13134
13558
  function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
13135
13559
  const cachedQualifiedIds = /* @__PURE__ */ new Set();
13136
13560
  const missingExtractors = [];
@@ -13240,7 +13664,7 @@ function findHighConfidenceRenames(opts) {
13240
13664
  if (opts.claimedNew.has(toPath)) continue;
13241
13665
  const toNode = opts.currentByPath.get(toPath);
13242
13666
  if (toNode.bodyHash === fromNode.bodyHash) {
13243
- ops.push({ from: fromPath, to: toPath, confidence: "high" });
13667
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.HIGH });
13244
13668
  opts.claimedDeleted.add(fromPath);
13245
13669
  opts.claimedNew.add(toPath);
13246
13670
  break;
@@ -13275,13 +13699,13 @@ function claimSingletonRenames(opts) {
13275
13699
  const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
13276
13700
  if (remaining.length === 1) {
13277
13701
  const fromPath = remaining[0];
13278
- ops.push({ from: fromPath, to: toPath, confidence: "medium" });
13702
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM });
13279
13703
  opts.issues.push({
13280
13704
  analyzerId: "auto-rename-medium",
13281
13705
  severity: "warn",
13282
13706
  nodeIds: [toPath],
13283
13707
  message: `Auto-rename (medium confidence): ${fromPath} \u2192 ${toPath}`,
13284
- data: { from: fromPath, to: toPath, confidence: "medium" }
13708
+ data: { from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM }
13285
13709
  });
13286
13710
  opts.claimedDeleted.add(fromPath);
13287
13711
  opts.claimedNew.add(toPath);
@@ -13719,6 +14143,7 @@ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextInde
13719
14143
  const cacheDecision = computeCacheDecision({
13720
14144
  extractors: wctx.opts.extractors,
13721
14145
  kind,
14146
+ provider: provider.id,
13722
14147
  nodePath: raw.path,
13723
14148
  bodyHash,
13724
14149
  sidecarAnnotationsHash,
@@ -13821,6 +14246,10 @@ function mergeExtractResult(extractResult, accum) {
13821
14246
  accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
13822
14247
  }
13823
14248
  for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
14249
+ for (const vn of extractResult.virtualNodes) {
14250
+ if (accum.nodes.some((n) => n.path === vn.path)) continue;
14251
+ accum.nodes.push(vn);
14252
+ }
13824
14253
  }
13825
14254
  function isPartialCacheHit(ctx) {
13826
14255
  return ctx.nodeHashCacheEligible && ctx.cacheDecision.cachedQualifiedIds.size > 0 && ctx.priorNode !== void 0;
@@ -14345,13 +14774,7 @@ var SCAN_RUNNER_TEXTS = {
14345
14774
  * DB-resident prior `ScanResult` fails `scan-result.schema.json`
14346
14775
  * validation.
14347
14776
  */
14348
- priorSchemaValidationFailed: "prior scan-result loaded from DB failed schema validation: {{errors}}. Run `sm db backup` then re-scan without --strict to rebuild from disk.",
14349
- /**
14350
- * Honest disclosure when the scan surface expanded beyond the cwd
14351
- * via `scan.extraFolders`. The list of paths makes it obvious which
14352
- * extra folders the operator just opted into.
14353
- */
14354
- includingExtraFoldersAdvisory: "Including extra folders: {{paths}}",
14777
+ priorSchemaValidationFailed: "prior scan-result loaded from DB failed schema validation: {{errors}}. Run `sm db backup` then re-scan without --strict to rebuild from disk.",
14355
14778
  /**
14356
14779
  * Reference-paths walker hit `REFERENCE_WALK_MAX_FILES` and stopped
14357
14780
  * early. The set may be incomplete for link validation; `core/broken-ref`
@@ -14366,6 +14789,14 @@ var SCAN_RUNNER_TEXTS = {
14366
14789
  referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.'
14367
14790
  };
14368
14791
 
14792
+ // core/runtime/scan-roots.ts
14793
+ function resolveScanRoots(inputs) {
14794
+ if (inputs.positionalRoots.length > 0) {
14795
+ return inputs.positionalRoots.slice();
14796
+ }
14797
+ return ["."];
14798
+ }
14799
+
14369
14800
  // core/runtime/reference-paths-walker.ts
14370
14801
  import { readdirSync as readdirSync9, statSync as statSync9 } from "fs";
14371
14802
  import { homedir as osHomedir2 } from "os";
@@ -14427,26 +14858,6 @@ function safeStat(path) {
14427
14858
  }
14428
14859
  }
14429
14860
 
14430
- // core/runtime/scan-roots.ts
14431
- function resolveScanRoots(inputs) {
14432
- if (inputs.positionalRoots.length > 0) {
14433
- return {
14434
- roots: inputs.positionalRoots.slice(),
14435
- fromExtra: []
14436
- };
14437
- }
14438
- const cwdRoot = ".";
14439
- const extra = inputs.extraFolders.map((r) => resolveScanPath(r, inputs.cwd));
14440
- const seen = /* @__PURE__ */ new Set();
14441
- const out = [cwdRoot];
14442
- for (const root of extra) {
14443
- if (seen.has(root)) continue;
14444
- seen.add(root);
14445
- out.push(root);
14446
- }
14447
- return { roots: out, fromExtra: extra };
14448
- }
14449
-
14450
14861
  // core/runtime/scan-runner.ts
14451
14862
  async function runScanForCommand(opts) {
14452
14863
  const ctx = opts.ctx ?? defaultRuntimeContext();
@@ -14464,13 +14875,7 @@ async function runScanForCommand(opts) {
14464
14875
  const strict = opts.strict || cfg.scan.strict === true;
14465
14876
  let effectiveRoots;
14466
14877
  try {
14467
- const resolution = resolveScanRoots({
14468
- positionalRoots: opts.roots,
14469
- cwd: ctx.cwd,
14470
- extraFolders: cfg.scan.extraFolders
14471
- });
14472
- effectiveRoots = resolution.roots;
14473
- emitRootsAdvisory(resolution.fromExtra, opts);
14878
+ effectiveRoots = resolveScanRoots({ positionalRoots: opts.roots });
14474
14879
  } catch (err) {
14475
14880
  return { kind: "config-error", message: formatErrorMessage(err) };
14476
14881
  }
@@ -14495,12 +14900,6 @@ async function runScanForCommand(opts) {
14495
14900
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
14496
14901
  return willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
14497
14902
  }
14498
- function emitRootsAdvisory(fromExtra, opts) {
14499
- if (fromExtra.length === 0) return;
14500
- opts.printer.info(
14501
- tx(SCAN_RUNNER_TEXTS.includingExtraFoldersAdvisory, { paths: fromExtra.join(", ") }) + "\n"
14502
- );
14503
- }
14504
14903
  function emitReferenceWalkAdvisory(walk2, opts) {
14505
14904
  if (walk2.truncated) {
14506
14905
  opts.printer.warn(SCAN_RUNNER_TEXTS.referenceWalkTruncated);
@@ -18518,6 +18917,13 @@ function createWatcherRuntime(opts) {
18518
18917
  strict,
18519
18918
  emitter
18520
18919
  };
18920
+ if (cfg.scan.referencePaths.length > 0) {
18921
+ const walk2 = walkReferencePaths(cfg.scan.referencePaths, cwd);
18922
+ if (walk2.paths.size > 0) {
18923
+ runOptions.referenceablePaths = walk2.paths;
18924
+ runOptions.cwd = cwd;
18925
+ }
18926
+ }
18521
18927
  if (composed) runOptions.extensions = composed;
18522
18928
  if (priorState) {
18523
18929
  runOptions.priorSnapshot = priorState.snapshot;
@@ -18952,12 +19358,11 @@ var ScanCommand = class extends SmCommand {
18952
19358
  the prior snapshot from the DB, reuse unchanged nodes, and only
18953
19359
  reprocess new / modified files.
18954
19360
 
18955
- Scans honour scan.extraFolders (append extra dirs verbatim, the
18956
- only way to extend the scan beyond cwd) and scan.referencePaths
18957
- (walk the configured dirs for link-validation only; files there
18958
- are not indexed). Both are
19361
+ Scans honour scan.referencePaths (walk the configured dirs for
19362
+ link-validation only; files there are not indexed). The key is
18959
19363
  privacy-sensitive; see "sm config set --help" for the --yes
18960
- gate.
19364
+ gate. To extend the indexed scan beyond cwd, pass extra roots
19365
+ positionally.
18961
19366
  `,
18962
19367
  examples: [
18963
19368
  ["Scan the current directory", "$0 scan"],
@@ -19418,7 +19823,7 @@ function renderDeltaIssues(issues) {
19418
19823
 
19419
19824
  // cli/commands/serve.ts
19420
19825
  import { spawn as spawn2 } from "child_process";
19421
- import { existsSync as existsSync27 } from "fs";
19826
+ import { existsSync as existsSync30 } from "fs";
19422
19827
  import { Command as Command33, Option as Option31 } from "clipanion";
19423
19828
 
19424
19829
  // cli/util/browser-launch.ts
@@ -19440,7 +19845,7 @@ import { WebSocketServer } from "ws";
19440
19845
  // server/app.ts
19441
19846
  import { Hono } from "hono";
19442
19847
  import { bodyLimit } from "hono/body-limit";
19443
- import { HTTPException as HTTPException13 } from "hono/http-exception";
19848
+ import { HTTPException as HTTPException15 } from "hono/http-exception";
19444
19849
 
19445
19850
  // core/config/service.ts
19446
19851
  var ConfigService = class {
@@ -19682,13 +20087,60 @@ var SERVER_TEXTS = {
19682
20087
  // silently widen the scan surface.
19683
20088
  projectPrefsBodyNotJson: "Request body must be valid JSON.",
19684
20089
  projectPrefsBodyNotObject: "Request body must be a JSON object.",
19685
- projectPrefsBodyEmpty: "Request body must contain a `scan` block with at least one of `extraFolders`, `referencePaths`.",
20090
+ projectPrefsBodyEmpty: "Request body must contain a `scan` block with `referencePaths`.",
19686
20091
  projectPrefsConfirmNotBoolean: "`confirm` must be a boolean.",
19687
- projectPrefsScanNotObject: '`scan` must be an object (e.g. `{"scan": {"extraFolders": ["~/.claude/agents"]}}`).',
20092
+ projectPrefsScanNotObject: '`scan` must be an object (e.g. `{"scan": {"referencePaths": ["~/Documents"]}}`).',
19688
20093
  projectPrefsListNotArray: "`{{key}}` must be an array of strings.",
19689
20094
  projectPrefsListEntryNotString: "`{{key}}` entries must be strings.",
19690
20095
  projectPrefsConfirmRequired: "This change opens disk access outside the project: {{paths}}. Re-issue the request with `confirm: true` to proceed.",
19691
20096
  projectPrefsPersistFailed: "Could not persist `{{key}}`: {{message}}",
20097
+ // Returned for every NEW entry that does not resolve to an existing
20098
+ // directory on disk. The list is comma-separated; pre-existing
20099
+ // entries are not re-validated.
20100
+ projectPrefsPathNotFound: "These folders do not exist on disk: {{paths}}. Add only paths that already exist.",
20101
+ // AJV `pattern` violation, an entry contains a comma. The UI rejects
20102
+ // comma input client-side; this message is the server-side safety
20103
+ // net (defense in depth).
20104
+ projectPrefsEntryHasComma: "Folder entries must not contain commas. Add one folder per entry.",
20105
+ // Server-stderr advisories emitted by `PATCH /api/project-preferences`
20106
+ // after a successful write. The operator running `sm serve` sees
20107
+ // each add / remove on the console without opening the config file.
20108
+ // `{{detail}}` is composed in JS (see `formatPathDetail`) so the
20109
+ // single template covers all three path shapes (home / relative /
20110
+ // absolute) without a template explosion.
20111
+ projectPrefsPathAdded: "project-prefs: + {{key}} {{detail}}",
20112
+ projectPrefsPathRemoved: "project-prefs: - {{key}} {{detail}}",
20113
+ // `PATCH /api/project-preferences` mutated `scan.*` and the
20114
+ // post-write `watcherService.restart()` call threw. The on-disk
20115
+ // write itself succeeded; the operator sees this advisory and
20116
+ // restarts the server to pick up the new root list manually.
20117
+ projectPrefsWatcherRestartFailed: "project-prefs: watcher restart after scan-config write failed ({{message}}). Restart `sm serve` to pick up the new roots.",
20118
+ // ---- project-ignore route (routes/project-ignore.ts) -------------------
20119
+ //
20120
+ // GET / PATCH /api/project-ignore. Backing is the project-root
20121
+ // `.skillmapignore` file (gitignore-syntax). Comments and blank
20122
+ // lines are preserved on write; only the active pattern list is
20123
+ // exchanged over the wire. No privacy gate, the patterns narrow the
20124
+ // scan surface and never widen disk access.
20125
+ projectIgnoreBodyNotJson: "Request body must be valid JSON.",
20126
+ projectIgnoreBodyNotObject: "Request body must be a JSON object.",
20127
+ projectIgnoreBodyEmpty: "Request body must contain a `patterns` array.",
20128
+ projectIgnoreListNotArray: "`patterns` must be an array of strings.",
20129
+ projectIgnoreEntryNotString: "`patterns` entries must be strings.",
20130
+ // AJV `minLength: 1` on each pattern after the route trims server-side;
20131
+ // surfaces when the operator sends `" "` or an empty string.
20132
+ projectIgnorePatternEmpty: "Pattern entries must not be empty or whitespace-only.",
20133
+ // AJV `pattern` violation: a single pattern carried a newline,
20134
+ // carriage return, or other ASCII control character. The UI rejects
20135
+ // these client-side; this message is the server-side safety net.
20136
+ projectIgnorePatternHasControlChar: "Pattern entries must be a single line without control characters.",
20137
+ // Duplicate detection runs after trim; the UI rejects duplicates
20138
+ // client-side, this is the server-side safety net.
20139
+ projectIgnorePatternDuplicate: 'Duplicate pattern: "{{pattern}}". Each pattern must be unique.',
20140
+ projectIgnorePersistFailed: "Could not persist `.skillmapignore`: {{message}}",
20141
+ projectIgnorePatternAdded: "project-ignore: + {{pattern}}",
20142
+ projectIgnorePatternRemoved: "project-ignore: - {{pattern}}",
20143
+ projectIgnoreWatcherRestartFailed: "project-ignore: watcher restart after `.skillmapignore` write failed ({{message}}). Restart `sm serve` to pick up the new filter.",
19692
20144
  // A connected client's outbound buffer exceeded the backpressure
19693
20145
  // threshold. The broadcaster closes the client with code 1009 and
19694
20146
  // unregisters it. Logged so operators can spot a wedged consumer.
@@ -19701,7 +20153,20 @@ var SERVER_TEXTS = {
19701
20153
  // is a synthetic `emitter.error` event; v14.4.a does not yet route
19702
20154
  // it through the broadcaster (would re-enter the same stringify
19703
20155
  // path), so we degrade to a logged warning.
19704
- wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped, failed to serialize event: {{message}}.\n"
20156
+ wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped, failed to serialize event: {{message}}.\n",
20157
+ // ---- active-provider route (routes/active-provider.ts) -----------
20158
+ //
20159
+ // GET / PUT /api/active-provider. The active provider lens selects
20160
+ // which provider's extractors / classifiers / resolution rules apply
20161
+ // to the whole project (see `architecture.md` §Active Provider Lens).
20162
+ // Changing the lens drops the `scan_*` zone atomically and prompts
20163
+ // the user to re-scan; `state_*` and `config_*` survive.
20164
+ activeProviderBodyNotJson: "Request body must be valid JSON.",
20165
+ activeProviderBodyNotObject: "Request body must be a JSON object.",
20166
+ activeProviderBodyMissing: "Request body must include `activeProvider` (a non-empty string).",
20167
+ activeProviderValueNotString: "`activeProvider` must be a string.",
20168
+ activeProviderValueEmpty: "`activeProvider` cannot be the empty string. Send the id of an enabled provider.",
20169
+ activeProviderPersistFailed: "Could not persist activeProvider: {{message}}"
19705
20170
  };
19706
20171
 
19707
20172
  // server/loopback-gate.ts
@@ -20588,11 +21053,6 @@ function registerPluginsRoute(app, deps) {
20588
21053
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
20589
21054
  });
20590
21055
  }
20591
- if (granularityOf(handle) !== "extension") {
20592
- throw new HTTPException8(400, {
20593
- message: tx(SERVER_TEXTS.pluginsGranularityBundleExpected, { id: bundleId })
20594
- });
20595
- }
20596
21056
  if (!hasExtension(handle, extensionId)) {
20597
21057
  throw new HTTPException8(404, {
20598
21058
  message: tx(SERVER_TEXTS.pluginsExtensionUnknown, { bundleId, extensionId })
@@ -20633,7 +21093,7 @@ function buildBuiltInItems(resolveEnabled) {
20633
21093
  return builtInBundles.map((bundle) => {
20634
21094
  const bundleEnabled = resolveEnabled(bundle.id);
20635
21095
  const bundleLocked = isPluginLocked(bundle.id);
20636
- const extensions = bundle.granularity === "extension" ? bundle.extensions.map((ext) => {
21096
+ const extensions = bundle.extensions.map((ext) => {
20637
21097
  const qualified = qualifiedExtensionId(bundle.id, ext.id);
20638
21098
  const extLocked = bundleLocked || isPluginLocked(qualified);
20639
21099
  return {
@@ -20644,7 +21104,7 @@ function buildBuiltInItems(resolveEnabled) {
20644
21104
  ...ext.description ? { description: ext.description } : {},
20645
21105
  ...extLocked ? { locked: true } : {}
20646
21106
  };
20647
- }) : void 0;
21107
+ });
20648
21108
  return {
20649
21109
  id: bundle.id,
20650
21110
  version: firstVersion(bundle.extensions),
@@ -20654,7 +21114,7 @@ function buildBuiltInItems(resolveEnabled) {
20654
21114
  source: "built-in",
20655
21115
  granularity: bundle.granularity,
20656
21116
  description: bundle.description,
20657
- ...extensions ? { extensions } : {},
21117
+ ...extensions.length > 0 ? { extensions } : {},
20658
21118
  ...bundleLocked ? { locked: true } : {}
20659
21119
  };
20660
21120
  });
@@ -20687,8 +21147,8 @@ function optionalDiscoveredFields(plugin, extensions) {
20687
21147
  if (extensions) out.extensions = extensions;
20688
21148
  return out;
20689
21149
  }
20690
- function projectExtensionRows(plugin, granularity, resolveEnabled, bundleLocked) {
20691
- if (granularity !== "extension" || !plugin.extensions) return void 0;
21150
+ function projectExtensionRows(plugin, _granularity, resolveEnabled, bundleLocked) {
21151
+ if (!plugin.extensions || plugin.extensions.length === 0) return void 0;
20692
21152
  return plugin.extensions.map((ext) => {
20693
21153
  const description = readInstanceDescription(ext.instance);
20694
21154
  const qualified = qualifiedExtensionId(plugin.id, ext.id);
@@ -20805,13 +21265,6 @@ function validateBulkChange(change, deps) {
20805
21265
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
20806
21266
  };
20807
21267
  }
20808
- if (granularityOf(handle) !== "extension") {
20809
- return {
20810
- status: 400,
20811
- code: "bad-query",
20812
- message: tx(SERVER_TEXTS.pluginsGranularityBundleExpected, { id: bundleId })
20813
- };
20814
- }
20815
21268
  if (!hasExtension(handle, extensionId)) {
20816
21269
  return {
20817
21270
  status: 404,
@@ -20922,26 +21375,203 @@ var parsePatchBody2 = makeBodyValidator(PATCH_BODY_SCHEMA, {
20922
21375
  }
20923
21376
  });
20924
21377
 
20925
- // server/routes/project-preferences.ts
21378
+ // server/routes/project-ignore.ts
20926
21379
  import { HTTPException as HTTPException10 } from "hono/http-exception";
20927
- function registerProjectPreferencesRoute(app, deps) {
20928
- app.get("/api/project-preferences", (c) => {
21380
+
21381
+ // server/util/skillmapignore-io.ts
21382
+ import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
21383
+ import { resolve as resolve33 } from "path";
21384
+ var IGNORE_FILENAME2 = ".skillmapignore";
21385
+ function readPatterns(cwd) {
21386
+ const path = resolve33(cwd, IGNORE_FILENAME2);
21387
+ if (!existsSync25(path)) return [];
21388
+ let raw;
21389
+ try {
21390
+ raw = readFileSync19(path, "utf8");
21391
+ } catch {
21392
+ return [];
21393
+ }
21394
+ return raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
21395
+ }
21396
+ function writePatterns(cwd, nextPatterns) {
21397
+ const path = resolve33(cwd, IGNORE_FILENAME2);
21398
+ const prior = existsSync25(path) ? safeRead(path) : "";
21399
+ const content = buildContent(prior, nextPatterns);
21400
+ writeFileSync4(path, content, "utf8");
21401
+ }
21402
+ function safeRead(path) {
21403
+ try {
21404
+ return readFileSync19(path, "utf8");
21405
+ } catch {
21406
+ return "";
21407
+ }
21408
+ }
21409
+ function buildContent(prior, nextPatterns) {
21410
+ const wanted = new Set(nextPatterns);
21411
+ const kept = /* @__PURE__ */ new Set();
21412
+ const outLines = [];
21413
+ for (const line of splitLines(prior)) {
21414
+ pushPriorLine(line, wanted, kept, outLines);
21415
+ }
21416
+ appendNewPatterns(nextPatterns, kept, outLines);
21417
+ return outLines.length === 0 ? "" : outLines.join("\n") + "\n";
21418
+ }
21419
+ function splitLines(prior) {
21420
+ if (prior.length === 0) return [];
21421
+ const lines = prior.split(/\r?\n/);
21422
+ if (lines[lines.length - 1] === "") lines.pop();
21423
+ return lines;
21424
+ }
21425
+ function pushPriorLine(line, wanted, kept, outLines) {
21426
+ const trimmed = line.trim();
21427
+ if (trimmed.length === 0 || trimmed.startsWith("#")) {
21428
+ outLines.push(line);
21429
+ return;
21430
+ }
21431
+ if (wanted.has(trimmed)) {
21432
+ outLines.push(trimmed);
21433
+ kept.add(trimmed);
21434
+ }
21435
+ }
21436
+ function appendNewPatterns(nextPatterns, kept, outLines) {
21437
+ for (const p of nextPatterns) {
21438
+ if (kept.has(p)) continue;
21439
+ outLines.push(p);
21440
+ kept.add(p);
21441
+ }
21442
+ }
21443
+
21444
+ // server/routes/project-ignore.ts
21445
+ function registerProjectIgnoreRoute(app, deps) {
21446
+ app.get("/api/project-ignore", (c) => {
20929
21447
  return c.json(buildEnvelope2(deps));
20930
21448
  });
20931
- app.patch("/api/project-preferences", async (c) => {
21449
+ app.patch("/api/project-ignore", async (c) => {
20932
21450
  const body = await parsePatchBody3(c.req.raw);
20933
- applyPatch2(deps, body);
21451
+ await applyPatch2(deps, body);
20934
21452
  return c.json(buildEnvelope2(deps));
20935
21453
  });
20936
21454
  }
20937
21455
  function buildEnvelope2(deps) {
21456
+ const cwd = deps.runtimeContext.cwd;
21457
+ return { patterns: readPatterns(cwd) };
21458
+ }
21459
+ async function applyPatch2(deps, body) {
21460
+ const cwd = deps.runtimeContext.cwd;
21461
+ const trimmed = [];
21462
+ const seen = /* @__PURE__ */ new Set();
21463
+ for (const raw of body.patterns) {
21464
+ const t = raw.trim();
21465
+ if (t.length === 0) {
21466
+ throw new HTTPException10(400, {
21467
+ message: SERVER_TEXTS.projectIgnorePatternEmpty
21468
+ });
21469
+ }
21470
+ if (seen.has(t)) {
21471
+ throw new HTTPException10(400, {
21472
+ message: tx(SERVER_TEXTS.projectIgnorePatternDuplicate, { pattern: t })
21473
+ });
21474
+ }
21475
+ seen.add(t);
21476
+ trimmed.push(t);
21477
+ }
21478
+ const before = readPatterns(cwd);
21479
+ try {
21480
+ writePatterns(cwd, trimmed);
21481
+ } catch (err) {
21482
+ throw new HTTPException10(400, {
21483
+ message: tx(SERVER_TEXTS.projectIgnorePersistFailed, {
21484
+ message: formatErrorMessage(err)
21485
+ })
21486
+ });
21487
+ }
21488
+ logPatternChanges(before, trimmed);
21489
+ if (arrayChanged(before, trimmed)) await maybeRestartWatcher(deps);
21490
+ }
21491
+ function arrayChanged(before, next) {
21492
+ if (before.length !== next.length) return true;
21493
+ const beforeSet = new Set(before);
21494
+ for (const p of next) {
21495
+ if (!beforeSet.has(p)) return true;
21496
+ }
21497
+ return false;
21498
+ }
21499
+ function logPatternChanges(before, next) {
21500
+ const beforeSet = new Set(before);
21501
+ const nextSet = new Set(next);
21502
+ for (const p of next) {
21503
+ if (beforeSet.has(p)) continue;
21504
+ log.warn(
21505
+ tx(SERVER_TEXTS.projectIgnorePatternAdded, {
21506
+ pattern: sanitizeForTerminal(p)
21507
+ })
21508
+ );
21509
+ }
21510
+ for (const p of before) {
21511
+ if (nextSet.has(p)) continue;
21512
+ log.warn(
21513
+ tx(SERVER_TEXTS.projectIgnorePatternRemoved, {
21514
+ pattern: sanitizeForTerminal(p)
21515
+ })
21516
+ );
21517
+ }
21518
+ }
21519
+ async function maybeRestartWatcher(deps) {
21520
+ const watcher = deps.watcherHolder.current;
21521
+ if (!watcher) return;
21522
+ try {
21523
+ await watcher.restart();
21524
+ } catch (err) {
21525
+ log.warn(
21526
+ tx(SERVER_TEXTS.projectIgnoreWatcherRestartFailed, {
21527
+ message: formatErrorMessage(err)
21528
+ })
21529
+ );
21530
+ }
21531
+ }
21532
+ var PATCH_BODY_SCHEMA2 = {
21533
+ type: "object",
21534
+ additionalProperties: false,
21535
+ required: ["patterns"],
21536
+ properties: {
21537
+ patterns: {
21538
+ type: "array",
21539
+ items: {
21540
+ type: "string",
21541
+ pattern: "^[^\\n\\r\\x00-\\x1F\\x7F]+$"
21542
+ }
21543
+ }
21544
+ }
21545
+ };
21546
+ var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
21547
+ notJson: SERVER_TEXTS.projectIgnoreBodyNotJson,
21548
+ notObject: SERVER_TEXTS.projectIgnoreBodyNotObject,
21549
+ invalid: SERVER_TEXTS.projectIgnoreBodyEmpty,
21550
+ mapping: {
21551
+ "/patterns:required": SERVER_TEXTS.projectIgnoreBodyEmpty,
21552
+ "/patterns:type:array": SERVER_TEXTS.projectIgnoreListNotArray,
21553
+ "/patterns/*:type:string": SERVER_TEXTS.projectIgnoreEntryNotString,
21554
+ "/patterns/*:pattern": SERVER_TEXTS.projectIgnorePatternHasControlChar
21555
+ }
21556
+ });
21557
+
21558
+ // server/routes/project-preferences.ts
21559
+ import { statSync as statSync10 } from "fs";
21560
+ import { HTTPException as HTTPException11 } from "hono/http-exception";
21561
+ function registerProjectPreferencesRoute(app, deps) {
21562
+ app.get("/api/project-preferences", (c) => {
21563
+ return c.json(buildEnvelope3(deps));
21564
+ });
21565
+ app.patch("/api/project-preferences", async (c) => {
21566
+ const body = await parsePatchBody4(c.req.raw);
21567
+ await applyPatch3(deps, body);
21568
+ return c.json(buildEnvelope3(deps));
21569
+ });
21570
+ }
21571
+ function buildEnvelope3(deps) {
20938
21572
  const cwd = deps.runtimeContext.cwd;
20939
21573
  return {
20940
21574
  scan: {
20941
- extraFolders: readConfigValue("scan.extraFolders", {
20942
- cwd,
20943
- default: []
20944
- }) ?? [],
20945
21575
  referencePaths: readConfigValue("scan.referencePaths", {
20946
21576
  cwd,
20947
21577
  default: []
@@ -20949,46 +21579,138 @@ function buildEnvelope2(deps) {
20949
21579
  }
20950
21580
  };
20951
21581
  }
20952
- function applyPatch2(deps, body) {
21582
+ async function applyPatch3(deps, body) {
20953
21583
  const writes = collectWrites(body);
20954
21584
  if (writes.length === 0) return;
20955
21585
  const cwd = deps.runtimeContext.cwd;
21586
+ const missingPaths = collectMissingPaths(writes, cwd);
21587
+ if (missingPaths.length > 0) {
21588
+ throw new HTTPException11(400, {
21589
+ message: tx(SERVER_TEXTS.projectPrefsPathNotFound, {
21590
+ paths: missingPaths.join(", ")
21591
+ })
21592
+ });
21593
+ }
20956
21594
  const exposures = writes.map((w) => projectPathExposure({ key: w.key, value: w.value, cwd })).filter((e) => e.expandsSurface);
20957
21595
  if (exposures.length > 0 && body.confirm !== true) {
20958
21596
  const exposed = exposures.flatMap((e) => e.exposedPaths);
20959
- throw new HTTPException10(412, {
21597
+ throw new HTTPException11(412, {
20960
21598
  message: tx(SERVER_TEXTS.projectPrefsConfirmRequired, {
20961
21599
  paths: exposed.join(", ")
20962
21600
  })
20963
21601
  });
20964
21602
  }
21603
+ let scanSurfaceMutated = false;
20965
21604
  for (const w of writes) {
20966
- try {
20967
- writeConfigValue(w.key, w.value, { target: "project-local", cwd });
20968
- } catch (err) {
20969
- const status = err instanceof ConfigValidationError ? 400 : 400;
20970
- throw new HTTPException10(status, {
20971
- message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
20972
- key: w.key,
20973
- message: formatErrorMessage(err)
20974
- })
20975
- });
20976
- }
21605
+ if (runWrite(w, cwd)) scanSurfaceMutated = true;
20977
21606
  }
21607
+ if (scanSurfaceMutated) await maybeRestartWatcher2(deps);
20978
21608
  deps.configService.reload();
20979
21609
  }
20980
21610
  function collectWrites(body) {
20981
21611
  if (!body.scan) return [];
20982
21612
  const out = [];
20983
- if (Array.isArray(body.scan.extraFolders)) {
20984
- out.push({ key: "scan.extraFolders", value: body.scan.extraFolders });
20985
- }
20986
21613
  if (Array.isArray(body.scan.referencePaths)) {
20987
21614
  out.push({ key: "scan.referencePaths", value: body.scan.referencePaths });
20988
21615
  }
20989
21616
  return out;
20990
21617
  }
20991
- var PATCH_BODY_SCHEMA2 = {
21618
+ function collectMissingPaths(writes, cwd) {
21619
+ const missing = [];
21620
+ for (const w of writes) {
21621
+ if (!Array.isArray(w.value)) continue;
21622
+ const current = readConfigValue(w.key, { cwd, default: [] }) ?? [];
21623
+ const currentSet = new Set(current);
21624
+ for (const entry of w.value) {
21625
+ if (currentSet.has(entry)) continue;
21626
+ if (!isExistingDirectory(entry, cwd)) missing.push(entry);
21627
+ }
21628
+ }
21629
+ return missing;
21630
+ }
21631
+ async function maybeRestartWatcher2(deps) {
21632
+ const watcher = deps.watcherHolder.current;
21633
+ if (!watcher) return;
21634
+ try {
21635
+ await watcher.restart();
21636
+ } catch (err) {
21637
+ log.warn(
21638
+ tx(SERVER_TEXTS.projectPrefsWatcherRestartFailed, {
21639
+ message: formatErrorMessage(err)
21640
+ })
21641
+ );
21642
+ }
21643
+ }
21644
+ function runWrite(w, cwd) {
21645
+ const before = readConfigValue(w.key, { cwd, default: [] }) ?? [];
21646
+ try {
21647
+ writeConfigValue(w.key, w.value, { target: "project-local", cwd });
21648
+ } catch (err) {
21649
+ throw new HTTPException11(400, {
21650
+ message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
21651
+ key: w.key,
21652
+ message: formatErrorMessage(err)
21653
+ })
21654
+ });
21655
+ }
21656
+ logPathChanges(w.key, before, w.value, cwd);
21657
+ return arrayChanged2(before, w.value);
21658
+ }
21659
+ function arrayChanged2(before, nextValue) {
21660
+ if (!Array.isArray(nextValue)) return false;
21661
+ const next = nextValue;
21662
+ if (before.length !== next.length) return true;
21663
+ const beforeSet = new Set(before);
21664
+ for (const p of next) {
21665
+ if (!beforeSet.has(p)) return true;
21666
+ }
21667
+ return false;
21668
+ }
21669
+ function logPathChanges(key, before, nextValue, cwd) {
21670
+ if (!Array.isArray(nextValue)) return;
21671
+ const next = nextValue;
21672
+ const beforeSet = new Set(before);
21673
+ const nextSet = new Set(next);
21674
+ for (const path of next) {
21675
+ if (beforeSet.has(path)) continue;
21676
+ log.warn(
21677
+ tx(SERVER_TEXTS.projectPrefsPathAdded, {
21678
+ key,
21679
+ detail: formatPathDetail(path, cwd)
21680
+ })
21681
+ );
21682
+ }
21683
+ for (const path of before) {
21684
+ if (nextSet.has(path)) continue;
21685
+ log.warn(
21686
+ tx(SERVER_TEXTS.projectPrefsPathRemoved, {
21687
+ key,
21688
+ detail: formatPathDetail(path, cwd)
21689
+ })
21690
+ );
21691
+ }
21692
+ }
21693
+ function formatPathDetail(path, cwd) {
21694
+ const safePath = sanitizeForTerminal(path);
21695
+ if (path.startsWith("~/") || path === "~") {
21696
+ const abs2 = sanitizeForTerminal(resolveScanPath(path, cwd));
21697
+ return `${safePath} (home) \u2192 ${abs2}`;
21698
+ }
21699
+ if (path.startsWith("/")) {
21700
+ return `${safePath} (absolute)`;
21701
+ }
21702
+ const abs = sanitizeForTerminal(resolveScanPath(path, cwd));
21703
+ return `${safePath} (relative) \u2192 ${abs}`;
21704
+ }
21705
+ function isExistingDirectory(entry, cwd) {
21706
+ const abs = resolveScanPath(entry, cwd);
21707
+ try {
21708
+ return statSync10(abs).isDirectory();
21709
+ } catch {
21710
+ return false;
21711
+ }
21712
+ }
21713
+ var PATCH_BODY_SCHEMA3 = {
20992
21714
  type: "object",
20993
21715
  additionalProperties: false,
20994
21716
  required: ["scan"],
@@ -20999,13 +21721,15 @@ var PATCH_BODY_SCHEMA2 = {
20999
21721
  additionalProperties: false,
21000
21722
  minProperties: 1,
21001
21723
  properties: {
21002
- extraFolders: { type: "array", items: { type: "string" } },
21003
- referencePaths: { type: "array", items: { type: "string" } }
21724
+ referencePaths: {
21725
+ type: "array",
21726
+ items: { type: "string", pattern: "^[^,]+$" }
21727
+ }
21004
21728
  }
21005
21729
  }
21006
21730
  }
21007
21731
  };
21008
- var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
21732
+ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
21009
21733
  notJson: SERVER_TEXTS.projectPrefsBodyNotJson,
21010
21734
  notObject: SERVER_TEXTS.projectPrefsBodyNotObject,
21011
21735
  invalid: SERVER_TEXTS.projectPrefsBodyEmpty,
@@ -21014,15 +21738,111 @@ var parsePatchBody3 = makeBodyValidator(PATCH_BODY_SCHEMA2, {
21014
21738
  "/scan:minProperties": SERVER_TEXTS.projectPrefsBodyEmpty,
21015
21739
  "/scan:type:object": SERVER_TEXTS.projectPrefsScanNotObject,
21016
21740
  "/confirm:type:boolean": SERVER_TEXTS.projectPrefsConfirmNotBoolean,
21017
- "/scan/extraFolders:type:array": tx(SERVER_TEXTS.projectPrefsListNotArray, { key: "scan.extraFolders" }),
21018
21741
  "/scan/referencePaths:type:array": tx(SERVER_TEXTS.projectPrefsListNotArray, { key: "scan.referencePaths" }),
21019
- "/scan/extraFolders/*:type:string": tx(SERVER_TEXTS.projectPrefsListEntryNotString, { key: "scan.extraFolders" }),
21020
- "/scan/referencePaths/*:type:string": tx(SERVER_TEXTS.projectPrefsListEntryNotString, { key: "scan.referencePaths" })
21742
+ "/scan/referencePaths/*:type:string": tx(SERVER_TEXTS.projectPrefsListEntryNotString, { key: "scan.referencePaths" }),
21743
+ "/scan/referencePaths/*:pattern": SERVER_TEXTS.projectPrefsEntryHasComma
21744
+ }
21745
+ });
21746
+
21747
+ // server/routes/active-provider.ts
21748
+ import { existsSync as existsSync27 } from "fs";
21749
+ import { HTTPException as HTTPException12 } from "hono/http-exception";
21750
+
21751
+ // core/config/active-provider.ts
21752
+ import { existsSync as existsSync26 } from "fs";
21753
+ import { join as join17 } from "path";
21754
+ var DETECTION_RULES = [
21755
+ { providerId: "claude", marker: ".claude" },
21756
+ { providerId: "gemini", marker: ".gemini" },
21757
+ { providerId: "openai", marker: ".codex" },
21758
+ { providerId: "openai", marker: "AGENTS.md" },
21759
+ { providerId: "cursor", marker: ".cursor" }
21760
+ ];
21761
+ function resolveActiveProvider(cwd) {
21762
+ const detected = detectProvidersFromFilesystem(cwd);
21763
+ const fromConfig = readConfigValue("activeProvider", { cwd });
21764
+ if (typeof fromConfig === "string" && fromConfig.length > 0) {
21765
+ return { resolved: fromConfig, source: "config", detected };
21766
+ }
21767
+ if (detected.length > 0) {
21768
+ return { resolved: detected[0], source: "autodetect", detected };
21769
+ }
21770
+ return { resolved: null, source: "none", detected };
21771
+ }
21772
+ function detectProvidersFromFilesystem(cwd) {
21773
+ const seen = /* @__PURE__ */ new Set();
21774
+ const out = [];
21775
+ for (const rule of DETECTION_RULES) {
21776
+ if (seen.has(rule.providerId)) continue;
21777
+ if (!existsSync26(join17(cwd, rule.marker))) continue;
21778
+ seen.add(rule.providerId);
21779
+ out.push(rule.providerId);
21780
+ }
21781
+ return out;
21782
+ }
21783
+
21784
+ // server/routes/active-provider.ts
21785
+ function registerActiveProviderRoute(app, deps) {
21786
+ app.get("/api/active-provider", (c) => {
21787
+ return c.json(buildEnvelope4(deps));
21788
+ });
21789
+ app.patch("/api/active-provider", async (c) => {
21790
+ const body = await parsePatchBody5(c.req.raw);
21791
+ const result = applyLensSwitch(deps, body.activeProvider);
21792
+ deps.configService.reload();
21793
+ return c.json({ ...buildEnvelope4(deps), switch: result });
21794
+ });
21795
+ }
21796
+ function buildEnvelope4(deps) {
21797
+ const r = resolveActiveProvider(deps.runtimeContext.cwd);
21798
+ return {
21799
+ activeProvider: r.resolved,
21800
+ detected: r.detected,
21801
+ source: r.source
21802
+ };
21803
+ }
21804
+ function applyLensSwitch(deps, newValue) {
21805
+ const cwd = deps.runtimeContext.cwd;
21806
+ try {
21807
+ writeConfigValue("activeProvider", newValue, { target: "project", cwd });
21808
+ } catch (err) {
21809
+ throw new HTTPException12(400, {
21810
+ message: tx(SERVER_TEXTS.activeProviderPersistFailed, {
21811
+ message: formatErrorMessage(err)
21812
+ })
21813
+ });
21814
+ }
21815
+ const dbPath = resolveDbPath({ db: void 0, cwd });
21816
+ if (!existsSync27(dbPath)) return { dropped: null };
21817
+ const dropResult = dropScanZone(dbPath);
21818
+ return {
21819
+ dropped: {
21820
+ tableCount: dropResult.tableCount,
21821
+ tableNames: dropResult.droppedTables
21822
+ }
21823
+ };
21824
+ }
21825
+ var PATCH_BODY_SCHEMA4 = {
21826
+ type: "object",
21827
+ additionalProperties: false,
21828
+ required: ["activeProvider"],
21829
+ properties: {
21830
+ activeProvider: { type: "string", minLength: 1 }
21831
+ }
21832
+ };
21833
+ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
21834
+ notJson: SERVER_TEXTS.activeProviderBodyNotJson,
21835
+ notObject: SERVER_TEXTS.activeProviderBodyNotObject,
21836
+ invalid: SERVER_TEXTS.activeProviderBodyMissing,
21837
+ mapping: {
21838
+ ":required": SERVER_TEXTS.activeProviderBodyMissing,
21839
+ "/activeProvider:type:string": SERVER_TEXTS.activeProviderValueNotString,
21840
+ "/activeProvider:minLength": SERVER_TEXTS.activeProviderValueEmpty
21021
21841
  }
21022
21842
  });
21023
21843
 
21024
21844
  // server/routes/scan.ts
21025
- import { HTTPException as HTTPException11 } from "hono/http-exception";
21845
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
21026
21846
 
21027
21847
  // server/scan-mutex.ts
21028
21848
  var inFlight = null;
@@ -21030,14 +21850,14 @@ async function withScanMutex(fn) {
21030
21850
  if (inFlight !== null) {
21031
21851
  throw new ScanBusyError();
21032
21852
  }
21033
- let resolve37;
21853
+ let resolve38;
21034
21854
  inFlight = new Promise((r) => {
21035
- resolve37 = r;
21855
+ resolve38 = r;
21036
21856
  });
21037
21857
  try {
21038
21858
  return await fn();
21039
21859
  } finally {
21040
- resolve37();
21860
+ resolve38();
21041
21861
  inFlight = null;
21042
21862
  }
21043
21863
  }
@@ -21077,61 +21897,80 @@ function buildWatcherErrorEvent(data) {
21077
21897
  }
21078
21898
 
21079
21899
  // server/watcher.ts
21080
- var WATCH_ROOT = ".";
21081
21900
  function createWatcherService(opts) {
21082
- const runtimeOpts = {
21083
- dbPath: opts.options.dbPath,
21084
- roots: [WATCH_ROOT],
21085
- runtimeContext: opts.runtimeContext,
21086
- noBuiltIns: opts.options.noBuiltIns,
21087
- noPlugins: opts.options.noPlugins,
21088
- emitterFactory: () => buildBroadcasterEmitter(opts.broadcaster),
21089
- runInitialBatch: true,
21090
- // BFF ordering: subscribe first so edits arriving during the initial
21091
- // scan queue against the armed chokidar and fire a follow-up batch.
21092
- subscribeBeforeInitial: true,
21093
- failOnInitialBatchError: false,
21094
- events: {
21095
- onBatch: (outcome) => {
21096
- if (outcome.kind === "error") {
21901
+ let currentRuntime = null;
21902
+ const buildRuntimeOpts = () => {
21903
+ const runtimeOpts = {
21904
+ dbPath: opts.options.dbPath,
21905
+ roots: ["."],
21906
+ runtimeContext: opts.runtimeContext,
21907
+ noBuiltIns: opts.options.noBuiltIns,
21908
+ noPlugins: opts.options.noPlugins,
21909
+ emitterFactory: () => buildBroadcasterEmitter(opts.broadcaster),
21910
+ runInitialBatch: true,
21911
+ // BFF ordering: subscribe first so edits arriving during the
21912
+ // initial scan queue against the armed chokidar and fire a
21913
+ // follow-up batch.
21914
+ subscribeBeforeInitial: true,
21915
+ failOnInitialBatchError: false,
21916
+ events: {
21917
+ onBatch: (outcome) => {
21918
+ if (outcome.kind === "error") {
21919
+ log.warn(
21920
+ tx(SERVER_TEXTS.watcherBatchFailed, {
21921
+ message: sanitizeForTerminal(outcome.message)
21922
+ })
21923
+ );
21924
+ }
21925
+ },
21926
+ onWatcherError: (message) => {
21097
21927
  log.warn(
21098
- tx(SERVER_TEXTS.watcherBatchFailed, {
21099
- message: sanitizeForTerminal(outcome.message)
21928
+ tx(SERVER_TEXTS.watcherError, {
21929
+ message: sanitizeForTerminal(message)
21930
+ })
21931
+ );
21932
+ opts.broadcaster.broadcast(buildWatcherErrorEvent({ message }));
21933
+ },
21934
+ onPluginWarning: (message) => {
21935
+ log.warn(sanitizeForTerminal(message));
21936
+ },
21937
+ onReady: (info) => {
21938
+ opts.broadcaster.broadcast(
21939
+ buildWatcherStartedEvent({ roots: info.roots, debounceMs: info.debounceMs })
21940
+ );
21941
+ log.info(
21942
+ tx(SERVER_TEXTS.watcherReady, {
21943
+ roots: info.roots.join(","),
21944
+ debounceMs: String(info.debounceMs)
21100
21945
  })
21101
21946
  );
21102
21947
  }
21103
- },
21104
- onWatcherError: (message) => {
21105
- log.warn(
21106
- tx(SERVER_TEXTS.watcherError, {
21107
- message: sanitizeForTerminal(message)
21108
- })
21109
- );
21110
- opts.broadcaster.broadcast(buildWatcherErrorEvent({ message }));
21111
- },
21112
- onPluginWarning: (message) => {
21113
- log.warn(sanitizeForTerminal(message));
21114
- },
21115
- onReady: (info) => {
21116
- opts.broadcaster.broadcast(
21117
- buildWatcherStartedEvent({ roots: info.roots, debounceMs: info.debounceMs })
21118
- );
21119
- log.info(
21120
- tx(SERVER_TEXTS.watcherReady, {
21121
- roots: info.roots.join(","),
21122
- debounceMs: String(info.debounceMs)
21123
- })
21124
- );
21125
21948
  }
21949
+ };
21950
+ if (opts.debounceMsOverride !== void 0) {
21951
+ runtimeOpts.debounceMsOverride = opts.debounceMsOverride;
21126
21952
  }
21953
+ return runtimeOpts;
21127
21954
  };
21128
- if (opts.debounceMsOverride !== void 0) {
21129
- runtimeOpts.debounceMsOverride = opts.debounceMsOverride;
21130
- }
21131
- const handle = createWatcherRuntime(runtimeOpts);
21132
21955
  return {
21133
- start: handle.start,
21134
- stop: handle.stop
21956
+ async start() {
21957
+ currentRuntime = createWatcherRuntime(buildRuntimeOpts());
21958
+ await currentRuntime.start();
21959
+ },
21960
+ async stop() {
21961
+ if (currentRuntime) {
21962
+ await currentRuntime.stop();
21963
+ currentRuntime = null;
21964
+ }
21965
+ },
21966
+ async restart() {
21967
+ if (currentRuntime) {
21968
+ await currentRuntime.stop();
21969
+ currentRuntime = null;
21970
+ }
21971
+ currentRuntime = createWatcherRuntime(buildRuntimeOpts());
21972
+ await currentRuntime.start();
21973
+ }
21135
21974
  };
21136
21975
  }
21137
21976
  function buildBroadcasterEmitter(broadcaster) {
@@ -21158,7 +21997,7 @@ function registerScanRoute(app, deps) {
21158
21997
  }
21159
21998
  async function runPersistedScan(c, deps) {
21160
21999
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21161
- throw new HTTPException11(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
22000
+ throw new HTTPException13(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
21162
22001
  }
21163
22002
  const dbExists = await tryWithSqlite(
21164
22003
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -21187,7 +22026,7 @@ async function runPersistedScan(c, deps) {
21187
22026
  emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
21188
22027
  });
21189
22028
  if (outcome.kind !== "ok") {
21190
- throw new HTTPException11(500, {
22029
+ throw new HTTPException13(500, {
21191
22030
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
21192
22031
  });
21193
22032
  }
@@ -21195,7 +22034,7 @@ async function runPersistedScan(c, deps) {
21195
22034
  });
21196
22035
  } catch (err) {
21197
22036
  if (err instanceof ScanBusyError) {
21198
- throw new HTTPException11(409, { message: SERVER_TEXTS.scanPostBusy });
22037
+ throw new HTTPException13(409, { message: SERVER_TEXTS.scanPostBusy });
21199
22038
  }
21200
22039
  throw err;
21201
22040
  }
@@ -21259,7 +22098,7 @@ function groupTagsBySource2(rows) {
21259
22098
  }
21260
22099
  async function runFreshScan(deps) {
21261
22100
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21262
- throw new HTTPException11(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
22101
+ throw new HTTPException13(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
21263
22102
  }
21264
22103
  const resolveEnabledOverride = await buildBffResolverOverride(deps);
21265
22104
  const outcome = await runScanForCommand({
@@ -21288,7 +22127,7 @@ async function runFreshScan(deps) {
21288
22127
  printer: bffScanRunnerPrinter
21289
22128
  });
21290
22129
  if (outcome.kind !== "ok") {
21291
- throw new HTTPException11(500, {
22130
+ throw new HTTPException13(500, {
21292
22131
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
21293
22132
  });
21294
22133
  }
@@ -21322,8 +22161,8 @@ function emptyScanResult() {
21322
22161
  }
21323
22162
 
21324
22163
  // server/routes/sidecar.ts
21325
- import { HTTPException as HTTPException12 } from "hono/http-exception";
21326
- import { resolve as resolve33 } from "path";
22164
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
22165
+ import { resolve as resolve34 } from "path";
21327
22166
  var STATUS_FRESH = "fresh";
21328
22167
  var ENVELOPE_KIND2 = "sidecar.bumped";
21329
22168
  var BUMP_BODY_SCHEMA = {
@@ -21357,13 +22196,13 @@ function registerSidecarRoutes(app, deps) {
21357
22196
  let absPath;
21358
22197
  try {
21359
22198
  assertContained(deps.runtimeContext.cwd, node.path);
21360
- absPath = resolve33(deps.runtimeContext.cwd, node.path);
22199
+ absPath = resolve34(deps.runtimeContext.cwd, node.path);
21361
22200
  } catch (err) {
21362
- throw new HTTPException12(500, { message: formatErrorMessage(err) });
22201
+ throw new HTTPException14(500, { message: formatErrorMessage(err) });
21363
22202
  }
21364
22203
  const result = invokeBump2(node, absPath, body);
21365
22204
  if (result.report.ok === false && result.report.reason === "fresh") {
21366
- throw new HTTPException12(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
22205
+ throw new HTTPException14(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
21367
22206
  }
21368
22207
  if (result.report.ok === true && result.report.noop === true) {
21369
22208
  const envelope2 = {
@@ -21390,7 +22229,7 @@ function registerSidecarRoutes(app, deps) {
21390
22229
  }
21391
22230
  } catch (err) {
21392
22231
  if (err instanceof EConsentRequiredError) throw err;
21393
- throw new HTTPException12(500, { message: formatErrorMessage(err) });
22232
+ throw new HTTPException14(500, { message: formatErrorMessage(err) });
21394
22233
  }
21395
22234
  if (body.confirm === true) {
21396
22235
  deps.configService.reload();
@@ -21427,7 +22266,7 @@ async function loadNode(deps, nodePath) {
21427
22266
  );
21428
22267
  const node = persisted?.nodes.find((n) => n.path === nodePath);
21429
22268
  if (!node) {
21430
- throw new HTTPException12(404, {
22269
+ throw new HTTPException14(404, {
21431
22270
  message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
21432
22271
  });
21433
22272
  }
@@ -21435,7 +22274,7 @@ async function loadNode(deps, nodePath) {
21435
22274
  }
21436
22275
  function invokeBump2(node, absPath, body) {
21437
22276
  if (!bumpAction.invoke) {
21438
- throw new HTTPException12(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
22277
+ throw new HTTPException14(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
21439
22278
  }
21440
22279
  const input = {};
21441
22280
  if (body.force === true) input.force = true;
@@ -21481,9 +22320,9 @@ function registerUpdateStatusRoute(app, deps) {
21481
22320
  }
21482
22321
 
21483
22322
  // server/static.ts
21484
- import { existsSync as existsSync25 } from "fs";
22323
+ import { existsSync as existsSync28 } from "fs";
21485
22324
  import { readFile as readFile5 } from "fs/promises";
21486
- import { extname, join as join17 } from "path";
22325
+ import { extname, join as join18 } from "path";
21487
22326
  import { serveStatic } from "@hono/node-server/serve-static";
21488
22327
  var INDEX_HTML = "index.html";
21489
22328
  var PLACEHOLDER_HTML = `<!doctype html>
@@ -21535,8 +22374,8 @@ function createSpaFallback(opts) {
21535
22374
  return async (c, _next) => {
21536
22375
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
21537
22376
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
21538
- const indexPath = join17(opts.uiDist, INDEX_HTML);
21539
- if (!existsSync25(indexPath)) return htmlResponse(c, placeholder);
22377
+ const indexPath = join18(opts.uiDist, INDEX_HTML);
22378
+ if (!existsSync28(indexPath)) return htmlResponse(c, placeholder);
21540
22379
  return fileResponse(c, indexPath);
21541
22380
  };
21542
22381
  }
@@ -21622,13 +22461,13 @@ function attachBroadcasterRoute(app, broadcaster) {
21622
22461
 
21623
22462
  // server/app.ts
21624
22463
  var BODY_LIMIT_BYTES = 1024 * 1024;
21625
- var DbMissingError = class extends HTTPException13 {
22464
+ var DbMissingError = class extends HTTPException15 {
21626
22465
  constructor(message) {
21627
22466
  super(500, { message });
21628
22467
  this.name = "DbMissingError";
21629
22468
  }
21630
22469
  };
21631
- var BulkValidationError = class extends HTTPException13 {
22470
+ var BulkValidationError = class extends HTTPException15 {
21632
22471
  id;
21633
22472
  code;
21634
22473
  constructor(init) {
@@ -21638,7 +22477,7 @@ var BulkValidationError = class extends HTTPException13 {
21638
22477
  this.code = init.code;
21639
22478
  }
21640
22479
  };
21641
- var LoopbackGateError = class extends HTTPException13 {
22480
+ var LoopbackGateError = class extends HTTPException15 {
21642
22481
  code;
21643
22482
  constructor(init) {
21644
22483
  super(403, { message: init.message });
@@ -21658,7 +22497,7 @@ function createApp(deps) {
21658
22497
  bodyLimit({
21659
22498
  maxSize: BODY_LIMIT_BYTES,
21660
22499
  onError: () => {
21661
- throw new HTTPException13(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
22500
+ throw new HTTPException15(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
21662
22501
  }
21663
22502
  })
21664
22503
  );
@@ -21682,7 +22521,8 @@ function createApp(deps) {
21682
22521
  kindRegistry: deps.kindRegistry,
21683
22522
  contributionsRegistry: deps.contributionsRegistry,
21684
22523
  pluginRuntime: deps.pluginRuntime,
21685
- configService
22524
+ configService,
22525
+ watcherHolder: deps.watcherHolder
21686
22526
  };
21687
22527
  registerScanRoute(app, { ...routeDeps, broadcaster: deps.broadcaster });
21688
22528
  registerNodesRoutes(app, routeDeps);
@@ -21698,8 +22538,10 @@ function createApp(deps) {
21698
22538
  registerUpdateStatusRoute(app, routeDeps);
21699
22539
  registerPreferencesRoute(app, routeDeps);
21700
22540
  registerProjectPreferencesRoute(app, routeDeps);
22541
+ registerActiveProviderRoute(app, routeDeps);
22542
+ registerProjectIgnoreRoute(app, routeDeps);
21701
22543
  app.all("/api/*", (c) => {
21702
- throw new HTTPException13(404, {
22544
+ throw new HTTPException15(404, {
21703
22545
  message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
21704
22546
  });
21705
22547
  });
@@ -21707,7 +22549,7 @@ function createApp(deps) {
21707
22549
  app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
21708
22550
  app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
21709
22551
  app.notFound((c) => {
21710
- throw new HTTPException13(404, {
22552
+ throw new HTTPException15(404, {
21711
22553
  message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
21712
22554
  });
21713
22555
  });
@@ -21762,7 +22604,7 @@ function formatError2(err, c) {
21762
22604
  };
21763
22605
  return c.json(envelope, 403);
21764
22606
  }
21765
- if (err instanceof HTTPException13) {
22607
+ if (err instanceof HTTPException15) {
21766
22608
  const status = err.status;
21767
22609
  const envelope = {
21768
22610
  ok: false,
@@ -22105,10 +22947,10 @@ function validateNoUi(noUi, uiDist) {
22105
22947
  }
22106
22948
 
22107
22949
  // server/paths.ts
22108
- import { existsSync as existsSync26, statSync as statSync10 } from "fs";
22109
- import { dirname as dirname18, isAbsolute as isAbsolute9, join as join18, resolve as resolve34 } from "path";
22950
+ import { existsSync as existsSync29, statSync as statSync11 } from "fs";
22951
+ import { dirname as dirname18, isAbsolute as isAbsolute9, join as join19, resolve as resolve35 } from "path";
22110
22952
  import { fileURLToPath as fileURLToPath5 } from "url";
22111
- var DEFAULT_UI_REL = join18("ui", "dist", "ui", "browser");
22953
+ var DEFAULT_UI_REL = join19("ui", "dist", "ui", "browser");
22112
22954
  var PACKAGE_UI_REL = "ui";
22113
22955
  var INDEX_HTML2 = "index.html";
22114
22956
  function resolveDefaultUiDist(ctx) {
@@ -22117,13 +22959,13 @@ function resolveDefaultUiDist(ctx) {
22117
22959
  return walkUpForUi(ctx.cwd);
22118
22960
  }
22119
22961
  function resolveExplicitUiDist(ctx, raw) {
22120
- return isAbsolute9(raw) ? raw : resolve34(ctx.cwd, raw);
22962
+ return isAbsolute9(raw) ? raw : resolve35(ctx.cwd, raw);
22121
22963
  }
22122
22964
  function isUiBundleDir(path) {
22123
- if (!existsSync26(path)) return false;
22965
+ if (!existsSync29(path)) return false;
22124
22966
  try {
22125
- if (!statSync10(path).isDirectory()) return false;
22126
- return existsSync26(join18(path, INDEX_HTML2));
22967
+ if (!statSync11(path).isDirectory()) return false;
22968
+ return existsSync29(join19(path, INDEX_HTML2));
22127
22969
  } catch {
22128
22970
  return false;
22129
22971
  }
@@ -22140,9 +22982,9 @@ function resolvePackageBundledUi() {
22140
22982
  function resolvePackageBundledUiFrom(here) {
22141
22983
  let current = here;
22142
22984
  for (let i = 0; i < 8; i++) {
22143
- const candidate = join18(current, PACKAGE_UI_REL);
22985
+ const candidate = join19(current, PACKAGE_UI_REL);
22144
22986
  if (isUiBundleDir(candidate)) return candidate;
22145
- const distHere = join18(current, "dist", PACKAGE_UI_REL);
22987
+ const distHere = join19(current, "dist", PACKAGE_UI_REL);
22146
22988
  if (isUiBundleDir(distHere)) return distHere;
22147
22989
  const parent = dirname18(current);
22148
22990
  if (parent === current) return null;
@@ -22151,9 +22993,9 @@ function resolvePackageBundledUiFrom(here) {
22151
22993
  return null;
22152
22994
  }
22153
22995
  function walkUpForUi(startDir) {
22154
- let current = resolve34(startDir);
22996
+ let current = resolve35(startDir);
22155
22997
  for (let i = 0; i < 64; i++) {
22156
- const candidate = join18(current, DEFAULT_UI_REL);
22998
+ const candidate = join19(current, DEFAULT_UI_REL);
22157
22999
  if (isUiBundleDir(candidate)) return candidate;
22158
23000
  const parent = dirname18(current);
22159
23001
  if (parent === current) return null;
@@ -22169,6 +23011,7 @@ async function createServer(options, extra = {}) {
22169
23011
  const broadcaster = new WsBroadcaster();
22170
23012
  const { pluginRuntime, kindRegistry } = await assemblePluginRuntime(options, runtimeContext);
22171
23013
  const { kernel, contributionsRegistry } = assembleKernel(pluginRuntime, options.noBuiltIns);
23014
+ const watcherHolder = { current: null };
22172
23015
  const app = createApp({
22173
23016
  options,
22174
23017
  specVersion,
@@ -22177,6 +23020,7 @@ async function createServer(options, extra = {}) {
22177
23020
  kindRegistry,
22178
23021
  contributionsRegistry,
22179
23022
  pluginRuntime,
23023
+ watcherHolder,
22180
23024
  kernel
22181
23025
  });
22182
23026
  const wss = new WebSocketServer({ noServer: true });
@@ -22196,6 +23040,7 @@ async function createServer(options, extra = {}) {
22196
23040
  try {
22197
23041
  await candidate.start();
22198
23042
  watcherService = candidate;
23043
+ watcherHolder.current = candidate;
22199
23044
  } catch (err) {
22200
23045
  const message = formatErrorMessage(err);
22201
23046
  log.warn(
@@ -22390,7 +23235,8 @@ function renderBanner(input) {
22390
23235
  dbDisplay,
22391
23236
  pathDisplay: formatCwdPath(input.cwd),
22392
23237
  browserLine,
22393
- colorEnabled: input.colorEnabled
23238
+ colorEnabled: input.colorEnabled,
23239
+ referencePaths: input.referencePaths ?? []
22394
23240
  });
22395
23241
  }
22396
23242
  function resolveColorEnabled(opts) {
@@ -22447,11 +23293,23 @@ function renderFiglet(input) {
22447
23293
  lines.push(` ${dimOpen}Server${dimClose} ${greenUnderline}${input.url}${greenUnderlineClose}`);
22448
23294
  lines.push(` ${dimOpen}Path${dimClose} ${input.pathDisplay}`);
22449
23295
  lines.push(` ${dimOpen}DB${dimClose} ${input.dbDisplay}`);
23296
+ lines.push(...renderListRows("Refs", input.referencePaths, dimOpen, dimClose));
22450
23297
  lines.push("");
22451
23298
  lines.push(` ${dimOpen}${input.browserLine}${dimClose}`);
22452
23299
  lines.push("");
22453
23300
  return lines.join("\n") + "\n";
22454
23301
  }
23302
+ function renderListRows(label, values, dimOpen, dimClose) {
23303
+ if (values.length === 0) return [];
23304
+ const out = [];
23305
+ const labelPad = " ".repeat(Math.max(1, 9 - label.length));
23306
+ const continuationPad = " ".repeat(11);
23307
+ out.push(` ${dimOpen}${label}${dimClose}${labelPad}${sanitizeForTerminal(values[0])}`);
23308
+ for (let i = 1; i < values.length; i += 1) {
23309
+ out.push(`${continuationPad}${sanitizeForTerminal(values[i])}`);
23310
+ }
23311
+ return out;
23312
+ }
22455
23313
  var EMPTY_ANSI = {
22456
23314
  dimOpen: "",
22457
23315
  dimClose: "",
@@ -22571,7 +23429,7 @@ var ServeCommand = class extends SmCommand {
22571
23429
  return ExitCode.Error;
22572
23430
  }
22573
23431
  const dbPath = resolveDbPath({ db: this.db, ...runtimeCtx });
22574
- if (this.db !== void 0 && !existsSync27(dbPath)) {
23432
+ if (this.db !== void 0 && !existsSync30(dbPath)) {
22575
23433
  this.printer.info(
22576
23434
  tx(SERVE_TEXTS.dbNotFound, { path: sanitizeForTerminal(dbPath) })
22577
23435
  );
@@ -22647,6 +23505,12 @@ var ServeCommand = class extends SmCommand {
22647
23505
  noColorFlag: this.noColor,
22648
23506
  env: process.env
22649
23507
  });
23508
+ let referencePaths = [];
23509
+ try {
23510
+ const cfg = loadConfig({ cwd: runtimeCtx.cwd }).effective;
23511
+ referencePaths = cfg.scan.referencePaths;
23512
+ } catch {
23513
+ }
22650
23514
  this.printer.info(
22651
23515
  renderBanner({
22652
23516
  version: VERSION,
@@ -22656,7 +23520,8 @@ var ServeCommand = class extends SmCommand {
22656
23520
  cwd: runtimeCtx.cwd,
22657
23521
  openBrowser: validation.options.open,
22658
23522
  isTTY,
22659
- colorEnabled
23523
+ colorEnabled,
23524
+ referencePaths
22660
23525
  })
22661
23526
  );
22662
23527
  if (validation.options.open) {
@@ -22975,22 +23840,27 @@ function renderLinksSection(direction, links, ansi) {
22975
23840
  const aggregated = aggregateLinks(links, projectField);
22976
23841
  const headerTpl = direction === "out" ? SHOW_TEXTS.linksOutSection : SHOW_TEXTS.linksInSection;
22977
23842
  const kindWidth = Math.max(...aggregated.map((g) => g.kind.length));
22978
- const confWidth = Math.max(...aggregated.map((g) => g.confidence.length));
23843
+ const confLabels = aggregated.map((g) => formatConfidence(g.confidence));
23844
+ const confWidth = Math.max(...confLabels.map((l) => l.length));
22979
23845
  const lines = [tx(headerTpl, { count: links.length })];
22980
- for (const grp of aggregated) {
23846
+ aggregated.forEach((grp, idx) => {
22981
23847
  const dup = grp.rowCount > 1 ? ansi.dim(tx(SHOW_TEXTS.linkDup, { count: grp.rowCount })) : "";
22982
23848
  lines.push(
22983
23849
  tx(SHOW_TEXTS.linkRow, {
22984
23850
  arrow: ansi.dim(arrow),
22985
23851
  kind: sanitizeForTerminal(grp.kind).padEnd(kindWidth),
22986
- confidence: ansi.dim(grp.confidence.padEnd(confWidth)),
23852
+ confidence: ansi.dim(confLabels[idx].padEnd(confWidth)),
22987
23853
  endpoint: sanitizeForTerminal(grp.endpoint),
22988
23854
  dup
22989
23855
  })
22990
23856
  );
22991
- }
23857
+ });
22992
23858
  return lines.join("");
22993
23859
  }
23860
+ function formatConfidence(c) {
23861
+ if (typeof c !== "number" || !Number.isFinite(c)) return "?";
23862
+ return `${Math.round(c * 100)}%`;
23863
+ }
22994
23864
  function renderIssuesSection(issues, nodePath, ansi) {
22995
23865
  const lines = [tx(SHOW_TEXTS.issuesSection, { count: issues.length })];
22996
23866
  const analyzerWidth = Math.max(
@@ -23057,18 +23927,13 @@ function aggregateLinks(links, endpointSide) {
23057
23927
  return a.kind.localeCompare(b.kind);
23058
23928
  });
23059
23929
  }
23060
- var CONFIDENCE_RANK = {
23061
- high: 2,
23062
- medium: 1,
23063
- low: 0
23064
- };
23065
23930
  function rankConfidenceForGrouping(c) {
23066
- return CONFIDENCE_RANK[c];
23931
+ return c;
23067
23932
  }
23068
23933
 
23069
23934
  // cli/commands/sidecar.ts
23070
- import { existsSync as existsSync28, unlinkSync as unlinkSync2 } from "fs";
23071
- import { resolve as resolve35 } from "path";
23935
+ import { existsSync as existsSync31, unlinkSync as unlinkSync2 } from "fs";
23936
+ import { resolve as resolve36 } from "path";
23072
23937
  import { Command as Command35, Option as Option33 } from "clipanion";
23073
23938
 
23074
23939
  // cli/i18n/sidecar.texts.ts
@@ -23219,7 +24084,7 @@ var SidecarRefreshCommand = class extends SmCommand {
23219
24084
  let absPath;
23220
24085
  try {
23221
24086
  assertContained(ctx.cwd, node.path);
23222
- absPath = resolve35(ctx.cwd, node.path);
24087
+ absPath = resolve36(ctx.cwd, node.path);
23223
24088
  } catch (err) {
23224
24089
  this.printer.error(
23225
24090
  tx(SIDECAR_TEXTS.refreshFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -23500,7 +24365,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23500
24365
  let absPath;
23501
24366
  try {
23502
24367
  assertContained(ctx.cwd, node.path);
23503
- absPath = resolve35(ctx.cwd, node.path);
24368
+ absPath = resolve36(ctx.cwd, node.path);
23504
24369
  } catch (err) {
23505
24370
  this.printer.error(
23506
24371
  tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -23508,7 +24373,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23508
24373
  return ExitCode.Error;
23509
24374
  }
23510
24375
  const sidecarAbsPath = sidecarPathFor(absPath);
23511
- if (existsSync28(sidecarAbsPath) && this.force !== true) {
24376
+ if (existsSync31(sidecarAbsPath) && this.force !== true) {
23512
24377
  this.printer.error(
23513
24378
  tx(SIDECAR_TEXTS.annotateExists, {
23514
24379
  glyph: errGlyph,
@@ -23518,7 +24383,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23518
24383
  );
23519
24384
  return ExitCode.Error;
23520
24385
  }
23521
- if (existsSync28(sidecarAbsPath) && this.force === true) {
24386
+ if (existsSync31(sidecarAbsPath) && this.force === true) {
23522
24387
  try {
23523
24388
  unlinkSync2(sidecarAbsPath);
23524
24389
  } catch (err) {
@@ -23748,8 +24613,8 @@ var STUB_COMMANDS = [
23748
24613
  ];
23749
24614
 
23750
24615
  // cli/commands/tutorial.ts
23751
- import { cpSync as cpSync2, existsSync as existsSync29, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync11 } from "fs";
23752
- import { dirname as dirname19, join as join19, resolve as resolve36 } from "path";
24616
+ import { cpSync as cpSync2, existsSync as existsSync32, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync12 } from "fs";
24617
+ import { dirname as dirname19, join as join20, resolve as resolve37 } from "path";
23753
24618
  import { fileURLToPath as fileURLToPath6 } from "url";
23754
24619
  import { Command as Command37, Option as Option35 } from "clipanion";
23755
24620
 
@@ -23845,9 +24710,9 @@ var TutorialCommand = class extends SmCommand {
23845
24710
  }
23846
24711
  const variant = rawVariant ?? DEFAULT_VARIANT;
23847
24712
  const spec = VARIANT_SPECS[variant];
23848
- const targetDir = join19(ctx.cwd, ".claude", "skills", spec.slug);
24713
+ const targetDir = join20(ctx.cwd, ".claude", "skills", spec.slug);
23849
24714
  const targetDisplay = `.claude/skills/${spec.slug}/`;
23850
- if (existsSync29(targetDir) && !this.force) {
24715
+ if (existsSync32(targetDir) && !this.force) {
23851
24716
  this.printer.error(
23852
24717
  tx(TUTORIAL_TEXTS.alreadyExists, {
23853
24718
  glyph: errGlyph,
@@ -23923,14 +24788,14 @@ function resolveSkillSourceDir(variant) {
23923
24788
  const here = dirname19(fileURLToPath6(import.meta.url));
23924
24789
  const candidates = [
23925
24790
  // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
23926
- resolve36(here, "../../..", spec.sourceDir),
24791
+ resolve37(here, "../../..", spec.sourceDir),
23927
24792
  // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
23928
- resolve36(here, "cli/tutorial", spec.slug),
24793
+ resolve37(here, "cli/tutorial", spec.slug),
23929
24794
  // bundled fallback: any-depth → cli/tutorial/<slug>
23930
- resolve36(here, "../cli/tutorial", spec.slug)
24795
+ resolve37(here, "../cli/tutorial", spec.slug)
23931
24796
  ];
23932
24797
  for (const candidate of candidates) {
23933
- if (existsSync29(candidate) && statSync11(candidate).isDirectory()) {
24798
+ if (existsSync32(candidate) && statSync12(candidate).isDirectory()) {
23934
24799
  cachedSourceDirs.set(variant, candidate);
23935
24800
  return candidate;
23936
24801
  }
@@ -24107,7 +24972,7 @@ await lifecycleDispatcher.dispatch(
24107
24972
  process.exit(exitCode);
24108
24973
  function resolveBareDefault() {
24109
24974
  const ctx = defaultRuntimeContext();
24110
- if (existsSync30(defaultProjectDbPath(ctx))) {
24975
+ if (existsSync33(defaultProjectDbPath(ctx))) {
24111
24976
  return ["serve"];
24112
24977
  }
24113
24978
  process.stderr.write(tx(ENTRY_TEXTS.bareNoProject, { cwd: ctx.cwd }));