@skill-map/cli 0.33.0 → 0.34.1

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.1",
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.activeProvider)) return false;
13530
13587
  return true;
13531
13588
  });
13532
13589
  const applicableQualifiedIds = new Set(
@@ -13550,10 +13607,11 @@ function matchesKindPrecondition(ex, kind) {
13550
13607
  return kindOnly === kind;
13551
13608
  });
13552
13609
  }
13553
- function matchesProviderPrecondition(ex, provider) {
13610
+ function matchesProviderPrecondition(ex, activeProvider) {
13554
13611
  const providers = ex.precondition?.provider;
13555
13612
  if (!providers || providers.length === 0) return true;
13556
- return providers.includes(provider);
13613
+ if (activeProvider === null) return false;
13614
+ return providers.includes(activeProvider);
13557
13615
  }
13558
13616
  function splitLegacy(applicableExtractors, applicableQualifiedIds, nodeHashCacheEligible) {
13559
13617
  const cachedQualifiedIds = /* @__PURE__ */ new Set();
@@ -13654,6 +13712,65 @@ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, appl
13654
13712
  return "obsolete";
13655
13713
  }
13656
13714
 
13715
+ // kernel/orchestrator/lift-mention-confidence.ts
13716
+ function liftMentionConfidence(links, nodes) {
13717
+ if (!links.some((l) => l.kind === "mentions")) return;
13718
+ const byPath3 = /* @__PURE__ */ new Set();
13719
+ for (const node of nodes) byPath3.add(node.path);
13720
+ const byNormalizedName = indexByNormalizedName2(nodes);
13721
+ for (const link2 of links) {
13722
+ if (link2.kind !== "mentions") continue;
13723
+ if (isResolved2(link2, byPath3, byNormalizedName)) {
13724
+ link2.confidence = 1;
13725
+ }
13726
+ }
13727
+ }
13728
+ function isResolved2(link2, byPath3, byNormalizedName) {
13729
+ const normalized = link2.trigger?.normalizedTrigger;
13730
+ if (normalized) {
13731
+ const withoutSigil = normalized.replace(/^[/@]/, "").trim();
13732
+ if (byNormalizedName.has(withoutSigil)) return true;
13733
+ }
13734
+ if (byPath3.has(link2.target)) return true;
13735
+ return false;
13736
+ }
13737
+ function indexByNormalizedName2(nodes) {
13738
+ const out = /* @__PURE__ */ new Map();
13739
+ for (const node of nodes) {
13740
+ const raw = node.frontmatter?.["name"];
13741
+ const name = typeof raw === "string" ? raw : "";
13742
+ if (!name) continue;
13743
+ out.set(normalizeTrigger(name), true);
13744
+ }
13745
+ return out;
13746
+ }
13747
+
13748
+ // kernel/orchestrator/post-walk-transforms.ts
13749
+ var POST_WALK_TRANSFORMS = [
13750
+ {
13751
+ id: "dedupe-links",
13752
+ description: "Collapse identical (source, target, kind, normalizedTrigger) edges across extractors; union sources[] and pick max confidence on merge.",
13753
+ run(links) {
13754
+ return dedupeLinks(links);
13755
+ }
13756
+ },
13757
+ {
13758
+ id: "lift-mention-confidence",
13759
+ description: "Bump resolved `mentions` links to confidence 1.0 once the full node graph is known (post-merge polish).",
13760
+ run(links, nodes) {
13761
+ liftMentionConfidence(links, nodes);
13762
+ }
13763
+ }
13764
+ ];
13765
+ function applyPostWalkTransforms(links, nodes, transforms = POST_WALK_TRANSFORMS) {
13766
+ let current = links;
13767
+ for (const transform of transforms) {
13768
+ const next = transform.run(current, nodes);
13769
+ if (next) current = next;
13770
+ }
13771
+ return current;
13772
+ }
13773
+
13657
13774
  // kernel/orchestrator/renames.ts
13658
13775
  function findHighConfidenceRenames(opts) {
13659
13776
  const ops = [];
@@ -13797,8 +13914,8 @@ function computeDriftStatus(args2) {
13797
13914
  }
13798
13915
 
13799
13916
  // 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";
13917
+ import { existsSync as existsSync20, readdirSync as readdirSync7, statSync as statSync6 } from "fs";
13918
+ import { join as join13, relative as relative4, sep as sep3 } from "path";
13802
13919
  function discoverOrphanSidecars(roots, shouldSkip) {
13803
13920
  const out = [];
13804
13921
  for (const root of roots) {
@@ -13814,7 +13931,7 @@ function walk(root, current, shouldSkip, out) {
13814
13931
  return;
13815
13932
  }
13816
13933
  for (const entry of entries) {
13817
- const full = join12(current, entry.name);
13934
+ const full = join13(current, entry.name);
13818
13935
  const rel = relative4(root, full).split(sep3).join("/");
13819
13936
  if (shouldSkip(rel)) continue;
13820
13937
  if (entry.isSymbolicLink()) continue;
@@ -13825,7 +13942,7 @@ function walk(root, current, shouldSkip, out) {
13825
13942
  if (!entry.isFile()) continue;
13826
13943
  if (!entry.name.endsWith(".sm")) continue;
13827
13944
  const expectedMd = `${full.slice(0, -".sm".length)}.md`;
13828
- if (existsSync19(expectedMd) && safeIsFile(expectedMd)) continue;
13945
+ if (existsSync20(expectedMd) && safeIsFile(expectedMd)) continue;
13829
13946
  out.push({ sidecarPath: full, relativePath: rel, expectedMdPath: expectedMd });
13830
13947
  }
13831
13948
  }
@@ -13839,7 +13956,7 @@ function safeIsFile(path) {
13839
13956
 
13840
13957
  // kernel/orchestrator/node-build.ts
13841
13958
  import { createHash } from "crypto";
13842
- import { existsSync as existsSync20 } from "fs";
13959
+ import { existsSync as existsSync21 } from "fs";
13843
13960
  import { isAbsolute as isAbsolute6, resolve as resolvePath } from "path";
13844
13961
  import "js-tiktoken/lite";
13845
13962
  import yaml4 from "js-yaml";
@@ -14003,11 +14120,11 @@ function resolveSidecarOverlay(relativePath2, nodePathForIssue, roots, liveBodyH
14003
14120
  }
14004
14121
  function resolveAbsoluteMdPath(relativePath2, roots) {
14005
14122
  if (isAbsolute6(relativePath2)) {
14006
- return existsSync20(relativePath2) ? relativePath2 : null;
14123
+ return existsSync21(relativePath2) ? relativePath2 : null;
14007
14124
  }
14008
14125
  for (const root of roots) {
14009
14126
  const candidate = resolvePath(root, relativePath2);
14010
- if (existsSync20(candidate)) return candidate;
14127
+ if (existsSync21(candidate)) return candidate;
14011
14128
  }
14012
14129
  return null;
14013
14130
  }
@@ -14143,7 +14260,7 @@ async function processRawNode(raw, provider, wctx, accum, claimedPaths, nextInde
14143
14260
  const cacheDecision = computeCacheDecision({
14144
14261
  extractors: wctx.opts.extractors,
14145
14262
  kind,
14146
- provider: provider.id,
14263
+ activeProvider: wctx.opts.activeProvider,
14147
14264
  nodePath: raw.path,
14148
14265
  bodyHash,
14149
14266
  sidecarAnnotationsHash,
@@ -14357,9 +14474,10 @@ async function runScanInternal(_kernel, options) {
14357
14474
  priorIndex: setup.priorIndex,
14358
14475
  priorExtractorRuns: setup.priorExtractorRuns,
14359
14476
  providerFrontmatter: setup.providerFrontmatter,
14360
- pluginStores: options.pluginStores
14477
+ pluginStores: options.pluginStores,
14478
+ activeProvider: resolveActiveProviderOption(options.activeProvider, options.roots)
14361
14479
  });
14362
- walked.internalLinks = dedupeLinks(walked.internalLinks);
14480
+ walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes);
14363
14481
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
14364
14482
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
14365
14483
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -14475,17 +14593,27 @@ function validateRoots(roots) {
14475
14593
  throw new Error(ORCHESTRATOR_TEXTS.runScanRootEmptyArray);
14476
14594
  }
14477
14595
  for (const root of roots) {
14478
- if (!existsSync21(root) || !statSync7(root).isDirectory()) {
14596
+ if (!existsSync22(root) || !statSync7(root).isDirectory()) {
14479
14597
  throw new Error(tx(ORCHESTRATOR_TEXTS.runScanRootMissing, { root }));
14480
14598
  }
14481
14599
  }
14482
14600
  }
14601
+ function resolveActiveProviderOption(optionValue, roots) {
14602
+ if (optionValue !== void 0) return optionValue;
14603
+ for (const root of roots) {
14604
+ const absRoot = isAbsolute7(root) ? root : resolve27(root);
14605
+ if (!existsSync22(absRoot)) continue;
14606
+ const detected = resolveActiveProvider(absRoot).resolved;
14607
+ if (detected !== null) return detected;
14608
+ }
14609
+ return null;
14610
+ }
14483
14611
 
14484
14612
  // kernel/scan/watcher.ts
14485
- import { resolve as resolve27, relative as relative5, sep as sep4 } from "path";
14613
+ import { resolve as resolve28, relative as relative5, sep as sep4 } from "path";
14486
14614
  import chokidar from "chokidar";
14487
14615
  function createChokidarWatcher(opts) {
14488
- const absRoots = opts.roots.map((r) => resolve27(opts.cwd, r));
14616
+ const absRoots = opts.roots.map((r) => resolve28(opts.cwd, r));
14489
14617
  const ignoreFilterOpt = opts.ignoreFilter;
14490
14618
  const getFilter = ignoreFilterOpt === void 0 ? void 0 : typeof ignoreFilterOpt === "function" ? ignoreFilterOpt : () => ignoreFilterOpt;
14491
14619
  const ignored = getFilter ? (path) => {
@@ -14704,7 +14832,7 @@ function createKernel() {
14704
14832
 
14705
14833
  // kernel/jobs/orphan-files.ts
14706
14834
  import { readdirSync as readdirSync8, statSync as statSync8 } from "fs";
14707
- import { join as join13, resolve as resolve28 } from "path";
14835
+ import { join as join14, resolve as resolve29 } from "path";
14708
14836
  function findOrphanJobFiles(jobsDir, referencedPaths) {
14709
14837
  let entries;
14710
14838
  try {
@@ -14722,7 +14850,7 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
14722
14850
  if (!entry.isFile()) continue;
14723
14851
  const name = entry.name;
14724
14852
  if (!name.endsWith(".md")) continue;
14725
- const abs = resolve28(join13(jobsDir, name));
14853
+ const abs = resolve29(join14(jobsDir, name));
14726
14854
  if (!referencedPaths.has(abs)) orphans.push(abs);
14727
14855
  }
14728
14856
  orphans.sort();
@@ -14786,7 +14914,48 @@ var SCAN_RUNNER_TEXTS = {
14786
14914
  * does not exist on disk. Surfaced once per missing root so the
14787
14915
  * operator notices a typo without the walker silently swallowing it.
14788
14916
  */
14789
- referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.'
14917
+ referenceWalkMissingRoot: 'scan.referencePaths: configured path "{{path}}" does not exist; skipped.',
14918
+ /**
14919
+ * Active-provider bootstrap: filesystem auto-detect found no
14920
+ * markers (`.claude/`, `.gemini/`, `.codex/`, `AGENTS.md`, `.cursor/`)
14921
+ * anywhere under cwd or the effective scan roots. Plain-markdown
14922
+ * projects keep scanning fine; provider-specific extractors silently
14923
+ * no-op for this scan.
14924
+ */
14925
+ 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.",
14926
+ /**
14927
+ * Active-provider bootstrap: filesystem auto-detect found exactly
14928
+ * one marker and persisted the detected id to project settings.
14929
+ */
14930
+ activeProviderAutodetected: "Auto-detected activeProvider = {{id}} from filesystem markers; persisted to .skill-map/settings.json.",
14931
+ /**
14932
+ * Active-provider bootstrap: persistence of the auto-detected id
14933
+ * failed (permission, disk full, etc). Non-fatal; the scan
14934
+ * continues with the value in memory for this run.
14935
+ */
14936
+ 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.",
14937
+ /**
14938
+ * Active-provider bootstrap: ambiguous detection (2+ markers
14939
+ * present), interactive prompt header. Followed by one
14940
+ * `activeProviderPromptOption` per detected provider id.
14941
+ */
14942
+ activeProviderPromptHeader: "Multiple provider markers detected. Pick the active lens for this project:",
14943
+ activeProviderPromptOption: " {{index}}) {{id}}",
14944
+ activeProviderPromptInput: "Enter the number or provider id: ",
14945
+ /**
14946
+ * Active-provider bootstrap: ambiguous detection under `--yes`. The
14947
+ * caller exits non-zero; this message names the candidates and how
14948
+ * to resolve.
14949
+ */
14950
+ activeProviderAmbiguousUnderYes: "Multiple provider markers detected ({{candidates}}) and --yes is set. Set the lens explicitly with `sm config set activeProvider <id>` and re-run.",
14951
+ /**
14952
+ * Active lens points at a bundle the operator has disabled (via
14953
+ * `sm plugins disable <id>` or the Settings UI). Classification keeps
14954
+ * running because it's provider-driven, but the lens-gated extractors
14955
+ * for the disabled bundle silently no-op. Without this warning the
14956
+ * graph quietly differs from what the lens implies.
14957
+ */
14958
+ 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
14959
  };
14791
14960
 
14792
14961
  // core/runtime/scan-roots.ts
@@ -14800,7 +14969,7 @@ function resolveScanRoots(inputs) {
14800
14969
  // core/runtime/reference-paths-walker.ts
14801
14970
  import { readdirSync as readdirSync9, statSync as statSync9 } from "fs";
14802
14971
  import { homedir as osHomedir2 } from "os";
14803
- import { isAbsolute as isAbsolute7, join as join14, resolve as resolve29 } from "path";
14972
+ import { isAbsolute as isAbsolute8, join as join15, resolve as resolve30 } from "path";
14804
14973
  var REFERENCE_WALK_MAX_FILES = 5e4;
14805
14974
  var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14806
14975
  "node_modules",
@@ -14808,10 +14977,10 @@ var SKIPPED_DIR_NAMES = /* @__PURE__ */ new Set([
14808
14977
  SKILL_MAP_DIR
14809
14978
  ]);
14810
14979
  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);
14980
+ if (raw.startsWith("~/")) return resolve30(join15(osHomedir2(), raw.slice(2)));
14981
+ if (raw === "~") return resolve30(osHomedir2());
14982
+ if (isAbsolute8(raw)) return resolve30(raw);
14983
+ return resolve30(cwd, raw);
14815
14984
  }
14816
14985
  function walkReferencePaths(rawRoots, cwd) {
14817
14986
  const paths = /* @__PURE__ */ new Set();
@@ -14840,7 +15009,7 @@ function walkInto(dir, out) {
14840
15009
  for (const entry of entries) {
14841
15010
  if (out.size >= REFERENCE_WALK_MAX_FILES) return true;
14842
15011
  if (entry.isSymbolicLink()) continue;
14843
- const full = join14(dir, entry.name);
15012
+ const full = join15(dir, entry.name);
14844
15013
  if (entry.isDirectory()) {
14845
15014
  if (SKIPPED_DIR_NAMES.has(entry.name)) continue;
14846
15015
  if (walkInto(full, out)) return true;
@@ -14858,6 +15027,101 @@ function safeStat(path) {
14858
15027
  }
14859
15028
  }
14860
15029
 
15030
+ // core/runtime/active-provider-bootstrap.ts
15031
+ import { createInterface as createInterface2 } from "readline";
15032
+ import { isAbsolute as isAbsolute9, join as join16 } from "path";
15033
+ async function bootstrapActiveProvider(opts) {
15034
+ const fromCwd = resolveActiveProvider(opts.cwd);
15035
+ if (fromCwd.source === "config") {
15036
+ return { kind: "ok", activeProvider: fromCwd.resolved, source: "config" };
15037
+ }
15038
+ const detected = aggregateDetected(opts.cwd, opts.effectiveRoots, fromCwd.detected);
15039
+ if (detected.length === 0) {
15040
+ opts.printer.warn(SCAN_RUNNER_TEXTS.activeProviderNoMarkerWarning);
15041
+ return { kind: "ok", activeProvider: null, source: "none" };
15042
+ }
15043
+ if (detected.length === 1) {
15044
+ const picked2 = detected[0];
15045
+ persistActiveProvider(opts.cwd, picked2, opts.printer);
15046
+ return { kind: "ok", activeProvider: picked2, source: "autodetect" };
15047
+ }
15048
+ if (opts.yes) {
15049
+ return { kind: "ambiguous", detected };
15050
+ }
15051
+ const picked = await promptForLens(detected, opts.stdin, opts.stderr);
15052
+ if (picked === null) {
15053
+ return { kind: "ambiguous", detected };
15054
+ }
15055
+ persistActiveProvider(opts.cwd, picked, opts.printer);
15056
+ return { kind: "ok", activeProvider: picked, source: "autodetect" };
15057
+ }
15058
+ function aggregateDetected(cwd, effectiveRoots, cwdDetected) {
15059
+ const out = [];
15060
+ const seen = /* @__PURE__ */ new Set();
15061
+ for (const id of cwdDetected) {
15062
+ if (seen.has(id)) continue;
15063
+ seen.add(id);
15064
+ out.push(id);
15065
+ }
15066
+ for (const root of effectiveRoots) {
15067
+ const absRoot = isAbsolute9(root) ? root : join16(cwd, root);
15068
+ const r = resolveActiveProvider(absRoot);
15069
+ for (const id of r.detected) {
15070
+ if (seen.has(id)) continue;
15071
+ seen.add(id);
15072
+ out.push(id);
15073
+ }
15074
+ }
15075
+ return out;
15076
+ }
15077
+ function persistActiveProvider(cwd, id, printer) {
15078
+ try {
15079
+ writeConfigValue("activeProvider", id, { target: "project", cwd });
15080
+ printer.info(tx(SCAN_RUNNER_TEXTS.activeProviderAutodetected, { id }));
15081
+ } catch (err) {
15082
+ const message = err instanceof Error ? err.message : String(err);
15083
+ printer.warn(
15084
+ tx(SCAN_RUNNER_TEXTS.activeProviderPersistFailed, { id, message })
15085
+ );
15086
+ }
15087
+ }
15088
+ function warnIfLensBundleDisabled(args2) {
15089
+ if (args2.activeProvider === null) return;
15090
+ if (args2.resolveEnabled(args2.activeProvider)) return;
15091
+ args2.printer.warn(
15092
+ tx(SCAN_RUNNER_TEXTS.activeProviderBundleDisabledWarning, {
15093
+ id: args2.activeProvider
15094
+ })
15095
+ );
15096
+ }
15097
+ async function promptForLens(detected, stdin, stderr) {
15098
+ const lines = [SCAN_RUNNER_TEXTS.activeProviderPromptHeader];
15099
+ for (let i = 0; i < detected.length; i += 1) {
15100
+ lines.push(
15101
+ tx(SCAN_RUNNER_TEXTS.activeProviderPromptOption, {
15102
+ index: i + 1,
15103
+ id: detected[i]
15104
+ })
15105
+ );
15106
+ }
15107
+ stderr.write(lines.join("\n") + "\n");
15108
+ const rl = createInterface2({ input: stdin, output: stderr });
15109
+ try {
15110
+ const answer = await new Promise(
15111
+ (resolveP) => rl.question(SCAN_RUNNER_TEXTS.activeProviderPromptInput, resolveP)
15112
+ );
15113
+ const trimmed = answer.trim();
15114
+ const asNumber = Number.parseInt(trimmed, 10);
15115
+ if (!Number.isNaN(asNumber) && asNumber >= 1 && asNumber <= detected.length) {
15116
+ return detected[asNumber - 1];
15117
+ }
15118
+ const asId = detected.find((d) => d.toLowerCase() === trimmed.toLowerCase());
15119
+ return asId ?? null;
15120
+ } finally {
15121
+ rl.close();
15122
+ }
15123
+ }
15124
+
14861
15125
  // core/runtime/scan-runner.ts
14862
15126
  async function runScanForCommand(opts) {
14863
15127
  const ctx = opts.ctx ?? defaultRuntimeContext();
@@ -14865,20 +15129,9 @@ async function runScanForCommand(opts) {
14865
15129
  const kernel = createKernel();
14866
15130
  const pluginRuntime = await preparePluginRuntime(opts, opts.printer);
14867
15131
  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
- }
15132
+ const scanInputs = loadScanInputs(opts, ctx);
15133
+ if ("kind" in scanInputs) return scanInputs;
15134
+ const { cfg, ignoreFilter, strict, effectiveRoots } = scanInputs;
14882
15135
  let referenceablePaths;
14883
15136
  if (cfg.scan.referencePaths.length > 0) {
14884
15137
  const walk2 = walkReferencePaths(cfg.scan.referencePaths, ctx.cwd);
@@ -14887,6 +15140,9 @@ async function runScanForCommand(opts) {
14887
15140
  }
14888
15141
  const loadPrior = makePriorLoader(opts.noBuiltIns, strict);
14889
15142
  const jobsDir = defaultProjectJobsDir(ctx);
15143
+ const lens = await resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime);
15144
+ if (lens.kind === "ambiguous-provider") return lens;
15145
+ const activeProvider = lens.activeProvider;
14890
15146
  const runScanWith = makeScanRunner(
14891
15147
  kernel,
14892
15148
  opts,
@@ -14895,11 +15151,37 @@ async function runScanForCommand(opts) {
14895
15151
  strict,
14896
15152
  extensions,
14897
15153
  referenceablePaths,
14898
- ctx.cwd
15154
+ ctx.cwd,
15155
+ activeProvider
14899
15156
  );
14900
15157
  const willPersist = !opts.noBuiltIns && !opts.dryRun;
14901
15158
  return willPersist ? runPersistPath(opts, dbPath, jobsDir, strict, loadPrior, runScanWith, extensions) : runEphemeralPath(opts, dbPath, strict, loadPrior, runScanWith);
14902
15159
  }
15160
+ async function resolveActiveLens(opts, ctx, effectiveRoots, pluginRuntime) {
15161
+ const bootstrap = await bootstrapActiveProvider({
15162
+ cwd: ctx.cwd,
15163
+ effectiveRoots,
15164
+ yes: opts.yes ?? false,
15165
+ stdin: opts.stdin ?? process.stdin,
15166
+ stderr: opts.stderr,
15167
+ printer: opts.printer
15168
+ });
15169
+ if (bootstrap.kind === "ambiguous") {
15170
+ return {
15171
+ kind: "ambiguous-provider",
15172
+ detected: bootstrap.detected,
15173
+ message: tx(SCAN_RUNNER_TEXTS.activeProviderAmbiguousUnderYes, {
15174
+ candidates: bootstrap.detected.join(", ")
15175
+ })
15176
+ };
15177
+ }
15178
+ warnIfLensBundleDisabled({
15179
+ activeProvider: bootstrap.activeProvider,
15180
+ resolveEnabled: opts.resolveEnabledOverride ?? pluginRuntime.resolveEnabled,
15181
+ printer: opts.printer
15182
+ });
15183
+ return { kind: "ok", activeProvider: bootstrap.activeProvider };
15184
+ }
14903
15185
  function emitReferenceWalkAdvisory(walk2, opts) {
14904
15186
  if (walk2.truncated) {
14905
15187
  opts.printer.warn(SCAN_RUNNER_TEXTS.referenceWalkTruncated);
@@ -14933,6 +15215,17 @@ function registerExtensions(kernel, pluginRuntime, opts) {
14933
15215
  registerEnabledExtensions(kernel, pluginRuntime, registerOpts);
14934
15216
  return extensions;
14935
15217
  }
15218
+ function loadScanInputs(opts, ctx) {
15219
+ try {
15220
+ const cfg = loadConfig({ strict: opts.strict, ...ctx }).effective;
15221
+ const ignoreFilter = buildScanIgnoreFilter(cfg, ctx.cwd);
15222
+ const strict = opts.strict || cfg.scan.strict === true;
15223
+ const effectiveRoots = resolveScanRoots({ positionalRoots: opts.roots });
15224
+ return { cfg, ignoreFilter, strict, effectiveRoots };
15225
+ } catch (err) {
15226
+ return { kind: "config-error", message: formatErrorMessage(err) };
15227
+ }
15228
+ }
14936
15229
  function buildScanIgnoreFilter(cfg, cwd) {
14937
15230
  const ignoreFileText = readIgnoreFileText(cwd);
14938
15231
  const ignoreFilterOpts = {};
@@ -14955,7 +15248,7 @@ function makePriorLoader(noBuiltIns, strict) {
14955
15248
  return loaded;
14956
15249
  };
14957
15250
  }
14958
- function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd) {
15251
+ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, extensions, referenceablePaths, scanCwd, activeProvider) {
14959
15252
  return async (prior, priorExtractorRuns, orphanJobFiles) => {
14960
15253
  if (opts.changed && prior === null) {
14961
15254
  opts.stderr.write(SCAN_RUNNER_TEXTS.changedNoPriorWarning);
@@ -14969,6 +15262,7 @@ function makeScanRunner(kernel, opts, effectiveRoots, ignoreFilter, strict, exte
14969
15262
  referenceablePaths,
14970
15263
  cwd: scanCwd,
14971
15264
  prior,
15265
+ activeProvider,
14972
15266
  ...priorExtractorRuns ? { priorExtractorRuns } : {},
14973
15267
  ...orphanJobFiles ? { orphanJobFiles } : {}
14974
15268
  });
@@ -14989,7 +15283,8 @@ function buildRunScanOptions(args2) {
14989
15283
  // visible from this caller" (legacy behaviour). The orchestrator
14990
15284
  // defaults to `[]` when the field is absent; we always pass the
14991
15285
  // array (possibly empty) to keep the wiring uniform.
14992
- orphanJobFiles: orphanJobFiles ?? []
15286
+ orphanJobFiles: orphanJobFiles ?? [],
15287
+ activeProvider: args2.activeProvider
14993
15288
  };
14994
15289
  if (args2.extensions) runOptions.extensions = args2.extensions;
14995
15290
  if (prior) {
@@ -15132,7 +15427,7 @@ var InitCommand = class extends SmCommand {
15132
15427
  async run() {
15133
15428
  const ctx = defaultRuntimeContext();
15134
15429
  const scopeRoot = ctx.cwd;
15135
- const skillMapDir = join15(scopeRoot, SKILL_MAP_DIR);
15430
+ const skillMapDir = join17(scopeRoot, SKILL_MAP_DIR);
15136
15431
  const settingsPath = defaultSettingsPath(scopeRoot);
15137
15432
  const localPath = defaultLocalSettingsPath(scopeRoot);
15138
15433
  const ignorePath = defaultIgnoreFilePath(scopeRoot);
@@ -15178,7 +15473,7 @@ var InitCommand = class extends SmCommand {
15178
15473
  const okGlyph = ansi.green("\u2713");
15179
15474
  const updated = await ensureGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
15180
15475
  if (updated) {
15181
- const gitignorePath = join15(scopeRoot, ".gitignore");
15476
+ const gitignorePath = join17(scopeRoot, ".gitignore");
15182
15477
  printer.info(
15183
15478
  GITIGNORE_ENTRIES.length === 1 ? tx(INIT_TEXTS.gitignoreUpdatedSingular, { glyph: okGlyph, path: gitignorePath }) : tx(INIT_TEXTS.gitignoreUpdatedPlural, {
15184
15479
  glyph: okGlyph,
@@ -15217,7 +15512,7 @@ async function dryRunFileMessage(path) {
15217
15512
  }
15218
15513
  async function writeDryRunGitignorePlan(printer, scopeRoot) {
15219
15514
  const wouldAdd = await previewGitignoreEntries(scopeRoot, GITIGNORE_ENTRIES);
15220
- const gitignorePath = join15(scopeRoot, ".gitignore");
15515
+ const gitignorePath = join17(scopeRoot, ".gitignore");
15221
15516
  if (wouldAdd.length === 0) {
15222
15517
  printer.info(tx(INIT_TEXTS.dryRunWouldLeaveGitignoreUnchanged, { path: gitignorePath }));
15223
15518
  } else if (wouldAdd.length === 1) {
@@ -15256,7 +15551,14 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
15256
15551
  strict,
15257
15552
  stderr,
15258
15553
  printer,
15259
- ctx: { cwd: scopeRoot }
15554
+ ctx: { cwd: scopeRoot },
15555
+ // Init's first scan is a provisioning step, not the user's
15556
+ // primary "show me my graph" call. Don't block waiting for the
15557
+ // operator to disambiguate the lens here; let init complete with
15558
+ // `activeProvider` unset and let the FIRST explicit `sm scan`
15559
+ // surface the prompt. Treat the `ambiguous-provider` outcome below
15560
+ // as a soft hint, not a failure.
15561
+ yes: true
15260
15562
  });
15261
15563
  const errGlyph = ansi.red("\u2715");
15262
15564
  if (outcome.kind === "config-error") {
@@ -15276,6 +15578,10 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
15276
15578
  );
15277
15579
  return ExitCode.Error;
15278
15580
  }
15581
+ if (outcome.kind === "ambiguous-provider") {
15582
+ printer.warn(outcome.message);
15583
+ return ExitCode.Ok;
15584
+ }
15279
15585
  const result = outcome.result;
15280
15586
  const hasErrors = result.issues.some((i) => i.severity === "error");
15281
15587
  printer.info(
@@ -15292,7 +15598,7 @@ async function runFirstScan(scopeRoot, strict, printer, stderr, ansi) {
15292
15598
  return hasErrors ? ExitCode.Issues : ExitCode.Ok;
15293
15599
  }
15294
15600
  async function previewGitignoreEntries(scopeRoot, entries) {
15295
- const path = join15(scopeRoot, ".gitignore");
15601
+ const path = join17(scopeRoot, ".gitignore");
15296
15602
  const body = await pathExists(path) ? await readFile2(path, "utf8") : "";
15297
15603
  const present = new Set(
15298
15604
  body.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"))
@@ -15300,7 +15606,7 @@ async function previewGitignoreEntries(scopeRoot, entries) {
15300
15606
  return entries.filter((entry) => !present.has(entry));
15301
15607
  }
15302
15608
  async function ensureGitignoreEntries(scopeRoot, entries) {
15303
- const path = join15(scopeRoot, ".gitignore");
15609
+ const path = join17(scopeRoot, ".gitignore");
15304
15610
  let body = "";
15305
15611
  if (await pathExists(path)) {
15306
15612
  body = await readFile2(path, "utf8");
@@ -16865,8 +17171,8 @@ var PLUGINS_TEXTS = {
16865
17171
  doctorIssueEntry: " {{glyph}} {{id}} {{status}}\n",
16866
17172
  doctorIssueBody: " {{line}}\n",
16867
17173
  // --- enable / disable -----------------------------------------------
16868
- toggleBothIdAndAll: "{{glyph}} Pass either an <id> or --all, not both.\n",
16869
- toggleNeitherIdNorAll: "{{glyph}} Pass <id> or --all.\n",
17174
+ toggleBothIdAndAll: "{{glyph}} Pass either one or more <id> arguments or --all, not both.\n",
17175
+ toggleNeitherIdNorAll: "{{glyph}} Pass one or more <id> arguments, or --all.\n",
16870
17176
  toggleResolveError: "{{error}}",
16871
17177
  toggleAppliedSingle: "{{verbPast}}: {{id}}\n",
16872
17178
  toggleAppliedManyHeader: "{{verbPast}}: {{count}} plugin(s)\n",
@@ -16968,9 +17274,9 @@ var PLUGINS_TEXTS = {
16968
17274
  };
16969
17275
 
16970
17276
  // cli/commands/plugins/shared.ts
16971
- import { resolve as resolve30 } from "path";
17277
+ import { resolve as resolve31 } from "path";
16972
17278
  function resolveSearchPaths2(opts, cwd) {
16973
- if (opts.pluginDir) return [resolve30(opts.pluginDir)];
17279
+ if (opts.pluginDir) return [resolve31(opts.pluginDir)];
16974
17280
  return [defaultProjectPluginsDir({ cwd })];
16975
17281
  }
16976
17282
  async function buildResolver() {
@@ -17852,7 +18158,7 @@ function buildDoctorJsonEnvelope(args2) {
17852
18158
  import { Command as Command25, Option as Option24 } from "clipanion";
17853
18159
  var TogglePluginsBase = class extends SmCommand {
17854
18160
  all = Option24.Boolean("--all", false);
17855
- id = Option24.String({ required: false });
18161
+ ids = Option24.Rest({ name: "ids" });
17856
18162
  async toggle(enabled) {
17857
18163
  const verb = enabled ? "enable" : "disable";
17858
18164
  const stderrAnsi = this.ansiFor("stderr");
@@ -17871,23 +18177,24 @@ var TogglePluginsBase = class extends SmCommand {
17871
18177
  return ExitCode.Ok;
17872
18178
  }
17873
18179
  /**
17874
- * `--all` vs `<id>` mutex check. The two are mutually exclusive and
17875
- * one must be present; surfaces a directed error on misuse.
18180
+ * `--all` vs `<id>...` mutex check. The two are mutually exclusive
18181
+ * and one must be present; surfaces a directed error on misuse.
18182
+ * Variadic positional accepts one or more ids.
17876
18183
  */
17877
18184
  #validateArgs(ansi) {
17878
18185
  const errGlyph = ansi.red("\u2715");
17879
- if (this.all && this.id) {
18186
+ if (this.all && this.ids.length > 0) {
17880
18187
  this.printer.error(tx(PLUGINS_TEXTS.toggleBothIdAndAll, { glyph: errGlyph }));
17881
18188
  return ExitCode.Error;
17882
18189
  }
17883
- if (!this.all && !this.id) {
18190
+ if (!this.all && this.ids.length === 0) {
17884
18191
  this.printer.error(tx(PLUGINS_TEXTS.toggleNeitherIdNorAll, { glyph: errGlyph }));
17885
18192
  return ExitCode.Error;
17886
18193
  }
17887
18194
  return null;
17888
18195
  }
17889
18196
  /**
17890
- * Resolve `<id>` against the catalogue or fan out via `--all`.
18197
+ * Resolve `<id>...` against the catalogue or fan out via `--all`.
17891
18198
  * Returns the target list on success, or the exit code on a
17892
18199
  * directed-error path (unknown id, granularity mismatch).
17893
18200
  *
@@ -17898,25 +18205,35 @@ var TogglePluginsBase = class extends SmCommand {
17898
18205
  * the directed error message when they try the bundle id directly,
17899
18206
  * so `--all` skips them here too and the real "disable every core
17900
18207
  * extension" intent is served by `--no-built-ins` on `sm scan`.
18208
+ *
18209
+ * Variadic mode is all-or-nothing: the first bad id aborts the
18210
+ * batch before any DB write, so the user never lands in a partial
18211
+ * state. Repeated ids in the same call are deduped.
17901
18212
  */
17902
18213
  #pickTargets(catalogue, verb, ansi) {
17903
18214
  if (this.all) {
17904
18215
  return catalogue.filter((b) => b.granularity === "bundle").map((b) => b.id);
17905
18216
  }
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;
18217
+ const keys = [];
18218
+ for (const rawId of this.ids) {
18219
+ const resolved = resolveToggleTarget(rawId, catalogue, verb, ansi);
18220
+ if ("error" in resolved) {
18221
+ this.printer.error(tx(PLUGINS_TEXTS.toggleResolveError, { error: resolved.error }));
18222
+ return ExitCode.NotFound;
18223
+ }
18224
+ keys.push(resolved.key);
17910
18225
  }
17911
- return [resolved.key];
18226
+ return [...new Set(keys)];
17912
18227
  }
17913
18228
  /**
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.
18229
+ * Host lock, see `src/kernel/config/locked-plugins.ts`. Bulk modes
18230
+ * (`--all` or an explicit batch of >1 ids) silently skip locked
18231
+ * targets so the user can still toggle the rest. Single-id mode
18232
+ * surfaces a directed exit-5 message so the user knows their one
18233
+ * intended target was refused.
17917
18234
  */
17918
18235
  #applyLockGate(targets, ansi) {
17919
- if (this.all) return targets.filter((id) => !isPluginLocked(id));
18236
+ if (this.all || this.ids.length > 1) return targets.filter((id) => !isPluginLocked(id));
17920
18237
  const lockedHit = targets.find((id) => isPluginLocked(id));
17921
18238
  if (!lockedHit) return targets;
17922
18239
  this.printer.error(
@@ -17972,12 +18289,19 @@ var PluginsEnableCommand = class extends TogglePluginsBase {
17972
18289
  static paths = [["plugins", "enable"]];
17973
18290
  static usage = Command25.Usage({
17974
18291
  category: "Plugins",
17975
- description: "Enable a plugin (or --all). Persists in config_plugins.",
18292
+ description: "Enable one or more plugins (or --all). Persists in config_plugins.",
17976
18293
  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.
18294
+ Writes a row to config_plugins with enabled=1 per id. Takes
18295
+ precedence over the team-shared baseline at
18296
+ settings.json#/plugins/<id>/enabled. Use sm plugins disable to
18297
+ flip; sm config reset plugins.<id>.enabled drops the settings.json
18298
+ baseline.
18299
+
18300
+ Accepts one or more ids in one call, e.g.
18301
+ 'sm plugins enable claude gemini openai'. Batches are
18302
+ all-or-nothing: a single unknown / mismatched id aborts before
18303
+ any write. Repeated ids are deduped. Locked plugins inside a
18304
+ batch are silently skipped.
17981
18305
 
17982
18306
  Granularity: a bundle-granularity plugin (default for user plugins,
17983
18307
  and the built-in 'claude' bundle) accepts only the bundle id. An
@@ -17994,12 +18318,18 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
17994
18318
  static paths = [["plugins", "disable"]];
17995
18319
  static usage = Command25.Usage({
17996
18320
  category: "Plugins",
17997
- description: "Disable a plugin (or --all). Persists in config_plugins; does not delete files.",
18321
+ description: "Disable one or more plugins (or --all). Persists in config_plugins; does not delete files.",
17998
18322
  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.
18323
+ Writes a row to config_plugins with enabled=0 per id. Discovery
18324
+ still surfaces the plugin in sm plugins list, but with
18325
+ status=disabled; its extensions are not imported and the kernel
18326
+ will not run them.
18327
+
18328
+ Accepts one or more ids in one call, e.g.
18329
+ 'sm plugins disable gemini openai agent-skills'. Batches are
18330
+ all-or-nothing: a single unknown / mismatched id aborts before
18331
+ any write. Repeated ids are deduped. Locked plugins inside a
18332
+ batch are silently skipped.
18003
18333
 
18004
18334
  Granularity: a bundle-granularity plugin (default for user plugins,
18005
18335
  and the built-in 'claude' bundle) accepts only the bundle id. An
@@ -18105,8 +18435,8 @@ function resolveBareToggle(id, catalogue, verb, ansi) {
18105
18435
  }
18106
18436
 
18107
18437
  // 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";
18438
+ import { existsSync as existsSync23, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3 } from "fs";
18439
+ import { join as join18, resolve as resolve32 } from "path";
18110
18440
  import { Command as Command26, Option as Option25 } from "clipanion";
18111
18441
  var PluginsCreateCommand = class extends SmCommand {
18112
18442
  static paths = [["plugins", "create"]];
@@ -18132,8 +18462,8 @@ var PluginsCreateCommand = class extends SmCommand {
18132
18462
  }
18133
18463
  const ctx = defaultRuntimeContext();
18134
18464
  const baseDir = defaultProjectPluginsDir(ctx);
18135
- const targetDir = this.at ? resolve31(this.at) : join16(baseDir, this.pluginId);
18136
- if (existsSync22(targetDir) && !this.force) {
18465
+ const targetDir = this.at ? resolve32(this.at) : join18(baseDir, this.pluginId);
18466
+ if (existsSync23(targetDir) && !this.force) {
18137
18467
  this.printer.error(
18138
18468
  tx(PLUGINS_TEXTS.createRefuseOverwrite, {
18139
18469
  glyph: errGlyph,
@@ -18143,7 +18473,7 @@ var PluginsCreateCommand = class extends SmCommand {
18143
18473
  return ExitCode.Error;
18144
18474
  }
18145
18475
  const extractorName = `${this.pluginId}-extractor`;
18146
- mkdirSync6(join16(targetDir, "extractors", extractorName), { recursive: true });
18476
+ mkdirSync6(join18(targetDir, "extractors", extractorName), { recursive: true });
18147
18477
  const specVersion = installedSpecVersion();
18148
18478
  const manifest = {
18149
18479
  id: this.pluginId,
@@ -18163,14 +18493,14 @@ var PluginsCreateCommand = class extends SmCommand {
18163
18493
  }
18164
18494
  };
18165
18495
  writeFileSync3(
18166
- join16(targetDir, "plugin.json"),
18496
+ join18(targetDir, "plugin.json"),
18167
18497
  JSON.stringify(manifest, null, 2) + "\n"
18168
18498
  );
18169
18499
  writeFileSync3(
18170
- join16(targetDir, "extractors", extractorName, "index.js"),
18500
+ join18(targetDir, "extractors", extractorName, "index.js"),
18171
18501
  scaffolderExtractorStub(extractorName)
18172
18502
  );
18173
- writeFileSync3(join16(targetDir, "README.md"), scaffolderReadme(this.pluginId));
18503
+ writeFileSync3(join18(targetDir, "README.md"), scaffolderReadme(this.pluginId));
18174
18504
  this.printer.data(
18175
18505
  tx(PLUGINS_TEXTS.createSuccess, {
18176
18506
  targetDir: sanitizeForTerminal(targetDir),
@@ -18373,7 +18703,7 @@ var PLUGIN_COMMANDS = [
18373
18703
 
18374
18704
  // cli/commands/refresh.ts
18375
18705
  import { readFile as readFile3 } from "fs/promises";
18376
- import { resolve as resolve32 } from "path";
18706
+ import { resolve as resolve33 } from "path";
18377
18707
  import { Command as Command29, Option as Option27 } from "clipanion";
18378
18708
 
18379
18709
  // cli/i18n/refresh.texts.ts
@@ -18657,7 +18987,7 @@ var RefreshCommand = class extends SmCommand {
18657
18987
  let body;
18658
18988
  try {
18659
18989
  assertContained(cwd, node.path);
18660
- const raw = await readFile3(resolve32(cwd, node.path), "utf8");
18990
+ const raw = await readFile3(resolve33(cwd, node.path), "utf8");
18661
18991
  body = stripFrontmatterFence(raw);
18662
18992
  } catch (err) {
18663
18993
  if (!this.json) {
@@ -19398,6 +19728,9 @@ var ScanCommand = class extends SmCommand {
19398
19728
  watch = Option29.Boolean("--watch", false, {
19399
19729
  description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
19400
19730
  });
19731
+ yes = Option29.Boolean("--yes", false, {
19732
+ 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."
19733
+ });
19401
19734
  // Each branch in the orchestrator maps to one validation gate
19402
19735
  // (--watch alias / --changed mutex / -g mutex / dispatch).
19403
19736
  // Splitting per branch scatters the gate from the value it gates.
@@ -19426,9 +19759,11 @@ var ScanCommand = class extends SmCommand {
19426
19759
  allowEmpty: this.allowEmpty,
19427
19760
  strict: this.strict,
19428
19761
  stderr: this.context.stderr,
19762
+ stdin: this.context.stdin,
19429
19763
  printer: this.printer,
19430
19764
  killSwitches: readConformanceKillSwitches(),
19431
- colorEnabled
19765
+ colorEnabled,
19766
+ yes: this.yes
19432
19767
  });
19433
19768
  return outcome.kind === "ok" ? this.renderOutcome(outcome.result, outcome.persistedTo, outcome.dbPath, outcome.strict) : this.renderFailure(outcome);
19434
19769
  }
@@ -19499,6 +19834,12 @@ var ScanCommand = class extends SmCommand {
19499
19834
  );
19500
19835
  return ExitCode.Error;
19501
19836
  }
19837
+ if (outcome.kind === "ambiguous-provider") {
19838
+ this.printer.info(
19839
+ tx(SCAN_TEXTS.scanFailure, { glyph: errGlyph, message: outcome.message })
19840
+ );
19841
+ return ExitCode.Error;
19842
+ }
19502
19843
  this.printer.info(
19503
19844
  tx(SCAN_TEXTS.scanFailure, { glyph: errGlyph, message: outcome.message })
19504
19845
  );
@@ -19581,7 +19922,7 @@ function plural(count, word) {
19581
19922
  }
19582
19923
 
19583
19924
  // cli/commands/scan-compare.ts
19584
- import { existsSync as existsSync23, readFileSync as readFileSync18 } from "fs";
19925
+ import { existsSync as existsSync24, readFileSync as readFileSync18 } from "fs";
19585
19926
  import { Command as Command32, Option as Option30 } from "clipanion";
19586
19927
  var ScanCompareCommand = class extends SmCommand {
19587
19928
  static paths = [["scan", "compare-with"]];
@@ -19693,7 +20034,7 @@ var ScanCompareCommand = class extends SmCommand {
19693
20034
  }
19694
20035
  };
19695
20036
  function loadAndValidateDump(path) {
19696
- if (!existsSync23(path)) {
20037
+ if (!existsSync24(path)) {
19697
20038
  throw new Error(tx(SCAN_TEXTS.compareDumpNotFound, { path }));
19698
20039
  }
19699
20040
  let raw;
@@ -20536,7 +20877,7 @@ function contentTypeFor(format) {
20536
20877
  }
20537
20878
 
20538
20879
  // server/health.ts
20539
- import { existsSync as existsSync24 } from "fs";
20880
+ import { existsSync as existsSync25 } from "fs";
20540
20881
  var FALLBACK_SCHEMA_VERSION = "1";
20541
20882
  function buildHealth(deps) {
20542
20883
  return {
@@ -20544,7 +20885,7 @@ function buildHealth(deps) {
20544
20885
  schemaVersion: FALLBACK_SCHEMA_VERSION,
20545
20886
  specVersion: deps.specVersion,
20546
20887
  implVersion: VERSION,
20547
- db: existsSync24(deps.dbPath) ? "present" : "missing",
20888
+ db: existsSync25(deps.dbPath) ? "present" : "missing",
20548
20889
  cwd: deps.cwd,
20549
20890
  dbPath: deps.dbPath
20550
20891
  };
@@ -20657,9 +20998,9 @@ import { HTTPException as HTTPException6 } from "hono/http-exception";
20657
20998
 
20658
20999
  // server/node-body.ts
20659
21000
  import { readFile as readFile4 } from "fs/promises";
20660
- import { isAbsolute as isAbsolute8, resolve as resolvePath2, relative as relativePath, sep as sep5 } from "path";
21001
+ import { isAbsolute as isAbsolute10, resolve as resolvePath2, relative as relativePath, sep as sep5 } from "path";
20661
21002
  async function readNodeBody(cwd, relPath) {
20662
- if (isAbsolute8(relPath)) return null;
21003
+ if (isAbsolute10(relPath)) return null;
20663
21004
  const absRoot = resolvePath2(cwd);
20664
21005
  const absFile = resolvePath2(absRoot, relPath);
20665
21006
  const rel = relativePath(absRoot, absFile);
@@ -21379,12 +21720,12 @@ var parsePatchBody2 = makeBodyValidator(PATCH_BODY_SCHEMA, {
21379
21720
  import { HTTPException as HTTPException10 } from "hono/http-exception";
21380
21721
 
21381
21722
  // 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";
21723
+ import { existsSync as existsSync26, readFileSync as readFileSync19, writeFileSync as writeFileSync4 } from "fs";
21724
+ import { resolve as resolve34 } from "path";
21384
21725
  var IGNORE_FILENAME2 = ".skillmapignore";
21385
21726
  function readPatterns(cwd) {
21386
- const path = resolve33(cwd, IGNORE_FILENAME2);
21387
- if (!existsSync25(path)) return [];
21727
+ const path = resolve34(cwd, IGNORE_FILENAME2);
21728
+ if (!existsSync26(path)) return [];
21388
21729
  let raw;
21389
21730
  try {
21390
21731
  raw = readFileSync19(path, "utf8");
@@ -21394,8 +21735,8 @@ function readPatterns(cwd) {
21394
21735
  return raw.split(/\r?\n/).map((l) => l.trim()).filter((l) => l.length > 0 && !l.startsWith("#"));
21395
21736
  }
21396
21737
  function writePatterns(cwd, nextPatterns) {
21397
- const path = resolve33(cwd, IGNORE_FILENAME2);
21398
- const prior = existsSync25(path) ? safeRead(path) : "";
21738
+ const path = resolve34(cwd, IGNORE_FILENAME2);
21739
+ const prior = existsSync26(path) ? safeRead(path) : "";
21399
21740
  const content = buildContent(prior, nextPatterns);
21400
21741
  writeFileSync4(path, content, "utf8");
21401
21742
  }
@@ -21747,41 +22088,6 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
21747
22088
  // server/routes/active-provider.ts
21748
22089
  import { existsSync as existsSync27 } from "fs";
21749
22090
  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
22091
  function registerActiveProviderRoute(app, deps) {
21786
22092
  app.get("/api/active-provider", (c) => {
21787
22093
  return c.json(buildEnvelope4(deps));
@@ -21850,14 +22156,14 @@ async function withScanMutex(fn) {
21850
22156
  if (inFlight !== null) {
21851
22157
  throw new ScanBusyError();
21852
22158
  }
21853
- let resolve38;
22159
+ let resolve39;
21854
22160
  inFlight = new Promise((r) => {
21855
- resolve38 = r;
22161
+ resolve39 = r;
21856
22162
  });
21857
22163
  try {
21858
22164
  return await fn();
21859
22165
  } finally {
21860
- resolve38();
22166
+ resolve39();
21861
22167
  inFlight = null;
21862
22168
  }
21863
22169
  }
@@ -22023,7 +22329,11 @@ async function runPersistedScan(c, deps) {
22023
22329
  pluginRuntime: deps.pluginRuntime,
22024
22330
  resolveEnabledOverride,
22025
22331
  printer: bffScanRunnerPrinter,
22026
- emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster)
22332
+ emitterFactory: () => buildBroadcasterEmitter(deps.broadcaster),
22333
+ // BFF has no TTY; ambiguous activeProvider must be resolved by
22334
+ // the operator via the Settings UI (PATCH /api/active-provider)
22335
+ // before the scan, not via interactive prompt here.
22336
+ yes: true
22027
22337
  });
22028
22338
  if (outcome.kind !== "ok") {
22029
22339
  throw new HTTPException13(500, {
@@ -22124,7 +22434,10 @@ async function runFreshScan(deps) {
22124
22434
  // fallback. The fresh-scan response body IS the ScanResult JSON,
22125
22435
  // so `data` is never used here; warn/info/error route through
22126
22436
  // `log.warn` (same surface the rest of the BFF uses).
22127
- printer: bffScanRunnerPrinter
22437
+ printer: bffScanRunnerPrinter,
22438
+ // BFF has no TTY; ambiguous activeProvider is the operator's
22439
+ // problem to resolve via the Settings UI, not via prompt here.
22440
+ yes: true
22128
22441
  });
22129
22442
  if (outcome.kind !== "ok") {
22130
22443
  throw new HTTPException13(500, {
@@ -22162,7 +22475,7 @@ function emptyScanResult() {
22162
22475
 
22163
22476
  // server/routes/sidecar.ts
22164
22477
  import { HTTPException as HTTPException14 } from "hono/http-exception";
22165
- import { resolve as resolve34 } from "path";
22478
+ import { resolve as resolve35 } from "path";
22166
22479
  var STATUS_FRESH = "fresh";
22167
22480
  var ENVELOPE_KIND2 = "sidecar.bumped";
22168
22481
  var BUMP_BODY_SCHEMA = {
@@ -22196,7 +22509,7 @@ function registerSidecarRoutes(app, deps) {
22196
22509
  let absPath;
22197
22510
  try {
22198
22511
  assertContained(deps.runtimeContext.cwd, node.path);
22199
- absPath = resolve34(deps.runtimeContext.cwd, node.path);
22512
+ absPath = resolve35(deps.runtimeContext.cwd, node.path);
22200
22513
  } catch (err) {
22201
22514
  throw new HTTPException14(500, { message: formatErrorMessage(err) });
22202
22515
  }
@@ -22322,7 +22635,7 @@ function registerUpdateStatusRoute(app, deps) {
22322
22635
  // server/static.ts
22323
22636
  import { existsSync as existsSync28 } from "fs";
22324
22637
  import { readFile as readFile5 } from "fs/promises";
22325
- import { extname, join as join18 } from "path";
22638
+ import { extname, join as join19 } from "path";
22326
22639
  import { serveStatic } from "@hono/node-server/serve-static";
22327
22640
  var INDEX_HTML = "index.html";
22328
22641
  var PLACEHOLDER_HTML = `<!doctype html>
@@ -22374,7 +22687,7 @@ function createSpaFallback(opts) {
22374
22687
  return async (c, _next) => {
22375
22688
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
22376
22689
  if (opts.uiDist === null) return htmlResponse(c, placeholder);
22377
- const indexPath = join18(opts.uiDist, INDEX_HTML);
22690
+ const indexPath = join19(opts.uiDist, INDEX_HTML);
22378
22691
  if (!existsSync28(indexPath)) return htmlResponse(c, placeholder);
22379
22692
  return fileResponse(c, indexPath);
22380
22693
  };
@@ -22948,9 +23261,9 @@ function validateNoUi(noUi, uiDist) {
22948
23261
 
22949
23262
  // server/paths.ts
22950
23263
  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";
23264
+ import { dirname as dirname18, isAbsolute as isAbsolute11, join as join20, resolve as resolve36 } from "path";
22952
23265
  import { fileURLToPath as fileURLToPath5 } from "url";
22953
- var DEFAULT_UI_REL = join19("ui", "dist", "ui", "browser");
23266
+ var DEFAULT_UI_REL = join20("ui", "dist", "ui", "browser");
22954
23267
  var PACKAGE_UI_REL = "ui";
22955
23268
  var INDEX_HTML2 = "index.html";
22956
23269
  function resolveDefaultUiDist(ctx) {
@@ -22959,13 +23272,13 @@ function resolveDefaultUiDist(ctx) {
22959
23272
  return walkUpForUi(ctx.cwd);
22960
23273
  }
22961
23274
  function resolveExplicitUiDist(ctx, raw) {
22962
- return isAbsolute9(raw) ? raw : resolve35(ctx.cwd, raw);
23275
+ return isAbsolute11(raw) ? raw : resolve36(ctx.cwd, raw);
22963
23276
  }
22964
23277
  function isUiBundleDir(path) {
22965
23278
  if (!existsSync29(path)) return false;
22966
23279
  try {
22967
23280
  if (!statSync11(path).isDirectory()) return false;
22968
- return existsSync29(join19(path, INDEX_HTML2));
23281
+ return existsSync29(join20(path, INDEX_HTML2));
22969
23282
  } catch {
22970
23283
  return false;
22971
23284
  }
@@ -22982,9 +23295,9 @@ function resolvePackageBundledUi() {
22982
23295
  function resolvePackageBundledUiFrom(here) {
22983
23296
  let current = here;
22984
23297
  for (let i = 0; i < 8; i++) {
22985
- const candidate = join19(current, PACKAGE_UI_REL);
23298
+ const candidate = join20(current, PACKAGE_UI_REL);
22986
23299
  if (isUiBundleDir(candidate)) return candidate;
22987
- const distHere = join19(current, "dist", PACKAGE_UI_REL);
23300
+ const distHere = join20(current, "dist", PACKAGE_UI_REL);
22988
23301
  if (isUiBundleDir(distHere)) return distHere;
22989
23302
  const parent = dirname18(current);
22990
23303
  if (parent === current) return null;
@@ -22993,9 +23306,9 @@ function resolvePackageBundledUiFrom(here) {
22993
23306
  return null;
22994
23307
  }
22995
23308
  function walkUpForUi(startDir) {
22996
- let current = resolve35(startDir);
23309
+ let current = resolve36(startDir);
22997
23310
  for (let i = 0; i < 64; i++) {
22998
- const candidate = join19(current, DEFAULT_UI_REL);
23311
+ const candidate = join20(current, DEFAULT_UI_REL);
22999
23312
  if (isUiBundleDir(candidate)) return candidate;
23000
23313
  const parent = dirname18(current);
23001
23314
  if (parent === current) return null;
@@ -23197,7 +23510,7 @@ var SERVE_TEXTS = {
23197
23510
  };
23198
23511
 
23199
23512
  // cli/util/serve-banner.ts
23200
- import { relative as relative7, isAbsolute as isAbsolute10 } from "path";
23513
+ import { relative as relative7, isAbsolute as isAbsolute12 } from "path";
23201
23514
  var ESC2 = {
23202
23515
  reset: "\x1B[0m",
23203
23516
  bold: "\x1B[1m",
@@ -23331,9 +23644,9 @@ function resolveAnsi(colorEnabled) {
23331
23644
  }
23332
23645
  function formatDbPath(dbPath, cwd) {
23333
23646
  const safe = sanitizeForTerminal(dbPath);
23334
- if (!isAbsolute10(safe)) return safe;
23647
+ if (!isAbsolute12(safe)) return safe;
23335
23648
  const rel = relative7(cwd, safe);
23336
- if (rel === "" || rel.startsWith("..") || isAbsolute10(rel)) {
23649
+ if (rel === "" || rel.startsWith("..") || isAbsolute12(rel)) {
23337
23650
  return safe;
23338
23651
  }
23339
23652
  return rel;
@@ -23933,7 +24246,7 @@ function rankConfidenceForGrouping(c) {
23933
24246
 
23934
24247
  // cli/commands/sidecar.ts
23935
24248
  import { existsSync as existsSync31, unlinkSync as unlinkSync2 } from "fs";
23936
- import { resolve as resolve36 } from "path";
24249
+ import { resolve as resolve37 } from "path";
23937
24250
  import { Command as Command35, Option as Option33 } from "clipanion";
23938
24251
 
23939
24252
  // cli/i18n/sidecar.texts.ts
@@ -24084,7 +24397,7 @@ var SidecarRefreshCommand = class extends SmCommand {
24084
24397
  let absPath;
24085
24398
  try {
24086
24399
  assertContained(ctx.cwd, node.path);
24087
- absPath = resolve36(ctx.cwd, node.path);
24400
+ absPath = resolve37(ctx.cwd, node.path);
24088
24401
  } catch (err) {
24089
24402
  this.printer.error(
24090
24403
  tx(SIDECAR_TEXTS.refreshFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -24365,7 +24678,7 @@ var SidecarAnnotateCommand = class extends SmCommand {
24365
24678
  let absPath;
24366
24679
  try {
24367
24680
  assertContained(ctx.cwd, node.path);
24368
- absPath = resolve36(ctx.cwd, node.path);
24681
+ absPath = resolve37(ctx.cwd, node.path);
24369
24682
  } catch (err) {
24370
24683
  this.printer.error(
24371
24684
  tx(SIDECAR_TEXTS.annotateFailed, { glyph: errGlyph, message: formatErrorMessage(err) })
@@ -24614,7 +24927,7 @@ var STUB_COMMANDS = [
24614
24927
 
24615
24928
  // cli/commands/tutorial.ts
24616
24929
  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";
24930
+ import { dirname as dirname19, join as join21, resolve as resolve38 } from "path";
24618
24931
  import { fileURLToPath as fileURLToPath6 } from "url";
24619
24932
  import { Command as Command37, Option as Option35 } from "clipanion";
24620
24933
 
@@ -24710,7 +25023,7 @@ var TutorialCommand = class extends SmCommand {
24710
25023
  }
24711
25024
  const variant = rawVariant ?? DEFAULT_VARIANT;
24712
25025
  const spec = VARIANT_SPECS[variant];
24713
- const targetDir = join20(ctx.cwd, ".claude", "skills", spec.slug);
25026
+ const targetDir = join21(ctx.cwd, ".claude", "skills", spec.slug);
24714
25027
  const targetDisplay = `.claude/skills/${spec.slug}/`;
24715
25028
  if (existsSync32(targetDir) && !this.force) {
24716
25029
  this.printer.error(
@@ -24788,11 +25101,11 @@ function resolveSkillSourceDir(variant) {
24788
25101
  const here = dirname19(fileURLToPath6(import.meta.url));
24789
25102
  const candidates = [
24790
25103
  // dev: src/cli/commands/ → repo-root .claude/skills/<slug>/
24791
- resolve37(here, "../../..", spec.sourceDir),
25104
+ resolve38(here, "../../..", spec.sourceDir),
24792
25105
  // bundled: dist/cli.js → dist/cli/tutorial/<slug> (sibling)
24793
- resolve37(here, "cli/tutorial", spec.slug),
25106
+ resolve38(here, "cli/tutorial", spec.slug),
24794
25107
  // bundled fallback: any-depth → cli/tutorial/<slug>
24795
- resolve37(here, "../cli/tutorial", spec.slug)
25108
+ resolve38(here, "../cli/tutorial", spec.slug)
24796
25109
  ];
24797
25110
  for (const candidate of candidates) {
24798
25111
  if (existsSync32(candidate) && statSync12(candidate).isDirectory()) {