@skill-map/cli 0.40.1 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +10 -1
  2. package/dist/cli.js +429 -95
  3. package/dist/cli.js.map +1 -1
  4. package/dist/index.js +39 -29
  5. package/dist/index.js.map +1 -1
  6. package/dist/kernel/index.d.ts +101 -0
  7. package/dist/kernel/index.js +39 -29
  8. package/dist/kernel/index.js.map +1 -1
  9. package/dist/migrations/001_initial.sql +8 -0
  10. package/dist/ui/chunk-5GD2GBPS.js +2190 -0
  11. package/dist/ui/{chunk-4HTOYDCM.js → chunk-5WJRN3LD.js} +1 -1
  12. package/dist/ui/{chunk-3SI3TVER.js → chunk-C2YUQODZ.js} +2 -2
  13. package/dist/ui/{chunk-NGIFGXW7.js → chunk-CFJBTDAA.js} +1 -1
  14. package/dist/ui/{chunk-YWWD62BR.js → chunk-HEJCH7BA.js} +1 -1
  15. package/dist/ui/chunk-HFPA56IM.js +1 -0
  16. package/dist/ui/chunk-HHPSCDLM.js +315 -0
  17. package/dist/ui/chunk-HP375T2O.js +2 -0
  18. package/dist/ui/chunk-HWP3HM55.js +123 -0
  19. package/dist/ui/{chunk-ZAEGBMF7.js → chunk-IUDL3NDH.js} +1 -1
  20. package/dist/ui/{chunk-Z3C2OSRL.js → chunk-JPYAASHN.js} +1 -1
  21. package/dist/ui/chunk-PZ6Q5AOT.js +1 -0
  22. package/dist/ui/chunk-XJL4DZ4M.js +1 -0
  23. package/dist/ui/{chunk-W2JMLJCF.js → chunk-XOHD5XWA.js} +1 -1
  24. package/dist/ui/chunk-YL6SWAFJ.js +1024 -0
  25. package/dist/ui/index.html +2 -2
  26. package/dist/ui/main-7VYTTJP7.js +3 -0
  27. package/dist/ui/{styles-6H4GSOHY.css → styles-HI4A6IWA.css} +1 -1
  28. package/migrations/001_initial.sql +8 -0
  29. package/package.json +2 -2
  30. package/dist/ui/chunk-4X4GYACU.js +0 -123
  31. package/dist/ui/chunk-7Q3IO77R.js +0 -317
  32. package/dist/ui/chunk-FL6RV2IG.js +0 -2
  33. package/dist/ui/chunk-HGNE4UVQ.js +0 -1
  34. package/dist/ui/chunk-IS5ULQSF.js +0 -1
  35. package/dist/ui/chunk-KVWYVO6I.js +0 -1
  36. package/dist/ui/chunk-N4XX4WPE.js +0 -2190
  37. package/dist/ui/chunk-P7TXZKUX.js +0 -2
  38. package/dist/ui/chunk-UVVXMEZT.js +0 -1025
  39. package/dist/ui/main-F7N5RV4Y.js +0 -3
