@skill-map/cli 0.32.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // cli/entry.ts
2
- import { existsSync as 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
@@ -511,6 +536,166 @@ var claudeProvider = {
511
536
  }
512
537
  };
513
538
 
539
+ // kernel/util/strip-code-blocks.ts
540
+ var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
541
+ function stripCodeBlocks(input) {
542
+ if (!input) return input;
543
+ const fenceless = stripFences(input);
544
+ return stripInline(fenceless);
545
+ }
546
+ function stripFences(input) {
547
+ const out = [];
548
+ const lines = input.split("\n");
549
+ let openFence = null;
550
+ for (const line of lines) {
551
+ if (openFence) {
552
+ const closer = matchClosingFence(line, openFence);
553
+ if (closer) {
554
+ out.push(blank(line));
555
+ openFence = null;
556
+ } else {
557
+ out.push(blank(line));
558
+ }
559
+ continue;
560
+ }
561
+ const open = FENCE_RE.exec(line);
562
+ if (open?.groups) {
563
+ openFence = open.groups["fence"];
564
+ out.push(blank(line));
565
+ continue;
566
+ }
567
+ out.push(line);
568
+ }
569
+ return out.join("\n");
570
+ }
571
+ function matchClosingFence(line, openFence) {
572
+ const m = FENCE_RE.exec(line);
573
+ if (!m?.groups) return false;
574
+ const fence = m.groups["fence"];
575
+ return fence[0] === openFence[0] && fence.length >= openFence.length;
576
+ }
577
+ function stripInline(input) {
578
+ return input.replace(/(`+)([\s\S]*?)\1/g, (_full, ticks, body) => {
579
+ return ticks.replace(/`/g, " ") + blank(body) + ticks.replace(/`/g, " ");
580
+ });
581
+ }
582
+ function blank(s) {
583
+ return s.replace(/[^\s]/g, " ");
584
+ }
585
+
586
+ // kernel/trigger-normalize.ts
587
+ function normalizeTrigger(source) {
588
+ let out = source.normalize("NFD");
589
+ out = out.replace(new RegExp("\\p{Mn}+", "gu"), "");
590
+ out = out.toLowerCase();
591
+ out = out.replace(/[-_\s]+/g, " ");
592
+ out = out.replace(/ +/g, " ");
593
+ return out.trim();
594
+ }
595
+
596
+ // plugins/claude/extractors/at-directive/index.ts
597
+ var ID = "at-directive";
598
+ var AT_RE = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
599
+ var FILE_EXT_RE = /\.(md|mdx|js|jsx|ts|tsx|json|yml|yaml|toml|txt|html|css|scss|less|py|rb|go|rs|java|c|cpp|h|hpp|sh|sql|svg|png|jpg|jpeg|gif|webp|pdf)$/i;
600
+ var atDirectiveExtractor = {
601
+ id: ID,
602
+ pluginId: "claude",
603
+ kind: "extractor",
604
+ version: "1.0.0",
605
+ description: "Detects `@<token>` directives in a node's body using Claude Code interpretation rules. A bare handle (e.g. `@team`) becomes a `mentions` link; a file-flavoured token (e.g. `@docs/api.md`, `@./readme.md`) becomes a `references` link. Gated by `precondition.provider: ['claude']` so Gemini / Cursor / Codex apply their own at-directive flavours via their own extractors.",
606
+ scope: "body",
607
+ precondition: { provider: ["claude"] },
608
+ extract(ctx) {
609
+ const seenMentions = /* @__PURE__ */ new Set();
610
+ const seenReferences = /* @__PURE__ */ new Set();
611
+ const body = stripCodeBlocks(ctx.body);
612
+ for (const match of body.matchAll(AT_RE)) {
613
+ const original = match[1];
614
+ const bare = original.slice(1);
615
+ const isReference = bare.startsWith("./") || bare.startsWith("../") || bare.startsWith("/") || FILE_EXT_RE.test(bare);
616
+ if (isReference) {
617
+ const target = bare.replace(/^\.\//, "");
618
+ if (seenReferences.has(target)) continue;
619
+ seenReferences.add(target);
620
+ ctx.emitLink({
621
+ source: ctx.node.path,
622
+ target,
623
+ kind: "references",
624
+ // 0.85: strong file signal (path prefix `./` / `../` / `/` OR
625
+ // a known file extension on the tail). One degree of inference
626
+ // (the runtime still resolves the path).
627
+ confidence: 0.85,
628
+ sources: [ID],
629
+ trigger: {
630
+ originalTrigger: original,
631
+ normalizedTrigger: target.toLowerCase()
632
+ }
633
+ });
634
+ continue;
635
+ }
636
+ const normalized = normalizeTrigger(original);
637
+ if (seenMentions.has(normalized)) continue;
638
+ seenMentions.add(normalized);
639
+ ctx.emitLink({
640
+ source: ctx.node.path,
641
+ target: original,
642
+ kind: "mentions",
643
+ // 0.5: genuine ambiguity. A bare `@handle` (no extension, no
644
+ // path prefix) could be an agent, a handle, or generic prose.
645
+ // The runtime decides at invocation time; the extractor leaves
646
+ // the question open.
647
+ confidence: 0.5,
648
+ sources: [ID],
649
+ trigger: {
650
+ originalTrigger: original,
651
+ normalizedTrigger: normalized
652
+ }
653
+ });
654
+ }
655
+ }
656
+ };
657
+
658
+ // plugins/claude/extractors/slash/index.ts
659
+ var ID2 = "slash";
660
+ var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
661
+ var slashExtractor = {
662
+ id: ID2,
663
+ pluginId: "claude",
664
+ kind: "extractor",
665
+ version: "1.0.0",
666
+ description: "Detects `/command` invocations in a node's body using Claude Code routing rules and turns each one into an arrow between nodes in the graph. Gated by `precondition.provider: ['claude']` so Gemini / Cursor / Codex apply their own slash flavours (Gemini has 4 routing separators, Codex deprecated user slash commands, etc.) via their own extractors.",
667
+ scope: "body",
668
+ precondition: { provider: ["claude"] },
669
+ extract(ctx) {
670
+ const seen = /* @__PURE__ */ new Set();
671
+ const body = stripCodeBlocks(ctx.body);
672
+ for (const match of body.matchAll(SLASH_RE)) {
673
+ const original = match[1];
674
+ const endIdx = (match.index ?? 0) + match[0].length;
675
+ const nextChar = body[endIdx];
676
+ if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
677
+ const normalized = normalizeTrigger(original);
678
+ if (seen.has(normalized)) continue;
679
+ seen.add(normalized);
680
+ ctx.emitLink({
681
+ source: ctx.node.path,
682
+ target: original,
683
+ kind: "invokes",
684
+ // 0.8: clean `/command` match after code-block strip. The
685
+ // post-match path guard above filters URL / file-path noise,
686
+ // so a hit is unambiguous syntax. Resolution against the live
687
+ // skill / command catalog happens downstream.
688
+ confidence: 0.8,
689
+ sources: [ID2],
690
+ trigger: {
691
+ originalTrigger: original,
692
+ normalizedTrigger: normalized
693
+ }
694
+ });
695
+ }
696
+ }
697
+ };
698
+
514
699
  // plugins/gemini/providers/gemini/schemas/agent.schema.json
515
700
  var agent_schema_default2 = {
516
701
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -629,6 +814,86 @@ var geminiProvider = {
629
814
  }
630
815
  };
631
816
 
817
+ // plugins/openai/providers/openai/schemas/agent.schema.json
818
+ var agent_schema_default3 = {
819
+ $schema: "https://json-schema.org/draft/2020-12/schema",
820
+ $id: "https://skill-map.dev/providers/openai/v1/frontmatter/agent.schema.json",
821
+ title: "FrontmatterCodexAgent",
822
+ description: "Frontmatter shape for nodes classified as `agent` by the OpenAI Codex Provider. Codex sub-agents live as TOML files under `.codex/agents/<name>.toml`; the entire file IS the agent definition (no markdown body). The TOML parser feeds the parsed root object into `frontmatter`, so this schema validates the same shape skill-map's other providers carry on per-kind frontmatter. Mirrors Codex's documented sub-agent fields (https://github.com/openai/codex) with `additionalProperties: true` so future additions flow through unchanged.",
823
+ allOf: [
824
+ { $ref: "https://skill-map.dev/spec/v0/frontmatter/base.schema.json" }
825
+ ],
826
+ type: "object",
827
+ additionalProperties: true,
828
+ properties: {
829
+ name: {
830
+ type: "string",
831
+ minLength: 1,
832
+ description: "Sub-agent identifier. Conventionally matches the filename stem."
833
+ },
834
+ description: {
835
+ type: "string",
836
+ description: "Short description of when this sub-agent applies. Codex surfaces this in the agent picker; skill-map mirrors it in the card."
837
+ },
838
+ model: {
839
+ type: "string",
840
+ description: "Model identifier (`gpt-4o`, `o3-mini`, etc.) the sub-agent runs against."
841
+ },
842
+ instructions: {
843
+ type: "string",
844
+ description: "Multi-line prompt body (TOML triple-quoted string)."
845
+ },
846
+ tools: {
847
+ type: "array",
848
+ items: { type: "string" },
849
+ description: "Tool ids this sub-agent is allowed to call."
850
+ },
851
+ mcp_servers: {
852
+ type: "array",
853
+ items: { type: "string" },
854
+ description: "MCP server ids attached to this sub-agent (`tools: [mcp__<server>__*]` follows the same pattern as Claude)."
855
+ },
856
+ approval_policy: {
857
+ type: "string",
858
+ enum: ["never", "on-request", "untrusted"],
859
+ description: "Codex approval policy for this sub-agent's destructive operations."
860
+ },
861
+ sandbox_mode: {
862
+ type: "string",
863
+ enum: ["read-only", "workspace-write", "danger-full-access"],
864
+ description: "Codex sandbox mode the sub-agent runs under."
865
+ }
866
+ }
867
+ };
868
+
869
+ // plugins/openai/providers/openai/index.ts
870
+ var openaiProvider = {
871
+ id: "openai",
872
+ pluginId: "openai",
873
+ kind: "provider",
874
+ version: "1.0.0",
875
+ description: "Walks OpenAI Codex CLI scope conventions (.codex/agents/*.toml).",
876
+ read: { extensions: [".toml"], parser: "toml" },
877
+ kinds: {
878
+ agent: {
879
+ schema: "./schemas/agent.schema.json",
880
+ schemaJson: agent_schema_default3,
881
+ ui: {
882
+ label: "Codex agents",
883
+ // Codex green; distinct from claude / gemini palettes.
884
+ color: "#22c55e",
885
+ colorDark: "#4ade80",
886
+ icon: { kind: "pi", id: "pi-bolt" }
887
+ }
888
+ }
889
+ },
890
+ classify(path) {
891
+ const lower = path.toLowerCase();
892
+ if (lower.startsWith(".codex/agents/") && lower.endsWith(".toml")) return "agent";
893
+ return null;
894
+ }
895
+ };
896
+
632
897
  // plugins/agent-skills/providers/agent-skills/schemas/skill.schema.json
633
898
  var skill_schema_default3 = {
634
899
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -722,9 +987,9 @@ var coreMarkdownProvider = {
722
987
  };
723
988
 
724
989
  // plugins/core/extractors/annotations/index.ts
725
- var ID = "annotations";
990
+ var ID3 = "annotations";
726
991
  var annotationsExtractor = {
727
- id: ID,
992
+ id: ID3,
728
993
  pluginId: "core",
729
994
  kind: "extractor",
730
995
  version: "1.0.0",
@@ -770,128 +1035,17 @@ function link(source, target) {
770
1035
  source,
771
1036
  target,
772
1037
  kind: "supersedes",
773
- confidence: "high",
774
- sources: [ID]
1038
+ confidence: 1,
1039
+ sources: [ID3]
775
1040
  };
776
1041
  }
777
1042
 
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
1043
  // plugins/core/extractors/external-url-counter/index.ts
890
- var ID3 = "external-url-counter";
1044
+ var ID4 = "external-url-counter";
891
1045
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
892
1046
  var TRAILING_PUNCT = /[.,;:!?]+$/;
893
1047
  var externalUrlCounterExtractor = {
894
- id: ID3,
1048
+ id: ID4,
895
1049
  pluginId: "core",
896
1050
  kind: "extractor",
897
1051
  version: "1.0.0",
@@ -937,8 +1091,8 @@ var externalUrlCounterExtractor = {
937
1091
  source: ctx.node.path,
938
1092
  target: normalized,
939
1093
  kind: "references",
940
- confidence: "low",
941
- sources: [ID3],
1094
+ confidence: 0.3,
1095
+ sources: [ID4],
942
1096
  trigger: {
943
1097
  originalTrigger: original,
944
1098
  normalizedTrigger: normalized
@@ -985,11 +1139,11 @@ function lineFor(lineStarts, offset) {
985
1139
 
986
1140
  // plugins/core/extractors/markdown-link/index.ts
987
1141
  import { posix as pathPosix } from "path";
988
- var ID4 = "markdown-link";
1142
+ var ID5 = "markdown-link";
989
1143
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
990
1144
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
991
1145
  var markdownLinkExtractor = {
992
- id: ID4,
1146
+ id: ID5,
993
1147
  pluginId: "core",
994
1148
  kind: "extractor",
995
1149
  version: "1.0.0",
@@ -1010,8 +1164,8 @@ var markdownLinkExtractor = {
1010
1164
  source: ctx.node.path,
1011
1165
  target: resolved,
1012
1166
  kind: "references",
1013
- confidence: "high",
1014
- sources: [ID4],
1167
+ confidence: 0.95,
1168
+ sources: [ID5],
1015
1169
  trigger: {
1016
1170
  originalTrigger: original,
1017
1171
  normalizedTrigger: resolved
@@ -1050,47 +1204,61 @@ function lineFor2(lineStarts, offset) {
1050
1204
  return lo + 1;
1051
1205
  }
1052
1206
 
1053
- // plugins/core/extractors/slash/index.ts
1054
- var ID5 = "slash";
1055
- var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
1056
- var slashExtractor = {
1057
- id: ID5,
1207
+ // plugins/core/extractors/mcp-tools/index.ts
1208
+ var ID6 = "mcp-tools";
1209
+ var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
1210
+ var mcpToolsExtractor = {
1211
+ id: ID6,
1058
1212
  pluginId: "core",
1059
1213
  kind: "extractor",
1060
1214
  version: "1.0.0",
1061
- description: "Detects `/command` invocations in a node's body and turns each one into an arrow between nodes in the graph.",
1062
- scope: "body",
1215
+ description: "Detects `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter and turns each unique server into an MCP node + a reference edge from the source.",
1216
+ scope: "frontmatter",
1063
1217
  extract(ctx) {
1064
- const seen = /* @__PURE__ */ new Set();
1065
- const body = stripCodeBlocks(ctx.body);
1066
- for (const match of body.matchAll(SLASH_RE)) {
1067
- const original = match[1];
1068
- const endIdx = (match.index ?? 0) + match[0].length;
1069
- const nextChar = body[endIdx];
1070
- if (nextChar && /[A-Za-z0-9_/-]/.test(nextChar)) continue;
1071
- const normalized = normalizeTrigger(original);
1072
- if (seen.has(normalized)) continue;
1073
- seen.add(normalized);
1218
+ const raw = ctx.frontmatter["tools"];
1219
+ if (!Array.isArray(raw)) return;
1220
+ const servers = collectMcpServers(raw);
1221
+ if (servers.size === 0) return;
1222
+ for (const server of servers) {
1223
+ const mcpPath = `mcp://${server}`;
1224
+ ctx.emitNode({
1225
+ path: mcpPath,
1226
+ kind: "mcp",
1227
+ virtual: true,
1228
+ provider: ctx.node.provider,
1229
+ derivedFrom: [ctx.node.path],
1230
+ frontmatter: { name: server }
1231
+ });
1074
1232
  ctx.emitLink({
1075
1233
  source: ctx.node.path,
1076
- target: original,
1077
- kind: "invokes",
1078
- confidence: "medium",
1079
- sources: [ID5],
1234
+ target: mcpPath,
1235
+ kind: "references",
1236
+ confidence: 0.85,
1237
+ sources: [ID6],
1080
1238
  trigger: {
1081
- originalTrigger: original,
1082
- normalizedTrigger: normalized
1239
+ originalTrigger: `mcp__${server}__*`,
1240
+ normalizedTrigger: mcpPath
1083
1241
  }
1084
1242
  });
1085
1243
  }
1086
1244
  }
1087
1245
  };
1246
+ function collectMcpServers(tools) {
1247
+ const out = /* @__PURE__ */ new Set();
1248
+ for (const t of tools) {
1249
+ if (typeof t !== "string" || t.length === 0) continue;
1250
+ const match = MCP_PATTERN.exec(t);
1251
+ if (!match) continue;
1252
+ out.add(match[1].toLowerCase());
1253
+ }
1254
+ return out;
1255
+ }
1088
1256
 
1089
1257
  // plugins/core/extractors/tools-count/index.ts
1090
- var ID6 = "tools-count";
1258
+ var ID7 = "tools-count";
1091
1259
  var TOOLTIP_MAX = 255;
1092
1260
  var toolsCountExtractor = {
1093
- id: ID6,
1261
+ id: ID7,
1094
1262
  pluginId: "core",
1095
1263
  kind: "extractor",
1096
1264
  version: "1.0.0",
@@ -1133,9 +1301,9 @@ var ANNOTATION_ORPHAN_TEXTS = {
1133
1301
  };
1134
1302
 
1135
1303
  // plugins/core/analyzers/annotation-orphan/index.ts
1136
- var ID7 = "annotation-orphan";
1304
+ var ID8 = "annotation-orphan";
1137
1305
  var annotationOrphanAnalyzer = {
1138
- id: ID7,
1306
+ id: ID8,
1139
1307
  pluginId: "core",
1140
1308
  kind: "analyzer",
1141
1309
  version: "1.0.0",
@@ -1148,7 +1316,7 @@ var annotationOrphanAnalyzer = {
1148
1316
  for (const orphan of orphans) {
1149
1317
  const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
1150
1318
  issues.push({
1151
- analyzerId: ID7,
1319
+ analyzerId: ID8,
1152
1320
  severity: "warn",
1153
1321
  nodeIds: [expectedMdRelative],
1154
1322
  message: tx(ANNOTATION_ORPHAN_TEXTS.message, {
@@ -1185,9 +1353,9 @@ var ANNOTATION_STALE_TEXTS = {
1185
1353
  };
1186
1354
 
1187
1355
  // plugins/core/analyzers/annotation-stale/index.ts
1188
- var ID8 = "annotation-stale";
1356
+ var ID9 = "annotation-stale";
1189
1357
  var annotationStaleAnalyzer = {
1190
- id: ID8,
1358
+ id: ID9,
1191
1359
  pluginId: "core",
1192
1360
  kind: "analyzer",
1193
1361
  version: "1.0.0",
@@ -1222,7 +1390,7 @@ var annotationStaleAnalyzer = {
1222
1390
  if (status === "fresh") continue;
1223
1391
  const message = status === "stale-body" ? tx(ANNOTATION_STALE_TEXTS.bodyDrift, { path: node.path }) : status === "stale-frontmatter" ? tx(ANNOTATION_STALE_TEXTS.frontmatterDrift, { path: node.path }) : tx(ANNOTATION_STALE_TEXTS.bothDrift, { path: node.path });
1224
1392
  issues.push({
1225
- analyzerId: ID8,
1393
+ analyzerId: ID9,
1226
1394
  severity: "warn",
1227
1395
  nodeIds: [node.path],
1228
1396
  message,
@@ -1268,9 +1436,9 @@ var BROKEN_REF_TEXTS = {
1268
1436
  };
1269
1437
 
1270
1438
  // plugins/core/analyzers/broken-ref/index.ts
1271
- var ID9 = "broken-ref";
1439
+ var ID10 = "broken-ref";
1272
1440
  var brokenRefAnalyzer = {
1273
- id: ID9,
1441
+ id: ID10,
1274
1442
  pluginId: "core",
1275
1443
  kind: "analyzer",
1276
1444
  version: "1.0.0",
@@ -1339,7 +1507,7 @@ function buildIssue(link2, hintCandidates = []) {
1339
1507
  trigger: link2.trigger?.normalizedTrigger ?? null
1340
1508
  };
1341
1509
  const issue = {
1342
- analyzerId: ID9,
1510
+ analyzerId: ID10,
1343
1511
  severity: "warn",
1344
1512
  nodeIds: [link2.source],
1345
1513
  message: tx(BROKEN_REF_TEXTS.message, {
@@ -1433,9 +1601,9 @@ function isPathStyleLink(link2) {
1433
1601
  }
1434
1602
 
1435
1603
  // plugins/core/analyzers/contribution-orphan/index.ts
1436
- var ID10 = "contribution-orphan";
1604
+ var ID11 = "contribution-orphan";
1437
1605
  var contributionOrphanAnalyzer = {
1438
- id: ID10,
1606
+ id: ID11,
1439
1607
  pluginId: "core",
1440
1608
  kind: "analyzer",
1441
1609
  version: "0.0.0",
@@ -1456,9 +1624,9 @@ var JOB_ORPHAN_FILE_TEXTS = {
1456
1624
  };
1457
1625
 
1458
1626
  // plugins/core/analyzers/job-orphan-file/index.ts
1459
- var ID11 = "job-orphan-file";
1627
+ var ID12 = "job-orphan-file";
1460
1628
  var jobOrphanFileAnalyzer = {
1461
- id: ID11,
1629
+ id: ID12,
1462
1630
  pluginId: "core",
1463
1631
  kind: "analyzer",
1464
1632
  version: "1.0.0",
@@ -1470,7 +1638,7 @@ var jobOrphanFileAnalyzer = {
1470
1638
  const issues = [];
1471
1639
  for (const filePath of orphans) {
1472
1640
  issues.push({
1473
- analyzerId: ID11,
1641
+ analyzerId: ID12,
1474
1642
  severity: "warn",
1475
1643
  nodeIds: [filePath],
1476
1644
  message: tx(JOB_ORPHAN_FILE_TEXTS.message, { filePath }),
@@ -1488,9 +1656,9 @@ var LINK_CONFLICT_TEXTS = {
1488
1656
  };
1489
1657
 
1490
1658
  // plugins/core/analyzers/link-conflict/index.ts
1491
- var ID12 = "link-conflict";
1659
+ var ID13 = "link-conflict";
1492
1660
  var linkConflictAnalyzer = {
1493
- id: ID12,
1661
+ id: ID13,
1494
1662
  pluginId: "core",
1495
1663
  kind: "analyzer",
1496
1664
  version: "1.0.0",
@@ -1538,7 +1706,7 @@ var linkConflictAnalyzer = {
1538
1706
  const [source, target] = key.split("\0");
1539
1707
  const kindList = variants.map((v) => v.kind).join(" / ");
1540
1708
  issues.push({
1541
- analyzerId: ID12,
1709
+ analyzerId: ID13,
1542
1710
  severity: "warn",
1543
1711
  nodeIds: [source, target],
1544
1712
  message: tx(LINK_CONFLICT_TEXTS.message, {
@@ -1553,14 +1721,7 @@ var linkConflictAnalyzer = {
1553
1721
  }
1554
1722
  };
1555
1723
  function rankConfidence(c) {
1556
- switch (c) {
1557
- case "high":
1558
- return 2;
1559
- case "medium":
1560
- return 1;
1561
- case "low":
1562
- return 0;
1563
- }
1724
+ return c;
1564
1725
  }
1565
1726
 
1566
1727
  // kernel/util/trigger-resolve.ts
@@ -1612,9 +1773,9 @@ function resolveLinkTargetToPath(link2, nameIndex) {
1612
1773
  }
1613
1774
 
1614
1775
  // plugins/core/analyzers/link-counts/index.ts
1615
- var ID13 = "link-counts";
1776
+ var ID14 = "link-counts";
1616
1777
  var linkCountsAnalyzer = {
1617
- id: ID13,
1778
+ id: ID14,
1618
1779
  pluginId: "core",
1619
1780
  kind: "analyzer",
1620
1781
  version: "1.0.0",
@@ -1678,11 +1839,11 @@ function formatBreakdown(byKind, direction) {
1678
1839
  }
1679
1840
 
1680
1841
  // plugins/core/analyzers/stability/index.ts
1681
- var ID14 = "stability";
1842
+ var ID15 = "stability";
1682
1843
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
1683
1844
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
1684
1845
  var stabilityAnalyzer = {
1685
- id: ID14,
1846
+ id: ID15,
1686
1847
  pluginId: "core",
1687
1848
  kind: "analyzer",
1688
1849
  version: "1.0.0",
@@ -1714,7 +1875,7 @@ var stabilityAnalyzer = {
1714
1875
  tooltip: EXPERIMENTAL_TOOLTIP
1715
1876
  });
1716
1877
  issues.push({
1717
- analyzerId: ID14,
1878
+ analyzerId: ID15,
1718
1879
  severity: "info",
1719
1880
  nodeIds: [node.path],
1720
1881
  message: `Node '${node.path}' is marked experimental: API may change.`,
@@ -1727,7 +1888,7 @@ var stabilityAnalyzer = {
1727
1888
  severity: "warn"
1728
1889
  });
1729
1890
  issues.push({
1730
- analyzerId: ID14,
1891
+ analyzerId: ID15,
1731
1892
  severity: "warn",
1732
1893
  nodeIds: [node.path],
1733
1894
  message: `Node '${node.path}' is marked deprecated: avoid in new code.`,
@@ -1761,9 +1922,9 @@ var SUPERSEDED_TEXTS = {
1761
1922
  };
1762
1923
 
1763
1924
  // plugins/core/analyzers/superseded/index.ts
1764
- var ID15 = "superseded";
1925
+ var ID16 = "superseded";
1765
1926
  var supersededAnalyzer = {
1766
- id: ID15,
1927
+ id: ID16,
1767
1928
  pluginId: "core",
1768
1929
  kind: "analyzer",
1769
1930
  version: "1.0.0",
@@ -1775,7 +1936,7 @@ var supersededAnalyzer = {
1775
1936
  const supersededBy = pickSupersededBy(node);
1776
1937
  if (supersededBy === null) continue;
1777
1938
  issues.push({
1778
- analyzerId: ID15,
1939
+ analyzerId: ID16,
1779
1940
  severity: "info",
1780
1941
  nodeIds: [node.path],
1781
1942
  message: tx(SUPERSEDED_TEXTS.message, {
@@ -1824,14 +1985,14 @@ var TRIGGER_COLLISION_TEXTS = {
1824
1985
  };
1825
1986
 
1826
1987
  // plugins/core/analyzers/trigger-collision/index.ts
1827
- var ID16 = "trigger-collision";
1988
+ var ID17 = "trigger-collision";
1828
1989
  var ADVERTISING_KINDS = /* @__PURE__ */ new Set([
1829
1990
  "command",
1830
1991
  "skill",
1831
1992
  "agent"
1832
1993
  ]);
1833
1994
  var triggerCollisionAnalyzer = {
1834
- id: ID16,
1995
+ id: ID17,
1835
1996
  pluginId: "core",
1836
1997
  kind: "analyzer",
1837
1998
  mode: "deterministic",
@@ -1930,7 +2091,7 @@ function analyzeTriggerBucket(normalized, claims) {
1930
2091
  part: parts[0]
1931
2092
  });
1932
2093
  return {
1933
- analyzerId: ID16,
2094
+ analyzerId: ID17,
1934
2095
  severity: "error",
1935
2096
  nodeIds,
1936
2097
  message,
@@ -1970,10 +2131,10 @@ var UNKNOWN_FIELD_TEXTS = {
1970
2131
  };
1971
2132
 
1972
2133
  // plugins/core/analyzers/unknown-field/index.ts
1973
- var ID17 = "unknown-field";
2134
+ var ID18 = "unknown-field";
1974
2135
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1975
2136
  var unknownFieldAnalyzer = {
1976
- id: ID17,
2137
+ id: ID18,
1977
2138
  pluginId: "core",
1978
2139
  kind: "analyzer",
1979
2140
  version: "1.0.0",
@@ -2031,7 +2192,7 @@ var unknownFieldAnalyzer = {
2031
2192
  for (const key of Object.keys(annotations)) {
2032
2193
  if (!knownAnnotationKeys.has(key)) {
2033
2194
  issues.push({
2034
- analyzerId: ID17,
2195
+ analyzerId: ID18,
2035
2196
  severity: "warn",
2036
2197
  nodeIds: [node.path],
2037
2198
  message: tx(UNKNOWN_FIELD_TEXTS.unknownAnnotationKey, {
@@ -2058,7 +2219,7 @@ var unknownFieldAnalyzer = {
2058
2219
  if (validator(value)) continue;
2059
2220
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
2060
2221
  issues.push({
2061
- analyzerId: ID17,
2222
+ analyzerId: ID18,
2062
2223
  severity: "warn",
2063
2224
  nodeIds: [node.path],
2064
2225
  message: tx(UNKNOWN_FIELD_TEXTS.pluginNamespaceInvalid, {
@@ -2074,7 +2235,7 @@ var unknownFieldAnalyzer = {
2074
2235
  continue;
2075
2236
  }
2076
2237
  issues.push({
2077
- analyzerId: ID17,
2238
+ analyzerId: ID18,
2078
2239
  severity: "warn",
2079
2240
  nodeIds: [node.path],
2080
2241
  message: tx(UNKNOWN_FIELD_TEXTS.unknownRootKey, {
@@ -2367,9 +2528,9 @@ var VALIDATE_ALL_TEXTS = {
2367
2528
  };
2368
2529
 
2369
2530
  // plugins/core/analyzers/validate-all/index.ts
2370
- var ID18 = "validate-all";
2531
+ var ID19 = "validate-all";
2371
2532
  var validateAllAnalyzer = {
2372
- id: ID18,
2533
+ id: ID19,
2373
2534
  pluginId: "core",
2374
2535
  kind: "analyzer",
2375
2536
  version: "1.0.0",
@@ -2432,7 +2593,7 @@ function collectNodeFindings(v, node, out) {
2432
2593
  const result = v.validate("node", toNodeForSchema(node));
2433
2594
  if (result.ok) return;
2434
2595
  out.push({
2435
- analyzerId: ID18,
2596
+ analyzerId: ID19,
2436
2597
  severity: "error",
2437
2598
  nodeIds: [node.path],
2438
2599
  message: tx(VALIDATE_ALL_TEXTS.nodeFailure, {
@@ -2451,7 +2612,7 @@ function collectFrontmatterBaseFindings(node, out) {
2451
2612
  if (isMissingStringField(fm, "description")) missing.push("description");
2452
2613
  if (missing.length === 0) return;
2453
2614
  out.push({
2454
- analyzerId: ID18,
2615
+ analyzerId: ID19,
2455
2616
  // `warn` (not `error`) so the default `sm scan` exit code stays
2456
2617
  // 0 even when nodes are missing frontmatter base fields. Strict
2457
2618
  // mode (`sm scan --strict`) still escalates to exit 1. Matches
@@ -2473,7 +2634,7 @@ function collectLinkFindings(v, link2, out) {
2473
2634
  const result = v.validate("link", toLinkForSchema(link2));
2474
2635
  if (result.ok) return;
2475
2636
  out.push({
2476
- analyzerId: ID18,
2637
+ analyzerId: ID19,
2477
2638
  severity: "error",
2478
2639
  nodeIds: [link2.source],
2479
2640
  message: tx(VALIDATE_ALL_TEXTS.linkFailure, {
@@ -2541,13 +2702,13 @@ var ASCII_FORMATTER_TEXTS = {
2541
2702
  };
2542
2703
 
2543
2704
  // plugins/core/formatters/ascii/index.ts
2544
- var ID19 = "ascii";
2705
+ var ID20 = "ascii";
2545
2706
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
2546
2707
  var asciiFormatter = {
2547
- id: ID19,
2708
+ id: ID20,
2548
2709
  pluginId: "core",
2549
2710
  kind: "formatter",
2550
- formatId: ID19,
2711
+ formatId: ID20,
2551
2712
  version: "1.0.0",
2552
2713
  description: "Renders the scan as plain text, grouped by kind, arrows, and issues. Used by `sm scan --format=ascii`.",
2553
2714
  // ASCII tree formatter, header + per-kind sections + per-issue
@@ -2642,14 +2803,14 @@ function renderSection(out, kind, group) {
2642
2803
  }
2643
2804
 
2644
2805
  // plugins/core/formatters/json/index.ts
2645
- var ID20 = "json";
2806
+ var ID21 = "json";
2646
2807
  var jsonFormatter = {
2647
- id: ID20,
2808
+ id: ID21,
2648
2809
  pluginId: "core",
2649
2810
  kind: "formatter",
2650
2811
  version: "1.0.0",
2651
2812
  description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json` when the full ScanResult is available). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
2652
- formatId: ID20,
2813
+ formatId: ID21,
2653
2814
  format(ctx) {
2654
2815
  if (ctx.scanResult !== void 0) {
2655
2816
  return JSON.stringify(ctx.scanResult);
@@ -2788,10 +2949,10 @@ function resolveSpecRoot2() {
2788
2949
  }
2789
2950
 
2790
2951
  // plugins/core/actions/bump/index.ts
2791
- var ID21 = "bump";
2952
+ var ID22 = "bump";
2792
2953
  var PLUGIN_ID = "core";
2793
2954
  var bumpAction = {
2794
- id: ID21,
2955
+ id: ID22,
2795
2956
  pluginId: PLUGIN_ID,
2796
2957
  kind: "action",
2797
2958
  version: "1.0.0",
@@ -2850,10 +3011,10 @@ function pickCurrentVersion(overlay) {
2850
3011
  }
2851
3012
 
2852
3013
  // plugins/core/actions/mark-superseded/index.ts
2853
- var ID22 = "mark-superseded";
3014
+ var ID23 = "mark-superseded";
2854
3015
  var PLUGIN_ID2 = "core";
2855
3016
  var markSupersededAction = {
2856
- id: ID22,
3017
+ id: ID23,
2857
3018
  pluginId: PLUGIN_ID2,
2858
3019
  kind: "action",
2859
3020
  version: "0.0.0",
@@ -2963,7 +3124,7 @@ var UPDATE_CHECK_TEXTS = {
2963
3124
  // package.json
2964
3125
  var package_default = {
2965
3126
  name: "@skill-map/cli",
2966
- version: "0.32.0",
3127
+ version: "0.33.0",
2967
3128
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
2968
3129
  license: "MIT",
2969
3130
  type: "module",
@@ -3044,6 +3205,7 @@ var package_default = {
3044
3205
  "js-yaml": "4.1.1",
3045
3206
  kysely: "0.28.17",
3046
3207
  semver: "7.7.4",
3208
+ "smol-toml": "1.6.1",
3047
3209
  typanion: "3.14.0",
3048
3210
  ws: "8.20.0"
3049
3211
  },
@@ -3339,14 +3501,16 @@ var updateCheckHook = {
3339
3501
 
3340
3502
  // plugins/built-ins.ts
3341
3503
  var claudeProvider2 = { ...claudeProvider, pluginId: "claude" };
3504
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude" };
3505
+ var slashExtractor2 = { ...slashExtractor, pluginId: "claude" };
3342
3506
  var geminiProvider2 = { ...geminiProvider, pluginId: "gemini" };
3507
+ var openaiProvider2 = { ...openaiProvider, pluginId: "openai" };
3343
3508
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills" };
3344
3509
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core" };
3345
3510
  var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core" };
3346
- var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "core" };
3347
3511
  var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core" };
3348
3512
  var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core" };
3349
- var slashExtractor2 = { ...slashExtractor, pluginId: "core" };
3513
+ var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core" };
3350
3514
  var toolsCountExtractor2 = { ...toolsCountExtractor, pluginId: "core" };
3351
3515
  var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core" };
3352
3516
  var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core" };
@@ -3371,7 +3535,9 @@ var builtInBundles = [
3371
3535
  granularity: "bundle",
3372
3536
  description: "Claude Code platform integration. Classifies files under `.claude/{agents,commands,skills}` and parses Claude-flavored frontmatter.",
3373
3537
  extensions: [
3374
- claudeProvider2
3538
+ claudeProvider2,
3539
+ atDirectiveExtractor2,
3540
+ slashExtractor2
3375
3541
  ]
3376
3542
  },
3377
3543
  {
@@ -3382,6 +3548,14 @@ var builtInBundles = [
3382
3548
  geminiProvider2
3383
3549
  ]
3384
3550
  },
3551
+ {
3552
+ id: "openai",
3553
+ granularity: "bundle",
3554
+ description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml` and (future) walks the hierarchical AGENTS.md cascade. Provider for the active-lens `openai` runtime.",
3555
+ extensions: [
3556
+ openaiProvider2
3557
+ ]
3558
+ },
3385
3559
  {
3386
3560
  id: "agent-skills",
3387
3561
  granularity: "bundle",
@@ -3397,10 +3571,9 @@ var builtInBundles = [
3397
3571
  extensions: [
3398
3572
  coreMarkdownProvider2,
3399
3573
  annotationsExtractor2,
3400
- atDirectiveExtractor2,
3401
3574
  externalUrlCounterExtractor2,
3402
3575
  markdownLinkExtractor2,
3403
- slashExtractor2,
3576
+ mcpToolsExtractor2,
3404
3577
  toolsCountExtractor2,
3405
3578
  annotationOrphanAnalyzer2,
3406
3579
  annotationStaleAnalyzer2,
@@ -5945,11 +6118,6 @@ var LINK_KIND_VALUES = Object.freeze([
5945
6118
  "mentions",
5946
6119
  "supersedes"
5947
6120
  ]);
5948
- var CONFIDENCE_VALUES = Object.freeze([
5949
- "high",
5950
- "medium",
5951
- "low"
5952
- ]);
5953
6121
  var SEVERITY_VALUES = Object.freeze([
5954
6122
  "error",
5955
6123
  "warn",
@@ -5981,7 +6149,7 @@ function isLinkKind(s) {
5981
6149
  return typeof s === "string" && LINK_KIND_VALUES.includes(s);
5982
6150
  }
5983
6151
  function isConfidence(s) {
5984
- return typeof s === "string" && CONFIDENCE_VALUES.includes(s);
6152
+ return typeof s === "number" && Number.isFinite(s) && s >= 0 && s <= 1;
5985
6153
  }
5986
6154
  function isSeverity(s) {
5987
6155
  return typeof s === "string" && SEVERITY_VALUES.includes(s);
@@ -5995,7 +6163,7 @@ function parseLinkKind(s, ctx) {
5995
6163
  function parseConfidence(s, ctx) {
5996
6164
  if (isConfidence(s)) return s;
5997
6165
  throw new Error(
5998
- `Invalid Confidence value ${formatValue(s)} at ${ctx}. Allowed: ${CONFIDENCE_VALUES.join(" | ")}.`
6166
+ `Invalid Confidence value ${formatValue(s)} at ${ctx}. Expected a finite number in [0..1].`
5999
6167
  );
6000
6168
  }
6001
6169
  function parseSeverity(s, ctx) {
@@ -8794,10 +8962,45 @@ var plainParser = {
8794
8962
  }
8795
8963
  };
8796
8964
 
8965
+ // plugins/core/parsers/toml/index.ts
8966
+ import { parse as parseToml } from "smol-toml";
8967
+ var tomlParser = {
8968
+ id: "toml",
8969
+ parse(raw, _path) {
8970
+ let parsed = {};
8971
+ const issues = [];
8972
+ try {
8973
+ const doc = parseToml(raw);
8974
+ if (doc && typeof doc === "object" && !Array.isArray(doc)) {
8975
+ parsed = stripPrototypePollution(doc);
8976
+ }
8977
+ } catch (err) {
8978
+ issues.push({
8979
+ code: "frontmatter-parse-error",
8980
+ message: sanitiseParseErrorMessage2(err)
8981
+ });
8982
+ }
8983
+ const out = {
8984
+ frontmatterRaw: raw,
8985
+ frontmatter: parsed,
8986
+ body: ""
8987
+ };
8988
+ if (issues.length > 0) {
8989
+ return { ...out, issues };
8990
+ }
8991
+ return out;
8992
+ }
8993
+ };
8994
+ function sanitiseParseErrorMessage2(err) {
8995
+ const raw = err instanceof Error ? err.message : String(err);
8996
+ return raw.replace(/[-]+/g, " ").replace(/\s+/g, " ").trim();
8997
+ }
8998
+
8797
8999
  // kernel/scan/parsers/index.ts
8798
9000
  var REGISTRY = /* @__PURE__ */ new Map([
8799
9001
  [frontmatterYamlParser.id, frontmatterYamlParser],
8800
- [plainParser.id, plainParser]
9002
+ [plainParser.id, plainParser],
9003
+ [tomlParser.id, tomlParser]
8801
9004
  ]);
8802
9005
  var FROZEN_IDS = new Set(REGISTRY.keys());
8803
9006
  function getParser(id) {
@@ -9469,6 +9672,42 @@ function relativeIfBelow(path, cwd) {
9469
9672
  return rel;
9470
9673
  }
9471
9674
 
9675
+ // cli/util/scan-zone-drop.ts
9676
+ import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
9677
+
9678
+ // cli/commands/db/shared.ts
9679
+ var SAFE_SQL_IDENTIFIER_RE = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
9680
+ function assertSafeIdentifier(name) {
9681
+ if (!SAFE_SQL_IDENTIFIER_RE.test(name)) {
9682
+ throw new Error(`refusing to operate on non-identifier table name: ${JSON.stringify(name)}`);
9683
+ }
9684
+ }
9685
+
9686
+ // cli/util/scan-zone-drop.ts
9687
+ function dropScanZone(dbPath) {
9688
+ const db = new DatabaseSync4(dbPath);
9689
+ try {
9690
+ const rows = db.prepare(
9691
+ "SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'scan\\_%' ESCAPE '\\'"
9692
+ ).all();
9693
+ for (const r of rows) assertSafeIdentifier(r.name);
9694
+ if (rows.length === 0) {
9695
+ return { tableCount: 0, droppedTables: [] };
9696
+ }
9697
+ db.exec("BEGIN");
9698
+ for (const { name } of rows) {
9699
+ db.exec(`DELETE FROM "${name}"`);
9700
+ }
9701
+ db.exec("COMMIT");
9702
+ return {
9703
+ tableCount: rows.length,
9704
+ droppedTables: rows.map((r) => r.name)
9705
+ };
9706
+ } finally {
9707
+ db.close();
9708
+ }
9709
+ }
9710
+
9472
9711
  // cli/i18n/config.texts.ts
9473
9712
  var CONFIG_TEXTS = {
9474
9713
  unknownKey: "{{glyph}} Unknown config key: {{key}}\n",
@@ -9511,6 +9750,20 @@ var CONFIG_TEXTS = {
9511
9750
  * screen what they just opted into.
9512
9751
  */
9513
9752
  privacyGateConfirmed: '{{glyph}} Opening disk access for "{{key}}":\n{{paths}}\n',
9753
+ /**
9754
+ * Confirmation printed after `sm config set activeProvider <id>`
9755
+ * succeeds. The lens change atomically drops the scan_* zone (per
9756
+ * `architecture.md` §Active Provider Lens) so the persisted graph
9757
+ * never carries stale node / link rows from the previous lens. We
9758
+ * surface what was cleared so the operator knows their state was
9759
+ * touched and what to do next.
9760
+ */
9761
+ lensSwitchedCleared: "{{glyph}} Lens switched. Cleared {{tableCount}} scan table(s): {{tableNames}}.\n {{hint}}\n",
9762
+ lensSwitchedClearedHint: "Run `sm scan` to repopulate the graph under the new lens.",
9763
+ /** Same lens-switch announcement when the DB was empty (no rows to clear). */
9764
+ lensSwitchedEmpty: "{{glyph}} Lens switched. Scan zone was already empty.\n {{hint}}\n",
9765
+ /** Lens switch happened before any `sm scan` ran (no DB file on disk yet). */
9766
+ lensSwitchedNoDb: "{{glyph}} Lens switched. Run `sm scan` to populate the graph under the new lens.\n",
9514
9767
  // --- list verb (sectioned human renderer) ----------------------------
9515
9768
  /** Section heading: ` General`, ` Scan`, … rendered before its rows. */
9516
9769
  listSectionHeader: " {{title}}\n",
@@ -9962,8 +10215,44 @@ var ConfigSetCommand = class extends SmCommand {
9962
10215
  )
9963
10216
  })
9964
10217
  );
10218
+ if (this.key === "activeProvider") {
10219
+ this.announceLensSwitch(ctx.cwd, ansi);
10220
+ }
9965
10221
  return ExitCode.Ok;
9966
10222
  }
10223
+ /**
10224
+ * Side effect of `sm config set activeProvider <id>`, atomically
10225
+ * drops the `scan_*` zone so the persisted graph never reflects the
10226
+ * wrong lens (see `architecture.md` §Active Provider Lens). The drop
10227
+ * is non-destructive of `state_*` / `config_*` rows; the operator
10228
+ * runs `sm scan` next to repopulate.
10229
+ *
10230
+ * Silent when no DB file exists on disk yet (fresh project that has
10231
+ * never run `sm scan`), the lens just gets set and the next scan
10232
+ * uses it.
10233
+ */
10234
+ announceLensSwitch(cwd, ansi) {
10235
+ const dbPath = resolveDbPath({ db: void 0, cwd });
10236
+ const okGlyph = ansi.green("\u2713");
10237
+ if (!existsSync14(dbPath)) {
10238
+ this.printer.info(tx(CONFIG_TEXTS.lensSwitchedNoDb, { glyph: okGlyph }));
10239
+ return;
10240
+ }
10241
+ const result = dropScanZone(dbPath);
10242
+ const hint = ansi.dim(CONFIG_TEXTS.lensSwitchedClearedHint);
10243
+ if (result.tableCount === 0) {
10244
+ this.printer.info(tx(CONFIG_TEXTS.lensSwitchedEmpty, { glyph: okGlyph, hint }));
10245
+ return;
10246
+ }
10247
+ this.printer.info(
10248
+ tx(CONFIG_TEXTS.lensSwitchedCleared, {
10249
+ glyph: okGlyph,
10250
+ tableCount: result.tableCount,
10251
+ tableNames: result.droppedTables.join(", "),
10252
+ hint
10253
+ })
10254
+ );
10255
+ }
9967
10256
  };
9968
10257
  var ConfigResetCommand = class extends SmCommand {
9969
10258
  static paths = [["config", "reset"]];
@@ -11001,18 +11290,8 @@ var DbRestoreCommand = class extends SmCommand {
11001
11290
 
11002
11291
  // cli/commands/db/reset.ts
11003
11292
  import { rm as rm2 } from "fs/promises";
11004
- import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
11293
+ import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
11005
11294
  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)}`);
11012
- }
11013
- }
11014
-
11015
- // cli/commands/db/reset.ts
11016
11295
  var DbResetCommand = class extends SmCommand {
11017
11296
  static paths = [["db", "reset"]];
11018
11297
  static usage = Command8.Usage({
@@ -11094,7 +11373,7 @@ var DbResetCommand = class extends SmCommand {
11094
11373
  return ExitCode.Error;
11095
11374
  }
11096
11375
  }
11097
- const db = new DatabaseSync4(path);
11376
+ const db = new DatabaseSync5(path);
11098
11377
  try {
11099
11378
  const rows = db.prepare(
11100
11379
  "SELECT name FROM sqlite_master WHERE type='table' AND (name LIKE 'scan\\_%' ESCAPE '\\'" + (this.state ? " OR name LIKE 'state\\_%' ESCAPE '\\'" : "") + ")"
@@ -11237,7 +11516,7 @@ var DbBrowserCommand = class extends SmCommand {
11237
11516
  };
11238
11517
 
11239
11518
  // cli/commands/db/dump.ts
11240
- import { DatabaseSync as DatabaseSync5 } from "node:sqlite";
11519
+ import { DatabaseSync as DatabaseSync6 } from "node:sqlite";
11241
11520
  import { Command as Command11, Option as Option10 } from "clipanion";
11242
11521
  var DbDumpCommand = class extends SmCommand {
11243
11522
  static paths = [["db", "dump"]];
@@ -11279,7 +11558,7 @@ var DbDumpCommand = class extends SmCommand {
11279
11558
  }
11280
11559
  };
11281
11560
  function dumpDatabaseToStream(dbPath, out, tables) {
11282
- const db = new DatabaseSync5(dbPath, { readOnly: true });
11561
+ const db = new DatabaseSync6(dbPath, { readOnly: true });
11283
11562
  try {
11284
11563
  out.write("PRAGMA foreign_keys=OFF;\n");
11285
11564
  out.write("BEGIN TRANSACTION;\n");
@@ -12745,12 +13024,22 @@ var ORCHESTRATOR_TEXTS = {
12745
13024
  runScanRootMissing: "runScan: root path '{{root}}' does not exist or is not a directory"
12746
13025
  };
12747
13026
 
13027
+ // kernel/types.ts
13028
+ var ConfidenceTier = Object.freeze({
13029
+ HIGH: 0.9,
13030
+ MEDIUM: 0.6,
13031
+ LOW: 0.3
13032
+ });
13033
+
12748
13034
  // kernel/orchestrator/extractors.ts
12749
13035
  async function runExtractorsForNode(opts) {
12750
13036
  const internalLinks = [];
12751
13037
  const externalLinks = [];
12752
13038
  const enrichmentBuffer = /* @__PURE__ */ new Map();
12753
13039
  const contributions = [];
13040
+ const signals = [];
13041
+ const virtualNodes = [];
13042
+ const virtualNodePaths = /* @__PURE__ */ new Set();
12754
13043
  const validators = loadSchemaValidators();
12755
13044
  for (const extractor of opts.extractors) {
12756
13045
  const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
@@ -12822,6 +13111,18 @@ async function runExtractorsForNode(opts) {
12822
13111
  emittedAt: Date.now()
12823
13112
  });
12824
13113
  };
13114
+ const emitSignal = (signal) => {
13115
+ const validated = validateSignal(extractor, signal, opts.emitter);
13116
+ if (!validated) return;
13117
+ signals.push(validated);
13118
+ };
13119
+ const emitNode = (emitted) => {
13120
+ if (virtualNodePaths.has(emitted.path)) return;
13121
+ const node = buildVirtualNode(extractor, emitted, opts.emitter);
13122
+ if (!node) return;
13123
+ virtualNodePaths.add(node.path);
13124
+ virtualNodes.push(node);
13125
+ };
12825
13126
  const store = opts.pluginStores?.get(extractor.pluginId);
12826
13127
  const ctx = buildExtractorContext(
12827
13128
  extractor,
@@ -12831,6 +13132,8 @@ async function runExtractorsForNode(opts) {
12831
13132
  emitLink,
12832
13133
  enrichNode,
12833
13134
  emitContribution,
13135
+ emitSignal,
13136
+ emitNode,
12834
13137
  store
12835
13138
  );
12836
13139
  await extractor.extract(ctx);
@@ -12839,7 +13142,9 @@ async function runExtractorsForNode(opts) {
12839
13142
  internalLinks,
12840
13143
  externalLinks,
12841
13144
  enrichments: Array.from(enrichmentBuffer.values()),
12842
- contributions
13145
+ contributions,
13146
+ signals,
13147
+ virtualNodes
12843
13148
  };
12844
13149
  }
12845
13150
  function readDeclaredContributions(extension) {
@@ -12864,7 +13169,7 @@ function emitExtensionError(emitter, qualifiedId2, nodePath, data) {
12864
13169
  })
12865
13170
  );
12866
13171
  }
12867
- function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, store) {
13172
+ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, emitSignal, emitNode, store) {
12868
13173
  const scope = extractor.scope ?? "both";
12869
13174
  const settings = extractor.resolvedSettings ?? {};
12870
13175
  return {
@@ -12875,9 +13180,62 @@ function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enr
12875
13180
  emitLink,
12876
13181
  enrichNode,
12877
13182
  emitContribution,
13183
+ emitSignal,
13184
+ emitNode,
12878
13185
  ...store !== void 0 ? { store } : {}
12879
13186
  };
12880
13187
  }
13188
+ var VIRTUAL_NODE_PLACEHOLDER_HASH = "0".repeat(64);
13189
+ function buildVirtualNode(extractor, emitted, emitter) {
13190
+ const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
13191
+ if (typeof emitted.path !== "string" || emitted.path.length === 0) {
13192
+ emitter.emit(
13193
+ makeEvent("extension.error", {
13194
+ kind: "virtual-node-missing-path",
13195
+ extensionId: qualifiedId2,
13196
+ message: `Extractor ${qualifiedId2} emitted a virtual node with no path; dropped.`
13197
+ })
13198
+ );
13199
+ return null;
13200
+ }
13201
+ if (typeof emitted.kind !== "string" || emitted.kind.length === 0) {
13202
+ emitter.emit(
13203
+ makeEvent("extension.error", {
13204
+ kind: "virtual-node-missing-kind",
13205
+ extensionId: qualifiedId2,
13206
+ virtualPath: emitted.path,
13207
+ message: `Extractor ${qualifiedId2} emitted a virtual node at '${emitted.path}' with no kind; dropped.`
13208
+ })
13209
+ );
13210
+ return null;
13211
+ }
13212
+ if (!Array.isArray(emitted.derivedFrom) || emitted.derivedFrom.length === 0) {
13213
+ emitter.emit(
13214
+ makeEvent("extension.error", {
13215
+ kind: "virtual-node-missing-derived-from",
13216
+ extensionId: qualifiedId2,
13217
+ virtualPath: emitted.path,
13218
+ message: `Extractor ${qualifiedId2} emitted a virtual node at '${emitted.path}' with empty derivedFrom; dropped.`
13219
+ })
13220
+ );
13221
+ return null;
13222
+ }
13223
+ const node = {
13224
+ path: emitted.path,
13225
+ kind: emitted.kind,
13226
+ provider: emitted.provider,
13227
+ bodyHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
13228
+ frontmatterHash: VIRTUAL_NODE_PLACEHOLDER_HASH,
13229
+ bytes: { frontmatter: 0, body: 0, total: 0 },
13230
+ linksOutCount: 0,
13231
+ linksInCount: 0,
13232
+ externalRefsCount: 0,
13233
+ virtual: true,
13234
+ derivedFrom: [...emitted.derivedFrom]
13235
+ };
13236
+ if (emitted.frontmatter) node.frontmatter = emitted.frontmatter;
13237
+ return node;
13238
+ }
12881
13239
  function validateLink(extractor, link2, emitter) {
12882
13240
  const knownKinds = ["invokes", "references", "mentions", "supersedes"];
12883
13241
  if (!knownKinds.includes(link2.kind)) {
@@ -12898,9 +13256,68 @@ function validateLink(extractor, link2, emitter) {
12898
13256
  );
12899
13257
  return null;
12900
13258
  }
12901
- const confidence = link2.confidence ?? "medium";
13259
+ const c = link2.confidence;
13260
+ if (c !== void 0 && (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1)) {
13261
+ const qualifiedId2 = `${extractor.pluginId}/${extractor.id}`;
13262
+ emitter.emit(
13263
+ makeEvent("extension.error", {
13264
+ kind: "link-confidence-out-of-range",
13265
+ extensionId: qualifiedId2,
13266
+ confidence: c,
13267
+ message: `Extractor ${qualifiedId2} emitted a Link with confidence ${String(c)} outside [0..1]; dropped.`
13268
+ })
13269
+ );
13270
+ return null;
13271
+ }
13272
+ const confidence = c ?? ConfidenceTier.MEDIUM;
12902
13273
  return { ...link2, confidence };
12903
13274
  }
13275
+ var KNOWN_LINK_KINDS = ["invokes", "references", "mentions", "supersedes"];
13276
+ function validateSignal(extractor, signal, emitter) {
13277
+ const qualifiedId2 = qualifiedExtensionId(extractor.pluginId, extractor.id);
13278
+ if (!Array.isArray(signal.candidates) || signal.candidates.length === 0) {
13279
+ emitter.emit(
13280
+ makeEvent("extension.error", {
13281
+ kind: "signal-no-candidates",
13282
+ extensionId: qualifiedId2,
13283
+ signal: { source: signal.source, scope: signal.scope },
13284
+ message: `Extractor ${qualifiedId2} emitted a Signal with no candidates; dropped.`
13285
+ })
13286
+ );
13287
+ return null;
13288
+ }
13289
+ for (const candidate of signal.candidates) {
13290
+ if (!isValidSignalCandidate(qualifiedId2, candidate, emitter)) return null;
13291
+ }
13292
+ return signal;
13293
+ }
13294
+ function isValidSignalCandidate(qualifiedId2, candidate, emitter) {
13295
+ if (!KNOWN_LINK_KINDS.includes(candidate.kind)) {
13296
+ emitter.emit(
13297
+ makeEvent("extension.error", {
13298
+ kind: "signal-candidate-kind-not-declared",
13299
+ extensionId: qualifiedId2,
13300
+ candidateKind: candidate.kind,
13301
+ declaredKinds: KNOWN_LINK_KINDS,
13302
+ message: `Extractor ${qualifiedId2} emitted a Signal candidate with off-enum kind '${String(candidate.kind)}'; dropped.`
13303
+ })
13304
+ );
13305
+ return false;
13306
+ }
13307
+ const c = candidate.confidence;
13308
+ if (typeof c !== "number" || !Number.isFinite(c) || c < 0 || c > 1) {
13309
+ emitter.emit(
13310
+ makeEvent("extension.error", {
13311
+ kind: "signal-candidate-confidence-out-of-range",
13312
+ extensionId: qualifiedId2,
13313
+ confidence: candidate.confidence,
13314
+ message: `Extractor ${qualifiedId2} emitted a Signal candidate with confidence ${String(c)} outside [0..1]; dropped.`
13315
+ })
13316
+ );
13317
+ return false;
13318
+ }
13319
+ return true;
13320
+ }
12904
13321
  function dedupeLinks(links) {
12905
13322
  const out = /* @__PURE__ */ new Map();
12906
13323
  for (const link2 of links) {
@@ -12947,7 +13364,9 @@ function recomputeExternalRefsCount(nodes, externalLinks, cachedPaths) {
12947
13364
  }
12948
13365
  }
12949
13366
  var EXTERNAL_URL_SCHEME_RE = /^[a-z][a-z0-9+\-.]+:/i;
13367
+ var VIRTUAL_NODE_SCHEME_RE = /^mcp:\/\//i;
12950
13368
  function isExternalUrlLink(link2) {
13369
+ if (VIRTUAL_NODE_SCHEME_RE.test(link2.target)) return false;
12951
13370
  return EXTERNAL_URL_SCHEME_RE.test(link2.target);
12952
13371
  }
12953
13372
 
@@ -13106,13 +13525,9 @@ function originatingNodeOf(link2, priorNodePaths) {
13106
13525
  }
13107
13526
  function computeCacheDecision(opts) {
13108
13527
  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
- });
13528
+ if (!matchesKindPrecondition(ex, opts.kind)) return false;
13529
+ if (!matchesProviderPrecondition(ex, opts.provider)) return false;
13530
+ return true;
13116
13531
  });
13117
13532
  const applicableQualifiedIds = new Set(
13118
13533
  applicableExtractors.map((ex) => qualifiedExtensionId(ex.pluginId, ex.id))
@@ -13126,6 +13541,20 @@ function computeCacheDecision(opts) {
13126
13541
  fullCacheHit: opts.nodeHashCacheEligible && split.missingExtractors.length === 0
13127
13542
  };
13128
13543
  }
13544
+ function matchesKindPrecondition(ex, kind) {
13545
+ const kinds = ex.precondition?.kind;
13546
+ if (!kinds || kinds.length === 0) return true;
13547
+ return kinds.some((qualified) => {
13548
+ const slashIdx = qualified.indexOf("/");
13549
+ const kindOnly = slashIdx === -1 ? qualified : qualified.slice(slashIdx + 1);
13550
+ return kindOnly === kind;
13551
+ });
13552
+ }
13553
+ function matchesProviderPrecondition(ex, provider) {
13554
+ const providers = ex.precondition?.provider;
13555
+ if (!providers || providers.length === 0) return true;
13556
+ return providers.includes(provider);
13557
+ }
13129
13558
  function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
13130
13559
  const cachedQualifiedIds = /* @__PURE__ */ new Set();
13131
13560
  const missingExtractors = [];
@@ -13235,7 +13664,7 @@ function findHighConfidenceRenames(opts) {
13235
13664
  if (opts.claimedNew.has(toPath)) continue;
13236
13665
  const toNode = opts.currentByPath.get(toPath);
13237
13666
  if (toNode.bodyHash === fromNode.bodyHash) {
13238
- ops.push({ from: fromPath, to: toPath, confidence: "high" });
13667
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.HIGH });
13239
13668
  opts.claimedDeleted.add(fromPath);
13240
13669
  opts.claimedNew.add(toPath);
13241
13670
  break;
@@ -13270,13 +13699,13 @@ function claimSingletonRenames(opts) {
13270
13699
  const remaining = candidates.filter((p) => !opts.claimedDeleted.has(p));
13271
13700
  if (remaining.length === 1) {
13272
13701
  const fromPath = remaining[0];
13273
- ops.push({ from: fromPath, to: toPath, confidence: "medium" });
13702
+ ops.push({ from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM });
13274
13703
  opts.issues.push({
13275
13704
  analyzerId: "auto-rename-medium",
13276
13705
  severity: "warn",
13277
13706
  nodeIds: [toPath],
13278
13707
  message: `Auto-rename (medium confidence): ${fromPath} \u2192 ${toPath}`,
13279
- data: { from: fromPath, to: toPath, confidence: "medium" }
13708
+ data: { from: fromPath, to: toPath, confidence: ConfidenceTier.MEDIUM }
13280
13709
  });
13281
13710
  opts.claimedDeleted.add(fromPath);
13282
13711
  opts.claimedNew.add(toPath);
@@ -13714,6 +14143,7 @@ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextInde
13714
14143
  const cacheDecision = computeCacheDecision({
13715
14144
  extractors: wctx.opts.extractors,
13716
14145
  kind,
14146
+ provider: provider.id,
13717
14147
  nodePath: raw.path,
13718
14148
  bodyHash,
13719
14149
  sidecarAnnotationsHash,
@@ -13816,6 +14246,10 @@ function mergeExtractResult(extractResult, accum) {
13816
14246
  accum.enrichmentBuffer.set(`${enr.nodePath}\0${enr.extractorId}`, enr);
13817
14247
  }
13818
14248
  for (const c of extractResult.contributions) accum.contributionsBuffer.push(c);
14249
+ for (const vn of extractResult.virtualNodes) {
14250
+ if (accum.nodes.some((n) => n.path === vn.path)) continue;
14251
+ accum.nodes.push(vn);
14252
+ }
13819
14253
  }
13820
14254
  function isPartialCacheHit(ctx) {
13821
14255
  return ctx.nodeHashCacheEligible && ctx.cacheDecision.cachedQualifiedIds.size > 0 && ctx.priorNode !== void 0;
@@ -19389,7 +19823,7 @@ function renderDeltaIssues(issues) {
19389
19823
 
19390
19824
  // cli/commands/serve.ts
19391
19825
  import { spawn as spawn2 } from "child_process";
19392
- import { existsSync as existsSync28 } from "fs";
19826
+ import { existsSync as existsSync30 } from "fs";
19393
19827
  import { Command as Command33, Option as Option31 } from "clipanion";
19394
19828
 
19395
19829
  // cli/util/browser-launch.ts
@@ -19411,7 +19845,7 @@ import { WebSocketServer } from "ws";
19411
19845
  // server/app.ts
19412
19846
  import { Hono } from "hono";
19413
19847
  import { bodyLimit } from "hono/body-limit";
19414
- import { HTTPException as HTTPException14 } from "hono/http-exception";
19848
+ import { HTTPException as HTTPException15 } from "hono/http-exception";
19415
19849
 
19416
19850
  // core/config/service.ts
19417
19851
  var ConfigService = class {
@@ -19719,7 +20153,20 @@ var SERVER_TEXTS = {
19719
20153
  // is a synthetic `emitter.error` event; v14.4.a does not yet route
19720
20154
  // it through the broadcaster (would re-enter the same stringify
19721
20155
  // path), so we degrade to a logged warning.
19722
- wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped, failed to serialize event: {{message}}.\n"
20156
+ wsBroadcastSerializeFailed: "skill-map server: ws broadcast dropped, failed to serialize event: {{message}}.\n",
20157
+ // ---- active-provider route (routes/active-provider.ts) -----------
20158
+ //
20159
+ // GET / PUT /api/active-provider. The active provider lens selects
20160
+ // which provider's extractors / classifiers / resolution rules apply
20161
+ // to the whole project (see `architecture.md` §Active Provider Lens).
20162
+ // Changing the lens drops the `scan_*` zone atomically and prompts
20163
+ // the user to re-scan; `state_*` and `config_*` survive.
20164
+ activeProviderBodyNotJson: "Request body must be valid JSON.",
20165
+ activeProviderBodyNotObject: "Request body must be a JSON object.",
20166
+ activeProviderBodyMissing: "Request body must include `activeProvider` (a non-empty string).",
20167
+ activeProviderValueNotString: "`activeProvider` must be a string.",
20168
+ activeProviderValueEmpty: "`activeProvider` cannot be the empty string. Send the id of an enabled provider.",
20169
+ activeProviderPersistFailed: "Could not persist activeProvider: {{message}}"
19723
20170
  };
19724
20171
 
19725
20172
  // server/loopback-gate.ts
@@ -20606,11 +21053,6 @@ function registerPluginsRoute(app, deps) {
20606
21053
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
20607
21054
  });
20608
21055
  }
20609
- if (granularityOf(handle) !== "extension") {
20610
- throw new HTTPException8(400, {
20611
- message: tx(SERVER_TEXTS.pluginsGranularityBundleExpected, { id: bundleId })
20612
- });
20613
- }
20614
21056
  if (!hasExtension(handle, extensionId)) {
20615
21057
  throw new HTTPException8(404, {
20616
21058
  message: tx(SERVER_TEXTS.pluginsExtensionUnknown, { bundleId, extensionId })
@@ -20651,7 +21093,7 @@ function buildBuiltInItems(resolveEnabled) {
20651
21093
  return builtInBundles.map((bundle) => {
20652
21094
  const bundleEnabled = resolveEnabled(bundle.id);
20653
21095
  const bundleLocked = isPluginLocked(bundle.id);
20654
- const extensions = bundle.granularity === "extension" ? bundle.extensions.map((ext) => {
21096
+ const extensions = bundle.extensions.map((ext) => {
20655
21097
  const qualified = qualifiedExtensionId(bundle.id, ext.id);
20656
21098
  const extLocked = bundleLocked || isPluginLocked(qualified);
20657
21099
  return {
@@ -20662,7 +21104,7 @@ function buildBuiltInItems(resolveEnabled) {
20662
21104
  ...ext.description ? { description: ext.description } : {},
20663
21105
  ...extLocked ? { locked: true } : {}
20664
21106
  };
20665
- }) : void 0;
21107
+ });
20666
21108
  return {
20667
21109
  id: bundle.id,
20668
21110
  version: firstVersion(bundle.extensions),
@@ -20672,7 +21114,7 @@ function buildBuiltInItems(resolveEnabled) {
20672
21114
  source: "built-in",
20673
21115
  granularity: bundle.granularity,
20674
21116
  description: bundle.description,
20675
- ...extensions ? { extensions } : {},
21117
+ ...extensions.length > 0 ? { extensions } : {},
20676
21118
  ...bundleLocked ? { locked: true } : {}
20677
21119
  };
20678
21120
  });
@@ -20705,8 +21147,8 @@ function optionalDiscoveredFields(plugin, extensions) {
20705
21147
  if (extensions) out.extensions = extensions;
20706
21148
  return out;
20707
21149
  }
20708
- function projectExtensionRows(plugin, granularity, resolveEnabled, bundleLocked) {
20709
- if (granularity !== "extension" || !plugin.extensions) return void 0;
21150
+ function projectExtensionRows(plugin, _granularity, resolveEnabled, bundleLocked) {
21151
+ if (!plugin.extensions || plugin.extensions.length === 0) return void 0;
20710
21152
  return plugin.extensions.map((ext) => {
20711
21153
  const description = readInstanceDescription(ext.instance);
20712
21154
  const qualified = qualifiedExtensionId(plugin.id, ext.id);
@@ -20823,13 +21265,6 @@ function validateBulkChange(change, deps) {
20823
21265
  message: tx(SERVER_TEXTS.pluginsUnknown, { id: bundleId })
20824
21266
  };
20825
21267
  }
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
21268
  if (!hasExtension(handle, extensionId)) {
20834
21269
  return {
20835
21270
  status: 404,
@@ -21309,9 +21744,106 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
21309
21744
  }
21310
21745
  });
21311
21746
 
21312
- // server/routes/scan.ts
21747
+ // server/routes/active-provider.ts
21748
+ import { existsSync as existsSync27 } from "fs";
21313
21749
  import { HTTPException as HTTPException12 } from "hono/http-exception";
21314
21750
 
21751
+ // core/config/active-provider.ts
21752
+ import { existsSync as existsSync26 } from "fs";
21753
+ import { join as join17 } from "path";
21754
+ var DETECTION_RULES = [
21755
+ { providerId: "claude", marker: ".claude" },
21756
+ { providerId: "gemini", marker: ".gemini" },
21757
+ { providerId: "openai", marker: ".codex" },
21758
+ { providerId: "openai", marker: "AGENTS.md" },
21759
+ { providerId: "cursor", marker: ".cursor" }
21760
+ ];
21761
+ function resolveActiveProvider(cwd) {
21762
+ const detected = detectProvidersFromFilesystem(cwd);
21763
+ const fromConfig = readConfigValue("activeProvider", { cwd });
21764
+ if (typeof fromConfig === "string" && fromConfig.length > 0) {
21765
+ return { resolved: fromConfig, source: "config", detected };
21766
+ }
21767
+ if (detected.length > 0) {
21768
+ return { resolved: detected[0], source: "autodetect", detected };
21769
+ }
21770
+ return { resolved: null, source: "none", detected };
21771
+ }
21772
+ function detectProvidersFromFilesystem(cwd) {
21773
+ const seen = /* @__PURE__ */ new Set();
21774
+ const out = [];
21775
+ for (const rule of DETECTION_RULES) {
21776
+ if (seen.has(rule.providerId)) continue;
21777
+ if (!existsSync26(join17(cwd, rule.marker))) continue;
21778
+ seen.add(rule.providerId);
21779
+ out.push(rule.providerId);
21780
+ }
21781
+ return out;
21782
+ }
21783
+
21784
+ // server/routes/active-provider.ts
21785
+ function registerActiveProviderRoute(app, deps) {
21786
+ app.get("/api/active-provider", (c) => {
21787
+ return c.json(buildEnvelope4(deps));
21788
+ });
21789
+ app.patch("/api/active-provider", async (c) => {
21790
+ const body = await parsePatchBody5(c.req.raw);
21791
+ const result = applyLensSwitch(deps, body.activeProvider);
21792
+ deps.configService.reload();
21793
+ return c.json({ ...buildEnvelope4(deps), switch: result });
21794
+ });
21795
+ }
21796
+ function buildEnvelope4(deps) {
21797
+ const r = resolveActiveProvider(deps.runtimeContext.cwd);
21798
+ return {
21799
+ activeProvider: r.resolved,
21800
+ detected: r.detected,
21801
+ source: r.source
21802
+ };
21803
+ }
21804
+ function applyLensSwitch(deps, newValue) {
21805
+ const cwd = deps.runtimeContext.cwd;
21806
+ try {
21807
+ writeConfigValue("activeProvider", newValue, { target: "project", cwd });
21808
+ } catch (err) {
21809
+ throw new HTTPException12(400, {
21810
+ message: tx(SERVER_TEXTS.activeProviderPersistFailed, {
21811
+ message: formatErrorMessage(err)
21812
+ })
21813
+ });
21814
+ }
21815
+ const dbPath = resolveDbPath({ db: void 0, cwd });
21816
+ if (!existsSync27(dbPath)) return { dropped: null };
21817
+ const dropResult = dropScanZone(dbPath);
21818
+ return {
21819
+ dropped: {
21820
+ tableCount: dropResult.tableCount,
21821
+ tableNames: dropResult.droppedTables
21822
+ }
21823
+ };
21824
+ }
21825
+ var PATCH_BODY_SCHEMA4 = {
21826
+ type: "object",
21827
+ additionalProperties: false,
21828
+ required: ["activeProvider"],
21829
+ properties: {
21830
+ activeProvider: { type: "string", minLength: 1 }
21831
+ }
21832
+ };
21833
+ var parsePatchBody5 = makeBodyValidator(PATCH_BODY_SCHEMA4, {
21834
+ notJson: SERVER_TEXTS.activeProviderBodyNotJson,
21835
+ notObject: SERVER_TEXTS.activeProviderBodyNotObject,
21836
+ invalid: SERVER_TEXTS.activeProviderBodyMissing,
21837
+ mapping: {
21838
+ ":required": SERVER_TEXTS.activeProviderBodyMissing,
21839
+ "/activeProvider:type:string": SERVER_TEXTS.activeProviderValueNotString,
21840
+ "/activeProvider:minLength": SERVER_TEXTS.activeProviderValueEmpty
21841
+ }
21842
+ });
21843
+
21844
+ // server/routes/scan.ts
21845
+ import { HTTPException as HTTPException13 } from "hono/http-exception";
21846
+
21315
21847
  // server/scan-mutex.ts
21316
21848
  var inFlight = null;
21317
21849
  async function withScanMutex(fn) {
@@ -21465,7 +21997,7 @@ function registerScanRoute(app, deps) {
21465
21997
  }
21466
21998
  async function runPersistedScan(c, deps) {
21467
21999
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21468
- throw new HTTPException12(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
22000
+ throw new HTTPException13(400, { message: SERVER_TEXTS.scanPostRequiresFullPipeline });
21469
22001
  }
21470
22002
  const dbExists = await tryWithSqlite(
21471
22003
  { databasePath: deps.options.dbPath, autoBackup: false },
@@ -21494,7 +22026,7 @@ async function runPersistedScan(c, deps) {
21494
22026
  emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
21495
22027
  });
21496
22028
  if (outcome.kind !== "ok") {
21497
- throw new HTTPException12(500, {
22029
+ throw new HTTPException13(500, {
21498
22030
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.scanGuardTrip, { existing: outcome.existing }) : outcome.message
21499
22031
  });
21500
22032
  }
@@ -21502,7 +22034,7 @@ async function runPersistedScan(c, deps) {
21502
22034
  });
21503
22035
  } catch (err) {
21504
22036
  if (err instanceof ScanBusyError) {
21505
- throw new HTTPException12(409, { message: SERVER_TEXTS.scanPostBusy });
22037
+ throw new HTTPException13(409, { message: SERVER_TEXTS.scanPostBusy });
21506
22038
  }
21507
22039
  throw err;
21508
22040
  }
@@ -21566,7 +22098,7 @@ function groupTagsBySource2(rows) {
21566
22098
  }
21567
22099
  async function runFreshScan(deps) {
21568
22100
  if (deps.options.noBuiltIns || deps.options.noPlugins) {
21569
- throw new HTTPException12(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
22101
+ throw new HTTPException13(400, { message: SERVER_TEXTS.freshScanRequiresPipeline });
21570
22102
  }
21571
22103
  const resolveEnabledOverride = await buildBffResolverOverride(deps);
21572
22104
  const outcome = await runScanForCommand({
@@ -21595,7 +22127,7 @@ async function runFreshScan(deps) {
21595
22127
  printer: bffScanRunnerPrinter
21596
22128
  });
21597
22129
  if (outcome.kind !== "ok") {
21598
- throw new HTTPException12(500, {
22130
+ throw new HTTPException13(500, {
21599
22131
  message: outcome.kind === "guard-trip" ? tx(SERVER_TEXTS.freshScanGuardTrip, { existing: outcome.existing }) : outcome.message
21600
22132
  });
21601
22133
  }
@@ -21629,7 +22161,7 @@ function emptyScanResult() {
21629
22161
  }
21630
22162
 
21631
22163
  // server/routes/sidecar.ts
21632
- import { HTTPException as HTTPException13 } from "hono/http-exception";
22164
+ import { HTTPException as HTTPException14 } from "hono/http-exception";
21633
22165
  import { resolve as resolve34 } from "path";
21634
22166
  var STATUS_FRESH = "fresh";
21635
22167
  var ENVELOPE_KIND2 = "sidecar.bumped";
@@ -21666,11 +22198,11 @@ function registerSidecarRoutes(app, deps) {
21666
22198
  assertContained(deps.runtimeContext.cwd, node.path);
21667
22199
  absPath = resolve34(deps.runtimeContext.cwd, node.path);
21668
22200
  } catch (err) {
21669
- throw new HTTPException13(500, { message: formatErrorMessage(err) });
22201
+ throw new HTTPException14(500, { message: formatErrorMessage(err) });
21670
22202
  }
21671
22203
  const result = invokeBump2(node, absPath, body);
21672
22204
  if (result.report.ok === false && result.report.reason === "fresh") {
21673
- throw new HTTPException13(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
22205
+ throw new HTTPException14(409, { message: SERVER_TEXTS.sidecarFreshRefusal });
21674
22206
  }
21675
22207
  if (result.report.ok === true && result.report.noop === true) {
21676
22208
  const envelope2 = {
@@ -21697,7 +22229,7 @@ function registerSidecarRoutes(app, deps) {
21697
22229
  }
21698
22230
  } catch (err) {
21699
22231
  if (err instanceof EConsentRequiredError) throw err;
21700
- throw new HTTPException13(500, { message: formatErrorMessage(err) });
22232
+ throw new HTTPException14(500, { message: formatErrorMessage(err) });
21701
22233
  }
21702
22234
  if (body.confirm === true) {
21703
22235
  deps.configService.reload();
@@ -21734,7 +22266,7 @@ async function loadNode(deps, nodePath) {
21734
22266
  );
21735
22267
  const node = persisted?.nodes.find((n) => n.path === nodePath);
21736
22268
  if (!node) {
21737
- throw new HTTPException13(404, {
22269
+ throw new HTTPException14(404, {
21738
22270
  message: tx(SERVER_TEXTS.nodeNotFound, { path: sanitizeForTerminal(nodePath) })
21739
22271
  });
21740
22272
  }
@@ -21742,7 +22274,7 @@ async function loadNode(deps, nodePath) {
21742
22274
  }
21743
22275
  function invokeBump2(node, absPath, body) {
21744
22276
  if (!bumpAction.invoke) {
21745
- throw new HTTPException13(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
22277
+ throw new HTTPException14(500, { message: SERVER_TEXTS.sidecarBumpInvokeMissing });
21746
22278
  }
21747
22279
  const input = {};
21748
22280
  if (body.force === true) input.force = true;
@@ -21788,9 +22320,9 @@ function registerUpdateStatusRoute(app, deps) {
21788
22320
  }
21789
22321
 
21790
22322
  // server/static.ts
21791
- import { existsSync as existsSync26 } from "fs";
22323
+ import { existsSync as existsSync28 } from "fs";
21792
22324
  import { readFile as readFile5 } from "fs/promises";
21793
- import { extname, join as join17 } from "path";
22325
+ import { extname, join as join18 } from "path";
21794
22326
  import { serveStatic } from "@hono/node-server/serve-static";
21795
22327
  var INDEX_HTML = "index.html";
21796
22328
  var PLACEHOLDER_HTML = `<!doctype html>
@@ -21842,8 +22374,8 @@ function createSpaFallback(opts) {
21842
22374
  return async (c, _next) => {
21843
22375
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
21844
22376
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
21845
- const indexPath = join17(opts.uiDist, INDEX_HTML);
21846
- if (!existsSync26(indexPath)) return htmlResponse(c, placeholder);
22377
+ const indexPath = join18(opts.uiDist, INDEX_HTML);
22378
+ if (!existsSync28(indexPath)) return htmlResponse(c, placeholder);
21847
22379
  return fileResponse(c, indexPath);
21848
22380
  };
21849
22381
  }
@@ -21929,13 +22461,13 @@ function attachBroadcasterRoute(app, broadcaster) {
21929
22461
 
21930
22462
  // server/app.ts
21931
22463
  var BODY_LIMIT_BYTES = 1024 * 1024;
21932
- var DbMissingError = class extends HTTPException14 {
22464
+ var DbMissingError = class extends HTTPException15 {
21933
22465
  constructor(message) {
21934
22466
  super(500, { message });
21935
22467
  this.name = "DbMissingError";
21936
22468
  }
21937
22469
  };
21938
- var BulkValidationError = class extends HTTPException14 {
22470
+ var BulkValidationError = class extends HTTPException15 {
21939
22471
  id;
21940
22472
  code;
21941
22473
  constructor(init) {
@@ -21945,7 +22477,7 @@ var BulkValidationError = class extends HTTPException14 {
21945
22477
  this.code = init.code;
21946
22478
  }
21947
22479
  };
21948
- var LoopbackGateError = class extends HTTPException14 {
22480
+ var LoopbackGateError = class extends HTTPException15 {
21949
22481
  code;
21950
22482
  constructor(init) {
21951
22483
  super(403, { message: init.message });
@@ -21965,7 +22497,7 @@ function createApp(deps) {
21965
22497
  bodyLimit({
21966
22498
  maxSize: BODY_LIMIT_BYTES,
21967
22499
  onError: () => {
21968
- throw new HTTPException14(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
22500
+ throw new HTTPException15(413, { message: tx(SERVER_TEXTS.bodyTooLarge, { maxBytes: String(BODY_LIMIT_BYTES) }) });
21969
22501
  }
21970
22502
  })
21971
22503
  );
@@ -22006,9 +22538,10 @@ function createApp(deps) {
22006
22538
  registerUpdateStatusRoute(app, routeDeps);
22007
22539
  registerPreferencesRoute(app, routeDeps);
22008
22540
  registerProjectPreferencesRoute(app, routeDeps);
22541
+ registerActiveProviderRoute(app, routeDeps);
22009
22542
  registerProjectIgnoreRoute(app, routeDeps);
22010
22543
  app.all("/api/*", (c) => {
22011
- throw new HTTPException14(404, {
22544
+ throw new HTTPException15(404, {
22012
22545
  message: tx(SERVER_TEXTS.unknownApiEndpoint, { path: sanitizeForTerminal(c.req.path) })
22013
22546
  });
22014
22547
  });
@@ -22016,7 +22549,7 @@ function createApp(deps) {
22016
22549
  app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
22017
22550
  app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
22018
22551
  app.notFound((c) => {
22019
- throw new HTTPException14(404, {
22552
+ throw new HTTPException15(404, {
22020
22553
  message: tx(SERVER_TEXTS.unknownPath, { path: sanitizeForTerminal(c.req.path) })
22021
22554
  });
22022
22555
  });
@@ -22071,7 +22604,7 @@ function formatError2(err, c) {
22071
22604
  };
22072
22605
  return c.json(envelope, 403);
22073
22606
  }
22074
- if (err instanceof HTTPException14) {
22607
+ if (err instanceof HTTPException15) {
22075
22608
  const status = err.status;
22076
22609
  const envelope = {
22077
22610
  ok: false,
@@ -22414,10 +22947,10 @@ function validateNoUi(noUi, uiDist) {
22414
22947
  }
22415
22948
 
22416
22949
  // 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";
22950
+ import { existsSync as existsSync29, statSync as statSync11 } from "fs";
22951
+ import { dirname as dirname18, isAbsolute as isAbsolute9, join as join19, resolve as resolve35 } from "path";
22419
22952
  import { fileURLToPath as fileURLToPath5 } from "url";
22420
- var DEFAULT_UI_REL = join18("ui", "dist", "ui", "browser");
22953
+ var DEFAULT_UI_REL = join19("ui", "dist", "ui", "browser");
22421
22954
  var PACKAGE_UI_REL = "ui";
22422
22955
  var INDEX_HTML2 = "index.html";
22423
22956
  function resolveDefaultUiDist(ctx) {
@@ -22429,10 +22962,10 @@ function resolveExplicitUiDist(ctx, raw) {
22429
22962
  return isAbsolute9(raw) ? raw : resolve35(ctx.cwd, raw);
22430
22963
  }
22431
22964
  function isUiBundleDir(path) {
22432
- if (!existsSync27(path)) return false;
22965
+ if (!existsSync29(path)) return false;
22433
22966
  try {
22434
22967
  if (!statSync11(path).isDirectory()) return false;
22435
- return existsSync27(join18(path, INDEX_HTML2));
22968
+ return existsSync29(join19(path, INDEX_HTML2));
22436
22969
  } catch {
22437
22970
  return false;
22438
22971
  }
@@ -22449,9 +22982,9 @@ function resolvePackageBundledUi() {
22449
22982
  function resolvePackageBundledUiFrom(here) {
22450
22983
  let current = here;
22451
22984
  for (let i = 0; i < 8; i++) {
22452
- const candidate = join18(current, PACKAGE_UI_REL);
22985
+ const candidate = join19(current, PACKAGE_UI_REL);
22453
22986
  if (isUiBundleDir(candidate)) return candidate;
22454
- const distHere = join18(current, "dist", PACKAGE_UI_REL);
22987
+ const distHere = join19(current, "dist", PACKAGE_UI_REL);
22455
22988
  if (isUiBundleDir(distHere)) return distHere;
22456
22989
  const parent = dirname18(current);
22457
22990
  if (parent === current) return null;
@@ -22462,7 +22995,7 @@ function resolvePackageBundledUiFrom(here) {
22462
22995
  function walkUpForUi(startDir) {
22463
22996
  let current = resolve35(startDir);
22464
22997
  for (let i = 0; i < 64; i++) {
22465
- const candidate = join18(current, DEFAULT_UI_REL);
22998
+ const candidate = join19(current, DEFAULT_UI_REL);
22466
22999
  if (isUiBundleDir(candidate)) return candidate;
22467
23000
  const parent = dirname18(current);
22468
23001
  if (parent === current) return null;
@@ -22896,7 +23429,7 @@ var ServeCommand = class extends SmCommand {
22896
23429
  return ExitCode.Error;
22897
23430
  }
22898
23431
  const dbPath = resolveDbPath({ db: this.db, ...runtimeCtx });
22899
- if (this.db !== void 0 && !existsSync28(dbPath)) {
23432
+ if (this.db !== void 0 && !existsSync30(dbPath)) {
22900
23433
  this.printer.info(
22901
23434
  tx(SERVE_TEXTS.dbNotFound, { path: sanitizeForTerminal(dbPath) })
22902
23435
  );
@@ -23307,22 +23840,27 @@ function renderLinksSection(direction, links, ansi) {
23307
23840
  const aggregated = aggregateLinks(links, projectField);
23308
23841
  const headerTpl = direction === "out" ? SHOW_TEXTS.linksOutSection : SHOW_TEXTS.linksInSection;
23309
23842
  const kindWidth = Math.max(...aggregated.map((g) => g.kind.length));
23310
- const confWidth = Math.max(...aggregated.map((g) => g.confidence.length));
23843
+ const confLabels = aggregated.map((g) => formatConfidence(g.confidence));
23844
+ const confWidth = Math.max(...confLabels.map((l) => l.length));
23311
23845
  const lines = [tx(headerTpl, { count: links.length })];
23312
- for (const grp of aggregated) {
23846
+ aggregated.forEach((grp, idx) => {
23313
23847
  const dup = grp.rowCount > 1 ? ansi.dim(tx(SHOW_TEXTS.linkDup, { count: grp.rowCount })) : "";
23314
23848
  lines.push(
23315
23849
  tx(SHOW_TEXTS.linkRow, {
23316
23850
  arrow: ansi.dim(arrow),
23317
23851
  kind: sanitizeForTerminal(grp.kind).padEnd(kindWidth),
23318
- confidence: ansi.dim(grp.confidence.padEnd(confWidth)),
23852
+ confidence: ansi.dim(confLabels[idx].padEnd(confWidth)),
23319
23853
  endpoint: sanitizeForTerminal(grp.endpoint),
23320
23854
  dup
23321
23855
  })
23322
23856
  );
23323
- }
23857
+ });
23324
23858
  return lines.join("");
23325
23859
  }
23860
+ function formatConfidence(c) {
23861
+ if (typeof c !== "number" || !Number.isFinite(c)) return "?";
23862
+ return `${Math.round(c * 100)}%`;
23863
+ }
23326
23864
  function renderIssuesSection(issues, nodePath, ansi) {
23327
23865
  const lines = [tx(SHOW_TEXTS.issuesSection, { count: issues.length })];
23328
23866
  const analyzerWidth = Math.max(
@@ -23389,17 +23927,12 @@ function aggregateLinks(links, endpointSide) {
23389
23927
  return a.kind.localeCompare(b.kind);
23390
23928
  });
23391
23929
  }
23392
- var CONFIDENCE_RANK = {
23393
- high: 2,
23394
- medium: 1,
23395
- low: 0
23396
- };
23397
23930
  function rankConfidenceForGrouping(c) {
23398
- return CONFIDENCE_RANK[c];
23931
+ return c;
23399
23932
  }
23400
23933
 
23401
23934
  // cli/commands/sidecar.ts
23402
- import { existsSync as existsSync29, unlinkSync as unlinkSync2 } from "fs";
23935
+ import { existsSync as existsSync31, unlinkSync as unlinkSync2 } from "fs";
23403
23936
  import { resolve as resolve36 } from "path";
23404
23937
  import { Command as Command35, Option as Option33 } from "clipanion";
23405
23938
 
@@ -23840,7 +24373,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23840
24373
  return ExitCode.Error;
23841
24374
  }
23842
24375
  const sidecarAbsPath = sidecarPathFor(absPath);
23843
- if (existsSync29(sidecarAbsPath) && this.force !== true) {
24376
+ if (existsSync31(sidecarAbsPath) && this.force !== true) {
23844
24377
  this.printer.error(
23845
24378
  tx(SIDECAR_TEXTS.annotateExists, {
23846
24379
  glyph: errGlyph,
@@ -23850,7 +24383,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
23850
24383
  );
23851
24384
  return ExitCode.Error;
23852
24385
  }
23853
- if (existsSync29(sidecarAbsPath) && this.force === true) {
24386
+ if (existsSync31(sidecarAbsPath) && this.force === true) {
23854
24387
  try {
23855
24388
  unlinkSync2(sidecarAbsPath);
23856
24389
  } catch (err) {
@@ -24080,8 +24613,8 @@ var STUB_COMMANDS = [
24080
24613
  ];
24081
24614
 
24082
24615
  // 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";
24616
+ import { cpSync as cpSync2, existsSync as existsSync32, mkdirSync as mkdirSync7, rmSync as rmSync2, statSync as statSync12 } from "fs";
24617
+ import { dirname as dirname19, join as join20, resolve as resolve37 } from "path";
24085
24618
  import { fileURLToPath as fileURLToPath6 } from "url";
24086
24619
  import { Command as Command37, Option as Option35 } from "clipanion";
24087
24620
 
@@ -24177,9 +24710,9 @@ var TutorialCommand = class extends SmCommand {
24177
24710
  }
24178
24711
  const variant = rawVariant ?? DEFAULT_VARIANT;
24179
24712
  const spec = VARIANT_SPECS[variant];
24180
- const targetDir = join19(ctx.cwd, ".claude", "skills", spec.slug);
24713
+ const targetDir = join20(ctx.cwd, ".claude", "skills", spec.slug);
24181
24714
  const targetDisplay = `.claude/skills/${spec.slug}/`;
24182
- if (existsSync30(targetDir) && !this.force) {
24715
+ if (existsSync32(targetDir) && !this.force) {
24183
24716
  this.printer.error(
24184
24717
  tx(TUTORIAL_TEXTS.alreadyExists, {
24185
24718
  glyph: errGlyph,
@@ -24262,7 +24795,7 @@ function resolveSkillSourceDir(variant) {
24262
24795
  resolve37(here, "../cli/tutorial", spec.slug)
24263
24796
  ];
24264
24797
  for (const candidate of candidates) {
24265
- if (existsSync30(candidate) && statSync12(candidate).isDirectory()) {
24798
+ if (existsSync32(candidate) && statSync12(candidate).isDirectory()) {
24266
24799
  cachedSourceDirs.set(variant, candidate);
24267
24800
  return candidate;
24268
24801
  }
@@ -24439,7 +24972,7 @@ await lifecycleDispatcher.dispatch(
24439
24972
  process.exit(exitCode);
24440
24973
  function resolveBareDefault() {
24441
24974
  const ctx = defaultRuntimeContext();
24442
- if (existsSync31(defaultProjectDbPath(ctx))) {
24975
+ if (existsSync33(defaultProjectDbPath(ctx))) {
24443
24976
  return ["serve"];
24444
24977
  }
24445
24978
  process.stderr.write(tx(ENTRY_TEXTS.bareNoProject, { cwd: ctx.cwd }));