@skill-map/cli 0.32.0 → 0.34.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 existsSync31 } 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
@@ -506,11 +531,181 @@ var claudeProvider = {
506
531
  const lower = path.toLowerCase();
507
532
  if (lower.startsWith(".claude/agents/")) return "agent";
508
533
  if (lower.startsWith(".claude/commands/")) return "command";
509
- if (lower.startsWith(".claude/skills/")) return "skill";
534
+ if (/^\.claude\/skills\/[^/]+\/skill\.md$/.test(lower)) return "skill";
510
535
  return null;
511
536
  }
512
537
  };
513
538
 
539
+ // plugins/claude/extractors/at-directive/index.ts
540
+ import { posix as pathPosix } from "path";
541
+
542
+ // kernel/util/strip-code-blocks.ts
543
+ var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
544
+ function stripCodeBlocks(input) {
545
+ if (!input) return input;
546
+ const fenceless = stripFences(input);
547
+ return stripInline(fenceless);
548
+ }
549
+ function stripFences(input) {
550
+ const out = [];
551
+ const lines = input.split("\n");
552
+ let openFence = null;
553
+ for (const line of lines) {
554
+ if (openFence) {
555
+ const closer = matchClosingFence(line, openFence);
556
+ if (closer) {
557
+ out.push(blank(line));
558
+ openFence = null;
559
+ } else {
560
+ out.push(blank(line));
561
+ }
562
+ continue;
563
+ }
564
+ const open = FENCE_RE.exec(line);
565
+ if (open?.groups) {
566
+ openFence = open.groups["fence"];
567
+ out.push(blank(line));
568
+ continue;
569
+ }
570
+ out.push(line);
571
+ }
572
+ return out.join("\n");
573
+ }
574
+ function matchClosingFence(line, openFence) {
575
+ const m = FENCE_RE.exec(line);
576
+ if (!m?.groups) return false;
577
+ const fence = m.groups["fence"];
578
+ return fence[0] === openFence[0] && fence.length >= openFence.length;
579
+ }
580
+ function stripInline(input) {
581
+ return input.replace(/(`+)([\s\S]*?)\1/g, (_full, ticks, body) => {
582
+ return ticks.replace(/`/g, " ") + blank(body) + ticks.replace(/`/g, " ");
583
+ });
584
+ }
585
+ function blank(s) {
586
+ return s.replace(/[^\s]/g, " ");
587
+ }
588
+
589
+ // kernel/trigger-normalize.ts
590
+ function normalizeTrigger(source) {
591
+ let out = source.normalize("NFD");
592
+ out = out.replace(new RegExp("\\p{Mn}+", "gu"), "");
593
+ out = out.toLowerCase();
594
+ out = out.replace(/[-_\s]+/g, " ");
595
+ out = out.replace(/ +/g, " ");
596
+ return out.trim();
597
+ }
598
+
599
+ // plugins/claude/extractors/at-directive/index.ts
600
+ var ID = "at-directive";
601
+ 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;
602
+ 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;
603
+ var atDirectiveExtractor = {
604
+ id: ID,
605
+ pluginId: "claude",
606
+ kind: "extractor",
607
+ version: "1.0.0",
608
+ 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.",
609
+ scope: "body",
610
+ precondition: { provider: ["claude"] },
611
+ extract(ctx) {
612
+ const seenMentions = /* @__PURE__ */ new Set();
613
+ const seenReferences = /* @__PURE__ */ new Set();
614
+ const body = stripCodeBlocks(ctx.body);
615
+ const sourceDir = pathPosix.dirname(ctx.node.path);
616
+ for (const match of body.matchAll(AT_RE)) {
617
+ const original = match[1];
618
+ const bare = original.slice(1);
619
+ if (bare.startsWith("/")) continue;
620
+ const isReference = bare.startsWith("./") || bare.startsWith("../") || FILE_EXT_RE.test(bare);
621
+ if (isReference) {
622
+ const target = resolveSourceRelative(sourceDir, bare);
623
+ const dedupKey = target.toLowerCase();
624
+ if (seenReferences.has(dedupKey)) continue;
625
+ seenReferences.add(dedupKey);
626
+ ctx.emitLink({
627
+ source: ctx.node.path,
628
+ target,
629
+ kind: "references",
630
+ // 0.85: strong file signal (path prefix `./` / `../` OR
631
+ // a known file extension on the tail). One degree of inference
632
+ // (the runtime still resolves the path).
633
+ confidence: 0.85,
634
+ sources: [ID],
635
+ trigger: {
636
+ originalTrigger: original,
637
+ normalizedTrigger: target
638
+ }
639
+ });
640
+ continue;
641
+ }
642
+ const normalized = normalizeTrigger(original);
643
+ if (seenMentions.has(normalized)) continue;
644
+ seenMentions.add(normalized);
645
+ ctx.emitLink({
646
+ source: ctx.node.path,
647
+ target: original,
648
+ kind: "mentions",
649
+ // 0.5: genuine ambiguity. A bare `@handle` (no extension, no
650
+ // path prefix) could be an agent, a handle, or generic prose.
651
+ // The runtime decides at invocation time; the extractor leaves
652
+ // the question open.
653
+ confidence: 0.5,
654
+ sources: [ID],
655
+ trigger: {
656
+ originalTrigger: original,
657
+ normalizedTrigger: normalized
658
+ }
659
+ });
660
+ }
661
+ }
662
+ };
663
+ function resolveSourceRelative(sourceDir, bare) {
664
+ const joined = sourceDir === "." ? bare : `${sourceDir}/${bare}`;
665
+ return pathPosix.normalize(joined);
666
+ }
667
+
668
+ // plugins/claude/extractors/slash/index.ts
669
+ var ID2 = "slash";
670
+ var SLASH_RE = /(?<![A-Za-z0-9_/.:?#=&])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
671
+ var slashExtractor = {
672
+ id: ID2,
673
+ pluginId: "claude",
674
+ kind: "extractor",
675
+ version: "1.0.0",
676
+ 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.",
677
+ scope: "body",
678
+ precondition: { provider: ["claude"] },
679
+ extract(ctx) {
680
+ const seen = /* @__PURE__ */ new Set();
681
+ const body = stripCodeBlocks(ctx.body);
682
+ for (const match of body.matchAll(SLASH_RE)) {
683
+ const original = match[1];
684
+ const endIdx = (match.index ?? 0) + match[0].length;
685
+ const nextChar = body[endIdx];
686
+ if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
687
+ const normalized = normalizeTrigger(original);
688
+ if (seen.has(normalized)) continue;
689
+ seen.add(normalized);
690
+ ctx.emitLink({
691
+ source: ctx.node.path,
692
+ target: original,
693
+ kind: "invokes",
694
+ // 0.8: clean `/command` match after code-block strip. The
695
+ // post-match path guard above filters URL / file-path noise,
696
+ // so a hit is unambiguous syntax. Resolution against the live
697
+ // skill / command catalog happens downstream.
698
+ confidence: 0.8,
699
+ sources: [ID2],
700
+ trigger: {
701
+ originalTrigger: original,
702
+ normalizedTrigger: normalized
703
+ }
704
+ });
705
+ }
706
+ }
707
+ };
708
+
514
709
  // plugins/gemini/providers/gemini/schemas/agent.schema.json
515
710
  var agent_schema_default2 = {
516
711
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -624,7 +819,87 @@ var geminiProvider = {
624
819
  classify(path) {
625
820
  const lower = path.toLowerCase();
626
821
  if (lower.startsWith(".gemini/agents/")) return "agent";
627
- if (lower.startsWith(".gemini/skills/")) return "skill";
822
+ if (/^\.gemini\/skills\/[^/]+\/skill\.md$/.test(lower)) return "skill";
823
+ return null;
824
+ }
825
+ };
826
+
827
+ // plugins/openai/providers/openai/schemas/agent.schema.json
828
+ var agent_schema_default3 = {
829
+ $schema: "https://json-schema.org/draft/2020-12/schema",
830
+ $id: "https://skill-map.dev/providers/openai/v1/frontmatter/agent.schema.json",
831
+ title: "FrontmatterCodexAgent",
832
+ 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.",
833
+ allOf: [
834
+ { $ref: "https://skill-map.dev/spec/v0/frontmatter/base.schema.json" }
835
+ ],
836
+ type: "object",
837
+ additionalProperties: true,
838
+ properties: {
839
+ name: {
840
+ type: "string",
841
+ minLength: 1,
842
+ description: "Sub-agent identifier. Conventionally matches the filename stem."
843
+ },
844
+ description: {
845
+ type: "string",
846
+ description: "Short description of when this sub-agent applies. Codex surfaces this in the agent picker; skill-map mirrors it in the card."
847
+ },
848
+ model: {
849
+ type: "string",
850
+ description: "Model identifier (`gpt-4o`, `o3-mini`, etc.) the sub-agent runs against."
851
+ },
852
+ instructions: {
853
+ type: "string",
854
+ description: "Multi-line prompt body (TOML triple-quoted string)."
855
+ },
856
+ tools: {
857
+ type: "array",
858
+ items: { type: "string" },
859
+ description: "Tool ids this sub-agent is allowed to call."
860
+ },
861
+ mcp_servers: {
862
+ type: "array",
863
+ items: { type: "string" },
864
+ description: "MCP server ids attached to this sub-agent (`tools: [mcp__<server>__*]` follows the same pattern as Claude)."
865
+ },
866
+ approval_policy: {
867
+ type: "string",
868
+ enum: ["never", "on-request", "untrusted"],
869
+ description: "Codex approval policy for this sub-agent's destructive operations."
870
+ },
871
+ sandbox_mode: {
872
+ type: "string",
873
+ enum: ["read-only", "workspace-write", "danger-full-access"],
874
+ description: "Codex sandbox mode the sub-agent runs under."
875
+ }
876
+ }
877
+ };
878
+
879
+ // plugins/openai/providers/openai/index.ts
880
+ var openaiProvider = {
881
+ id: "openai",
882
+ pluginId: "openai",
883
+ kind: "provider",
884
+ version: "1.0.0",
885
+ description: "Walks OpenAI Codex CLI scope conventions (.codex/agents/*.toml).",
886
+ read: { extensions: [".toml"], parser: "toml" },
887
+ kinds: {
888
+ agent: {
889
+ schema: "./schemas/agent.schema.json",
890
+ schemaJson: agent_schema_default3,
891
+ ui: {
892
+ label: "Codex agents",
893
+ // Codex green; distinct from claude / gemini palettes.
894
+ color: "#22c55e",
895
+ colorDark: "#4ade80",
896
+ icon: { kind: "pi", id: "pi-bolt" }
897
+ }
898
+ }
899
+ },
900
+ classify(path) {
901
+ const lower = path.toLowerCase();
902
+ if (lower.startsWith(".codex/agents/") && lower.endsWith(".toml")) return "agent";
628
903
  return null;
629
904
  }
630
905
  };
@@ -664,7 +939,7 @@ var agentSkillsProvider = {
664
939
  }
665
940
  },
666
941
  classify(path) {
667
- if (path.toLowerCase().startsWith(".agents/skills/")) return "skill";
942
+ if (/^\.agents\/skills\/[^/]+\/skill\.md$/.test(path.toLowerCase())) return "skill";
668
943
  return null;
669
944
  }
670
945
  };
@@ -722,9 +997,9 @@ var coreMarkdownProvider = {
722
997
  };
723
998
 
724
999
  // plugins/core/extractors/annotations/index.ts
725
- var ID = "annotations";
1000
+ var ID3 = "annotations";
726
1001
  var annotationsExtractor = {
727
- id: ID,
1002
+ id: ID3,
728
1003
  pluginId: "core",
729
1004
  kind: "extractor",
730
1005
  version: "1.0.0",
@@ -770,128 +1045,17 @@ function link(source, target) {
770
1045
  source,
771
1046
  target,
772
1047
  kind: "supersedes",
773
- confidence: "high",
774
- sources: [ID]
1048
+ confidence: 1,
1049
+ sources: [ID3]
775
1050
  };
776
1051
  }
777
1052
 
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
- };
888
-
889
1053
  // plugins/core/extractors/external-url-counter/index.ts
890
- var ID3 = "external-url-counter";
1054
+ var ID4 = "external-url-counter";
891
1055
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
892
1056
  var TRAILING_PUNCT = /[.,;:!?]+$/;
893
1057
  var externalUrlCounterExtractor = {
894
- id: ID3,
1058
+ id: ID4,
895
1059
  pluginId: "core",
896
1060
  kind: "extractor",
897
1061
  version: "1.0.0",
@@ -937,8 +1101,8 @@ var externalUrlCounterExtractor = {
937
1101
  source: ctx.node.path,
938
1102
  target: normalized,
939
1103
  kind: "references",
940
- confidence: "low",
941
- sources: [ID3],
1104
+ confidence: 0.3,
1105
+ sources: [ID4],
942
1106
  trigger: {
943
1107
  originalTrigger: original,
944
1108
  normalizedTrigger: normalized
@@ -984,12 +1148,12 @@ function lineFor(lineStarts, offset) {
984
1148
  }
985
1149
 
986
1150
  // plugins/core/extractors/markdown-link/index.ts
987
- import { posix as pathPosix } from "path";
988
- var ID4 = "markdown-link";
1151
+ import { posix as pathPosix2 } from "path";
1152
+ var ID5 = "markdown-link";
989
1153
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
990
1154
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
991
1155
  var markdownLinkExtractor = {
992
- id: ID4,
1156
+ id: ID5,
993
1157
  pluginId: "core",
994
1158
  kind: "extractor",
995
1159
  version: "1.0.0",
@@ -998,7 +1162,7 @@ var markdownLinkExtractor = {
998
1162
  extract(ctx) {
999
1163
  const seen = /* @__PURE__ */ new Set();
1000
1164
  const lineStarts = computeLineStarts2(ctx.body);
1001
- const sourceDir = pathPosix.dirname(ctx.node.path);
1165
+ const sourceDir = pathPosix2.dirname(ctx.node.path);
1002
1166
  for (const match of ctx.body.matchAll(LINK_RE)) {
1003
1167
  const original = match[2];
1004
1168
  const resolved = resolveTarget(sourceDir, original);
@@ -1010,8 +1174,8 @@ var markdownLinkExtractor = {
1010
1174
  source: ctx.node.path,
1011
1175
  target: resolved,
1012
1176
  kind: "references",
1013
- confidence: "high",
1014
- sources: [ID4],
1177
+ confidence: 0.95,
1178
+ sources: [ID5],
1015
1179
  trigger: {
1016
1180
  originalTrigger: original,
1017
1181
  normalizedTrigger: resolved
@@ -1030,7 +1194,7 @@ function resolveTarget(sourceDir, raw) {
1030
1194
  if (URL_SCHEME_RE.test(trimmed)) return null;
1031
1195
  if (trimmed.startsWith("/")) return null;
1032
1196
  const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1033
- return pathPosix.normalize(joined);
1197
+ return pathPosix2.normalize(joined);
1034
1198
  }
1035
1199
  function computeLineStarts2(body) {
1036
1200
  const starts = [0];
@@ -1050,47 +1214,61 @@ function lineFor2(lineStarts, offset) {
1050
1214
  return lo + 1;
1051
1215
  }
1052
1216
 
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,
1217
+ // plugins/core/extractors/mcp-tools/index.ts
1218
+ var ID6 = "mcp-tools";
1219
+ var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
1220
+ var mcpToolsExtractor = {
1221
+ id: ID6,
1058
1222
  pluginId: "core",
1059
1223
  kind: "extractor",
1060
1224
  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",
1225
+ 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.",
1226
+ scope: "frontmatter",
1063
1227
  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);
1228
+ const raw = ctx.frontmatter["tools"];
1229
+ if (!Array.isArray(raw)) return;
1230
+ const servers = collectMcpServers(raw);
1231
+ if (servers.size === 0) return;
1232
+ for (const server of servers) {
1233
+ const mcpPath = `mcp://${server}`;
1234
+ ctx.emitNode({
1235
+ path: mcpPath,
1236
+ kind: "mcp",
1237
+ virtual: true,
1238
+ provider: ctx.node.provider,
1239
+ derivedFrom: [ctx.node.path],
1240
+ frontmatter: { name: server }
1241
+ });
1074
1242
  ctx.emitLink({
1075
1243
  source: ctx.node.path,
1076
- target: original,
1077
- kind: "invokes",
1078
- confidence: "medium",
1079
- sources: [ID5],
1244
+ target: mcpPath,
1245
+ kind: "references",
1246
+ confidence: 0.85,
1247
+ sources: [ID6],
1080
1248
  trigger: {
1081
- originalTrigger: original,
1082
- normalizedTrigger: normalized
1249
+ originalTrigger: `mcp__${server}__*`,
1250
+ normalizedTrigger: mcpPath
1083
1251
  }
1084
1252
  });
1085
1253
  }
1086
1254
  }
1087
1255
  };
1256
+ function collectMcpServers(tools) {
1257
+ const out = /* @__PURE__ */ new Set();
1258
+ for (const t of tools) {
1259
+ if (typeof t !== "string" || t.length === 0) continue;
1260
+ const match = MCP_PATTERN.exec(t);
1261
+ if (!match) continue;
1262
+ out.add(match[1].toLowerCase());
1263
+ }
1264
+ return out;
1265
+ }
1088
1266
 
1089
1267
  // plugins/core/extractors/tools-count/index.ts
1090
- var ID6 = "tools-count";
1268
+ var ID7 = "tools-count";
1091
1269
  var TOOLTIP_MAX = 255;
