@skill-map/cli 0.55.0 → 0.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // cli/entry.ts
2
2
 
3
- !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="e21852cd-d92e-5bdf-abc7-02fffaa89502")}catch(e){}}();
3
+ !function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="49fdc2af-2694-5cd0-ac19-33f8d1dffa9e")}catch(e){}}();
4
4
  import { existsSync as existsSync33 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -197,6 +197,10 @@ function matchesFilter(hook, event) {
197
197
  function buildHookContext(_hook, trigger, event) {
198
198
  const data = event.data ?? {};
199
199
  const ctx = {
200
+ // `settings` is always populated (possibly empty) so hooks can read
201
+ // `ctx.settings.<id>` without a presence check. The composer
202
+ // populated `resolvedSettings` on each composed hook.
203
+ settings: _hook.resolvedSettings ?? {},
200
204
  event: {
201
205
  type: trigger,
202
206
  timestamp: event.timestamp,
@@ -246,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
246
250
  // package.json
247
251
  var package_default = {
248
252
  name: "@skill-map/cli",
249
- version: "0.55.0",
253
+ version: "0.56.0",
250
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
251
255
  license: "MIT",
252
256
  type: "module",
@@ -567,6 +571,41 @@ var ANTIGRAVITY_PLUGIN_ID = "antigravity";
567
571
  var AGENT_SKILLS_PLUGIN_ID = "agent-skills";
568
572
 
569
573
  // plugins/claude/providers/claude/index.ts
574
+ var RESERVED_SLASH_NAMES = [
575
+ "help",
576
+ "clear",
577
+ "compact",
578
+ "cost",
579
+ "init",
580
+ "model",
581
+ "agents",
582
+ "login",
583
+ "logout",
584
+ "mcp",
585
+ "memory",
586
+ "config",
587
+ "doctor",
588
+ "permissions",
589
+ "add-dir",
590
+ "bug",
591
+ "pr-comments",
592
+ "release-notes",
593
+ "review",
594
+ "terminal-setup",
595
+ "vim",
596
+ "output-style",
597
+ "hooks",
598
+ "install-github-app",
599
+ "migrate-installer",
600
+ "upgrade",
601
+ "resume",
602
+ "exit",
603
+ "quit",
604
+ "security-review",
605
+ "statusline",
606
+ "usage",
607
+ "feedback"
608
+ ];
570
609
  var claudeProvider = {
571
610
  id: "claude",
572
611
  pluginId: CLAUDE_PLUGIN_ID,
@@ -718,53 +757,18 @@ var claudeProvider = {
718
757
  // declaring one of these (e.g. `.claude/commands/help.md`) is
719
758
  // silently shadowed: the runtime runs the built-in instead.
720
759
  //
721
- // - `command`: canonical list of built-in slash commands documented
722
- // at https://docs.claude.com/en/docs/claude-code/slash-commands.
723
- // The list will drift as Anthropic adds / removes commands;
724
- // updates ship via a kernel patch + changeset (the catalog is
725
- // considered API surface that users rely on the analyzer to
726
- // reflect).
727
- // - `agent`: built-in agents Anthropic ships with the CLI. Smaller
760
+ // - `command` / `skill`: both reserve `RESERVED_SLASH_NAMES` (defined
761
+ // above). Per the resolution matrix they share the `/` invocation
762
+ // namespace, so a built-in slash command shadows a user node of
763
+ // EITHER kind that claims its name (a `skill` named `help` is as
764
+ // unreachable as a `command` named `help`). The catalog is API
765
+ // surface users rely on the analyzer to reflect.
766
+ // - `agent`: built-in agents Anthropic ships with the CLI, invoked
767
+ // through the `@` / Task namespace (separate from `/`). Smaller
728
768
  // surface than commands today.
729
- // - `skill`: no built-in skills today (skills are user-defined and
730
- // discovered from disk); the key is omitted on purpose, defaulting
731
- // to no reserved names for the kind.
732
769
  reservedNames: {
733
- command: [
734
- "help",
735
- "clear",
736
- "compact",
737
- "cost",
738
- "init",
739
- "model",
740
- "agents",
741
- "login",
742
- "logout",
743
- "mcp",
744
- "memory",
745
- "config",
746
- "doctor",
747
- "permissions",
748
- "add-dir",
749
- "bug",
750
- "pr-comments",
751
- "release-notes",
752
- "review",
753
- "terminal-setup",
754
- "vim",
755
- "output-style",
756
- "hooks",
757
- "install-github-app",
758
- "migrate-installer",
759
- "upgrade",
760
- "resume",
761
- "exit",
762
- "quit",
763
- "security-review",
764
- "statusline",
765
- "usage",
766
- "feedback"
767
- ],
770
+ command: RESERVED_SLASH_NAMES,
771
+ skill: RESERVED_SLASH_NAMES,
768
772
  agent: [
769
773
  "general-purpose",
770
774
  "output-style-setup",
@@ -1564,6 +1568,15 @@ var count2 = {
1564
1568
  emitWhenEmpty: false,
1565
1569
  priority: 30
1566
1570
  };
1571
+ var SETTING_IGNORED_DOMAINS = "ignored-domains";
1572
+ var settings = {
1573
+ [SETTING_IGNORED_DOMAINS]: {
1574
+ type: "string-list",
1575
+ label: "Ignored domains",
1576
+ description: "Hostnames to exclude from the external-URL count (e.g. internal mirrors, link shorteners).",
1577
+ default: []
1578
+ }
1579
+ };
1567
1580
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1568
1581
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1569
1582
  var externalUrlCounterExtractor = {
@@ -1572,6 +1585,14 @@ var externalUrlCounterExtractor = {
1572
1585
  kind: "extractor",
1573
1586
  description: "Counts the distinct external URLs in a node's body and shows the count on the card. Example: a body linking `https://example.com` and `https://docs.rs` shows a count of 2.",
1574
1587
  scope: "body",
1588
+ /**
1589
+ * Operator-configurable hostnames to exclude from the count. A URL
1590
+ * whose normalized `hostname` is in this list is skipped entirely:
1591
+ * no Signal, no contribution increment. Default empty (count every
1592
+ * external URL). The operator sets it via
1593
+ * `sm plugins config core/external-url-counter ignored-domains '["…"]'`.
1594
+ */
1595
+ settings,
1575
1596
  /**
1576
1597
  * Phase 6 / View contribution system, surface the distinct-URL
1577
1598
  * count as a card-footer-left chip alongside the in/out link
@@ -1591,6 +1612,7 @@ var externalUrlCounterExtractor = {
1591
1612
  ui: { count: count2 },
1592
1613
  extract(ctx) {
1593
1614
  const seen = /* @__PURE__ */ new Set();
1615
+ const ignoredDomains = readIgnoredDomains(ctx.settings[SETTING_IGNORED_DOMAINS]);
1594
1616
  const body = stripCodeBlocks(ctx.body);
1595
1617
  const lineStarts = computeLineStarts(body);
1596
1618
  for (const match of body.matchAll(URL_RE)) {
@@ -1598,8 +1620,9 @@ var externalUrlCounterExtractor = {
1598
1620
  if (original.length === 0) continue;
1599
1621
  const normalized = normalizeUrl(original);
1600
1622
  if (normalized === null) continue;
1601
- if (seen.has(normalized)) continue;
1602
- seen.add(normalized);
1623
+ if (ignoredDomains.has(normalized.host)) continue;
1624
+ if (seen.has(normalized.href)) continue;
1625
+ seen.add(normalized.href);
1603
1626
  const offset = match.index ?? 0;
1604
1627
  const line = lineFor(lineStarts, offset);
1605
1628
  ctx.emitSignal({
@@ -1611,12 +1634,12 @@ var externalUrlCounterExtractor = {
1611
1634
  {
1612
1635
  extractorId: ID6,
1613
1636
  kind: "references",
1614
- target: normalized,
1637
+ target: normalized.href,
1615
1638
  confidence: 0.3,
1616
1639
  rationale: "external URL pseudo-link, counted then dropped",
1617
1640
  trigger: {
1618
1641
  originalTrigger: original,
1619
- normalizedTrigger: normalized
1642
+ normalizedTrigger: normalized.href
1620
1643
  }
1621
1644
  }
1622
1645
  ]
@@ -1630,12 +1653,20 @@ var externalUrlCounterExtractor = {
1630
1653
  function stripTrailingPunctuation(raw) {
1631
1654
  return raw.replace(TRAILING_PUNCT, "");
1632
1655
  }
1656
+ function readIgnoredDomains(value) {
1657
+ if (!Array.isArray(value)) return /* @__PURE__ */ new Set();
1658
+ const out = /* @__PURE__ */ new Set();
1659
+ for (const entry of value) {
1660
+ if (typeof entry === "string" && entry.length > 0) out.add(entry.toLowerCase());
1661
+ }
1662
+ return out;
1663
+ }
1633
1664
  function normalizeUrl(raw) {
1634
1665
  try {
1635
1666
  const url = new URL(raw);
1636
1667
  url.hostname = url.hostname.toLowerCase();
1637
1668
  url.hash = "";
1638
- return url.href;
1669
+ return { href: url.href, host: url.hostname };
1639
1670
  } catch {
1640
1671
  return null;
1641
1672
  }
@@ -2222,7 +2253,7 @@ var ID15 = "link-conflict";
2222
2253
  var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2223
2254
  var linkConflictAnalyzer = {
2224
2255
  id: ID15,
2225
- pluginId: "core",
2256
+ pluginId: CORE_PLUGIN_ID,
2226
2257
  kind: "analyzer",
2227
2258
  description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
2228
2259
  mode: "deterministic",
@@ -2287,52 +2318,30 @@ function rankConfidence(c) {
2287
2318
  return c;
2288
2319
  }
2289
2320
 
2290
- // kernel/util/trigger-resolve.ts
2291
- function buildNameIndex(nodes) {
2292
- const out = /* @__PURE__ */ new Map();
2293
- indexByCanonicalName(nodes, out);
2294
- fillIndexWithPathBasename(nodes, out);
2295
- return out;
2296
- }
2297
- function indexByCanonicalName(nodes, out) {
2298
- for (const node of nodes) {
2299
- const raw = canonicalName(node);
2300
- if (raw === null) continue;
2301
- const key = normalizeTrigger(raw);
2302
- if (!out.has(key)) out.set(key, node.path);
2303
- }
2321
+ // kernel/util/link-lines.ts
2322
+ function isSelfLoop(link) {
2323
+ if (link.source === link.target) return true;
2324
+ if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
2325
+ return false;
2304
2326
  }
2305
- function fillIndexWithPathBasename(nodes, out) {
2306
- for (const node of nodes) {
2307
- if (canonicalName(node) !== null) continue;
2308
- const derived = pathBasenameForLink(node.path);
2309
- if (derived.length === 0) continue;
2310
- const key = normalizeTrigger(derived);
2311
- if (!out.has(key)) out.set(key, node.path);
2327
+ function linkLines(link) {
2328
+ const lines = /* @__PURE__ */ new Set();
2329
+ for (const occ of link.occurrences ?? []) {
2330
+ const line = occ.location?.line;
2331
+ if (typeof line === "number") lines.add(line);
2312
2332
  }
2313
- }
2314
- function canonicalName(node) {
2315
- const raw = node.frontmatter?.["name"];
2316
- if (typeof raw !== "string" || raw.length === 0) return null;
2317
- return raw;
2318
- }
2319
- function pathBasenameForLink(path) {
2320
- const segments = path.split("/").filter((s) => s.length > 0);
2321
- if (segments.length === 0) return path;
2322
- const last = segments[segments.length - 1];
2323
- if (last === "SKILL.md" && segments.length >= 2) {
2324
- return segments[segments.length - 2];
2333
+ if (lines.size === 0) {
2334
+ const line = link.location?.line;
2335
+ if (typeof line === "number") lines.add(line);
2325
2336
  }
2326
- return last.replace(/\.md$/, "");
2337
+ return [...lines].sort((a, b) => a - b);
2327
2338
  }
2328
- function resolveLinkTargetToPath(link, nameIndex) {
2329
- const raw = link.target;
2330
- const sigil = raw.charAt(0);
2331
- if (sigil !== "/" && sigil !== "@") return raw;
2332
- const normalizedTrigger = link.trigger?.normalizedTrigger;
2333
- const normalized = typeof normalizedTrigger === "string" ? normalizedTrigger.replace(/^[/@]/, "").trim() : normalizeTrigger(raw.slice(1));
2334
- const resolved = nameIndex.get(normalized);
2335
- return resolved ?? raw;
2339
+ function linkWhere(link, texts) {
2340
+ const lines = linkLines(link);
2341
+ if (lines.length === 0) return "";
2342
+ return tx(lines.length === 1 ? texts.single : texts.plural, {
2343
+ lines: lines.join(", ")
2344
+ });
2336
2345
  }
2337
2346
 
2338
2347
  // plugins/core/analyzers/link-counter/index.ts
@@ -2359,13 +2368,12 @@ var linkCounterAnalyzer = {
2359
2368
  mode: "deterministic",
2360
2369
  ui: { linksIn, linksOut },
2361
2370
  evaluate(ctx) {
2362
- const nameIndex = buildNameIndex(ctx.nodes);
2363
2371
  const perTarget = /* @__PURE__ */ new Map();
2364
2372
  const perSource = /* @__PURE__ */ new Map();
2365
2373
  for (const link of ctx.links) {
2366
- const resolvedTarget = resolveLinkTargetToPath(link, nameIndex);
2367
- if (link.source === link.target || link.source === resolvedTarget) continue;
2368
- bump(perTarget, resolvedTarget, link.kind);
2374
+ if (isSelfLoop(link)) continue;
2375
+ const target = link.resolvedTarget ?? link.target;
2376
+ bump(perTarget, target, link.kind);
2369
2377
  bump(perSource, link.source, link.kind);
2370
2378
  }
2371
2379
  for (const node of ctx.nodes) {
@@ -2399,27 +2407,6 @@ function formatBreakdown(byKind, direction) {
2399
2407
  return [direction, ...lines].join("\n");
2400
2408
  }
2401
2409
 
2402
- // kernel/util/link-lines.ts
2403
- function linkLines(link) {
2404
- const lines = /* @__PURE__ */ new Set();
2405
- for (const occ of link.occurrences ?? []) {
2406
- const line = occ.location?.line;
2407
- if (typeof line === "number") lines.add(line);
2408
- }
2409
- if (lines.size === 0) {
2410
- const line = link.location?.line;
2411
- if (typeof line === "number") lines.add(line);
2412
- }
2413
- return [...lines].sort((a, b) => a - b);
2414
- }
2415
- function linkWhere(link, texts) {
2416
- const lines = linkLines(link);
2417
- if (lines.length === 0) return "";
2418
- return tx(lines.length === 1 ? texts.single : texts.plural, {
2419
- lines: lines.join(", ")
2420
- });
2421
- }
2422
-
2423
2410
  // plugins/core/analyzers/link-self-loop/text.ts
2424
2411
  var LINK_SELF_LOOP_TEXTS = {
2425
2412
  /**
@@ -2465,22 +2452,13 @@ var linkSelfLoopAnalyzer = {
2465
2452
  data: {
2466
2453
  target: link.target,
2467
2454
  resolvedTarget: link.resolvedTarget ?? link.target,
2468
- kind: link.kind,
2469
- // Mark explicitly so UI / downstream consumers can read this
2470
- // single field instead of re-computing the `source === target`
2471
- // predicate themselves.
2472
- selfLoop: true
2455
+ kind: link.kind
2473
2456
  }
2474
2457
  });
2475
2458
  }
2476
2459
  return issues;
2477
2460
  }
2478
2461
  };
2479
- function isSelfLoop(link) {
2480
- if (link.source === link.target) return true;
2481
- if (link.resolvedTarget && link.source === link.resolvedTarget) return true;
2482
- return false;
2483
- }
2484
2462
 
2485
2463
  // kernel/orchestrator/node-identifiers.ts
2486
2464
  import { posix as pathPosix4 } from "path";
@@ -2530,6 +2508,15 @@ function liftResolvedLinkConfidence(links, nodes, ctx) {
2530
2508
  if (link.confidence < 1) applyResolution(link, indexes, ctx);
2531
2509
  }
2532
2510
  }
2511
+ function collectBrokenLinks(links, nodes, ctx) {
2512
+ const broken = /* @__PURE__ */ new Set();
2513
+ if (links.length === 0) return broken;
2514
+ const indexes = buildIndexes(nodes, ctx);
2515
+ for (const link of links) {
2516
+ if (isGenuinelyBroken(link, indexes)) broken.add(link);
2517
+ }
2518
+ return broken;
2519
+ }
2533
2520
  function applyResolution(link, indexes, ctx) {
2534
2521
  const resolution = resolve2(link, indexes, ctx);
2535
2522
  if (resolution === "none") {
@@ -2606,7 +2593,7 @@ var NAME_RESERVED_TEXTS = {
2606
2593
  * a runtime built-in. Same wording skill-map shipped before the
2607
2594
  * source-side link finding landed.
2608
2595
  */
2609
- message: "Built-in {{provider}} {{kind}}:\nShadowed by this file; the runtime uses its built-in instead. Rename the file or its `frontmatter.name`.",
2596
+ message: "Name collision: this {{kind}} name is already used by the {{provider}} runtime built-in, which shadows this file. Rename the file or its `frontmatter.name`.",
2610
2597
  /**
2611
2598
  * Source-side message: emitted on the node that AUTHORED a link
2612
2599
  * whose target resolves to a reserved name. Explains WHY the link's
@@ -2614,7 +2601,7 @@ var NAME_RESERVED_TEXTS = {
2614
2601
  * the kernel saw the target match a runtime built-in and downgraded
2615
2602
  * the edge so the operator notices.
2616
2603
  */
2617
- linkMessage: "{{target}}:\nResolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; edge downgraded to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2604
+ linkMessage: "{{target}}:\nName collision: resolves to a {{provider}} built-in ({{reservedKind}} `{{reservedPath}}`){{where}}; the built-in wins, so this edge drops to confidence {{confidence}}. Rename the target file or its `frontmatter.name`.",
2618
2605
  /** Location suffix after the built-in parens, one detection site. */
2619
2606
  whereSingle: " (line {{lines}})",
2620
2607
  /** Location suffix after the built-in parens, several detection sites. */
@@ -2653,7 +2640,9 @@ var nameReservedAnalyzer = {
2653
2640
  }
2654
2641
  for (const link of ctx.links) {
2655
2642
  if (link.confidence !== RESERVED_TARGET_CONFIDENCE) continue;
2656
- const reservedNode = findReservedNodeForLink(link, reserved, byPath3);
2643
+ const reservedPath = link.resolvedTarget;
2644
+ if (!reservedPath || !reserved.has(reservedPath)) continue;
2645
+ const reservedNode = byPath3.get(reservedPath);
2657
2646
  if (!reservedNode) continue;
2658
2647
  issues.push({
2659
2648
  analyzerId: ID18,
@@ -2686,41 +2675,6 @@ function linkWhereSuffix(link) {
2686
2675
  plural: NAME_RESERVED_TEXTS.wherePlural
2687
2676
  });
2688
2677
  }
2689
- function findReservedNodeForLink(link, reserved, byPath3) {
2690
- if (reserved.has(link.target)) {
2691
- const node = byPath3.get(link.target);
2692
- if (node) return node;
2693
- }
2694
- const trigger = link.trigger?.normalizedTrigger;
2695
- if (!trigger) return null;
2696
- const stripped = trigger.replace(/^[/@]/, "").trim();
2697
- if (stripped.length === 0) return null;
2698
- for (const path of reserved) {
2699
- const node = byPath3.get(path);
2700
- if (!node) continue;
2701
- if (matchesNodeIdentifier(node, stripped)) return node;
2702
- }
2703
- return null;
2704
- }
2705
- function matchesNodeIdentifier(node, stripped) {
2706
- const candidates = [];
2707
- const fmName = node.frontmatter?.["name"];
2708
- if (typeof fmName === "string" && fmName.length > 0) candidates.push(normaliseId(fmName));
2709
- const basename = node.path.split("/").pop() ?? "";
2710
- if (basename) {
2711
- const stem = basename.replace(/\.[^.]+$/, "");
2712
- if (stem) candidates.push(normaliseId(stem));
2713
- }
2714
- const segs = node.path.split("/");
2715
- if (segs.length >= 2) {
2716
- const dirBase = segs[segs.length - 2];
2717
- if (dirBase) candidates.push(normaliseId(dirBase));
2718
- }
2719
- return candidates.includes(stripped);
2720
- }
2721
- function normaliseId(raw) {
2722
- return raw.normalize("NFD").replace(new RegExp("\\p{Mn}+", "gu"), "").toLowerCase().replace(/[-_\s]+/g, " ").replace(/ +/g, " ").trim();
2723
- }
2724
2678
 
2725
2679
  // plugins/core/analyzers/node-stability/text.ts
2726
2680
  var NODE_STABILITY_TEXTS = {
@@ -2891,7 +2845,7 @@ function pickSupersededBy(node) {
2891
2845
  }
2892
2846
 
2893
2847
  // plugins/core/analyzers/reference-broken/index.ts
2894
- import { posix as pathPosix5, resolve as resolve3 } from "path";
2848
+ import { resolve as resolve3 } from "path";
2895
2849
 
2896
2850
  // plugins/core/analyzers/reference-broken/text.ts
2897
2851
  var REFERENCE_BROKEN_TEXTS = {
@@ -2922,13 +2876,7 @@ var REFERENCE_BROKEN_TEXTS = {
2922
2876
  // Tooltips for the per-node view-contribution badges. Singular vs
2923
2877
  // plural keeps the count grammar correct without a sub-template.
2924
2878
  alertTooltipSingle: "This node has a broken reference. Open the inspector for details.",
2925
- alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details.",
2926
- // Fix-summary copy when the broken trigger has a same-named file on
2927
- // disk that does not advertise `name:` in its frontmatter. Two
2928
- // variants for single vs multiple candidates; same template family
2929
- // as the alert tooltips above.
2930
- hintSummarySingle: "Add `name: {{name}}` to the frontmatter of {{candidate}} so this reference resolves.",
2931
- hintSummaryMany: "Add `name: {{name}}` to the frontmatter of one of these files so this reference resolves: {{candidates}}."
2879
+ alertTooltipMany: "This node has {{count}} broken references. Open the inspector for details."
2932
2880
  };
2933
2881
 
2934
2882
  // plugins/core/analyzers/reference-broken/index.ts
@@ -2944,33 +2892,31 @@ var referenceBrokenAnalyzer = {
2944
2892
  // aggregate severity counters now owned by `core/issue-counter`. The
2945
2893
  // detection logic stays intact, only the chip emission is gone.
2946
2894
  ui: {},
2947
- // The resolver, the reference-paths escape hatch, and the hint
2948
- // index all share the per-link loop, splitting would re-walk
2949
- // `ctx.links` once per concern. The per-source aggregation that
2950
- // historically lived alongside (driving the now-retired chip
2951
- // emission) moved into `core/issue-counter`.
2895
+ // Pure projector of the orchestrator's genuinely-broken verdict
2896
+ // (`ctx.brokenLinks`, computed once from the same name index the
2897
+ // confidence lift uses), with the reference-paths escape hatch layered
2898
+ // on top. The per-source aggregation that historically lived alongside
2899
+ // (driving the now-retired chip emission) moved into
2900
+ // `core/issue-counter`.
2952
2901
  evaluate(ctx) {
2953
- const byPath3 = new Set(ctx.nodes.map((n) => n.path));
2954
- const byNormalizedName = indexByNormalizedName(ctx.nodes);
2955
- const byBasenameWithoutName = indexByBasenameWithoutName(ctx.nodes);
2956
- const refIndex = ctx.referenceablePaths && ctx.referenceablePaths.size > 0 && ctx.cwd ? { paths: ctx.referenceablePaths, cwd: ctx.cwd } : null;
2902
+ const broken = ctx.brokenLinks;
2903
+ if (!broken || broken.size === 0) return [];
2904
+ const refIndex = buildReferenceIndex(ctx);
2957
2905
  const issues = [];
2958
2906
  for (const link of ctx.links) {
2959
- if (isResolved(link, byPath3, byNormalizedName)) continue;
2907
+ if (!broken.has(link)) continue;
2960
2908
  if (refIndex && resolvesViaReferencePaths(link, refIndex)) continue;
2961
- const candidates = findHintCandidates(link, byBasenameWithoutName);
2962
- issues.push(buildIssue(link, candidates));
2909
+ issues.push(buildIssue(link));
2963
2910
  }
2964
2911
  return issues;
2965
2912
  }
2966
2913
  };
2967
- function buildIssue(link, hintCandidates = []) {
2968
- const data = {
2969
- target: link.target,
2970
- kind: link.kind,
2971
- trigger: link.trigger?.normalizedTrigger ?? null
2972
- };
2973
- const issue = {
2914
+ function buildReferenceIndex(ctx) {
2915
+ if (!ctx.referenceablePaths || ctx.referenceablePaths.size === 0 || !ctx.cwd) return null;
2916
+ return { paths: ctx.referenceablePaths, cwd: ctx.cwd };
2917
+ }
2918
+ function buildIssue(link) {
2919
+ return {
2974
2920
  analyzerId: ID21,
2975
2921
  // `error`, not `warn`: a link whose target is not in the scan is a
2976
2922
  // structural defect the operator must notice, and the card chip
@@ -2988,86 +2934,17 @@ function buildIssue(link, hintCandidates = []) {
2988
2934
  plural: REFERENCE_BROKEN_TEXTS.wherePlural
2989
2935
  })
2990
2936
  }),
2991
- data
2992
- };
2993
- if (hintCandidates.length > 0) attachHint(issue, data, link, hintCandidates);
2994
- return issue;
2995
- }
2996
- function attachHint(issue, data, link, hintCandidates) {
2997
- const suggestedName = (link.trigger?.normalizedTrigger ?? "").replace(/^[/@]/, "").trim();
2998
- const candidatePaths = hintCandidates.map((n) => n.path);
2999
- data["hint"] = {
3000
- kind: "missing-frontmatter-name",
3001
- suggestedName,
3002
- candidates: candidatePaths
3003
- };
3004
- issue.fix = {
3005
- summary: candidatePaths.length === 1 ? tx(REFERENCE_BROKEN_TEXTS.hintSummarySingle, {
3006
- name: suggestedName,
3007
- candidate: candidatePaths[0]
3008
- }) : tx(REFERENCE_BROKEN_TEXTS.hintSummaryMany, {
3009
- name: suggestedName,
3010
- candidates: candidatePaths.join(", ")
3011
- }),
3012
- autofixable: false
2937
+ data: {
2938
+ target: link.target,
2939
+ kind: link.kind,
2940
+ trigger: link.trigger?.normalizedTrigger ?? null
2941
+ }
3013
2942
  };
3014
2943
  }
3015
2944
  function resolvesViaReferencePaths(link, refIndex) {
3016
2945
  if (!isPathStyleLink(link)) return false;
3017
2946
  return refIndex.paths.has(resolve3(refIndex.cwd, link.target));
3018
2947
  }
3019
- function indexByNormalizedName(nodes) {
3020
- const out = /* @__PURE__ */ new Map();
3021
- for (const node of nodes) {
3022
- const raw = node.frontmatter?.["name"];
3023
- const name = typeof raw === "string" ? raw : "";
3024
- if (!name) continue;
3025
- const key = normalizeTrigger(name);
3026
- const bucket = out.get(key) ?? [];
3027
- bucket.push(node);
3028
- out.set(key, bucket);
3029
- }
3030
- return out;
3031
- }
3032
- function basenameWithoutExt(path) {
3033
- const base = pathPosix5.basename(path);
3034
- const ext = pathPosix5.extname(base);
3035
- return ext ? base.slice(0, -ext.length) : base;
3036
- }
3037
- function indexByBasenameWithoutName(nodes) {
3038
- const out = /* @__PURE__ */ new Map();
3039
- for (const node of nodes) {
3040
- const raw = node.frontmatter?.["name"];
3041
- const name = typeof raw === "string" ? raw : "";
3042
- if (name) continue;
3043
- const bare = basenameWithoutExt(node.path);
3044
- if (!bare) continue;
3045
- const key = normalizeTrigger(bare);
3046
- if (!key) continue;
3047
- const bucket = out.get(key) ?? [];
3048
- bucket.push(node);
3049
- out.set(key, bucket);
3050
- }
3051
- return out;
3052
- }
3053
- function findHintCandidates(link, idx) {
3054
- const normalized = link.trigger?.normalizedTrigger;
3055
- if (!normalized) return [];
3056
- const sigil = normalized.charAt(0);
3057
- if (sigil !== "/" && sigil !== "@") return [];
3058
- const withoutSigil = normalized.slice(1).trim();
3059
- if (!withoutSigil) return [];
3060
- return idx.get(withoutSigil) ?? [];
3061
- }
3062
- function isResolved(link, byPath3, byNormalizedName) {
3063
- const normalized = link.trigger?.normalizedTrigger;
3064
- if (normalized) {
3065
- const withoutSigil = normalized.replace(/^[/@]/, "").trim();
3066
- if (byNormalizedName.has(withoutSigil)) return true;
3067
- }
3068
- if (byPath3.has(link.target)) return true;
3069
- return false;
3070
- }
3071
2948
  function isPathStyleLink(link) {
3072
2949
  const sigil = link.trigger?.normalizedTrigger?.charAt(0);
3073
2950
  if (sigil === "/" || sigil === "@") return false;
@@ -3106,12 +2983,9 @@ var referenceRedundantAnalyzer = {
3106
2983
  mode: "deterministic",
3107
2984
  evaluate(ctx) {
3108
2985
  if (ctx.links.length === 0) return [];
3109
- const byPath3 = /* @__PURE__ */ new Map();
3110
- for (const node of ctx.nodes) byPath3.set(node.path, node);
3111
- const byName = buildNameIndex2(ctx.nodes);
3112
2986
  const groups = /* @__PURE__ */ new Map();
3113
2987
  for (const link of ctx.links) {
3114
- const resolved = resolveTargetPath(link, byPath3, byName);
2988
+ const resolved = link.resolvedTarget;
3115
2989
  if (!resolved) continue;
3116
2990
  const key = `${link.source}\0${resolved}`;
3117
2991
  const bucket = groups.get(key);
@@ -3192,45 +3066,6 @@ function formatGroupedOccurrences(occurrences) {
3192
3066
  })
3193
3067
  ).join(REFERENCE_REDUNDANT_TEXTS.occurrenceSeparator);
3194
3068
  }
3195
- function buildNameIndex2(nodes) {
3196
- const out = /* @__PURE__ */ new Map();
3197
- for (const node of nodes) {
3198
- for (const candidate of collectIdentifiers(node)) {
3199
- const normalised = normalizeTrigger(candidate);
3200
- if (!normalised) continue;
3201
- const bucket = out.get(normalised);
3202
- if (bucket) bucket.push(node.path);
3203
- else out.set(normalised, [node.path]);
3204
- }
3205
- }
3206
- return out;
3207
- }
3208
- function collectIdentifiers(node) {
3209
- const out = [];
3210
- const fmName = node.frontmatter?.["name"];
3211
- if (typeof fmName === "string" && fmName.length > 0) out.push(fmName);
3212
- const segs = node.path.split("/");
3213
- const last = segs[segs.length - 1] ?? "";
3214
- if (last) {
3215
- const stem = last.replace(/\.[^.]+$/, "");
3216
- if (stem) out.push(stem);
3217
- }
3218
- if (segs.length >= 2) {
3219
- const dirBase = segs[segs.length - 2];
3220
- if (dirBase) out.push(dirBase);
3221
- }
3222
- return out;
3223
- }
3224
- function resolveTargetPath(link, byPath3, byName) {
3225
- if (byPath3.has(link.target)) return link.target;
3226
- const trigger = link.trigger?.normalizedTrigger;
3227
- if (!trigger) return null;
3228
- const stripped = trigger.replace(/^[/@]/, "").trim();
3229
- if (!stripped) return null;
3230
- const candidates = byName.get(stripped);
3231
- if (!candidates || candidates.length === 0) return null;
3232
- return candidates[0] ?? null;
3233
- }
3234
3069
 
3235
3070
  // kernel/adapters/schema-validators.ts
3236
3071
  import { readFileSync as readFileSync2 } from "fs";
@@ -4789,11 +4624,11 @@ function validateOrDefault(parsed) {
4789
4624
  if (!result.ok) return defaultSettings();
4790
4625
  return result.data;
4791
4626
  }
4792
- function backfillSubObjects(settings) {
4627
+ function backfillSubObjects(settings2) {
4793
4628
  return {
4794
- ...settings,
4795
- updateCheck: settings.updateCheck ?? {},
4796
- telemetry: settings.telemetry ?? {}
4629
+ ...settings2,
4630
+ updateCheck: settings2.updateCheck ?? {},
4631
+ telemetry: settings2.telemetry ?? {}
4797
4632
  };
4798
4633
  }
4799
4634
  function writeUserSettings(patch) {
@@ -4813,24 +4648,24 @@ function writeUserSettings(patch) {
4813
4648
  }
4814
4649
  }
4815
4650
  function isUpdateCheckEnabled() {
4816
- const settings = readUserSettings();
4817
- return settings.updateCheck?.enabled !== false;
4651
+ const settings2 = readUserSettings();
4652
+ return settings2.updateCheck?.enabled !== false;
4818
4653
  }
4819
4654
  function isErrorTelemetryEnabled() {
4820
- const settings = readUserSettings();
4821
- return settings.telemetry?.errorsEnabled === true;
4655
+ const settings2 = readUserSettings();
4656
+ return settings2.telemetry?.errorsEnabled === true;
4822
4657
  }
4823
4658
  function isUsageCliTelemetryEnabled() {
4824
- const settings = readUserSettings();
4825
- return settings.telemetry?.usageCliEnabled === true;
4659
+ const settings2 = readUserSettings();
4660
+ return settings2.telemetry?.usageCliEnabled === true;
4826
4661
  }
4827
4662
  function isUsageUiTelemetryEnabled() {
4828
- const settings = readUserSettings();
4829
- return settings.telemetry?.usageUiEnabled === true;
4663
+ const settings2 = readUserSettings();
4664
+ return settings2.telemetry?.usageUiEnabled === true;
4830
4665
  }
4831
4666
  function readAnonymousId() {
4832
- const settings = readUserSettings();
4833
- return settings.telemetry?.anonymousId ?? null;
4667
+ const settings2 = readUserSettings();
4668
+ return settings2.telemetry?.anonymousId ?? null;
4834
4669
  }
4835
4670
  function ensureAnonymousId(generate = () => randomUUID()) {
4836
4671
  const existing = readAnonymousId();
@@ -4840,12 +4675,12 @@ function ensureAnonymousId(generate = () => randomUUID()) {
4840
4675
  return id;
4841
4676
  }
4842
4677
  function hasTelemetryPromptBeenShown() {
4843
- const settings = readUserSettings();
4844
- return typeof settings.telemetry?.promptedAt === "number";
4678
+ const settings2 = readUserSettings();
4679
+ return typeof settings2.telemetry?.promptedAt === "number";
4845
4680
  }
4846
4681
  function hasSeenFirstRun() {
4847
- const settings = readUserSettings();
4848
- return typeof settings.telemetry?.firstRunAt === "number";
4682
+ const settings2 = readUserSettings();
4683
+ return typeof settings2.telemetry?.firstRunAt === "number";
4849
4684
  }
4850
4685
  function mergeSettings(current, patch) {
4851
4686
  const merged = {
@@ -6255,6 +6090,10 @@ function removeConfigValue(key, opts) {
6255
6090
  writeJsonAtomic(path, merged);
6256
6091
  return true;
6257
6092
  }
6093
+ function getValueSource(key, opts) {
6094
+ const loaded = loadConfigForScope(opts);
6095
+ return loaded.sources.get(key);
6096
+ }
6258
6097
  function loadConfigForScope(opts) {
6259
6098
  return loadConfig({
6260
6099
  cwd: opts.cwd,
@@ -11695,6 +11534,7 @@ function filterBuiltInManifests(manifests, resolveEnabled) {
11695
11534
  // core/runtime/plugin-runtime/composer.ts
11696
11535
  function composeScanExtensions(opts) {
11697
11536
  const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
11537
+ const resolveSettings = opts.resolveSettings;
11698
11538
  const providers = [];
11699
11539
  const extractors = [];
11700
11540
  const analyzers = [];
@@ -11703,20 +11543,21 @@ function composeScanExtensions(opts) {
11703
11543
  if (!opts.noBuiltIns) {
11704
11544
  accumulateBuiltInScanExtensions(
11705
11545
  { providers, extractors, analyzers, hooks, actions },
11706
- resolveEnabled
11546
+ resolveEnabled,
11547
+ resolveSettings
11707
11548
  );
11708
11549
  }
11709
11550
  for (const ext of opts.pluginRuntime.extensions.providers) {
11710
- if (isPluginExtensionEnabled(ext, resolveEnabled)) providers.push(ext);
11551
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) providers.push(withResolvedSettings(ext, resolveSettings));
11711
11552
  }
11712
11553
  for (const ext of opts.pluginRuntime.extensions.extractors) {
11713
- if (isPluginExtensionEnabled(ext, resolveEnabled)) extractors.push(ext);
11554
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) extractors.push(withResolvedSettings(ext, resolveSettings));
11714
11555
  }
11715
11556
  for (const ext of opts.pluginRuntime.extensions.analyzers) {
11716
- if (isPluginExtensionEnabled(ext, resolveEnabled)) analyzers.push(ext);
11557
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) analyzers.push(withResolvedSettings(ext, resolveSettings));
11717
11558
  }
11718
11559
  for (const ext of opts.pluginRuntime.extensions.hooks) {
11719
- if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(ext);
11560
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) hooks.push(withResolvedSettings(ext, resolveSettings));
11720
11561
  }
11721
11562
  for (const ext of opts.pluginRuntime.extensions.actions) {
11722
11563
  if (isPluginExtensionEnabled(ext, resolveEnabled)) actions.push(ext);
@@ -11735,22 +11576,26 @@ function composeScanExtensions(opts) {
11735
11576
  actions
11736
11577
  };
11737
11578
  }
11738
- function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
11579
+ function withResolvedSettings(ext, resolveSettings) {
11580
+ if (!resolveSettings) return ext;
11581
+ return { ...ext, resolvedSettings: resolveSettings(ext) };
11582
+ }
11583
+ function accumulateBuiltInScanExtensions(buckets, resolveEnabled, resolveSettings) {
11739
11584
  for (const plugin of builtInPlugins) {
11740
11585
  for (const ext of plugin.extensions) {
11741
11586
  if (!isBuiltInExtensionEnabled(plugin, ext, resolveEnabled)) continue;
11742
11587
  switch (ext.kind) {
11743
11588
  case "provider":
11744
- buckets.providers.push(ext);
11589
+ buckets.providers.push(withResolvedSettings(ext, resolveSettings));
11745
11590
  break;
11746
11591
  case "extractor":
11747
- buckets.extractors.push(ext);
11592
+ buckets.extractors.push(withResolvedSettings(ext, resolveSettings));
11748
11593
  break;
11749
11594
  case "analyzer":
11750
- buckets.analyzers.push(ext);
11595
+ buckets.analyzers.push(withResolvedSettings(ext, resolveSettings));
11751
11596
  break;
11752
11597
  case "hook":
11753
- buckets.hooks.push(ext);
11598
+ buckets.hooks.push(withResolvedSettings(ext, resolveSettings));
11754
11599
  break;
11755
11600
  case "action":
11756
11601
  buckets.actions.push(ext);
@@ -11768,18 +11613,19 @@ function accumulateBuiltInScanExtensions(buckets, resolveEnabled) {
11768
11613
  function composeFormatters(opts) {
11769
11614
  const noBuiltIns = opts.noBuiltIns ?? false;
11770
11615
  const resolveEnabled = opts.resolveEnabled ?? opts.pluginRuntime.resolveEnabled;
11616
+ const resolveSettings = opts.resolveSettings;
11771
11617
  const out = [];
11772
11618
  if (!noBuiltIns) {
11773
11619
  for (const plugin of builtInPlugins) {
11774
11620
  for (const ext of plugin.extensions) {
11775
11621
  if (ext.kind !== "formatter") continue;
11776
11622
  if (!isBuiltInExtensionEnabled(plugin, ext, resolveEnabled)) continue;
11777
- out.push(ext);
11623
+ out.push(withResolvedSettings(ext, resolveSettings));
11778
11624
  }
11779
11625
  }
11780
11626
  }
11781
11627
  for (const ext of opts.pluginRuntime.extensions.formatters) {
11782
- if (isPluginExtensionEnabled(ext, resolveEnabled)) out.push(ext);
11628
+ if (isPluginExtensionEnabled(ext, resolveEnabled)) out.push(withResolvedSettings(ext, resolveSettings));
11783
11629
  }
11784
11630
  return out;
11785
11631
  }
@@ -14966,6 +14812,9 @@ var GraphCommand = class extends SmCommand {
14966
14812
  nodes: scan.nodes,
14967
14813
  links: scan.links,
14968
14814
  issues: scan.issues,
14815
+ // Resolved settings of the formatter (empty when the formatter
14816
+ // declares none, or when the composer did not populate them).
14817
+ settings: formatter.resolvedSettings ?? {},
14969
14818
  // Pass the full persisted scan so format-specific renderers
14970
14819
  // that mirror a `ScanResult` envelope (today: built-in `json`)
14971
14820
  // can emit it verbatim without re-deriving fields like
@@ -15977,12 +15826,12 @@ function emitExtensionError(emitter, qualifiedId2, nodePath, data) {
15977
15826
  }
15978
15827
  function buildExtractorContext(extractor, node, body, frontmatter, emitLink, enrichNode, emitContribution, emitSignal, emitNode, store) {
15979
15828
  const scope = extractor.scope ?? "both";
15980
- const settings = extractor.resolvedSettings ?? {};
15829
+ const settings2 = extractor.resolvedSettings ?? {};
15981
15830
  return {
15982
15831
  node,
15983
15832
  body: scope === "frontmatter" ? "" : body,
15984
15833
  frontmatter: scope === "body" ? {} : frontmatter,
15985
- settings,
15834
+ settings: settings2,
15986
15835
  emitLink,
15987
15836
  enrichNode,
15988
15837
  emitContribution,
@@ -16281,7 +16130,7 @@ function runActionProjections(actions, nodes, links, emitter) {
16281
16130
  }
16282
16131
 
16283
16132
  // kernel/orchestrator/analyzers.ts
16284
- async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, signals, seedIssues = []) {
16133
+ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sidecarRoots, annotationContributions, viewContributions, orphanJobFiles, referenceablePaths, cwd, registeredActionIds, emitter, hookDispatcher, reservedNodePaths, brokenLinks, signals, seedIssues = []) {
16285
16134
  const issues = [...seedIssues];
16286
16135
  const contributions = [];
16287
16136
  const contributionErrors = [];
@@ -16358,6 +16207,10 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16358
16207
  const emitted = await analyzer.evaluate({
16359
16208
  nodes,
16360
16209
  links: internalLinks,
16210
+ // `settings` is always populated (possibly empty) so analyzers can
16211
+ // read `ctx.settings.<id>` without a presence check. The composer
16212
+ // populated `resolvedSettings` on each composed analyzer.
16213
+ settings: analyzer.resolvedSettings ?? {},
16361
16214
  orphanSidecars: analyzerOrphans,
16362
16215
  sidecarRoots,
16363
16216
  annotationContributions,
@@ -16371,6 +16224,7 @@ async function runAnalyzers(analyzers, nodes, internalLinks, orphanSidecars, sid
16371
16224
  ...referenceablePaths ? { referenceablePaths } : {},
16372
16225
  ...cwd ? { cwd } : {},
16373
16226
  ...reservedNodePaths ? { reservedNodePaths } : {},
16227
+ ...brokenLinks ? { brokenLinks } : {},
16374
16228
  ...signals && signals.length > 0 ? { signals } : {},
16375
16229
  emitContribution
16376
16230
  });
@@ -17593,6 +17447,7 @@ async function runScanInternal(_kernel, options) {
17593
17447
  walked.signals = resolved.resolvedSignals;
17594
17448
  const postWalkCtx = buildPostWalkTransformCtx(exts.providers, walked.nodes, activeProviderId);
17595
17449
  walked.internalLinks = applyPostWalkTransforms(walked.internalLinks, walked.nodes, postWalkCtx);
17450
+ const brokenLinks = collectBrokenLinks(walked.internalLinks, walked.nodes, postWalkCtx);
17596
17451
  recomputeLinkCounts(walked.nodes, walked.internalLinks);
17597
17452
  recomputeExternalRefsCount(walked.nodes, walked.externalLinks, walked.cachedPaths);
17598
17453
  await dispatchExtractorCompleted(exts.extractors, emitter, hookDispatcher);
@@ -17614,6 +17469,7 @@ async function runScanInternal(_kernel, options) {
17614
17469
  emitter,
17615
17470
  hookDispatcher,
17616
17471
  postWalkCtx.reservedNodePaths,
17472
+ brokenLinks,
17617
17473
  walked.signals,
17618
17474
  // Seed the accumulator with orchestrator-emitted frontmatter
17619
17475
  // issues so the aggregate phase (`core/issue-counter`) counts
@@ -18058,6 +17914,159 @@ function findOrphanJobFiles(jobsDir, referencedPaths) {
18058
17914
  return { orphanFilePaths: orphans, referencedCount: referencedPaths.size };
18059
17915
  }
18060
17916
 
17917
+ // core/config/plugin-settings.ts
17918
+ var defaultWarn = (message) => log.warn(message);
17919
+ function resolveExtensionSettings(manifest, config, onWarn = defaultWarn) {
17920
+ const declarations = manifest.settings;
17921
+ if (!declarations || Object.keys(declarations).length === 0) return {};
17922
+ const overrides = readSettingsOverrides(config, manifest.pluginId, manifest.id);
17923
+ const resolved = {};
17924
+ for (const [settingId, declaration] of Object.entries(declarations)) {
17925
+ const outcome = resolveOneSetting(manifest, settingId, declaration, overrides, onWarn);
17926
+ if (outcome.hasValue) resolved[settingId] = outcome.value;
17927
+ }
17928
+ return resolved;
17929
+ }
17930
+ function resolveOneSetting(manifest, settingId, declaration, overrides, onWarn) {
17931
+ const fallback = declarationDefault(declaration);
17932
+ const toFallback = () => fallback !== void 0 ? { hasValue: true, value: fallback } : { hasValue: false };
17933
+ if (!Object.prototype.hasOwnProperty.call(overrides, settingId)) return toFallback();
17934
+ const candidate = overrides[settingId];
17935
+ const check = validateSettingValue(declaration, candidate);
17936
+ if (check.ok) return { hasValue: true, value: candidate };
17937
+ onWarn(
17938
+ `Setting '${settingId}' for extension '${manifest.pluginId}/${manifest.id}' is invalid (${check.reason}); falling back to the declared default.`
17939
+ );
17940
+ return toFallback();
17941
+ }
17942
+ function buildSettingsResolver(config, onWarn = defaultWarn) {
17943
+ return (ext) => resolveExtensionSettings(ext, config, onWarn);
17944
+ }
17945
+ function declarationDefault(declaration) {
17946
+ return "default" in declaration ? declaration.default : void 0;
17947
+ }
17948
+ function readSettingsOverrides(config, pluginId, extId) {
17949
+ const entry = config.plugins?.[pluginId];
17950
+ const ext = entry?.extensions?.[extId];
17951
+ const settings2 = ext?.settings;
17952
+ if (settings2 && typeof settings2 === "object" && !Array.isArray(settings2)) {
17953
+ return settings2;
17954
+ }
17955
+ return {};
17956
+ }
17957
+ var OK = { ok: true, reason: "" };
17958
+ function fail2(reason) {
17959
+ return { ok: false, reason };
17960
+ }
17961
+ function validateSettingValue(declaration, value) {
17962
+ switch (declaration.type) {
17963
+ case "string-list":
17964
+ return validateStringList(value, declaration.min, declaration.max, declaration.itemMaxLength);
17965
+ case "single-string":
17966
+ return validateSingleString(value, declaration.minLength, declaration.maxLength, declaration.pattern);
17967
+ case "boolean-flag":
17968
+ return typeof value === "boolean" ? OK : fail2("expected a boolean");
17969
+ case "integer":
17970
+ return validateInteger(value, declaration.min, declaration.max);
17971
+ case "number":
17972
+ return validateNumber(value, declaration.min, declaration.max);
17973
+ case "enum-pick":
17974
+ return validateEnumPick(value, declaration.options.map((o) => o.value));
17975
+ case "enum-multipick":
17976
+ return validateEnumMultipick(value, declaration.options.map((o) => o.value), declaration.min, declaration.max);
17977
+ case "path-glob":
17978
+ return validatePathGlob(value, declaration.multiple === true);
17979
+ case "regex":
17980
+ return validateRegex(value, declaration.flags);
17981
+ case "secret":
17982
+ return typeof value === "string" ? OK : fail2("expected a string");
17983
+ case "key-value-list":
17984
+ return validateKeyValueList(value, declaration.min, declaration.max);
17985
+ default: {
17986
+ const _exhaustive = declaration;
17987
+ return fail2(`unknown input-type: ${String(_exhaustive.type)}`);
17988
+ }
17989
+ }
17990
+ }
17991
+ function isStringArray(value) {
17992
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
17993
+ }
17994
+ function validateStringList(value, min, max, itemMaxLength) {
17995
+ if (!isStringArray(value)) return fail2("expected an array of strings");
17996
+ if (min !== void 0 && value.length < min) return fail2(`expected at least ${min} item(s)`);
17997
+ if (max !== void 0 && value.length > max) return fail2(`expected at most ${max} item(s)`);
17998
+ const cap = itemMaxLength ?? 256;
17999
+ if (value.some((item) => item.length > cap)) return fail2(`item exceeds ${cap} characters`);
18000
+ return OK;
18001
+ }
18002
+ function validateSingleString(value, minLength, maxLength, pattern) {
18003
+ if (typeof value !== "string") return fail2("expected a string");
18004
+ if (minLength !== void 0 && value.length < minLength) return fail2(`expected at least ${minLength} characters`);
18005
+ if (maxLength !== void 0 && value.length > maxLength) return fail2(`expected at most ${maxLength} characters`);
18006
+ return matchesPattern(value, pattern);
18007
+ }
18008
+ function matchesPattern(value, pattern) {
18009
+ if (pattern === void 0) return OK;
18010
+ let re;
18011
+ try {
18012
+ re = new RegExp(pattern);
18013
+ } catch {
18014
+ return OK;
18015
+ }
18016
+ return re.test(value) ? OK : fail2(`does not match pattern ${pattern}`);
18017
+ }
18018
+ function validateInteger(value, min, max) {
18019
+ if (typeof value !== "number" || !Number.isInteger(value)) return fail2("expected an integer");
18020
+ if (!Number.isSafeInteger(value)) return fail2("integer out of safe range");
18021
+ if (min !== void 0 && value < min) return fail2(`expected >= ${min}`);
18022
+ if (max !== void 0 && value > max) return fail2(`expected <= ${max}`);
18023
+ return OK;
18024
+ }
18025
+ function validateNumber(value, min, max) {
18026
+ if (typeof value !== "number" || !Number.isFinite(value)) return fail2("expected a finite number");
18027
+ if (min !== void 0 && value < min) return fail2(`expected >= ${min}`);
18028
+ if (max !== void 0 && value > max) return fail2(`expected <= ${max}`);
18029
+ return OK;
18030
+ }
18031
+ function validateEnumPick(value, allowed) {
18032
+ if (typeof value !== "string") return fail2("expected a string");
18033
+ if (!allowed.includes(value)) return fail2(`expected one of: ${allowed.join(", ")}`);
18034
+ return OK;
18035
+ }
18036
+ function validateEnumMultipick(value, allowed, min, max) {
18037
+ if (!isStringArray(value)) return fail2("expected an array of strings");
18038
+ const allowedSet = new Set(allowed);
18039
+ if (value.some((v) => !allowedSet.has(v))) return fail2(`every entry must be one of: ${allowed.join(", ")}`);
18040
+ if (min !== void 0 && value.length < min) return fail2(`expected at least ${min} selection(s)`);
18041
+ if (max !== void 0 && value.length > max) return fail2(`expected at most ${max} selection(s)`);
18042
+ return OK;
18043
+ }
18044
+ function validatePathGlob(value, multiple) {
18045
+ if (multiple) {
18046
+ return isStringArray(value) ? OK : fail2("expected an array of glob strings");
18047
+ }
18048
+ return typeof value === "string" ? OK : fail2("expected a glob string");
18049
+ }
18050
+ function validateRegex(value, flags) {
18051
+ if (typeof value !== "string") return fail2("expected a string");
18052
+ try {
18053
+ new RegExp(value, flags ?? "");
18054
+ } catch (err) {
18055
+ return fail2(`is not a compilable regex (${err instanceof Error ? err.message : String(err)})`);
18056
+ }
18057
+ return OK;
18058
+ }
18059
+ function validateKeyValueList(value, min, max) {
18060
+ if (!Array.isArray(value)) return fail2("expected an array of { key, value } entries");
18061
+ const wellShaped = value.every(
18062
+ (entry) => entry !== null && typeof entry === "object" && typeof entry.key === "string" && typeof entry.value === "string"
18063
+ );
18064
+ if (!wellShaped) return fail2("every entry must be { key: string, value: string }");
18065
+ if (min !== void 0 && value.length < min) return fail2(`expected at least ${min} entr(y/ies)`);
18066
+ if (max !== void 0 && value.length > max) return fail2(`expected at most ${max} entr(y/ies)`);
18067
+ return OK;
18068
+ }
18069
+
18061
18070
  // core/runtime/i18n/progress-emitter.texts.ts
18062
18071
  var PROGRESS_EMITTER_TEXTS = {
18063
18072
  /**
@@ -18558,10 +18567,10 @@ async function runScanForCommand(opts) {
18558
18567
  const dbPath = resolveDbPath({ db: void 0, ...ctx });
18559
18568
  const kernel = createKernel();
18560
18569
  const pluginRuntime = await preparePluginRuntime(opts, opts.printer);
18561
- const extensions = registerExtensions(kernel, pluginRuntime, opts);
18562
18570
  const scanInputs = loadScanInputs(opts, ctx);
18563
18571
  if ("kind" in scanInputs) return scanInputs;
18564
18572
  const { cfg, ignoreFilter, strict, effectiveRoots } = scanInputs;
18573
+ const extensions = registerExtensions(kernel, pluginRuntime, opts, cfg);
18565
18574
  let referenceablePaths;
18566
18575
  if (cfg.scan.referencePaths.length > 0) {
18567
18576
  const walk3 = walkReferencePaths(cfg.scan.referencePaths, ctx.cwd);
@@ -18657,10 +18666,11 @@ async function preparePluginRuntime(opts, printer) {
18657
18666
  pluginRuntime.emitWarnings(printer);
18658
18667
  return pluginRuntime;
18659
18668
  }
18660
- function registerExtensions(kernel, pluginRuntime, opts) {
18669
+ function registerExtensions(kernel, pluginRuntime, opts, cfg) {
18661
18670
  const composeOpts = {
18662
18671
  noBuiltIns: opts.noBuiltIns,
18663
- pluginRuntime
18672
+ pluginRuntime,
18673
+ resolveSettings: buildSettingsResolver(cfg)
18664
18674
  };
18665
18675
  if (opts.killSwitches) composeOpts.killSwitches = opts.killSwitches;
18666
18676
  if (opts.resolveEnabledOverride) composeOpts.resolveEnabled = opts.resolveEnabledOverride;
@@ -20204,7 +20214,7 @@ async function findActiveOrphanIssues(adapter, predicate) {
20204
20214
  (issue) => ORPHAN_RULE_IDS.includes(issue.analyzerId) && predicate(issue)
20205
20215
  );
20206
20216
  }
20207
- function isStringArray(v) {
20217
+ function isStringArray2(v) {
20208
20218
  return Array.isArray(v) && v.every((s) => typeof s === "string");
20209
20219
  }
20210
20220
  var OrphansCommand = class extends SmCommand {
@@ -20575,7 +20585,7 @@ var OrphansUndoRenameCommand = class extends SmCommand {
20575
20585
  return { ok: false, exitCode: ExitCode.NotFound };
20576
20586
  }
20577
20587
  const dataCandidates = issue.data ? issue.data["candidates"] : void 0;
20578
- if (!isStringArray(dataCandidates) || !dataCandidates.includes(this.from)) {
20588
+ if (!isStringArray2(dataCandidates) || !dataCandidates.includes(this.from)) {
20579
20589
  this.printer.error(
20580
20590
  tx(ORPHANS_TEXTS.undoAmbiguousNotInCandidates, { glyph: errGlyph, from: this.from })
20581
20591
  );
@@ -22729,11 +22739,12 @@ var INPUT_TYPES_CATALOG = [
22729
22739
  { id: "single-string", summary: "Single text input." },
22730
22740
  { id: "boolean-flag", summary: "On/off toggle." },
22731
22741
  { id: "integer", summary: "Integer with optional bounds." },
22742
+ { id: "number", summary: "Decimal number with optional bounds." },
22732
22743
  { id: "enum-pick", summary: "Pick one from a closed set." },
22733
22744
  { id: "enum-multipick", summary: "Pick zero or more from a closed set." },
22734
22745
  { id: "path-glob", summary: "Glob pattern (single or multiple)." },
22735
22746
  { id: "regex", summary: "ECMAScript regex pattern body." },
22736
- { id: "secret", summary: "Sensitive string (encrypted at rest)." },
22747
+ { id: "secret", summary: "Sensitive string, forced into project-local storage (gitignored), not encrypted." },
22737
22748
  { id: "key-value-list", summary: "Editable mapping of strings to strings." }
22738
22749
  ];
22739
22750
 
@@ -22806,6 +22817,375 @@ var PluginsUpgradeCommand = class extends SmCommand {
22806
22817
  }
22807
22818
  };
22808
22819
 
22820
+ // cli/commands/plugins/config.ts
22821
+ import { Command as Command29, Option as Option27 } from "clipanion";
22822
+
22823
+ // cli/i18n/plugins-config.texts.ts
22824
+ var PLUGINS_CONFIG_TEXTS = {
22825
+ // --- id-shape redirects ----------------------------------------------
22826
+ // `sm plugins config` operates on one extension. A bare plugin id is
22827
+ // the wrong granularity, redirect to `sm plugins list <id>`.
22828
+ bareId: '{{glyph}} `sm plugins config` needs a qualified `<plugin>/<ext>` id; "{{id}}" is a plugin.\n {{hint}}\n',
22829
+ bareIdHint: "Run `sm plugins list {{id}}` to see the extensions, then `sm plugins config {{id}}/<ext>`.",
22830
+ // --- no declared settings --------------------------------------------
22831
+ noSettings: '{{glyph}} Extension "{{id}}" declares no configurable settings.\n {{hint}}\n',
22832
+ noSettingsHint: "Run `sm plugins show {{id}}` to inspect the extension.",
22833
+ unknownSetting: '{{glyph}} Unknown setting "{{settingId}}" for extension "{{id}}".\n {{hint}}\n',
22834
+ unknownSettingHint: "Declared settings: {{declared}}.",
22835
+ // --- coercion / validation -------------------------------------------
22836
+ coerceFailed: '{{glyph}} Could not parse "{{value}}" as type {{type}} for setting "{{settingId}}".\n {{hint}}\n',
22837
+ coerceFailedHint: "{{detail}}",
22838
+ validationFailed: '{{glyph}} Invalid value for setting "{{settingId}}" ({{type}}): {{reason}}.\n',
22839
+ writeFailed: '{{glyph}} Failed to write setting "{{settingId}}": {{message}}\n',
22840
+ // --- table view (no settingId) ---------------------------------------
22841
+ /** Section header above the settings table. */
22842
+ tableHeader: " Settings for {{id}}\n",
22843
+ /** One table row: setting id, effective value, source layer tag. */
22844
+ tableRow: " {{settingId}} {{value}}{{sourceTag}}\n",
22845
+ /** Dim suffix showing which layer set the effective value. */
22846
+ tableSourceTag: " [{{source}}]",
22847
+ /** Redaction placeholder for `secret`-typed values in any output. */
22848
+ redacted: "<redacted>",
22849
+ // --- write / reset receipts ------------------------------------------
22850
+ setWritten: "{{glyph}} Set {{settingId}} = {{value}} for {{id}}{{wroteTag}}\n",
22851
+ setWroteTag: " (wrote {{path}})",
22852
+ resetRemoved: "{{glyph}} Cleared {{settingId}} for {{id}}; falls back to the declared default{{wroteTag}}\n",
22853
+ resetNoOverride: "{{glyph}} No override set for {{settingId}} on {{id}}; nothing to clear.\n",
22854
+ // --- re-scan footer ---------------------------------------------------
22855
+ rescanFooter: "{{hint}}\n",
22856
+ rescanFooterText: "Settings are read once per scan; run `sm scan` to apply."
22857
+ };
22858
+
22859
+ // cli/commands/plugins/config.ts
22860
+ var PluginsConfigCommand = class extends SmCommand {
22861
+ static paths = [["plugins", "config"]];
22862
+ static usage = Command29.Usage({
22863
+ category: "Plugins",
22864
+ description: "Read or write an extension's declared settings.",
22865
+ details: `
22866
+ Operates on a single extension by its qualified \`<plugin>/<ext>\`
22867
+ id. With no settingId it prints a table of each declared setting,
22868
+ its effective value, and the config layer that set it. With a
22869
+ settingId + value it coerces the shell string to the declared
22870
+ input-type, validates it, and writes
22871
+ \`plugins.<plugin>.extensions.<ext>.settings.<settingId>\` to
22872
+ settings.json (or settings.local.json for \`secret\` settings).
22873
+ \`--reset\` removes the override so the manifest default applies.
22874
+ Secret values are shown as <redacted>. Run \`sm scan\` to apply.
22875
+ `
22876
+ });
22877
+ id = Option27.String({ required: true });
22878
+ settingId = Option27.String({ required: false });
22879
+ value = Option27.String({ required: false });
22880
+ reset = Option27.Boolean("--reset", false, {
22881
+ description: "Remove the override for <settingId> so the manifest default applies."
22882
+ });
22883
+ pluginDir = Option27.String("--plugin-dir", { required: false });
22884
+ // Read-only when listing; the write / reset paths emit their own
22885
+ // receipt. `sm config` exempts the config family from "done in <…>";
22886
+ // mirror that here for the read path. The write path keeps the line.
22887
+ emitElapsed = true;
22888
+ // CLI orchestrator: each branch is one validation gate (bare id /
22889
+ // unknown extension / no settings / unknown setting) or a mode
22890
+ // dispatch (table vs set vs reset). Splitting per branch scatters the
22891
+ // gate from the value it gates.
22892
+ // eslint-disable-next-line complexity
22893
+ async run() {
22894
+ const ctx = defaultRuntimeContext();
22895
+ const stderrAnsi = this.ansiFor("stderr");
22896
+ if (!this.id.includes("/")) {
22897
+ this.printer.error(
22898
+ tx(PLUGINS_CONFIG_TEXTS.bareId, {
22899
+ glyph: stderrAnsi.red("\u2715"),
22900
+ id: sanitizeForTerminal(this.id),
22901
+ hint: stderrAnsi.dim(
22902
+ tx(PLUGINS_CONFIG_TEXTS.bareIdHint, { id: sanitizeForTerminal(this.id) })
22903
+ )
22904
+ })
22905
+ );
22906
+ return ExitCode.Error;
22907
+ }
22908
+ const plugins = await loadAll({ pluginDir: this.pluginDir });
22909
+ const parsed = parseQualifiedExtensionId(this.id, pluginCatalogue(plugins));
22910
+ if (!parsed.ok) {
22911
+ this.printer.error(renderQualifiedIdError(parsed, this.id, stderrAnsi));
22912
+ return ExitCode.NotFound;
22913
+ }
22914
+ const { pluginId, extId } = parsed;
22915
+ const declarations = resolveDeclaredSettings(pluginId, extId, plugins);
22916
+ if (!declarations || Object.keys(declarations).length === 0) {
22917
+ this.printer.error(
22918
+ tx(PLUGINS_CONFIG_TEXTS.noSettings, {
22919
+ glyph: stderrAnsi.red("\u2715"),
22920
+ id: sanitizeForTerminal(this.id),
22921
+ hint: stderrAnsi.dim(
22922
+ tx(PLUGINS_CONFIG_TEXTS.noSettingsHint, { id: sanitizeForTerminal(this.id) })
22923
+ )
22924
+ })
22925
+ );
22926
+ return ExitCode.NotFound;
22927
+ }
22928
+ if (this.settingId === void 0) {
22929
+ return this.renderTable(pluginId, extId, declarations, ctx.cwd);
22930
+ }
22931
+ const declaration = declarations[this.settingId];
22932
+ if (!declaration) {
22933
+ this.printer.error(
22934
+ tx(PLUGINS_CONFIG_TEXTS.unknownSetting, {
22935
+ glyph: stderrAnsi.red("\u2715"),
22936
+ settingId: sanitizeForTerminal(this.settingId),
22937
+ id: sanitizeForTerminal(this.id),
22938
+ hint: stderrAnsi.dim(
22939
+ tx(PLUGINS_CONFIG_TEXTS.unknownSettingHint, {
22940
+ declared: Object.keys(declarations).map((k) => `'${k}'`).join(", ")
22941
+ })
22942
+ )
22943
+ })
22944
+ );
22945
+ return ExitCode.NotFound;
22946
+ }
22947
+ if (this.reset) {
22948
+ return this.resetSetting(pluginId, extId, this.settingId, declaration, ctx.cwd);
22949
+ }
22950
+ if (this.value === void 0) {
22951
+ return this.renderTable(pluginId, extId, { [this.settingId]: declaration }, ctx.cwd);
22952
+ }
22953
+ return this.writeSetting(pluginId, extId, this.settingId, declaration, this.value, ctx.cwd);
22954
+ }
22955
+ /**
22956
+ * Render the settings table (human) or the resolved set (`--json`).
22957
+ * Effective values come from the kernel settings resolver so the table
22958
+ * shows exactly what `ctx.settings.<id>` would see at scan time
22959
+ * (default overlaid by the config override, validated). Secret values
22960
+ * are redacted.
22961
+ */
22962
+ renderTable(pluginId, extId, declarations, cwd) {
22963
+ const { effective } = loadConfig({ cwd });
22964
+ const resolved = resolveExtensionSettings({ pluginId, id: extId, settings: declarations }, effective, () => {
22965
+ });
22966
+ if (this.json) {
22967
+ const payload = {};
22968
+ for (const [settingId, declaration] of Object.entries(declarations)) {
22969
+ payload[settingId] = declaration.type === "secret" ? PLUGINS_CONFIG_TEXTS.redacted : resolved[settingId] ?? null;
22970
+ }
22971
+ this.printer.data(JSON.stringify(payload) + "\n");
22972
+ return ExitCode.Ok;
22973
+ }
22974
+ const ansi = this.ansiFor("stdout");
22975
+ const lines = [tx(PLUGINS_CONFIG_TEXTS.tableHeader, { id: `${pluginId}/${extId}` })];
22976
+ const idWidth = Math.max(...Object.keys(declarations).map((k) => k.length));
22977
+ for (const [settingId, declaration] of Object.entries(declarations)) {
22978
+ const display = declaration.type === "secret" ? PLUGINS_CONFIG_TEXTS.redacted : formatValue2(resolved[settingId]);
22979
+ const dotKey = settingDotKey(pluginId, extId, settingId);
22980
+ const source = getValueSource(dotKey, { cwd });
22981
+ const sourceTag = source ? ansi.dim(tx(PLUGINS_CONFIG_TEXTS.tableSourceTag, { source: layerLabel(source) })) : "";
22982
+ lines.push(
22983
+ tx(PLUGINS_CONFIG_TEXTS.tableRow, {
22984
+ settingId: settingId.padEnd(idWidth),
22985
+ value: sanitizeForTerminal(display),
22986
+ sourceTag
22987
+ })
22988
+ );
22989
+ }
22990
+ this.printer.data(lines.join(""));
22991
+ return ExitCode.Ok;
22992
+ }
22993
+ writeSetting(pluginId, extId, settingId, declaration, raw, cwd) {
22994
+ const stderrAnsi = this.ansiFor("stderr");
22995
+ const errGlyph = stderrAnsi.red("\u2715");
22996
+ const coerced = coerceCliValue(declaration, raw);
22997
+ if (!coerced.ok) {
22998
+ this.printer.error(
22999
+ tx(PLUGINS_CONFIG_TEXTS.coerceFailed, {
23000
+ glyph: errGlyph,
23001
+ value: sanitizeForTerminal(raw),
23002
+ type: declaration.type,
23003
+ settingId: sanitizeForTerminal(settingId),
23004
+ hint: stderrAnsi.dim(tx(PLUGINS_CONFIG_TEXTS.coerceFailedHint, { detail: coerced.reason }))
23005
+ })
23006
+ );
23007
+ return ExitCode.Error;
23008
+ }
23009
+ let invalidReason = null;
23010
+ resolveExtensionSettings(
23011
+ { pluginId, id: extId, settings: { [settingId]: declaration } },
23012
+ { plugins: { [pluginId]: { extensions: { [extId]: { settings: { [settingId]: coerced.value } } } } } },
23013
+ (message) => {
23014
+ invalidReason = message;
23015
+ }
23016
+ );
23017
+ if (invalidReason !== null) {
23018
+ this.printer.error(
23019
+ tx(PLUGINS_CONFIG_TEXTS.validationFailed, {
23020
+ glyph: errGlyph,
23021
+ settingId: sanitizeForTerminal(settingId),
23022
+ type: declaration.type,
23023
+ reason: invalidReason
23024
+ })
23025
+ );
23026
+ return ExitCode.Error;
23027
+ }
23028
+ const target = declaration.type === "secret" ? "project-local" : "project";
23029
+ const dotKey = settingDotKey(pluginId, extId, settingId);
23030
+ try {
23031
+ writeConfigValue(dotKey, coerced.value, { target, cwd });
23032
+ } catch (err) {
23033
+ return this.renderWriteError(err, settingId);
23034
+ }
23035
+ const ansi = this.ansiFor("stdout");
23036
+ const display = declaration.type === "secret" ? PLUGINS_CONFIG_TEXTS.redacted : formatValue2(coerced.value);
23037
+ const path = target === "project-local" ? defaultLocalSettingsPath(cwd) : defaultSettingsPath(cwd);
23038
+ this.printer.data(
23039
+ tx(PLUGINS_CONFIG_TEXTS.setWritten, {
23040
+ glyph: ansi.green("\u2713"),
23041
+ settingId,
23042
+ value: sanitizeForTerminal(display),
23043
+ id: `${pluginId}/${extId}`,
23044
+ wroteTag: ansi.dim(tx(PLUGINS_CONFIG_TEXTS.setWroteTag, { path: relativeIfBelow(path, cwd) }))
23045
+ })
23046
+ );
23047
+ this.printRescanFooter();
23048
+ return ExitCode.Ok;
23049
+ }
23050
+ resetSetting(pluginId, extId, settingId, declaration, cwd) {
23051
+ const target = declaration.type === "secret" ? "project-local" : "project";
23052
+ const dotKey = settingDotKey(pluginId, extId, settingId);
23053
+ const ansi = this.ansiFor("stdout");
23054
+ let removed;
23055
+ try {
23056
+ removed = removeConfigValue(dotKey, { target, cwd });
23057
+ } catch (err) {
23058
+ return this.renderWriteError(err, settingId);
23059
+ }
23060
+ if (!removed) {
23061
+ this.printer.data(
23062
+ tx(PLUGINS_CONFIG_TEXTS.resetNoOverride, {
23063
+ glyph: ansi.green("\u2713"),
23064
+ settingId,
23065
+ id: `${pluginId}/${extId}`
23066
+ })
23067
+ );
23068
+ return ExitCode.Ok;
23069
+ }
23070
+ const path = target === "project-local" ? defaultLocalSettingsPath(cwd) : defaultSettingsPath(cwd);
23071
+ this.printer.data(
23072
+ tx(PLUGINS_CONFIG_TEXTS.resetRemoved, {
23073
+ glyph: ansi.green("\u2713"),
23074
+ settingId,
23075
+ id: `${pluginId}/${extId}`,
23076
+ wroteTag: ansi.dim(tx(PLUGINS_CONFIG_TEXTS.setWroteTag, { path: relativeIfBelow(path, cwd) }))
23077
+ })
23078
+ );
23079
+ this.printRescanFooter();
23080
+ return ExitCode.Ok;
23081
+ }
23082
+ renderWriteError(err, settingId) {
23083
+ const ansi = this.ansiFor("stderr");
23084
+ const glyph = ansi.red("\u2715");
23085
+ if (err instanceof ForbiddenSegmentError) {
23086
+ this.printer.error(
23087
+ tx(PLUGINS_CONFIG_TEXTS.writeFailed, { glyph, settingId, message: err.message })
23088
+ );
23089
+ return ExitCode.Error;
23090
+ }
23091
+ if (err instanceof ProjectLocalOnlyKeyError) {
23092
+ this.printer.error(
23093
+ tx(PLUGINS_CONFIG_TEXTS.writeFailed, { glyph, settingId, message: err.message })
23094
+ );
23095
+ return ExitCode.Error;
23096
+ }
23097
+ if (err instanceof ConfigValidationError) {
23098
+ this.printer.error(
23099
+ tx(PLUGINS_CONFIG_TEXTS.writeFailed, { glyph, settingId, message: err.errors })
23100
+ );
23101
+ return ExitCode.Error;
23102
+ }
23103
+ throw err;
23104
+ }
23105
+ printRescanFooter() {
23106
+ if (this.json) return;
23107
+ const ansi = this.ansiFor("stdout");
23108
+ this.printer.data(
23109
+ tx(PLUGINS_CONFIG_TEXTS.rescanFooter, {
23110
+ hint: ansi.dim(PLUGINS_CONFIG_TEXTS.rescanFooterText)
23111
+ })
23112
+ );
23113
+ }
23114
+ };
23115
+ function resolveDeclaredSettings(pluginId, extId, plugins) {
23116
+ for (const plugin of builtInPlugins) {
23117
+ if (plugin.id !== pluginId) continue;
23118
+ for (const ext of plugin.extensions) {
23119
+ if (ext.id === extId) return ext.settings;
23120
+ }
23121
+ }
23122
+ const match = plugins.find((p) => p.id === pluginId);
23123
+ const userExt = match?.extensions?.find((e) => e.id === extId);
23124
+ return readInstanceSettings(userExt?.instance);
23125
+ }
23126
+ function readInstanceSettings(instance) {
23127
+ if (instance === null || typeof instance !== "object") return void 0;
23128
+ const settings2 = instance.settings;
23129
+ if (settings2 && typeof settings2 === "object" && !Array.isArray(settings2)) {
23130
+ return settings2;
23131
+ }
23132
+ return void 0;
23133
+ }
23134
+ function coerceCliValue(declaration, raw) {
23135
+ switch (declaration.type) {
23136
+ case "integer":
23137
+ case "number": {
23138
+ const n = Number(raw);
23139
+ if (raw.trim() === "" || Number.isNaN(n)) return { ok: false, reason: "expected a numeric value" };
23140
+ return { ok: true, value: n };
23141
+ }
23142
+ case "boolean-flag": {
23143
+ if (raw === "true") return { ok: true, value: true };
23144
+ if (raw === "false") return { ok: true, value: false };
23145
+ return { ok: false, reason: "expected `true` or `false`" };
23146
+ }
23147
+ case "string-list":
23148
+ case "enum-multipick":
23149
+ case "key-value-list":
23150
+ return parseJsonValue(raw);
23151
+ case "path-glob":
23152
+ return declaration.multiple === true ? parseJsonValue(raw) : { ok: true, value: raw };
23153
+ case "single-string":
23154
+ case "enum-pick":
23155
+ case "regex":
23156
+ case "secret":
23157
+ return { ok: true, value: raw };
23158
+ default: {
23159
+ const _exhaustive = declaration;
23160
+ return { ok: false, reason: `unknown input-type: ${String(_exhaustive.type)}` };
23161
+ }
23162
+ }
23163
+ }
23164
+ function parseJsonValue(raw) {
23165
+ try {
23166
+ return { ok: true, value: JSON.parse(raw) };
23167
+ } catch {
23168
+ return { ok: false, reason: 'expected a JSON value (e.g. ["a","b"])' };
23169
+ }
23170
+ }
23171
+ function settingDotKey(pluginId, extId, settingId) {
23172
+ return `plugins.${pluginId}.extensions.${extId}.settings.${settingId}`;
23173
+ }
23174
+ function formatValue2(value) {
23175
+ if (value === void 0 || value === null) return "null";
23176
+ if (Array.isArray(value) || typeof value === "object") return JSON.stringify(value);
23177
+ return String(value);
23178
+ }
23179
+ var LAYER_LABEL = {
23180
+ defaults: "default",
23181
+ project: "settings.json",
23182
+ "project-local": "settings.local.json",
23183
+ override: "override"
23184
+ };
23185
+ function layerLabel(layer) {
23186
+ return LAYER_LABEL[layer];
23187
+ }
23188
+
22809
23189
  // cli/commands/plugins.ts
22810
23190
  var PLUGIN_COMMANDS = [
22811
23191
  PluginsListCommand,
@@ -22815,13 +23195,14 @@ var PLUGIN_COMMANDS = [
22815
23195
  PluginsDisableCommand,
22816
23196
  PluginsCreateCommand,
22817
23197
  PluginsSlotsListCommand,
22818
- PluginsUpgradeCommand
23198
+ PluginsUpgradeCommand,
23199
+ PluginsConfigCommand
22819
23200
  ];
22820
23201
 
22821
23202
  // cli/commands/refresh.ts
22822
23203
  import { readFile as readFile4 } from "fs/promises";
22823
23204
  import { resolve as resolve34 } from "path";
22824
- import { Command as Command29, Option as Option27 } from "clipanion";
23205
+ import { Command as Command30, Option as Option28 } from "clipanion";
22825
23206
 
22826
23207
  // cli/i18n/refresh.texts.ts
22827
23208
  var REFRESH_TEXTS = {
@@ -22877,7 +23258,7 @@ var REFRESH_TEXTS = {
22877
23258
  // cli/commands/refresh.ts
22878
23259
  var RefreshCommand = class extends SmCommand {
22879
23260
  static paths = [["refresh"]];
22880
- static usage = Command29.Usage({
23261
+ static usage = Command30.Usage({
22881
23262
  category: "Scan",
22882
23263
  description: "Refresh enrichment rows: granular (single node) or batch (every stale row).",
22883
23264
  details: `
@@ -22899,11 +23280,11 @@ var RefreshCommand = class extends SmCommand {
22899
23280
  ["Refresh every node with stale enrichments", "$0 refresh --stale"]
22900
23281
  ]
22901
23282
  });
22902
- nodePath = Option27.String({ name: "node", required: false });
22903
- stale = Option27.Boolean("--stale", false, {
23283
+ nodePath = Option28.String({ name: "node", required: false });
23284
+ stale = Option28.Boolean("--stale", false, {
22904
23285
  description: "Refresh every node carrying a stale enrichment row (no-op in this revision; reserved for future Action-prob enrichments)."
22905
23286
  });
22906
- noPlugins = Option27.Boolean("--no-plugins", false, {
23287
+ noPlugins = Option28.Boolean("--no-plugins", false, {
22907
23288
  description: "Skip drop-in plugin discovery; use only the built-in extractor set."
22908
23289
  });
22909
23290
  // The remaining cyclomatic count comes from CLI ergonomics that don't
@@ -22937,9 +23318,11 @@ var RefreshCommand = class extends SmCommand {
22937
23318
  const pluginRuntime = this.noPlugins ? emptyPluginRuntime() : await loadPluginRuntime();
22938
23319
  pluginRuntime.emitWarnings(this.printer);
22939
23320
  listBuiltIns();
23321
+ const refreshCfg = loadConfig({ cwd: ctx.cwd }).effective;
22940
23322
  const composed = composeScanExtensions({
22941
23323
  noBuiltIns: false,
22942
23324
  pluginRuntime,
23325
+ resolveSettings: buildSettingsResolver(refreshCfg),
22943
23326
  killSwitches: readConformanceKillSwitches()
22944
23327
  });
22945
23328
  const allExtractors = composed?.extractors ?? [];
@@ -23205,7 +23588,7 @@ var IntentionalFailCommand = class extends SmCommand {
23205
23588
  };
23206
23589
 
23207
23590
  // cli/commands/scan.ts
23208
- import { Command as Command31, Option as Option29 } from "clipanion";
23591
+ import { Command as Command32, Option as Option30 } from "clipanion";
23209
23592
 
23210
23593
  // kernel/util/format-bytes.ts
23211
23594
  var UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
@@ -23357,7 +23740,7 @@ var SCAN_TEXTS = {
23357
23740
  };
23358
23741
 
23359
23742
  // cli/commands/watch.ts
23360
- import { Command as Command30, Option as Option28 } from "clipanion";
23743
+ import { Command as Command31, Option as Option29 } from "clipanion";
23361
23744
 
23362
23745
  // core/watcher/runtime.ts
23363
23746
  import { dirname as dirname18 } from "path";
@@ -23479,7 +23862,8 @@ function createWatcherRuntime(opts) {
23479
23862
  const composeOpts = {
23480
23863
  noBuiltIns: opts.noBuiltIns,
23481
23864
  pluginRuntime,
23482
- resolveEnabled: resolveEnabledOverride
23865
+ resolveEnabled: resolveEnabledOverride,
23866
+ resolveSettings: buildSettingsResolver(cfg)
23483
23867
  };
23484
23868
  if (opts.killSwitches) composeOpts.killSwitches = opts.killSwitches;
23485
23869
  const composed = composeScanExtensions(composeOpts);
@@ -23896,7 +24280,7 @@ async function runWatchLoop(opts) {
23896
24280
  }
23897
24281
  var WatchCommand = class extends SmCommand {
23898
24282
  static paths = [["watch"]];
23899
- static usage = Command30.Usage({
24283
+ static usage = Command31.Usage({
23900
24284
  category: "Scan",
23901
24285
  description: "Watch roots and run an incremental scan after each debounced batch of filesystem events.",
23902
24286
  details: `
@@ -23920,21 +24304,21 @@ var WatchCommand = class extends SmCommand {
23920
24304
  ["Stream ScanResult per batch as ndjson", "$0 watch --json"]
23921
24305
  ]
23922
24306
  });
23923
- roots = Option28.Rest({ name: "roots" });
23924
- noTokens = Option28.Boolean("--no-tokens", false, {
24307
+ roots = Option29.Rest({ name: "roots" });
24308
+ noTokens = Option29.Boolean("--no-tokens", false, {
23925
24309
  description: "Skip per-node token counts (cl100k_base BPE)."
23926
24310
  });
23927
- strict = Option28.Boolean("--strict", false, {
24311
+ strict = Option29.Boolean("--strict", false, {
23928
24312
  description: "Promote frontmatter-validation findings from warn to error inside each batch. Does not change the watcher exit code."
23929
24313
  });
23930
- noPlugins = Option28.Boolean("--no-plugins", false, {
24314
+ noPlugins = Option29.Boolean("--no-plugins", false, {
23931
24315
  description: "Skip drop-in plugin discovery for the watcher session."
23932
24316
  });
23933
- maxConsecutiveFailures = Option28.String("--max-consecutive-failures", {
24317
+ maxConsecutiveFailures = Option29.String("--max-consecutive-failures", {
23934
24318
  required: false,
23935
24319
  description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
23936
24320
  });
23937
- maxNodes = Option28.String("--max-nodes", {
24321
+ maxNodes = Option29.String("--max-nodes", {
23938
24322
  required: false,
23939
24323
  description: "Per-batch override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When a batch hits the cap, additional files are dropped and the UI surfaces the persistent oversized banner. Validation: integer >= 1."
23940
24324
  });
@@ -24001,7 +24385,7 @@ function parseMaxNodesLimit(raw, stderr, noColor) {
24001
24385
  // cli/commands/scan.ts
24002
24386
  var ScanCommand = class extends SmCommand {
24003
24387
  static paths = [["scan"]];
24004
- static usage = Command31.Usage({
24388
+ static usage = Command32.Usage({
24005
24389
  category: "Scan",
24006
24390
  description: "Scan roots for markdown nodes, run extractors and analyzers.",
24007
24391
  details: `
@@ -24036,35 +24420,35 @@ var ScanCommand = class extends SmCommand {
24036
24420
  ["What would the next incremental scan persist?", "$0 scan --changed -n --json"]
24037
24421
  ]
24038
24422
  });
24039
- roots = Option29.Rest({ name: "roots" });
24040
- noBuiltIns = Option29.Boolean("--no-built-ins", false, {
24423
+ roots = Option30.Rest({ name: "roots" });
24424
+ noBuiltIns = Option30.Boolean("--no-built-ins", false, {
24041
24425
  description: "Skip the built-in extension set. Yields a zero-filled ScanResult (kernel-empty-boot parity); skips DB persistence."
24042
24426
  });
24043
- noPlugins = Option29.Boolean("--no-plugins", false, {
24427
+ noPlugins = Option30.Boolean("--no-plugins", false, {
24044
24428
  description: "Skip drop-in plugin discovery. Only the built-in set runs. Combine with --no-built-ins for a fully empty pipeline."
24045
24429
  });
24046
- noTokens = Option29.Boolean("--no-tokens", false, {
24430
+ noTokens = Option30.Boolean("--no-tokens", false, {
24047
24431
  description: "Skip per-node token counts (cl100k_base BPE). Leaves node.tokens undefined; spec-valid since the field is optional."
24048
24432
  });
24049
- dryRun = Option29.Boolean("-n,--dry-run", false, {
24433
+ dryRun = Option30.Boolean("-n,--dry-run", false, {
24050
24434
  description: "Run the scan in memory and skip every DB write. Combined with --changed, still opens the DB read-side to load the prior snapshot."
24051
24435
  });
24052
- changed = Option29.Boolean("--changed", false, {
24436
+ changed = Option30.Boolean("--changed", false, {
24053
24437
  description: "Incremental scan: reuse unchanged nodes from the persisted prior snapshot. Degrades to a full scan if no prior snapshot exists."
24054
24438
  });
24055
- allowEmpty = Option29.Boolean("--allow-empty", false, {
24439
+ allowEmpty = Option30.Boolean("--allow-empty", false, {
24056
24440
  description: "Allow a zero-result scan to wipe an already-populated DB (replace-all replace by zero rows). Off by default to avoid the typo-trap where an invalid root silently clears your data."
24057
24441
  });
24058
- strict = Option29.Boolean("--strict", false, {
24442
+ strict = Option30.Boolean("--strict", false, {
24059
24443
  description: "Promote frontmatter-validation findings from warn to error (exit code 1 on any violation). Overrides scan.strict from config when both are set."
24060
24444
  });
24061
- watch = Option29.Boolean("--watch", false, {
24445
+ watch = Option30.Boolean("--watch", false, {
24062
24446
  description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
24063
24447
  });
24064
- yes = Option29.Boolean("--yes", false, {
24448
+ yes = Option30.Boolean("--yes", false, {
24065
24449
  description: "Non-interactive mode. For ambiguous activeProvider auto-detect, multiple provider markers (.claude/, .codex/, AGENTS.md, .cursor/) under the scan tree exit non-zero instead of prompting; set the lens manually via `sm config set activeProvider <id>` and re-run. Also auto-confirms the pre-1.0 schema-drift rebuild (when the DB was written by a different skill-map major.minor it is deleted and regenerated) instead of prompting."
24066
24450
  });
24067
- maxNodes = Option29.String("--max-nodes", {
24451
+ maxNodes = Option30.String("--max-nodes", {
24068
24452
  required: false,
24069
24453
  description: "Per-invocation override of `scan.maxNodes` (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. When the walker hits the cap, additional files are dropped and the scan is marked oversized in scan_meta (the UI raises a persistent banner pointing at the .skillmapignore editor in Settings \u2192 Project). Validation: integer >= 1."
24070
24454
  });
@@ -24405,10 +24789,10 @@ function countNoun(count3, singular, plural) {
24405
24789
 
24406
24790
  // cli/commands/scan-compare.ts
24407
24791
  import { access, readFile as readFile5 } from "fs/promises";
24408
- import { Command as Command32, Option as Option30 } from "clipanion";
24792
+ import { Command as Command33, Option as Option31 } from "clipanion";
24409
24793
  var ScanCompareCommand = class extends SmCommand {
24410
24794
  static paths = [["scan", "compare-with"]];
24411
- static usage = Command32.Usage({
24795
+ static usage = Command33.Usage({
24412
24796
  category: "Scan",
24413
24797
  description: "Run a fresh scan in memory and emit a delta against the saved ScanResult dump at <dump>. Read-only.",
24414
24798
  details: `
@@ -24436,15 +24820,15 @@ var ScanCompareCommand = class extends SmCommand {
24436
24820
  ["JSON output for tooling", "$0 scan compare-with baseline.json --json"]
24437
24821
  ]
24438
24822
  });
24439
- dump = Option30.String({ required: true });
24440
- roots = Option30.Rest({ name: "roots" });
24441
- noTokens = Option30.Boolean("--no-tokens", false, {
24823
+ dump = Option31.String({ required: true });
24824
+ roots = Option31.Rest({ name: "roots" });
24825
+ noTokens = Option31.Boolean("--no-tokens", false, {
24442
24826
  description: "Skip per-node token counts during the fresh scan."
24443
24827
  });
24444
- strict = Option30.Boolean("--strict", false, {
24828
+ strict = Option31.Boolean("--strict", false, {
24445
24829
  description: "Promote layered-config warnings and frontmatter-validation findings from warn to error."
24446
24830
  });
24447
- noPlugins = Option30.Boolean("--no-plugins", false, {
24831
+ noPlugins = Option31.Boolean("--no-plugins", false, {
24448
24832
  description: "Skip drop-in plugin discovery."
24449
24833
  });
24450
24834
  // Cyclomatic count comes from CLI ergonomics: 3 distinct try/catch
@@ -24486,6 +24870,7 @@ var ScanCompareCommand = class extends SmCommand {
24486
24870
  const composedExtensions = composeScanExtensions({
24487
24871
  noBuiltIns: false,
24488
24872
  pluginRuntime,
24873
+ resolveSettings: buildSettingsResolver(cfg),
24489
24874
  killSwitches: readConformanceKillSwitches()
24490
24875
  });
24491
24876
  let current;
@@ -24649,7 +25034,7 @@ function renderDeltaIssues(issues) {
24649
25034
  // cli/commands/serve.ts
24650
25035
  import { spawn as spawn2 } from "child_process";
24651
25036
  import { existsSync as existsSync31 } from "fs";
24652
- import { Command as Command33, Option as Option31 } from "clipanion";
25037
+ import { Command as Command34, Option as Option32 } from "clipanion";
24653
25038
 
24654
25039
  // kernel/util/dev-mode.ts
24655
25040
  import { sep as sep6 } from "path";
@@ -24910,8 +25295,22 @@ var SERVER_TEXTS = {
24910
25295
  // The single-id variants above still apply for per-entry validation
24911
25296
  // (unknown id, granularity mismatch, lock); these cover the
24912
25297
  // body-shape level.
24913
- pluginsChangesRequired: "Request body must include a `changes` array of `{ id, enabled }` entries.",
24914
- pluginsChangeMalformed: "Each entry in `changes` must have a string `id` and a boolean `enabled`.",
25298
+ pluginsChangesRequired: "Request body must include a `changes` array of `{ id, enabled?, settings? }` entries.",
25299
+ pluginsChangeMalformed: "Each entry in `changes` must have a string `id` plus at least one of `enabled` (boolean) or `settings` (object).",
25300
+ // 400, a bulk change carries `settings` against a bare plugin id.
25301
+ // Settings are per-extension, so they require a qualified
25302
+ // `<plugin>/<ext>` id; a bare plugin id is the wrong granularity.
25303
+ pluginsSettingsRequireQualifiedId: 'Settings can only be written on a qualified `<plugin>/<ext>` id, not the bare plugin id "{{id}}".',
25304
+ // 400, a bulk change carries `settings` for an extension that declares
25305
+ // none in its manifest.
25306
+ pluginsSettingsNoneDeclared: 'Extension "{{pluginId}}/{{extensionId}}" declares no configurable settings.',
25307
+ // 400, a `settings` entry names a settingId the extension does not
25308
+ // declare, or its value fails the declared input-type's rules. The
25309
+ // `{{reason}}` is the resolver's per-type validation message.
25310
+ pluginsSettingsInvalid: 'Invalid setting "{{settingId}}" for "{{pluginId}}/{{extensionId}}": {{reason}}.',
25311
+ // 500, a settings write failed at persist time (AJV revalidation of
25312
+ // the merged config file rejected the result).
25313
+ pluginsSettingsPersistFailed: 'Failed to persist settings for "{{id}}": {{message}}.',
24915
25314
  // ---- preferences route (routes/preferences.ts) --------------------------
24916
25315
  //
24917
25316
  // GET / PATCH /api/preferences. The PATCH body is shaped
@@ -25425,13 +25824,15 @@ function registerGraphRoute(app, deps) {
25425
25824
  }
25426
25825
  function renderGraphPayload(formatter, loaded) {
25427
25826
  const scan = loaded ?? { nodes: [], links: [], issues: [] };
25827
+ const settings2 = formatter.resolvedSettings ?? {};
25428
25828
  if (loaded === null) {
25429
- return formatter.format({ nodes: scan.nodes, links: scan.links, issues: scan.issues });
25829
+ return formatter.format({ nodes: scan.nodes, links: scan.links, issues: scan.issues, settings: settings2 });
25430
25830
  }
25431
25831
  return formatter.format({
25432
25832
  nodes: scan.nodes,
25433
25833
  links: scan.links,
25434
25834
  issues: scan.issues,
25835
+ settings: settings2,
25435
25836
  scanResult: loaded
25436
25837
  });
25437
25838
  }
@@ -25891,6 +26292,72 @@ function normalizeArrayIndices(path) {
25891
26292
  return path.replace(/\/\d+(?=\/|$)/g, "/*");
25892
26293
  }
25893
26294
 
26295
+ // server/routes/plugins-settings.ts
26296
+ function readManifestSettings(manifestLike) {
26297
+ if (manifestLike === null || typeof manifestLike !== "object") return void 0;
26298
+ const settings2 = manifestLike.settings;
26299
+ if (settings2 && typeof settings2 === "object" && !Array.isArray(settings2)) {
26300
+ return settings2;
26301
+ }
26302
+ return void 0;
26303
+ }
26304
+ function projectExtensionSettings(pluginId, extId, declarations, config) {
26305
+ if (!declarations || Object.keys(declarations).length === 0) return {};
26306
+ const settings2 = Object.entries(declarations).map(
26307
+ ([id, declaration]) => ({ ...declaration, id })
26308
+ );
26309
+ const ref = { pluginId, id: extId, settings: declarations };
26310
+ const resolved = resolveExtensionSettings(ref, config, () => {
26311
+ });
26312
+ const settingValues = {};
26313
+ const secretSet = [];
26314
+ for (const [id, declaration] of Object.entries(declarations)) {
26315
+ if (declaration.type === "secret") {
26316
+ if (Object.prototype.hasOwnProperty.call(resolved, id)) secretSet.push(id);
26317
+ continue;
26318
+ }
26319
+ if (Object.prototype.hasOwnProperty.call(resolved, id)) {
26320
+ settingValues[id] = resolved[id];
26321
+ }
26322
+ }
26323
+ return {
26324
+ settings: settings2,
26325
+ settingValues,
26326
+ ...secretSet.length > 0 ? { secretSettingsSet: secretSet } : {}
26327
+ };
26328
+ }
26329
+ function validateSettingsPatch(pluginId, extId, declarations, patch) {
26330
+ for (const [settingId, value] of Object.entries(patch)) {
26331
+ const declaration = declarations?.[settingId];
26332
+ if (!declaration) {
26333
+ return {
26334
+ settingId,
26335
+ reason: `extension "${pluginId}/${extId}" declares no setting "${settingId}"`
26336
+ };
26337
+ }
26338
+ let invalidReason = null;
26339
+ resolveExtensionSettings(
26340
+ { pluginId, id: extId, settings: { [settingId]: declaration } },
26341
+ { plugins: { [pluginId]: { extensions: { [extId]: { settings: { [settingId]: value } } } } } },
26342
+ (message) => {
26343
+ invalidReason = message;
26344
+ }
26345
+ );
26346
+ if (invalidReason !== null) {
26347
+ return { settingId, reason: invalidReason };
26348
+ }
26349
+ }
26350
+ return null;
26351
+ }
26352
+ function persistSettingsPatch(pluginId, extId, declarations, patch, cwd) {
26353
+ for (const [settingId, value] of Object.entries(patch)) {
26354
+ const declaration = declarations?.[settingId];
26355
+ const target = declaration?.type === "secret" ? "project-local" : "project";
26356
+ const dotKey = `plugins.${pluginId}.extensions.${extId}.settings.${settingId}`;
26357
+ writeConfigValue(dotKey, value, { target, cwd });
26358
+ }
26359
+ }
26360
+
25894
26361
  // server/routes/plugins.ts
25895
26362
  var SINGLE_PATCH_BODY_SCHEMA = {
25896
26363
  type: "object",
@@ -25920,10 +26387,18 @@ var BULK_PATCH_BODY_SCHEMA = {
25920
26387
  items: {
25921
26388
  type: "object",
25922
26389
  additionalProperties: false,
25923
- required: ["id", "enabled"],
26390
+ // `id` is the only mandatory field; a change carries `enabled`,
26391
+ // `settings`, or both. `minProperties: 2` enforces at least one
26392
+ // of the two beyond `id` (an `{ id }`-only entry is a no-op the
26393
+ // client should not send). Per-setting type validation runs in
26394
+ // code against the manifest, so `settings` is a permissive
26395
+ // object here (the body schema only fixes its container shape).
26396
+ required: ["id"],
26397
+ minProperties: 2,
25924
26398
  properties: {
25925
26399
  id: { type: "string", minLength: 1 },
25926
- enabled: { type: "boolean" }
26400
+ enabled: { type: "boolean" },
26401
+ settings: { type: "object" }
25927
26402
  }
25928
26403
  }
25929
26404
  }
@@ -25935,7 +26410,13 @@ var parseBulkPatchBody = makeBodyValidator(BULK_PATCH_BODY_SCHEMA, {
25935
26410
  invalid: SERVER_TEXTS.pluginsChangeMalformed,
25936
26411
  mapping: {
25937
26412
  "/changes:required": SERVER_TEXTS.pluginsChangesRequired,
25938
- "/changes:type:array": SERVER_TEXTS.pluginsChangesRequired
26413
+ "/changes:type:array": SERVER_TEXTS.pluginsChangesRequired,
26414
+ // A change with neither `enabled` nor `settings` (just `id`) trips
26415
+ // `minProperties`; surface the same "malformed entry" message.
26416
+ "/changes/*:minProperties": SERVER_TEXTS.pluginsChangeMalformed,
26417
+ "/changes/*:type:object": SERVER_TEXTS.pluginsChangeMalformed,
26418
+ "/changes/*/settings:type:object": SERVER_TEXTS.pluginsChangeMalformed,
26419
+ "/changes/*/enabled:type:boolean": SERVER_TEXTS.pluginsChangeMalformed
25939
26420
  }
25940
26421
  });
25941
26422
  function registerPluginsRoute(app, deps) {
@@ -26019,17 +26500,24 @@ function registerPluginsRoute(app, deps) {
26019
26500
  });
26020
26501
  }
26021
26502
  function listItems(deps, resolveEnabled) {
26503
+ const config = deps.configService.effective();
26022
26504
  return [
26023
- ...deps.options.noBuiltIns ? [] : buildBuiltInItems(resolveEnabled),
26024
- ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled)
26505
+ ...deps.options.noBuiltIns ? [] : buildBuiltInItems(resolveEnabled, config),
26506
+ ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled, config)
26025
26507
  ];
26026
26508
  }
26027
- function buildBuiltInItems(resolveEnabled) {
26509
+ function buildBuiltInItems(resolveEnabled, config) {
26028
26510
  return sortPluginsForPresentation(builtInPlugins).map((plugin) => {
26029
26511
  const pluginLocked = isPluginLocked(plugin.id);
26030
26512
  const extensions = plugin.extensions.map((ext) => {
26031
26513
  const qualified = qualifiedExtensionId(plugin.id, ext.id);
26032
26514
  const extLocked = pluginLocked || isPluginLocked(qualified);
26515
+ const settings2 = projectExtensionSettings(
26516
+ plugin.id,
26517
+ ext.id,
26518
+ readManifestSettings(ext),
26519
+ config
26520
+ );
26033
26521
  return {
26034
26522
  id: ext.id,
26035
26523
  kind: ext.kind,
@@ -26037,7 +26525,8 @@ function buildBuiltInItems(resolveEnabled) {
26037
26525
  enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
26038
26526
  ...ext.description ? { description: ext.description } : {},
26039
26527
  ...ext.stability ? { stability: ext.stability } : {},
26040
- ...extLocked ? { locked: true } : {}
26528
+ ...extLocked ? { locked: true } : {},
26529
+ ...settings2
26041
26530
  };
26042
26531
  });
26043
26532
  const pluginEnabled = extensions.some((e) => e.enabled);
@@ -26054,12 +26543,12 @@ function buildBuiltInItems(resolveEnabled) {
26054
26543
  };
26055
26544
  });
26056
26545
  }
26057
- function buildDiscoveredItems(discovered, deps, resolveEnabled) {
26058
- return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled));
26546
+ function buildDiscoveredItems(discovered, deps, resolveEnabled, config) {
26547
+ return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled, config));
26059
26548
  }
26060
- function buildDiscoveredItem(plugin, deps, resolveEnabled) {
26549
+ function buildDiscoveredItem(plugin, deps, resolveEnabled, config) {
26061
26550
  const pluginLocked = isPluginLocked(plugin.id);
26062
- const extensions = projectExtensionRows(plugin, resolveEnabled, pluginLocked);
26551
+ const extensions = projectExtensionRows(plugin, resolveEnabled, pluginLocked, config);
26063
26552
  const optional = optionalDiscoveredFields(plugin, extensions);
26064
26553
  return {
26065
26554
  id: plugin.id,
@@ -26080,12 +26569,18 @@ function optionalDiscoveredFields(plugin, extensions) {
26080
26569
  if (extensions) out.extensions = extensions;
26081
26570
  return out;
26082
26571
  }
26083
- function projectExtensionRows(plugin, resolveEnabled, pluginLocked) {
26572
+ function projectExtensionRows(plugin, resolveEnabled, pluginLocked, config) {
26084
26573
  if (!plugin.extensions || plugin.extensions.length === 0) return void 0;
26085
26574
  return plugin.extensions.map((ext) => {
26086
26575
  const description = readInstanceDescription(ext.instance);
26087
26576
  const qualified = qualifiedExtensionId(plugin.id, ext.id);
26088
26577
  const extLocked = pluginLocked || isPluginLocked(qualified);
26578
+ const settings2 = projectExtensionSettings(
26579
+ plugin.id,
26580
+ ext.id,
26581
+ readManifestSettings(ext.instance),
26582
+ config
26583
+ );
26089
26584
  return {
26090
26585
  id: ext.id,
26091
26586
  kind: ext.kind,
@@ -26093,7 +26588,8 @@ function projectExtensionRows(plugin, resolveEnabled, pluginLocked) {
26093
26588
  enabled: resolveEnabled(qualified, installedDefaultEnabled(ext.stability)),
26094
26589
  ...description ? { description } : {},
26095
26590
  ...ext.stability ? { stability: ext.stability } : {},
26096
- ...extLocked ? { locked: true } : {}
26591
+ ...extLocked ? { locked: true } : {},
26592
+ ...settings2
26097
26593
  };
26098
26594
  });
26099
26595
  }
@@ -26211,25 +26707,35 @@ function projectListResponse(c, deps, overrides) {
26211
26707
  );
26212
26708
  }
26213
26709
  function validateBulkChange(change, deps) {
26214
- const slash = change.id.indexOf("/");
26215
- if (slash < 0) {
26216
- const handle2 = findHandle(change.id, deps);
26217
- if (!handle2) {
26218
- return {
26219
- status: 404,
26220
- code: "not-found",
26221
- message: tx(SERVER_TEXTS.pluginsUnknown, { id: change.id })
26222
- };
26223
- }
26224
- if (isPluginLocked(change.id)) {
26225
- return {
26226
- status: 403,
26227
- code: "locked",
26228
- message: tx(SERVER_TEXTS.pluginsLocked, { id: change.id })
26229
- };
26230
- }
26231
- return null;
26710
+ return change.id.includes("/") ? validateQualifiedBulkChange(change, deps) : validateBareBulkChange(change, deps);
26711
+ }
26712
+ function validateBareBulkChange(change, deps) {
26713
+ if (change.settings !== void 0) {
26714
+ return {
26715
+ status: 400,
26716
+ code: "bad-query",
26717
+ message: tx(SERVER_TEXTS.pluginsSettingsRequireQualifiedId, { id: change.id })
26718
+ };
26719
+ }
26720
+ const handle = findHandle(change.id, deps);
26721
+ if (!handle) {
26722
+ return {
26723
+ status: 404,
26724
+ code: "not-found",
26725
+ message: tx(SERVER_TEXTS.pluginsUnknown, { id: change.id })
26726
+ };
26727
+ }
26728
+ if (isPluginLocked(change.id)) {
26729
+ return {
26730
+ status: 403,
26731
+ code: "locked",
26732
+ message: tx(SERVER_TEXTS.pluginsLocked, { id: change.id })
26733
+ };
26232
26734
  }
26735
+ return null;
26736
+ }
26737
+ function validateQualifiedBulkChange(change, deps) {
26738
+ const slash = change.id.indexOf("/");
26233
26739
  const pluginId = change.id.slice(0, slash);
26234
26740
  const extensionId = change.id.slice(slash + 1);
26235
26741
  const handle = findHandle(pluginId, deps);
@@ -26254,13 +26760,49 @@ function validateBulkChange(change, deps) {
26254
26760
  message: tx(SERVER_TEXTS.pluginsExtensionLocked, { pluginId, extensionId })
26255
26761
  };
26256
26762
  }
26763
+ if (change.settings !== void 0) {
26764
+ return validateChangeSettings(handle, pluginId, extensionId, change.settings);
26765
+ }
26257
26766
  return null;
26258
26767
  }
26768
+ function validateChangeSettings(handle, pluginId, extensionId, patch) {
26769
+ const declarations = handleExtensionSettings(handle, extensionId);
26770
+ if (!declarations || Object.keys(declarations).length === 0) {
26771
+ return {
26772
+ status: 400,
26773
+ code: "bad-query",
26774
+ message: tx(SERVER_TEXTS.pluginsSettingsNoneDeclared, { pluginId, extensionId })
26775
+ };
26776
+ }
26777
+ const failure = validateSettingsPatch(pluginId, extensionId, declarations, patch);
26778
+ if (failure !== null) {
26779
+ return {
26780
+ status: 400,
26781
+ code: "bad-query",
26782
+ message: tx(SERVER_TEXTS.pluginsSettingsInvalid, {
26783
+ settingId: failure.settingId,
26784
+ pluginId,
26785
+ extensionId,
26786
+ reason: failure.reason
26787
+ })
26788
+ };
26789
+ }
26790
+ return null;
26791
+ }
26792
+ function handleExtensionSettings(handle, extensionId) {
26793
+ if (handle.kind === "built-in") {
26794
+ const ext2 = handle.plugin.extensions.find((e) => e.id === extensionId);
26795
+ return readManifestSettings(ext2);
26796
+ }
26797
+ const ext = (handle.plugin.extensions ?? []).find((e) => e.id === extensionId);
26798
+ return readManifestSettings(ext?.instance);
26799
+ }
26259
26800
  async function persistBulkAndProject(c, deps, changes) {
26260
26801
  const overrides = await tryWithSqlite(
26261
26802
  { databasePath: deps.options.dbPath, autoBackup: false },
26262
26803
  async (adapter) => {
26263
26804
  for (const change of changes) {
26805
+ if (change.enabled === void 0) continue;
26264
26806
  const writeKeys = expandBulkChangeKeys(change, deps);
26265
26807
  for (const key of writeKeys) {
26266
26808
  await applyChangeToAdapter(adapter, key, change.enabled);
@@ -26269,8 +26811,40 @@ async function persistBulkAndProject(c, deps, changes) {
26269
26811
  return await adapter.pluginConfig.loadOverrideMap();
26270
26812
  }
26271
26813
  );
26814
+ if (overrides === null) {
26815
+ throw new DbMissingError(
26816
+ tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
26817
+ );
26818
+ }
26819
+ const settingsTouched = persistBulkSettings(deps, changes);
26820
+ if (settingsTouched) deps.configService.reload();
26272
26821
  return projectListResponse(c, deps, overrides);
26273
26822
  }
26823
+ function persistBulkSettings(deps, changes) {
26824
+ const cwd = deps.runtimeContext.cwd;
26825
+ let touched = false;
26826
+ for (const change of changes) {
26827
+ if (change.settings === void 0) continue;
26828
+ const slash = change.id.indexOf("/");
26829
+ if (slash < 0) continue;
26830
+ const pluginId = change.id.slice(0, slash);
26831
+ const extensionId = change.id.slice(slash + 1);
26832
+ const handle = findHandle(pluginId, deps);
26833
+ const declarations = handle ? handleExtensionSettings(handle, extensionId) : void 0;
26834
+ try {
26835
+ persistSettingsPatch(pluginId, extensionId, declarations, change.settings, cwd);
26836
+ } catch (err) {
26837
+ throw new HTTPException9(500, {
26838
+ message: tx(SERVER_TEXTS.pluginsSettingsPersistFailed, {
26839
+ id: change.id,
26840
+ message: err instanceof Error ? err.message : String(err)
26841
+ })
26842
+ });
26843
+ }
26844
+ if (Object.keys(change.settings).length > 0) touched = true;
26845
+ }
26846
+ return touched;
26847
+ }
26274
26848
  function expandBulkChangeKeys(change, deps) {
26275
26849
  if (change.id.includes("/")) return [change.id];
26276
26850
  const handle = findHandle(change.id, deps);
@@ -28622,7 +29196,7 @@ function formatCwdPath(cwd) {
28622
29196
  // cli/commands/serve.ts
28623
29197
  var ServeCommand = class extends SmCommand {
28624
29198
  static paths = [["serve"]];
28625
- static usage = Command33.Usage({
29199
+ static usage = Command34.Usage({
28626
29200
  category: "Setup",
28627
29201
  description: "Start the Hono BFF (single-port: REST + WebSocket + SPA bundle).",
28628
29202
  details: `
@@ -28646,18 +29220,18 @@ var ServeCommand = class extends SmCommand {
28646
29220
  ["Point at a pre-built UI bundle", "$0 serve --ui-dist ./ui/dist/browser"]
28647
29221
  ]
28648
29222
  });
28649
- port = Option31.String("--port", {
29223
+ port = Option32.String("--port", {
28650
29224
  required: false,
28651
29225
  description: "Listening port (default 4242). 0 = OS-assigned."
28652
29226
  });
28653
- host = Option31.String("--host", {
29227
+ host = Option32.String("--host", {
28654
29228
  required: false,
28655
29229
  description: "Listening host (default 127.0.0.1). Loopback-only enforced when --dev-cors is set."
28656
29230
  });
28657
- noBuiltIns = Option31.Boolean("--no-built-ins", false, {
29231
+ noBuiltIns = Option32.Boolean("--no-built-ins", false, {
28658
29232
  description: "Skip built-in plugin registration (parity with sm scan --no-built-ins)."
28659
29233
  });
28660
- noPlugins = Option31.Boolean("--no-plugins", false, {
29234
+ noPlugins = Option32.Boolean("--no-plugins", false, {
28661
29235
  description: "Skip drop-in plugin discovery."
28662
29236
  });
28663
29237
  // `Option.Boolean('--open', true)`, Clipanion's parser auto-derives
@@ -28667,31 +29241,31 @@ var ServeCommand = class extends SmCommand {
28667
29241
  // two registrations for the same flag and rejects the invocation
28668
29242
  // with "Ambiguous Syntax Error". Same convention shipped by every
28669
29243
  // other `--no-...` flag in the CLI tree.
28670
- open = Option31.Boolean("--open", true, {
29244
+ open = Option32.Boolean("--open", true, {
28671
29245
  description: "Auto-open the SPA in the user's default browser after listen. --no-open opts out."
28672
29246
  });
28673
- devCors = Option31.Boolean("--dev-cors", false, {
29247
+ devCors = Option32.Boolean("--dev-cors", false, {
28674
29248
  description: "Enable permissive CORS for the Angular dev-server proxy workflow."
28675
29249
  });
28676
29250
  // `--ui-dist` is intentionally undocumented in the Usage block above
28677
29251
  // (the demo build pipeline + tests rely on it; everyday users never
28678
29252
  // need it). Clipanion still exposes it on the parser; the Usage
28679
29253
  // omission is the "hidden" contract per the 14.1 brief.
28680
- uiDist = Option31.String("--ui-dist", { required: false, hidden: true });
28681
- noUi = Option31.Boolean("--no-ui", false, {
29254
+ uiDist = Option32.String("--ui-dist", { required: false, hidden: true });
29255
+ noUi = Option32.Boolean("--no-ui", false, {
28682
29256
  description: "Don't serve the Angular UI bundle. Use this when running the BFF alongside `ui:dev` (Angular dev server with HMR). The root `/` then renders an inline placeholder pointing the user at the dev server."
28683
29257
  });
28684
- noWatcher = Option31.Boolean("--no-watcher", false, {
29258
+ noWatcher = Option32.Boolean("--no-watcher", false, {
28685
29259
  description: "Disable the chokidar-fed scan-and-broadcast loop. Use only for CI / read-only deployments."
28686
29260
  });
28687
- yes = Option31.Boolean("--yes", false, {
29261
+ yes = Option32.Boolean("--yes", false, {
28688
29262
  description: "Skip the interactive prompt and rebuild the local cache when the on-disk DB has drifted (version skew or an inline schema change). Non-TTY invocations rebuild without asking regardless of this flag."
28689
29263
  });
28690
29264
  // `--watcher-debounce-ms` is undocumented sugar for advanced users
28691
29265
  // who want to tighten / relax the watcher's batching window without
28692
29266
  // editing settings.json. Hidden flag, the Usage block omits it.
28693
- watcherDebounceMs = Option31.String("--watcher-debounce-ms", { required: false, hidden: true });
28694
- maxNodes = Option31.String("--max-nodes", {
29267
+ watcherDebounceMs = Option32.String("--watcher-debounce-ms", { required: false, hidden: true });
29268
+ maxNodes = Option32.String("--max-nodes", {
28695
29269
  required: false,
28696
29270
  description: "Per-invocation override of scan.maxNodes (default 256). Bidirectional: raises OR lowers the recommended cap on classified nodes. Applies to every scan the server runs (initial watcher pass, debounced batches, POST /api/scan, GET /api/scan?fresh=1). Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
28697
29271
  });
@@ -29035,7 +29609,7 @@ function tryOpenBrowser(url, stderr, warnGlyph) {
29035
29609
  }
29036
29610
 
29037
29611
  // cli/commands/show.ts
29038
- import { Command as Command34, Option as Option32 } from "clipanion";
29612
+ import { Command as Command35, Option as Option33 } from "clipanion";
29039
29613
 
29040
29614
  // cli/i18n/show.texts.ts
29041
29615
  var SHOW_TEXTS = {
@@ -29086,7 +29660,7 @@ var SHOW_TEXTS = {
29086
29660
  // cli/commands/show.ts
29087
29661
  var ShowCommand = class extends SmCommand {
29088
29662
  static paths = [["show"]];
29089
- static usage = Command34.Usage({
29663
+ static usage = Command35.Usage({
29090
29664
  category: "Browse",
29091
29665
  description: "Node detail: weight, frontmatter, links, issues.",
29092
29666
  details: `
@@ -29102,7 +29676,7 @@ var ShowCommand = class extends SmCommand {
29102
29676
  ["Machine-readable detail", "$0 show .claude/agents/architect.md --json"]
29103
29677
  ]
29104
29678
  });
29105
- nodePath = Option32.String({ required: true });
29679
+ nodePath = Option33.String({ required: true });
29106
29680
  async run() {
29107
29681
  const dbPath = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
29108
29682
  const exit = requireDbOrExit(dbPath, this.context.stderr);
@@ -29344,7 +29918,7 @@ function rankConfidenceForGrouping(c) {
29344
29918
  // cli/commands/sidecar.ts
29345
29919
  import { unlink as unlink3 } from "fs/promises";
29346
29920
  import { resolve as resolve38 } from "path";
29347
- import { Command as Command35, Option as Option33 } from "clipanion";
29921
+ import { Command as Command36, Option as Option34 } from "clipanion";
29348
29922
 
29349
29923
  // cli/i18n/sidecar.texts.ts
29350
29924
  var SIDECAR_TEXTS = {
@@ -29425,7 +29999,7 @@ async function runWithSidecarConsent(bag, ansi, dispatch) {
29425
29999
  }
29426
30000
  var SidecarRefreshCommand = class extends SmCommand {
29427
30001
  static paths = [["sidecar", "refresh"]];
29428
- static usage = Command35.Usage({
30002
+ static usage = Command36.Usage({
29429
30003
  category: "Actions",
29430
30004
  description: "Refresh a sidecar's `for.{bodyHash, frontmatterHash}` to match the live node. Does NOT bump the version.",
29431
30005
  details: `
@@ -29442,8 +30016,8 @@ var SidecarRefreshCommand = class extends SmCommand {
29442
30016
  ["Refresh a node's sidecar hashes", "$0 sidecar refresh .claude/agents/architect.md"]
29443
30017
  ]
29444
30018
  });
29445
- nodePath = Option33.String({ required: true });
29446
- yes = Option33.Boolean("--yes", false, {
30019
+ nodePath = Option34.String({ required: true });
30020
+ yes = Option34.Boolean("--yes", false, {
29447
30021
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
29448
30022
  });
29449
30023
  async run() {
@@ -29565,7 +30139,7 @@ var SidecarRefreshCommand = class extends SmCommand {
29565
30139
  };
29566
30140
  var SidecarPruneCommand = class extends SmCommand {
29567
30141
  static paths = [["sidecar", "prune"]];
29568
- static usage = Command35.Usage({
30142
+ static usage = Command36.Usage({
29569
30143
  category: "Actions",
29570
30144
  description: "Delete orphan .sm files (sidecars whose accompanying .md no longer exists).",
29571
30145
  details: `
@@ -29587,8 +30161,8 @@ var SidecarPruneCommand = class extends SmCommand {
29587
30161
  ["Delete every orphan .sm file (non-interactive)", "$0 sidecar prune --yes"]
29588
30162
  ]
29589
30163
  });
29590
- dryRun = Option33.Boolean("-n,--dry-run", false);
29591
- yes = Option33.Boolean("--yes,--force", false, {
30164
+ dryRun = Option34.Boolean("-n,--dry-run", false);
30165
+ yes = Option34.Boolean("--yes,--force", false, {
29592
30166
  description: "Skip the interactive confirmation prompt. Required for non-interactive callers (CI, pre-commit hooks)."
29593
30167
  });
29594
30168
  // Complexity is from per-orphan handling, empty-set / dry-run /
@@ -29708,7 +30282,7 @@ var SidecarPruneCommand = class extends SmCommand {
29708
30282
  };
29709
30283
  var SidecarAnnotateCommand = class extends SmCommand {
29710
30284
  static paths = [["sidecar", "annotate"]];
29711
- static usage = Command35.Usage({
30285
+ static usage = Command36.Usage({
29712
30286
  category: "Actions",
29713
30287
  description: "Scaffold an empty `<basename>.sm` next to a node ready for editing.",
29714
30288
  details: `
@@ -29726,9 +30300,9 @@ var SidecarAnnotateCommand = class extends SmCommand {
29726
30300
  ["Overwrite an existing one", "$0 sidecar annotate .claude/agents/architect.md --force"]
29727
30301
  ]
29728
30302
  });
29729
- nodePath = Option33.String({ required: true });
29730
- force = Option33.Boolean("--force", false);
29731
- yes = Option33.Boolean("--yes", false, {
30303
+ nodePath = Option34.String({ required: true });
30304
+ force = Option34.Boolean("--force", false);
30305
+ yes = Option34.Boolean("--yes", false, {
29732
30306
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
29733
30307
  });
29734
30308
  async run() {
@@ -29867,7 +30441,7 @@ var SIDECAR_COMMANDS = [
29867
30441
  ];
29868
30442
 
29869
30443
  // cli/commands/stubs.ts
29870
- import { Command as Command36, Option as Option34 } from "clipanion";
30444
+ import { Command as Command37, Option as Option35 } from "clipanion";
29871
30445
 
29872
30446
  // cli/i18n/stubs.texts.ts
29873
30447
  var STUBS_TEXTS = {
@@ -29893,7 +30467,7 @@ var StubCommand = class extends SmCommand {
29893
30467
  };
29894
30468
  var DoctorCommand = class extends StubCommand {
29895
30469
  static paths = [["doctor"]];
29896
- static usage = Command36.Usage({
30470
+ static usage = Command37.Usage({
29897
30471
  category: "Setup",
29898
30472
  description: planned("Diagnostic report: DB integrity, pending migrations, orphan rows, plugin status, runner availability.")
29899
30473
  });
@@ -29901,18 +30475,18 @@ var DoctorCommand = class extends StubCommand {
29901
30475
  };
29902
30476
  var FindingsCommand = class extends StubCommand {
29903
30477
  static paths = [["findings"]];
29904
- static usage = Command36.Usage({
30478
+ static usage = Command37.Usage({
29905
30479
  category: "Browse",
29906
30480
  description: planned("Probabilistic findings: injection, stale summaries, low confidence.")
29907
30481
  });
29908
- kind = Option34.String("--kind", { required: false });
29909
- since = Option34.String("--since", { required: false });
29910
- threshold = Option34.String("--threshold", { required: false });
30482
+ kind = Option35.String("--kind", { required: false });
30483
+ since = Option35.String("--since", { required: false });
30484
+ threshold = Option35.String("--threshold", { required: false });
29911
30485
  verbName = "findings";
29912
30486
  };
29913
30487
  var ActionsListCommand = class extends StubCommand {
29914
30488
  static paths = [["actions", "list"]];
29915
- static usage = Command36.Usage({
30489
+ static usage = Command37.Usage({
29916
30490
  category: "Jobs",
29917
30491
  description: planned("Registered action types (manifest view).")
29918
30492
  });
@@ -29920,103 +30494,103 @@ var ActionsListCommand = class extends StubCommand {
29920
30494
  };
29921
30495
  var ActionsShowCommand = class extends StubCommand {
29922
30496
  static paths = [["actions", "show"]];
29923
- static usage = Command36.Usage({
30497
+ static usage = Command37.Usage({
29924
30498
  category: "Jobs",
29925
30499
  description: planned("Full action manifest, including preconditions and expected duration.")
29926
30500
  });
29927
- id = Option34.String({ required: true });
30501
+ id = Option35.String({ required: true });
29928
30502
  verbName = "actions show";
29929
30503
  };
29930
30504
  var JobSubmitCommand = class extends StubCommand {
29931
30505
  static paths = [["job", "submit"]];
29932
- static usage = Command36.Usage({
30506
+ static usage = Command37.Usage({
29933
30507
  category: "Jobs",
29934
30508
  description: planned("Enqueue a single job or fan out to every matching node (--all).")
29935
30509
  });
29936
- action = Option34.String({ required: true });
29937
- node = Option34.String("-n", { required: false });
29938
- all = Option34.Boolean("--all", false);
30510
+ action = Option35.String({ required: true });
30511
+ node = Option35.String("-n", { required: false });
30512
+ all = Option35.Boolean("--all", false);
29939
30513
  // CLI flag stays `--run`; field name is `runFlag` per the
29940
30514
  // shadow-avoidance convention documented on `SmCommand`.
29941
- runFlag = Option34.Boolean("--run", false);
29942
- force = Option34.Boolean("--force", false);
29943
- ttl = Option34.String("--ttl", { required: false });
29944
- priority = Option34.String("--priority", { required: false });
30515
+ runFlag = Option35.Boolean("--run", false);
30516
+ force = Option35.Boolean("--force", false);
30517
+ ttl = Option35.String("--ttl", { required: false });
30518
+ priority = Option35.String("--priority", { required: false });
29945
30519
  verbName = "job submit";
29946
30520
  };
29947
30521
  var JobListCommand = class extends StubCommand {
29948
30522
  static paths = [["job", "list"]];
29949
- static usage = Command36.Usage({ category: "Jobs", description: planned("List jobs.") });
29950
- status = Option34.String("--status", { required: false });
29951
- action = Option34.String("--action", { required: false });
29952
- node = Option34.String("--node", { required: false });
30523
+ static usage = Command37.Usage({ category: "Jobs", description: planned("List jobs.") });
30524
+ status = Option35.String("--status", { required: false });
30525
+ action = Option35.String("--action", { required: false });
30526
+ node = Option35.String("--node", { required: false });
29953
30527
  verbName = "job list";
29954
30528
  };
29955
30529
  var JobShowCommand = class extends StubCommand {
29956
30530
  static paths = [["job", "show"]];
29957
- static usage = Command36.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
29958
- id = Option34.String({ required: true });
30531
+ static usage = Command37.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
30532
+ id = Option35.String({ required: true });
29959
30533
  verbName = "job show";
29960
30534
  };
29961
30535
  var JobPreviewCommand = class extends StubCommand {
29962
30536
  static paths = [["job", "preview"]];
29963
- static usage = Command36.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
29964
- id = Option34.String({ required: true });
30537
+ static usage = Command37.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
30538
+ id = Option35.String({ required: true });
29965
30539
  verbName = "job preview";
29966
30540
  };
29967
30541
  var JobClaimCommand = class extends StubCommand {
29968
30542
  static paths = [["job", "claim"]];
29969
- static usage = Command36.Usage({
30543
+ static usage = Command37.Usage({
29970
30544
  category: "Jobs",
29971
30545
  description: planned("Atomic primitive: return next queued job id, mark it running.")
29972
30546
  });
29973
- filter = Option34.String("--filter", { required: false });
30547
+ filter = Option35.String("--filter", { required: false });
29974
30548
  verbName = "job claim";
29975
30549
  };
29976
30550
  var JobRunCommand = class extends StubCommand {
29977
30551
  static paths = [["job", "run"]];
29978
- static usage = Command36.Usage({
30552
+ static usage = Command37.Usage({
29979
30553
  category: "Jobs",
29980
30554
  description: planned("Full CLI-runner loop: claim + spawn + record.")
29981
30555
  });
29982
- all = Option34.Boolean("--all", false);
29983
- max = Option34.String("--max", { required: false });
30556
+ all = Option35.Boolean("--all", false);
30557
+ max = Option35.String("--max", { required: false });
29984
30558
  verbName = "job run";
29985
30559
  };
29986
30560
  var JobStatusCommand = class extends StubCommand {
29987
30561
  static paths = [["job", "status"]];
29988
- static usage = Command36.Usage({
30562
+ static usage = Command37.Usage({
29989
30563
  category: "Jobs",
29990
30564
  description: planned("Counts (per status) or single-job status.")
29991
30565
  });
29992
- id = Option34.String({ required: false });
30566
+ id = Option35.String({ required: false });
29993
30567
  verbName = "job status";
29994
30568
  };
29995
30569
  var JobCancelCommand = class extends StubCommand {
29996
30570
  static paths = [["job", "cancel"]];
29997
- static usage = Command36.Usage({
30571
+ static usage = Command37.Usage({
29998
30572
  category: "Jobs",
29999
30573
  description: planned("Force a running job to failed with reason user-cancelled.")
30000
30574
  });
30001
- id = Option34.String({ required: false });
30002
- all = Option34.Boolean("--all", false);
30575
+ id = Option35.String({ required: false });
30576
+ all = Option35.Boolean("--all", false);
30003
30577
  verbName = "job cancel";
30004
30578
  };
30005
30579
  var RecordCommand = class extends StubCommand {
30006
30580
  static paths = [["record"]];
30007
- static usage = Command36.Usage({
30581
+ static usage = Command37.Usage({
30008
30582
  category: "Jobs",
30009
30583
  description: planned("Close a running job with success or failure. Nonce is the sole credential.")
30010
30584
  });
30011
- id = Option34.String("--id", { required: true });
30012
- nonce = Option34.String("--nonce", { required: true });
30013
- status = Option34.String("--status", { required: true });
30014
- report = Option34.String("--report", { required: false });
30015
- tokensIn = Option34.String("--tokens-in", { required: false });
30016
- tokensOut = Option34.String("--tokens-out", { required: false });
30017
- durationMs = Option34.String("--duration-ms", { required: false });
30018
- model = Option34.String("--model", { required: false });
30019
- error = Option34.String("--error", { required: false });
30585
+ id = Option35.String("--id", { required: true });
30586
+ nonce = Option35.String("--nonce", { required: true });
30587
+ status = Option35.String("--status", { required: true });
30588
+ report = Option35.String("--report", { required: false });
30589
+ tokensIn = Option35.String("--tokens-in", { required: false });
30590
+ tokensOut = Option35.String("--tokens-out", { required: false });
30591
+ durationMs = Option35.String("--duration-ms", { required: false });
30592
+ model = Option35.String("--model", { required: false });
30593
+ error = Option35.String("--error", { required: false });
30020
30594
  verbName = "record";
30021
30595
  };
30022
30596
  var STUB_COMMANDS = [
@@ -30040,7 +30614,7 @@ import { cpSync as cpSync2, existsSync as existsSync32, mkdirSync as mkdirSync6,
30040
30614
  import { dirname as dirname20, join as join21, resolve as resolve39 } from "path";
30041
30615
  import { createInterface as createInterface5 } from "readline";
30042
30616
  import { fileURLToPath as fileURLToPath7 } from "url";
30043
- import { Command as Command37, Option as Option35 } from "clipanion";
30617
+ import { Command as Command38, Option as Option36 } from "clipanion";
30044
30618
 
30045
30619
  // cli/i18n/tutorial.texts.ts
30046
30620
  var TUTORIAL_TEXTS = {
@@ -30102,7 +30676,7 @@ var TRIGGER_EN = "run the tutorial";
30102
30676
  var TRIGGER_ES = "ejecuta el tutorial";
30103
30677
  var TutorialCommand = class extends SmCommand {
30104
30678
  static paths = [["tutorial"]];
30105
- static usage = Command37.Usage({
30679
+ static usage = Command38.Usage({
30106
30680
  category: "Setup",
30107
30681
  description: "Materialize an interactive tester tutorial as a Claude Code skill folder under `<cwd>/.claude/skills/`.",
30108
30682
  details: `
@@ -30125,15 +30699,15 @@ var TutorialCommand = class extends SmCommand {
30125
30699
  // more. Accept one so a stale `sm tutorial master` lands on a friendly
30126
30700
  // usage error (guarded in `run()`) instead of clipanion's generic
30127
30701
  // "extraneous argument" message.
30128
- legacyPositional = Option35.String({ required: false });
30702
+ legacyPositional = Option36.String({ required: false });
30129
30703
  // Named `forProvider`, NOT `for` (reserved word). The CLI surface stays
30130
30704
  // `--for`; selects the destination Provider whose `scaffold.skillDir`
30131
30705
  // the skill is materialised under, skipping the interactive prompt.
30132
- forProvider = Option35.String("--for", {
30706
+ forProvider = Option36.String("--for", {
30133
30707
  required: false,
30134
30708
  description: "Destination provider id (e.g. claude, agent-skills). Skips the prompt."
30135
30709
  });
30136
- force = Option35.Boolean("--force", false, {
30710
+ force = Option36.Boolean("--force", false, {
30137
30711
  description: "Overwrite an existing target directory without prompting."
30138
30712
  });
30139
30713
  async run() {
@@ -30364,7 +30938,7 @@ function resolveSkillSourceDir() {
30364
30938
  }
30365
30939
 
30366
30940
  // cli/commands/version.ts
30367
- import { Command as Command38 } from "clipanion";
30941
+ import { Command as Command39 } from "clipanion";
30368
30942
 
30369
30943
  // cli/i18n/version.texts.ts
30370
30944
  var VERSION_TEXTS = {
@@ -30379,7 +30953,7 @@ var VERSION_TEXTS = {
30379
30953
  // cli/commands/version.ts
30380
30954
  var VersionCommand = class extends SmCommand {
30381
30955
  static paths = [["version"]];
30382
- static usage = Command38.Usage({
30956
+ static usage = Command39.Usage({
30383
30957
  category: "Introspection",
30384
30958
  description: "Print the CLI / kernel / spec / runtime / db-schema version matrix."
30385
30959
  });
@@ -30578,4 +31152,4 @@ function resolveBareDefault() {
30578
31152
  process.exit(ExitCode.Error);
30579
31153
  }
30580
31154
  //# sourceMappingURL=cli.js.map
30581
- //# debugId=e21852cd-d92e-5bdf-abc7-02fffaa89502
31155
+ //# debugId=49fdc2af-2694-5cd0-ac19-33f8d1dffa9e