@skill-map/cli 0.33.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -531,11 +531,14 @@ var claudeProvider = {
531
531
  const lower = path.toLowerCase();
532
532
  if (lower.startsWith(".claude/agents/")) return "agent";
533
533
  if (lower.startsWith(".claude/commands/")) return "command";
534
- if (lower.startsWith(".claude/skills/")) return "skill";
534
+ if (/^\.claude\/skills\/[^/]+\/skill\.md$/.test(lower)) return "skill";
535
535
  return null;
536
536
  }
537
537
  };
538
538
 
539
+ // plugins/claude/extractors/at-directive/index.ts
540
+ import { posix as pathPosix } from "path";
541
+
539
542
  // kernel/util/strip-code-blocks.ts
540
543
  var FENCE_RE = /^(?<indent> {0,3})(?<fence>`{3,}|~{3,})/;
541
544
  function stripCodeBlocks(input) {
@@ -609,26 +612,29 @@ var atDirectiveExtractor = {
609
612
  const seenMentions = /* @__PURE__ */ new Set();
610
613
  const seenReferences = /* @__PURE__ */ new Set();
611
614
  const body = stripCodeBlocks(ctx.body);
615
+ const sourceDir = pathPosix.dirname(ctx.node.path);
612
616
  for (const match of body.matchAll(AT_RE)) {
613
617
  const original = match[1];
614
618
  const bare = original.slice(1);
615
- const isReference = bare.startsWith("./") || bare.startsWith("../") || bare.startsWith("/") || FILE_EXT_RE.test(bare);
619
+ if (bare.startsWith("/")) continue;
620
+ const isReference = bare.startsWith("./") || bare.startsWith("../") || FILE_EXT_RE.test(bare);
616
621
  if (isReference) {
617
- const target = bare.replace(/^\.\//, "");
618
- if (seenReferences.has(target)) continue;
619
- seenReferences.add(target);
622
+ const target = resolveSourceRelative(sourceDir, bare);
623
+ const dedupKey = target.toLowerCase();
624
+ if (seenReferences.has(dedupKey)) continue;
625
+ seenReferences.add(dedupKey);
620
626
  ctx.emitLink({
621
627
  source: ctx.node.path,
622
628
  target,
623
629
  kind: "references",
624
- // 0.85: strong file signal (path prefix `./` / `../` / `/` OR
630
+ // 0.85: strong file signal (path prefix `./` / `../` OR
625
631
  // a known file extension on the tail). One degree of inference
626
632
  // (the runtime still resolves the path).
627
633
  confidence: 0.85,
628
634
  sources: [ID],
629
635
  trigger: {
630
636
  originalTrigger: original,
631
- normalizedTrigger: target.toLowerCase()
637
+ normalizedTrigger: target
632
638
  }
633
639
  });
634
640
  continue;
@@ -654,10 +660,14 @@ var atDirectiveExtractor = {
654
660
  }
655
661
  }
656
662
  };
663
+ function resolveSourceRelative(sourceDir, bare) {
664
+ const joined = sourceDir === "." ? bare : `${sourceDir}/${bare}`;
665
+ return pathPosix.normalize(joined);
666
+ }
657
667
 
658
668
  // plugins/claude/extractors/slash/index.ts
659
669
  var ID2 = "slash";
660
- var SLASH_RE = /(?<![A-Za-z0-9_/.:?#])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
670
+ var SLASH_RE = /(?<![A-Za-z0-9_/.:?#=&])(\/[a-z0-9][a-z0-9_-]*(?::[a-z0-9][a-z0-9_-]*)?)/gi;
661
671
  var slashExtractor = {
662
672
  id: ID2,
663
673
  pluginId: "claude",
@@ -809,7 +819,7 @@ var geminiProvider = {
809
819
  classify(path) {
810
820
  const lower = path.toLowerCase();
811
821
  if (lower.startsWith(".gemini/agents/")) return "agent";
812
- if (lower.startsWith(".gemini/skills/")) return "skill";
822
+ if (/^\.gemini\/skills\/[^/]+\/skill\.md$/.test(lower)) return "skill";
813
823
  return null;
814
824
  }
815
825
  };
@@ -929,7 +939,7 @@ var agentSkillsProvider = {
929
939
  }
930
940
  },
931
941
  classify(path) {
932
- if (path.toLowerCase().startsWith(".agents/skills/")) return "skill";
942
+ if (/^\.agents\/skills\/[^/]+\/skill\.md$/.test(path.toLowerCase())) return "skill";
933
943
  return null;
934
944
  }
935
945
  };
@@ -1138,7 +1148,7 @@ function lineFor(lineStarts, offset) {
1138
1148
  }
1139
1149
 
1140
1150
  // plugins/core/extractors/markdown-link/index.ts
1141
- import { posix as pathPosix } from "path";
1151
+ import { posix as pathPosix2 } from "path";
1142
1152
  var ID5 = "markdown-link";
1143
1153
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1144
1154
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
@@ -1152,7 +1162,7 @@ var markdownLinkExtractor = {
1152
1162
  extract(ctx) {
1153
1163
  const seen = /* @__PURE__ */ new Set();
1154
1164
  const lineStarts = computeLineStarts2(ctx.body);
1155
- const sourceDir = pathPosix.dirname(ctx.node.path);
1165
+ const sourceDir = pathPosix2.dirname(ctx.node.path);
1156
1166
  for (const match of ctx.body.matchAll(LINK_RE)) {
1157
1167
  const original = match[2];
1158
1168
  const resolved = resolveTarget(sourceDir, original);
@@ -1184,7 +1194,7 @@ function resolveTarget(sourceDir, raw) {
1184
1194
  if (URL_SCHEME_RE.test(trimmed)) return null;
1185
1195
  if (trimmed.startsWith("/")) return null;
1186
1196
  const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1187
- return pathPosix.normalize(joined);
1197
+ return pathPosix2.normalize(joined);
1188
1198
  }
1189
1199
  function computeLineStarts2(body) {
1190
1200
  const starts = [0];
@@ -1417,7 +1427,7 @@ function tooltipFor(status) {
1417
1427
  }
1418
1428
 
1419
1429
  // plugins/core/analyzers/broken-ref/index.ts
1420
- import { posix as pathPosix2, resolve } from "path";
1430
+ import { posix as pathPosix3, resolve } from "path";
1421
1431
 
1422
1432
  // plugins/core/analyzers/broken-ref/text.ts
1423
1433
  var BROKEN_REF_TEXTS = {
@@ -1556,8 +1566,8 @@ function indexByNormalizedName(nodes) {
1556
1566
  return out;
1557
1567
  }
1558
1568
  function basenameWithoutExt(path) {
1559
- const base = pathPosix2.basename(path);
1560
- const ext = pathPosix2.extname(base);
1569
+ const base = pathPosix3.basename(path);
1570
+ const ext = pathPosix3.extname(base);
1561
1571
  return ext ? base.slice(0, -ext.length) : base;
1562
1572
  }
1563
1573
  function indexByBasenameWithoutName(nodes) {
@@ -3124,7 +3134,7 @@ var UPDATE_CHECK_TEXTS = {
3124
3134
  // package.json
3125
3135
  var package_default = {
3126
3136
  name: "@skill-map/cli",
3127
- version: "0.33.0",
3137
+ version: "0.34.0",
3128
3138
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
3129
3139
  license: "MIT",
3130
3140
  type: "module",
@@ -5058,7 +5068,7 @@ var AsyncMutex = class {
5058
5068
  this.#locked = true;
5059
5069
  return;
5060
5070
  }
5061
- await new Promise((resolve38) => this.#waiters.push(resolve38));
5071
+ await new Promise((resolve39) => this.#waiters.push(resolve39));
5062
5072
  this.#locked = true;
5063
5073
  }
5064
5074
  unlock() {
@@ -9660,9 +9670,42 @@ function trimRedundantPath(message, primary) {
9660
9670
  }
9661
9671
 
9662
9672
  // cli/commands/config.ts
9663
- import { existsSync as existsSync14 } from "fs";
9673
+ import { existsSync as existsSync15 } from "fs";
9664
9674
  import { Command as Command4, Option as Option4 } from "clipanion";
9665
9675
 
9676
+ // core/config/active-provider.ts
9677
+ import { existsSync as existsSync14 } from "fs";
9678
+ import { join as join10 } from "path";
9679
+ var DETECTION_RULES = [
9680
+ { providerId: "claude", marker: ".claude" },
9681
+ { providerId: "gemini", marker: ".gemini" },
9682
+ { providerId: "openai", marker: ".codex" },
9683
+ { providerId: "openai", marker: "AGENTS.md" },
9684
+ { providerId: "cursor", marker: ".cursor" }
9685
+ ];
9686
+ function resolveActiveProvider(cwd) {
9687
+ const detected = detectProvidersFromFilesystem(cwd);
9688
+ const fromConfig = readConfigValue("activeProvider", { cwd });
9689
+ if (typeof fromConfig === "string" && fromConfig.length > 0) {
9690
+ return { resolved: fromConfig, source: "config", detected };
9691
+ }
9692
+ if (detected.length > 0) {
9693
+ return { resolved: detected[0], source: "autodetect", detected };
9694
+ }
9695
+ return { resolved: null, source: "none", detected };
9696
+ }
9697
+ function detectProvidersFromFilesystem(cwd) {
9698
+ const seen = /* @__PURE__ */ new Set();
9699
+ const out = [];
9700
+ for (const rule of DETECTION_RULES) {
9701
+ if (seen.has(rule.providerId)) continue;
9702
+ if (!existsSync14(join10(cwd, rule.marker))) continue;
9703
+ seen.add(rule.providerId);
9704
+ out.push(rule.providerId);
9705
+ }
9706
+ return out;
9707
+ }
9708
+
9666
9709
  // cli/util/path-display.ts
9667
9710
  import { isAbsolute as isAbsolute4, relative as pathRelative } from "path";
9668
9711
  function relativeIfBelow(path, cwd) {
@@ -9801,6 +9844,9 @@ function suggestConfigKey(effective, typed, ansi) {
9801
9844
  hint: ansi.dim(tx(CONFIG_TEXTS.unknownKeySuggestionHint, { suggestions: formatted }))
9802
9845
  });
9803
9846
  }
9847
+ var KNOWN_DEFAULTLESS_KEY_RESOLVERS = {
9848
+ activeProvider: (cwd) => resolveActiveProvider(cwd).resolved
9849
+ };
9804
9850
  function parseCliValue(raw) {
9805
9851
  try {
9806
9852
  return JSON.parse(raw);
@@ -9973,6 +10019,11 @@ function formatValueListHuman(value, ansi) {
9973
10019
  }
9974
10020
  return String(value);
9975
10021
  }
10022
+ function resolveConfigGetValue(lookupValue, key, cwd) {
10023
+ if (lookupValue !== void 0) return lookupValue;
10024
+ const runtimeResolver = KNOWN_DEFAULTLESS_KEY_RESOLVERS[key];
10025
+ return runtimeResolver ? runtimeResolver(cwd) : void 0;
10026
+ }
9976
10027
  var ConfigGetCommand = class extends SmCommand {
9977
10028
  static paths = [["config", "get"]];
9978
10029
  static usage = Command4.Usage({
@@ -9987,16 +10038,14 @@ var ConfigGetCommand = class extends SmCommand {
9987
10038
  strict = Option4.Boolean("--strict", false);
9988
10039
  emitElapsed = false;
9989
10040
  async run() {
9990
- const result = tryLoadConfig(
9991
- { strict: this.strict, ...defaultRuntimeContext() },
9992
- this.context.stderr
9993
- );
10041
+ const ctx = defaultRuntimeContext();
10042
+ const result = tryLoadConfig({ strict: this.strict, ...ctx }, this.context.stderr);
9994
10043
  if (!result.ok) return result.exitCode;
9995
10044
  const { effective, warnings } = result.loaded;
9996
10045
  for (const w of warnings) this.printer.info(w + "\n");
9997
10046
  const lookup = safeGetAtPath(effective, this.key, this.context.stderr);
9998
10047
  if (!lookup.ok) return lookup.exitCode;
9999
- const { value } = lookup;
10048
+ const value = resolveConfigGetValue(lookup.value, this.key, ctx.cwd);
10000
10049
  if (value === void 0) {
10001
10050
  const ansi = this.ansiFor("stderr");
10002
10051
  this.printer.info(
@@ -10036,10 +10085,8 @@ var ConfigShowCommand = class extends SmCommand {
10036
10085
  // the value it gates.
10037
10086
  // eslint-disable-next-line complexity
10038
10087
  async run() {
10039
- const result = tryLoadConfig(
10040
- { strict: this.strict, ...defaultRuntimeContext() },
10041
- this.context.stderr
10042
- );
10088
+ const ctx = defaultRuntimeContext();
10089
+ const result = tryLoadConfig({ strict: this.strict, ...ctx }, this.context.stderr);
10043
10090
  if (!result.ok) return result.exitCode;
10044
10091
  const { effective, sources, warnings } = result.loaded;
10045
10092
  for (const w of warnings) this.printer.info(w + "\n");
@@ -10062,6 +10109,12 @@ var ConfigShowCommand = class extends SmCommand {
10062
10109
  }
10063
10110
  throw err;
10064
10111
  }
10112
+ if (value === void 0) {
10113
+ const runtimeResolver = KNOWN_DEFAULTLESS_KEY_RESOLVERS[this.key];
10114
+ if (runtimeResolver) {
10115
+ value = runtimeResolver(ctx.cwd);
10116
+ }
10117
+ }
10065
10118
  if (value === void 0) {
10066
10119
  this.printer.info(tx(CONFIG_TEXTS.unknownKey, { glyph: errGlyphShow, key: this.key }));
10067
10120
  return ExitCode.NotFound;
@@ -10234,7 +10287,7 @@ var ConfigSetCommand = class extends SmCommand {
10234
10287
  announceLensSwitch(cwd, ansi) {
10235
10288
  const dbPath = resolveDbPath({ db: void 0, cwd });
10236
10289
  const okGlyph = ansi.green("\u2713");
10237
- if (!existsSync14(dbPath)) {
10290
+ if (!existsSync15(dbPath)) {
10238
10291
  this.printer.info(tx(CONFIG_TEXTS.lensSwitchedNoDb, { glyph: okGlyph }));
10239
10292
  return;
10240
10293
  }
@@ -10274,7 +10327,7 @@ var ConfigResetCommand = class extends SmCommand {
10274
10327
  const path = targetSettingsPath2(target, ctx.cwd);
10275
10328
  const ansi = this.ansiFor("stdout");
10276
10329
  const okGlyph = ansi.green("\u2713");
10277
- if (!existsSync14(path)) {
10330
+ if (!existsSync15(path)) {
10278
10331
  this.printer.data(
10279
10332
  tx(CONFIG_TEXTS.unsetNoOverride, {
10280
10333
  glyph: okGlyph,
@@ -10349,16 +10402,16 @@ var CONFIG_COMMANDS = [
10349
10402
  ];
10350
10403
 
10351
10404
  // cli/commands/conformance.ts
10352
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
10405
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
10353
10406
  import { dirname as dirname12, resolve as resolve21 } from "path";
10354
10407
  import { fileURLToPath as fileURLToPath4 } from "url";
10355
10408
  import { Command as Command5, Option as Option5 } from "clipanion";
10356
10409
 
10357
10410
  // conformance/index.ts
10358
10411
  import { spawnSync as spawnSync2 } from "child_process";
10359
- import { cpSync, existsSync as existsSync15, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, statSync as statSync3 } from "fs";
10412
+ import { cpSync, existsSync as existsSync16, mkdtempSync, readdirSync as readdirSync5, readFileSync as readFileSync14, rmSync, statSync as statSync3 } from "fs";
10360
10413
  import { tmpdir } from "os";
10361
- import { isAbsolute as isAbsolute5, join as join10, relative as relative3, resolve as resolve19 } from "path";
10414
+ import { isAbsolute as isAbsolute5, join as join11, relative as relative3, resolve as resolve19 } from "path";
10362
10415
 
10363
10416
  // conformance/i18n/runner.texts.ts
10364
10417
  var CONFORMANCE_RUNNER_TEXTS = {
@@ -10394,9 +10447,9 @@ function disableEnv(setup) {
10394
10447
  function runConformanceCase(options) {
10395
10448
  const raw = readFileSync14(options.casePath, "utf8");
10396
10449
  const c = JSON.parse(raw);
10397
- const fixturesRoot = options.fixturesRoot ?? join10(options.specRoot, "conformance", "fixtures");
10450
+ const fixturesRoot = options.fixturesRoot ?? join11(options.specRoot, "conformance", "fixtures");
10398
10451
  const safeId = c.id.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32);
10399
- const scope = mkdtempSync(join10(tmpdir(), `sm-conformance-${safeId}-`));
10452
+ const scope = mkdtempSync(join11(tmpdir(), `sm-conformance-${safeId}-`));
10400
10453
  const setupEnv = disableEnv(c.setup);
10401
10454
  try {
10402
10455
  const priorFailure = runPriorScansSetup(c, options, scope, fixturesRoot, setupEnv);
@@ -10468,9 +10521,9 @@ function replaceFixture(scope, fixturesRoot, fixture) {
10468
10521
  assertContained2(fixturesRoot, fixture, "fixture");
10469
10522
  for (const entry of readdirSync5(scope)) {
10470
10523
  if (entry === KERNEL_SKILL_MAP_DIR) continue;
10471
- rmSync(join10(scope, entry), { recursive: true, force: true });
10524
+ rmSync(join11(scope, entry), { recursive: true, force: true });
10472
10525
  }
10473
- const src = join10(fixturesRoot, fixture);
10526
+ const src = join11(fixturesRoot, fixture);
10474
10527
  cpSync(src, scope, { recursive: true });
10475
10528
  }
10476
10529
  function assertContained2(root, rel, label) {
@@ -10507,7 +10560,7 @@ function evaluateAssertion(a, ctx) {
10507
10560
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
10508
10561
  }
10509
10562
  const abs = resolve19(ctx.scope, a.path);
10510
- return existsSync15(abs) ? { ok: true, type: a.type } : {
10563
+ return existsSync16(abs) ? { ok: true, type: a.type } : {
10511
10564
  ok: false,
10512
10565
  type: a.type,
10513
10566
  reason: tx(CONFORMANCE_RUNNER_TEXTS.fileNotFound, { path: a.path })
@@ -10520,9 +10573,9 @@ function evaluateAssertion(a, ctx) {
10520
10573
  } catch (err) {
10521
10574
  return { ok: false, type: a.type, reason: formatErrorMessage(err) };
10522
10575
  }
10523
- const fixturePath = join10(ctx.fixturesRoot, a.fixture);
10576
+ const fixturePath = join11(ctx.fixturesRoot, a.fixture);
10524
10577
  const targetPath = resolve19(ctx.scope, a.path);
10525
- if (!existsSync15(targetPath)) {
10578
+ if (!existsSync16(targetPath)) {
10526
10579
  return {
10527
10580
  ok: false,
10528
10581
  type: a.type,
@@ -10703,7 +10756,7 @@ var CONFORMANCE_TEXTS = {
10703
10756
  };
10704
10757
 
10705
10758
  // cli/util/conformance-scopes.ts
10706
- import { existsSync as existsSync16, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
10759
+ import { existsSync as existsSync17, readdirSync as readdirSync6, statSync as statSync4 } from "fs";
10707
10760
  import { dirname as dirname11, resolve as resolve20 } from "path";
10708
10761
  import { createRequire as createRequire6 } from "module";
10709
10762
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -10723,7 +10776,7 @@ function resolveCliWorkspaceRoot() {
10723
10776
  let cursor = here;
10724
10777
  for (let depth = 0; depth < 6; depth += 1) {
10725
10778
  const candidate = resolve20(cursor, "plugins");
10726
- if (existsSync16(candidate) && statSync4(candidate).isDirectory()) {
10779
+ if (existsSync17(candidate) && statSync4(candidate).isDirectory()) {
10727
10780
  return cursor;
10728
10781
  }
10729
10782
  const parent = dirname11(cursor);
@@ -10743,7 +10796,7 @@ function collectProviderScopes(specRoot) {
10743
10796
  return out;
10744
10797
  }
10745
10798
  const pluginsRoot = resolve20(workspaceRoot, "plugins");
10746
- if (!existsSync16(pluginsRoot)) return out;
10799
+ if (!existsSync17(pluginsRoot)) return out;
10747
10800
  for (const bundleEntry of readdirSync6(pluginsRoot)) {
10748
10801
  const bundleDir = resolve20(pluginsRoot, bundleEntry);
10749
10802
  if (!isDir(bundleDir)) continue;
@@ -10755,7 +10808,7 @@ function collectProviderScopes(specRoot) {
10755
10808
  }
10756
10809
  function isDir(path) {
10757
10810
  try {
10758
- return existsSync16(path) && statSync4(path).isDirectory();
10811
+ return existsSync17(path) && statSync4(path).isDirectory();
10759
10812
  } catch {
10760
10813
  return false;
10761
10814
  }
@@ -10765,10 +10818,10 @@ function collectBundleProviderScopes(providersRoot, specRoot, out) {
10765
10818
  const providerDir = resolve20(providersRoot, entry);
10766
10819
  if (!isDir(providerDir)) continue;
10767
10820
  const conformanceDir = resolve20(providerDir, "conformance");
10768
- if (!existsSync16(conformanceDir)) continue;
10821
+ if (!existsSync17(conformanceDir)) continue;
10769
10822
  const casesDir = resolve20(conformanceDir, "cases");
10770
10823
  const fixturesDir = resolve20(conformanceDir, "fixtures");
10771
- if (!existsSync16(casesDir) || !existsSync16(fixturesDir)) continue;
10824
+ if (!existsSync17(casesDir) || !existsSync17(fixturesDir)) continue;
10772
10825
  out.push({
10773
10826
  id: `provider:${entry}`,
10774
10827
  kind: "provider",
@@ -10806,7 +10859,7 @@ function selectConformanceScopes(scope) {
10806
10859
  return [match];
10807
10860
  }
10808
10861
  function listCaseFiles(scope) {
10809
- if (!existsSync16(scope.casesDir)) return [];
10862
+ if (!existsSync17(scope.casesDir)) return [];
10810
10863
  return readdirSync6(scope.casesDir).filter((entry) => entry.endsWith(".json")).sort().map((entry) => resolve20(scope.casesDir, entry));
10811
10864
  }
10812
10865
 
@@ -10825,7 +10878,7 @@ function resolveBinary() {
10825
10878
  let cursor = here;
10826
10879
  for (let depth = 0; depth < 6; depth += 1) {
10827
10880
  const candidate = resolve21(cursor, "bin", "sm.js");
10828
- if (existsSync17(candidate)) return candidate;
10881
+ if (existsSync18(candidate)) return candidate;
10829
10882
  const parent = dirname12(cursor);
10830
10883
  if (parent === cursor) break;
10831
10884
  cursor = parent;
@@ -10891,7 +10944,7 @@ var ConformanceRunCommand = class extends SmCommand {
10891
10944
  return ExitCode.Error;
10892
10945
  }
10893
10946
  const binary = resolveBinary();
10894
- if (!existsSync17(binary)) {
10947
+ if (!existsSync18(binary)) {
10895
10948
  if (this.json) {
10896
10949
  this.#emitJsonError(
10897
10950
  "internal",
@@ -11083,7 +11136,7 @@ function writeStreamSnippet(stream, header, text) {
11083
11136
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
11084
11137
 
11085
11138
  // cli/commands/db/backup.ts
11086
- import { dirname as dirname13, join as join11, resolve as resolve22 } from "path";
11139
+ import { dirname as dirname13, join as join12, resolve as resolve22 } from "path";
11087
11140
  import { Command as Command6, Option as Option6 } from "clipanion";
11088
11141
 
11089
11142
  // cli/i18n/db.texts.ts
@@ -11169,7 +11222,7 @@ var DbBackupCommand = class extends SmCommand {
11169
11222
  const exit = requireDbOrExit(path, this.context.stderr);
11170
11223
  if (exit !== null) return exit;
11171
11224
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
11172
- const outPath = this.out ? resolve22(this.out) : join11(dirname13(path), "backups", `${ts}.db`);
11225
+ const outPath = this.out ? resolve22(this.out) : join12(dirname13(path), "backups", `${ts}.db`);
11173
11226
  await withSqlite({ databasePath: path, autoMigrate: false }, async (storage) => {
11174
11227
  storage.migrations.writeBackup(outPath);
11175
11228
  });
@@ -12823,7 +12876,7 @@ function registeredVerbPaths(cli2) {
12823
12876
  // cli/commands/hooks.ts
12824
12877
  import {
12825
12878
  chmodSync,
12826
- existsSync as existsSync18,
12879
+ existsSync as existsSync19,
12827
12880
  mkdirSync as mkdirSync5,
12828
12881
  readFileSync as readFileSync17,
12829
12882
  statSync as statSync5,
@@ -12922,7 +12975,7 @@ var HooksInstallCommand = class extends SmCommand {
12922
12975
  }
12923
12976
  const hooksDir = resolve26(repoRoot, ".git", "hooks");
12924
12977
  const hookPath = resolve26(hooksDir, "pre-commit");
12925
- const existing = existsSync18(hookPath) ? readFileSync17(hookPath, "utf8") : null;
12978
+ const existing = existsSync19(hookPath) ? readFileSync17(hookPath, "utf8") : null;
12926
12979
  const planned2 = computePlannedHookContent(existing);
12927
12980
  if (planned2.kind === "already-installed") {
12928
12981
  this.printer.info(tx(HOOKS_TEXTS.alreadyInstalled, { glyph: okGlyph, hookPath }));
@@ -12948,7 +13001,7 @@ var HooksInstallCommand = class extends SmCommand {
12948
13001
  return ExitCode.Ok;
12949
13002
  }
12950
13003
  try {
12951
- if (!existsSync18(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
13004
+ if (!existsSync19(hooksDir)) mkdirSync5(hooksDir, { recursive: true });
12952
13005
  writeFileSync2(hookPath, planned2.content, { encoding: "utf8" });
12953
13006
  ensureExecutableBit(hookPath);
12954
13007
  } catch (err) {
@@ -12979,7 +13032,7 @@ var HooksInstallCommand = class extends SmCommand {
12979
13032
  function findGitRepoRoot(cwd) {
12980
13033
  let current = cwd;
12981
13034
  while (true) {
12982
- if (existsSync18(resolve26(current, ".git"))) return current;
13035
+ if (existsSync19(resolve26(current, ".git"))) return current;
12983
13036
  const parent = dirname16(current);
12984
13037
  if (parent === current) return null;
12985
13038
  current = parent;
@@ -13001,11 +13054,12 @@ var HOOKS_COMMANDS = [HooksInstallCommand];
13001
13054
 
13002
13055
  // cli/commands/init.ts
13003
13056
  import { mkdir as mkdir3, readFile as readFile2, writeFile } from "fs/promises";
13004
- import { join as join15 } from "path";
13057
+ import { join as join17 } from "path";
13005
13058
  import { Command as Command17, Option as Option16 } from "clipanion";
13006
13059
 
13007
13060
  // kernel/orchestrator/index.ts
13008
- import { existsSync as existsSync21, statSync as statSync7 } from "fs";
13061
+ import { existsSync as existsSync22, statSync as statSync7 } from "fs";
13062
+ import { isAbsolute as isAbsolute7, resolve as resolve27 } from "path";
13009
13063
  import { Tiktoken as Tiktoken2 } from "js-tiktoken/lite";
13010
13064
  import cl100k_base from "js-tiktoken/ranks/cl100k_base";
13011
13065
 
@@ -13332,6 +13386,9 @@ function dedupeLinks(links) {
13332
13386
  existing.sources = [...existing.sources, src];
13333
13387
  }
13334
13388
  }
13389
+ if (link2.confidence > existing.confidence) {
13390
+ existing.confidence = link2.confidence;
13391
+ }
13335
13392
  continue;
13336
13393
  }
13337
13394
  out.set(key, link2);
@@ -13526,7 +13583,7 @@ function originatingNodeOf(link2, priorNodePaths) {
13526
13583
  function computeCacheDecision(opts) {
13527
13584
  const applicableExtractors = opts.extractors.filter((ex) => {
13528
13585
  if (!matchesKindPrecondition(ex, opts.kind)) return false;
13529
- if (!matchesProviderPrecondition(ex, opts.provider)) return false;
13586
+ if (!matchesProviderPrecondition(ex, opts.provider, opts.activeProvider)) return false;
13530
13587
  return true;
13531
13588
  });
13532
13589
  const applicableQualifiedIds = new Set(
@@ -13550,10 +13607,12 @@ function matchesKindPrecondition(ex, kind) {
13550
13607
  return kindOnly === kind;
13551
13608
  });
13552
13609
  }
13553
- function matchesProviderPrecondition(ex, provider) {
13610
+ function matchesProviderPrecondition(ex, nodeProvider, activeProvider) {
13554
13611
  const providers = ex.precondition?.provider;
13555
13612
  if (!providers || providers.length === 0) return true;
13556
- return providers.includes(provider);
13613
+ if (!providers.includes(nodeProvider)) return false;
13614
+ if (activeProvider === null) return false;
13615
+ return providers.includes(activeProvider);
13557
13616
  }
13558
13617
  function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
13559
13618
  const cachedQualifiedIds = /* @__PURE__ */ new Set();
@@ -13654,6 +13713,65 @@ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, appl
13654
13713
  return "obsolete";
13655
13714
  }
13656
13715
 
13716
+ // kernel/orchestrator/lift-mention-confidence.ts
13717
+ function liftMentionConfidence(links, nodes) {
13718
+ if (!links.some((l) => l.kind === "mentions")) return;
13719
+ const byPath3 = /* @__PURE__ */ new Set();
13720
+ for (const node of nodes) byPath3.add(node.path);
13721
+ const byNormalizedName = indexByNormalizedName2(nodes);
13722
+ for (const link2 of links) {
13723
+ if (link2.kind !== "mentions") continue;
13724
+ if (isResolved2(link2, byPath3, byNormalizedName)) {
13725
+ link2.confidence = 1;
13726
+ }
13727
+ }
13728
+ }
13729
+ function isResolved2(link2, byPath3, byNormalizedName) {
13730
+ const normalized = link2.trigger?.normalizedTrigger;
13731
+ if (normalized) {
13732
+ const withoutSigil = normalized.replace(/^[/@]/, "").trim();
13733
+ if (byNormalizedName.has(withoutSigil)) return true;
13734
+ }
13735
+ if (byPath3.has(link2.target)) return true;
13736
+ return false;
13737
+ }
13738
+ function indexByNormalizedName2(nodes) {
13739
+ const out = /* @__PURE__ */ new Map();
13740
+ for (const node of nodes) {
13741
+ const raw = node.frontmatter?.["name"];
13742
+ const name = typeof raw === "string" ? raw : "";
13743
+ if (!name) continue;
13744
+ out.set(normalizeTrigger(name), true);
13745
+ }
13746
+ return out;
13747
+ }
13748
+
13749
+ // kernel/orchestrator/post-walk-transforms.ts
13750
+ var POST_WALK_TRANSFORMS = [
13751
+ {
13752
+ id: "dedupe-links",
13753
+ description: "Collapse identical (source, target, kind, normalizedTrigger) edges across extractors; union sources[] and pick max confidence on merge.",
13754
+ run(links) {
13755
+ return dedupeLinks(links);
13756
+ }
13757
+ },
13758
+ {
13759
+ id: "lift-mention-confidence",
13760
+ description: "Bump resolved `mentions` links to confidence 1.0 once the full node graph is known (post-merge polish).",
13761
+ run(links, nodes) {
13762
+ liftMentionConfidence(links, nodes);
13763
+ }
13764
+ }
13765
+ ];
13766
+ function applyPostWalkTransforms(links, nodes, transforms = POST_WALK_TRANSFORMS) {
13767
+ let current = links;
13768
+ for (const transform of transforms) {
13769
+ const next = transform.run(current, nodes);
13770
+ if (next) current = next;
13771
+ }
13772
+ return current;
13773
+ }
13774
+
13657
13775
  // kernel/orchestrator/renames.ts
13658
13776
  function findHighConfidenceRenames(opts) {
13659
13777
  const ops = [];
@@ -13797,8 +13915,8 @@ function computeDriftStatus(args2) {
13797
13915
  }
13798
13916
 
13799
13917
  // kernel/sidecar/discover-orphans.ts
13800
- import { existsSync as existsSync19, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13801
- import { join as join12, relative as relative4, sep as sep3 } from "path";
13918
+ import { existsSync as existsSync20, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13919
+ import { join as join13, relative as relative4, sep as sep3 } from "path";
13802
13920
  function discoverOrphanSidecars(roots, shouldSkip) {
13803
13921
  const out = [];
13804
13922
  for (const root of roots) {
@@ -13814,7 +13932,7 @@ function walk(root, current, shouldSkip, out) {
13814
13932
  return;
13815
13933
  }
13816
13934
  for (const entry of entries) {
13817
- const full = join12(current, entry.name);
13935
+ const full = join13(current, entry.name);
13818
13936
  const rel = relative4(root, full).split(sep3).join("/");
13819
13937
  if (shouldSkip(rel)) continue;
13820
13938
  if (entry.isSymbolicLink()) continue;
@@ -13825,7 +13943,7 @@ function walk(root, current, shouldSkip, out) {
13825
13943
  if (!entry.isFile()) continue;
13826
13944
  if (!entry.name.endsWith(".sm")) continue;
13827
13945
  const expectedMd = `${full.slice(0, -".sm".length)}.md`;
13828
- if (existsSync19(expectedMd) && safeIsFile(expectedMd)) continue;
13946
+ if (existsSync20(expectedMd) && safeIsFile(expectedMd)) continue;
13829
13947
  out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
13830
13948
  }
13831
13949
  }
@@ -13839,7 +13957,7 @@ function safeIsFile(path) {
13839
13957
 
13840
13958
  // kernel/orchestrator/node-build.ts
13841
13959
  import { createHash } from "crypto";
13842
- import { existsSync as existsSync20 } from "fs";
13960
+ import { existsSync as existsSync21 } from "fs";
13843
13961
  import { isAbsolute as isAbsolute6, resolve as resolvePath } from "path";
13844
13962
  import "js-tiktoken/lite";
13845
13963
  import yaml4 from "js-yaml";
@@ -14003,11 +14121,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
14003
14121
  }
14004
14122
  function resolveAbsoluteMdPath(relativePath2, roots) {
14005
14123
  if (isAbsolute6(relativePath2)) {
14006
- return existsSync20(relativePath2) ? relativePath2 : null;
14124
+ return existsSync21(relativePath2) ? relativePath2 : null;
14007
14125
  }
14008
14126
  for (const root of roots) {
14009
14127
  const candidate = resolvePath(root, relativePath2);
14010
- if (existsSync20(candidate)) return candidate;
14128
+ if (existsSync21(candidate)) return candidate;
14011
14129
  }
14012
14130
  return null;
14013
14131
  }
@@ -14144,6 +14262,7 @@ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextInde
14144
14262
  extractors: wctx.opts.extractors,
14145
14263
  kind,
14146
14264
  provider: provider.id,
14265
+ activeProvider: wctx.opts.activeProvider,
14147
14266
  nodePath: raw.path,
14148
14267
  bodyHash,
14149
14268
  sidecarAnnotationsHash,
@@ -14357,9 +14476,10 @@ async function runScanInternal(_kernel, options) {
14357
14476
  priorIndex: setup.priorIndex,
14358
14477
  priorExtractorRuns: setup.priorExtractorRuns,
14359
14478
  providerFrontmatter: setup.providerFrontmatter,
14360
- pluginStores: options.pluginStores
14479
+ pluginStores: options.pluginStores,
14480
+ activeProvider: resolveActiveProviderOption(options.activeProvider, options.roots)
14361
14481
  });
14362
- walked.internalLinks = dedupeLinks(walked.internalLinks);
14482
+ walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes);
14363
14483
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
14364
14484
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
14365
14485
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -14475,17 +14595,27 @@ function validateRoots(roots) {
14475
14595
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
14476
14596
  }
14477
14597
  for (const root of roots) {
14478
- if (!existsSync21(root) || !statSync7(root).isDirectory()) {
14598
+ if (!existsSync22(root) || !statSync7(root).isDirectory()) {
14479
14599
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
14480
14600
  }
14481
14601
  }
14482
14602
  }
14603
+ function resolveActiveProviderOption(optionValue, roots) {
14604
+ if (optionValue !== void 0) return optionValue;
14605
+ for (const root of roots) {
14606
+ const absRoot = isAbsolute7(root) ? root : resolve27(root);
14607
+ if (!existsSync22(absRoot)) continue;
14608
+ const detected = resolveActiveProvider(absRoot).resolved;
14609
+ if (detected !== null) return detected;
14610
+ }
14611
+ return null;
14612
+ }
14483
14613
 
14484
14614
  // kernel/scan/watcher.ts
14485
- import { resolve as resolve27, relative as relative5, sep as sep4 } from "path";
14615
+ import { resolve as resolve28, relative as relative5, sep as sep4 } from "path";
14486
14616
  import chokidar from "chokidar";
14487
14617
  function createChokidarWatcher(opts) {
14488
- const absRoots = opts.roots.map((r) => resolve27(opts.cwd, r));
14618
+ const absRoots = opts.roots.map((r) => resolve28(opts.cwd, r));
14489
14619
  const ignoreFilterOpt = opts.ignoreFilter;
14490
14620
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
14491
14621
  const ignored = getFilter ? (path) => {
@@ -14704,7 +14834,7 @@ function createKernel() {
14704
14834
 
14705
14835
  // kernel/jobs/orphan-files.ts
14706
14836
  import { readdirSync as readdirSync8, statSync as statSync8 } from "fs";
14707
- import { join as join13, resolve as resolve28 } from "path";
14837
+ import { join as join14, resolve as resolve29 } from "path";
14708
14838
  function findOrphanJobFiles(jobsDir, referencedPaths) {
14709
14839
  let entries;
14710
14840
  try {
@@ -14722,7 +14852,7 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
14722
14852
  if (!entry.isFile()) continue;
14723
14853
  const name = entry.name;
14724
14854
  if (!name.endsWith(".md")) continue;
14725
- const abs = resolve28(join13(jobsDir, name));
14855
+ const abs = resolve29(join14(jobsDir, name));
14726
14856
  if (!referencedPaths.has(abs)) orphans.push(abs);
14727
14857
  }
14728
14858
  orphans.sort();
@@ -14786,7 +14916,48 @@ var SCAN_RUNNER_TEXTS = {
14786
14916
  * does not exist on disk. Surfaced once per missing root so the
14787
14917
  * operator notices a typo without the walker silently swallowing it.
14788
14918
  */
14789
- referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.'
14919
+ referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.',
14920
+ /**
14921
+ * Active-provider bootstrap: filesystem auto-detect found no
14922
+ * markers (`.claude/`, `.gemini/`, `.codex/`, `AGENTS.md`, `.cursor/`)
14923
+ * anywhere under cwd or the effective scan roots. Plain-markdown
14924
+ * projects keep scanning fine; provider-specific extractors silently
14925
+ * no-op for this scan.
14926
+ */
14927
+ activeProviderNoMarkerWarning: "No provider markers detected (.claude/, .gemini/, .codex/, AGENTS.md, .cursor/). Scanning as universal markdown only; provider-specific link types (e.g. claude @-directives, /-commands) will not appear in the graph. Set `activeProvider` in .skill-map/settings.json or install a provider plugin to enable them.",
14928
+ /**
14929
+ * Active-provider bootstrap: filesystem auto-detect found exactly
14930
+ * one marker and persisted the detected id to project settings.
14931
+ */
14932
+ activeProviderAutodetected: "Auto-detected activeProvider = {{id}} from filesystem markers; persisted to .skill-map/settings.json.",
14933
+ /**
14934
+ * Active-provider bootstrap: persistence of the auto-detected id
14935
+ * failed (permission, disk full, etc). Non-fatal; the scan
14936
+ * continues with the value in memory for this run.
14937
+ */
14938
+ activeProviderPersistFailed: "Auto-detected activeProvider = {{id}}, but persisting to .skill-map/settings.json failed: {{message}}. Run `sm config set activeProvider {{id}}` manually to make the choice sticky.",
14939
+ /**
14940
+ * Active-provider bootstrap: ambiguous detection (2+ markers
14941
+ * present), interactive prompt header. Followed by one
14942
+ * `activeProviderPromptOption` per detected provider id.
14943
+ */
14944
+ activeProviderPromptHeader: "Multiple provider markers detected. Pick the active lens for this project:",
14945
+ activeProviderPromptOption: " {{index}}) {{id}}",
14946
+ activeProviderPromptInput: "Enter the number or provider id: ",
14947
+ /**
14948
+ * Active-provider bootstrap: ambiguous detection under `--yes`. The
14949
+ * caller exits non-zero; this message names the candidates and how
14950
+ * to resolve.
14951
+ */
14952
+ activeProviderAmbiguousUnderYes: "Multiple provider markers detected ({{candidates}}) and --yes is set. Set the lens explicitly with `sm config set activeProvider <id>` and re-run.",
14953
+ /**
14954
+ * Active lens points at a bundle the operator has disabled (via
14955
+ * `sm plugins disable <id>` or the Settings UI). Classification keeps
14956
+ * running because it's provider-driven, but the lens-gated extractors
14957
+ * for the disabled bundle silently no-op. Without this warning the
14958
+ * graph quietly differs from what the lens implies.
14959
+ */
14960
+ activeProviderBundleDisabledWarning: 'activeProvider = "{{id}}" but the "{{id}}" plugin bundle is currently disabled; provider-specific extractors will not run. Re-enable the bundle with `sm plugins enable {{id}}` or switch the lens with `sm config set activeProvider <id>` to silence this warning.'
14790
14961
  };
14791
14962
 
14792
14963
  // core/runtime/scan-roots.ts
@@ -14800,7 +14971,7 @@ function resolveScanRoots(inputs) {
14800
14971
  // core/runtime/reference-paths-walker.ts
14801
14972
  import { readdirSync as readdirSync9, statSync as statSync9 } from "fs";
14802
14973
  import { homedir as osHomedir2 } from "os";
14803
- import { isAbsolute as isAbsolute7, join as join14, resolve as resolve29 } from "path";
14974
+ import { isAbsolute as isAbsolute8, join as join15, resolve as resolve30 } from "path";
14804
14975
  var REFERENCE_WALK_MAX_FILES = 5e4;
14805
14976
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14806
14977
  "node_modules",
@@ -14808,10 +14979,10 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14808
14979
  SKILL_MAP_DIR
14809
14980
  ]);
14810
14981
  function resolveScanPath(raw, cwd) {
14811
- if (raw.startsWith("~/")) return resolve29(join14(osHomedir2(), raw.slice(2)));
14812
- if (raw === "~") return resolve29(osHomedir2());
14813
- if (isAbsolute7(raw)) return resolve29(raw);
14814
- return resolve29(cwd, raw);
14982
+ if (raw.startsWith("~/")) return resolve30(join15(osHomedir2(), raw.slice(2)));
14983
+ if (raw === "~") return resolve30(osHomedir2());
14984
+ if (isAbsolute8(raw)) return resolve30(raw);
14985
+ return resolve30(cwd, raw);
14815
14986
  }
14816
14987
  function walkReferencePaths(rawRoots, cwd) {
14817
14988
  const paths = /* @__PURE__ */ new Set();
@@ -14840,7 +15011,7 @@ function walkInto(dir, out) {
14840
15011
  for (const entry of entries) {
14841
15012
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
14842
15013
  if (entry.isSymbolicLink()) continue;
14843
- const full = join14(dir, entry.name);
15014
+ const full = join15(dir, entry.name);
14844
15015
  if (entry.isDirectory()) {
14845
15016
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
14846
15017
  if (walkInto(full, out)) return true;
@@ -14858,6 +15029,101 @@ function safeStat(path) {
14858
15029
  }
14859
15030
  }
14860
15031
 
15032
+ // core/runtime/active-provider-bootstrap.ts
15033
+ import { createInterface as createInterface2 } from "readline";
15034
+ import { isAbsolute as isAbsolute9, join as join16 } from "path";
15035
+ async function bootstrapActiveProvider(opts) {
15036
+ const fromCwd = resolveActiveProvider(opts.cwd);
15037
+ if (fromCwd.source === "config") {
15038
+ return { kind: "ok", activeProvider: fromCwd.resolved, source: "config" };
15039
+ }
15040
+ const detected = aggregateDetected(opts.cwd, opts.effectiveRoots, fromCwd.detected);
15041
+ if (detected.length === 0) {
15042
+ opts.printer.warn(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarning);
15043
+ return { kind: "ok", activeProvider: null, source: "none" };
15044
+ }
15045
+ if (detected.length === 1) {
15046
+ const picked2 = detected[0];
15047
+ persistActiveProvider(opts.cwd, picked2, opts.printer);
15048
+ return { kind: "ok", activeProvider: picked2, source: "autodetect" };
15049
+ }
15050
+ if (opts.yes) {
15051
+ return { kind: "ambiguous", detected };
15052
+ }
15053
+ const picked = await promptForLens(detected, opts.stdin, opts.stderr);
15054
+ if (picked === null) {
15055
+ return { kind: "ambiguous", detected };
15056
+ }
15057
+ persistActiveProvider(opts.cwd, picked, opts.printer);
15058
+ return { kind: "ok", activeProvider: picked, source: "autodetect" };
15059
+ }
15060
+ function aggregateDetected(cwd, effectiveRoots, cwdDetected) {
15061
+ const out = [];
15062
+ const seen = /* @__PURE__ */ new Set();
15063
+ for (const id of cwdDetected) {
15064
+ if (seen.has(id)) continue;
15065
+ seen.add(id);
15066
+ out.push(id);
15067
+ }
15068
+ for (const root of effectiveRoots) {
15069
+ const absRoot = isAbsolute9(root) ? root : join16(cwd, root);
15070
+ const r = resolveActiveProvider(absRoot);
15071
+ for (const id of r.detected) {
15072
+ if (seen.has(id)) continue;
15073
+ seen.add(id);
15074
+ out.push(id);
15075
+ }
15076
+ }
15077
+ return out;
15078
+ }
15079
+ function persistActiveProvider(cwd, id, printer) {
15080
+ try {
15081
+ writeConfigValue("activeProvider", id, { target: "project", cwd });
15082
+ printer.info(tx(SCAN_RUNNER_TEXTS.activeProviderAutodetected, { id }));
15083
+ } catch (err) {
15084
+ const message = err instanceof Error ? err.message : String(err);
15085
+ printer.warn(
15086
+ tx(SCAN_RUNNER_TEXTS.activeProviderPersistFailed, { id, message })
15087
+ );
15088
+ }
15089
+ }
15090
+ function warnIfLensBundleDisabled(args2) {
15091
+ if (args2.activeProvider === null) return;
15092
+ if (args2.resolveEnabled(args2.activeProvider)) return;
15093
+ args2.printer.warn(
15094
+ tx(SCAN_RUNNER_TEXTS.activeProviderBundleDisabledWarning, {
15095
+ id: args2.activeProvider
15096
+ })
15097
+ );
15098
+ }
15099
+ async function promptForLens(detected, stdin, stderr) {
15100
+ const lines = [SCAN_RUNNER_TEXTS.activeProviderPromptHeader];
15101
+ for (let i = 0; i < detected.length; i += 1) {
15102
+ lines.push(
15103
+ tx(SCAN_RUNNER_TEXTS.activeProviderPromptOption, {
15104
+ index: i + 1,
15105
+ id: detected[i]
15106
+ })
15107
+ );
15108
+ }
15109
+ stderr.write(lines.join("\n") + "\n");
15110
+ const rl = createInterface2({ input: stdin, output: stderr });
15111
+ try {
15112
+ const answer = await new Promise(
15113
+ (resolveP) => rl.question(SCAN_RUNNER_TEXTS.activeProviderPromptInput, resolveP)
15114
+ );
15115
+ const trimmed = answer.trim();
15116
+ const asNumber = Number.parseInt(trimmed, 10);
15117
+ if (!Number.isNaN(asNumber) && asNumber >= 1 && asNumber <= detected.length) {
15118
+ return detected[asNumber - 1];
15119
+ }
15120
+ const asId = detected.find((d) => d.toLowerCase() === trimmed.toLowerCase());
15121
+ return asId ?? null;
15122
+ } finally {
15123
+ rl.close();
15124
+ }
15125
+ }
15126
+
14861
15127
  // core/runtime/scan-runner.ts
14862
15128
  async function runScanForCommand(opts) {
14863
15129
  const ctx = opts.ctx ?? defaultRuntimeContext();
@@ -14865,20 +15131,9 @@ async function runScanForCommand(opts) {
14865
15131
  const kernel = createKernel();
14866
15132
  const pluginRuntime = await preparePluginRuntime(opts, opts.printer);
14867
15133
  const extensions = registerExtensions(kernel, pluginRuntime, opts);
14868
- let cfg;
14869
- try {
14870
- cfg = loadConfig({ strict: opts.strict, ...ctx }).effective;
14871
- } catch (err) {
14872
- return { kind: "config-error", message: formatErrorMessage(err) };
14873
- }
14874
- const ignoreFilter = buildScanIgnoreFilter(cfg, ctx.cwd);
14875
- const strict = opts.strict || cfg.scan.strict === true;
14876
- let effectiveRoots;
14877
- try {
14878
- effectiveRoots = resolveScanRoots({ positionalRoots: opts.roots });
14879
- } catch (err) {
14880
- return { kind: "config-error", message: formatErrorMessage(err) };
14881
- }
15134
+ const scanInputs = loadScanInputs(opts, ctx);
15135
+ if ("kind" in scanInputs) return scanInputs;
15136
+ const { cfg, ignoreFilter, strict, effectiveRoots } = scanInputs;
14882
15137
  let referenceablePaths;
14883
15138
  if (cfg.scan.referencePaths.length > 0) {
14884
15139
  const walk2 = walkReferencePaths(cfg.scan.referencePaths, ctx.cwd);
@@ -14887,6 +15142,9 @@ async function runScanForCommand(opts) {
14887
15142
  }
14888
15143
  const loadPrior = makePriorLoader(opts.noBuiltIns, strict);
14889
15144
  const jobsDir = defaultProjectJobsDir(ctx);
15145
+ const lens = await resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime);
15146
+ if (lens.kind === "ambiguous-provider") return lens;
15147
+ const activeProvider = lens.activeProvider;
14890
15148
  const runScanWith = makeScanRunner(
14891
15149
  kernel,
14892
15150
  opts,
@@ -14895,11 +15153,37 @@ async function runScanForCommand(opts) {
14895
15153
  strict,
14896
15154
  extensions,
14897
15155
  referenceablePaths,
14898
- ctx.cwd
15156
+ ctx.cwd,
15157
+ activeProvider
14899
15158
  );
14900
15159
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
14901
15160
  return willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
14902
15161
  }
15162
+ async function resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime) {
15163
+ const bootstrap = await bootstrapActiveProvider({
15164
+ cwd: ctx.cwd,
15165
+ effectiveRoots,
15166
+ yes: opts.yes ?? false,
15167
+ stdin: opts.stdin ?? process.stdin,
15168
+ stderr: opts.stderr,
15169
+ printer: opts.printer
15170
+ });
15171
+ if (bootstrap.kind === "ambiguous") {
15172
+ return {
15173
+ kind: "ambiguous-provider",
15174
+ detected: bootstrap.detected,
15175
+ message: tx(SCAN_RUNNER_TEXTS.activeProviderAmbiguousUnderYes, {
15176
+ candidates: bootstrap.detected.join(", ")
15177
+ })
15178
+ };
15179
+ }
15180
+ warnIfLensBundleDisabled({
15181
+ activeProvider: bootstrap.activeProvider,
15182
+ resolveEnabled: opts.resolveEnabledOverride ?? pluginRuntime.resolveEnabled,
15183
+ printer: opts.printer
15184
+ });
15185
+ return { kind: "ok", activeProvider: bootstrap.activeProvider };
15186
+ }
14903
15187
  function emitReferenceWalkAdvisory(walk2, opts) {
14904
15188
  if (walk2.truncated) {
14905
15189
  opts.printer.warn(SCAN_RUNNER_TEXTS.referenceWalkTruncated);
@@ -14933,6 +15217,17 @@ function registerExtensions(kernel, pluginRuntime, opts) {
14933
15217
  registerEnabledExtensions(kernel, pluginRuntime, registerOpts);
14934
15218
  return extensions;
14935
15219
  }
15220
+ function loadScanInputs(opts, ctx) {
15221
+ try {
15222
+ const cfg = loadConfig({ strict: opts.strict, ...ctx }).effective;
15223
+ const ignoreFilter = buildScanIgnoreFilter(cfg, ctx.cwd);
15224
+ const strict = opts.strict || cfg.scan.strict === true;
15225
+ const effectiveRoots = resolveScanRoots({ positionalRoots: opts.roots });
15226
+ return { cfg, ignoreFilter, strict, effectiveRoots };
15227
+ } catch (err) {
15228
+ return { kind: "config-error", message: formatErrorMessage(err) };
15229
+ }
15230
+ }
14936
15231
  function buildScanIgnoreFilter(cfg, cwd) {
14937
15232
  const ignoreFileText = readIgnoreFileText(cwd);
14938
15233
  const ignoreFilterOpts = {};
@@ -14955,7 +15250,7 @@ function makePriorLoader(noBuiltIns, strict) {
14955
15250
  return loaded;
14956
15251
  };
14957
15252
  }
14958
- function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd) {
15253
+ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider) {
14959
15254
  return async (prior, priorExtractorRuns, orphanJobFiles) => {
14960
15255
  if (opts.changed && prior === null) {
14961
15256
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
@@ -14969,6 +15264,7 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
14969
15264
  referenceablePaths,
14970
15265
  cwd: scanCwd,
14971
15266
  prior,
15267
+ activeProvider,
14972
15268
  ...priorExtractorRuns ? { priorExtractorRuns } : {},
14973
15269
  ...orphanJobFiles ? { orphanJobFiles } : {}
14974
15270
  });
@@ -14989,7 +15285,8 @@ function buildRunScanOptions(args2) {
14989
15285
  // visible from this caller" (legacy behaviour). The orchestrator
14990
15286
  // defaults to `[]` when the field is absent; we always pass the
14991
15287
  // array (possibly empty) to keep the wiring uniform.
14992
- orphanJobFiles: orphanJobFiles ?? []
15288
+ orphanJobFiles: orphanJobFiles ?? [],
15289
+ activeProvider: args2.activeProvider
14993
15290
  };
14994
15291
  if (args2.extensions) runOptions.extensions = args2.extensions;
14995
15292
  if (prior) {
@@ -15132,7 +15429,7 @@ var InitCommand = class extends SmCommand {
15132
15429
  async run() {
15133
15430
  const ctx = defaultRuntimeContext();
15134
15431
  const scopeRoot = ctx.cwd;
15135
- const skillMapDir = join15(scopeRoot, SKILL_MAP_DIR);
15432
+ const skillMapDir = join17(scopeRoot, SKILL_MAP_DIR);
15136
15433
  const settingsPath = defaultSettingsPath(scopeRoot);
15137
15434
  const localPath = defaultLocalSettingsPath(scopeRoot);
15138
15435
  const ignorePath = defaultIgnoreFilePath(scopeRoot);
@@ -15178,7 +15475,7 @@ var InitCommand = class extends SmCommand {
15178
15475
  const okGlyph = ansi.green("\u2713");
15179
15476
  const updated = await ensureGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
15180
15477
  if (updated) {
15181
- const gitignorePath = join15(scopeRoot, ".gitignore");
15478
+ const gitignorePath = join17(scopeRoot, ".gitignore");
15182
15479
  printer.info(
15183
15480
  GITIGNORE_ENTRIES.length === 1 ? tx(INIT_TEXTS.gitignoreUpdatedSingular, { glyph: okGlyph, path: gitignorePath }) : tx(INIT_TEXTS.gitignoreUpdatedPlural, {
15184
15481
  glyph: okGlyph,
@@ -15217,7 +15514,7 @@ async function dryRunFileMessage(path) {
15217
15514
  }
15218
15515
  async function writeDryRunGitignorePlan(printer, scopeRoot) {
15219
15516
  const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
15220
- const gitignorePath = join15(scopeRoot, ".gitignore");
15517
+ const gitignorePath = join17(scopeRoot, ".gitignore");
15221
15518
  if (wouldAdd.length === 0) {
15222
15519
  printer.info(tx(INIT_TEXTS.dryRunWouldLeaveGitignoreUnchanged, { path: gitignorePath }));
15223
15520
  } else if (wouldAdd.length === 1) {
@@ -15256,7 +15553,14 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
15256
15553
  strict,
15257
15554
  stderr,
15258
15555
  printer,
15259
- ctx: { cwd: scopeRoot }
15556
+ ctx: { cwd: scopeRoot },
15557
+ // Init's first scan is a provisioning step, not the user's
15558
+ // primary "show me my graph" call. Don't block waiting for the
15559
+ // operator to disambiguate the lens here; let init complete with
15560
+ // `activeProvider` unset and let the FIRST explicit `sm scan`
15561
+ // surface the prompt. Treat the `ambiguous-provider` outcome below
15562
+ // as a soft hint, not a failure.
15563
+ yes: true
15260
15564
  });
15261
15565
  const errGlyph = ansi.red("\u2715");
15262
15566
  if (outcome.kind === "config-error") {
@@ -15276,6 +15580,10 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
15276
15580
  );
15277
15581
  return ExitCode.Error;
15278
15582
  }
15583
+ if (outcome.kind === "ambiguous-provider") {
15584
+ printer.warn(outcome.message);
15585
+ return ExitCode.Ok;
15586
+ }
15279
15587
  const result = outcome.result;
15280
15588
  const hasErrors = result.issues.some((i) => i.severity === "error");
15281
15589
  printer.info(
@@ -15292,7 +15600,7 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
15292
15600
  return hasErrors ? ExitCode.Issues : ExitCode.Ok;
15293
15601
  }
15294
15602
  async function previewGitignoreEntries(scopeRoot, entries) {
15295
- const path = join15(scopeRoot, ".gitignore");
15603
+ const path = join17(scopeRoot, ".gitignore");
15296
15604
  const body = await pathExists(path) ? await readFile2(path, "utf8") : "";
15297
15605
  const present = new Set(
15298
15606
  body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
@@ -15300,7 +15608,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
15300
15608
  return entries.filter((entry) => !present.has(entry));
15301
15609
  }
15302
15610
  async function ensureGitignoreEntries(scopeRoot, entries) {
15303
- const path = join15(scopeRoot, ".gitignore");
15611
+ const path = join17(scopeRoot, ".gitignore");
15304
15612
  let body = "";
15305
15613
  if (await pathExists(path)) {
15306
15614
  body = await readFile2(path, "utf8");
@@ -16865,8 +17173,8 @@ var PLUGINS_TEXTS = {
16865
17173
  doctorIssueEntry: " {{glyph}} {{id}} {{status}}\n",
16866
17174
  doctorIssueBody: " {{line}}\n",
16867
17175
  // --- enable / disable -----------------------------------------------
16868
- toggleBothIdAndAll: "{{glyph}} Pass either an <id> or --all, not both.\n",
16869
- toggleNeitherIdNorAll: "{{glyph}} Pass <id> or --all.\n",
17176
+ toggleBothIdAndAll: "{{glyph}} Pass either one or more <id> arguments or --all, not both.\n",
17177
+ toggleNeitherIdNorAll: "{{glyph}} Pass one or more <id> arguments, or --all.\n",
16870
17178
  toggleResolveError: "{{error}}",
16871
17179
  toggleAppliedSingle: "{{verbPast}}: {{id}}\n",
16872
17180
  toggleAppliedManyHeader: "{{verbPast}}: {{count}} plugin(s)\n",
@@ -16968,9 +17276,9 @@ var PLUGINS_TEXTS = {
16968
17276
  };
16969
17277
 
16970
17278
  // cli/commands/plugins/shared.ts
16971
- import { resolve as resolve30 } from "path";
17279
+ import { resolve as resolve31 } from "path";
16972
17280
  function resolveSearchPaths2(opts, cwd) {
16973
- if (opts.pluginDir) return [resolve30(opts.pluginDir)];
17281
+ if (opts.pluginDir) return [resolve31(opts.pluginDir)];
16974
17282
  return [defaultProjectPluginsDir({ cwd })];
16975
17283
  }
16976
17284
  async function buildResolver() {
@@ -17852,7 +18160,7 @@ function buildDoctorJsonEnvelope(args2) {
17852
18160
  import { Command as Command25, Option as Option24 } from "clipanion";
17853
18161
  var TogglePluginsBase = class extends SmCommand {
17854
18162
  all = Option24.Boolean("--all", false);
17855
- id = Option24.String({ required: false });
18163
+ ids = Option24.Rest({ name: "ids" });
17856
18164
  async toggle(enabled) {
17857
18165
  const verb = enabled ? "enable" : "disable";
17858
18166
  const stderrAnsi = this.ansiFor("stderr");
@@ -17871,23 +18179,24 @@ var TogglePluginsBase = class extends SmCommand {
17871
18179
  return ExitCode.Ok;
17872
18180
  }
17873
18181
  /**
17874
- * `--all` vs `<id>` mutex check. The two are mutually exclusive and
17875
- * one must be present; surfaces a directed error on misuse.
18182
+ * `--all` vs `<id>...` mutex check. The two are mutually exclusive
18183
+ * and one must be present; surfaces a directed error on misuse.
18184
+ * Variadic positional accepts one or more ids.
17876
18185
  */
17877
18186
  #validateArgs(ansi) {
17878
18187
  const errGlyph = ansi.red("\u2715");
17879
- if (this.all && this.id) {
18188
+ if (this.all && this.ids.length > 0) {
17880
18189
  this.printer.error(tx(PLUGINS_TEXTS.toggleBothIdAndAll, { glyph: errGlyph }));
17881
18190
  return ExitCode.Error;
17882
18191
  }
17883
- if (!this.all && !this.id) {
18192
+ if (!this.all && this.ids.length === 0) {
17884
18193
  this.printer.error(tx(PLUGINS_TEXTS.toggleNeitherIdNorAll, { glyph: errGlyph }));
17885
18194
  return ExitCode.Error;
17886
18195
  }
17887
18196
  return null;
17888
18197
  }
17889
18198
  /**
17890
- * Resolve `<id>` against the catalogue or fan out via `--all`.
18199
+ * Resolve `<id>...` against the catalogue or fan out via `--all`.
17891
18200
  * Returns the target list on success, or the exit code on a
17892
18201
  * directed-error path (unknown id, granularity mismatch).
17893
18202
  *
@@ -17898,25 +18207,35 @@ var TogglePluginsBase = class extends SmCommand {
17898
18207
  * the directed error message when they try the bundle id directly,
17899
18208
  * so `--all` skips them here too and the real "disable every core
17900
18209
  * extension" intent is served by `--no-built-ins` on `sm scan`.
18210
+ *
18211
+ * Variadic mode is all-or-nothing: the first bad id aborts the
18212
+ * batch before any DB write, so the user never lands in a partial
18213
+ * state. Repeated ids in the same call are deduped.
17901
18214
  */
17902
18215
  #pickTargets(catalogue, verb, ansi) {
17903
18216
  if (this.all) {
17904
18217
  return catalogue.filter((b) => b.granularity === "bundle").map((b) => b.id);
17905
18218
  }
17906
- const resolved = resolveToggleTarget(this.id, catalogue, verb, ansi);
17907
- if ("error" in resolved) {
17908
- this.printer.error(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
17909
- return ExitCode.NotFound;
18219
+ const keys = [];
18220
+ for (const rawId of this.ids) {
18221
+ const resolved = resolveToggleTarget(rawId, catalogue, verb, ansi);
18222
+ if ("error" in resolved) {
18223
+ this.printer.error(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
18224
+ return ExitCode.NotFound;
18225
+ }
18226
+ keys.push(resolved.key);
17910
18227
  }
17911
- return [resolved.key];
18228
+ return [...new Set(keys)];
17912
18229
  }
17913
18230
  /**
17914
- * Host lock, see `src/kernel/config/locked-plugins.ts`. `--all`
17915
- * silently skips locked targets so the user can still toggle the
17916
- * rest. Single-id mode surfaces a directed exit-5 message.
18231
+ * Host lock, see `src/kernel/config/locked-plugins.ts`. Bulk modes
18232
+ * (`--all` or an explicit batch of >1 ids) silently skip locked
18233
+ * targets so the user can still toggle the rest. Single-id mode
18234
+ * surfaces a directed exit-5 message so the user knows their one
18235
+ * intended target was refused.
17917
18236
  */
17918
18237
  #applyLockGate(targets, ansi) {
17919
- if (this.all) return targets.filter((id) => !isPluginLocked(id));
18238
+ if (this.all || this.ids.length > 1) return targets.filter((id) => !isPluginLocked(id));
17920
18239
  const lockedHit = targets.find((id) => isPluginLocked(id));
17921
18240
  if (!lockedHit) return targets;
17922
18241
  this.printer.error(
@@ -17972,12 +18291,19 @@ var PluginsEnableCommand = class extends TogglePluginsBase {
17972
18291
  static paths = [["plugins", "enable"]];
17973
18292
  static usage = Command25.Usage({
17974
18293
  category: "Plugins",
17975
- description: "Enable a plugin (or --all). Persists in config_plugins.",
18294
+ description: "Enable one or more plugins (or --all). Persists in config_plugins.",
17976
18295
  details: `
17977
- Writes a row to config_plugins with enabled=1. Takes precedence
17978
- over the team-shared baseline at settings.json#/plugins/<id>/enabled.
17979
- Use sm plugins disable to flip; sm config reset plugins.<id>.enabled
17980
- drops the settings.json baseline.
18296
+ Writes a row to config_plugins with enabled=1 per id. Takes
18297
+ precedence over the team-shared baseline at
18298
+ settings.json#/plugins/<id>/enabled. Use sm plugins disable to
18299
+ flip; sm config reset plugins.<id>.enabled drops the settings.json
18300
+ baseline.
18301
+
18302
+ Accepts one or more ids in one call, e.g.
18303
+ 'sm plugins enable claude gemini openai'. Batches are
18304
+ all-or-nothing: a single unknown / mismatched id aborts before
18305
+ any write. Repeated ids are deduped. Locked plugins inside a
18306
+ batch are silently skipped.
17981
18307
 
17982
18308
  Granularity: a bundle-granularity plugin (default for user plugins,
17983
18309
  and the built-in 'claude' bundle) accepts only the bundle id. An
@@ -17994,12 +18320,18 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
17994
18320
  static paths = [["plugins", "disable"]];
17995
18321
  static usage = Command25.Usage({
17996
18322
  category: "Plugins",
17997
- description: "Disable a plugin (or --all). Persists in config_plugins; does not delete files.",
18323
+ description: "Disable one or more plugins (or --all). Persists in config_plugins; does not delete files.",
17998
18324
  details: `
17999
- Writes a row to config_plugins with enabled=0. Discovery still
18000
- surfaces the plugin in sm plugins list, but with status=disabled;
18001
- its extensions are not imported and the kernel will not run
18002
- them.
18325
+ Writes a row to config_plugins with enabled=0 per id. Discovery
18326
+ still surfaces the plugin in sm plugins list, but with
18327
+ status=disabled; its extensions are not imported and the kernel
18328
+ will not run them.
18329
+
18330
+ Accepts one or more ids in one call, e.g.
18331
+ 'sm plugins disable gemini openai agent-skills'. Batches are
18332
+ all-or-nothing: a single unknown / mismatched id aborts before
18333
+ any write. Repeated ids are deduped. Locked plugins inside a
18334
+ batch are silently skipped.
18003
18335
 
18004
18336
  Granularity: a bundle-granularity plugin (default for user plugins,
18005
18337
  and the built-in 'claude' bundle) accepts only the bundle id. An
@@ -18105,8 +18437,8 @@ function resolveBareToggle(id, catalogue, verb, ansi) {
18105
18437
  }
18106
18438
 
18107
18439
  // cli/commands/plugins/create.ts
18108
- import { existsSync as existsSync22, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
18109
- import { join as join16, resolve as resolve31 } from "path";
18440
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
18441
+ import { join as join18, resolve as resolve32 } from "path";
18110
18442
  import { Command as Command26, Option as Option25 } from "clipanion";
18111
18443
  var PluginsCreateCommand = class extends SmCommand {
18112
18444
  static paths = [["plugins", "create"]];
@@ -18132,8 +18464,8 @@ var PluginsCreateCommand = class extends SmCommand {
18132
18464
  }
18133
18465
  const ctx = defaultRuntimeContext();
18134
18466
  const baseDir = defaultProjectPluginsDir(ctx);
18135
- const targetDir = this.at ? resolve31(this.at) : join16(baseDir, this.pluginId);
18136
- if (existsSync22(targetDir) && !this.force) {
18467
+ const targetDir = this.at ? resolve32(this.at) : join18(baseDir, this.pluginId);
18468
+ if (existsSync23(targetDir) && !this.force) {
18137
18469
  this.printer.error(
18138
18470
  tx(PLUGINS_TEXTS.createRefuseOverwrite, {
18139
18471
  glyph: errGlyph,
@@ -18143,7 +18475,7 @@ var PluginsCreateCommand = class extends SmCommand {
18143
18475
  return ExitCode.Error;
18144
18476
  }
18145
18477
  const extractorName = `${this.pluginId}-extractor`;
18146
- mkdirSync6(join16(targetDir, "extractors", extractorName), { recursive: true });
18478
+ mkdirSync6(join18(targetDir, "extractors", extractorName), { recursive: true });
18147
18479
  const specVersion = installedSpecVersion();
18148
18480
  const manifest = {
18149
18481
  id: this.pluginId,
@@ -18163,14 +18495,14 @@ var PluginsCreateCommand = class extends SmCommand {
18163
18495
  }
18164
18496
  };
18165
18497
  writeFileSync3(
18166
- join16(targetDir, "plugin.json"),
18498
+ join18(targetDir, "plugin.json"),
18167
18499
  JSON.stringify(manifest, null, 2) + "\n"
18168
18500
  );
18169
18501
  writeFileSync3(
18170
- join16(targetDir, "extractors", extractorName, "index.js"),
18502
+ join18(targetDir, "extractors", extractorName, "index.js"),
18171
18503
  scaffolderExtractorStub(extractorName)
18172
18504
  );
18173
- writeFileSync3(join16(targetDir, "README.md"), scaffolderReadme(this.pluginId));
18505
+ writeFileSync3(join18(targetDir, "README.md"), scaffolderReadme(this.pluginId));
18174
18506
  this.printer.data(
18175
18507
  tx(PLUGINS_TEXTS.createSuccess, {
18176
18508
  targetDir: sanitizeForTerminal(targetDir),
@@ -18373,7 +18705,7 @@ var PLUGIN_COMMANDS = [
18373
18705
 
18374
18706
  // cli/commands/refresh.ts
18375
18707
  import { readFile as readFile3 } from "fs/promises";
18376
- import { resolve as resolve32 } from "path";
18708
+ import { resolve as resolve33 } from "path";
18377
18709
  import { Command as Command29, Option as Option27 } from "clipanion";
18378
18710
 
18379
18711
  // cli/i18n/refresh.texts.ts
@@ -18657,7 +18989,7 @@ var RefreshCommand = class extends SmCommand {
18657
18989
  let body;
18658
18990
  try {
18659
18991
  assertContained(cwd, node.path);
18660
- const raw = await readFile3(resolve32(cwd, node.path), "utf8");
18992
+ const raw = await readFile3(resolve33(cwd, node.path), "utf8");
18661
18993
  body = stripFrontmatterFence(raw);
18662
18994
  } catch (err) {
18663
18995
  if (!this.json) {
@@ -19398,6 +19730,9 @@ var ScanCommand = class extends SmCommand {
19398
19730
  watch = Option29.Boolean("--watch", false, {
19399
19731
  description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
19400
19732
  });
19733
+ yes = Option29.Boolean("--yes", false, {
19734
+ description: "Non-interactive mode for ambiguous activeProvider auto-detect. With `--yes`, multiple provider markers (.claude/, .gemini/, .codex/, AGENTS.md, .cursor/) under the scan tree exit non-zero instead of prompting the operator. Set the lens manually via `sm config set activeProvider <id>` and re-run."
19735
+ });
19401
19736
  // Each branch in the orchestrator maps to one validation gate
19402
19737
  // (--watch alias / --changed mutex / -g mutex / dispatch).
19403
19738
  // Splitting per branch scatters the gate from the value it gates.
@@ -19426,9 +19761,11 @@ var ScanCommand = class extends SmCommand {
19426
19761
  allowEmpty: this.allowEmpty,
19427
19762
  strict: this.strict,
19428
19763
  stderr: this.context.stderr,
19764
+ stdin: this.context.stdin,
19429
19765
  printer: this.printer,
19430
19766
  killSwitches: readConformanceKillSwitches(),
19431
- colorEnabled
19767
+ colorEnabled,
19768
+ yes: this.yes
19432
19769
  });
19433
19770
  return outcome.kind === "ok" ? this.renderOutcome(outcome.result, outcome.persistedTo, outcome.dbPath, outcome.strict) : this.renderFailure(outcome);
19434
19771
  }
@@ -19499,6 +19836,12 @@ var ScanCommand = class extends SmCommand {
19499
19836
  );
19500
19837
  return ExitCode.Error;
19501
19838
  }
19839
+ if (outcome.kind === "ambiguous-provider") {
19840
+ this.printer.info(
19841
+ tx(SCAN_TEXTS.scanFailure, { glyph: errGlyph, message: outcome.message })
19842
+ );
19843
+ return ExitCode.Error;
19844
+ }
19502
19845
  this.printer.info(
19503
19846
  tx(SCAN_TEXTS.scanFailure, { glyph: errGlyph, message: outcome.message })
19504
19847
  );
@@ -19581,7 +19924,7 @@ function plural(count, word) {
19581
19924
  }
19582
19925
 
19583
19926
  // cli/commands/scan-compare.ts
19584
- import { existsSync as existsSync23, readFileSync as readFileSync18 } from "fs";
19927
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
19585
19928
  import { Command as Command32, Option as Option30 } from "clipanion";
19586
19929
  var ScanCompareCommand = class extends SmCommand {
19587
19930
  static paths = [["scan", "compare-with"]];
@@ -19693,7 +20036,7 @@ var ScanCompareCommand = class extends SmCommand {
19693
20036
  }
19694
20037
  };
19695
20038
  function loadAndValidateDump(path) {
19696
- if (!existsSync23(path)) {
20039
+ if (!existsSync24(path)) {
19697
20040
  throw new Error(tx(SCAN_TEXTS.compareDumpNotFound, { path }));
19698
20041
  }
19699
20042
  let raw;
@@ -20536,7 +20879,7 @@ function contentTypeFor(format) {
20536
20879
  }
20537
20880
 
20538
20881
  // server/health.ts
20539
- import { existsSync as existsSync24 } from "fs";
20882
+ import { existsSync as existsSync25 } from "fs";
20540
20883
  var FALLBACK_SCHEMA_VERSION = "1";
20541
20884
  function buildHealth(deps) {
20542
20885
  return {
@@ -20544,7 +20887,7 @@ function buildHealth(deps) {
20544
20887
  schemaVersion: FALLBACK_SCHEMA_VERSION,
20545
20888
  specVersion: deps.specVersion,
20546
20889
  implVersion: VERSION,
20547
- db: existsSync24(deps.dbPath) ? "present" : "missing",
20890
+ db: existsSync25(deps.dbPath) ? "present" : "missing",
20548
20891
  cwd: deps.cwd,
20549
20892
  dbPath: deps.dbPath
20550
20893
  };
@@ -20657,9 +21000,9 @@ import { HTTPException as HTTPException6 } from "hono/http-exception";
20657
21000
 
20658
21001
  // server/node-body.ts
20659
21002
  import { readFile as readFile4 } from "fs/promises";
20660
- import { isAbsolute as isAbsolute8, resolve as resolvePath2, relative as relativePath, sep as sep5 } from "path";
21003
+ import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as sep5 } from "path";
20661
21004
  async function readNodeBody(cwd, relPath) {
20662
- if (isAbsolute8(relPath)) return null;
21005
+ if (isAbsolute10(relPath)) return null;
20663
21006
  const absRoot = resolvePath2(cwd);
20664
21007
  const absFile = resolvePath2(absRoot, relPath);
20665
21008
  const rel = relativePath(absRoot, absFile);
@@ -21379,12 +21722,12 @@ var parsePatchBody2 = makeBodyValidator(PATCH_BODY_SCHEMA, {
21379
21722
  import { HTTPException as HTTPException10 } from "hono/http-exception";
21380
21723
 
21381
21724
  // server/util/skillmapignore-io.ts
21382
- import { existsSync as existsSync25, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
21383
- import { resolve as resolve33 } from "path";
21725
+ import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
21726
+ import { resolve as resolve34 } from "path";
21384
21727
  var IGNORE_FILENAME2 = ".skillmapignore";
21385
21728
  function readPatterns(cwd) {
21386
- const path = resolve33(cwd, IGNORE_FILENAME2);
21387
- if (!existsSync25(path)) return [];
21729
+ const path = resolve34(cwd, IGNORE_FILENAME2);
21730
+ if (!existsSync26(path)) return [];
21388
21731
  let raw;
21389
21732
  try {
21390
21733
  raw = readFileSync19(path, "utf8");
@@ -21394,8 +21737,8 @@ function readPatterns(cwd) {
21394
21737
  return raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
21395
21738
  }
21396
21739
  function writePatterns(cwd, nextPatterns) {
21397
- const path = resolve33(cwd, IGNORE_FILENAME2);
21398
- const prior = existsSync25(path) ? safeRead(path) : "";
21740
+ const path = resolve34(cwd, IGNORE_FILENAME2);
21741
+ const prior = existsSync26(path) ? safeRead(path) : "";
21399
21742
  const content = buildContent(prior, nextPatterns);
21400
21743
  writeFileSync4(path, content, "utf8");
21401
21744
  }
@@ -21747,41 +22090,6 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
21747
22090
  // server/routes/active-provider.ts
21748
22091
  import { existsSync as existsSync27 } from "fs";
21749
22092
  import { HTTPException as HTTPException12 } from "hono/http-exception";
21750
-
21751
- // core/config/active-provider.ts
21752
- import { existsSync as existsSync26 } from "fs";
21753
- import { join as join17 } from "path";
21754
- var DETECTION_RULES = [
21755
- { providerId: "claude", marker: ".claude" },
21756
- { providerId: "gemini", marker: ".gemini" },
21757
- { providerId: "openai", marker: ".codex" },
21758
- { providerId: "openai", marker: "AGENTS.md" },
21759
- { providerId: "cursor", marker: ".cursor" }
21760
- ];
21761
- function resolveActiveProvider(cwd) {
21762
- const detected = detectProvidersFromFilesystem(cwd);
21763
- const fromConfig = readConfigValue("activeProvider", { cwd });
21764
- if (typeof fromConfig === "string" && fromConfig.length > 0) {
21765
- return { resolved: fromConfig, source: "config", detected };
21766
- }
21767
- if (detected.length > 0) {
21768
- return { resolved: detected[0], source: "autodetect", detected };
21769
- }
21770
- return { resolved: null, source: "none", detected };
21771
- }
21772
- function detectProvidersFromFilesystem(cwd) {
21773
- const seen = /* @__PURE__ */ new Set();
21774
- const out = [];
21775
- for (const rule of DETECTION_RULES) {
21776
- if (seen.has(rule.providerId)) continue;
21777
- if (!existsSync26(join17(cwd, rule.marker))) continue;
21778
- seen.add(rule.providerId);
21779
- out.push(rule.providerId);
21780
- }
21781
- return out;
21782
- }
21783
-
21784
- // server/routes/active-provider.ts
21785
22093
  function registerActiveProviderRoute(app, deps) {
21786
22094
  app.get("/api/active-provider", (c) => {
21787
22095
  return c.json(buildEnvelope4(deps));
@@ -21850,14 +22158,14 @@ async function withScanMutex(fn) {
21850
22158
  if (inFlight !== null) {
21851
22159
  throw new ScanBusyError();
21852
22160
  }
21853
- let resolve38;
22161
+ let resolve39;
21854
22162
  inFlight = new Promise((r) => {
21855
- resolve38 = r;
22163
+ resolve39 = r;
21856
22164
  });
21857
22165
  try {
21858
22166
  return await fn();
21859
22167
  } finally {
21860
- resolve38();
22168
+ resolve39();
21861
22169
  inFlight = null;
21862
22170
  }
21863
22171
  }
@@ -22023,7 +22331,11 @@ async function runPersistedScan(c, deps) {
22023
22331
  pluginRuntime: deps.pluginRuntime,
22024
22332
  resolveEnabledOverride,
22025
22333
  printer: bffScanRunnerPrinter,
22026
- emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
22334
+ emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster),
22335
+ // BFF has no TTY; ambiguous activeProvider must be resolved by
22336
+ // the operator via the Settings UI (PATCH /api/active-provider)
22337
+ // before the scan, not via interactive prompt here.
22338
+ yes: true
22027
22339
  });
22028
22340
  if (outcome.kind !== "ok") {
22029
22341
  throw new HTTPException13(500, {
@@ -22124,7 +22436,10 @@ async function runFreshScan(deps) {
22124
22436
  // fallback. The fresh-scan response body IS the ScanResult JSON,
22125
22437
  // so `data` is never used here; warn/info/error route through
22126
22438
  // `log.warn` (same surface the rest of the BFF uses).
22127
- printer: bffScanRunnerPrinter
22439
+ printer: bffScanRunnerPrinter,
22440
+ // BFF has no TTY; ambiguous activeProvider is the operator's
22441
+ // problem to resolve via the Settings UI, not via prompt here.
22442
+ yes: true
22128
22443
  });
22129
22444
  if (outcome.kind !== "ok") {
22130
22445
  throw new HTTPException13(500, {
@@ -22162,7 +22477,7 @@ function emptyScanResult() {
22162
22477
 
22163
22478
  // server/routes/sidecar.ts
22164
22479
  import { HTTPException as HTTPException14 } from "hono/http-exception";
22165
- import { resolve as resolve34 } from "path";
22480
+ import { resolve as resolve35 } from "path";
22166
22481
  var STATUS_FRESH = "fresh";
22167
22482
  var ENVELOPE_KIND2 = "sidecar.bumped";
22168
22483
  var BUMP_BODY_SCHEMA = {
@@ -22196,7 +22511,7 @@ function registerSidecarRoutes(app, deps) {
22196
22511
  let absPath;
22197
22512
  try {
22198
22513
  assertContained(deps.runtimeContext.cwd, node.path);
22199
- absPath = resolve34(deps.runtimeContext.cwd, node.path);
22514
+ absPath = resolve35(deps.runtimeContext.cwd, node.path);
22200
22515
  } catch (err) {
22201
22516
  throw new HTTPException14(500, { message: formatErrorMessage(err) });
22202
22517
  }
@@ -22322,7 +22637,7 @@ function registerUpdateStatusRoute(app, deps) {
22322
22637
  // server/static.ts
22323
22638
  import { existsSync as existsSync28 } from "fs";
22324
22639
  import { readFile as readFile5 } from "fs/promises";
22325
- import { extname, join as join18 } from "path";
22640
+ import { extname, join as join19 } from "path";
22326
22641
  import { serveStatic } from "@hono/node-server/serve-static";
22327
22642
  var INDEX_HTML = "index.html";
22328
22643
  var PLACEHOLDER_HTML = `<!doctype html>
@@ -22374,7 +22689,7 @@ function createSpaFallback(opts) {
22374
22689
  return async (c, _next) => {
22375
22690
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
22376
22691
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
22377
- const indexPath = join18(opts.uiDist, INDEX_HTML);
22692
+ const indexPath = join19(opts.uiDist, INDEX_HTML);
22378
22693
  if (!existsSync28(indexPath)) return htmlResponse(c, placeholder);
22379
22694
  return fileResponse(c, indexPath);
22380
22695
  };
@@ -22948,9 +23263,9 @@ function validateNoUi(noUi, uiDist) {
22948
23263
 
22949
23264
  // server/paths.ts
22950
23265
  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";
23266
+ import { dirname as dirname18, isAbsolute as isAbsolute11, join as join20, resolve as resolve36 } from "path";
22952
23267
  import { fileURLToPath as fileURLToPath5 } from "url";
22953
- var DEFAULT_UI_REL = join19("ui", "dist", "ui", "browser");
23268
+ var DEFAULT_UI_REL = join20("ui", "dist", "ui", "browser");
22954
23269
  var PACKAGE_UI_REL = "ui";
22955
23270
  var INDEX_HTML2 = "index.html";
22956
23271
  function resolveDefaultUiDist(ctx) {
@@ -22959,13 +23274,13 @@ function resolveDefaultUiDist(ctx) {
22959
23274
  return walkUpForUi(ctx.cwd);
22960
23275
  }
22961
23276
  function resolveExplicitUiDist(ctx, raw) {
22962
- return isAbsolute9(raw) ? raw : resolve35(ctx.cwd, raw);
23277
+ return isAbsolute11(raw) ? raw : resolve36(ctx.cwd, raw);
22963
23278
  }
22964
23279
  function isUiBundleDir(path) {
22965
23280
  if (!existsSync29(path)) return false;
22966
23281
  try {
22967
23282
  if (!statSync11(path).isDirectory()) return false;
22968
- return existsSync29(join19(path, INDEX_HTML2));
23283
+ return existsSync29(join20(path, INDEX_HTML2));
22969
23284
  } catch {
22970
23285
  return false;
22971
23286
  }
@@ -22982,9 +23297,9 @@ function resolvePackageBundledUi() {
22982
23297
  function resolvePackageBundledUiFrom(here) {
22983
23298
  let current = here;
22984
23299
  for (let i = 0; i < 8; i++) {
22985
- const candidate = join19(current, PACKAGE_UI_REL);
23300
+ const candidate = join20(current, PACKAGE_UI_REL);
22986
23301
  if (isUiBundleDir(candidate)) return candidate;
22987
- const distHere = join19(current, "dist", PACKAGE_UI_REL);
23302
+ const distHere = join20(current, "dist", PACKAGE_UI_REL);
22988
23303
  if (isUiBundleDir(distHere)) return distHere;
22989
23304
  const parent = dirname18(current);
22990
23305
  if (parent === current) return null;
@@ -22993,9 +23308,9 @@ function resolvePackageBundledUiFrom(here) {
22993
23308
  return null;
22994
23309
  }
22995
23310
  function walkUpForUi(startDir) {
22996
- let current = resolve35(startDir);
23311
+ let current = resolve36(startDir);
22997
23312
  for (let i = 0; i < 64; i++) {
22998
- const candidate = join19(current, DEFAULT_UI_REL);
23313
+ const candidate = join20(current, DEFAULT_UI_REL);
22999
23314
  if (isUiBundleDir(candidate)) return candidate;
23000
23315
  const parent = dirname18(current);
23001
23316
  if (parent === current) return null;
@@ -23197,7 +23512,7 @@ var SERVE_TEXTS = {
23197
23512
  };
23198
23513
 
23199
23514
  // cli/util/serve-banner.ts
23200
- import { relative as relative7, isAbsolute as isAbsolute10 } from "path";
23515
+ import { relative as relative7, isAbsolute as isAbsolute12 } from "path";
23201
23516
  var ESC2 = {
23202
23517
  reset: "\x1B[0m",
23203
23518
  bold: "\x1B[1m",
@@ -23331,9 +23646,9 @@ function resolveAnsi(colorEnabled) {
23331
23646
  }
23332
23647
  function formatDbPath(dbPath, cwd) {
23333
23648
  const safe = sanitizeForTerminal(dbPath);
23334
- if (!isAbsolute10(safe)) return safe;
23649
+ if (!isAbsolute12(safe)) return safe;
23335
23650
  const rel = relative7(cwd, safe);
23336
- if (rel === "" || rel.startsWith("..") || isAbsolute10(rel)) {
23651
+ if (rel === "" || rel.startsWith("..") || isAbsolute12(rel)) {
23337
23652
  return safe;
23338
23653
  }
23339
23654
  return rel;
@@ -23933,7 +24248,7 @@ function rankConfidenceForGrouping(c) {
23933
24248
 
23934
24249
  // cli/commands/sidecar.ts
23935
24250
  import { existsSync as existsSync31, unlinkSync as unlinkSync2 } from "fs";
23936
- import { resolve as resolve36 } from "path";
24251
+ import { resolve as resolve37 } from "path";
23937
24252
  import { Command as Command35, Option as Option33 } from "clipanion";
23938
24253
 
23939
24254
  // cli/i18n/sidecar.texts.ts
@@ -24084,7 +24399,7 @@ var SidecarRefreshCommand = class extends SmCommand {
24084
24399
  let absPath;
24085
24400
  try {
24086
24401
  assertContained(ctx.cwd, node.path);
24087
- absPath = resolve36(ctx.cwd, node.path);
24402
+ absPath = resolve37(ctx.cwd, node.path);
24088
24403
  } catch (err) {
24089
24404
  this.printer.error(
24090
24405
  tx(SIDECAR_TEXTS.refreshFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -24365,7 +24680,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
24365
24680
  let absPath;
24366
24681
  try {
24367
24682
  assertContained(ctx.cwd, node.path);
24368
- absPath = resolve36(ctx.cwd, node.path);
24683
+ absPath = resolve37(ctx.cwd, node.path);
24369
24684
  } catch (err) {
24370
24685
  this.printer.error(
24371
24686
  tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -24614,7 +24929,7 @@ var STUB_COMMANDS = [
24614
24929
 
24615
24930
  // cli/commands/tutorial.ts
24616
24931
  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";
24932
+ import { dirname as dirname19, join as join21, resolve as resolve38 } from "path";
24618
24933
  import { fileURLToPath as fileURLToPath6 } from "url";
24619
24934
  import { Command as Command37, Option as Option35 } from "clipanion";
24620
24935
 
@@ -24710,7 +25025,7 @@ var TutorialCommand = class extends SmCommand {
24710
25025
  }
24711
25026
  const variant = rawVariant ?? DEFAULT_VARIANT;
24712
25027
  const spec = VARIANT_SPECS[variant];
24713
- const targetDir = join20(ctx.cwd, ".claude", "skills", spec.slug);
25028
+ const targetDir = join21(ctx.cwd, ".claude", "skills", spec.slug);
24714
25029
  const targetDisplay = `.claude/skills/${spec.slug}/`;
24715
25030
  if (existsSync32(targetDir) && !this.force) {
24716
25031
  this.printer.error(
@@ -24788,11 +25103,11 @@ function resolveSkillSourceDir(variant) {
24788
25103
  const here = dirname19(fileURLToPath6(import.meta.url));
24789
25104
  const candidates = [
24790
25105
  // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
24791
- resolve37(here, "../../..", spec.sourceDir),
25106
+ resolve38(here, "../../..", spec.sourceDir),
24792
25107
  // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
24793
- resolve37(here, "cli/tutorial", spec.slug),
25108
+ resolve38(here, "cli/tutorial", spec.slug),
24794
25109
  // bundled fallback: any-depth → cli/tutorial/<slug>
24795
- resolve37(here, "../cli/tutorial", spec.slug)
25110
+ resolve38(here, "../cli/tutorial", spec.slug)
24796
25111
  ];
24797
25112
  for (const candidate of candidates) {
24798
25113
  if (existsSync32(candidate) && statSync12(candidate).isDirectory()) {