1092
1270
  var toolsCountExtractor = {
1093
- id: ID6,
1271
+ id: ID7,
1094
1272
  pluginId: "core",
1095
1273
  kind: "extractor",
1096
1274
  version: "1.0.0",
@@ -1133,9 +1311,9 @@ var ANNOTATION_ORPHAN_TEXTS = {
1133
1311
  };
1134
1312
 
1135
1313
  // plugins/core/analyzers/annotation-orphan/index.ts
1136
- var ID7 = "annotation-orphan";
1314
+ var ID8 = "annotation-orphan";
1137
1315
  var annotationOrphanAnalyzer = {
1138
- id: ID7,
1316
+ id: ID8,
1139
1317
  pluginId: "core",
1140
1318
  kind: "analyzer",
1141
1319
  version: "1.0.0",
@@ -1148,7 +1326,7 @@ var annotationOrphanAnalyzer = {
1148
1326
  for (const orphan of orphans) {
1149
1327
  const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1150
1328
  issues.push({
1151
- analyzerId: ID7,
1329
+ analyzerId: ID8,
1152
1330
  severity: "warn",
1153
1331
  nodeIds: [expectedMdRelative],
1154
1332
  message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
@@ -1185,9 +1363,9 @@ var ANNOTATION_STALE_TEXTS = {
1185
1363
  };
1186
1364
 
1187
1365
  // plugins/core/analyzers/annotation-stale/index.ts
1188
- var ID8 = "annotation-stale";
1366
+ var ID9 = "annotation-stale";
1189
1367
  var annotationStaleAnalyzer = {
1190
- id: ID8,
1368
+ id: ID9,
1191
1369
  pluginId: "core",
1192
1370
  kind: "analyzer",
1193
1371
  version: "1.0.0",
@@ -1222,7 +1400,7 @@ var annotationStaleAnalyzer = {
1222
1400
  if (status === "fresh") continue;
1223
1401
  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
1402
  issues.push({
1225
- analyzerId: ID8,
1403
+ analyzerId: ID9,
1226
1404
  severity: "warn",
1227
1405
  nodeIds: [node.path],
1228
1406
  message,
@@ -1249,7 +1427,7 @@ function tooltipFor(status) {
1249
1427
  }
1250
1428
 
1251
1429
  // plugins/core/analyzers/broken-ref/index.ts
1252
- import { posix as pathPosix2, resolve } from "path";
1430
+ import { posix as pathPosix3, resolve } from "path";
1253
1431
 
1254
1432
  // plugins/core/analyzers/broken-ref/text.ts
1255
1433
  var BROKEN_REF_TEXTS = {
@@ -1268,9 +1446,9 @@ var BROKEN_REF_TEXTS = {
1268
1446
  };
1269
1447
 
1270
1448
  // plugins/core/analyzers/broken-ref/index.ts
1271
- var ID9 = "broken-ref";
1449
+ var ID10 = "broken-ref";
1272
1450
  var brokenRefAnalyzer = {
1273
- id: ID9,
1451
+ id: ID10,
1274
1452
  pluginId: "core",
1275
1453
  kind: "analyzer",
1276
1454
  version: "1.0.0",
@@ -1339,7 +1517,7 @@ function buildIssue(link2, hintCandidates = []) {
1339
1517
  trigger: link2.trigger?.normalizedTrigger ?? null
1340
1518
  };
1341
1519
  const issue = {
1342
- analyzerId: ID9,
1520
+ analyzerId: ID10,
1343
1521
  severity: "warn",
1344
1522
  nodeIds: [link2.source],
1345
1523
  message: tx(BROKEN_REF_TEXTS.message, {
@@ -1388,8 +1566,8 @@ function indexByNormalizedName(nodes) {
1388
1566
  return out;
1389
1567
  }
1390
1568
  function basenameWithoutExt(path) {
1391
- const base = pathPosix2.basename(path);
1392
- const ext = pathPosix2.extname(base);
1569
+ const base = pathPosix3.basename(path);
1570
+ const ext = pathPosix3.extname(base);
1393
1571
  return ext ? base.slice(0, -ext.length) : base;
1394
1572
  }
1395
1573
  function indexByBasenameWithoutName(nodes) {
@@ -1433,9 +1611,9 @@ function isPathStyleLink(link2) {
1433
1611
  }
1434
1612
 
1435
1613
  // plugins/core/analyzers/contribution-orphan/index.ts
1436
- var ID10 = "contribution-orphan";
1614
+ var ID11 = "contribution-orphan";
1437
1615
  var contributionOrphanAnalyzer = {
1438
- id: ID10,
1616
+ id: ID11,
1439
1617
  pluginId: "core",
1440
1618
  kind: "analyzer",
1441
1619
  version: "0.0.0",
@@ -1456,9 +1634,9 @@ var JOB_ORPHAN_FILE_TEXTS = {
1456
1634
  };
1457
1635
 
1458
1636
  // plugins/core/analyzers/job-orphan-file/index.ts
1459
- var ID11 = "job-orphan-file";
1637
+ var ID12 = "job-orphan-file";
1460
1638
  var jobOrphanFileAnalyzer = {
1461
- id: ID11,
1639
+ id: ID12,
1462
1640
  pluginId: "core",
1463
1641
  kind: "analyzer",
1464
1642
  version: "1.0.0",
@@ -1470,7 +1648,7 @@ var jobOrphanFileAnalyzer = {
1470
1648
  const issues = [];
1471
1649
  for (const filePath of orphans) {
1472
1650
  issues.push({
1473
- analyzerId: ID11,
1651
+ analyzerId: ID12,
1474
1652
  severity: "warn",
1475
1653
  nodeIds: [filePath],
1476
1654
  message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
@@ -1488,9 +1666,9 @@ var LINK_CONFLICT_TEXTS = {
1488
1666
  };
1489
1667
 
1490
1668
  // plugins/core/analyzers/link-conflict/index.ts
1491
- var ID12 = "link-conflict";
1669
+ var ID13 = "link-conflict";
1492
1670
  var linkConflictAnalyzer = {
1493
- id: ID12,
1671
+ id: ID13,
1494
1672
  pluginId: "core",
1495
1673
  kind: "analyzer",
1496
1674
  version: "1.0.0",
@@ -1538,7 +1716,7 @@ var linkConflictAnalyzer = {
1538
1716
  const [source, target] = key.split("\0");
1539
1717
  const kindList = variants.map((v) => v.kind).join(" / ");
1540
1718
  issues.push({
1541
- analyzerId: ID12,
1719
+ analyzerId: ID13,
1542
1720
  severity: "warn",
1543
1721
  nodeIds: [source, target],
1544
1722
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -1553,14 +1731,7 @@ var linkConflictAnalyzer = {
1553
1731
  }
1554
1732
  };
1555
1733
  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
- }
1734
+ return c;
1564
1735
  }
1565
1736
 
1566
1737
  // kernel/util/trigger-resolve.ts
@@ -1612,9 +1783,9 @@ function resolveLinkTargetToPath(link2, nameIndex) {
1612
1783
  }
1613
1784
 
1614
1785
  // plugins/core/analyzers/link-counts/index.ts
1615
- var ID13 = "link-counts";
1786
+ var ID14 = "link-counts";
1616
1787
  var linkCountsAnalyzer = {
1617
- id: ID13,
1788
+ id: ID14,
1618
1789
  pluginId: "core",
1619
1790
  kind: "analyzer",
1620
1791
  version: "1.0.0",
@@ -1678,11 +1849,11 @@ function formatBreakdown(byKind, direction) {
1678
1849
  }
1679
1850
 
1680
1851
  // plugins/core/analyzers/stability/index.ts
1681
- var ID14 = "stability";
1852
+ var ID15 = "stability";
1682
1853
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
1683
1854
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
1684
1855
  var stabilityAnalyzer = {
1685
- id: ID14,
1856
+ id: ID15,
1686
1857
  pluginId: "core",
1687
1858
  kind: "analyzer",
1688
1859
  version: "1.0.0",
@@ -1714,7 +1885,7 @@ var stabilityAnalyzer = {
1714
1885
  tooltip: EXPERIMENTAL_TOOLTIP
1715
1886
  });
1716
1887
  issues.push({
1717
- analyzerId: ID14,
1888
+ analyzerId: ID15,
1718
1889
  severity: "info",
1719
1890
  nodeIds: [node.path],
1720
1891
  message: `Node '${node.path}' is marked experimental: API may change.`,
@@ -1727,7 +1898,7 @@ var stabilityAnalyzer = {
1727
1898
  severity: "warn"
1728
1899
  });
1729
1900
  issues.push({
1730
- analyzerId: ID14,
1901
+ analyzerId: ID15,
1731
1902
  severity: "warn",
1732
1903
  nodeIds: [node.path],
1733
1904
  message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
@@ -1761,9 +1932,9 @@ var SUPERSEDED_TEXTS = {
1761
1932
  };
1762
1933
 
1763
1934
  // plugins/core/analyzers/superseded/index.ts
1764
- var ID15 = "superseded";
1935
+ var ID16 = "superseded";
1765
1936
  var supersededAnalyzer = {
1766
- id: ID15,
1937
+ id: ID16,
1767
1938
  pluginId: "core",
1768
1939
  kind: "analyzer",
1769
1940
  version: "1.0.0",
@@ -1775,7 +1946,7 @@ var supersededAnalyzer = {
1775
1946
  const supersededBy = pickSupersededBy(node);
1776
1947
  if (supersededBy === null) continue;
1777
1948
  issues.push({
1778
- analyzerId: ID15,
1949
+ analyzerId: ID16,
1779
1950
  severity: "info",
1780
1951
  nodeIds: [node.path],
1781
1952
  message: tx(SUPERSEDED_TEXTS.message, {
@@ -1824,14 +1995,14 @@ var TRIGGER_COLLISION_TEXTS = {
1824
1995
  };
1825
1996
 
1826
1997
  // plugins/core/analyzers/trigger-collision/index.ts
1827
- var ID16 = "trigger-collision";
1998
+ var ID17 = "trigger-collision";
1828
1999
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
1829
2000
  "command",
1830
2001
  "skill",
1831
2002
  "agent"
1832
2003
  ]);
1833
2004
  var triggerCollisionAnalyzer = {
1834
- id: ID16,
2005
+ id: ID17,
1835
2006
  pluginId: "core",
1836
2007
  kind: "analyzer",
1837
2008
  mode: "deterministic",
@@ -1930,7 +2101,7 @@ function analyzeTriggerBucket(normalized, claims) {
1930
2101
  part: parts[0]
1931
2102
  });
1932
2103
  return {
1933
- analyzerId: ID16,
2104
+ analyzerId: ID17,
1934
2105
  severity: "error",
1935
2106
  nodeIds,
1936
2107
  message,
@@ -1970,10 +2141,10 @@ var UNKNOWN_FIELD_TEXTS = {
1970
2141
  };
1971
2142
 
1972
2143
  // plugins/core/analyzers/unknown-field/index.ts
1973
- var ID17 = "unknown-field";
2144
+ var ID18 = "unknown-field";
1974
2145
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1975
2146
  var unknownFieldAnalyzer = {
1976
- id: ID17,
2147
+ id: ID18,
1977
2148
  pluginId: "core",
1978
2149
  kind: "analyzer",
1979
2150
  version: "1.0.0",
@@ -2031,7 +2202,7 @@ var unknownFieldAnalyzer = {
2031
2202
  for (const key of Object.keys(annotations)) {
2032
2203
  if (!knownAnnotationKeys.has(key)) {
2033
2204
  issues.push({
2034
- analyzerId: ID17,
2205
+ analyzerId: ID18,
2035
2206
  severity: "warn",
2036
2207
  nodeIds: [node.path],
2037
2208
  message: tx(UNKNOWN_FIELD_TEXTS.unknownAnnotationKey, {
@@ -2058,7 +2229,7 @@ var unknownFieldAnalyzer = {
2058
2229
  if (validator(value)) continue;
2059
2230
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
2060
2231
  issues.push({
2061
- analyzerId: ID17,
2232
+ analyzerId: ID18,
2062
2233
  severity: "warn",
2063
2234
  nodeIds: [node.path],
2064
2235
  message: tx(UNKNOWN_FIELD_TEXTS.pluginNamespaceInvalid, {
@@ -2074,7 +2245,7 @@ var unknownFieldAnalyzer = {
2074
2245
  continue;
2075
2246
  }
2076
2247
  issues.push({
2077
- analyzerId: ID17,
2248
+ analyzerId: ID18,
2078
2249
  severity: "warn",
2079
2250
  nodeIds: [node.path],
2080
2251
  message: tx(UNKNOWN_FIELD_TEXTS.unknownRootKey, {
@@ -2367,9 +2538,9 @@ var VALIDATE_ALL_TEXTS = {
2367
2538
  };
2368
2539
 
2369
2540
  // plugins/core/analyzers/validate-all/index.ts
2370
- var ID18 = "validate-all";
2541
+ var ID19 = "validate-all";
2371
2542
  var validateAllAnalyzer = {
2372
- id: ID18,
2543
+ id: ID19,
2373
2544
  pluginId: "core",
2374
2545
  kind: "analyzer",
2375
2546
  version: "1.0.0",
@@ -2432,7 +2603,7 @@ function collectNodeFindings(v, node, out) {
2432
2603
  const result = v.validate("node", toNodeForSchema(node));
2433
2604
  if (result.ok) return;
2434
2605
  out.push({
2435
- analyzerId: ID18,
2606
+ analyzerId: ID19,
2436
2607
  severity: "error",
2437
2608
  nodeIds: [node.path],
2438
2609
  message: tx(VALIDATE_ALL_TEXTS.nodeFailure, {
@@ -2451,7 +2622,7 @@ function collectFrontmatterBaseFindings(node, out) {
2451
2622
  if (isMissingStringField(fm, "description")) missing.push("description");
2452
2623
  if (missing.length === 0) return;
2453
2624
  out.push({
2454
- analyzerId: ID18,
2625
+ analyzerId: ID19,
2455
2626
  // `warn` (not `error`) so the default `sm scan` exit code stays
2456
2627
  // 0 even when nodes are missing frontmatter base fields. Strict
2457
2628
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -2473,7 +2644,7 @@ function collectLinkFindings(v, link2, out) {
2473
2644
  const result = v.validate("link", toLinkForSchema(link2));
2474
2645
  if (result.ok) return;
2475
2646
  out.push({
2476
- analyzerId: ID18,
2647
+ analyzerId: ID19,
2477
2648
  severity: "error",
2478
2649
  nodeIds: [link2.source],
2479
2650
  message: tx(VALIDATE_ALL_TEXTS.linkFailure, {
@@ -2541,13 +2712,13 @@ var ASCII_FORMATTER_TEXTS = {
2541
2712
  };
2542
2713
 
2543
2714
  // plugins/core/formatters/ascii/index.ts
2544
- var ID19 = "ascii";
2715
+ var ID20 = "ascii";
2545
2716
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
2546
2717
  var asciiFormatter = {
2547
- id: ID19,
2718
+ id: ID20,
2548
2719
  pluginId: "core",
2549
2720
  kind: "formatter",
2550
- formatId: ID19,
2721
+ formatId: ID20,
2551
2722
  version: "1.0.0",
2552
2723
  description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
2553
2724
  // ASCII tree formatter, header + per-kind sections + per-issue
@@ -2642,14 +2813,14 @@ function renderSection(out, kind, group) {
2642
2813
  }
2643
2814
 
2644
2815
  // plugins/core/formatters/json/index.ts
2645
- var ID20 = "json";
2816
+ var ID21 = "json";
2646
2817
  var jsonFormatter = {
2647
- id: ID20,
2818
+ id: ID21,
2648
2819
  pluginId: "core",
2649
2820
  kind: "formatter",
2650
2821
  version: "1.0.0",
2651
2822
  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,
2823
+ formatId: ID21,
2653
2824
  format(ctx) {
2654
2825
  if (ctx.scanResult !== void 0) {
2655
2826
  return JSON.stringify(ctx.scanResult);
@@ -2788,10 +2959,10 @@ function resolveSpecRoot2() {
2788
2959
  }
2789
2960
 
2790
2961
  // plugins/core/actions/bump/index.ts
2791
- var ID21 = "bump";
2962
+ var ID22 = "bump";
2792
2963
  var PLUGIN_ID = "core";
2793
2964
  var bumpAction = {
2794
- id: ID21,
2965
+ id: ID22,
2795
2966
  pluginId: PLUGIN_ID,
2796
2967
  kind: "action",
2797
2968
  version: "1.0.0",
@@ -2850,10 +3021,10 @@ function pickCurrentVersion(overlay) {
2850
3021
  }
2851
3022
 
2852
3023
  // plugins/core/actions/mark-superseded/index.ts
2853
- var ID22 = "mark-superseded";
3024
+ var ID23 = "mark-superseded";
2854
3025
  var PLUGIN_ID2 = "core";
2855
3026
  var markSupersededAction = {
2856
- id: ID22,
3027
+ id: ID23,
2857
3028
  pluginId: PLUGIN_ID2,
2858
3029
  kind: "action",
2859
3030
  version: "0.0.0",
@@ -2963,7 +3134,7 @@ var UPDATE_CHECK_TEXTS = {
2963
3134
  // package.json
2964
3135
  var package_default = {
2965
3136
  name: "@skill-map/cli",
2966
- version: "0.32.0",
3137
+ version: "0.34.0",
2967
3138
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2968
3139
  license: "MIT",
2969
3140
  type: "module",
@@ -3044,6 +3215,7 @@ var package_default = {
3044
3215
  "js-yaml": "4.1.1",
3045
3216
  kysely: "0.28.17",
3046
3217
  semver: "7.7.4",
3218
+ "smol-toml": "1.6.1",
3047
3219
  typanion: "3.14.0",
3048
3220
  ws: "8.20.0"
3049
3221
  },
@@ -3339,14 +3511,16 @@ var updateCheckHook = {
3339
3511
 
3340
3512
  // plugins/built-ins.ts
3341
3513
  var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
3514
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude" };
3515
+ var slashExtractor2 = { ...slashExtractor, pluginId: "claude" };
3342
3516
  var geminiProvider2 = { ...geminiProvider, pluginId: "gemini" };
3517
+ var openaiProvider2 = { ...openaiProvider, pluginId: "openai" };
3343
3518
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
3344
3519
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core" };
3345
3520
  var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
3346
- var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "core" };
3347
3521
  var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
3348
3522
  var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
3349
- var slashExtractor2 = { ...slashExtractor, pluginId: "core" };
3523
+ var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core" };
3350
3524
  var toolsCountExtractor2 = { ...toolsCountExtractor, pluginId: "core" };
3351
3525
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core" };
3352
3526
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core" };
@@ -3371,7 +3545,9 @@ var builtInBundles = [
3371
3545
  granularity: "bundle",
3372
3546
  description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and parses Claude-flavored frontmatter.",
3373
3547
  extensions: [
3374
- claudeProvider2
3548
+ claudeProvider2,
3549
+ atDirectiveExtractor2,
3550
+ slashExtractor2
3375
3551
  ]
3376
3552
  },
3377
3553
  {
@@ -3382,6 +3558,14 @@ var builtInBundles = [
3382
3558
  geminiProvider2
3383
3559
  ]
3384
3560
  },
3561
+ {
3562
+ id: "openai",
3563
+ granularity: "bundle",
3564
+ 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.",
3565
+ extensions: [
3566
+ openaiProvider2
3567
+ ]
3568
+ },
3385
3569
  {
3386
3570
  id: "agent-skills",
3387
3571
  granularity: "bundle",
@@ -3397,10 +3581,9 @@ var builtInBundles = [
3397
3581
  extensions: [
3398
3582
  coreMarkdownProvider2,
3399
3583
  annotationsExtractor2,
3400
- atDirectiveExtractor2,
3401
3584
  externalUrlCounterExtractor2,
3402
3585
  markdownLinkExtractor2,
3403
- slashExtractor2,
3586
+ mcpToolsExtractor2,
3404
3587
  toolsCountExtractor2,
3405
3588
  annotationOrphanAnalyzer2,
3406
3589
  annotationStaleAnalyzer2,
@@ -4885,7 +5068,7 @@ var AsyncMutex = class {
4885
5068
  this.#locked = true;
4886
5069
  return;
4887
5070
  }
4888
- await new Promise((resolve38) => this.#waiters.push(resolve38));
5071
+ await new Promise((resolve39) => this.#waiters.push(resolve39));
4889
5072
  this.#locked = true;
4890
5073
  }
4891
5074
  unlock() {
@@ -5945,11 +6128,6 @@ var LINK_KIND_VALUES = Object.freeze([
5945
6128
  "mentions",
5946
6129
  "supersedes"
5947
6130
  ]);
5948
- var CONFIDENCE_VALUES = Object.freeze([
5949
- "high",
5950
- "medium",
5951
- "low"
5952
- ]);
5953
6131
  var SEVERITY_VALUES = Object.freeze([
5954
6132
  "error",
5955
6133
  "warn",
@@ -5981,7 +6159,7 @@ function isLinkKind(s) {
5981
6159
  return typeof s === "string" && LINK_KIND_VALUES.includes(s);
5982
6160
  }
5983
6161
  function isConfidence(s) {
5984
- return typeof s === "string" && CONFIDENCE_VALUES.includes(s);
6162
+ return typeof s === "number" && Number.isFinite(s) && s >= 0 && s <= 1;
5985
6163
  }
5986
6164
  function isSeverity(s) {
5987
6165
  return typeof s === "string" && SEVERITY_VALUES.includes(s);
@@ -5995,7 +6173,7 @@ function parseLinkKind(s, ctx) {
5995
6173
  function parseConfidence(s, ctx) {
5996
6174
  if (isConfidence(s)) return s;
5997
6175
  throw new Error(
5998
- `Invalid Confidence value ${formatValue(s)} at ${ctx}. Allowed: ${CONFIDENCE_VALUES.join(" | ")}.`
6176
+ `Invalid Confidence value ${formatValue(s)} at ${ctx}. Expected a finite number in [0..1].`
5999
6177
  );
6000
6178
  }
6001
6179
  function parseSeverity(s, ctx) {
@@ -8794,10 +8972,45 @@ var plainParser = {
8794
8972
  }
8795
8973
  };
8796
8974
 
8975
+ // plugins/core/parsers/toml/index.ts
8976
+ import { parse as parseToml } from "smol-toml";
8977
+ var tomlParser = {
8978
+ id: "toml",
8979
+ parse(raw, _path) {
8980
+ let parsed = {};
8981
+ const issues = [];
8982
+ try {
8983
+ const doc = parseToml(raw);
8984
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
8985
+ parsed = stripPrototypePollution(doc);
8986
+ }
8987
+ } catch (err) {
8988
+ issues.push({
8989
+ code: "frontmatter-parse-error",
8990
+ message: sanitiseParseErrorMessage2(err)
8991
+ });
8992
+ }
8993
+ const out = {
8994
+ frontmatterRaw: raw,
8995
+ frontmatter: parsed,
8996
+ body: ""
8997
+ };
8998
+ if (issues.length > 0) {
8999
+ return { ...out, issues };
9000
+ }
9001
+ return out;
9002
+ }
9003
+ };
9004
+ function sanitiseParseErrorMessage2(err) {
9005
+ const raw = err instanceof Error ? err.message : String(err);
9006
+ return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
9007
+ }
9008
+
8797
9009
  // kernel/scan/parsers/index.ts
8798
9010
  var REGISTRY = /* @__PURE__ */ new Map([
8799
9011
  [frontmatterYamlParser.id, frontmatterYamlParser],
8800
- [plainParser.id, plainParser]
9012
+ [plainParser.id, plainParser],
9013
+ [tomlParser.id, tomlParser]
8801
9014
  ]);
8802
9015
  var FROZEN_IDS = new Set(REGISTRY.keys());
8803
9016
  function getParser(id) {
@@ -9457,9 +9670,42 @@ function trimRedundantPath(message, primary) {
9457
9670
  }
9458
9671
 
9459
9672
  // cli/commands/config.ts
9460
- import { existsSync as existsSync14 } from "fs";
9673
+ import { existsSync as existsSync15 } from "fs";
9461
9674
  import { Command as Command4, Option as Option4 } from "clipanion";
9462
9675
 
9676
+ // core/config/active-provider.ts
9677
+ import { existsSync as existsSync14 } from "fs";
9678
+ import { join as join10 } from "path";
9679
+ var DETECTION_RULES = [
9680
+ { providerId: "claude", marker: ".claude" },
9681
+ { providerId: "gemini", marker: ".gemini" },
9682
+ { providerId: "openai", marker: ".codex" },
9683
+ { providerId: "openai", marker: "AGENTS.md" },
9684
+ { providerId: "cursor", marker: ".cursor" }
9685
+ ];
9686
+ function resolveActiveProvider(cwd) {
9687
+ const detected = detectProvidersFromFilesystem(cwd);
9688
+ const fromConfig = readConfigValue("activeProvider", { cwd });
9689
+ if (typeof fromConfig === "string" && fromConfig.length > 0) {
9690
+ return { resolved: fromConfig, source: "config", detected };
9691
+ }
9692
+ if (detected.length > 0) {
9693
+ return { resolved: detected[0], source: "autodetect", detected };
9694
+ }
9695
+ return { resolved: null, source: "none", detected };
9696
+ }
9697
+ function detectProvidersFromFilesystem(cwd) {
9698
+ const seen = /* @__PURE__ */ new Set();
9699
+ const out = [];
9700
+ for (const rule of DETECTION_RULES) {
9701
+ if (seen.has(rule.providerId)) continue;
9702
+ if (!existsSync14(join10(cwd, rule.marker))) continue;
9703
+ seen.add(rule.providerId);
9704
+ out.push(rule.providerId);
9705
+ }
9706
+ return out;
9707
+ }
9708
+
9463
9709
  // cli/util/path-display.ts
9464
9710
  import { isAbsolute as isAbsolute4, relative as pathRelative } from "path";
9465
9711
  function relativeIfBelow(path, cwd) {
@@ -9469,6 +9715,42 @@ function relativeIfBelow(path, cwd) {
9469
9715
  return rel;
9470
9716
  }
9471
9717
 
9718
+ // cli/util/scan-zone-drop.ts
9719
+ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
9720
+
9721
+ // cli/commands/db/shared.ts
9722
+ var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
9723
+ function assertSafeIdentifier(name) {
9724
+ if (!SAFE_SQL_IDENTIFIER_RE.test(name)) {
9725
+ throw new Error(`refusing to operate on non-identifier table name: ${JSON.stringify(name)}`);
9726
+ }
9727
+ }
9728
+
9729
+ // cli/util/scan-zone-drop.ts
9730
+ function dropScanZone(dbPath) {
9731
+ const db = new DatabaseSync4(dbPath);
9732
+ try {
9733
+ const rows = db.prepare(
9734
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'scan\\_%' ESCAPE '\\'"
9735
+ ).all();
9736
+ for (const r of rows) assertSafeIdentifier(r.name);
9737
+ if (rows.length === 0) {
9738
+ return { tableCount: 0, droppedTables: [] };
9739
+ }
9740
+ db.exec("BEGIN");
9741
+ for (const { name } of rows) {
9742
+ db.exec(`DELETE FROM "${name}"`);
9743
+ }
9744
+ db.exec("COMMIT");
9745
+ return {
9746
+ tableCount: rows.length,
9747
+ droppedTables: rows.map((r) => r.name)
9748
+ };
9749
+ } finally {
9750
+ db.close();
9751
+ }
9752
+ }
9753
+
9472
9754
  // cli/i18n/config.texts.ts
9473
9755
  var CONFIG_TEXTS = {
9474
9756
  unknownKey: "{{glyph}} Unknown config key: {{key}}\n",
@@ -9511,6 +9793,20 @@ var CONFIG_TEXTS = {
9511
9793
  * screen what they just opted into.
9512
9794
  */
9513
9795
  privacyGateConfirmed: '{{glyph}} Opening disk access for "{{key}}":\n{{paths}}\n',
9796
+ /**
9797
+ * Confirmation printed after `sm config set activeProvider <id>`
9798
+ * succeeds. The lens change atomically drops the scan_* zone (per
9799
+ * `architecture.md` §Active Provider Lens) so the persisted graph
9800
+ * never carries stale node / link rows from the previous lens. We
9801
+ * surface what was cleared so the operator knows their state was
9802
+ * touched and what to do next.
9803
+ */
9804
+ lensSwitchedCleared: "{{glyph}} Lens switched. Cleared {{tableCount}} scan table(s): {{tableNames}}.\n {{hint}}\n",
9805
+ lensSwitchedClearedHint: "Run `sm scan` to repopulate the graph under the new lens.",
9806
+ /** Same lens-switch announcement when the DB was empty (no rows to clear). */
9807
+ lensSwitchedEmpty: "{{glyph}} Lens switched. Scan zone was already empty.\n {{hint}}\n",
9808
+ /** Lens switch happened before any `sm scan` ran (no DB file on disk yet). */
9809
+ lensSwitchedNoDb: "{{glyph}} Lens switched. Run `sm scan` to populate the graph under the new lens.\n",
9514
9810
  // --- list verb (sectioned human renderer) ----------------------------
9515
9811
  /** Section heading: ` General`, ` Scan`, … rendered before its rows. */
9516
9812
  listSectionHeader: " {{title}}\n",
@@ -9548,6 +9844,9 @@ function suggestConfigKey(effective, typed, ansi) {
9548
9844
  hint: ansi.dim(tx(CONFIG_TEXTS.unknownKeySuggestionHint, { suggestions: formatted }))
9549
9845
  });
9550
9846
  }
9847
+ var KNOWN_DEFAULTLESS_KEY_RESOLVERS = {
9848
+ activeProvider: (cwd) => resolveActiveProvider(cwd).resolved
9849
+ };
9551
9850
  function parseCliValue(raw) {
9552
9851
  try {
9553
9852
  return JSON.parse(raw);
@@ -9720,6 +10019,11 @@ function formatValueListHuman(value, ansi) {
9720
10019
  }
9721
10020
  return String(value);
9722
10021
  }
10022
+ function resolveConfigGetValue(lookupValue, key, cwd) {
10023
+ if (lookupValue !== void 0) return lookupValue;
10024
+ const runtimeResolver = KNOWN_DEFAULTLESS_KEY_RESOLVERS[key];
10025
+ return runtimeResolver ? runtimeResolver(cwd) : void 0;
10026
+ }
9723
10027
  var ConfigGetCommand = class extends SmCommand {
9724
10028
  static paths = [["config", "get"]];
9725
10029
  static usage = Command4.Usage({
@@ -9734,16 +10038,14 @@ var ConfigGetCommand = class extends SmCommand {
9734
10038
  strict = Option4.Boolean("--strict", false);
9735
10039
  emitElapsed = false;
9736
10040
  async run() {
9737
- const result = tryLoadConfig(
9738
- { strict: this.strict, ...defaultRuntimeContext() },
9739
- this.context.stderr
9740
- );
10041
+ const ctx = defaultRuntimeContext();
10042
+ const result = tryLoadConfig({ strict: this.strict, ...ctx }, this.context.stderr);
9741
10043
  if (!result.ok) return result.exitCode;
9742
10044
  const { effective, warnings } = result.loaded;
9743
10045
  for (const w of warnings) this.printer.info(w + "\n");
9744
10046
  const lookup = safeGetAtPath(effective, this.key, this.context.stderr);
9745
10047
  if (!lookup.ok) return lookup.exitCode;
9746
- const { value } = lookup;
10048
+ const value = resolveConfigGetValue(lookup.value, this.key, ctx.cwd);
9747
10049
  if (value === void 0) {
9748
10050
  const ansi = this.ansiFor("stderr");
9749
10051
  this.printer.info(
@@ -9783,10 +10085,8 @@ var ConfigShowCommand = class extends SmCommand {
9783
10085
  // the value it gates.
9784
10086
  // eslint-disable-next-line complexity
9785
10087
  async run() {
9786
- const result = tryLoadConfig(
9787
- { strict: this.strict, ...defaultRuntimeContext() },
9788
- this.context.stderr
9789
- );
10088
+ const ctx = defaultRuntimeContext();
10089
+ const result = tryLoadConfig({ strict: this.strict, ...ctx }, this.context.stderr);
9790
10090
  if (!result.ok) return result.exitCode;
9791
10091
  const { effective, sources, warnings } = result.loaded;
9792
10092
  for (const w of warnings) this.printer.info(w + "\n");
@@ -9809,6 +10109,12 @@ var ConfigShowCommand = class extends SmCommand {
9809
10109
  }
9810
10110
  throw err;
9811
10111
  }
10112
+ if (value === void 0) {
10113
+ const runtimeResolver = KNOWN_DEFAULTLESS_KEY_RESOLVERS[this.key];
10114
+ if (runtimeResolver) {
10115
+ value = runtimeResolver(ctx.cwd);
10116
+ }
10117
+ }
9812
10118
  if (value === void 0) {
9813
10119
  this.printer.info(tx(CONFIG_TEXTS.unknownKey, { glyph: errGlyphShow, key: this.key }));
9814
10120
  return ExitCode.NotFound;
@@ -9962,8 +10268,44 @@ var ConfigSetCommand = class extends SmCommand {
9962
10268
  )
9963
10269
  })
9964
10270
  );
10271
+ if (this.key === "activeProvider") {
10272
+ this.announceLensSwitch(ctx.cwd, ansi);
10273
+ }
9965
10274
  return ExitCode.Ok;
9966
10275
  }
10276
+ /**
10277
+ * Side effect of `sm config set activeProvider <id>`, atomically
10278
+ * drops the `scan_*` zone so the persisted graph never reflects the
10279
+ * wrong lens (see `architecture.md` §Active Provider Lens). The drop
10280
+ * is non-destructive of `state_*` / `config_*` rows; the operator
10281
+ * runs `sm scan` next to repopulate.
10282
+ *
10283
+ * Silent when no DB file exists on disk yet (fresh project that has
10284
+ * never run `sm scan`), the lens just gets set and the next scan
10285
+ * uses it.
10286
+ */
10287
+ announceLensSwitch(cwd, ansi) {
10288
+ const dbPath = resolveDbPath({ db: void 0, cwd });
10289
+ const okGlyph = ansi.green("\u2713");
10290
+ if (!existsSync15(dbPath)) {
10291
+ this.printer.info(tx(CONFIG_TEXTS.lensSwitchedNoDb, { glyph: okGlyph }));
10292
+ return;
10293
+ }
10294
+ const result = dropScanZone(dbPath);
10295
+ const hint = ansi.dim(CONFIG_TEXTS.lensSwitchedClearedHint);
10296
+ if (result.tableCount === 0) {
10297
+ this.printer.info(tx(CONFIG_TEXTS.lensSwitchedEmpty, { glyph: okGlyph, hint }));
10298
+ return;
10299
+ }
10300
+ this.printer.info(
10301
+ tx(CONFIG_TEXTS.lensSwitchedCleared, {
10302
+ glyph: okGlyph,
10303
+ tableCount: result.tableCount,
10304
+ tableNames: result.droppedTables.join(", "),
10305
+ hint
10306
+ })
10307
+ );
10308
+ }
9967
10309
  };
9968
10310
  var ConfigResetCommand = class extends SmCommand {
9969
10311
  static paths = [["config", "reset"]];
@@ -9985,7 +10327,7 @@ var ConfigResetCommand = class extends SmCommand {
9985
10327
  const path = targetSettingsPath2(target, ctx.cwd);
9986
10328
  const ansi = this.ansiFor("stdout");
9987
10329
  const okGlyph = ansi.green("\u2713");
9988
- if (!existsSync14(path)) {
10330
+ if (!existsSync15(path)) {
9989
10331
  this.printer.data(
9990
10332
  tx(CONFIG_TEXTS.unsetNoOverride, {
9991
10333
  glyph: okGlyph,
@@ -10060,16 +10402,16 @@ var CONFIG_COMMANDS = [
10060
10402
  ];
10061
10403
 
10062
10404
  // cli/commands/conformance.ts
10063
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
10405
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
10064
10406
  import { dirname as dirname12, resolve as resolve21 } from "path";
10065
10407
  import { fileURLToPath as fileURLToPath4 } from "url";
10066
10408
  import { Command as Command5, Option as Option5 } from "clipanion";
10067
10409
 
10068
10410
  // conformance/index.ts
10069
10411
  import { spawnSync as spawnSync2 } from "child_process";
10070
- import { cpSync, existsSync as existsSync15, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, statSync as statSync3 } from "fs";
10412
+ import { cpSync, existsSync as existsSync16, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, statSync as statSync3 } from "fs";
10071
10413
  import { tmpdir } from "os";
10072
- import { isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve19 } from "path";
10414
+ import { isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve19 } from "path";
10073
10415
 
10074
10416
  // conformance/i18n/runner.texts.ts
10075
10417
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -10105,9 +10447,9 @@ function disableEnv(setup) {
10105
10447
  function runConformanceCase(options) {
10106
10448
  const raw = readFileSync14(options.casePath, "utf8");
10107
10449
  const c = JSON.parse(raw);
10108
- const fixturesRoot = options.fixturesRoot ?? join10(options.specRoot, "conformance", "fixtures");
10450
+ const fixturesRoot = options.fixturesRoot ?? join11(options.specRoot, "conformance", "fixtures");
10109
10451
  const safeId = c.id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32);
10110
- const scope = mkdtempSync(join10(tmpdir(), `sm-conformance-${safeId}-`));
10452
+ const scope = mkdtempSync(join11(tmpdir(), `sm-conformance-${safeId}-`));
10111
10453
  const setupEnv = disableEnv(c.setup);
10112
10454
  try {
10113
10455
  const priorFailure = runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv);
@@ -10179,9 +10521,9 @@ function replaceFixture(scope, fixturesRoot, fixture) {
10179
10521
  assertContained2(fixturesRoot, fixture, "fixture");
10180
10522
  for (const entry of readdirSync5(scope)) {
10181
10523
  if (entry === KERNEL_SKILL_MAP_DIR) continue;
10182
- rmSync(join10(scope, entry), { recursive: true, force: true });
10524
+ rmSync(join11(scope, entry), { recursive: true, force: true });
10183
10525
  }
10184
- const src = join10(fixturesRoot, fixture);
10526
+ const src = join11(fixturesRoot, fixture);
10185
10527
  cpSync(src, scope, { recursive: true });
10186
10528
  }
10187
10529
  function assertContained2(root, rel, label) {
@@ -10218,7 +10560,7 @@ function evaluateAssertion(a, ctx) {
10218
10560
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
10219
10561
  }
10220
10562
  const abs = resolve19(ctx.scope, a.path);
10221
- return existsSync15(abs) ? { ok: true, type: a.type } : {
10563
+ return existsSync16(abs) ? { ok: true, type: a.type } : {
10222
10564
  ok: false,
10223
10565
  type: a.type,
10224
10566
  reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path })
@@ -10231,9 +10573,9 @@ function evaluateAssertion(a, ctx) {
10231
10573
  } catch (err) {
10232
10574
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
10233
10575
  }
10234
- const fixturePath = join10(ctx.fixturesRoot, a.fixture);
10576
+ const fixturePath = join11(ctx.fixturesRoot, a.fixture);
10235
10577
  const targetPath = resolve19(ctx.scope, a.path);
10236
- if (!existsSync15(targetPath)) {
10578
+ if (!existsSync16(targetPath)) {
10237
10579
  return {
10238
10580
  ok: false,
10239
10581
  type: a.type,
@@ -10414,7 +10756,7 @@ var CONFORMANCE_TEXTS = {
10414
10756
  };
10415
10757
 
10416
10758
  // cli/util/conformance-scopes.ts
10417
- import { existsSync as existsSync16, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
10759
+ import { existsSync as existsSync17, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
10418
10760
  import { dirname as dirname11, resolve as resolve20 } from "path";
10419
10761
  import { createRequire as createRequire6 } from "module";
10420
10762
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -10434,7 +10776,7 @@ function resolveCliWorkspaceRoot() {
10434
10776
  let cursor = here;
10435
10777
  for (let depth = 0; depth < 6; depth += 1) {
10436
10778
  const candidate = resolve20(cursor, "plugins");
10437
- if (existsSync16(candidate) && statSync4(candidate).isDirectory()) {
10779
+ if (existsSync17(candidate) && statSync4(candidate).isDirectory()) {
10438
10780
  return cursor;
10439
10781
  }
10440
10782
  const parent = dirname11(cursor);
@@ -10454,7 +10796,7 @@ function collectProviderScopes(specRoot) {
10454
10796
  return out;
10455
10797
  }
10456
10798
  const pluginsRoot = resolve20(workspaceRoot, "plugins");
10457
- if (!existsSync16(pluginsRoot)) return out;
10799
+ if (!existsSync17(pluginsRoot)) return out;
10458
10800
  for (const bundleEntry of readdirSync6(pluginsRoot)) {
10459
10801
  const bundleDir = resolve20(pluginsRoot, bundleEntry);
10460
10802
  if (!isDir(bundleDir)) continue;
@@ -10466,7 +10808,7 @@ function collectProviderScopes(specRoot) {
10466
10808
  }
10467
10809
  function isDir(path) {
10468
10810
  try {
10469
- return existsSync16(path) && statSync4(path).isDirectory();
10811
+ return existsSync17(path) && statSync4(path).isDirectory();
10470
10812
  } catch {
10471
10813
  return false;
10472
10814
  }
@@ -10476,10 +10818,10 @@ function collectBundleProviderScopes(providersRoot, specRoot, out) {
10476
10818
  const providerDir = resolve20(providersRoot, entry);
10477
10819
  if (!isDir(providerDir)) continue;
10478
10820
  const conformanceDir = resolve20(providerDir, "conformance");
10479
- if (!existsSync16(conformanceDir)) continue;
10821
+ if (!existsSync17(conformanceDir)) continue;
10480
10822
  const casesDir = resolve20(conformanceDir, "cases");
10481
10823
  const fixturesDir = resolve20(conformanceDir, "fixtures");
10482
- if (!existsSync16(casesDir) || !existsSync16(fixturesDir)) continue;
10824
+ if (!existsSync17(casesDir) || !existsSync17(fixturesDir)) continue;
10483
10825
  out.push({
10484
10826
  id: `provider:${entry}`,
10485
10827
  kind: "provider",
@@ -10517,7 +10859,7 @@ function selectConformanceScopes(scope) {
10517
10859
  return [match];
10518
10860
  }
10519
10861
  function listCaseFiles(scope) {
10520
- if (!existsSync16(scope.casesDir)) return [];
10862
+ if (!existsSync17(scope.casesDir)) return [];
10521
10863
  return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
10522
10864
  }
10523
10865
 
@@ -10536,7 +10878,7 @@ function resolveBinary() {
10536
10878
  let cursor = here;
10537
10879
  for (let depth = 0; depth < 6; depth += 1) {
10538
10880
  const candidate = resolve21(cursor, "bin", "sm.js");
10539
- if (existsSync17(candidate)) return candidate;
10881
+ if (existsSync18(candidate)) return candidate;
10540
10882
  const parent = dirname12(cursor);
10541
10883
  if (parent === cursor) break;
10542
10884
  cursor = parent;
@@ -10602,7 +10944,7 @@ var ConformanceRunCommand = class extends SmCommand {
10602
10944
  return ExitCode.Error;
10603
10945
  }
10604
10946
  const binary = resolveBinary();
10605
- if (!existsSync17(binary)) {
10947
+ if (!existsSync18(binary)) {
10606
10948
  if (this.json) {
10607
10949
  this.#emitJsonError(
10608
10950
  "internal",
@@ -10794,7 +11136,7 @@ function writeStreamSnippet(stream, header, text) {
10794
11136
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
10795
11137
 
10796
11138
  // cli/commands/db/backup.ts
10797
- import { dirname as dirname13, join as join11, resolve as resolve22 } from "path";
11139
+ import { dirname as dirname13, join as join12, resolve as resolve22 } from "path";
10798
11140
  import { Command as Command6, Option as Option6 } from "clipanion";
10799
11141
 
10800
11142
  // cli/i18n/db.texts.ts
@@ -10880,7 +11222,7 @@ var DbBackupCommand = class extends SmCommand {
10880
11222
  const exit = requireDbOrExit(path, this.context.stderr);
10881
11223
  if (exit !== null) return exit;
10882
11224
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
10883
- const outPath = this.out ? resolve22(this.out) : join11(dirname13(path), "backups", `${ts}.db`);
11225
+ const outPath = this.out ? resolve22(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
10884
11226
  await withSqlite({ databasePath: path, autoMigrate: false }, async (storage) => {
10885
11227
  storage.migrations.writeBackup(outPath);
10886
11228
  });
@@ -10991,28 +11333,18 @@ var DbRestoreCommand = class extends SmCommand {
10991
11333
  this.printer.data(
10992
11334
  tx(DB_TEXTS.restoreDone, {
10993
11335
  glyph: ansi.green("\u2713"),
10994
- sourcePath: relativeIfBelow(sourcePath, cwd),
10995
- target: relativeIfBelow(target, cwd)
10996
- })
10997
- );
10998
- return ExitCode.Ok;
10999
- }
11000
- };
11001
-
11002
- // cli/commands/db/reset.ts
11003
- import { rm as rm2 } from "fs/promises";
11004
- import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
11005
- import { Command as Command8, Option as Option8 } from "clipanion";
11006
-
11007
- // cli/commands/db/shared.ts
11008
- var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
11009
- function assertSafeIdentifier(name) {
11010
- if (!SAFE_SQL_IDENTIFIER_RE.test(name)) {
11011
- throw new Error(`refusing to operate on non-identifier table name: ${JSON.stringify(name)}`);
11336
+ sourcePath: relativeIfBelow(sourcePath, cwd),
11337
+ target: relativeIfBelow(target, cwd)
11338
+ })
11339
+ );
11340
+ return ExitCode.Ok;
11012
11341
  }
11013
- }
11342
+ };
11014
11343
 
11015
11344
  // cli/commands/db/reset.ts
11345
+ import { rm as rm2 } from "fs/promises";
11346
+ import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
11347
+ import { Command as Command8, Option as Option8 } from "clipanion";
11016
11348
  var DbResetCommand = class extends SmCommand {
11017
11349
  static paths = [["db", "reset"]];
11018
11350
  static usage = Command8.Usage({
@@ -11094,7 +11426,7 @@ var DbResetCommand = class extends SmCommand {
11094
11426
  return ExitCode.Error;
11095
11427
  }
11096
11428
  }
11097
- const db = new DatabaseSync4(path);
11429
+ const db = new DatabaseSync5(path);
11098
11430
  try {
11099
11431
  const rows = db.prepare(
11100
11432
  "SELECT name FROM sqlite_master WHERE type='table' AND (name LIKE 'scan\\_%' ESCAPE '\\'" + (this.state ? " OR name LIKE 'state\\_%' ESCAPE '\\'" : "") + ")"
@@ -11237,7 +11569,7 @@ var DbBrowserCommand = class extends SmCommand {
11237
11569
  };
11238
11570
 
11239
11571
  // cli/commands/db/dump.ts
11240
- import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
11572
+ import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
11241
11573
  import { Command as Command11, Option as Option10 } from "clipanion";
11242
11574
  var DbDumpCommand = class extends SmCommand {
11243
11575
  static paths = [["db", "dump"]];
@@ -11279,7 +11611,7 @@ var DbDumpCommand = class extends SmCommand {
11279
11611
  }
11280
11612
  };
11281
11613
  function dumpDatabaseToStream(dbPath, out, tables) {
11282
- const db = new DatabaseSync5(dbPath, { readOnly: true });
11614
+ const db = new DatabaseSync6(dbPath, { readOnly: true });
11283
11615
  try {
11284
11616
  out.write("PRAGMA foreign_keys=OFF;\n");
11285
11617
  out.write("BEGIN TRANSACTION;\n");
@@ -12544,7 +12876,7 @@ function registeredVerbPaths(cli2) {
12544
12876
  // cli/commands/hooks.ts
12545
12877
  import {
12546
12878
  chmodSync,
12547
- existsSync as existsSync18,
12879
+ existsSync as existsSync19,
12548
12880
  mkdirSync as mkdirSync5,
12549
12881
  readFileSync as readFileSync17,
12550
12882
  statSync as statSync5,
@@ -12643,7 +12975,7 @@ var HooksInstallCommand = class extends SmCommand {
12643
12975
  }
12644
12976
  const hooksDir = resolve26(repoRoot, ".git", "hooks");
12645
12977
  const hookPath = resolve26(hooksDir, "pre-commit");
12646
- const existing = existsSync18(hookPath) ? readFileSync17(hookPath, "utf8") : null;
12978
+ const existing = existsSync19(hookPath) ? readFileSync17(hookPath, "utf8") : null;
12647
12979
  const planned2 = computePlannedHookContent(existing);
12648
12980
  if (planned2.kind === "already-installed") {
12649
12981
  this.printer.info(tx(HOOKS_TEXTS.alreadyInstalled, { glyph: okGlyph, hookPath }));
@@ -12669,7 +13001,7 @@ var HooksInstallCommand = class extends SmCommand {
12669
13001
  return ExitCode.Ok;
12670
13002
  }
12671
13003
  try {
12672
- if (!existsSync18(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
13004
+ if (!existsSync19(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
12673
13005
  writeFileSync2(hookPath, planned2.content, { encoding: "utf8" });
12674
13006
  ensureExecutableBit(hookPath);
12675
13007
  } catch (err) {
@@ -12700,7 +13032,7 @@ var HooksInstallCommand = class extends SmCommand {
12700
13032
  function findGitRepoRoot(cwd) {
12701
13033
  let current = cwd;
12702
13034
  while (true) {
12703
- if (existsSync18(resolve26(current, ".git"))) return current;
13035
+ if (existsSync19(resolve26(current, ".git"))) return current;
12704
13036
  const parent = dirname16(current);
12705
13037
  if (parent === current) return null;
12706
13038
  current = parent;
@@ -12722,11 +13054,12 @@ var HOOKS_COMMANDS = [HooksInstallCommand];
12722
13054
 
12723
13055
  // cli/commands/init.ts
12724
13056
  import { mkdir as mkdir3, readFile as readFile2, writeFile } from "fs/promises";
12725
- import { join as join15 } from "path";
13057
+ import { join as join17 } from "path";
12726
13058
  import { Command as Command17, Option as Option16 } from "clipanion";
12727
13059
 
12728
13060
  // kernel/orchestrator/index.ts
12729
- import { existsSync as existsSync21, statSync as statSync7 } from "fs";
13061
+ import { existsSync as existsSync22, statSync as statSync7 } from "fs";
13062
+ import { isAbsolute as isAbsolute7, resolve as resolve27 } from "path";
12730
13063
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
12731
13064
  import cl100k_base from "js-tiktoken/ranks/cl100k_base";
12732
13065
 
@@ -12745,12 +13078,22 @@ var ORCHESTRATOR_TEXTS = {
12745
13078
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
12746
13079
  };
12747
13080
 
13081
+ // kernel/types.ts
13082
+ var ConfidenceTier = Object.freeze({
13083
+ HIGH: 0.9,
13084
+ MEDIUM: 0.6,
13085
+ LOW: 0.3
13086
+ });
13087
+
12748
13088
  // kernel/orchestrator/extractors.ts
12749
13089
  async function runExtractorsForNode(opts) {
12750
13090
  const internalLinks = [];
12751
13091
  const externalLinks = [];
12752
13092
  const enrichmentBuffer = /* @__PURE__ */ new Map();
12753
13093
  const contributions = [];
13094
+ const signals = [];
13095
+ const virtualNodes = [];
13096
+ const virtualNodePaths = /* @__PURE__ */ new Set();
12754
13097
  const validators = loadSchemaValidators();
12755
13098
  for (const extractor of opts.extractors) {
12756
13099
  const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
@@ -12822,6 +13165,18 @@ async function runExtractorsForNode(opts) {
12822
13165
  emittedAt: Date.now()
12823
13166
  });
12824
13167
  };
13168
+ const emitSignal = (signal) => {
13169
+ const validated = validateSignal(extractor, signal, opts.emitter);
13170
+ if (!validated) return;
13171
+ signals.push(validated);
13172
+ };
13173
+ const emitNode = (emitted) => {
13174
+ if (virtualNodePaths.has(emitted.path)) return;
13175
+ const node = buildVirtualNode(extractor, emitted, opts.emitter);
13176
+ if (!node) return;
13177
+ virtualNodePaths.add(node.path);
13178
+ virtualNodes.push(node);
13179
+ };
12825
13180
  const store = opts.pluginStores?.get(extractor.pluginId);
12826
13181
  const ctx = buildExtractorContext(
12827
13182
  extractor,
@@ -12831,6 +13186,8 @@ async function runExtractorsForNode(opts) {
12831
13186
  emitLink,
12832
13187
  enrichNode,
12833
13188
  emitContribution,
13189
+ emitSignal,
13190
+ emitNode,
12834
13191
  store
12835
13192
  );
12836
13193
  await extractor.extract(ctx);
@@ -12839,7 +13196,9 @@ async function runExtractorsForNode(opts) {
12839
13196
  internalLinks,
12840
13197
  externalLinks,
12841
13198
  enrichments: Array.from(enrichmentBuffer.values()),
12842
- contributions
13199
+ contributions,
13200
+ signals,
13201
+ virtualNodes
12843
13202
  };
12844
13203
  }
12845
13204
  function readDeclaredContributions(extension) {
@@ -12864,7 +13223,7 @@ function emitExtensionError(emitter, qualifiedId2, nodePath, data) {
12864
13223
  })
12865
13224
  );
12866
13225
  }
12867
- function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
13226
+ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, emitSignal, emitNode, store) {
12868
13227
  const scope = extractor.scope ?? "both";
12869
13228
  const settings = extractor.resolvedSettings ?? {};
12870
13229
  return {
@@ -12875,9 +13234,62 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
12875
13234
  emitLink,
12876
13235
  enrichNode,
12877
13236
  emitContribution,
13237
+ emitSignal,
13238
+ emitNode,
12878
13239
  ...store !== void 0 ? { store } : {}
12879
13240
  };
12880
13241
  }
13242
+ var VIRTUAL_NODE_PLACEHOLDER_HASH = "0".repeat(64);
13243
+ function buildVirtualNode(extractor, emitted, emitter) {
13244
+ const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
13245
+ if (typeof emitted.path !== "string" || emitted.path.length === 0) {
13246
+ emitter.emit(
13247
+ makeEvent("extension.error", {
13248
+ kind: "virtual-node-missing-path",
13249
+ extensionId: qualifiedId2,
13250
+ message: `Extractor ${qualifiedId2} emitted a virtual node with no path; dropped.`
13251
+ })
13252
+ );
13253
+ return null;
13254
+ }
13255
+ if (typeof emitted.kind !== "string" || emitted.kind.length === 0) {
13256
+ emitter.emit(
13257
+ makeEvent("extension.error", {
13258
+ kind: "virtual-node-missing-kind",
13259
+ extensionId: qualifiedId2,
13260
+ virtualPath: emitted.path,
13261
+ message: `Extractor ${qualifiedId2} emitted a virtual node at '${emitted.path}' with no kind; dropped.`
13262
+ })
13263
+ );
13264
+ return null;
13265
+ }
13266
+ if (!Array.isArray(emitted.derivedFrom) || emitted.derivedFrom.length === 0) {
13267
+ emitter.emit(
13268
+ makeEvent("extension.error", {
13269
+ kind: "virtual-node-missing-derived-from",
13270
+ extensionId: qualifiedId2,
13271
+ virtualPath: emitted.path,
13272
+ message: `Extractor ${qualifiedId2} emitted a virtual node at '${emitted.path}' with empty derivedFrom; dropped.`
13273
+ })
13274
+ );
13275
+ return null;
13276
+ }
13277
+ const node = {
13278
+ path: emitted.path,
13279
+ kind: emitted.kind,
13280
+ provider: emitted.provider,
13281
+ bodyHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
13282
+ frontmatterHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
13283
+ bytes: { frontmatter: 0, body: 0, total: 0 },
13284
+ linksOutCount: 0,
13285
+ linksInCount: 0,
13286
+ externalRefsCount: 0,
13287
+ virtual: true,
13288
+ derivedFrom: [...emitted.derivedFrom]
13289
+ };
13290
+ if (emitted.frontmatter) node.frontmatter = emitted.frontmatter;
13291
+ return node;
13292
+ }
12881
13293
  function validateLink(extractor, link2, emitter) {
12882
13294
  const knownKinds = ["invokes", "references", "mentions", "supersedes"];
12883
13295
  if (!knownKinds.includes(link2.kind)) {
@@ -12898,9 +13310,68 @@ function validateLink(extractor, link2, emitter) {
12898
13310
  );
12899
13311
  return null;
12900
13312
  }
12901
- const confidence = link2.confidence ?? "medium";
13313
+ const c = link2.confidence;
13314
+ if (c !== void 0 && (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1)) {
13315
+ const qualifiedId2 = `${extractor.pluginId}/${extractor.id}`;
13316
+ emitter.emit(
13317
+ makeEvent("extension.error", {
13318
+ kind: "link-confidence-out-of-range",
13319
+ extensionId: qualifiedId2,
13320
+ confidence: c,
13321
+ message: `Extractor ${qualifiedId2} emitted a Link with confidence ${String(c)} outside [0..1]; dropped.`
13322
+ })
13323
+ );
13324
+ return null;
13325
+ }
13326
+ const confidence = c ?? ConfidenceTier.MEDIUM;
12902
13327
  return { ...link2, confidence };
12903
13328
  }
13329
+ var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes"];
13330
+ function validateSignal(extractor, signal, emitter) {
13331
+ const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
13332
+ if (!Array.isArray(signal.candidates) || signal.candidates.length === 0) {
13333
+ emitter.emit(
13334
+ makeEvent("extension.error", {
13335
+ kind: "signal-no-candidates",
13336
+ extensionId: qualifiedId2,
13337
+ signal: { source: signal.source, scope: signal.scope },
13338
+ message: `Extractor ${qualifiedId2} emitted a Signal with no candidates; dropped.`
13339
+ })
13340
+ );
13341
+ return null;
13342
+ }
13343
+ for (const candidate of signal.candidates) {
13344
+ if (!isValidSignalCandidate(qualifiedId2, candidate, emitter)) return null;
13345
+ }
13346
+ return signal;
13347
+ }
13348
+ function isValidSignalCandidate(qualifiedId2, candidate, emitter) {
13349
+ if (!KNOWN_LINK_KINDS.includes(candidate.kind)) {
13350
+ emitter.emit(
13351
+ makeEvent("extension.error", {
13352
+ kind: "signal-candidate-kind-not-declared",
13353
+ extensionId: qualifiedId2,
13354
+ candidateKind: candidate.kind,
13355
+ declaredKinds: KNOWN_LINK_KINDS,
13356
+ message: `Extractor ${qualifiedId2} emitted a Signal candidate with off-enum kind '${String(candidate.kind)}'; dropped.`
13357
+ })
13358
+ );
13359
+ return false;
13360
+ }
13361
+ const c = candidate.confidence;
13362
+ if (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1) {
13363
+ emitter.emit(
13364
+ makeEvent("extension.error", {
13365
+ kind: "signal-candidate-confidence-out-of-range",
13366
+ extensionId: qualifiedId2,
13367
+ confidence: candidate.confidence,
13368
+ message: `Extractor ${qualifiedId2} emitted a Signal candidate with confidence ${String(c)} outside [0..1]; dropped.`
13369
+ })
13370
+ );
13371
+ return false;
13372
+ }
13373
+ return true;
13374
+ }
12904
13375
  function dedupeLinks(links) {
12905
13376
  const out = /* @__PURE__ */ new Map();
12906
13377
  for (const link2 of links) {
@@ -12915,6 +13386,9 @@ function dedupeLinks(links) {
12915
13386
  existing.sources = [...existing.sources, src];
12916
13387
  }
12917
13388
  }
13389
+ if (link2.confidence > existing.confidence) {
13390
+ existing.confidence = link2.confidence;
13391
+ }
12918
13392
  continue;
12919
13393
  }
12920
13394
  out.set(key, link2);
@@ -12947,7 +13421,9 @@ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
12947
13421
  }
12948
13422
  }
12949
13423
  var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
13424
+ var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
12950
13425
  function isExternalUrlLink(link2) {
13426
+ if (VIRTUAL_NODE_SCHEME_RE.test(link2.target)) return false;
12951
13427
  return EXTERNAL_URL_SCHEME_RE.test(link2.target);
12952
13428
  }
12953
13429
 
@@ -13106,13 +13582,9 @@ function originatingNodeOf(link2, priorNodePaths) {
13106
13582
  }
13107
13583
  function computeCacheDecision(opts) {
13108
13584
  const applicableExtractors = opts.extractors.filter((ex) => {
13109
- const kinds = ex.precondition?.kind;
13110
- if (!kinds || kinds.length === 0) return true;
13111
- return kinds.some((qualified) => {
13112
- const slashIdx = qualified.indexOf("/");
13113
- const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
13114
- return kindOnly === opts.kind;
13115
- });
13585
+ if (!matchesKindPrecondition(ex, opts.kind)) return false;
13586
+ if (!matchesProviderPrecondition(ex, opts.provider, opts.activeProvider)) return false;
13587
+ return true;
13116
13588
  });
13117
13589
  const applicableQualifiedIds = new Set(
13118
13590
  applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
@@ -13126,6 +13598,22 @@ function computeCacheDecision(opts) {
13126
13598
  fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0
13127
13599
  };
13128
13600
  }
13601
+ function matchesKindPrecondition(ex, kind) {
13602
+ const kinds = ex.precondition?.kind;
13603
+ if (!kinds || kinds.length === 0) return true;
13604
+ return kinds.some((qualified) => {
13605
+ const slashIdx = qualified.indexOf("/");
13606
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
13607
+ return kindOnly === kind;
13608
+ });
13609
+ }
13610
+ function matchesProviderPrecondition(ex, nodeProvider, activeProvider) {
13611
+ const providers = ex.precondition?.provider;
13612
+ if (!providers || providers.length === 0) return true;
13613
+ if (!providers.includes(nodeProvider)) return false;
13614
+ if (activeProvider === null) return false;
13615
+ return providers.includes(activeProvider);
13616
+ }
13129
13617
  function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
13130
13618
  const cachedQualifiedIds = /* @__PURE__ */ new Set();
13131
13619
  const missingExtractors = [];
@@ -13225,6 +13713,65 @@ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, appl
13225
13713
  return "obsolete";
13226
13714
  }
13227
13715
 
13716
+ // kernel/orchestrator/lift-mention-confidence.ts
13717
+ function liftMentionConfidence(links, nodes) {
13718
+ if (!links.some((l) => l.kind === "mentions")) return;
13719
+ const byPath3 = /* @__PURE__ */ new Set();
13720
+ for (const node of nodes) byPath3.add(node.path);
13721
+ const byNormalizedName = indexByNormalizedName2(nodes);
13722
+ for (const link2 of links) {
13723
+ if (link2.kind !== "mentions") continue;
13724
+ if (isResolved2(link2, byPath3, byNormalizedName)) {
13725
+ link2.confidence = 1;
13726
+ }
13727
+ }
13728
+ }
13729
+ function isResolved2(link2, byPath3, byNormalizedName) {
13730
+ const normalized = link2.trigger?.normalizedTrigger;
13731
+ if (normalized) {
13732
+ const withoutSigil = normalized.replace(/^[/@]/, "").trim();
13733
+ if (byNormalizedName.has(withoutSigil)) return true;
13734
+ }
13735
+ if (byPath3.has(link2.target)) return true;
13736
+ return false;
13737
+ }
13738
+ function indexByNormalizedName2(nodes) {
13739
+ const out = /* @__PURE__ */ new Map();
13740
+ for (const node of nodes) {
13741
+ const raw = node.frontmatter?.["name"];
13742
+ const name = typeof raw === "string" ? raw : "";
13743
+ if (!name) continue;
13744
+ out.set(normalizeTrigger(name), true);
13745
+ }
13746
+ return out;
13747
+ }
13748
+
13749
+ // kernel/orchestrator/post-walk-transforms.ts
13750
+ var POST_WALK_TRANSFORMS = [
13751
+ {
13752
+ id: "dedupe-links",
13753
+ description: "Collapse identical (source, target, kind, normalizedTrigger) edges across extractors; union sources[] and pick max confidence on merge.",
13754
+ run(links) {
13755
+ return dedupeLinks(links);
13756
+ }
13757
+ },
13758
+ {
13759
+ id: "lift-mention-confidence",
13760
+ description: "Bump resolved `mentions` links to confidence 1.0 once the full node graph is known (post-merge polish).",
13761
+ run(links, nodes) {
13762
+ liftMentionConfidence(links, nodes);
13763
+ }
13764
+ }
13765
+ ];
13766
+ function applyPostWalkTransforms(links, nodes, transforms = POST_WALK_TRANSFORMS) {
13767
+ let current = links;
13768
+ for (const transform of transforms) {
13769
+ const next = transform.run(current, nodes);
13770
+ if (next) current = next;
13771
+ }
13772
+ return current;
13773
+ }
13774
+
13228
13775
  // kernel/orchestrator/renames.ts
13229
13776
  function findHighConfidenceRenames(opts) {
13230
13777
  const ops = [];
@@ -13235,7 +13782,7 @@ function findHighConfidenceRenames(opts) {
13235
13782
  if (opts.claimedNew.has(toPath)) continue;
13236
13783
  const toNode = opts.currentByPath.get(toPath);
13237
13784
  if (toNode.bodyHash === fromNode.bodyHash) {
13238
- ops.push({ from: fromPath, to: toPath, confidence: "high" });
13785
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.HIGH });
13239
13786
  opts.claimedDeleted.add(fromPath);
13240
13787
  opts.claimedNew.add(toPath);
13241
13788
  break;
@@ -13270,13 +13817,13 @@ function claimSingletonRenames(opts) {
13270
13817
  const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
13271
13818
  if (remaining.length === 1) {
13272
13819
  const fromPath = remaining[0];
13273
- ops.push({ from: fromPath, to: toPath, confidence: "medium" });
13820
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM });
13274
13821
  opts.issues.push({
13275
13822
  analyzerId: "auto-rename-medium",
13276
13823
  severity: "warn",
13277
13824
  nodeIds: [toPath],
13278
13825
  message: `Auto-rename (medium confidence): ${fromPath} \u2192 ${toPath}`,
13279
- data: { from: fromPath, to: toPath, confidence: "medium" }
13826
+ data: { from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM }
13280
13827
  });
13281
13828
  opts.claimedDeleted.add(fromPath);
13282
13829
  opts.claimedNew.add(toPath);
@@ -13368,8 +13915,8 @@ function computeDriftStatus(args2) {
13368
13915
  }
13369
13916
 
13370
13917
  // kernel/sidecar/discover-orphans.ts
13371
- import { existsSync as existsSync19, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13372
- import { join as join12, relative as relative4, sep as sep3 } from "path";
13918
+ import { existsSync as existsSync20, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13919
+ import { join as join13, relative as relative4, sep as sep3 } from "path";
13373
13920
  function discoverOrphanSidecars(roots, shouldSkip) {
13374
13921
  const out = [];
13375
13922
  for (const root of roots) {
@@ -13385,7 +13932,7 @@ function walk(root, current, shouldSkip, out) {
13385
13932
  return;
13386
13933
  }
13387
13934
  for (const entry of entries) {
13388
- const full = join12(current, entry.name);
13935
+ const full = join13(current, entry.name);
13389
13936
  const rel = relative4(root, full).split(sep3).join("/");
13390
13937
  if (shouldSkip(rel)) continue;
13391
13938
  if (entry.isSymbolicLink()) continue;
@@ -13396,7 +13943,7 @@ function walk(root, current, shouldSkip, out) {
13396
13943
  if (!entry.isFile()) continue;
13397
13944
  if (!entry.name.endsWith(".sm")) continue;
13398
13945
  const expectedMd = `${full.slice(0, -".sm".length)}.md`;
13399
- if (existsSync19(expectedMd) && safeIsFile(expectedMd)) continue;
13946
+ if (existsSync20(expectedMd) && safeIsFile(expectedMd)) continue;
13400
13947
  out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
13401
13948
  }
13402
13949
  }
@@ -13410,7 +13957,7 @@ function safeIsFile(path) {
13410
13957
 
13411
13958
  // kernel/orchestrator/node-build.ts
13412
13959
  import { createHash } from "crypto";
13413
- import { existsSync as existsSync20 } from "fs";
13960
+ import { existsSync as existsSync21 } from "fs";
13414
13961
  import { isAbsolute as isAbsolute6, resolve as resolvePath } from "path";
13415
13962
  import "js-tiktoken/lite";
13416
13963
  import yaml4 from "js-yaml";
@@ -13574,11 +14121,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
13574
14121
  }
13575
14122
  function resolveAbsoluteMdPath(relativePath2, roots) {
13576
14123
  if (isAbsolute6(relativePath2)) {
13577
- return existsSync20(relativePath2) ? relativePath2 : null;
14124
+ return existsSync21(relativePath2) ? relativePath2 : null;
13578
14125
  }
13579
14126
  for (const root of roots) {
13580
14127
  const candidate = resolvePath(root, relativePath2);
13581
- if (existsSync20(candidate)) return candidate;
14128
+ if (existsSync21(candidate)) return candidate;
13582
14129
  }
13583
14130
  return null;
13584
14131
  }
@@ -13714,6 +14261,8 @@ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextInde
13714
14261
  const cacheDecision = computeCacheDecision({
13715
14262
  extractors: wctx.opts.extractors,
13716
14263
  kind,
14264
+ provider: provider.id,
14265
+ activeProvider: wctx.opts.activeProvider,
13717
14266
  nodePath: raw.path,
13718
14267
  bodyHash,
13719
14268
  sidecarAnnotationsHash,
@@ -13816,6 +14365,10 @@ function mergeExtractResult(extractResult, accum) {
13816
14365
  accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
13817
14366
  }
13818
14367
  for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
14368
+ for (const vn of extractResult.virtualNodes) {
14369
+ if (accum.nodes.some((n) => n.path === vn.path)) continue;
14370
+ accum.nodes.push(vn);
14371
+ }
13819
14372
  }
13820
14373
  function isPartialCacheHit(ctx) {
13821
14374
  return ctx.nodeHashCacheEligible && ctx.cacheDecision.cachedQualifiedIds.size > 0 && ctx.priorNode !== void 0;
@@ -13923,9 +14476,10 @@ async function runScanInternal(_kernel, options) {
13923
14476
  priorIndex: setup.priorIndex,
13924
14477
  priorExtractorRuns: setup.priorExtractorRuns,
13925
14478
  providerFrontmatter: setup.providerFrontmatter,
13926
- pluginStores: options.pluginStores
14479
+ pluginStores: options.pluginStores,
14480
+ activeProvider: resolveActiveProviderOption(options.activeProvider, options.roots)
13927
14481
  });
13928
- walked.internalLinks = dedupeLinks(walked.internalLinks);
14482
+ walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes);
13929
14483
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
13930
14484
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
13931
14485
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -14041,17 +14595,27 @@ function validateRoots(roots) {
14041
14595
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
14042
14596
  }
14043
14597
  for (const root of roots) {
14044
- if (!existsSync21(root) || !statSync7(root).isDirectory()) {
14598
+ if (!existsSync22(root) || !statSync7(root).isDirectory()) {
14045
14599
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
14046
14600
  }
14047
14601
  }
14048
14602
  }
14603
+ function resolveActiveProviderOption(optionValue, roots) {
14604
+ if (optionValue !== void 0) return optionValue;
14605
+ for (const root of roots) {
14606
+ const absRoot = isAbsolute7(root) ? root : resolve27(root);
14607
+ if (!existsSync22(absRoot)) continue;
14608
+ const detected = resolveActiveProvider(absRoot).resolved;
14609
+ if (detected !== null) return detected;
14610
+ }
14611
+ return null;
14612
+ }
14049
14613
 
14050
14614
  // kernel/scan/watcher.ts
14051
- import { resolve as resolve27, relative as relative5, sep as sep4 } from "path";
14615
+ import { resolve as resolve28, relative as relative5, sep as sep4 } from "path";
14052
14616
  import chokidar from "chokidar";
14053
14617
  function createChokidarWatcher(opts) {
14054
- const absRoots = opts.roots.map((r) => resolve27(opts.cwd, r));
14618
+ const absRoots = opts.roots.map((r) => resolve28(opts.cwd, r));
14055
14619
  const ignoreFilterOpt = opts.ignoreFilter;
14056
14620
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
14057
14621
  const ignored = getFilter ? (path) => {
@@ -14270,7 +14834,7 @@ function createKernel() {
14270
14834
 
14271
14835
  // kernel/jobs/orphan-files.ts
14272
14836
  import { readdirSync as readdirSync8, statSync as statSync8 } from "fs";
14273
- import { join as join13, resolve as resolve28 } from "path";
14837
+ import { join as join14, resolve as resolve29 } from "path";
14274
14838
  function findOrphanJobFiles(jobsDir, referencedPaths) {
14275
14839
  let entries;
14276
14840
  try {
@@ -14288,7 +14852,7 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
14288
14852
  if (!entry.isFile()) continue;
14289
14853
  const name = entry.name;
14290
14854
  if (!name.endsWith(".md")) continue;
14291
- const abs = resolve28(join13(jobsDir, name));
14855
+ const abs = resolve29(join14(jobsDir, name));
14292
14856
  if (!referencedPaths.has(abs)) orphans.push(abs);
14293
14857
  }
14294
14858
  orphans.sort();
@@ -14352,7 +14916,48 @@ var SCAN_RUNNER_TEXTS = {
14352
14916
  * does not exist on disk. Surfaced once per missing root so the
14353
14917
  * operator notices a typo without the walker silently swallowing it.
14354
14918
  */
14355
- referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.'
14919
+ referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.',
14920
+ /**
14921
+ * Active-provider bootstrap: filesystem auto-detect found no
14922
+ * markers (`.claude/`, `.gemini/`, `.codex/`, `AGENTS.md`, `.cursor/`)
14923
+ * anywhere under cwd or the effective scan roots. Plain-markdown
14924
+ * projects keep scanning fine; provider-specific extractors silently
14925
+ * no-op for this scan.
14926
+ */
14927
+ activeProviderNoMarkerWarning: "No provider markers detected (.claude/, .gemini/, .codex/, AGENTS.md, .cursor/). Scanning as universal markdown only; provider-specific link types (e.g. claude @-directives, /-commands) will not appear in the graph. Set `activeProvider` in .skill-map/settings.json or install a provider plugin to enable them.",
14928
+ /**
14929
+ * Active-provider bootstrap: filesystem auto-detect found exactly
14930
+ * one marker and persisted the detected id to project settings.
14931
+ */
14932
+ activeProviderAutodetected: "Auto-detected activeProvider = {{id}} from filesystem markers; persisted to .skill-map/settings.json.",
14933
+ /**
14934
+ * Active-provider bootstrap: persistence of the auto-detected id
14935
+ * failed (permission, disk full, etc). Non-fatal; the scan
14936
+ * continues with the value in memory for this run.
14937
+ */
14938
+ activeProviderPersistFailed: "Auto-detected activeProvider = {{id}}, but persisting to .skill-map/settings.json failed: {{message}}. Run `sm config set activeProvider {{id}}` manually to make the choice sticky.",
14939
+ /**
14940
+ * Active-provider bootstrap: ambiguous detection (2+ markers
14941
+ * present), interactive prompt header. Followed by one
14942
+ * `activeProviderPromptOption` per detected provider id.
14943
+ */
14944
+ activeProviderPromptHeader: "Multiple provider markers detected. Pick the active lens for this project:",
14945
+ activeProviderPromptOption: " {{index}}) {{id}}",
14946
+ activeProviderPromptInput: "Enter the number or provider id: ",
14947
+ /**
14948
+ * Active-provider bootstrap: ambiguous detection under `--yes`. The
14949
+ * caller exits non-zero; this message names the candidates and how
14950
+ * to resolve.
14951
+ */
14952
+ activeProviderAmbiguousUnderYes: "Multiple provider markers detected ({{candidates}}) and --yes is set. Set the lens explicitly with `sm config set activeProvider <id>` and re-run.",
14953
+ /**
14954
+ * Active lens points at a bundle the operator has disabled (via
14955
+ * `sm plugins disable <id>` or the Settings UI). Classification keeps
14956
+ * running because it's provider-driven, but the lens-gated extractors
14957
+ * for the disabled bundle silently no-op. Without this warning the
14958
+ * graph quietly differs from what the lens implies.
14959
+ */
14960
+ activeProviderBundleDisabledWarning: 'activeProvider = "{{id}}" but the "{{id}}" plugin bundle is currently disabled; provider-specific extractors will not run. Re-enable the bundle with `sm plugins enable {{id}}` or switch the lens with `sm config set activeProvider <id>` to silence this warning.'
14356
14961
  };
14357
14962
 
14358
14963
  // core/runtime/scan-roots.ts
@@ -14366,7 +14971,7 @@ function resolveScanRoots(inputs) {
14366
14971
  // core/runtime/reference-paths-walker.ts
14367
14972
  import { readdirSync as readdirSync9, statSync as statSync9 } from "fs";
14368
14973
  import { homedir as osHomedir2 } from "os";
14369
- import { isAbsolute as isAbsolute7, join as join14, resolve as resolve29 } from "path";
14974
+ import { isAbsolute as isAbsolute8, join as join15, resolve as resolve30 } from "path";
14370
14975
  var REFERENCE_WALK_MAX_FILES = 5e4;
14371
14976
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14372
14977
  "node_modules",
@@ -14374,10 +14979,10 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14374
14979
  SKILL_MAP_DIR
14375
14980
  ]);
14376
14981
  function resolveScanPath(raw, cwd) {
14377
- if (raw.startsWith("~/")) return resolve29(join14(osHomedir2(), raw.slice(2)));
14378
- if (raw === "~") return resolve29(osHomedir2());
14379
- if (isAbsolute7(raw)) return resolve29(raw);
14380
- return resolve29(cwd, raw);
14982
+ if (raw.startsWith("~/")) return resolve30(join15(osHomedir2(), raw.slice(2)));
14983
+ if (raw === "~") return resolve30(osHomedir2());
14984
+ if (isAbsolute8(raw)) return resolve30(raw);
14985
+ return resolve30(cwd, raw);
14381
14986
  }
14382
14987
  function walkReferencePaths(rawRoots, cwd) {
14383
14988
  const paths = /* @__PURE__ */ new Set();
@@ -14406,7 +15011,7 @@ function walkInto(dir, out) {
14406
15011
  for (const entry of entries) {
14407
15012
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
14408
15013
  if (entry.isSymbolicLink()) continue;
14409
- const full = join14(dir, entry.name);
15014
+ const full = join15(dir, entry.name);
14410
15015
  if (entry.isDirectory()) {
14411
15016
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
14412
15017
  if (walkInto(full, out)) return true;
@@ -14424,6 +15029,101 @@ function safeStat(path) {
14424
15029
  }
14425
15030
  }
14426
15031
 
15032
+ // core/runtime/active-provider-bootstrap.ts
15033
+ import { createInterface as createInterface2 } from "readline";
15034
+ import { isAbsolute as isAbsolute9, join as join16 } from "path";
15035
+ async function bootstrapActiveProvider(opts) {
15036
+ const fromCwd = resolveActiveProvider(opts.cwd);
15037
+ if (fromCwd.source === "config") {
15038
+ return { kind: "ok", activeProvider: fromCwd.resolved, source: "config" };
15039
+ }
15040
+ const detected = aggregateDetected(opts.cwd, opts.effectiveRoots, fromCwd.detected);
15041
+ if (detected.length === 0) {
15042
+ opts.printer.warn(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarning);
15043
+ return { kind: "ok", activeProvider: null, source: "none" };
15044
+ }
15045
+ if (detected.length === 1) {
15046
+ const picked2 = detected[0];
15047
+ persistActiveProvider(opts.cwd, picked2, opts.printer);
15048
+ return { kind: "ok", activeProvider: picked2, source: "autodetect" };
15049
+ }
15050
+ if (opts.yes) {
15051
+ return { kind: "ambiguous", detected };
15052
+ }
15053
+ const picked = await promptForLens(detected, opts.stdin, opts.stderr);
15054
+ if (picked === null) {
15055
+ return { kind: "ambiguous", detected };
15056
+ }
15057
+ persistActiveProvider(opts.cwd, picked, opts.printer);
15058
+ return { kind: "ok", activeProvider: picked, source: "autodetect" };
15059
+ }
15060
+ function aggregateDetected(cwd, effectiveRoots, cwdDetected) {
15061
+ const out = [];
15062
+ const seen = /* @__PURE__ */ new Set();
15063
+ for (const id of cwdDetected) {
15064
+ if (seen.has(id)) continue;
15065
+ seen.add(id);
15066
+ out.push(id);
15067
+ }
15068
+ for (const root of effectiveRoots) {
15069
+ const absRoot = isAbsolute9(root) ? root : join16(cwd, root);
15070
+ const r = resolveActiveProvider(absRoot);
15071
+ for (const id of r.detected) {
15072
+ if (seen.has(id)) continue;
15073
+ seen.add(id);
15074
+ out.push(id);
15075
+ }
15076
+ }
15077
+ return out;
15078
+ }
15079
+ function persistActiveProvider(cwd, id, printer) {
15080
+ try {
15081
+ writeConfigValue("activeProvider", id, { target: "project", cwd });
15082
+ printer.info(tx(SCAN_RUNNER_TEXTS.activeProviderAutodetected, { id }));
15083
+ } catch (err) {
15084
+ const message = err instanceof Error ? err.message : String(err);
15085
+ printer.warn(
15086
+ tx(SCAN_RUNNER_TEXTS.activeProviderPersistFailed, { id, message })
15087
+ );
15088
+ }
15089
+ }
15090
+ function warnIfLensBundleDisabled(args2) {
15091
+ if (args2.activeProvider === null) return;
15092
+ if (args2.resolveEnabled(args2.activeProvider)) return;
15093
+ args2.printer.warn(
15094
+ tx(SCAN_RUNNER_TEXTS.activeProviderBundleDisabledWarning, {
15095
+ id: args2.activeProvider
15096
+ })
15097
+ );
15098
+ }
15099
+ async function promptForLens(detected, stdin, stderr) {
15100
+ const lines = [SCAN_RUNNER_TEXTS.activeProviderPromptHeader];
15101
+ for (let i = 0; i < detected.length; i += 1) {
15102
+ lines.push(
15103
+ tx(SCAN_RUNNER_TEXTS.activeProviderPromptOption, {
15104
+ index: i + 1,
15105
+ id: detected[i]
15106
+ })
15107
+ );
15108
+ }
15109
+ stderr.write(lines.join("\n") + "\n");
15110
+ const rl = createInterface2({ input: stdin, output: stderr });
15111
+ try {
15112
+ const answer = await new Promise(
15113
+ (resolveP) => rl.question(SCAN_RUNNER_TEXTS.activeProviderPromptInput, resolveP)
15114
+ );
15115
+ const trimmed = answer.trim();
15116
+ const asNumber = Number.parseInt(trimmed, 10);
15117
+ if (!Number.isNaN(asNumber) && asNumber >= 1 && asNumber <= detected.length) {
15118
+ return detected[asNumber - 1];
15119
+ }
15120
+ const asId = detected.find((d) => d.toLowerCase() === trimmed.toLowerCase());
15121
+ return asId ?? null;
15122
+ } finally {
15123
+ rl.close();
15124
+ }
15125
+ }
15126
+
14427
15127
  // core/runtime/scan-runner.ts
14428
15128
  async function runScanForCommand(opts) {
14429
15129
  const ctx = opts.ctx ?? defaultRuntimeContext();
@@ -14431,20 +15131,9 @@ async function runScanForCommand(opts) {
14431
15131
  const kernel = createKernel();
14432
15132
  const pluginRuntime = await preparePluginRuntime(opts, opts.printer);
14433
15133
  const extensions = registerExtensions(kernel, pluginRuntime, opts);
14434
- let cfg;
14435
- try {
14436
- cfg = loadConfig({ strict: opts.strict, ...ctx }).effective;
14437
- } catch (err) {
14438
- return { kind: "config-error", message: formatErrorMessage(err) };
14439
- }
14440
- const ignoreFilter = buildScanIgnoreFilter(cfg, ctx.cwd);
14441
- const strict = opts.strict || cfg.scan.strict === true;
14442
- let effectiveRoots;
14443
- try {
14444
- effectiveRoots = resolveScanRoots({ positionalRoots: opts.roots });
14445
- } catch (err) {
14446
- return { kind: "config-error", message: formatErrorMessage(err) };
14447
- }
15134
+ const scanInputs = loadScanInputs(opts, ctx);
15135
+ if ("kind" in scanInputs) return scanInputs;
15136
+ const { cfg, ignoreFilter, strict, effectiveRoots } = scanInputs;
14448
15137
  let referenceablePaths;
14449
15138
  if (cfg.scan.referencePaths.length > 0) {
14450
15139
  const walk2 = walkReferencePaths(cfg.scan.referencePaths, ctx.cwd);
@@ -14453,6 +15142,9 @@ async function runScanForCommand(opts) {
14453
15142
  }
14454
15143
  const loadPrior = makePriorLoader(opts.noBuiltIns, strict);
14455
15144
  const jobsDir = defaultProjectJobsDir(ctx);
15145
+ const lens = await resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime);
15146
+ if (lens.kind === "ambiguous-provider") return lens;
15147
+ const activeProvider = lens.activeProvider;
14456
15148
  const runScanWith = makeScanRunner(
14457
15149
  kernel,
14458
15150
  opts,
@@ -14461,11 +15153,37 @@ async function runScanForCommand(opts) {
14461
15153
  strict,
14462
15154
  extensions,
14463
15155
  referenceablePaths,
14464
- ctx.cwd
15156
+ ctx.cwd,
15157
+ activeProvider
14465
15158
  );
14466
15159
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
14467
15160
  return willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
14468
15161
  }
15162
+ async function resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime) {
15163
+ const bootstrap = await bootstrapActiveProvider({
15164
+ cwd: ctx.cwd,
15165
+ effectiveRoots,
15166
+ yes: opts.yes ?? false,
15167
+ stdin: opts.stdin ?? process.stdin,
15168
+ stderr: opts.stderr,
15169
+ printer: opts.printer
15170
+ });
15171
+ if (bootstrap.kind === "ambiguous") {
15172
+ return {
15173
+ kind: "ambiguous-provider",
15174
+ detected: bootstrap.detected,
15175
+ message: tx(SCAN_RUNNER_TEXTS.activeProviderAmbiguousUnderYes, {
15176
+ candidates: bootstrap.detected.join(", ")
15177
+ })
15178
+ };
15179
+ }
15180
+ warnIfLensBundleDisabled({
15181
+ activeProvider: bootstrap.activeProvider,
15182
+ resolveEnabled: opts.resolveEnabledOverride ?? pluginRuntime.resolveEnabled,
15183
+ printer: opts.printer
15184
+ });
15185
+ return { kind: "ok", activeProvider: bootstrap.activeProvider };
15186
+ }
14469
15187
  function emitReferenceWalkAdvisory(walk2, opts) {
14470
15188
  if (walk2.truncated) {
14471
15189
  opts.printer.warn(SCAN_RUNNER_TEXTS.referenceWalkTruncated);
@@ -14499,6 +15217,17 @@ function registerExtensions(kernel, pluginRuntime, opts) {
14499
15217
  registerEnabledExtensions(kernel, pluginRuntime, registerOpts);
14500
15218
  return extensions;
14501
15219
  }
15220
+ function loadScanInputs(opts, ctx) {
15221
+ try {
15222
+ const cfg = loadConfig({ strict: opts.strict, ...ctx }).effective;
15223
+ const ignoreFilter = buildScanIgnoreFilter(cfg, ctx.cwd);
15224
+ const strict = opts.strict || cfg.scan.strict === true;
15225
+ const effectiveRoots = resolveScanRoots({ positionalRoots: opts.roots });
15226
+ return { cfg, ignoreFilter, strict, effectiveRoots };
15227
+ } catch (err) {
15228
+ return { kind: "config-error", message: formatErrorMessage(err) };
15229
+ }
15230
+ }
14502
15231
  function buildScanIgnoreFilter(cfg, cwd) {
14503
15232
  const ignoreFileText = readIgnoreFileText(cwd);
14504
15233
  const ignoreFilterOpts = {};
@@ -14521,7 +15250,7 @@ function makePriorLoader(noBuiltIns, strict) {
14521
15250
  return loaded;
14522
15251
  };
14523
15252
  }
14524
- function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd) {
15253
+ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider) {
14525
15254
  return async (prior, priorExtractorRuns, orphanJobFiles) => {
14526
15255
  if (opts.changed && prior === null) {
14527
15256
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
@@ -14535,6 +15264,7 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
14535
15264
  referenceablePaths,
14536
15265
  cwd: scanCwd,
14537
15266
  prior,
15267
+ activeProvider,
14538
15268
  ...priorExtractorRuns ? { priorExtractorRuns } : {},
14539
15269
  ...orphanJobFiles ? { orphanJobFiles } : {}
14540
15270
  });
@@ -14555,7 +15285,8 @@ function buildRunScanOptions(args2) {
14555
15285
  // visible from this caller" (legacy behaviour). The orchestrator
14556
15286
  // defaults to `[]` when the field is absent; we always pass the
14557
15287
  // array (possibly empty) to keep the wiring uniform.
14558
- orphanJobFiles: orphanJobFiles ?? []
15288
+ orphanJobFiles: orphanJobFiles ?? [],
15289
+ activeProvider: args2.activeProvider
14559
15290
  };
14560
15291
  if (args2.extensions) runOptions.extensions = args2.extensions;
14561
15292
  if (prior) {
@@ -14698,7 +15429,7 @@ var InitCommand = class extends SmCommand {
14698
15429
  async run() {
14699
15430
  const ctx = defaultRuntimeContext();
14700
15431
  const scopeRoot = ctx.cwd;
14701
- const skillMapDir = join15(scopeRoot, SKILL_MAP_DIR);
15432
+ const skillMapDir = join17(scopeRoot, SKILL_MAP_DIR);
14702
15433
  const settingsPath = defaultSettingsPath(scopeRoot);
14703
15434
  const localPath = defaultLocalSettingsPath(scopeRoot);
14704
15435
  const ignorePath = defaultIgnoreFilePath(scopeRoot);
@@ -14744,7 +15475,7 @@ var InitCommand = class extends SmCommand {
14744
15475
  const okGlyph = ansi.green("\u2713");
14745
15476
  const updated = await ensureGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
14746
15477
  if (updated) {
14747
- const gitignorePath = join15(scopeRoot, ".gitignore");
15478
+ const gitignorePath = join17(scopeRoot, ".gitignore");
14748
15479
  printer.info(
14749
15480
  GITIGNORE_ENTRIES.length === 1 ? tx(INIT_TEXTS.gitignoreUpdatedSingular, { glyph: okGlyph, path: gitignorePath }) : tx(INIT_TEXTS.gitignoreUpdatedPlural, {
14750
15481
  glyph: okGlyph,
@@ -14783,7 +15514,7 @@ async function dryRunFileMessage(path) {
14783
15514
  }
14784
15515
  async function writeDryRunGitignorePlan(printer, scopeRoot) {
14785
15516
  const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
14786
- const gitignorePath = join15(scopeRoot, ".gitignore");
15517
+ const gitignorePath = join17(scopeRoot, ".gitignore");
14787
15518
  if (wouldAdd.length === 0) {
14788
15519
  printer.info(tx(INIT_TEXTS.dryRunWouldLeaveGitignoreUnchanged, { path: gitignorePath }));
14789
15520
  } else if (wouldAdd.length === 1) {
@@ -14822,7 +15553,14 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
14822
15553
  strict,
14823
15554
  stderr,
14824
15555
  printer,
14825
- ctx: { cwd: scopeRoot }
15556
+ ctx: { cwd: scopeRoot },
15557
+ // Init's first scan is a provisioning step, not the user's
15558
+ // primary "show me my graph" call. Don't block waiting for the
15559
+ // operator to disambiguate the lens here; let init complete with
15560
+ // `activeProvider` unset and let the FIRST explicit `sm scan`
15561
+ // surface the prompt. Treat the `ambiguous-provider` outcome below
15562
+ // as a soft hint, not a failure.
15563
+ yes: true
14826
15564
  });
14827
15565
  const errGlyph = ansi.red("\u2715");
14828
15566
  if (outcome.kind === "config-error") {
@@ -14842,6 +15580,10 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
14842
15580
  );
14843
15581
  return ExitCode.Error;
14844
15582
  }
15583
+ if (outcome.kind === "ambiguous-provider") {
15584
+ printer.warn(outcome.message);
15585
+ return ExitCode.Ok;
15586
+ }
14845
15587
  const result = outcome.result;
14846
15588
  const hasErrors = result.issues.some((i) => i.severity === "error");
14847
15589
  printer.info(
@@ -14858,7 +15600,7 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
14858
15600
  return hasErrors ? ExitCode.Issues : ExitCode.Ok;
14859
15601
  }
14860
15602
  async function previewGitignoreEntries(scopeRoot, entries) {
14861
- const path = join15(scopeRoot, ".gitignore");
15603
+ const path = join17(scopeRoot, ".gitignore");
14862
15604
  const body = await pathExists(path) ? await readFile2(path, "utf8") : "";
14863
15605
  const present = new Set(
14864
15606
  body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
@@ -14866,7 +15608,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
14866
15608
  return entries.filter((entry) => !present.has(entry));
14867
15609
  }
14868
15610
  async function ensureGitignoreEntries(scopeRoot, entries) {
14869
- const path = join15(scopeRoot, ".gitignore");
15611
+ const path = join17(scopeRoot, ".gitignore");
14870
15612
  let body = "";
14871
15613
  if (await pathExists(path)) {
14872
15614
  body = await readFile2(path, "utf8");
@@ -16431,8 +17173,8 @@ var PLUGINS_TEXTS = {
16431
17173
  doctorIssueEntry: " {{glyph}} {{id}} {{status}}\n",
16432
17174
  doctorIssueBody: " {{line}}\n",
16433
17175
  // --- enable / disable -----------------------------------------------
16434
- toggleBothIdAndAll: "{{glyph}} Pass either an <id> or --all, not both.\n",
16435
- toggleNeitherIdNorAll: "{{glyph}} Pass <id> or --all.\n",
17176
+ toggleBothIdAndAll: "{{glyph}} Pass either one or more <id> arguments or --all, not both.\n",
17177
+ toggleNeitherIdNorAll: "{{glyph}} Pass one or more <id> arguments, or --all.\n",
16436
17178
  toggleResolveError: "{{error}}",
16437
17179
  toggleAppliedSingle: "{{verbPast}}: {{id}}\n",
16438
17180
  toggleAppliedManyHeader: "{{verbPast}}: {{count}} plugin(s)\n",
@@ -16534,9 +17276,9 @@ var PLUGINS_TEXTS = {
16534
17276
  };
16535
17277
 
16536
17278
  // cli/commands/plugins/shared.ts
16537
- import { resolve as resolve30 } from "path";
17279
+ import { resolve as resolve31 } from "path";
16538
17280
  function resolveSearchPaths2(opts, cwd) {
16539
- if (opts.pluginDir) return [resolve30(opts.pluginDir)];
17281
+ if (opts.pluginDir) return [resolve31(opts.pluginDir)];
16540
17282
  return [defaultProjectPluginsDir({ cwd })];
16541
17283
  }
16542
17284
  async function buildResolver() {
@@ -17418,7 +18160,7 @@ function buildDoctorJsonEnvelope(args2) {
17418
18160
  import { Command as Command25, Option as Option24 } from "clipanion";
17419
18161
  var TogglePluginsBase = class extends SmCommand {
17420
18162
  all = Option24.Boolean("--all", false);
17421
- id = Option24.String({ required: false });
18163
+ ids = Option24.Rest({ name: "ids" });
17422
18164
  async toggle(enabled) {
17423
18165
  const verb = enabled ? "enable" : "disable";
17424
18166
  const stderrAnsi = this.ansiFor("stderr");
@@ -17437,23 +18179,24 @@ var TogglePluginsBase = class extends SmCommand {
17437
18179
  return ExitCode.Ok;
17438
18180
  }
17439
18181
  /**
17440
- * `--all` vs `<id>` mutex check. The two are mutually exclusive and
17441
- * one must be present; surfaces a directed error on misuse.
18182
+ * `--all` vs `<id>...` mutex check. The two are mutually exclusive
18183
+ * and one must be present; surfaces a directed error on misuse.
18184
+ * Variadic positional accepts one or more ids.
17442
18185
  */
17443
18186
  #validateArgs(ansi) {
17444
18187
  const errGlyph = ansi.red("\u2715");
17445
- if (this.all && this.id) {
18188
+ if (this.all && this.ids.length > 0) {
17446
18189
  this.printer.error(tx(PLUGINS_TEXTS.toggleBothIdAndAll, { glyph: errGlyph }));
17447
18190
  return ExitCode.Error;
17448
18191
  }
17449
- if (!this.all && !this.id) {
18192
+ if (!this.all && this.ids.length === 0) {
17450
18193
  this.printer.error(tx(PLUGINS_TEXTS.toggleNeitherIdNorAll, { glyph: errGlyph }));
17451
18194
  return ExitCode.Error;
17452
18195
  }
17453
18196
  return null;
17454
18197
  }
17455
18198
  /**
17456
- * Resolve `<id>` against the catalogue or fan out via `--all`.
18199
+ * Resolve `<id>...` against the catalogue or fan out via `--all`.
17457
18200
  * Returns the target list on success, or the exit code on a
17458
18201
  * directed-error path (unknown id, granularity mismatch).
17459
18202
  *
@@ -17464,25 +18207,35 @@ var TogglePluginsBase = class extends SmCommand {
17464
18207
  * the directed error message when they try the bundle id directly,
17465
18208
  * so `--all` skips them here too and the real "disable every core
17466
18209
  * extension" intent is served by `--no-built-ins` on `sm scan`.
18210
+ *
18211
+ * Variadic mode is all-or-nothing: the first bad id aborts the
18212
+ * batch before any DB write, so the user never lands in a partial
18213
+ * state. Repeated ids in the same call are deduped.
17467
18214
  */
17468
18215
  #pickTargets(catalogue, verb, ansi) {
17469
18216
  if (this.all) {
17470
18217
  return catalogue.filter((b) => b.granularity === "bundle").map((b) => b.id);
17471
18218
  }
17472
- const resolved = resolveToggleTarget(this.id, catalogue, verb, ansi);
17473
- if ("error" in resolved) {
17474
- this.printer.error(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
17475
- return ExitCode.NotFound;
18219
+ const keys = [];
18220
+ for (const rawId of this.ids) {
18221
+ const resolved = resolveToggleTarget(rawId, catalogue, verb, ansi);
18222
+ if ("error" in resolved) {
18223
+ this.printer.error(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
18224
+ return ExitCode.NotFound;
18225
+ }
18226
+ keys.push(resolved.key);
17476
18227
  }
17477
- return [resolved.key];
18228
+ return [...new Set(keys)];
17478
18229
  }
17479
18230
  /**
17480
- * Host lock, see `src/kernel/config/locked-plugins.ts`. `--all`
17481
- * silently skips locked targets so the user can still toggle the
17482
- * rest. Single-id mode surfaces a directed exit-5 message.
18231
+ * Host lock, see `src/kernel/config/locked-plugins.ts`. Bulk modes
18232
+ * (`--all` or an explicit batch of >1 ids) silently skip locked
18233
+ * targets so the user can still toggle the rest. Single-id mode
18234
+ * surfaces a directed exit-5 message so the user knows their one
18235
+ * intended target was refused.
17483
18236
  */
17484
18237
  #applyLockGate(targets, ansi) {
17485
- if (this.all) return targets.filter((id) => !isPluginLocked(id));
18238
+ if (this.all || this.ids.length > 1) return targets.filter((id) => !isPluginLocked(id));
17486
18239
  const lockedHit = targets.find((id) => isPluginLocked(id));
17487
18240
  if (!lockedHit) return targets;
17488
18241
  this.printer.error(
@@ -17538,12 +18291,19 @@ var PluginsEnableCommand = class extends TogglePluginsBase {
17538
18291
  static paths = [["plugins", "enable"]];
17539
18292
  static usage = Command25.Usage({
17540
18293
  category: "Plugins",
17541
- description: "Enable a plugin (or --all). Persists in config_plugins.",
18294
+ description: "Enable one or more plugins (or --all). Persists in config_plugins.",
17542
18295
  details: `
17543
- Writes a row to config_plugins with enabled=1. Takes precedence
17544
- over the team-shared baseline at settings.json#/plugins/<id>/enabled.
17545
- Use sm plugins disable to flip; sm config reset plugins.<id>.enabled
17546
- drops the settings.json baseline.
18296
+ Writes a row to config_plugins with enabled=1 per id. Takes
18297
+ precedence over the team-shared baseline at
18298
+ settings.json#/plugins/<id>/enabled. Use sm plugins disable to
18299
+ flip; sm config reset plugins.<id>.enabled drops the settings.json
18300
+ baseline.
18301
+
18302
+ Accepts one or more ids in one call, e.g.
18303
+ 'sm plugins enable claude gemini openai'. Batches are
18304
+ all-or-nothing: a single unknown / mismatched id aborts before
18305
+ any write. Repeated ids are deduped. Locked plugins inside a
18306
+ batch are silently skipped.
17547
18307
 
17548
18308
  Granularity: a bundle-granularity plugin (default for user plugins,
17549
18309
  and the built-in 'claude' bundle) accepts only the bundle id. An
@@ -17560,12 +18320,18 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
17560
18320
  static paths = [["plugins", "disable"]];
17561
18321
  static usage = Command25.Usage({
17562
18322
  category: "Plugins",
17563
- description: "Disable a plugin (or --all). Persists in config_plugins; does not delete files.",
18323
+ description: "Disable one or more plugins (or --all). Persists in config_plugins; does not delete files.",
17564
18324
  details: `
17565
- Writes a row to config_plugins with enabled=0. Discovery still
17566
- surfaces the plugin in sm plugins list, but with status=disabled;
17567
- its extensions are not imported and the kernel will not run
17568
- them.
18325
+ Writes a row to config_plugins with enabled=0 per id. Discovery
18326
+ still surfaces the plugin in sm plugins list, but with
18327
+ status=disabled; its extensions are not imported and the kernel
18328
+ will not run them.
18329
+
18330
+ Accepts one or more ids in one call, e.g.
18331
+ 'sm plugins disable gemini openai agent-skills'. Batches are
18332
+ all-or-nothing: a single unknown / mismatched id aborts before
18333
+ any write. Repeated ids are deduped. Locked plugins inside a
18334
+ batch are silently skipped.
17569
18335
 
17570
18336
  Granularity: a bundle-granularity plugin (default for user plugins,
17571
18337
  and the built-in 'claude' bundle) accepts only the bundle id. An
@@ -17671,8 +18437,8 @@ function resolveBareToggle(id, catalogue, verb, ansi) {
17671
18437
  }
17672
18438
 
17673
18439
  // cli/commands/plugins/create.ts
17674
- import { existsSync as existsSync22, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
17675
- import { join as join16, resolve as resolve31 } from "path";
18440
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
18441
+ import { join as join18, resolve as resolve32 } from "path";
17676
18442
  import { Command as Command26, Option as Option25 } from "clipanion";
17677
18443
  var PluginsCreateCommand = class extends SmCommand {
17678
18444
  static paths = [["plugins", "create"]];
@@ -17698,8 +18464,8 @@ var PluginsCreateCommand = class extends SmCommand {
17698
18464
  }
17699
18465
  const ctx = defaultRuntimeContext();
17700
18466
  const baseDir = defaultProjectPluginsDir(ctx);
17701
- const targetDir = this.at ? resolve31(this.at) : join16(baseDir, this.pluginId);
17702
- if (existsSync22(targetDir) && !this.force) {
18467
+ const targetDir = this.at ? resolve32(this.at) : join18(baseDir, this.pluginId);
18468
+ if (existsSync23(targetDir) && !this.force) {
17703
18469
  this.printer.error(
17704
18470
  tx(PLUGINS_TEXTS.createRefuseOverwrite, {
17705
18471
  glyph: errGlyph,
@@ -17709,7 +18475,7 @@ var PluginsCreateCommand = class extends SmCommand {
17709
18475
  return ExitCode.Error;
17710
18476
  }
17711
18477
  const extractorName = `${this.pluginId}-extractor`;
17712
- mkdirSync6(join16(targetDir, "extractors", extractorName), { recursive: true });
18478
+ mkdirSync6(join18(targetDir, "extractors", extractorName), { recursive: true });
17713
18479
  const specVersion = installedSpecVersion();
17714
18480
  const manifest = {
17715
18481
  id: this.pluginId,
@@ -17729,14 +18495,14 @@ var PluginsCreateCommand = class extends SmCommand {
17729
18495
  }
17730
18496
  };
17731
18497
  writeFileSync3(
17732
- join16(targetDir, "plugin.json"),
18498
+ join18(targetDir, "plugin.json"),
17733
18499
  JSON.stringify(manifest, null, 2) + "\n"
17734
18500
  );
17735
18501
  writeFileSync3(
17736
- join16(targetDir, "extractors", extractorName, "index.js"),
18502
+ join18(targetDir, "extractors", extractorName, "index.js"),
17737
18503
  scaffolderExtractorStub(extractorName)
17738
18504
  );
17739
- writeFileSync3(join16(targetDir, "README.md"), scaffolderReadme(this.pluginId));
18505
+ writeFileSync3(join18(targetDir, "README.md"), scaffolderReadme(this.pluginId));
17740
18506
  this.printer.data(
17741
18507
  tx(PLUGINS_TEXTS.createSuccess, {
17742
18508
  targetDir: sanitizeForTerminal(targetDir),
@@ -17939,7 +18705,7 @@ var PLUGIN_COMMANDS = [
17939
18705
 
17940
18706
  // cli/commands/refresh.ts
17941
18707
  import { readFile as readFile3 } from "fs/promises";
17942
- import { resolve as resolve32 } from "path";
18708
+ import { resolve as resolve33 } from "path";
17943
18709
  import { Command as Command29, Option as Option27 } from "clipanion";
17944
18710
 
17945
18711
  // cli/i18n/refresh.texts.ts
@@ -18223,7 +18989,7 @@ var RefreshCommand = class extends SmCommand {
18223
18989
  let body;
18224
18990
  try {
18225
18991
  assertContained(cwd, node.path);
18226
- const raw = await readFile3(resolve32(cwd, node.path), "utf8");
18992
+ const raw = await readFile3(resolve33(cwd, node.path), "utf8");
18227
18993
  body = stripFrontmatterFence(raw);
18228
18994
  } catch (err) {
18229
18995
  if (!this.json) {
@@ -18964,6 +19730,9 @@ var ScanCommand = class extends SmCommand {
18964
19730
  watch = Option29.Boolean("--watch", false, {
18965
19731
  description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
18966
19732
  });
19733
+ yes = Option29.Boolean("--yes", false, {
19734
+ description: "Non-interactive mode for ambiguous activeProvider auto-detect. With `--yes`, multiple provider markers (.claude/, .gemini/, .codex/, AGENTS.md, .cursor/) under the scan tree exit non-zero instead of prompting the operator. Set the lens manually via `sm config set activeProvider <id>` and re-run."
19735
+ });
18967
19736
  // Each branch in the orchestrator maps to one validation gate
18968
19737
  // (--watch alias / --changed mutex / -g mutex / dispatch).
18969
19738
  // Splitting per branch scatters the gate from the value it gates.
@@ -18992,9 +19761,11 @@ var ScanCommand = class extends SmCommand {
18992
19761
  allowEmpty: this.allowEmpty,
18993
19762
  strict: this.strict,
18994
19763
  stderr: this.context.stderr,
19764
+ stdin: this.context.stdin,
18995
19765
  printer: this.printer,
18996
19766
  killSwitches: readConformanceKillSwitches(),
18997
- colorEnabled
19767
+ colorEnabled,
19768
+ yes: this.yes
18998
19769
  });
18999
19770
  return outcome.kind === "ok" ? this.renderOutcome(outcome.result, outcome.persistedTo, outcome.dbPath, outcome.strict) : this.renderFailure(outcome);
19000
19771
  }
@@ -19065,6 +19836,12 @@ var ScanCommand = class extends SmCommand {
19065
19836
  );
19066
19837
  return ExitCode.Error;
19067
19838
  }
19839
+ if (outcome.kind === "ambiguous-provider") {
19840
+ this.printer.info(
19841
+ tx(SCAN_TEXTS.scanFailure, { glyph: errGlyph, message: outcome.message })
19842
+ );
19843
+ return ExitCode.Error;
19844
+ }
19068
19845
  this.printer.info(
19069
19846
  tx(SCAN_TEXTS.scanFailure, { glyph: errGlyph, message: outcome.message })
19070
19847
  );
@@ -19147,7 +19924,7 @@ function plural(count, word) {
19147
19924
  }
19148
19925
 
19149
19926
  // cli/commands/scan-compare.ts
19150
- import { existsSync as existsSync23, readFileSync as readFileSync18 } from "fs";
19927
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
19151
19928
  import { Command as Command32, Option as Option30 } from "clipanion";
19152
19929
  var ScanCompareCommand = class extends SmCommand {
19153
19930
  static paths = [["scan", "compare-with"]];
@@ -19259,7 +20036,7 @@ var ScanCompareCommand = class extends SmCommand {
19259
20036
  }
19260
20037
  };
19261
20038
  function loadAndValidateDump(path) {
19262
- if (!existsSync23(path)) {
20039
+ if (!existsSync24(path)) {
19263
20040
  throw new Error(tx(SCAN_TEXTS.compareDumpNotFound, { path }));
19264
20041
  }
19265
20042
  let raw;
@@ -19389,7 +20166,7 @@ function renderDeltaIssues(issues) {
19389
20166
 
19390
20167
  // cli/commands/serve.ts
19391
20168
  import { spawn as spawn2 } from "child_process";
19392
- import { existsSync as existsSync28 } from "fs";
20169
+ import { existsSync as existsSync30 } from "fs";
19393
20170
  import { Command as Command33, Option as Option31 } from "clipanion";
19394
20171
 
19395
20172
  // cli/util/browser-launch.ts
@@ -19411,7 +20188,7 @@ import { WebSocketServer } from "ws";
19411
20188
  // server/app.ts
19412
20189
  import { Hono } from "hono";
19413
20190
  import { bodyLimit } from "hono/body-limit";
19414
- import { HTTPException as HTTPException14 } from "hono/http-exception";
20191
+ import { HTTPException as HTTPException15 } from "hono/http-exception";
19415
20192
 
19416
20193
  // core/config/service.ts
19417
20194
  var ConfigService = class {
@@ -19719,7 +20496,20 @@ var SERVER_TEXTS = {
19719
20496
  // is a synthetic `emitter.error` event; v14.4.a does not yet route
19720
20497
  // it through the broadcaster (would re-enter the same stringify
19721
20498
  // path), so we degrade to a logged warning.
19722
- wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped, failed to serialize event: {{message}}.\n"
20499
+ wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped, failed to serialize event: {{message}}.\n",
20500
+ // ---- active-provider route (routes/active-provider.ts) -----------
20501
+ //
20502
+ // GET / PUT /api/active-provider. The active provider lens selects
20503
+ // which provider's extractors / classifiers / resolution rules apply
20504
+ // to the whole project (see `architecture.md` §Active Provider Lens).
20505
+ // Changing the lens drops the `scan_*` zone atomically and prompts
20506
+ // the user to re-scan; `state_*` and `config_*` survive.
20507
+ activeProviderBodyNotJson: "Request body must be valid JSON.",
20508
+ activeProviderBodyNotObject: "Request body must be a JSON object.",
20509
+ activeProviderBodyMissing: "Request body must include `activeProvider` (a non-empty string).",
20510
+ activeProviderValueNotString: "`activeProvider` must be a string.",
20511
+ activeProviderValueEmpty: "`activeProvider` cannot be the empty string. Send the id of an enabled provider.",
20512
+ activeProviderPersistFailed: "Could not persist activeProvider: {{message}}"
19723
20513
  };
19724
20514
 
19725
20515
  // server/loopback-gate.ts
@@ -20089,7 +20879,7 @@ function contentTypeFor(format) {
20089
20879
  }
20090
20880
 
20091
20881
  // server/health.ts
20092
- import { existsSync as existsSync24 } from "fs";
20882
+ import { existsSync as existsSync25 } from "fs";
20093
20883
  var FALLBACK_SCHEMA_VERSION = "1";
20094
20884
  function buildHealth(deps) {
20095
20885
  return {
@@ -20097,7 +20887,7 @@ function buildHealth(deps) {
20097
20887
  schemaVersion: FALLBACK_SCHEMA_VERSION,
20098
20888
  specVersion: deps.specVersion,
20099
20889
  implVersion: VERSION,
20100
- db: existsSync24(deps.dbPath) ? "present" : "missing",
20890
+ db: existsSync25(deps.dbPath) ? "present" : "missing",
20101
20891
  cwd: deps.cwd,
20102
20892
  dbPath: deps.dbPath
20103
20893
  };
@@ -20210,9 +21000,9 @@ import { HTTPException as HTTPException6 } from "hono/http-exception";
20210
21000
 
20211
21001
  // server/node-body.ts
20212
21002
  import { readFile as readFile4 } from "fs/promises";
20213
- import { isAbsolute as isAbsolute8, resolve as resolvePath2, relative as relativePath, sep as sep5 } from "path";
21003
+ import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as sep5 } from "path";
20214
21004
  async function readNodeBody(cwd, relPath) {
20215
- if (isAbsolute8(relPath)) return null;
21005
+ if (isAbsolute10(relPath)) return null;
20216
21006
  const absRoot = resolvePath2(cwd);
20217
21007
  const absFile = resolvePath2(absRoot, relPath);
20218
21008
  const rel = relativePath(absRoot, absFile);
@@ -20606,11 +21396,6 @@ function registerPluginsRoute(app, deps) {
20606
21396
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
20607
21397
  });
20608
21398
  }
20609
- if (granularityOf(handle) !== "extension") {
20610
- throw new HTTPException8(400, {
20611
- message: tx(SERVER_TEXTS.pluginsGranularityBundleExpected, { id: bundleId })
20612
- });
20613
- }
20614
21399
  if (!hasExtension(handle, extensionId)) {
20615
21400
  throw new HTTPException8(404, {
20616
21401
  message: tx(SERVER_TEXTS.pluginsExtensionUnknown, { bundleId, extensionId })
@@ -20651,7 +21436,7 @@ function buildBuiltInItems(resolveEnabled) {
20651
21436
  return builtInBundles.map((bundle) => {
20652
21437
  const bundleEnabled = resolveEnabled(bundle.id);
20653
21438
  const bundleLocked = isPluginLocked(bundle.id);
20654
- const extensions = bundle.granularity === "extension" ? bundle.extensions.map((ext) => {
21439
+ const extensions = bundle.extensions.map((ext) => {
20655
21440
  const qualified = qualifiedExtensionId(bundle.id, ext.id);
20656
21441
  const extLocked = bundleLocked || isPluginLocked(qualified);
20657
21442
  return {
@@ -20662,7 +21447,7 @@ function buildBuiltInItems(resolveEnabled) {
20662
21447
  ...ext.description ? { description: ext.description } : {},
20663
21448
  ...extLocked ? { locked: true } : {}
20664
21449
  };
20665
- }) : void 0;
21450
+ });
20666
21451
  return {
20667
21452
  id: bundle.id,
20668
21453
  version: firstVersion(bundle.extensions),
@@ -20672,7 +21457,7 @@ function buildBuiltInItems(resolveEnabled) {
20672
21457
  source: "built-in",
20673
21458
  granularity: bundle.granularity,
20674
21459
  description: bundle.description,
20675
- ...extensions ? { extensions } : {},
21460
+ ...extensions.length > 0 ? { extensions } : {},
20676
21461
  ...bundleLocked ? { locked: true } : {}
20677
21462
  };
20678
21463
  });
@@ -20705,8 +21490,8 @@ function optionalDiscoveredFields(plugin, extensions) {
20705
21490
  if (extensions) out.extensions = extensions;
20706
21491
  return out;
20707
21492
  }
20708
- function projectExtensionRows(plugin, granularity, resolveEnabled, bundleLocked) {
20709
- if (granularity !== "extension" || !plugin.extensions) return void 0;
21493
+ function projectExtensionRows(plugin, _granularity, resolveEnabled, bundleLocked) {
21494
+ if (!plugin.extensions || plugin.extensions.length === 0) return void 0;
20710
21495
  return plugin.extensions.map((ext) => {
20711
21496
  const description = readInstanceDescription(ext.instance);
20712
21497
  const qualified = qualifiedExtensionId(plugin.id, ext.id);
@@ -20823,13 +21608,6 @@ function validateBulkChange(change, deps) {
20823
21608
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
20824
21609
  };
20825
21610
  }
20826
- if (granularityOf(handle) !== "extension") {
20827
- return {
20828
- status: 400,
20829
- code: "bad-query",
20830
- message: tx(SERVER_TEXTS.pluginsGranularityBundleExpected, { id: bundleId })
20831
- };
20832
- }
20833
21611
  if (!hasExtension(handle, extensionId)) {
20834
21612
  return {
20835
21613
  status: 404,
@@ -20944,12 +21722,12 @@ var parsePatchBody2 = makeBodyValidator(PATCH_BODY_SCHEMA, {
20944
21722
  import { HTTPException as HTTPException10 } from "hono/http-exception";
20945
21723
 
20946
21724
  // server/util/skillmapignore-io.ts
20947
- import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
20948
- import { resolve as resolve33 } from "path";
21725
+ import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
21726
+ import { resolve as resolve34 } from "path";
20949
21727
  var IGNORE_FILENAME2 = ".skillmapignore";
20950
21728
  function readPatterns(cwd) {
20951
- const path = resolve33(cwd, IGNORE_FILENAME2);
20952
- if (!existsSync25(path)) return [];
21729
+ const path = resolve34(cwd, IGNORE_FILENAME2);
21730
+ if (!existsSync26(path)) return [];
20953
21731
  let raw;
20954
21732
  try {
20955
21733
  raw = readFileSync19(path, "utf8");
@@ -20959,8 +21737,8 @@ function readPatterns(cwd) {
20959
21737
  return raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
20960
21738
  }
20961
21739
  function writePatterns(cwd, nextPatterns) {
20962
- const path = resolve33(cwd, IGNORE_FILENAME2);
20963
- const prior = existsSync25(path) ? safeRead(path) : "";
21740
+ const path = resolve34(cwd, IGNORE_FILENAME2);
21741
+ const prior = existsSync26(path) ? safeRead(path) : "";
20964
21742
  const content = buildContent(prior, nextPatterns);
20965
21743
  writeFileSync4(path, content, "utf8");
20966
21744
  }
@@ -21309,8 +22087,70 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
21309
22087
  }
21310
22088
  });
21311
22089
 
21312
- // server/routes/scan.ts
22090
+ // server/routes/active-provider.ts
22091
+ import { existsSync as existsSync27 } from "fs";
21313
22092
  import { HTTPException as HTTPException12 } from "hono/http-exception";
22093
+ function registerActiveProviderRoute(app, deps) {
22094
+ app.get("/api/active-provider", (c) => {
22095
+ return c.json(buildEnvelope4(deps));
22096
+ });
22097
+ app.patch("/api/active-provider", async (c) => {
22098
+ const body = await parsePatchBody5(c.req.raw);
22099
+ const result = applyLensSwitch(deps, body.activeProvider);
22100
+ deps.configService.reload();
22101
+ return c.json({ ...buildEnvelope4(deps), switch: result });
22102
+ });
22103
+ }
22104
+ function buildEnvelope4(deps) {
22105
+ const r = resolveActiveProvider(deps.runtimeContext.cwd);
22106
+ return {
22107
+ activeProvider: r.resolved,
22108
+ detected: r.detected,
22109
+ source: r.source
22110
+ };
22111
+ }
22112
+ function applyLensSwitch(deps, newValue) {
22113
+ const cwd = deps.runtimeContext.cwd;
22114
+ try {
22115
+ writeConfigValue("activeProvider", newValue, { target: "project", cwd });
22116
+ } catch (err) {
22117
+ throw new HTTPException12(400, {
22118
+ message: tx(SERVER_TEXTS.activeProviderPersistFailed, {
22119
+ message: formatErrorMessage(err)
22120
+ })
22121
+ });
22122
+ }
22123
+ const dbPath = resolveDbPath({ db: void 0, cwd });
22124
+ if (!existsSync27(dbPath)) return { dropped: null };
22125
+ const dropResult = dropScanZone(dbPath);
22126
+ return {
22127
+ dropped: {
22128
+ tableCount: dropResult.tableCount,
22129
+ tableNames: dropResult.droppedTables
22130
+ }
22131
+ };
22132
+ }
22133
+ var PATCH_BODY_SCHEMA4 = {
22134
+ type: "object",
22135
+ additionalProperties: false,
22136
+ required: ["activeProvider"],
22137
+ properties: {
22138
+ activeProvider: { type: "string", minLength: 1 }
22139
+ }
22140
+ };
22141
+ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
22142
+ notJson: SERVER_TEXTS.activeProviderBodyNotJson,
22143
+ notObject: SERVER_TEXTS.activeProviderBodyNotObject,
22144
+ invalid: SERVER_TEXTS.activeProviderBodyMissing,
22145
+ mapping: {
22146
+ ":required": SERVER_TEXTS.activeProviderBodyMissing,
22147
+ "/activeProvider:type:string": SERVER_TEXTS.activeProviderValueNotString,
22148
+ "/activeProvider:minLength": SERVER_TEXTS.activeProviderValueEmpty
22149
+ }
22150
+ });
22151
+
22152
+ // server/routes/scan.ts
22153
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
21314
22154
 
21315
22155
  // server/scan-mutex.ts
21316
22156
  var inFlight = null;
@@ -21318,14 +22158,14 @@ async function withScanMutex(fn) {
21318
22158
  if (inFlight !== null) {
21319
22159
  throw new ScanBusyError();
21320
22160
  }
21321
- let resolve38;
22161
+ let resolve39;
21322
22162
  inFlight = new Promise((r) => {
21323
- resolve38 = r;
22163
+ resolve39 = r;
21324
22164
  });
21325
22165
  try {
21326
22166
  return await fn();
21327
22167
  } finally {
21328
- resolve38();
22168
+ resolve39();
21329
22169
  inFlight = null;
21330
22170
  }
21331
22171
  }
@@ -21465,7 +22305,7 @@ function registerScanRoute(app, deps) {
21465
22305
  }
21466
22306
  async function runPersistedScan(c, deps) {
21467
22307
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21468
- throw new HTTPException12(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
22308
+ throw new HTTPException13(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
21469
22309
  }
21470
22310
  const dbExists = await tryWithSqlite(
21471
22311
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -21491,10 +22331,14 @@ async function runPersistedScan(c, deps) {
21491
22331
  pluginRuntime: deps.pluginRuntime,
21492
22332
  resolveEnabledOverride,
21493
22333
  printer: bffScanRunnerPrinter,
21494
- emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
22334
+ emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster),
22335
+ // BFF has no TTY; ambiguous activeProvider must be resolved by
22336
+ // the operator via the Settings UI (PATCH /api/active-provider)
22337
+ // before the scan, not via interactive prompt here.
22338
+ yes: true
21495
22339
  });
21496
22340
  if (outcome.kind !== "ok") {
21497
- throw new HTTPException12(500, {
22341
+ throw new HTTPException13(500, {
21498
22342
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
21499
22343
  });
21500
22344
  }
@@ -21502,7 +22346,7 @@ async function runPersistedScan(c, deps) {
21502
22346
  });
21503
22347
  } catch (err) {
21504
22348
  if (err instanceof ScanBusyError) {
21505
- throw new HTTPException12(409, { message: SERVER_TEXTS.scanPostBusy });
22349
+ throw new HTTPException13(409, { message: SERVER_TEXTS.scanPostBusy });
21506
22350
  }
21507
22351
  throw err;
21508
22352
  }
@@ -21566,7 +22410,7 @@ function groupTagsBySource2(rows) {
21566
22410
  }
21567
22411
  async function runFreshScan(deps) {
21568
22412
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21569
- throw new HTTPException12(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
22413
+ throw new HTTPException13(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
21570
22414
  }
21571
22415
  const resolveEnabledOverride = await buildBffResolverOverride(deps);
21572
22416
  const outcome = await runScanForCommand({
@@ -21592,10 +22436,13 @@ async function runFreshScan(deps) {
21592
22436
  // fallback. The fresh-scan response body IS the ScanResult JSON,
21593
22437
  // so `data` is never used here; warn/info/error route through
21594
22438
  // `log.warn` (same surface the rest of the BFF uses).
21595
- printer: bffScanRunnerPrinter
22439
+ printer: bffScanRunnerPrinter,
22440
+ // BFF has no TTY; ambiguous activeProvider is the operator's
22441
+ // problem to resolve via the Settings UI, not via prompt here.
22442
+ yes: true
21596
22443
  });
21597
22444
  if (outcome.kind !== "ok") {
21598
- throw new HTTPException12(500, {
22445
+ throw new HTTPException13(500, {
21599
22446
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
21600
22447
  });
21601
22448
  }
@@ -21629,8 +22476,8 @@ function emptyScanResult() {
21629
22476
  }
21630
22477
 
21631
22478
  // server/routes/sidecar.ts
21632
- import { HTTPException as HTTPException13 } from "hono/http-exception";
21633
- import { resolve as resolve34 } from "path";
22479
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
22480
+ import { resolve as resolve35 } from "path";
21634
22481
  var STATUS_FRESH = "fresh";
21635
22482
  var ENVELOPE_KIND2 = "sidecar.bumped";
21636
22483
  var BUMP_BODY_SCHEMA = {
@@ -21664,13 +22511,13 @@ function registerSidecarRoutes(app, deps) {
21664
22511
  let absPath;
21665
22512
  try {
21666
22513
  assertContained(deps.runtimeContext.cwd, node.path);
21667
- absPath = resolve34(deps.runtimeContext.cwd, node.path);
22514
+ absPath = resolve35(deps.runtimeContext.cwd, node.path);
21668
22515
  } catch (err) {
21669
- throw new HTTPException13(500, { message: formatErrorMessage(err) });
22516
+ throw new HTTPException14(500, { message: formatErrorMessage(err) });
21670
22517
  }
21671
22518
  const result = invokeBump2(node, absPath, body);
21672
22519
  if (result.report.ok === false && result.report.reason === "fresh") {
21673
- throw new HTTPException13(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
22520
+ throw new HTTPException14(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
21674
22521
  }
21675
22522
  if (result.report.ok === true && result.report.noop === true) {
21676
22523
  const envelope2 = {
@@ -21697,7 +22544,7 @@ function registerSidecarRoutes(app, deps) {
21697
22544
  }
21698
22545
  } catch (err) {
21699
22546
  if (err instanceof EConsentRequiredError) throw err;
21700
- throw new HTTPException13(500, { message: formatErrorMessage(err) });
22547
+ throw new HTTPException14(500, { message: formatErrorMessage(err) });
21701
22548
  }
21702
22549
  if (body.confirm === true) {
21703
22550
  deps.configService.reload();
@@ -21734,7 +22581,7 @@ async function loadNode(deps, nodePath) {
21734
22581
  );
21735
22582
  const node = persisted?.nodes.find((n) => n.path === nodePath);
21736
22583
  if (!node) {
21737
- throw new HTTPException13(404, {
22584
+ throw new HTTPException14(404, {
21738
22585
  message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
21739
22586
  });
21740
22587
  }
@@ -21742,7 +22589,7 @@ async function loadNode(deps, nodePath) {
21742
22589
  }
21743
22590
  function invokeBump2(node, absPath, body) {
21744
22591
  if (!bumpAction.invoke) {
21745
- throw new HTTPException13(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
22592
+ throw new HTTPException14(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
21746
22593
  }
21747
22594
  const input = {};
21748
22595
  if (body.force === true) input.force = true;
@@ -21788,9 +22635,9 @@ function registerUpdateStatusRoute(app, deps) {
21788
22635
  }
21789
22636
 
21790
22637
  // server/static.ts
21791
- import { existsSync as existsSync26 } from "fs";
22638
+ import { existsSync as existsSync28 } from "fs";
21792
22639
  import { readFile as readFile5 } from "fs/promises";
21793
- import { extname, join as join17 } from "path";
22640
+ import { extname, join as join19 } from "path";
21794
22641
  import { serveStatic } from "@hono/node-server/serve-static";
21795
22642
  var INDEX_HTML = "index.html";
21796
22643
  var PLACEHOLDER_HTML = `<!doctype html>
@@ -21842,8 +22689,8 @@ function createSpaFallback(opts) {
21842
22689
  return async (c, _next) => {
21843
22690
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
21844
22691
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
21845
- const indexPath = join17(opts.uiDist, INDEX_HTML);
21846
- if (!existsSync26(indexPath)) return htmlResponse(c, placeholder);
22692
+ const indexPath = join19(opts.uiDist, INDEX_HTML);
22693
+ if (!existsSync28(indexPath)) return htmlResponse(c, placeholder);
21847
22694
  return fileResponse(c, indexPath);
21848
22695
  };
21849
22696
  }
@@ -21929,13 +22776,13 @@ function attachBroadcasterRoute(app, broadcaster) {
21929
22776
 
21930
22777
  // server/app.ts
21931
22778
  var BODY_LIMIT_BYTES = 1024 * 1024;
21932
- var DbMissingError = class extends HTTPException14 {
22779
+ var DbMissingError = class extends HTTPException15 {
21933
22780
  constructor(message) {
21934
22781
  super(500, { message });
21935
22782
  this.name = "DbMissingError";
21936
22783
  }
21937
22784
  };
21938
- var BulkValidationError = class extends HTTPException14 {
22785
+ var BulkValidationError = class extends HTTPException15 {
21939
22786
  id;
21940
22787
  code;
21941
22788
  constructor(init) {
@@ -21945,7 +22792,7 @@ var BulkValidationError = class extends HTTPException14 {
21945
22792
  this.code = init.code;
21946
22793
  }
21947
22794
  };
21948
- var LoopbackGateError = class extends HTTPException14 {
22795
+ var LoopbackGateError = class extends HTTPException15 {
21949
22796
  code;
21950
22797
  constructor(init) {
21951
22798
  super(403, { message: init.message });
@@ -21965,7 +22812,7 @@ function createApp(deps) {
21965
22812
  bodyLimit({
21966
22813
  maxSize: BODY_LIMIT_BYTES,
21967
22814
  onError: () => {
21968
- throw new HTTPException14(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
22815
+ throw new HTTPException15(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
21969
22816
  }
21970
22817
  })
21971
22818
  );
@@ -22006,9 +22853,10 @@ function createApp(deps) {
22006
22853
  registerUpdateStatusRoute(app, routeDeps);
22007
22854
  registerPreferencesRoute(app, routeDeps);
22008
22855
  registerProjectPreferencesRoute(app, routeDeps);
22856
+ registerActiveProviderRoute(app, routeDeps);
22009
22857
  registerProjectIgnoreRoute(app, routeDeps);
22010
22858
  app.all("/api/*", (c) => {
22011
- throw new HTTPException14(404, {
22859
+ throw new HTTPException15(404, {
22012
22860
  message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
22013
22861
  });
22014
22862
  });
@@ -22016,7 +22864,7 @@ function createApp(deps) {
22016
22864
  app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
22017
22865
  app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
22018
22866
  app.notFound((c) => {
22019
- throw new HTTPException14(404, {
22867
+ throw new HTTPException15(404, {
22020
22868
  message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
22021
22869
  });
22022
22870
  });
@@ -22071,7 +22919,7 @@ function formatError2(err, c) {
22071
22919
  };
22072
22920
  return c.json(envelope, 403);
22073
22921
  }
22074
- if (err instanceof HTTPException14) {
22922
+ if (err instanceof HTTPException15) {
22075
22923
  const status = err.status;
22076
22924
  const envelope = {
22077
22925
  ok: false,
@@ -22414,10 +23262,10 @@ function validateNoUi(noUi, uiDist) {
22414
23262
  }
22415
23263
 
22416
23264
  // server/paths.ts
22417
- import { existsSync as existsSync27, statSync as statSync11 } from "fs";
22418
- import { dirname as dirname18, isAbsolute as isAbsolute9, join as join18, resolve as resolve35 } from "path";
23265
+ import { existsSync as existsSync29, statSync as statSync11 } from "fs";
23266
+ import { dirname as dirname18, isAbsolute as isAbsolute11, join as join20, resolve as resolve36 } from "path";
22419
23267
  import { fileURLToPath as fileURLToPath5 } from "url";
22420
- var DEFAULT_UI_REL = join18("ui", "dist", "ui", "browser");
23268
+ var DEFAULT_UI_REL = join20("ui", "dist", "ui", "browser");
22421
23269
  var PACKAGE_UI_REL = "ui";
22422
23270
  var INDEX_HTML2 = "index.html";
22423
23271
  function resolveDefaultUiDist(ctx) {
@@ -22426,13 +23274,13 @@ function resolveDefaultUiDist(ctx) {
22426
23274
  return walkUpForUi(ctx.cwd);
22427
23275
  }
22428
23276
  function resolveExplicitUiDist(ctx, raw) {
22429
- return isAbsolute9(raw) ? raw : resolve35(ctx.cwd, raw);
23277
+ return isAbsolute11(raw) ? raw : resolve36(ctx.cwd, raw);
22430
23278
  }
22431
23279
  function isUiBundleDir(path) {
22432
- if (!existsSync27(path)) return false;
23280
+ if (!existsSync29(path)) return false;
22433
23281
  try {
22434
23282
  if (!statSync11(path).isDirectory()) return false;
22435
- return existsSync27(join18(path, INDEX_HTML2));
23283
+ return existsSync29(join20(path, INDEX_HTML2));
22436
23284
  } catch {
22437
23285
  return false;
22438
23286
  }
@@ -22449,9 +23297,9 @@ function resolvePackageBundledUi() {
22449
23297
  function resolvePackageBundledUiFrom(here) {
22450
23298
  let current = here;
22451
23299
  for (let i = 0; i < 8; i++) {
22452
- const candidate = join18(current, PACKAGE_UI_REL);
23300
+ const candidate = join20(current, PACKAGE_UI_REL);
22453
23301
  if (isUiBundleDir(candidate)) return candidate;
22454
- const distHere = join18(current, "dist", PACKAGE_UI_REL);
23302
+ const distHere = join20(current, "dist", PACKAGE_UI_REL);
22455
23303
  if (isUiBundleDir(distHere)) return distHere;
22456
23304
  const parent = dirname18(current);
22457
23305
  if (parent === current) return null;
@@ -22460,9 +23308,9 @@ function resolvePackageBundledUiFrom(here) {
22460
23308
  return null;
22461
23309
  }
22462
23310
  function walkUpForUi(startDir) {
22463
- let current = resolve35(startDir);
23311
+ let current = resolve36(startDir);
22464
23312
  for (let i = 0; i < 64; i++) {
22465
- const candidate = join18(current, DEFAULT_UI_REL);
23313
+ const candidate = join20(current, DEFAULT_UI_REL);
22466
23314
  if (isUiBundleDir(candidate)) return candidate;
22467
23315
  const parent = dirname18(current);
22468
23316
  if (parent === current) return null;
@@ -22664,7 +23512,7 @@ var SERVE_TEXTS = {
22664
23512
  };
22665
23513
 
22666
23514
  // cli/util/serve-banner.ts
22667
- import { relative as relative7, isAbsolute as isAbsolute10 } from "path";
23515
+ import { relative as relative7, isAbsolute as isAbsolute12 } from "path";
22668
23516
  var ESC2 = {
22669
23517
  reset: "\x1B[0m",
22670
23518
  bold: "\x1B[1m",
@@ -22798,9 +23646,9 @@ function resolveAnsi(colorEnabled) {
22798
23646
  }
22799
23647
  function formatDbPath(dbPath, cwd) {
22800
23648
  const safe = sanitizeForTerminal(dbPath);
22801
- if (!isAbsolute10(safe)) return safe;
23649
+ if (!isAbsolute12(safe)) return safe;
22802
23650
  const rel = relative7(cwd, safe);
22803
- if (rel === "" || rel.startsWith("..") || isAbsolute10(rel)) {
23651
+ if (rel === "" || rel.startsWith("..") || isAbsolute12(rel)) {
22804
23652
  return safe;
22805
23653
  }
22806
23654
  return rel;
@@ -22896,7 +23744,7 @@ var ServeCommand = class extends SmCommand {
22896
23744
  return ExitCode.Error;
22897
23745
  }
22898
23746
  const dbPath = resolveDbPath({ db: this.db, ...runtimeCtx });
22899
- if (this.db !== void 0 && !existsSync28(dbPath)) {
23747
+ if (this.db !== void 0 && !existsSync30(dbPath)) {
22900
23748
  this.printer.info(
22901
23749
  tx(SERVE_TEXTS.dbNotFound, { path: sanitizeForTerminal(dbPath) })
22902
23750
  );
@@ -23307,22 +24155,27 @@ function renderLinksSection(direction, links, ansi) {
23307
24155
  const aggregated = aggregateLinks(links, projectField);
23308
24156
  const headerTpl = direction === "out" ? SHOW_TEXTS.linksOutSection : SHOW_TEXTS.linksInSection;
23309
24157
  const kindWidth = Math.max(...aggregated.map((g) => g.kind.length));
23310
- const confWidth = Math.max(...aggregated.map((g) => g.confidence.length));
24158
+ const confLabels = aggregated.map((g) => formatConfidence(g.confidence));
24159
+ const confWidth = Math.max(...confLabels.map((l) => l.length));
23311
24160
  const lines = [tx(headerTpl, { count: links.length })];
23312
- for (const grp of aggregated) {
24161
+ aggregated.forEach((grp, idx) => {
23313
24162
  const dup = grp.rowCount > 1 ? ansi.dim(tx(SHOW_TEXTS.linkDup, { count: grp.rowCount })) : "";
23314
24163
  lines.push(
23315
24164
  tx(SHOW_TEXTS.linkRow, {
23316
24165
  arrow: ansi.dim(arrow),
23317
24166
  kind: sanitizeForTerminal(grp.kind).padEnd(kindWidth),
23318
- confidence: ansi.dim(grp.confidence.padEnd(confWidth)),
24167
+ confidence: ansi.dim(confLabels[idx].padEnd(confWidth)),
23319
24168
  endpoint: sanitizeForTerminal(grp.endpoint),
23320
24169
  dup
23321
24170
  })
23322
24171
  );
23323
- }
24172
+ });
23324
24173
  return lines.join("");
23325
24174
  }
24175
+ function formatConfidence(c) {
24176
+ if (typeof c !== "number" || !Number.isFinite(c)) return "?";
24177
+ return `${Math.round(c * 100)}%`;
24178
+ }
23326
24179
  function renderIssuesSection(issues, nodePath, ansi) {
23327
24180
  const lines = [tx(SHOW_TEXTS.issuesSection, { count: issues.length })];
23328
24181
  const analyzerWidth = Math.max(
@@ -23389,18 +24242,13 @@ function aggregateLinks(links, endpointSide) {
23389
24242
  return a.kind.localeCompare(b.kind);
23390
24243
  });
23391
24244
  }
23392
- var CONFIDENCE_RANK = {
23393
- high: 2,
23394
- medium: 1,
23395
- low: 0
23396
- };
23397
24245
  function rankConfidenceForGrouping(c) {
23398
- return CONFIDENCE_RANK[c];
24246
+ return c;
23399
24247
  }
23400
24248
 
23401
24249
  // cli/commands/sidecar.ts
23402
- import { existsSync as existsSync29, unlinkSync as unlinkSync2 } from "fs";
23403
- import { resolve as resolve36 } from "path";
24250
+ import { existsSync as existsSync31, unlinkSync as unlinkSync2 } from "fs";
24251
+ import { resolve as resolve37 } from "path";
23404
24252
  import { Command as Command35, Option as Option33 } from "clipanion";
23405
24253
 
23406
24254
  // cli/i18n/sidecar.texts.ts
@@ -23551,7 +24399,7 @@ var SidecarRefreshCommand = class extends SmCommand {
23551
24399
  let absPath;
23552
24400
  try {
23553
24401
  assertContained(ctx.cwd, node.path);
23554
- absPath = resolve36(ctx.cwd, node.path);
24402
+ absPath = resolve37(ctx.cwd, node.path);
23555
24403
  } catch (err) {
23556
24404
  this.printer.error(
23557
24405
  tx(SIDECAR_TEXTS.refreshFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -23832,7 +24680,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23832
24680
  let absPath;
23833
24681
  try {
23834
24682
  assertContained(ctx.cwd, node.path);
23835
- absPath = resolve36(ctx.cwd, node.path);
24683
+ absPath = resolve37(ctx.cwd, node.path);
23836
24684
  } catch (err) {
23837
24685
  this.printer.error(
23838
24686
  tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -23840,7 +24688,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23840
24688
  return ExitCode.Error;
23841
24689
  }
23842
24690
  const sidecarAbsPath = sidecarPathFor(absPath);
23843
- if (existsSync29(sidecarAbsPath) && this.force !== true) {
24691
+ if (existsSync31(sidecarAbsPath) && this.force !== true) {
23844
24692
  this.printer.error(
23845
24693
  tx(SIDECAR_TEXTS.annotateExists, {
23846
24694
  glyph: errGlyph,
@@ -23850,7 +24698,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23850
24698
  );
23851
24699
  return ExitCode.Error;
23852
24700
  }
23853
- if (existsSync29(sidecarAbsPath) && this.force === true) {
24701
+ if (existsSync31(sidecarAbsPath) && this.force === true) {
23854
24702
  try {
23855
24703
  unlinkSync2(sidecarAbsPath);
23856
24704
  } catch (err) {
@@ -24080,8 +24928,8 @@ var STUB_COMMANDS = [
24080
24928
  ];
24081
24929
 
24082
24930
  // cli/commands/tutorial.ts
24083
- import { cpSync as cpSync2, existsSync as existsSync30, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync12 } from "fs";
24084
- import { dirname as dirname19, join as join19, resolve as resolve37 } from "path";
24931
+ import { cpSync as cpSync2, existsSync as existsSync32, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync12 } from "fs";
24932
+ import { dirname as dirname19, join as join21, resolve as resolve38 } from "path";
24085
24933
  import { fileURLToPath as fileURLToPath6 } from "url";
24086
24934
  import { Command as Command37, Option as Option35 } from "clipanion";
24087
24935
 
@@ -24177,9 +25025,9 @@ var TutorialCommand = class extends SmCommand {
24177
25025
  }
24178
25026
  const variant = rawVariant ?? DEFAULT_VARIANT;
24179
25027
  const spec = VARIANT_SPECS[variant];
24180
- const targetDir = join19(ctx.cwd, ".claude", "skills", spec.slug);
25028
+ const targetDir = join21(ctx.cwd, ".claude", "skills", spec.slug);
24181
25029
  const targetDisplay = `.claude/skills/${spec.slug}/`;
24182
- if (existsSync30(targetDir) && !this.force) {
25030
+ if (existsSync32(targetDir) && !this.force) {
24183
25031
  this.printer.error(
24184
25032
  tx(TUTORIAL_TEXTS.alreadyExists, {
24185
25033
  glyph: errGlyph,
@@ -24255,14 +25103,14 @@ function resolveSkillSourceDir(variant) {
24255
25103
  const here = dirname19(fileURLToPath6(import.meta.url));
24256
25104
  const candidates = [
24257
25105
  // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
24258
- resolve37(here, "../../..", spec.sourceDir),
25106
+ resolve38(here, "../../..", spec.sourceDir),
24259
25107
  // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
24260
- resolve37(here, "cli/tutorial", spec.slug),
25108
+ resolve38(here, "cli/tutorial", spec.slug),
24261
25109
  // bundled fallback: any-depth → cli/tutorial/<slug>
24262
- resolve37(here, "../cli/tutorial", spec.slug)
25110
+ resolve38(here, "../cli/tutorial", spec.slug)
24263
25111
  ];
24264
25112
  for (const candidate of candidates) {
24265
- if (existsSync30(candidate) && statSync12(candidate).isDirectory()) {
25113
+ if (existsSync32(candidate) && statSync12(candidate).isDirectory()) {
24266
25114
  cachedSourceDirs.set(variant, candidate);
24267
25115
  return candidate;
24268
25116
  }
@@ -24439,7 +25287,7 @@ await lifecycleDispatcher.dispatch(
24439
25287
  process.exit(exitCode);
24440
25288
  function resolveBareDefault() {
24441
25289
  const ctx = defaultRuntimeContext();
24442
- if (existsSync31(defaultProjectDbPath(ctx))) {
25290
+ if (existsSync33(defaultProjectDbPath(ctx))) {
24443
25291
  return ["serve"];
24444
25292
  }
24445
25293
  process.stderr.write(tx(ENTRY_TEXTS.bareNoProject, { cwd: ctx.cwd }));