package/dist/cli.js CHANGED
@@ -442,6 +442,19 @@ var claudeProvider = {
442
442
  pluginId: CLAUDE_PLUGIN_ID,
443
443
  kind: "provider",
444
444
  description: "Classifies files under `.claude/{agents,commands,skills}` as Claude Code agents, commands, and skills.",
445
+ // Provider identity for the active-lens dropdown, the topbar lens chip,
446
+ // and the per-node provider chip. Anthropic brand terracotta; the dark
447
+ // variant lifts luminosity for dark mode. Verbatim from the previous
448
+ // static UI catalog (`ui/src/services/provider-ui.ts`).
449
+ presentation: {
450
+ label: "Claude",
451
+ color: "#cc785c",
452
+ colorDark: "#e89270"
453
+ },
454
+ // Auto-detect marker: a `.claude/` directory under the scope root marks
455
+ // a Claude Code project. Provider-owned (replaces the old central
456
+ // detection table in `src/core/config/active-provider.ts`).
457
+ detect: { markers: [".claude"] },
445
458
  // Vendor provider: Claude Code only reads its own `.claude/` territory
446
459
  // and ignores `.codex/` / Antigravity layouts at runtime. Gating the
447
460
  // classifier behind the active lens prevents the walker from inventing
@@ -863,6 +876,18 @@ var antigravityProvider = {
863
876
  pluginId: ANTIGRAVITY_PLUGIN_ID,
864
877
  kind: "provider",
865
878
  description: "Declares the Google Antigravity runtime and its reserved built-in names.",
879
+ // Provider identity for the active-lens dropdown, the topbar lens chip,
880
+ // and the per-node provider chip. Antigravity violet, distinct from the
881
+ // other vendor palettes.
882
+ presentation: {
883
+ label: "Antigravity",
884
+ color: "#7c3aed",
885
+ colorDark: "#a78bfa"
886
+ },
887
+ // No `detect` block: Antigravity has no vendor-specific workspace marker
888
+ // (it adopted the open-standard `.agents/`, owned by `agent-skills`), so
889
+ // it is never auto-suggested. The lens is set manually via
890
+ // `sm config set activeProvider antigravity`.
866
891
  // Vendor provider: marked gated for the day Antigravity grows its own
867
892
  // on-disk kind beyond the open standard. Today `kinds: {}` and
868
893
  // `classify` returns `null` for every path, so the flag is inert; the
@@ -1011,6 +1036,18 @@ var openaiProvider = {
1011
1036
  pluginId: OPENAI_PLUGIN_ID,
1012
1037
  kind: "provider",
1013
1038
  description: "Classifies files under `.codex/agents/*.toml` as OpenAI Codex CLI sub-agents.",
1039
+ // Provider identity for the active-lens dropdown, the topbar lens chip,
1040
+ // and the per-node provider chip. Codex green, distinct from the Claude
1041
+ // palette so the chip reads at a glance.
1042
+ presentation: {
1043
+ label: "OpenAI Codex",
1044
+ color: "#22c55e",
1045
+ colorDark: "#4ade80"
1046
+ },
1047
+ // Auto-detect markers: a `.codex/` directory or a root `AGENTS.md` marks
1048
+ // a Codex CLI project. Provider-owned (replaces the old central
1049
+ // detection table in `src/core/config/active-provider.ts`).
1050
+ detect: { markers: [".codex", "AGENTS.md"] },
1014
1051
  // Vendor provider: Codex CLI only reads its own `.codex/` territory.
1015
1052
  // Gating the classifier behind the active lens keeps the walker from
1016
1053
  // claiming Codex agents under a `claude` (or any other) lens, where
@@ -1068,6 +1105,20 @@ var agentSkillsProvider = {
1068
1105
  pluginId: AGENT_SKILLS_PLUGIN_ID,
1069
1106
  kind: "provider",
1070
1107
  description: "Classifies files under `.agents/skills/<name>/SKILL.md` as Agent Skills.",
1108
+ // Provider identity for the active-lens dropdown, the topbar lens chip,
1109
+ // and the per-node provider chip. Neutral slate (this is the
1110
+ // vendor-agnostic open-standard Provider, not a brand). Verbatim from
1111
+ // the previous static UI catalog (`ui/src/services/provider-ui.ts`).
1112
+ presentation: {
1113
+ label: "Open Skills",
1114
+ color: "#64748b",
1115
+ colorDark: "#94a3b8"
1116
+ },
1117
+ // Auto-detect marker: a `.agents/` directory marks an open-standard
1118
+ // project. This is also the marker a Google/Antigravity project carries
1119
+ // (Antigravity adopted the open standard), so such projects auto-detect
1120
+ // as this universal lens. Provider-owned.
1121
+ detect: { markers: [".agents"] },
1071
1122
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
1072
1123
  kinds: {
1073
1124
  skill: {
@@ -1115,6 +1166,20 @@ var coreMarkdownProvider = {
1115
1166
  pluginId: CORE_PLUGIN_ID,
1116
1167
  kind: "provider",
1117
1168
  description: "Universal `.md` fallback. Claims any markdown file that no vendor-specific provider has classified.",
1169
+ // Provider identity. `hideChip: true` suppresses the per-card provider
1170
+ // chip: this fallback carries the majority of nodes in any project, so
1171
+ // badging every generic `.md` would be noise and dilute the chip's
1172
+ // purpose (signalling a NON-default platform). The Provider still shows
1173
+ // in the active-lens dropdown and the topbar lens chip with this label.
1174
+ presentation: {
1175
+ label: "Markdown",
1176
+ color: "#9ca3af",
1177
+ colorDark: "#6b7280",
1178
+ hideChip: true
1179
+ },
1180
+ // No `detect` block: the universal fallback is never auto-suggested as a
1181
+ // lens (selecting it would gate out every vendor Provider). Operators
1182
+ // pick it manually if they want an open-standard-only view.
1118
1183
  read: { extensions: [".md"], parser: "frontmatter-yaml" },
1119
1184
  // Per spec § A.6, defaultRefreshAction values MUST be qualified
1120
1185
  // action ids. The summarize-markdown action is not yet implemented
@@ -3855,7 +3920,7 @@ var UPDATE_CHECK_TEXTS = {
3855
3920
  // package.json
3856
3921
  var package_default = {
3857
3922
  name: "@skill-map/cli",
3858
- version: "0.40.1",
3923
+ version: "0.42.0",
3859
3924
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
3860
3925
  license: "MIT",
3861
3926
  type: "module",
@@ -4287,40 +4352,40 @@ var updateCheckHook = {
4287
4352
  };
4288
4353
 
4289
4354
  // plugins/built-ins.ts
4290
- var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: "0.40.1" };
4291
- var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: "0.40.1" };
4292
- var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: "0.40.1" };
4293
- var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: "0.40.1" };
4294
- var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: "0.40.1" };
4295
- var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: "0.40.1" };
4296
- var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: "0.40.1" };
4297
- var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: "0.40.1" };
4298
- var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: "0.40.1" };
4299
- var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: "0.40.1" };
4300
- var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: "0.40.1" };
4301
- var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: "0.40.1" };
4302
- var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: "0.40.1" };
4303
- var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: "0.40.1" };
4304
- var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: "0.40.1" };
4305
- var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: "0.40.1" };
4306
- var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: "0.40.1" };
4307
- var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: "0.40.1" };
4308
- var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: "0.40.1" };
4309
- var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: "0.40.1" };
4310
- var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: "0.40.1" };
4311
- var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: "0.40.1" };
4312
- var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: "0.40.1" };
4313
- var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: "0.40.1" };
4314
- var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: "0.40.1" };
4315
- var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: "0.40.1" };
4316
- var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: "0.40.1" };
4317
- var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: "0.40.1" };
4318
- var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: "0.40.1" };
4319
- var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: "0.40.1" };
4320
- var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: "0.40.1" };
4321
- var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: "0.40.1" };
4322
- var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: "0.40.1" };
4323
- var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: "0.40.1" };
4355
+ var claudeProvider2 = { ...claudeProvider, pluginId: "claude", version: "0.42.0" };
4356
+ var atDirectiveExtractor2 = { ...atDirectiveExtractor, pluginId: "claude", version: "0.42.0" };
4357
+ var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", version: "0.42.0" };
4358
+ var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: "0.42.0" };
4359
+ var openaiProvider2 = { ...openaiProvider, pluginId: "openai", version: "0.42.0" };
4360
+ var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: "0.42.0" };
4361
+ var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: "0.42.0" };
4362
+ var annotationsExtractor2 = { ...annotationsExtractor, pluginId: "core", version: "0.42.0" };
4363
+ var externalUrlCounterExtractor2 = { ...externalUrlCounterExtractor, pluginId: "core", version: "0.42.0" };
4364
+ var markdownLinkExtractor2 = { ...markdownLinkExtractor, pluginId: "core", version: "0.42.0" };
4365
+ var mcpToolsExtractor2 = { ...mcpToolsExtractor, pluginId: "core", version: "0.42.0" };
4366
+ var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "core", version: "0.42.0" };
4367
+ var annotationFieldUnknownAnalyzer2 = { ...annotationFieldUnknownAnalyzer, pluginId: "core", version: "0.42.0" };
4368
+ var annotationOrphanAnalyzer2 = { ...annotationOrphanAnalyzer, pluginId: "core", version: "0.42.0" };
4369
+ var annotationStaleAnalyzer2 = { ...annotationStaleAnalyzer, pluginId: "core", version: "0.42.0" };
4370
+ var contributionOrphanAnalyzer2 = { ...contributionOrphanAnalyzer, pluginId: "core", version: "0.42.0" };
4371
+ var issueCounterAnalyzer2 = { ...issueCounterAnalyzer, pluginId: "core", version: "0.42.0" };
4372
+ var jobFileOrphanAnalyzer2 = { ...jobFileOrphanAnalyzer, pluginId: "core", version: "0.42.0" };
4373
+ var linkConflictAnalyzer2 = { ...linkConflictAnalyzer, pluginId: "core", version: "0.42.0" };
4374
+ var linkCounterAnalyzer2 = { ...linkCounterAnalyzer, pluginId: "core", version: "0.42.0" };
4375
+ var linkSelfLoopAnalyzer2 = { ...linkSelfLoopAnalyzer, pluginId: "core", version: "0.42.0" };
4376
+ var nameReservedAnalyzer2 = { ...nameReservedAnalyzer, pluginId: "core", version: "0.42.0" };
4377
+ var nodeStabilityAnalyzer2 = { ...nodeStabilityAnalyzer, pluginId: "core", version: "0.42.0" };
4378
+ var nodeSupersededAnalyzer2 = { ...nodeSupersededAnalyzer, pluginId: "core", version: "0.42.0" };
4379
+ var referenceBrokenAnalyzer2 = { ...referenceBrokenAnalyzer, pluginId: "core", version: "0.42.0" };
4380
+ var referenceRedundantAnalyzer2 = { ...referenceRedundantAnalyzer, pluginId: "core", version: "0.42.0" };
4381
+ var schemaViolationAnalyzer2 = { ...schemaViolationAnalyzer, pluginId: "core", version: "0.42.0" };
4382
+ var signalCollisionAnalyzer2 = { ...signalCollisionAnalyzer, pluginId: "core", version: "0.42.0" };
4383
+ var triggerCollisionAnalyzer2 = { ...triggerCollisionAnalyzer, pluginId: "core", version: "0.42.0" };
4384
+ var asciiFormatter2 = { ...asciiFormatter, pluginId: "core", version: "0.42.0" };
4385
+ var jsonFormatter2 = { ...jsonFormatter, pluginId: "core", version: "0.42.0" };
4386
+ var nodeBumpAction2 = { ...nodeBumpAction, pluginId: "core", version: "0.42.0" };
4387
+ var nodeSupersedeAction2 = { ...nodeSupersedeAction, pluginId: "core", version: "0.42.0" };
4388
+ var updateCheckHook2 = { ...updateCheckHook, pluginId: "core", version: "0.42.0" };
4324
4389
  var builtInBundles = [
4325
4390
  {
4326
4391
  id: "claude",
@@ -4898,6 +4963,7 @@ var defaults_default = {
4898
4963
  strict: false,
4899
4964
  followSymlinks: false,
4900
4965
  maxFileSizeBytes: 1048576,
4966
+ maxNodes: 256,
4901
4967
  watch: {
4902
4968
  debounceMs: 300
4903
4969
  },
@@ -7037,6 +7103,8 @@ async function loadScanResult(db) {
7037
7103
  roots: parseJsonArray(metaRow.rootsJson),
7038
7104
  providers: parseJsonArray(metaRow.providersJson),
7039
7105
  scannedBy,
7106
+ recommendedNodeLimit: metaRow.recommendedNodeLimit,
7107
+ overrideMaxNodes: metaRow.overrideMaxNodes,
7040
7108
  nodes,
7041
7109
  links,
7042
7110
  issues,
@@ -7060,6 +7128,11 @@ async function loadScanResult(db) {
7060
7128
  scannedAt,
7061
7129
  roots: ["."],
7062
7130
  providers: [],
7131
+ // Synthetic envelope, default to the design cap (256) so SPA reads
7132
+ // the same shape across cold-boot and pre-cap-aware DBs. A real
7133
+ // scan overwrites scan_meta with the live values on next run.
7134
+ recommendedNodeLimit: 256,
7135
+ overrideMaxNodes: null,
7063
7136
  nodes,
7064
7137
  links,
7065
7138
  issues,
@@ -7645,7 +7718,14 @@ function metaToRow(result) {
7645
7718
  providersJson: JSON.stringify(result.providers),
7646
7719
  statsFilesWalked: result.stats.filesWalked,
7647
7720
  statsFilesSkipped: result.stats.filesSkipped,
7648
- statsDurationMs: result.stats.durationMs
7721
+ statsDurationMs: result.stats.durationMs,
7722
+ ...projectNodeLimitColumns(result)
7723
+ };
7724
+ }
7725
+ function projectNodeLimitColumns(result) {
7726
+ return {
7727
+ recommendedNodeLimit: result.recommendedNodeLimit ?? 256,
7728
+ overrideMaxNodes: result.overrideMaxNodes ?? null
7649
7729
  };
7650
7730
  }
7651
7731
  function extractorRunToRow(record) {
@@ -10766,21 +10846,8 @@ import { Command as Command4, Option as Option4 } from "clipanion";
10766
10846
  // core/config/active-provider.ts
10767
10847
  import { existsSync as existsSync14 } from "fs";
10768
10848
  import { join as join10 } from "path";
10769
- var DETECTION_RULES = [
10770
- { providerId: "claude", marker: ".claude" },
10771
- // `gemini` retired 2026-05-22: Google replaced the Gemini CLI with the
10772
- // Antigravity CLI (released 2026-05-19; Gemini CLI sunsets 2026-06-18).
10773
- // Antigravity adopted the open-standard `.agents/` instead of a
10774
- // vendor-specific directory, so detection of a Google CLI project
10775
- // falls through to the universal `agent-skills` lens (`.agents/`
10776
- // already classifies via that neutral provider). The lens can still
10777
- // be set manually via `sm config set activeProvider antigravity`.
10778
- { providerId: "openai", marker: ".codex" },
10779
- { providerId: "openai", marker: "AGENTS.md" },
10780
- { providerId: "cursor", marker: ".cursor" }
10781
- ];
10782
- function resolveActiveProvider(cwd) {
10783
- const detected = detectProvidersFromFilesystem(cwd);
10849
+ function resolveActiveProvider(cwd, providers = []) {
10850
+ const detected = detectProvidersFromFilesystem(cwd, providers);
10784
10851
  const fromConfig = readConfigValue("activeProvider", { cwd });
10785
10852
  if (typeof fromConfig === "string" && fromConfig.length > 0) {
10786
10853
  return { resolved: fromConfig, source: "config", detected };
@@ -10790,14 +10857,16 @@ function resolveActiveProvider(cwd) {
10790
10857
  }
10791
10858
  return { resolved: null, source: "none", detected };
10792
10859
  }
10793
- function detectProvidersFromFilesystem(cwd) {
10860
+ function detectProvidersFromFilesystem(cwd, providers) {
10794
10861
  const seen = /* @__PURE__ */ new Set();
10795
10862
  const out = [];
10796
- for (const rule of DETECTION_RULES) {
10797
- if (seen.has(rule.providerId)) continue;
10798
- if (!existsSync14(join10(cwd, rule.marker))) continue;
10799
- seen.add(rule.providerId);
10800
- out.push(rule.providerId);
10863
+ for (const provider of providers) {
10864
+ if (seen.has(provider.id)) continue;
10865
+ const markers = provider.detect?.markers;
10866
+ if (!markers || markers.length === 0) continue;
10867
+ if (!markers.some((marker) => existsSync14(join10(cwd, marker)))) continue;
10868
+ seen.add(provider.id);
10869
+ out.push(provider.id);
10801
10870
  }
10802
10871
  return out;
10803
10872
  }
@@ -10952,7 +11021,7 @@ function suggestConfigKey(effective, typed, ansi) {
10952
11021
  });
10953
11022
  }
10954
11023
  var KNOWN_DEFAULTLESS_KEY_RESOLVERS = {
10955
- activeProvider: (cwd) => resolveActiveProvider(cwd).resolved
11024
+ activeProvider: (cwd) => resolveActiveProvider(cwd, builtIns().providers).resolved
10956
11025
  };
10957
11026
  function parseCliValue(raw) {
10958
11027
  try {
@@ -11349,7 +11418,7 @@ var ConfigSetCommand = class extends SmCommand {
11349
11418
  cwd: ctx.cwd
11350
11419
  });
11351
11420
  if (this.key === "activeProvider" && typeof value === "string") {
11352
- const detected = resolveActiveProvider(ctx.cwd).detected;
11421
+ const detected = resolveActiveProvider(ctx.cwd, builtIns().providers).detected;
11353
11422
  writeConfigValue("activeProviderMarkers", [...detected], {
11354
11423
  target,
11355
11424
  cwd: ctx.cwd
@@ -15673,17 +15742,26 @@ async function walkAndExtract(opts) {
15673
15742
  const walkOptions = opts.ignoreFilter ? { ignoreFilter: opts.ignoreFilter } : {};
15674
15743
  let filesWalked = 0;
15675
15744
  let index = 0;
15745
+ const effectiveMaxNodes = opts.overrideMaxNodes ?? opts.recommendedNodeLimit;
15746
+ let capReached = false;
15676
15747
  const activeProviders = opts.providers.filter((provider) => {
15677
15748
  if (!provider.gatedByActiveLens) return true;
15678
15749
  if (opts.activeProvider === null) return true;
15679
15750
  return provider.id === opts.activeProvider;
15680
15751
  });
15681
- for (const provider of activeProviders) {
15752
+ const advance = async (raw, provider) => {
15753
+ const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
15754
+ if (advanced) index += 1;
15755
+ };
15756
+ outer: for (const provider of activeProviders) {
15682
15757
  for await (const raw of resolveProviderWalk(provider)(opts.roots, walkOptions)) {
15683
15758
  filesWalked += 1;
15684
15759
  if (claimedPaths.has(raw.path)) continue;
15685
- const advanced = await processRawNode(raw, provider, wctx, accum, claimedPaths, index + 1);
15686
- if (advanced) index += 1;
15760
+ if (accum.nodes.length >= effectiveMaxNodes) {
15761
+ capReached = true;
15762
+ break outer;
15763
+ }
15764
+ await advance(raw, provider);
15687
15765
  }
15688
15766
  }
15689
15767
  const orphanSidecars = discoverOrphanSidecars(opts.roots);
@@ -15694,6 +15772,9 @@ async function walkAndExtract(opts) {
15694
15772
  cachedPaths: accum.cachedPaths,
15695
15773
  frontmatterIssues: accum.frontmatterIssues,
15696
15774
  filesWalked,
15775
+ recommendedNodeLimit: opts.recommendedNodeLimit,
15776
+ overrideMaxNodes: opts.overrideMaxNodes,
15777
+ capReached,
15697
15778
  enrichments: [...accum.enrichmentBuffer.values()],
15698
15779
  extractorRuns: accum.extractorRuns,
15699
15780
  contributions: accum.contributionsBuffer,
@@ -15957,7 +16038,11 @@ async function runScanInternal(_kernel, options) {
15957
16038
  const scanStartedEvent = makeEvent("scan.started", { roots: options.roots });
15958
16039
  emitter.emit(scanStartedEvent);
15959
16040
  await hookDispatcher.dispatch("scan.started", scanStartedEvent);
15960
- const activeProviderId = resolveActiveProviderOption(options.activeProvider, options.roots);
16041
+ const activeProviderId = resolveActiveProviderOption(
16042
+ options.activeProvider,
16043
+ options.roots,
16044
+ exts.providers
16045
+ );
15961
16046
  const walked = await walkAndExtract({
15962
16047
  providers: exts.providers,
15963
16048
  extractors: exts.extractors,
@@ -15972,7 +16057,9 @@ async function runScanInternal(_kernel, options) {
15972
16057
  priorExtractorRuns: setup.priorExtractorRuns,
15973
16058
  providerFrontmatter: setup.providerFrontmatter,
15974
16059
  pluginStores: options.pluginStores,
15975
- activeProvider: activeProviderId
16060
+ activeProvider: activeProviderId,
16061
+ recommendedNodeLimit: options.recommendedNodeLimit ?? 256,
16062
+ overrideMaxNodes: options.overrideMaxNodes ?? null
15976
16063
  });
15977
16064
  const activeProvider = activeProviderId ? exts.providers.find((p) => p.id === activeProviderId) ?? null : null;
15978
16065
  const resolved = resolveSignals({
@@ -16140,6 +16227,8 @@ function buildScanReturn(walked, issues, renameOps, stats, options, setup) {
16140
16227
  roots: options.roots,
16141
16228
  providers: setup.exts.providers.map((a) => a.id),
16142
16229
  scannedBy: SCANNED_BY,
16230
+ recommendedNodeLimit: walked.recommendedNodeLimit,
16231
+ overrideMaxNodes: walked.overrideMaxNodes,
16143
16232
  nodes: walked.nodes,
16144
16233
  links: walked.internalLinks,
16145
16234
  issues,
@@ -16162,12 +16251,12 @@ function validateRoots(roots) {
16162
16251
  }
16163
16252
  }
16164
16253
  }
16165
- function resolveActiveProviderOption(optionValue, roots) {
16254
+ function resolveActiveProviderOption(optionValue, roots, providers) {
16166
16255
  if (optionValue !== void 0) return optionValue;
16167
16256
  for (const root of roots) {
16168
16257
  const absRoot = isAbsolute7(root) ? root : resolve28(root);
16169
16258
  if (!existsSync21(absRoot)) continue;
16170
- const detected = resolveActiveProvider(absRoot).resolved;
16259
+ const detected = resolveActiveProvider(absRoot, providers).resolved;
16171
16260
  if (detected !== null) return detected;
16172
16261
  }
16173
16262
  return null;
@@ -16636,17 +16725,23 @@ function safeStat(path) {
16636
16725
  import { createInterface as createInterface2 } from "readline";
16637
16726
  import { isAbsolute as isAbsolute9, join as join16 } from "path";
16638
16727
  async function bootstrapActiveProvider(opts) {
16639
- const fromCwd = resolveActiveProvider(opts.cwd);
16728
+ const fromCwd = resolveActiveProvider(opts.cwd, opts.providers);
16640
16729
  if (fromCwd.source === "config") {
16641
16730
  const currentMarkers = aggregateDetected(
16642
16731
  opts.cwd,
16643
16732
  opts.effectiveRoots,
16644
- fromCwd.detected
16733
+ fromCwd.detected,
16734
+ opts.providers
16645
16735
  );
16646
16736
  handleDrift(opts, fromCwd.resolved, currentMarkers);
16647
16737
  return { kind: "ok", activeProvider: fromCwd.resolved, source: "config" };
16648
16738
  }
16649
- const detected = aggregateDetected(opts.cwd, opts.effectiveRoots, fromCwd.detected);
16739
+ const detected = aggregateDetected(
16740
+ opts.cwd,
16741
+ opts.effectiveRoots,
16742
+ fromCwd.detected,
16743
+ opts.providers
16744
+ );
16650
16745
  if (detected.length === 0) {
16651
16746
  const warnGlyph = opts.style?.warnGlyph ?? "\u26A0";
16652
16747
  const dim = opts.style?.dim ?? ((s) => s);
@@ -16678,7 +16773,7 @@ async function bootstrapActiveProvider(opts) {
16678
16773
  persistActiveProvider(opts.cwd, picked, detected, opts.printer);
16679
16774
  return { kind: "ok", activeProvider: picked, source: "autodetect" };
16680
16775
  }
16681
- function aggregateDetected(cwd, effectiveRoots, cwdDetected) {
16776
+ function aggregateDetected(cwd, effectiveRoots, cwdDetected, providers) {
16682
16777
  const out = [];
16683
16778
  const seen = /* @__PURE__ */ new Set();
16684
16779
  for (const id of cwdDetected) {
@@ -16688,7 +16783,7 @@ function aggregateDetected(cwd, effectiveRoots, cwdDetected) {
16688
16783
  }
16689
16784
  for (const root of effectiveRoots) {
16690
16785
  const absRoot = isAbsolute9(root) ? root : join16(cwd, root);
16691
- const r = resolveActiveProvider(absRoot);
16786
+ const r = resolveActiveProvider(absRoot, providers);
16692
16787
  for (const id of r.detected) {
16693
16788
  if (seen.has(id)) continue;
16694
16789
  seen.add(id);
@@ -16818,7 +16913,13 @@ async function runScanForCommand(opts) {
16818
16913
  }
16819
16914
  const loadPrior = makePriorLoader(opts.noBuiltIns, strict);
16820
16915
  const jobsDir = defaultProjectJobsDir(ctx);
16821
- const lens = await resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime);
16916
+ const lens = await resolveActiveLens(
16917
+ opts,
16918
+ ctx,
16919
+ effectiveRoots,
16920
+ pluginRuntime,
16921
+ detectionProviders(extensions)
16922
+ );
16822
16923
  if (lens.kind === "ambiguous-provider") return lens;
16823
16924
  const activeProvider = lens.activeProvider;
16824
16925
  const runScanWith = makeScanRunner(
@@ -16830,15 +16931,20 @@ async function runScanForCommand(opts) {
16830
16931
  extensions,
16831
16932
  referenceablePaths,
16832
16933
  ctx.cwd,
16833
- activeProvider
16934
+ activeProvider,
16935
+ cfg.scan.maxNodes
16834
16936
  );
16835
16937
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
16836
16938
  return willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
16837
16939
  }
16838
- async function resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime) {
16940
+ function detectionProviders(extensions) {
16941
+ return extensions?.providers ?? [];
16942
+ }
16943
+ async function resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime, providers) {
16839
16944
  const bootstrap = await bootstrapActiveProvider({
16840
16945
  cwd: ctx.cwd,
16841
16946
  effectiveRoots,
16947
+ providers,
16842
16948
  yes: opts.yes ?? false,
16843
16949
  stdin: opts.stdin ?? process.stdin,
16844
16950
  stderr: opts.stderr,
@@ -16931,7 +17037,7 @@ function makePriorLoader(noBuiltIns, strict) {
16931
17037
  return loaded;
16932
17038
  };
16933
17039
  }
16934
- function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider) {
17040
+ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider, recommendedNodeLimit) {
16935
17041
  return async (prior, priorExtractorRuns, orphanJobFiles) => {
16936
17042
  if (opts.changed && prior === null) {
16937
17043
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
@@ -16946,6 +17052,7 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
16946
17052
  cwd: scanCwd,
16947
17053
  prior,
16948
17054
  activeProvider,
17055
+ recommendedNodeLimit,
16949
17056
  ...priorExtractorRuns ? { priorExtractorRuns } : {},
16950
17057
  ...orphanJobFiles ? { orphanJobFiles } : {}
16951
17058
  });
@@ -16959,15 +17066,15 @@ function buildRunScanOptions(args2) {
16959
17066
  tokenize: !opts.noTokens,
16960
17067
  ignoreFilter: args2.ignoreFilter,
16961
17068
  strict: args2.strict,
16962
- emitter: opts.emitterFactory ? opts.emitterFactory() : createStderrProgressEmitter(opts.stderr, {
16963
- colorEnabled: opts.colorEnabled === true
16964
- }),
17069
+ emitter: buildRunScanEmitter(opts),
16965
17070
  // Orphan job-file detection, empty list means "no orphans
16966
17071
  // visible from this caller" (legacy behaviour). The orchestrator
16967
17072
  // defaults to `[]` when the field is absent; we always pass the
16968
17073
  // array (possibly empty) to keep the wiring uniform.
16969
17074
  orphanJobFiles: orphanJobFiles ?? [],
16970
- activeProvider: args2.activeProvider
17075
+ activeProvider: args2.activeProvider,
17076
+ recommendedNodeLimit: args2.recommendedNodeLimit,
17077
+ overrideMaxNodes: opts.maxNodes ?? null
16971
17078
  };
16972
17079
  if (args2.extensions) runOptions.extensions = args2.extensions;
16973
17080
  if (prior) {
@@ -16979,6 +17086,12 @@ function buildRunScanOptions(args2) {
16979
17086
  runOptions.cwd = args2.cwd;
16980
17087
  return runOptions;
16981
17088
  }
17089
+ function buildRunScanEmitter(opts) {
17090
+ if (opts.emitterFactory) return opts.emitterFactory();
17091
+ return createStderrProgressEmitter(opts.stderr, {
17092
+ colorEnabled: opts.colorEnabled === true
17093
+ });
17094
+ }
16982
17095
  async function runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) {
16983
17096
  let outcome;
16984
17097
  try {
@@ -21004,6 +21117,22 @@ var SCAN_TEXTS = {
21004
21117
  persistedTo: " {{dbPath}}\n",
21005
21118
  /** Body line for dry-run mode, same indent, marker tail. */
21006
21119
  wouldPersist: " would persist to {{dbPath}} (dry-run)\n",
21120
+ /**
21121
+ * Cap-hit notice, printed when the walker stopped accepting nodes
21122
+ * because `--max-nodes` (or the `scan.maxNodes` setting) was reached.
21123
+ * `{{glyph}}` is the yellow warning glyph, `{{limit}}` the effective
21124
+ * cap, `{{source}}` either `--max-nodes` or `scan.maxNodes`. The hint
21125
+ * names both escape routes the user has: trimming `.skillmapignore`
21126
+ * (preferred) or raising the cap with `--max-nodes <N>`.
21127
+ */
21128
+ scanCappedNotice: "{{glyph}} Scan capped at {{limit}} nodes ({{source}}).\n {{hint}}\n",
21129
+ scanCappedNoticeHint: "Trim .skillmapignore to exclude noisy paths (preferred), or re-run with --max-nodes <N> to raise the cap. Past the recommended limit the graph is hard to read and analyzer signal drops.",
21130
+ /**
21131
+ * Validation message for an invalid `--max-nodes` value. Surfaced as a
21132
+ * §3.1b two-line block.
21133
+ */
21134
+ maxNodesInvalid: "{{glyph}} --max-nodes must be an integer >= 1 (got `{{value}}`).\n {{hint}}\n",
21135
+ maxNodesInvalidHint: "Pass a positive integer, e.g. --max-nodes 256.",
21007
21136
  // --- scan compare-with sub-verb --------------------------------------
21008
21137
  compareErrorPrefix: "sm scan compare-with: {{message}}\n",
21009
21138
  compareDumpNotFound: "dump file not found: {{path}}",
@@ -21167,7 +21296,9 @@ function createWatcherRuntime(opts) {
21167
21296
  tokenize,
21168
21297
  ignoreFilter,
21169
21298
  strict,
21170
- emitter
21299
+ emitter,
21300
+ recommendedNodeLimit: cfg.scan.maxNodes,
21301
+ overrideMaxNodes: opts.maxNodesOverride ?? null
21171
21302
  };
21172
21303
  if (cfg.scan.referencePaths.length > 0) {
21173
21304
  const walk2 = walkReferencePaths(cfg.scan.referencePaths, cwd);
@@ -21380,7 +21511,12 @@ var WATCH_TEXTS = {
21380
21511
  * accepted shape so the operator can re-run without `--help`.
21381
21512
  */
21382
21513
  maxConsecutiveFailuresInvalid: "{{glyph}} sm watch: --max-consecutive-failures must be a non-negative integer (got {{raw}}).\n {{hint}}\n",
21383
- maxConsecutiveFailuresInvalidHint: "Pass an integer >= 0 (0 disables the circuit-breaker; the default is 5)."
21514
+ maxConsecutiveFailuresInvalidHint: "Pass an integer >= 0 (0 disables the circuit-breaker; the default is 5).",
21515
+ /**
21516
+ * §3.1b two-line block. Validation rejection for `--max-nodes`.
21517
+ */
21518
+ maxNodesInvalid: "{{glyph}} sm watch: --max-nodes must be an integer >= 1 (got {{raw}}).\n {{hint}}\n",
21519
+ maxNodesInvalidHint: "Pass a positive integer, e.g. --max-nodes 256."
21384
21520
  };
21385
21521
 
21386
21522
  // cli/commands/watch.ts
@@ -21443,6 +21579,7 @@ async function runWatchLoop(opts) {
21443
21579
  circuitBreaker: { maxConsecutiveFailures: breakerLimit },
21444
21580
  killSwitches: readConformanceKillSwitches(),
21445
21581
  ...opts.maxBatches !== void 0 ? { maxBatches: opts.maxBatches } : {},
21582
+ ...opts.maxNodes !== void 0 ? { maxNodesOverride: opts.maxNodes } : {},
21446
21583
  events: {
21447
21584
  onBatch: (outcome) => {
21448
21585
  if (outcome.kind === "ok") {
@@ -21556,6 +21693,10 @@ var WatchCommand = class extends SmCommand {
21556
21693
  required: false,
21557
21694
  description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
21558
21695
  });
21696
+ maxNodes = Option28.String("--max-nodes", {
21697
+ required: false,
21698
+ description: "Per-batch override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When a batch hits the cap, additional files are dropped and the UI surfaces the persistent oversized banner. Validation: integer >= 1."
21699
+ });
21559
21700
  // Long-running verb, the watcher prints its own "stopped" line on
21560
21701
  // SIGINT / SIGTERM. Adding `done in <…>` after that would be noise.
21561
21702
  emitElapsed = false;
@@ -21563,6 +21704,8 @@ var WatchCommand = class extends SmCommand {
21563
21704
  const roots = this.roots.length > 0 ? this.roots : ["."];
21564
21705
  const breaker = parseBreakerLimit(this.maxConsecutiveFailures, this.context.stderr, this.noColor);
21565
21706
  if (breaker === null) return ExitCode.Error;
21707
+ const maxNodes = parseMaxNodesLimit(this.maxNodes, this.context.stderr, this.noColor);
21708
+ if (maxNodes === null) return ExitCode.Error;
21566
21709
  const watchOpts = {
21567
21710
  roots,
21568
21711
  json: this.json,
@@ -21575,6 +21718,7 @@ var WatchCommand = class extends SmCommand {
21575
21718
  printer: this.printer
21576
21719
  };
21577
21720
  if (breaker !== void 0) watchOpts.maxConsecutiveFailures = breaker;
21721
+ if (maxNodes !== void 0) watchOpts.maxNodes = maxNodes;
21578
21722
  return runWatchLoop(watchOpts);
21579
21723
  }
21580
21724
  };
@@ -21595,6 +21739,23 @@ function parseBreakerLimit(raw, stderr, noColor) {
21595
21739
  }
21596
21740
  return parsed;
21597
21741
  }
21742
+ function parseMaxNodesLimit(raw, stderr, noColor) {
21743
+ if (raw === void 0) return void 0;
21744
+ const n = Number(raw);
21745
+ if (!Number.isInteger(n) || n < 1) {
21746
+ const stderrTty = stderr;
21747
+ const ansi = ansiFor({ isTTY: stderrTty.isTTY === true, noColorFlag: noColor });
21748
+ stderr.write(
21749
+ tx(WATCH_TEXTS.maxNodesInvalid, {
21750
+ glyph: ansi.red("\u2715"),
21751
+ raw,
21752
+ hint: ansi.dim(WATCH_TEXTS.maxNodesInvalidHint)
21753
+ })
21754
+ );
21755
+ return null;
21756
+ }
21757
+ return n;
21758
+ }
21598
21759
 
21599
21760
  // cli/commands/scan.ts
21600
21761
  var ScanCommand = class extends SmCommand {
@@ -21662,10 +21823,16 @@ var ScanCommand = class extends SmCommand {
21662
21823
  yes = Option29.Boolean("--yes", false, {
21663
21824
  description: "Non-interactive mode for ambiguous activeProvider auto-detect. With `--yes`, multiple provider markers (.claude/, .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."
21664
21825
  });
21826
+ maxNodes = Option29.String("--max-nodes", {
21827
+ required: false,
21828
+ description: "Per-invocation override of `scan.maxNodes` (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When the walker hits the cap, additional files are dropped and the scan is marked oversized in scan_meta (the UI raises a persistent banner pointing at the .skillmapignore editor in Settings \u2192 Project). Validation: integer >= 1."
21829
+ });
21665
21830
  // Each branch in the orchestrator maps to one validation gate
21666
21831
  // (--watch alias / --changed mutex / -g mutex / dispatch).
21667
21832
  // Splitting per branch scatters the gate from the value it gates.
21668
21833
  async run() {
21834
+ const parsedMaxNodes = this.parseMaxNodesFlag();
21835
+ if (parsedMaxNodes.kind === "error") return parsedMaxNodes.exit;
21669
21836
  if (this.watch) return this.runWatchAlias();
21670
21837
  if (this.changed && this.noBuiltIns) {
21671
21838
  const ansi = this.ansiFor("stderr");
@@ -21701,10 +21868,33 @@ var ScanCommand = class extends SmCommand {
21701
21868
  killSwitches: readConformanceKillSwitches(),
21702
21869
  colorEnabled,
21703
21870
  yes: this.yes,
21704
- style
21871
+ style,
21872
+ ...parsedMaxNodes.value !== void 0 ? { maxNodes: parsedMaxNodes.value } : {}
21705
21873
  });
21706
21874
  return outcome.kind === "ok" ? this.renderOutcome(outcome.result, outcome.persistedTo, outcome.dbPath, outcome.strict) : this.renderFailure(outcome);
21707
21875
  }
21876
+ /**
21877
+ * Parse `--max-nodes <N>`. Returns either the integer value (or
21878
+ * `undefined` when the flag was omitted) or an error sentinel after
21879
+ * printing the validation block. Invalid (non-integer, < 1) exits 2
21880
+ * per spec/cli-contract.md §Node cap.
21881
+ */
21882
+ parseMaxNodesFlag() {
21883
+ if (this.maxNodes === void 0) return { kind: "ok", value: void 0 };
21884
+ const n = Number(this.maxNodes);
21885
+ if (!Number.isInteger(n) || n < 1) {
21886
+ const ansi = this.ansiFor("stderr");
21887
+ this.printer.info(
21888
+ tx(SCAN_TEXTS.maxNodesInvalid, {
21889
+ glyph: ansi.red("\u2715"),
21890
+ value: this.maxNodes,
21891
+ hint: ansi.dim(SCAN_TEXTS.maxNodesInvalidHint)
21892
+ })
21893
+ );
21894
+ return { kind: "error", exit: ExitCode.Error };
21895
+ }
21896
+ return { kind: "ok", value: n };
21897
+ }
21708
21898
  /**
21709
21899
  * `--watch` is a thin alias for the `sm watch` verb. Combining
21710
21900
  * `--watch` with one-shot-only flags is incoherent, the watcher
@@ -21724,6 +21914,7 @@ var ScanCommand = class extends SmCommand {
21724
21914
  }
21725
21915
  this.emitElapsed = false;
21726
21916
  const roots = this.roots.length > 0 ? this.roots : ["."];
21917
+ const parsedMaxNodes = this.parseMaxNodesFlag();
21727
21918
  return runWatchLoop({
21728
21919
  roots,
21729
21920
  json: this.json,
@@ -21733,7 +21924,8 @@ var ScanCommand = class extends SmCommand {
21733
21924
  db: this.db,
21734
21925
  noPlugins: this.noPlugins,
21735
21926
  context: this.context,
21736
- printer: this.printer
21927
+ printer: this.printer,
21928
+ ...parsedMaxNodes.kind === "ok" && parsedMaxNodes.value !== void 0 ? { maxNodes: parsedMaxNodes.value } : {}
21737
21929
  });
21738
21930
  }
21739
21931
  /**
@@ -21820,8 +22012,32 @@ var ScanCommand = class extends SmCommand {
21820
22012
  })
21821
22013
  );
21822
22014
  }
22015
+ this.maybePrintCapNotice(result, ansi);
21823
22016
  return exitCode2;
21824
22017
  }
22018
+ /**
22019
+ * Surface the §Node cap notice when the walker actually stopped
22020
+ * accepting files because of the cap. Derivation: `filesWalked >
22021
+ * effectiveLimit` means the walker incremented past the cap at least
22022
+ * once (i.e. classified the (limit+1)-th raw before breaking). When
22023
+ * the project has EXACTLY the cap many files the loop ends naturally
22024
+ * without ever firing the break, so the notice stays silent.
22025
+ */
22026
+ maybePrintCapNotice(result, ansi) {
22027
+ const recommended = result.recommendedNodeLimit;
22028
+ if (recommended === void 0) return;
22029
+ const override = result.overrideMaxNodes ?? null;
22030
+ const effectiveLimit = override ?? recommended;
22031
+ if (result.stats.filesWalked <= effectiveLimit) return;
22032
+ this.printer.info(
22033
+ tx(SCAN_TEXTS.scanCappedNotice, {
22034
+ glyph: ansi.yellow("\u26A0"),
22035
+ limit: String(effectiveLimit),
22036
+ source: override !== null ? "--max-nodes" : "scan.maxNodes",
22037
+ hint: ansi.dim(SCAN_TEXTS.scanCappedNoticeHint)
22038
+ })
22039
+ );
22040
+ }
21825
22041
  /**
21826
22042
  * `--json` output path. Under `--strict` (H4) self-validates the
21827
22043
  * ScanResult against `scan-result.schema.json` before emitting it,
@@ -22706,15 +22922,17 @@ function buildListEnvelope(opts) {
22706
22922
  filters: opts.filters,
22707
22923
  counts,
22708
22924
  kindRegistry: opts.kindRegistry,
22925
+ providerRegistry: opts.providerRegistry,
22709
22926
  contributionsRegistry: opts.contributionsRegistry
22710
22927
  };
22711
22928
  }
22712
- function buildValueEnvelope(kind, value, kindRegistry, contributionsRegistry) {
22929
+ function buildValueEnvelope(kind, value, kindRegistry, providerRegistry, contributionsRegistry) {
22713
22930
  return {
22714
22931
  schemaVersion: REST_ENVELOPE_SCHEMA_VERSION,
22715
22932
  kind,
22716
22933
  value,
22717
22934
  kindRegistry,
22935
+ providerRegistry,
22718
22936
  contributionsRegistry
22719
22937
  };
22720
22938
  }
@@ -22731,7 +22949,15 @@ function registerConfigRoute(app, deps) {
22731
22949
  for (const warn of loaded.warnings) {
22732
22950
  log.warn(sanitizeForTerminal(warn));
22733
22951
  }
22734
- return c.json(buildValueEnvelope("config", loaded.effective, deps.kindRegistry, deps.contributionsRegistry));
22952
+ return c.json(
22953
+ buildValueEnvelope(
22954
+ "config",
22955
+ loaded.effective,
22956
+ deps.kindRegistry,
22957
+ deps.providerRegistry,
22958
+ deps.contributionsRegistry
22959
+ )
22960
+ );
22735
22961
  });
22736
22962
  }
22737
22963
 
@@ -22929,6 +23155,7 @@ function registerIssuesRoute(app, deps) {
22929
23155
  total: result?.total ?? 0,
22930
23156
  page: { offset: inputs.filter.offset, limit: inputs.filter.limit },
22931
23157
  kindRegistry: deps.kindRegistry,
23158
+ providerRegistry: deps.providerRegistry,
22932
23159
  contributionsRegistry: deps.contributionsRegistry
22933
23160
  })
22934
23161
  );
@@ -22991,6 +23218,7 @@ function registerLinksRoute(app, deps) {
22991
23218
  },
22992
23219
  total: filtered.length,
22993
23220
  kindRegistry: deps.kindRegistry,
23221
+ providerRegistry: deps.providerRegistry,
22994
23222
  contributionsRegistry: deps.contributionsRegistry
22995
23223
  })
22996
23224
  );
@@ -23155,6 +23383,7 @@ function registerNodesRoutes(app, deps) {
23155
23383
  links: { incoming: bundle.linksIn, outgoing: bundle.linksOut },
23156
23384
  issues: bundle.issues,
23157
23385
  kindRegistry: deps.kindRegistry,
23386
+ providerRegistry: deps.providerRegistry,
23158
23387
  contributionsRegistry: deps.contributionsRegistry
23159
23388
  });
23160
23389
  });
@@ -23219,6 +23448,7 @@ function registerNodesRoutes(app, deps) {
23219
23448
  total,
23220
23449
  page: { offset, limit },
23221
23450
  kindRegistry: deps.kindRegistry,
23451
+ providerRegistry: deps.providerRegistry,
23222
23452
  contributionsRegistry: deps.contributionsRegistry
23223
23453
  })
23224
23454
  );
@@ -23373,6 +23603,7 @@ function registerPluginsRoute(app, deps) {
23373
23603
  filters: {},
23374
23604
  total: items.length,
23375
23605
  kindRegistry: deps.kindRegistry,
23606
+ providerRegistry: deps.providerRegistry,
23376
23607
  contributionsRegistry: deps.contributionsRegistry
23377
23608
  })
23378
23609
  );
@@ -23589,6 +23820,7 @@ function projectListResponse(c, deps, overrides) {
23589
23820
  filters: {},
23590
23821
  total: items.length,
23591
23822
  kindRegistry: deps.kindRegistry,
23823
+ providerRegistry: deps.providerRegistry,
23592
23824
  contributionsRegistry: deps.contributionsRegistry
23593
23825
  })
23594
23826
  );
@@ -24129,7 +24361,7 @@ function registerActiveProviderRoute(app, deps) {
24129
24361
  });
24130
24362
  }
24131
24363
  function buildEnvelope4(deps) {
24132
- const r = resolveActiveProvider(deps.runtimeContext.cwd);
24364
+ const r = resolveActiveProvider(deps.runtimeContext.cwd, deps.providers);
24133
24365
  return {
24134
24366
  activeProvider: r.resolved,
24135
24367
  detected: r.detected,
@@ -24285,6 +24517,9 @@ function createWatcherService(opts) {
24285
24517
  if (opts.debounceMsOverride !== void 0) {
24286
24518
  runtimeOpts.debounceMsOverride = opts.debounceMsOverride;
24287
24519
  }
24520
+ if (opts.options.maxNodes !== void 0) {
24521
+ runtimeOpts.maxNodesOverride = opts.options.maxNodes;
24522
+ }
24288
24523
  return runtimeOpts;
24289
24524
  };
24290
24525
  return {
@@ -24362,7 +24597,11 @@ async function runPersistedScan(c, deps) {
24362
24597
  // BFF has no TTY; ambiguous activeProvider must be resolved by
24363
24598
  // the operator via the Settings UI (PATCH /api/active-provider)
24364
24599
  // before the scan, not via interactive prompt here.
24365
- yes: true
24600
+ yes: true,
24601
+ // `--max-nodes` from the `sm serve` invocation (or the bare
24602
+ // `sm --max-nodes <N>` shortcut) flows through to every scan
24603
+ // the BFF runs so the override is honoured end-to-end.
24604
+ ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
24366
24605
  });
24367
24606
  if (outcome.kind !== "ok") {
24368
24607
  throw new HTTPException13(500, {
@@ -24466,7 +24705,10 @@ async function runFreshScan(deps) {
24466
24705
  printer: bffScanRunnerPrinter,
24467
24706
  // BFF has no TTY; ambiguous activeProvider is the operator's
24468
24707
  // problem to resolve via the Settings UI, not via prompt here.
24469
- yes: true
24708
+ yes: true,
24709
+ // Carry `--max-nodes` from `sm serve` into the fresh-scan path
24710
+ // too so a UI-driven refresh honours the same cap as the watcher.
24711
+ ...deps.options.maxNodes !== void 0 ? { maxNodes: deps.options.maxNodes } : {}
24470
24712
  });
24471
24713
  if (outcome.kind !== "ok") {
24472
24714
  throw new HTTPException13(500, {
@@ -24488,6 +24730,12 @@ function emptyScanResult() {
24488
24730
  scannedAt: Date.now(),
24489
24731
  roots: ["."],
24490
24732
  providers: [],
24733
+ // Surface the design default so the SPA reads the same field shape
24734
+ // on cold boot as on populated DBs. 256 mirrors `scan.maxNodes`
24735
+ // from `src/config/defaults.json`; the temporary testing default
24736
+ // (2) only applies after a real scan walks through the kernel.
24737
+ recommendedNodeLimit: 256,
24738
+ overrideMaxNodes: null,
24491
24739
  nodes: [],
24492
24740
  links: [],
24493
24741
  issues: [],
@@ -24861,6 +25109,8 @@ function createApp(deps) {
24861
25109
  options: deps.options,
24862
25110
  runtimeContext: deps.runtimeContext,
24863
25111
  kindRegistry: deps.kindRegistry,
25112
+ providerRegistry: deps.providerRegistry,
25113
+ providers: deps.providers,
24864
25114
  contributionsRegistry: deps.contributionsRegistry,
24865
25115
  pluginRuntime: deps.pluginRuntime,
24866
25116
  configService,
@@ -25156,6 +25406,25 @@ function buildKindRegistry(providers) {
25156
25406
  return registry;
25157
25407
  }
25158
25408
 
25409
+ // server/provider-registry.ts
25410
+ function buildProviderRegistry(providers) {
25411
+ const registry = {};
25412
+ for (const provider of providers) {
25413
+ const ui = provider.presentation;
25414
+ if (!ui) continue;
25415
+ const entry = {
25416
+ label: ui.label,
25417
+ color: ui.color
25418
+ };
25419
+ if (ui.colorDark !== void 0) entry.colorDark = ui.colorDark;
25420
+ if (ui.emoji !== void 0) entry.emoji = ui.emoji;
25421
+ if (ui.icon !== void 0) entry.icon = ui.icon;
25422
+ if (ui.hideChip !== void 0) entry.hideChip = ui.hideChip;
25423
+ registry[provider.id] = entry;
25424
+ }
25425
+ return registry;
25426
+ }
25427
+
25159
25428
  // server/contributions-registry.ts
25160
25429
  function buildContributionsRegistry(kernel) {
25161
25430
  const registry = {};
@@ -25200,6 +25469,8 @@ function validateServerOptions(input) {
25200
25469
  if (watcherError !== null) return { ok: false, error: watcherError };
25201
25470
  const debounceError = validateWatcherDebounce(input.watcherDebounceMs);
25202
25471
  if (debounceError !== null) return { ok: false, error: debounceError };
25472
+ const maxNodesError = validateMaxNodes(input.maxNodes);
25473
+ if (maxNodesError !== null) return { ok: false, error: maxNodesError };
25203
25474
  const noUiError = validateNoUi(filled.noUi, filled.uiDist);
25204
25475
  if (noUiError !== null) return { ok: false, error: noUiError };
25205
25476
  const options = {
@@ -25217,6 +25488,9 @@ function validateServerOptions(input) {
25217
25488
  if (input.watcherDebounceMs !== void 0) {
25218
25489
  options.watcherDebounceMs = input.watcherDebounceMs;
25219
25490
  }
25491
+ if (input.maxNodes !== void 0) {
25492
+ options.maxNodes = input.maxNodes;
25493
+ }
25220
25494
  return { ok: true, options };
25221
25495
  }
25222
25496
  function applyDefaults(input) {
@@ -25277,6 +25551,17 @@ function validateWatcherDebounce(value) {
25277
25551
  }
25278
25552
  return null;
25279
25553
  }
25554
+ function validateMaxNodes(value) {
25555
+ if (value === void 0) return null;
25556
+ if (!Number.isInteger(value) || value < 1) {
25557
+ return {
25558
+ code: "max-nodes-invalid",
25559
+ message: `--max-nodes must be an integer >= 1 (got ${value})`,
25560
+ value: String(value)
25561
+ };
25562
+ }
25563
+ return null;
25564
+ }
25280
25565
  function validateNoUi(noUi, uiDist) {
25281
25566
  if (noUi && uiDist !== null) {
25282
25567
  return {
@@ -25351,7 +25636,7 @@ async function createServer(options, extra = {}) {
25351
25636
  const specVersion = await resolveSpecVersion2();
25352
25637
  const runtimeContext = extra.runtimeContext ?? defaultRuntimeContext();
25353
25638
  const broadcaster = new WsBroadcaster();
25354
- const { pluginRuntime, kindRegistry } = await assemblePluginRuntime(options, runtimeContext);
25639
+ const { pluginRuntime, kindRegistry, providerRegistry, providers } = await assemblePluginRuntime(options, runtimeContext);
25355
25640
  const { kernel, contributionsRegistry } = assembleKernel(pluginRuntime, options.noBuiltIns);
25356
25641
  const watcherHolder = { current: null };
25357
25642
  const app = createApp({
@@ -25360,6 +25645,8 @@ async function createServer(options, extra = {}) {
25360
25645
  broadcaster,
25361
25646
  runtimeContext,
25362
25647
  kindRegistry,
25648
+ providerRegistry,
25649
+ providers,
25363
25650
  contributionsRegistry,
25364
25651
  pluginRuntime,
25365
25652
  watcherHolder,
@@ -25418,11 +25705,10 @@ async function assemblePluginRuntime(options, runtimeContext) {
25418
25705
  log.warn(sanitizeForTerminal(warn));
25419
25706
  }
25420
25707
  const builtInProviders = options.noBuiltIns ? [] : collectBuiltInProviders();
25421
- const kindRegistry = buildKindRegistry([
25422
- ...builtInProviders,
25423
- ...pluginRuntime.extensions.providers
25424
- ]);
25425
- return { pluginRuntime, kindRegistry };
25708
+ const allProviders = [...builtInProviders, ...pluginRuntime.extensions.providers];
25709
+ const kindRegistry = buildKindRegistry(allProviders);
25710
+ const providerRegistry = buildProviderRegistry(allProviders);
25711
+ return { pluginRuntime, kindRegistry, providerRegistry, providers: allProviders };
25426
25712
  }
25427
25713
  function assembleKernel(pluginRuntime, noBuiltIns) {
25428
25714
  const kernel = createKernel();
@@ -25556,6 +25842,12 @@ var SERVE_TEXTS = {
25556
25842
  */
25557
25843
  watcherDebounceInvalid: "{{glyph}} sm serve: --watcher-debounce-ms must be a non-negative integer (got {{value}}).\n {{hint}}\n",
25558
25844
  watcherDebounceInvalidHint: "Pass an integer >= 0 (e.g. 250).",
25845
+ /**
25846
+ * §3.1b error block for an invalid `--max-nodes <N>`. Same shape as
25847
+ * the watcher-debounce template family.
25848
+ */
25849
+ maxNodesInvalid: "{{glyph}} sm serve: --max-nodes must be an integer >= 1 (got {{value}}).\n {{hint}}\n",
25850
+ maxNodesInvalidHint: "Pass a positive integer, e.g. --max-nodes 256.",
25559
25851
  // --- --no-ui flag-validation failures (ExitCode.Error) ------------------
25560
25852
  /**
25561
25853
  * §3.1b error block when `--no-ui` is paired with an explicit
@@ -25825,6 +26117,10 @@ var ServeCommand = class extends SmCommand {
25825
26117
  // who want to tighten / relax the watcher's batching window without
25826
26118
  // editing settings.json. Hidden flag, the Usage block omits it.
25827
26119
  watcherDebounceMs = Option31.String("--watcher-debounce-ms", { required: false, hidden: true });
26120
+ maxNodes = Option31.String("--max-nodes", {
26121
+ required: false,
26122
+ description: "Per-invocation override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. Applies to every scan the server runs (initial watcher pass, debounced batches, POST /api/scan, GET /api/scan?fresh=1). Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
26123
+ });
25828
26124
  // Long-running daemon, `done in <…>` after a graceful shutdown is
25829
26125
  // noise. Mirrors `sm watch`'s opt-out.
25830
26126
  emitElapsed = false;
@@ -25903,6 +26199,17 @@ var ServeCommand = class extends SmCommand {
25903
26199
  );
25904
26200
  return ExitCode.Error;
25905
26201
  }
26202
+ const maxNodesResult = parseMaxNodes(this.maxNodes);
26203
+ if (!maxNodesResult.ok) {
26204
+ this.printer.info(
26205
+ tx(SERVE_TEXTS.maxNodesInvalid, {
26206
+ glyph: errGlyph,
26207
+ value: sanitizeForTerminal(maxNodesResult.value),
26208
+ hint: stderrAnsi.dim(SERVE_TEXTS.maxNodesInvalidHint)
26209
+ })
26210
+ );
26211
+ return ExitCode.Error;
26212
+ }
25906
26213
  const input = {
25907
26214
  dbPath,
25908
26215
  uiDist: resolvedUiDist,
@@ -25916,6 +26223,7 @@ var ServeCommand = class extends SmCommand {
25916
26223
  if (portResult.port !== void 0) input.port = portResult.port;
25917
26224
  if (this.host !== void 0) input.host = this.host;
25918
26225
  if (debounceResult.value !== void 0) input.watcherDebounceMs = debounceResult.value;
26226
+ if (maxNodesResult.value !== void 0) input.maxNodes = maxNodesResult.value;
25919
26227
  const validation = validateServerOptions(input);
25920
26228
  if (!validation.ok) {
25921
26229
  this.printer.info(formatValidationError(validation.error, stderrAnsi));
@@ -25985,6 +26293,12 @@ function parseDebounce(raw) {
25985
26293
  if (parsed === null) return { ok: false, value: raw };
25986
26294
  return { ok: true, value: parsed };
25987
26295
  }
26296
+ function parseMaxNodes(raw) {
26297
+ if (raw === void 0) return { ok: true, value: void 0 };
26298
+ const n = Number(raw);
26299
+ if (!Number.isInteger(n) || n < 1) return { ok: false, value: raw };
26300
+ return { ok: true, value: n };
26301
+ }
25988
26302
  function resolveUiDist(ctx, raw) {
25989
26303
  if (raw === void 0) {
25990
26304
  return { ok: true, uiDist: resolveDefaultUiDist(ctx) };
@@ -26031,6 +26345,12 @@ function formatValidationError(err, ansi) {
26031
26345
  value: sanitizeForTerminal(err.value),
26032
26346
  hint: ansi.dim(SERVE_TEXTS.watcherDebounceInvalidHint)
26033
26347
  });
26348
+ case "max-nodes-invalid":
26349
+ return tx(SERVE_TEXTS.maxNodesInvalid, {
26350
+ glyph: errGlyph,
26351
+ value: sanitizeForTerminal(err.value),
26352
+ hint: ansi.dim(SERVE_TEXTS.maxNodesInvalidHint)
26353
+ });
26034
26354
  case "no-ui-conflicts-ui-dist":
26035
26355
  return tx(SERVE_TEXTS.noUiConflictsUiDist, {
26036
26356
  glyph: errGlyph,
@@ -27409,7 +27729,7 @@ var logLevel = resolveLogLevel({
27409
27729
  errStream: process.stderr
27410
27730
  });
27411
27731
  configureLogger(new Logger({ level: logLevel, stream: process.stderr }));
27412
- var bareArgs = args.length === 0 ? resolveBareDefault() : null;
27732
+ var bareArgs = resolveBareInvocation(args);
27413
27733
  var routedArgs = routeHelpArgs(bareArgs ?? args, cli);
27414
27734
  var lifecycleDispatcher = makeHookDispatcher(
27415
27735
  builtIns().hooks ?? [],
@@ -27452,6 +27772,20 @@ await lifecycleDispatcher.dispatch(
27452
27772
  makeEvent("shutdown", { exitCode })
27453
27773
  );
27454
27774
  process.exit(exitCode);
27775
+ function resolveBareInvocation(rawArgs) {
27776
+ if (rawArgs.length === 0) return resolveBareDefault();
27777
+ const first = rawArgs[0];
27778
+ const passthrough = /* @__PURE__ */ new Set(["--help", "-h", "--version", "-V", "-v"]);
27779
+ if (first !== void 0 && first.startsWith("-") && !passthrough.has(first)) {
27780
+ const isSingleDashLong = !first.startsWith("--") && first.length > 2;
27781
+ if (isSingleDashLong) return null;
27782
+ if (existsSync30(defaultProjectDbPath(defaultRuntimeContext()))) {
27783
+ return ["serve", ...rawArgs];
27784
+ }
27785
+ return resolveBareDefault();
27786
+ }
27787
+ return null;
27788
+ }
27455
27789
  function resolveBareDefault() {
27456
27790
  const ctx = defaultRuntimeContext();
27457
27791
  if (existsSync30(defaultProjectDbPath(ctx))) {