@skill-map/cli 0.68.1 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/cli/tutorial/sm-tutorial/SKILL.md +8 -3
  2. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/agents-hub/providers/codex/en/agents-hub.md +2 -0
  3. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/agents-hub/providers/codex/es/agents-hub.md +2 -0
  4. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/codex/en/todo-bullet-agent.md +1 -0
  5. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/codex/en/todo-bullet-command.md +1 -0
  6. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/codex/en/todo-bullet-skill.md +1 -0
  7. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/codex/es/todo-bullet-agent.md +1 -0
  8. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/codex/es/todo-bullet-command.md +1 -0
  9. package/dist/cli/tutorial/sm-tutorial/fixtures-data/edits/todo-connectors/providers/codex/es/todo-bullet-skill.md +1 -0
  10. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/harness/providers/codex/en/__PROVIDER__/skills/publish/SKILL.md +2 -2
  11. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/harness/providers/codex/es/__PROVIDER__/skills/publish/SKILL.md +2 -2
  12. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/master/providers/codex/en/.codex/agents/master-agent.toml +1 -1
  13. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/master/providers/codex/es/.codex/agents/master-agent.toml +1 -1
  14. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/codex/en/.codex/agents/content-editor.toml +1 -1
  15. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/portfolio/providers/codex/es/.codex/agents/content-editor.toml +1 -1
  16. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/prologue/providers/codex/en/.codex/agents/demo-agent.toml +1 -1
  17. package/dist/cli/tutorial/sm-tutorial/fixtures-data/sets/prologue/providers/codex/es/.codex/agents/demo-agent.toml +1 -1
  18. package/dist/cli/tutorial/sm-tutorial/references/_core.md +57 -29
  19. package/dist/cli/tutorial/sm-tutorial/references/_manifest.json +70 -70
  20. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +38 -38
  21. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +3 -3
  22. package/dist/cli/tutorial/sm-tutorial/references/part-authoring.md +25 -8
  23. package/dist/cli/tutorial/sm-tutorial/references/part-basic-daily.md +91 -38
  24. package/dist/cli/tutorial/sm-tutorial/references/part-basic-fundamentals.md +7 -8
  25. package/dist/cli/tutorial/sm-tutorial/references/part-basic-kickoff.md +1 -1
  26. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +1 -1
  27. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +173 -56
  28. package/dist/cli/tutorial/sm-tutorial/references/part-fundamentals.md +14 -13
  29. package/dist/cli/tutorial/sm-tutorial/references/part-plugins.md +1 -1
  30. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +14 -12
  31. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +2 -2
  32. package/dist/cli/tutorial/sm-tutorial/scripts/lib/paths.js +6 -5
  33. package/dist/cli.js +963 -451
  34. package/dist/conformance/index.js +3 -3
  35. package/dist/index.js +17 -14
  36. package/dist/kernel/index.d.ts +54 -17
  37. package/dist/kernel/index.js +17 -14
  38. package/dist/migrations/001_initial.sql +7 -3
  39. package/dist/ui/{chunk-22EQLC23.js → chunk-EVNCL7FV.js} +21 -21
  40. package/dist/ui/chunk-GUGB4JY5.js +1 -0
  41. package/dist/ui/chunk-RJUHQQOF.js +3 -0
  42. package/dist/ui/{chunk-KMHXNOFZ.js → chunk-RSPEJBPT.js} +1 -1
  43. package/dist/ui/chunk-SQCXHF3J.js +2 -0
  44. package/dist/ui/index.html +1 -1
  45. package/dist/ui/main-K4O6LCIJ.js +4 -0
  46. package/migrations/001_initial.sql +7 -3
  47. package/package.json +2 -2
  48. package/dist/ui/chunk-K3ZRQNN5.js +0 -2
  49. package/dist/ui/chunk-PU5OP5RN.js +0 -1
  50. package/dist/ui/chunk-TLMV4LOQ.js +0 -3
  51. package/dist/ui/main-R7BIU4HU.js +0 -4
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]="a842c024-162c-5a3a-85db-1daea4b6735f")}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]="ca8113af-a7ac-5cd7-b45c-03a5974bca86")}catch(e){}}();
4
4
  import { existsSync as existsSync34 } from "fs";
5
5
  import { Builtins, Cli as Cli2 } from "clipanion";
6
6
 
@@ -250,7 +250,7 @@ function bucketByKind(kind, instance, bag) {
250
250
  // package.json
251
251
  var package_default = {
252
252
  name: "@skill-map/cli",
253
- version: "0.68.1",
253
+ version: "0.70.0",
254
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
255
255
  license: "MIT",
256
256
  type: "module",
@@ -633,7 +633,10 @@ var claudeProvider = {
633
633
  presentation: {
634
634
  label: "Anthropic's Claude",
635
635
  color: "#cc785c",
636
- colorDark: "#e89270"
636
+ colorDark: "#e89270",
637
+ // Claude Code invokes both commands and skills with `/`; the palette
638
+ // paints this as the `invokes` edge glyph under the claude lens.
639
+ invocationSigil: "/"
637
640
  },
638
641
  // Auto-detect marker: a `.claude/` directory under the scope root marks
639
642
  // a Claude Code project. Provider-owned (replaces the old central
@@ -913,13 +916,11 @@ var atDirectiveExtractor = {
913
916
  kind: "extractor",
914
917
  description: "Detects `@<token>` directives in a node's body using Claude Code rules, choosing the link kind by token shape. Example: a bare handle `@team` becomes a `mentions` link, while a file-flavoured token `@docs/api.md` becomes a `references` link.",
915
918
  scope: "body",
916
- // Authorised under the claude AND codex lenses: OpenAI Codex sub-agents
917
- // reference each other with the same `@<name>` mention grammar, and their
918
- // prompt (the TOML `developer_instructions` body, fed in via the codex
919
- // provider's `read.bodyField`) carries the same `@` tokens. The gate is the active
920
- // lens, not the node's provider, so under `codex` this parses `@` across
921
- // the project's markdown surface just as it does under `claude`.
922
- precondition: { provider: ["claude", "codex"] },
919
+ // Claude-only. This is Claude's `@<name>` grammar, where a bare handle is
920
+ // an agent / entity MENTION. OpenAI Codex's `@` is a file-path picker, not
921
+ // a mention grammar, so codex is NOT gated here; the codex-owned `at-file`
922
+ // extractor covers `@`-as-file-reference under the codex lens.
923
+ precondition: { provider: ["claude"] },
923
924
  // eslint-disable-next-line complexity
924
925
  extract(ctx) {
925
926
  const seenMentions = /* @__PURE__ */ new Set();
@@ -1007,16 +1008,15 @@ var slashCommandExtractor = {
1007
1008
  kind: "extractor",
1008
1009
  description: "Turns `/command` invocations in a node's body into arrows that point at the resolved slash command or skill, using Claude Code routing rules. Example: `/deploy` in the body draws an arrow to the `deploy` command.",
1009
1010
  scope: "body",
1010
- // Also authorised under the codex and antigravity lenses, which share the
1011
- // `/command` grammar. Under codex a sub-agent's prompt body (the TOML
1012
- // `developer_instructions` field) has its `/command` tokens parsed for
1013
- // pipeline parity; codex resolves them to its open-standard skills
1014
- // (`invokes: ['skill']`). Under antigravity a workflow / skill / AGENTS.md
1015
- // body's `/name` tokens resolve to BOTH skills and workflows
1016
- // (`invokes: ['skill', 'workflow']`), since Antigravity invokes either by
1017
- // the same slash. A lens that declares no `invokes` resolution leaves the
1018
- // signals unresolved (no spurious edges).
1019
- precondition: { provider: ["claude", "codex", "antigravity"] },
1011
+ // Also authorised under the antigravity lens, which shares the `/command`
1012
+ // grammar: a workflow / skill / AGENTS.md body's `/name` tokens resolve to
1013
+ // BOTH skills and workflows (`invokes: ['skill', 'workflow']`), since
1014
+ // Antigravity invokes either by the same slash. NOT gated under codex:
1015
+ // OpenAI Codex reserves `/` for its OWN built-in commands (`/model`,
1016
+ // `/init`, ...) and invokes user skills with `$` instead (parsed by the
1017
+ // codex `dollar-skill` extractor). A lens that declares no `invokes`
1018
+ // resolution leaves the signals unresolved (no spurious edges).
1019
+ precondition: { provider: ["claude", "antigravity"] },
1020
1020
  extract(ctx) {
1021
1021
  const seen = /* @__PURE__ */ new Set();
1022
1022
  const body = stripCodeAndHtml(ctx.body);
@@ -1241,26 +1241,32 @@ var agentSkillsProvider = {
1241
1241
  stability: "stable",
1242
1242
  // Auto-detect marker: a `.agents/` directory marks an open-standard
1243
1243
  // project. This is also the marker a Google/Antigravity project carries
1244
- // (Antigravity adopted the open standard). The marker only produces an
1245
- // auto-detect candidate once this experimental provider is enabled.
1246
- // Provider-owned.
1247
- detect: { markers: [".agents"] },
1244
+ // (Antigravity adopted the open standard) and the shared skill home a
1245
+ // Codex project populates under `.agents/skills/`. `fallback: true` makes
1246
+ // this candidate yield to any vendor marker present alongside `.agents/`:
1247
+ // a `.codex/` + `.agents/` project resolves `codex` outright, never an
1248
+ // ambiguous `codex` vs `agent-skills` prompt. The `.agents/` marker only
1249
+ // wins when no vendor marker is present. Provider-owned.
1250
+ detect: { markers: [".agents"], fallback: true },
1248
1251
  // Authoring target for `sm tutorial`: the open standard discovers skills
1249
1252
  // under `.agents/skills/<name>/SKILL.md`. `aka` lists Antigravity, which
1250
1253
  // shares this territory AND the BASIC tutorial track (skill + markdown,
1251
1254
  // references), so a tester on Antigravity scaffolds here. OpenAI Codex
1252
1255
  // also reads `.agents/skills/`, but Codex is a RICH-track lens (it has the
1253
- // `agent` kind, slash and `@`), so advertising it under this basic row
1256
+ // `agent` kind, plus `$`-skill invocation and `@`-file references), so
1257
+ // advertising it under this basic row
1254
1258
  // would hand it the wrong book; Codex is surfaced once a Codex rich
1255
1259
  // scaffold target lands. `aka` is display-only, `--for` matches the id.
1256
1260
  scaffold: { skillDir: ".agents/skills", aka: ["Google's Antigravity"] },
1257
1261
  read: COMMONS_READ,
1258
1262
  kinds: COMMONS_KINDS,
1259
1263
  resolution: COMMONS_RESOLUTION,
1260
- // Base reserved-name catalog (self-scope under the `agent-skills` lens). The
1261
- // shared export is inherited by every Provider that adopts the open
1262
- // standard (see `COMMONS_RESERVED_NAMES` above).
1263
- reservedNames: COMMONS_RESERVED_NAMES,
1264
+ // NO `reservedNames`: the neutral open standard has no `/`-invocation, a
1265
+ // skill activates by its `description` and connects via markdown links, so
1266
+ // a skill name cannot shadow a built-in `/` command. The shared
1267
+ // `COMMONS_RESERVED_NAMES` export above is for `/`-invoking vendors
1268
+ // (Antigravity) to spread, NOT applied here. See spec/architecture.md
1269
+ // §Provider · reservedNames.
1264
1270
  classify: classifyCommonsPath
1265
1271
  };
1266
1272
 
@@ -1308,7 +1314,10 @@ var antigravityProvider = {
1308
1314
  presentation: {
1309
1315
  label: "Google's Antigravity",
1310
1316
  color: "#7c3aed",
1311
- colorDark: "#a78bfa"
1317
+ colorDark: "#a78bfa",
1318
+ // Antigravity bolts `/`-invocation onto the open standard (skills and
1319
+ // workflows are `/<name>`), so the `invokes` edge glyph is the slash.
1320
+ invocationSigil: "/"
1312
1321
  },
1313
1322
  // Auto-detect marker: Antigravity's workflows live under `.agent/workflows/`
1314
1323
  // (SINGULAR `.agent`), its one vendor-specific on-disk territory. Skills
@@ -1461,7 +1470,11 @@ var codexProvider = {
1461
1470
  presentation: {
1462
1471
  label: "OpenAI's Codex",
1463
1472
  color: "#22c55e",
1464
- colorDark: "#4ade80"
1473
+ colorDark: "#4ade80",
1474
+ // Codex invokes skills with `$` (`$publish`); `/` is reserved for its
1475
+ // own built-in commands (`/model`, `/init`), so the `invokes` edge glyph
1476
+ // under the codex lens is the dollar, not the slash.
1477
+ invocationSigil: "$"
1465
1478
  },
1466
1479
  // Auto-detect marker: a `.codex/` directory marks a Codex CLI project.
1467
1480
  // `AGENTS.md` is intentionally NOT a marker: it is the open agents.md
@@ -1527,19 +1540,21 @@ var codexProvider = {
1527
1540
  // `frontmatter.name`.
1528
1541
  ...COMMONS_KINDS
1529
1542
  },
1530
- // Mentions resolve to agents (`@<name>`, the Codex sub-agent handle).
1531
- // Slash invocations resolve to skills (`invokes: ['skill']`, inherited
1532
- // from the open standard), so a `/skill-name` in an agent's prompt links
1533
- // to its `.agents/skills/` skill.
1543
+ // Skill invocations resolve to skills (`invokes: ['skill']`, inherited
1544
+ // from the open standard): a `$skill-name` in an agent's prompt (parsed by
1545
+ // the codex `dollar-skill` extractor) links to its `.agents/skills/` skill.
1546
+ // NO `mentions` entry: Codex's `@` is a file picker, parsed by the
1547
+ // codex-owned `at-file` extractor as a path-resolved `references` link, not
1548
+ // an agent-mention grammar.
1534
1549
  resolution: {
1535
- mentions: ["agent"],
1536
1550
  ...COMMONS_RESOLUTION
1537
1551
  },
1538
- // Open-standard reserved-name base (the universal cross-agent slash
1539
- // verbs an agent CLI ships built-in), inherited from `agent-skills` and
1540
- // applied under the codex lens via SELF scope: a user skill that shadows
1541
- // one is flagged by `core/name-reserved`.
1542
- reservedNames: COMMONS_RESERVED_NAMES,
1552
+ // NO `reservedNames`: Codex invokes skills via `$` (the `dollar-skill`
1553
+ // extractor), a namespace disjoint from its built-in `/` commands, so a
1554
+ // `$`-skill named `model` does NOT collide with `/model`. Reserved-skill
1555
+ // names only apply to lenses that invoke skills through the `/` command
1556
+ // channel (claude, antigravity). See spec/architecture.md §Provider ·
1557
+ // reservedNames.
1543
1558
  classify(path) {
1544
1559
  const lower = path.toLowerCase();
1545
1560
  if (lower.startsWith(".codex/agents/") && lower.endsWith(".toml")) return "agent";
@@ -1547,6 +1562,123 @@ var codexProvider = {
1547
1562
  }
1548
1563
  };
1549
1564
 
1565
+ // plugins/codex/extractors/at-file/index.ts
1566
+ import { posix as pathPosix2 } from "path";
1567
+ var ID4 = "at-file";
1568
+ var AT_RE2 = /(?:^|[^A-Za-z0-9_@])(@(?:\.{1,2}\/|\/)?[a-z0-9](?:[a-z0-9_\-./]*[a-z0-9_])?(?::[a-z0-9][a-z0-9_-]*)?)/gi;
1569
+ var FILE_EXT_RE2 = /\.(md|mdx|js|jsx|ts|tsx|json|yml|yaml|toml|txt|html|css|scss|less|py|rb|go|rs|java|c|cpp|h|hpp|sh|sql|svg|png|jpg|jpeg|gif|webp|pdf)$/i;
1570
+ var atFileExtractor = {
1571
+ id: ID4,
1572
+ pluginId: OPENAI_PLUGIN_ID,
1573
+ kind: "extractor",
1574
+ description: "Detects `@<file>` references in a node's body under the OpenAI Codex lens, where `@` is a file picker. A path- or extension-shaped token becomes a `references` link to that file; a bare `@handle` forms no edge. Example: `@builder.toml` in an agent's prompt draws an arrow to the `builder` agent file.",
1575
+ scope: "body",
1576
+ // Codex-only: under the codex lens `@` is a file-path picker, so only
1577
+ // file-shaped tokens form references. The claude `at-directive` (bare
1578
+ // `@handle` → agent mention) is NOT gated under codex.
1579
+ precondition: { provider: ["codex"] },
1580
+ extract(ctx) {
1581
+ const seenReferences = /* @__PURE__ */ new Set();
1582
+ const body = stripCodeAndHtml(ctx.body);
1583
+ const lineStarts = computeLineStarts(body);
1584
+ const sourceDir = pathPosix2.dirname(ctx.node.path);
1585
+ for (const match of body.matchAll(AT_RE2)) {
1586
+ const original = match[1];
1587
+ const bare = original.slice(1);
1588
+ const rationale = classifyAtToken(bare);
1589
+ if (rationale === null) continue;
1590
+ const target = resolveSourceRelative2(sourceDir, bare);
1591
+ const dedupKey = target.toLowerCase();
1592
+ if (seenReferences.has(dedupKey)) continue;
1593
+ seenReferences.add(dedupKey);
1594
+ const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
1595
+ const line = lineFor(lineStarts, captureOffset);
1596
+ ctx.emitSignal({
1597
+ source: ctx.node.path,
1598
+ scope: "body",
1599
+ range: { start: captureOffset, end: captureOffset + original.length, line },
1600
+ raw: original,
1601
+ candidates: [
1602
+ {
1603
+ extractorId: ID4,
1604
+ kind: "references",
1605
+ target,
1606
+ // 0.85: strong file signal (path prefix or known extension),
1607
+ // one degree of inference (the runtime resolves the path).
1608
+ confidence: 0.85,
1609
+ rationale,
1610
+ trigger: {
1611
+ originalTrigger: original,
1612
+ normalizedTrigger: target
1613
+ }
1614
+ }
1615
+ ]
1616
+ });
1617
+ }
1618
+ }
1619
+ };
1620
+ function classifyAtToken(bare) {
1621
+ if (bare.startsWith("/")) return null;
1622
+ if (bare.startsWith("./") || bare.startsWith("../")) return "relative path prefix";
1623
+ if (FILE_EXT_RE2.test(bare)) return "known file extension";
1624
+ return null;
1625
+ }
1626
+ function resolveSourceRelative2(sourceDir, bare) {
1627
+ const joined = sourceDir === "." ? bare : `${sourceDir}/${bare}`;
1628
+ return pathPosix2.normalize(joined);
1629
+ }
1630
+
1631
+ // plugins/codex/extractors/dollar-skill/index.ts
1632
+ var ID5 = "dollar-skill";
1633
+ var DOLLAR_RE = /(?<![A-Za-z0-9_$])(\$[a-z][a-z0-9_-]*)/g;
1634
+ var dollarSkillExtractor = {
1635
+ id: ID5,
1636
+ pluginId: OPENAI_PLUGIN_ID,
1637
+ kind: "extractor",
1638
+ description: "Turns `$skill` invocations in a node's body into arrows that point at the resolved Codex skill, using OpenAI Codex routing rules. Example: `$check-links` in the body draws an arrow to the `check-links` skill.",
1639
+ scope: "body",
1640
+ // Codex-only: `$skill` is OpenAI Codex's explicit skill-invocation
1641
+ // grammar. The codex provider resolves it to its open-standard skills
1642
+ // (`invokes: ['skill']`). Other lenses do not parse `$`.
1643
+ precondition: { provider: ["codex"] },
1644
+ extract(ctx) {
1645
+ const seen = /* @__PURE__ */ new Set();
1646
+ const body = stripCodeAndHtml(ctx.body);
1647
+ const lineStarts = computeLineStarts(body);
1648
+ for (const match of body.matchAll(DOLLAR_RE)) {
1649
+ const original = match[1];
1650
+ const normalized = normalizeTrigger(original);
1651
+ if (seen.has(normalized)) continue;
1652
+ seen.add(normalized);
1653
+ const captureOffset = (match.index ?? 0) + match[0].indexOf(original);
1654
+ const line = lineFor(lineStarts, captureOffset);
1655
+ ctx.emitSignal({
1656
+ source: ctx.node.path,
1657
+ scope: "body",
1658
+ range: { start: captureOffset, end: captureOffset + original.length, line },
1659
+ raw: original,
1660
+ candidates: [
1661
+ {
1662
+ extractorId: ID5,
1663
+ kind: "invokes",
1664
+ target: original,
1665
+ // 0.8: clean `$skill` match after code-block strip. The
1666
+ // lowercase-letter guard filters currency / env-var noise, so a
1667
+ // hit is unambiguous syntax. Resolution against the live skill
1668
+ // catalog happens downstream.
1669
+ confidence: 0.8,
1670
+ rationale: "unambiguous $skill syntax post code-block strip",
1671
+ trigger: {
1672
+ originalTrigger: original,
1673
+ normalizedTrigger: normalized
1674
+ }
1675
+ }
1676
+ ]
1677
+ });
1678
+ }
1679
+ }
1680
+ };
1681
+
1550
1682
  // plugins/core/providers/core-markdown/schemas/markdown.schema.json
1551
1683
  var markdown_schema_default = {
1552
1684
  $schema: "https://json-schema.org/draft/2020-12/schema",
@@ -1634,11 +1766,11 @@ var coreMarkdownProvider = {
1634
1766
  };
1635
1767
 
1636
1768
  // plugins/core/extractors/backtick-path/index.ts
1637
- import { posix as pathPosix2 } from "path";
1638
- var ID4 = "backtick-path";
1769
+ import { posix as pathPosix3 } from "path";
1770
+ var ID6 = "backtick-path";
1639
1771
  var PATH_RE = /(?<![\w/:.-])(?:\.{1,2}\/)?[\w][\w.-]*(?:\/[\w.-]+)*\.md\b(?![\w/])/g;
1640
1772
  var backtickPathExtractor = {
1641
- id: ID4,
1773
+ id: ID6,
1642
1774
  pluginId: CORE_PLUGIN_ID,
1643
1775
  kind: "extractor",
1644
1776
  description: "Turns relative .md paths written inside code spans and fenced blocks into arrows between nodes in the graph. Example: a backticked `references/rules.md` path draws an arrow to that file.",
@@ -1647,7 +1779,7 @@ var backtickPathExtractor = {
1647
1779
  const seen = /* @__PURE__ */ new Set();
1648
1780
  const body = extractCodeRegions(ctx.body);
1649
1781
  const lineStarts = computeLineStarts(body);
1650
- const sourceDir = pathPosix2.dirname(ctx.node.path);
1782
+ const sourceDir = pathPosix3.dirname(ctx.node.path);
1651
1783
  for (const match of body.matchAll(PATH_RE)) {
1652
1784
  const original = match[0];
1653
1785
  const resolved = resolveTarget(sourceDir, original);
@@ -1663,7 +1795,7 @@ var backtickPathExtractor = {
1663
1795
  raw: original,
1664
1796
  candidates: [
1665
1797
  {
1666
- extractorId: ID4,
1798
+ extractorId: ID6,
1667
1799
  kind: "points",
1668
1800
  target: resolved,
1669
1801
  // 0.85: a strong file signal with one degree of inference,
@@ -1688,11 +1820,11 @@ function resolveTarget(sourceDir, raw) {
1688
1820
  if (trimmed.length === 0) return null;
1689
1821
  if (trimmed.startsWith("/")) return null;
1690
1822
  const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1691
- return pathPosix2.normalize(joined);
1823
+ return pathPosix3.normalize(joined);
1692
1824
  }
1693
1825
 
1694
1826
  // plugins/core/extractors/external-url-counter/index.ts
1695
- var ID5 = "external-url-counter";
1827
+ var ID7 = "external-url-counter";
1696
1828
  var count2 = {
1697
1829
  slot: "card.footer.left",
1698
1830
  icon: "pi-link",
@@ -1712,7 +1844,7 @@ var settings = {
1712
1844
  var URL_RE = /https?:\/\/[^\s<>"'`)\]]+/g;
1713
1845
  var TRAILING_PUNCT = /[.,;:!?]+$/;
1714
1846
  var externalUrlCounterExtractor = {
1715
- id: ID5,
1847
+ id: ID7,
1716
1848
  pluginId: CORE_PLUGIN_ID,
1717
1849
  kind: "extractor",
1718
1850
  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.",
@@ -1764,7 +1896,7 @@ var externalUrlCounterExtractor = {
1764
1896
  raw: original,
1765
1897
  candidates: [
1766
1898
  {
1767
- extractorId: ID5,
1899
+ extractorId: ID7,
1768
1900
  kind: "references",
1769
1901
  target: normalized.href,
1770
1902
  confidence: 0.3,
@@ -1805,12 +1937,12 @@ function normalizeUrl(raw) {
1805
1937
  }
1806
1938
 
1807
1939
  // plugins/core/extractors/markdown-link/index.ts
1808
- import { posix as pathPosix3 } from "path";
1809
- var ID6 = "markdown-link";
1940
+ import { posix as pathPosix4 } from "path";
1941
+ var ID8 = "markdown-link";
1810
1942
  var LINK_RE = /(?<!!)\[([^\]]*)\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g;
1811
1943
  var URL_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;
1812
1944
  var markdownLinkExtractor = {
1813
- id: ID6,
1945
+ id: ID8,
1814
1946
  pluginId: CORE_PLUGIN_ID,
1815
1947
  kind: "extractor",
1816
1948
  description: "Turns markdown links (`[text](path)`) in a node's body into arrows between nodes in the graph. Example: `[the guide](docs/guide.md)` draws an arrow to `docs/guide.md`.",
@@ -1819,7 +1951,7 @@ var markdownLinkExtractor = {
1819
1951
  const seen = /* @__PURE__ */ new Set();
1820
1952
  const body = stripCodeAndHtml(ctx.body);
1821
1953
  const lineStarts = computeLineStarts(body);
1822
- const sourceDir = pathPosix3.dirname(ctx.node.path);
1954
+ const sourceDir = pathPosix4.dirname(ctx.node.path);
1823
1955
  for (const match of body.matchAll(LINK_RE)) {
1824
1956
  const original = match[2];
1825
1957
  const resolved = resolveTarget2(sourceDir, original);
@@ -1835,7 +1967,7 @@ var markdownLinkExtractor = {
1835
1967
  raw: match[0],
1836
1968
  candidates: [
1837
1969
  {
1838
- extractorId: ID6,
1970
+ extractorId: ID8,
1839
1971
  kind: "references",
1840
1972
  target: resolved,
1841
1973
  // 0.95: the `[text](path)` syntax is unambiguous (the spec's
@@ -1866,14 +1998,14 @@ function resolveTarget2(sourceDir, raw) {
1866
1998
  if (URL_SCHEME_RE.test(trimmed)) return null;
1867
1999
  if (trimmed.startsWith("/")) return null;
1868
2000
  const joined = sourceDir === "." ? trimmed : `${sourceDir}/${trimmed}`;
1869
- return pathPosix3.normalize(joined);
2001
+ return pathPosix4.normalize(joined);
1870
2002
  }
1871
2003
 
1872
2004
  // plugins/core/extractors/mcp-tools/index.ts
1873
- var ID7 = "mcp-tools";
2005
+ var ID9 = "mcp-tools";
1874
2006
  var MCP_PATTERN = /^mcp__([a-z0-9][a-z0-9_-]*)__[a-z0-9_-]+$/i;
1875
2007
  var mcpToolsExtractor = {
1876
- id: ID7,
2008
+ id: ID9,
1877
2009
  pluginId: CORE_PLUGIN_ID,
1878
2010
  kind: "extractor",
1879
2011
  description: "Turns `tools: [mcp__<server>__<tool>]` entries in a node's frontmatter into an MCP node per unique server and an arrow from the source to each one. Example: `tools: [mcp__github__create_pr]` adds an `mcp://github` node and an arrow to it.",
@@ -1904,7 +2036,7 @@ var mcpToolsExtractor = {
1904
2036
  raw: `mcp__${server}__*`,
1905
2037
  candidates: [
1906
2038
  {
1907
- extractorId: ID7,
2039
+ extractorId: ID9,
1908
2040
  kind: "references",
1909
2041
  target: mcpPath,
1910
2042
  confidence: 0.85,
@@ -1973,10 +2105,10 @@ var ANNOTATION_FIELD_UNKNOWN_TEXTS = {
1973
2105
  };
1974
2106
 
1975
2107
  // plugins/core/analyzers/annotation-field-unknown/index.ts
1976
- var ID8 = "annotation-field-unknown";
2108
+ var ID10 = "annotation-field-unknown";
1977
2109
  var RESERVED_ROOT_BLOCKS = /* @__PURE__ */ new Set(["identity", "annotations", "settings", "audit"]);
1978
2110
  var annotationFieldUnknownAnalyzer = {
1979
- id: ID8,
2111
+ id: ID10,
1980
2112
  pluginId: CORE_PLUGIN_ID,
1981
2113
  kind: "analyzer",
1982
2114
  description: "Flags typos or unrecognized keys in sidecars (`.sm`).",
@@ -2011,7 +2143,7 @@ var annotationFieldUnknownAnalyzer = {
2011
2143
  for (const key of Object.keys(annotations)) {
2012
2144
  if (!knownAnnotationKeys.has(key)) {
2013
2145
  issues.push({
2014
- analyzerId: ID8,
2146
+ analyzerId: ID10,
2015
2147
  severity: "warn",
2016
2148
  nodeIds: [node.path],
2017
2149
  message: formatFinding({
@@ -2037,7 +2169,7 @@ var annotationFieldUnknownAnalyzer = {
2037
2169
  if (validator(value)) continue;
2038
2170
  const errors = (validator.errors ?? []).map((e) => `${e.instancePath || "(root)"} ${e.message ?? e.keyword}`).join("; ");
2039
2171
  issues.push({
2040
- analyzerId: ID8,
2172
+ analyzerId: ID10,
2041
2173
  severity: "warn",
2042
2174
  nodeIds: [node.path],
2043
2175
  message: formatFinding({
@@ -2052,7 +2184,7 @@ var annotationFieldUnknownAnalyzer = {
2052
2184
  continue;
2053
2185
  }
2054
2186
  issues.push({
2055
- analyzerId: ID8,
2187
+ analyzerId: ID10,
2056
2188
  severity: "warn",
2057
2189
  nodeIds: [node.path],
2058
2190
  message: formatFinding({
@@ -2124,9 +2256,9 @@ var ANNOTATION_ORPHAN_TEXTS = {
2124
2256
  };
2125
2257
 
2126
2258
  // plugins/core/analyzers/annotation-orphan/index.ts
2127
- var ID9 = "annotation-orphan";
2259
+ var ID11 = "annotation-orphan";
2128
2260
  var annotationOrphanAnalyzer = {
2129
- id: ID9,
2261
+ id: ID11,
2130
2262
  pluginId: CORE_PLUGIN_ID,
2131
2263
  kind: "analyzer",
2132
2264
  description: "Flags sidecars (`.sm`) whose `.md` file no longer exists.",
@@ -2138,7 +2270,7 @@ var annotationOrphanAnalyzer = {
2138
2270
  for (const orphan of orphans) {
2139
2271
  const expectedMdRelative = orphan.relativePath.endsWith(".sm") ? `${orphan.relativePath.slice(0, -".sm".length)}.md` : `${orphan.relativePath}.md`;
2140
2272
  issues.push({
2141
- analyzerId: ID9,
2273
+ analyzerId: ID11,
2142
2274
  severity: "warn",
2143
2275
  nodeIds: [expectedMdRelative],
2144
2276
  message: formatFinding({
@@ -2181,7 +2313,7 @@ var ANNOTATION_STALE_TEXTS = {
2181
2313
  };
2182
2314
 
2183
2315
  // plugins/core/analyzers/annotation-stale/index.ts
2184
- var ID10 = "annotation-stale";
2316
+ var ID12 = "annotation-stale";
2185
2317
  var staleIcon = {
2186
2318
  slot: "card.footer.right",
2187
2319
  icon: "pi-clock",
@@ -2194,7 +2326,7 @@ var staleBadge = {
2194
2326
  priority: 20
2195
2327
  };
2196
2328
  var annotationStaleAnalyzer = {
2197
- id: ID10,
2329
+ id: ID12,
2198
2330
  pluginId: CORE_PLUGIN_ID,
2199
2331
  kind: "analyzer",
2200
2332
  description: "Marks sidecars (`.sm`) that are out of date with their `.md`.",
@@ -2214,7 +2346,7 @@ var annotationStaleAnalyzer = {
2214
2346
  const status = staleStatus(node.sidecar);
2215
2347
  if (status === null) continue;
2216
2348
  issues.push({
2217
- analyzerId: ID10,
2349
+ analyzerId: ID12,
2218
2350
  severity: "info",
2219
2351
  nodeIds: [node.path],
2220
2352
  message: formatFinding({ body: messageFor(status) }),
@@ -2260,9 +2392,9 @@ function tooltipFor(status) {
2260
2392
  }
2261
2393
 
2262
2394
  // plugins/core/analyzers/contribution-orphan/index.ts
2263
- var ID11 = "contribution-orphan";
2395
+ var ID13 = "contribution-orphan";
2264
2396
  var contributionOrphanAnalyzer = {
2265
- id: ID11,
2397
+ id: ID13,
2266
2398
  pluginId: CORE_PLUGIN_ID,
2267
2399
  kind: "analyzer",
2268
2400
  description: "Warns about plugin data referencing nodes renamed or deleted in the latest scan.",
@@ -2294,12 +2426,12 @@ var EXTRACTOR_COLLISION_TEXTS = {
2294
2426
  };
2295
2427
 
2296
2428
  // plugins/core/analyzers/extractor-collision/index.ts
2297
- var ID12 = "extractor-collision";
2429
+ var ID14 = "extractor-collision";
2298
2430
  function signalLines(signal) {
2299
2431
  return signal.range && typeof signal.range.line === "number" ? [signal.range.line] : void 0;
2300
2432
  }
2301
2433
  var extractorCollisionAnalyzer = {
2302
- id: ID12,
2434
+ id: ID14,
2303
2435
  pluginId: CORE_PLUGIN_ID,
2304
2436
  kind: "analyzer",
2305
2437
  description: "Reports when two extractors detect something at the same span of body text and the resolver drops one.",
@@ -2323,7 +2455,7 @@ function makeIssue(signal) {
2323
2455
  const loserRange = signal.range ? `${signal.range.start}-${signal.range.end}` : "unknown";
2324
2456
  const winnerRange = `${winner.range.start}-${winner.range.end}`;
2325
2457
  return {
2326
- analyzerId: ID12,
2458
+ analyzerId: ID14,
2327
2459
  severity: "warn",
2328
2460
  nodeIds: [signal.source],
2329
2461
  message: formatFinding({
@@ -2367,7 +2499,7 @@ var ISSUE_COUNTER_TEXTS = {
2367
2499
  };
2368
2500
 
2369
2501
  // plugins/core/analyzers/issue-counter/index.ts
2370
- var ID13 = "issue-counter";
2502
+ var ID15 = "issue-counter";
2371
2503
  var warnCount = {
2372
2504
  slot: "card.footer.right",
2373
2505
  icon: "pi-exclamation-triangle",
@@ -2403,7 +2535,7 @@ function emitTierChips(ctx, ref, severity, counts, singleTooltip, manyTooltip) {
2403
2535
  }
2404
2536
  }
2405
2537
  var issueCounterAnalyzer = {
2406
- id: ID13,
2538
+ id: ID15,
2407
2539
  pluginId: CORE_PLUGIN_ID,
2408
2540
  kind: "analyzer",
2409
2541
  description: "Emits one aggregate severity chip per node (error + warn counts) from the live issue accumulator.",
@@ -2466,7 +2598,7 @@ var LINK_COUNTER_TEXTS = {
2466
2598
  };
2467
2599
 
2468
2600
  // plugins/core/analyzers/link-counter/index.ts
2469
- var ID14 = "link-counter";
2601
+ var ID16 = "link-counter";
2470
2602
  var linksIn = {
2471
2603
  slot: "card.footer.left",
2472
2604
  icon: "pi-download",
@@ -2482,7 +2614,7 @@ var linksOut = {
2482
2614
  priority: 20
2483
2615
  };
2484
2616
  var linkCounterAnalyzer = {
2485
- id: ID14,
2617
+ id: ID16,
2486
2618
  pluginId: CORE_PLUGIN_ID,
2487
2619
  kind: "analyzer",
2488
2620
  description: "Counts incoming and outgoing links per node.",
@@ -2547,10 +2679,10 @@ var LINK_KIND_CONFLICT_TEXTS = {
2547
2679
  };
2548
2680
 
2549
2681
  // plugins/core/analyzers/link-kind-conflict/index.ts
2550
- var ID15 = "link-kind-conflict";
2682
+ var ID17 = "link-kind-conflict";
2551
2683
  var NON_CONFLICTING_KINDS = /* @__PURE__ */ new Set(["points"]);
2552
2684
  var linkKindConflictAnalyzer = {
2553
- id: ID15,
2685
+ id: ID17,
2554
2686
  pluginId: CORE_PLUGIN_ID,
2555
2687
  kind: "analyzer",
2556
2688
  description: "Flags conflicting arrow meanings between extractors (e.g. `references` vs `invokes`).",
@@ -2598,7 +2730,7 @@ var linkKindConflictAnalyzer = {
2598
2730
  const [source, target] = key.split("\0");
2599
2731
  const kindList = variants.map((v) => v.kind).join(" / ");
2600
2732
  issues.push({
2601
- analyzerId: ID15,
2733
+ analyzerId: ID17,
2602
2734
  severity: "warn",
2603
2735
  nodeIds: [source, target],
2604
2736
  message: formatFinding({
@@ -2637,9 +2769,9 @@ var LINK_SELF_LOOP_TEXTS = {
2637
2769
  };
2638
2770
 
2639
2771
  // plugins/core/analyzers/link-self-loop/index.ts
2640
- var ID16 = "link-self-loop";
2772
+ var ID18 = "link-self-loop";
2641
2773
  var linkSelfLoopAnalyzer = {
2642
- id: ID16,
2774
+ id: ID18,
2643
2775
  pluginId: CORE_PLUGIN_ID,
2644
2776
  kind: "analyzer",
2645
2777
  description: "Flags links whose source is also their own resolved target (e.g. a body heading like `# /deploy` inside the file that defines `/deploy`).",
@@ -2649,7 +2781,7 @@ var linkSelfLoopAnalyzer = {
2649
2781
  for (const link of ctx.links) {
2650
2782
  if (!isSelfLoop(link)) continue;
2651
2783
  issues.push({
2652
- analyzerId: ID16,
2784
+ analyzerId: ID18,
2653
2785
  severity: "warn",
2654
2786
  nodeIds: [link.source],
2655
2787
  message: formatFinding({
@@ -2681,9 +2813,9 @@ var NAME_COLLISION_TEXTS = {
2681
2813
  };
2682
2814
 
2683
2815
  // plugins/core/analyzers/name-collision/index.ts
2684
- var ID17 = "name-collision";
2816
+ var ID19 = "name-collision";
2685
2817
  var nameCollisionAnalyzer = {
2686
- id: ID17,
2818
+ id: ID19,
2687
2819
  pluginId: CORE_PLUGIN_ID,
2688
2820
  kind: "analyzer",
2689
2821
  mode: "deterministic",
@@ -2697,7 +2829,7 @@ var nameCollisionAnalyzer = {
2697
2829
  for (const [name, claims] of collisions) {
2698
2830
  const paths = claims.map((c) => c.path);
2699
2831
  issues.push({
2700
- analyzerId: ID17,
2832
+ analyzerId: ID19,
2701
2833
  severity: "error",
2702
2834
  nodeIds: paths,
2703
2835
  message: formatFinding({
@@ -2747,9 +2879,9 @@ var NAME_RESERVED_TEXTS = {
2747
2879
  };
2748
2880
 
2749
2881
  // plugins/core/analyzers/name-reserved/index.ts
2750
- var ID18 = "name-reserved";
2882
+ var ID20 = "name-reserved";
2751
2883
  var nameReservedAnalyzer = {
2752
- id: ID18,
2884
+ id: ID20,
2753
2885
  pluginId: CORE_PLUGIN_ID,
2754
2886
  kind: "analyzer",
2755
2887
  description: "Flags two kinds of reserved-name collision: a file whose name shadows a built-in command of the active runtime, and a link that resolves to one of those reserved names.",
@@ -2767,7 +2899,7 @@ var nameReservedAnalyzer = {
2767
2899
  const node = byPath3.get(path);
2768
2900
  if (!node) continue;
2769
2901
  issues.push({
2770
- analyzerId: ID18,
2902
+ analyzerId: ID20,
2771
2903
  severity: "warn",
2772
2904
  nodeIds: [node.path],
2773
2905
  message: formatFinding({
@@ -2789,7 +2921,7 @@ var nameReservedAnalyzer = {
2789
2921
  adjust(link, { kind: "delta", value: -RESERVED_PENALTY });
2790
2922
  }
2791
2923
  issues.push({
2792
- analyzerId: ID18,
2924
+ analyzerId: ID20,
2793
2925
  severity: "warn",
2794
2926
  nodeIds: [link.source],
2795
2927
  message: formatFinding({
@@ -2841,7 +2973,7 @@ var NODE_STABILITY_TEXTS = {
2841
2973
  };
2842
2974
 
2843
2975
  // plugins/core/analyzers/node-stability/index.ts
2844
- var ID19 = "node-stability";
2976
+ var ID21 = "node-stability";
2845
2977
  var EXPERIMENTAL_TOOLTIP = "Experimental: API may change";
2846
2978
  var DEPRECATED_TOOLTIP = "Deprecated: avoid in new code";
2847
2979
  var experimental = {
@@ -2859,7 +2991,7 @@ var deprecated = {
2859
2991
  priority: 10
2860
2992
  };
2861
2993
  var nodeStabilityAnalyzer = {
2862
- id: ID19,
2994
+ id: ID21,
2863
2995
  pluginId: CORE_PLUGIN_ID,
2864
2996
  kind: "analyzer",
2865
2997
  description: "Surfaces a node's stability stage on the card: `deprecated` as a chip plus a finding, `experimental` as a chip only; `stable` and unset stay silent.",
@@ -2881,7 +3013,7 @@ var nodeStabilityAnalyzer = {
2881
3013
  severity: "warn"
2882
3014
  });
2883
3015
  issues.push({
2884
- analyzerId: ID19,
3016
+ analyzerId: ID21,
2885
3017
  severity: "warn",
2886
3018
  nodeIds: [node.path],
2887
3019
  message: formatFinding({ body: tx(NODE_STABILITY_TEXTS.deprecated) }),
@@ -2933,9 +3065,9 @@ var REFERENCE_BROKEN_TEXTS = {
2933
3065
  };
2934
3066
 
2935
3067
  // plugins/core/analyzers/reference-broken/index.ts
2936
- var ID20 = "reference-broken";
3068
+ var ID22 = "reference-broken";
2937
3069
  var referenceBrokenAnalyzer = {
2938
- id: ID20,
3070
+ id: ID22,
2939
3071
  pluginId: CORE_PLUGIN_ID,
2940
3072
  kind: "analyzer",
2941
3073
  description: "Flags arrows pointing at a node not part of the current scan.",
@@ -2978,7 +3110,7 @@ function penalizeBrokenConfidence(adjust, link) {
2978
3110
  }
2979
3111
  function buildIssue(link) {
2980
3112
  return {
2981
- analyzerId: ID20,
3113
+ analyzerId: ID22,
2982
3114
  // `error`, not `warn`: a link whose target is not in the scan is a
2983
3115
  // structural defect the operator must notice, and the card chip
2984
3116
  // paints `danger` (red) to match. Per the chip-vs-issue policy in
@@ -3042,9 +3174,9 @@ var REFERENCE_REDUNDANT_TEXTS = {
3042
3174
  };
3043
3175
 
3044
3176
  // plugins/core/analyzers/reference-redundant/index.ts
3045
- var ID21 = "reference-redundant";
3177
+ var ID23 = "reference-redundant";
3046
3178
  var referenceRedundantAnalyzer = {
3047
- id: ID21,
3179
+ id: ID23,
3048
3180
  pluginId: CORE_PLUGIN_ID,
3049
3181
  kind: "analyzer",
3050
3182
  description: "Flags when one node references the same target through two or more different links (e.g. a markdown link plus a `references:` entry).",
@@ -3067,7 +3199,7 @@ var referenceRedundantAnalyzer = {
3067
3199
  const [source, resolvedTarget] = key.split("\0");
3068
3200
  const flat = flattenOccurrences(links);
3069
3201
  issues.push({
3070
- analyzerId: ID21,
3202
+ analyzerId: ID23,
3071
3203
  severity: "info",
3072
3204
  nodeIds: [source],
3073
3205
  message: formatFinding({
@@ -3403,9 +3535,9 @@ var SCHEMA_VIOLATION_TEXTS = {
3403
3535
  };
3404
3536
 
3405
3537
  // plugins/core/analyzers/schema-violation/index.ts
3406
- var ID22 = "schema-violation";
3538
+ var ID24 = "schema-violation";
3407
3539
  var schemaViolationAnalyzer = {
3408
- id: ID22,
3540
+ id: ID24,
3409
3541
  pluginId: CORE_PLUGIN_ID,
3410
3542
  kind: "analyzer",
3411
3543
  description: "Flags nodes or links that violate the project schemas.",
@@ -3455,7 +3587,7 @@ function collectNodeFindings(v, node, out) {
3455
3587
  const result = v.validate("node", toNodeForSchema(node));
3456
3588
  if (result.ok) return;
3457
3589
  out.push({
3458
- analyzerId: ID22,
3590
+ analyzerId: ID24,
3459
3591
  severity: "error",
3460
3592
  nodeIds: [node.path],
3461
3593
  message: formatFinding({
@@ -3470,7 +3602,7 @@ function collectLinkFindings(v, link, out) {
3470
3602
  const result = v.validate("link", toLinkForSchema(link));
3471
3603
  if (result.ok) return;
3472
3604
  out.push({
3473
- analyzerId: ID22,
3605
+ analyzerId: ID24,
3474
3606
  severity: "error",
3475
3607
  nodeIds: [link.source],
3476
3608
  message: formatFinding({
@@ -3539,13 +3671,13 @@ var ASCII_FORMATTER_TEXTS = {
3539
3671
  };
3540
3672
 
3541
3673
  // plugins/core/formatters/ascii/index.ts
3542
- var ID23 = "ascii";
3674
+ var ID25 = "ascii";
3543
3675
  var KIND_ORDER = ["agent", "command", "skill", "markdown"];
3544
3676
  var asciiFormatter = {
3545
- id: ID23,
3677
+ id: ID25,
3546
3678
  pluginId: CORE_PLUGIN_ID,
3547
3679
  kind: "formatter",
3548
- formatId: ID23,
3680
+ formatId: ID25,
3549
3681
  description: "Renders the scan as plain text in three sections: nodes (grouped by kind), arrows, and issues. Used by `sm scan --format ascii`.",
3550
3682
  // ASCII tree formatter, header + per-kind sections + per-issue
3551
3683
  // section. Each section iterates and renders; splitting per section
@@ -3639,13 +3771,13 @@ function renderSection(out, kind, group) {
3639
3771
  }
3640
3772
 
3641
3773
  // plugins/core/formatters/json/index.ts
3642
- var ID24 = "json";
3774
+ var ID26 = "json";
3643
3775
  var jsonFormatter = {
3644
- id: ID24,
3776
+ id: ID26,
3645
3777
  pluginId: CORE_PLUGIN_ID,
3646
3778
  kind: "formatter",
3647
3779
  description: "Renders the persisted scan as JSON (conforms to `scan-result.schema.json`). Used by `sm graph --format json` and `GET /api/graph?format=json`.",
3648
- formatId: ID24,
3780
+ formatId: ID26,
3649
3781
  format(ctx) {
3650
3782
  if (ctx.scanResult !== void 0) {
3651
3783
  return JSON.stringify(ctx.scanResult);
@@ -3792,13 +3924,13 @@ var BUMP_TEXTS = {
3792
3924
  };
3793
3925
 
3794
3926
  // plugins/core/actions/node-bump/index.ts
3795
- var ID25 = "node-bump";
3927
+ var ID27 = "node-bump";
3796
3928
  var bumpButton = {
3797
3929
  slot: "inspector.action.button",
3798
3930
  priority: 10
3799
3931
  };
3800
3932
  var nodeBumpAction = {
3801
- id: ID25,
3933
+ id: ID27,
3802
3934
  pluginId: CORE_PLUGIN_ID,
3803
3935
  kind: "action",
3804
3936
  description: "Marks a node as updated: bumps `annotations.version`, refreshes sidecar hashes, and records the timestamp.",
@@ -3901,13 +4033,13 @@ var NODE_SET_STABILITY_TEXTS = {
3901
4033
  };
3902
4034
 
3903
4035
  // plugins/core/actions/node-set-stability/index.ts
3904
- var ID26 = "node-set-stability";
4036
+ var ID28 = "node-set-stability";
3905
4037
  var setStabilityButton = {
3906
4038
  slot: "inspector.action.button",
3907
4039
  priority: 15
3908
4040
  };
3909
4041
  var nodeSetStabilityAction = {
3910
- id: ID26,
4042
+ id: ID28,
3911
4043
  pluginId: CORE_PLUGIN_ID,
3912
4044
  kind: "action",
3913
4045
  description: "Sets the lifecycle stage of the current node (writes `stability` to the sidecar).",
@@ -3979,9 +4111,9 @@ function invokeSetStability(input, ctx) {
3979
4111
  }
3980
4112
 
3981
4113
  // plugins/core/actions/node-set-tags/index.ts
3982
- var ID27 = "node-set-tags";
4114
+ var ID29 = "node-set-tags";
3983
4115
  var nodeSetTagsAction = {
3984
- id: ID27,
4116
+ id: ID29,
3985
4117
  pluginId: CORE_PLUGIN_ID,
3986
4118
  kind: "action",
3987
4119
  description: "Sets the taxonomy tags of the current node (writes `tags` to the sidecar; whole-array replace).",
@@ -4523,6 +4655,8 @@ var slashCommandExtractor2 = { ...slashCommandExtractor, pluginId: "claude", ver
4523
4655
  var toolsCounterExtractor2 = { ...toolsCounterExtractor, pluginId: "claude", version: VERSION };
4524
4656
  var antigravityProvider2 = { ...antigravityProvider, pluginId: "antigravity", version: VERSION };
4525
4657
  var codexProvider2 = { ...codexProvider, pluginId: "codex", version: VERSION };
4658
+ var atFileExtractor2 = { ...atFileExtractor, pluginId: "codex", version: VERSION };
4659
+ var dollarSkillExtractor2 = { ...dollarSkillExtractor, pluginId: "codex", version: VERSION };
4526
4660
  var agentSkillsProvider2 = { ...agentSkillsProvider, pluginId: "agent-skills", version: VERSION };
4527
4661
  var coreMarkdownProvider2 = { ...coreMarkdownProvider, pluginId: "core", version: VERSION };
4528
4662
  var backtickPathExtractor2 = { ...backtickPathExtractor, pluginId: "core", version: VERSION };
@@ -4572,7 +4706,9 @@ var builtInPlugins = [
4572
4706
  id: "codex",
4573
4707
  description: "OpenAI Codex CLI platform integration. Classifies TOML sub-agent definitions under `.codex/agents/*.toml`.",
4574
4708
  extensions: [
4575
- codexProvider2
4709
+ codexProvider2,
4710
+ atFileExtractor2,
4711
+ dollarSkillExtractor2
4576
4712
  ]
4577
4713
  },
4578
4714
  {
@@ -5551,7 +5687,8 @@ var defaults_default = {
5551
5687
  // kernel/config/loader.ts
5552
5688
  var PROJECT_LOCAL_ONLY_KEYS = /* @__PURE__ */ new Set([
5553
5689
  "allowEditSmFiles",
5554
- "scan.referencePaths"
5690
+ "scan.referencePaths",
5691
+ "pluginTrust.projectEnabled"
5555
5692
  ]);
5556
5693
  var DEFAULTS = defaults_default;
5557
5694
  function loadConfig(opts) {
@@ -5832,7 +5969,8 @@ function enumerateConfigPaths(obj, prefix = "") {
5832
5969
 
5833
5970
  // core/config/helper.ts
5834
5971
  var PRIVACY_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
5835
- "scan.referencePaths"
5972
+ "scan.referencePaths",
5973
+ "pluginTrust.projectEnabled"
5836
5974
  ]);
5837
5975
  var ProjectLocalOnlyKeyError = class extends Error {
5838
5976
  constructor(key) {
@@ -5919,6 +6057,14 @@ function projectPathExposure(inputs) {
5919
6057
  if (exposed.length === 0) return empty;
5920
6058
  return { expandsSurface: true, exposedPaths: exposed };
5921
6059
  }
6060
+ function projectTrustExposure(inputs) {
6061
+ if (inputs.value !== true) return { expandsSurface: false };
6062
+ const before = readConfigValue("pluginTrust.projectEnabled", {
6063
+ cwd: inputs.cwd,
6064
+ default: false
6065
+ }) ?? false;
6066
+ return { expandsSurface: before !== true };
6067
+ }
5922
6068
  function resolveScanPathForExposure(raw, cwd) {
5923
6069
  if (raw.startsWith("~/")) return resolve7(join4(osHomedir(), raw.slice(2)));
5924
6070
  if (raw === "~") return resolve7(osHomedir());
@@ -7567,40 +7713,38 @@ function formatMigrationName(m) {
7567
7713
  }
7568
7714
 
7569
7715
  // kernel/adapters/sqlite/plugins.ts
7570
- async function setPluginEnabled(db, pluginId, enabled, now = Date.now()) {
7716
+ async function setPluginTrusted(db, pluginId, trusted, now = Date.now()) {
7571
7717
  await db.insertInto("config_plugins").values({
7572
7718
  pluginId,
7573
- enabled: enabled ? 1 : 0,
7574
- configJson: null,
7719
+ trusted: trusted ? 1 : 0,
7575
7720
  updatedAt: now
7576
7721
  }).onConflict(
7577
7722
  (oc) => oc.column("pluginId").doUpdateSet({
7578
- enabled: enabled ? 1 : 0,
7723
+ trusted: trusted ? 1 : 0,
7579
7724
  updatedAt: now
7580
7725
  })
7581
7726
  ).execute();
7582
7727
  }
7583
- async function getPluginEnabled(db, pluginId) {
7584
- const row = await db.selectFrom("config_plugins").select(["enabled"]).where("pluginId", "=", pluginId).executeTakeFirst();
7728
+ async function getPluginTrusted(db, pluginId) {
7729
+ const row = await db.selectFrom("config_plugins").select(["trusted"]).where("pluginId", "=", pluginId).executeTakeFirst();
7585
7730
  if (!row) return void 0;
7586
- return row.enabled === 1;
7731
+ return row.trusted === 1;
7587
7732
  }
7588
- async function listPluginOverrides(db) {
7589
- const rows = await db.selectFrom("config_plugins").select(["pluginId", "enabled", "configJson", "updatedAt"]).orderBy("pluginId", "asc").execute();
7733
+ async function listPluginTrust(db) {
7734
+ const rows = await db.selectFrom("config_plugins").select(["pluginId", "trusted", "updatedAt"]).orderBy("pluginId", "asc").execute();
7590
7735
  return rows.map((r) => ({
7591
7736
  pluginId: r.pluginId,
7592
- enabled: r.enabled === 1,
7593
- configJson: r.configJson,
7737
+ trusted: r.trusted === 1,
7594
7738
  updatedAt: r.updatedAt
7595
7739
  }));
7596
7740
  }
7597
- async function deletePluginOverride(db, pluginId) {
7741
+ async function deletePluginTrust(db, pluginId) {
7598
7742
  await db.deleteFrom("config_plugins").where("pluginId", "=", pluginId).execute();
7599
7743
  }
7600
- async function loadPluginOverrideMap(db) {
7601
- const rows = await listPluginOverrides(db);
7744
+ async function loadPluginTrustMap(db) {
7745
+ const rows = await listPluginTrust(db);
7602
7746
  const out = /* @__PURE__ */ new Map();
7603
- for (const row of rows) out.set(row.pluginId, row.enabled);
7747
+ for (const row of rows) out.set(row.pluginId, row.trusted);
7604
7748
  return out;
7605
7749
  }
7606
7750
 
@@ -8755,7 +8899,7 @@ var SqliteStorageAdapter = class {
8755
8899
  jobs;
8756
8900
  favorites;
8757
8901
  preferences;
8758
- pluginConfig;
8902
+ trust;
8759
8903
  migrations;
8760
8904
  pluginMigrations;
8761
8905
  constructor(options) {
@@ -8872,12 +9016,12 @@ var SqliteStorageAdapter = class {
8872
9016
  loadUpdateCheckCache: () => loadUpdateCheckCache(this.db),
8873
9017
  saveUpdateCheckCache: (cache) => saveUpdateCheckCache(this.db, cache)
8874
9018
  };
8875
- this.pluginConfig = {
8876
- set: (pluginId, enabled) => setPluginEnabled(this.db, pluginId, enabled),
8877
- get: (pluginId) => getPluginEnabled(this.db, pluginId),
8878
- list: () => listPluginOverrides(this.db),
8879
- delete: (pluginId) => deletePluginOverride(this.db, pluginId),
8880
- loadOverrideMap: () => loadPluginOverrideMap(this.db)
9019
+ this.trust = {
9020
+ set: (pluginId, trusted) => setPluginTrusted(this.db, pluginId, trusted),
9021
+ get: (pluginId) => getPluginTrusted(this.db, pluginId),
9022
+ list: () => listPluginTrust(this.db),
9023
+ delete: (pluginId) => deletePluginTrust(this.db, pluginId),
9024
+ loadTrustMap: () => loadPluginTrustMap(this.db)
8881
9025
  };
8882
9026
  const path = this.#options.databasePath;
8883
9027
  this.migrations = {
@@ -10064,21 +10208,22 @@ var PLUGIN_LOADER_TEXTS = {
10064
10208
  // schema. Both are GitHub blob URLs.
10065
10209
  invalidManifestExtensionShape: "{{relEntry}}: {{errors}}. See {{docUrl}}.",
10066
10210
  importExceededTimeout: "import exceeded {{timeoutMs}}ms; likely a top-level side effect (network call, infinite loop, large blocking work). Move side effects into the runtime methods (`detect` / `evaluate` / `render` / etc.).",
10067
- disabledByConfig: "disabled by config_plugins or settings.json",
10211
+ disabledByConfig: "disabled by settings.json (plugins.<id>.enabled)",
10068
10212
  /**
10069
10213
  * Reason stamped on a project-local disk plugin discovered but not
10070
- * imported because the operator never granted local trust. Distinct
10071
- * from `disabledByConfig` (an explicit toggle-off): this id has no
10072
- * `config_plugins` override at all, so its code stays unexecuted until
10073
- * `sm plugins enable` records local intent.
10214
+ * imported because the operator never granted local import trust.
10215
+ * Distinct from `disabledByConfig` (an explicit operational toggle-off
10216
+ * in the config layers): this id is enabled but carries no
10217
+ * `config_plugins` trust grant, so its code stays unexecuted until
10218
+ * `sm plugins trust` records local consent.
10074
10219
  */
10075
- untrustedNotLoaded: "not loaded: project-local plugin is untrusted until enabled. Run `sm plugins enable {{pluginId}}` to load it.",
10220
+ untrustedNotLoaded: "not loaded: project-local plugin is enabled but not trusted on this machine. Run `sm plugins trust {{pluginId}}` to load it.",
10076
10221
  /**
10077
10222
  * One-time aggregate notice the runtime emits when project-local
10078
10223
  * plugins were found on disk but left unloaded for lack of trust. The
10079
10224
  * `{{count}}` plugins ride the scan without executing any code.
10080
10225
  */
10081
- untrustedPluginsFoundNotice: "{{count}} project-local plugin(s) found in .skill-map/plugins/ but not loaded (untrusted). Their code did NOT run. Review with `sm plugins list`, then enable any you trust with `sm plugins enable <id>`.",
10226
+ untrustedPluginsFoundNotice: "{{count}} project-local plugin(s) found in .skill-map/plugins/ but not loaded (untrusted). Their code did NOT run. Review with `sm plugins list`, then trust any you vetted with `sm plugins trust <id>`.",
10082
10227
  invalidManifestDirMismatch: "directory name '{{dirName}}' does not match manifest id '{{manifestId}}'. Rename the directory to match the id, or update the manifest id to match the directory.",
10083
10228
  idCollision: "Plugin '{{id}}' at {{pathA}} collides with the plugin at {{pathB}}. Rename one and rerun.",
10084
10229
  loadErrorPluginIdMismatch: "{{relEntry}}: extension declares pluginId '{{declared}}' but its plugin.json declares id '{{manifestId}}'. Remove the explicit pluginId from the extension; the loader injects it from plugin.json#/id.",
@@ -11031,26 +11176,33 @@ var SHIPS_DISABLED = /* @__PURE__ */ new Set([
11031
11176
  function installedDefaultEnabled(stability) {
11032
11177
  return stability === void 0 || !SHIPS_DISABLED.has(stability);
11033
11178
  }
11034
- function resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault = true) {
11179
+ function resolvePluginEnabled(pluginId, cfg, installedDefault = true) {
11035
11180
  if (isPluginLocked(pluginId)) return true;
11036
- if (dbOverrides.has(pluginId)) return dbOverrides.get(pluginId) === true;
11181
+ const slash = pluginId.indexOf("/");
11182
+ if (slash >= 0) {
11183
+ return resolveQualifiedEnabled(
11184
+ pluginId.slice(0, slash),
11185
+ pluginId.slice(slash + 1),
11186
+ cfg,
11187
+ installedDefault
11188
+ );
11189
+ }
11037
11190
  const settingsEntry = cfg.plugins[pluginId];
11038
11191
  if (settingsEntry?.enabled !== void 0) return settingsEntry.enabled;
11039
11192
  return installedDefault;
11040
11193
  }
11041
- function makeEnabledResolver(cfg, dbOverrides) {
11042
- return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault);
11194
+ function resolveQualifiedEnabled(plugin, ext, cfg, installedDefault) {
11195
+ const pluginEntry = cfg.plugins[plugin];
11196
+ const perExt = pluginEntry?.extensions?.[ext]?.enabled;
11197
+ if (perExt !== void 0) return perExt;
11198
+ if (pluginEntry?.enabled !== void 0) return pluginEntry.enabled;
11199
+ return installedDefault;
11043
11200
  }
11044
- function makeImportTrustResolver(dbOverrides) {
11045
- return (pluginId) => {
11046
- if (isPluginLocked(pluginId)) return true;
11047
- const prefix = `${pluginId}/`;
11048
- for (const [key, enabled] of dbOverrides) {
11049
- if (!enabled) continue;
11050
- if (key === pluginId || key.startsWith(prefix)) return true;
11051
- }
11052
- return false;
11053
- };
11201
+ function makeEnabledResolver(cfg) {
11202
+ return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, installedDefault);
11203
+ }
11204
+ function makeTrustResolver(trustMap, trustProjectEnabled) {
11205
+ return (pluginId) => isPluginLocked(pluginId) || trustMap.get(pluginId) === true || trustProjectEnabled;
11054
11206
  }
11055
11207
 
11056
11208
  // core/runtime/plugin-runtime/resolver.ts
@@ -11075,11 +11227,15 @@ async function buildResolverInputs(ctx) {
11075
11227
  db: void 0,
11076
11228
  ...ctx
11077
11229
  });
11078
- const dbOverrides = await tryWithSqlite(
11230
+ const trustMap = await tryWithSqlite(
11079
11231
  { databasePath: dbPath, autoBackup: false },
11080
- (adapter) => adapter.pluginConfig.loadOverrideMap()
11232
+ (adapter) => adapter.trust.loadTrustMap()
11081
11233
  ) ?? /* @__PURE__ */ new Map();
11082
- return { resolveEnabled: makeEnabledResolver(cfg, dbOverrides), dbOverrides };
11234
+ return {
11235
+ resolveEnabled: makeEnabledResolver(cfg),
11236
+ trustMap,
11237
+ trustProjectEnabled: cfg.pluginTrust?.projectEnabled ?? false
11238
+ };
11083
11239
  }
11084
11240
 
11085
11241
  // kernel/scan/walk-content.ts
@@ -11616,11 +11772,13 @@ async function loadPluginRuntime(opts = {}) {
11616
11772
  const searchPaths = resolveSearchPaths(opts, ctx);
11617
11773
  const validators = loadSchemaValidators();
11618
11774
  let resolveEnabled;
11619
- let dbOverrides;
11775
+ let trustMap;
11776
+ let trustProjectEnabled;
11620
11777
  try {
11621
11778
  const inputs = await buildResolverInputs(ctx);
11622
11779
  resolveEnabled = inputs.resolveEnabled;
11623
- dbOverrides = inputs.dbOverrides;
11780
+ trustMap = inputs.trustMap;
11781
+ trustProjectEnabled = inputs.trustProjectEnabled;
11624
11782
  } catch {
11625
11783
  }
11626
11784
  const loaderOpts = {
@@ -11630,7 +11788,10 @@ async function loadPluginRuntime(opts = {}) {
11630
11788
  };
11631
11789
  if (resolveEnabled) loaderOpts.resolveEnabled = resolveEnabled;
11632
11790
  if (!opts.pluginDir) {
11633
- loaderOpts.resolveImportTrust = makeImportTrustResolver(dbOverrides ?? /* @__PURE__ */ new Map());
11791
+ loaderOpts.resolveImportTrust = makeTrustResolver(
11792
+ trustMap ?? /* @__PURE__ */ new Map(),
11793
+ trustProjectEnabled ?? false
11794
+ );
11634
11795
  }
11635
11796
  const loader = createPluginLoader(loaderOpts);
11636
11797
  const discovered = await loader.discoverAndLoadAll();
@@ -12151,14 +12312,16 @@ import { existsSync as existsSync16 } from "fs";
12151
12312
  import { join as join10 } from "path";
12152
12313
  function detectProvidersFromFilesystem(cwd, providers) {
12153
12314
  const seen = /* @__PURE__ */ new Set();
12154
- const out = [];
12315
+ const matched = [];
12155
12316
  for (const provider of providers) {
12156
12317
  if (seen.has(provider.id)) continue;
12157
12318
  if (!isDetectableUnderCwd(cwd, provider)) continue;
12158
12319
  seen.add(provider.id);
12159
- out.push(provider.id);
12320
+ matched.push(provider);
12160
12321
  }
12161
- return out;
12322
+ const hasVendor = matched.some((p) => p.detect?.fallback !== true);
12323
+ const kept = hasVendor ? matched.filter((p) => p.detect?.fallback !== true) : matched;
12324
+ return kept.map((p) => p.id);
12162
12325
  }
12163
12326
  function isDetectableUnderCwd(cwd, provider) {
12164
12327
  if (!installedDefaultEnabled(provider.stability)) return false;
@@ -12269,6 +12432,18 @@ var CONFIG_TEXTS = {
12269
12432
  * screen what they just opted into.
12270
12433
  */
12271
12434
  privacyGateConfirmed: '{{glyph}} Opening disk access for "{{key}}":\n{{paths}}\n',
12435
+ /**
12436
+ * Surfaced when `sm config set pluginTrust.projectEnabled true` is run
12437
+ * without `--yes`. Turning the opt-in on expands the LOCAL
12438
+ * code-execution surface (every plugin the project enables becomes
12439
+ * trusted), so the verb refuses without confirmation.
12440
+ */
12441
+ trustGateRequired: '{{glyph}} sm config: setting "pluginTrust.projectEnabled" to true trusts every plugin this project enables.\n Their code may then import and run on this machine without a per-plugin trust grant.\n {{hint}}\n',
12442
+ trustGateRequiredHint: "Rerun with --yes to confirm. Turning it off needs no flag. Prefer per-plugin `sm plugins trust <id>` for narrower consent.",
12443
+ /**
12444
+ * Receipt printed when the trust gate has been confirmed via `--yes`.
12445
+ */
12446
+ trustGateConfirmed: "{{glyph}} Local plugin trust opt-in enabled: every plugin this project enables is now trusted on this machine.\n",
12272
12447
  /**
12273
12448
  * Confirmation printed after `sm config set activeProvider <id>`
12274
12449
  * succeeds. The lens change atomically drops the scan_* zone (per
@@ -12665,7 +12840,7 @@ var ConfigSetCommand = class extends SmCommand {
12665
12840
  key = Option4.String({ required: true });
12666
12841
  value = Option4.String({ required: true });
12667
12842
  yes = Option4.Boolean("--yes", false, {
12668
- description: "Confirm a privacy-sensitive write that opens disk access outside the project (scan.referencePaths)."
12843
+ description: "Confirm a surface-expanding write: disk access outside the project (scan.referencePaths) or blanket local plugin trust (pluginTrust.projectEnabled)."
12669
12844
  });
12670
12845
  // CLI orchestrator: each branch is one validation gate (forbidden
12671
12846
  // segment / privacy guard / schema violation) or output dispatch.
@@ -12678,32 +12853,12 @@ var ConfigSetCommand = class extends SmCommand {
12678
12853
  const stderrAnsi = this.ansiFor("stderr");
12679
12854
  const errGlyph = stderrAnsi.red("\u2715");
12680
12855
  const value = parseCliValue(this.value);
12681
- if (PRIVACY_SENSITIVE_KEYS.has(this.key)) {
12682
- const exposure = projectPathExposure({
12683
- key: this.key,
12684
- value,
12685
- cwd: ctx.cwd
12686
- });
12687
- if (exposure.expandsSurface && !this.yes) {
12688
- this.printer.info(
12689
- tx(CONFIG_TEXTS.privacyGateRequired, {
12690
- glyph: errGlyph,
12691
- key: this.key,
12692
- paths: exposure.exposedPaths.map((p) => ` - ${p}`).join("\n"),
12693
- hint: stderrAnsi.dim(CONFIG_TEXTS.privacyGateRequiredHint)
12694
- })
12695
- );
12696
- return ExitCode.Error;
12697
- }
12698
- if (exposure.expandsSurface) {
12699
- this.printer.info(
12700
- tx(CONFIG_TEXTS.privacyGateConfirmed, {
12701
- glyph: stderrAnsi.dim("\u24D8"),
12702
- key: this.key,
12703
- paths: exposure.exposedPaths.map((p) => ` - ${p}`).join("\n")
12704
- })
12705
- );
12706
- }
12856
+ if (this.key === "pluginTrust.projectEnabled") {
12857
+ const trustGate = this.#applyTrustGate(value, ctx.cwd, errGlyph, stderrAnsi);
12858
+ if (trustGate !== null) return trustGate;
12859
+ } else if (PRIVACY_SENSITIVE_KEYS.has(this.key)) {
12860
+ const pathGate = this.#applyPathGate(value, ctx.cwd, errGlyph, stderrAnsi);
12861
+ if (pathGate !== null) return pathGate;
12707
12862
  }
12708
12863
  if (this.key === "activeProvider" && typeof value === "string") {
12709
12864
  const known = new Set(builtIns().providers.map((p) => p.id));
@@ -12781,6 +12936,58 @@ var ConfigSetCommand = class extends SmCommand {
12781
12936
  }
12782
12937
  return ExitCode.Ok;
12783
12938
  }
12939
+ /**
12940
+ * Disk-access privacy gate for `scan.referencePaths`-style keys.
12941
+ * Returns an exit code to bail with (gate refused) or `null` to
12942
+ * proceed. On a confirmed (`--yes`) expansion it prints the receipt
12943
+ * and returns `null`.
12944
+ */
12945
+ #applyPathGate(value, cwd, errGlyph, stderrAnsi) {
12946
+ const exposure = projectPathExposure({ key: this.key, value, cwd });
12947
+ if (!exposure.expandsSurface) return null;
12948
+ if (!this.yes) {
12949
+ this.printer.info(
12950
+ tx(CONFIG_TEXTS.privacyGateRequired, {
12951
+ glyph: errGlyph,
12952
+ key: this.key,
12953
+ paths: exposure.exposedPaths.map((p) => ` - ${p}`).join("\n"),
12954
+ hint: stderrAnsi.dim(CONFIG_TEXTS.privacyGateRequiredHint)
12955
+ })
12956
+ );
12957
+ return ExitCode.Error;
12958
+ }
12959
+ this.printer.info(
12960
+ tx(CONFIG_TEXTS.privacyGateConfirmed, {
12961
+ glyph: stderrAnsi.dim("\u24D8"),
12962
+ key: this.key,
12963
+ paths: exposure.exposedPaths.map((p) => ` - ${p}`).join("\n")
12964
+ })
12965
+ );
12966
+ return null;
12967
+ }
12968
+ /**
12969
+ * Code-execution-surface gate for `pluginTrust.projectEnabled`.
12970
+ * Turning the local opt-in ON trusts every plugin the project enables,
12971
+ * so it requires `--yes`. Returns an exit code to bail with, or `null`
12972
+ * to proceed (on a confirmed expansion it prints the receipt).
12973
+ */
12974
+ #applyTrustGate(value, cwd, errGlyph, stderrAnsi) {
12975
+ const exposure = projectTrustExposure({ value, cwd });
12976
+ if (!exposure.expandsSurface) return null;
12977
+ if (!this.yes) {
12978
+ this.printer.info(
12979
+ tx(CONFIG_TEXTS.trustGateRequired, {
12980
+ glyph: errGlyph,
12981
+ hint: stderrAnsi.dim(CONFIG_TEXTS.trustGateRequiredHint)
12982
+ })
12983
+ );
12984
+ return ExitCode.Error;
12985
+ }
12986
+ this.printer.info(
12987
+ tx(CONFIG_TEXTS.trustGateConfirmed, { glyph: stderrAnsi.dim("\u24D8") })
12988
+ );
12989
+ return null;
12990
+ }
12784
12991
  /**
12785
12992
  * Side effect of `sm config set activeProvider <id>`, atomically
12786
12993
  * drops the `scan_*` zone so the persisted graph never reflects the
@@ -13101,7 +13308,7 @@ function grantFixturePluginTrust(scope, binary, env) {
13101
13308
  const db = new DatabaseSync7(dbPath);
13102
13309
  try {
13103
13310
  const stmt = db.prepare(
13104
- "INSERT INTO config_plugins (plugin_id, enabled, updated_at) VALUES (?, 1, 0) ON CONFLICT(plugin_id) DO UPDATE SET enabled = 1"
13311
+ "INSERT INTO config_plugins (plugin_id, trusted, updated_at) VALUES (?, 1, 0) ON CONFLICT(plugin_id) DO UPDATE SET trusted = 1"
13105
13312
  );
13106
13313
  for (const id of ids) stmt.run(id);
13107
13314
  } finally {
@@ -17185,7 +17392,7 @@ function classifyLinkSource(source, shortIdToQualified, cachedQualifiedIds, appl
17185
17392
  }
17186
17393
 
17187
17394
  // kernel/orchestrator/node-identifiers.ts
17188
- import { posix as pathPosix4 } from "path";
17395
+ import { posix as pathPosix5 } from "path";
17189
17396
  function deriveNodeIdentifiers(node, kindDescriptor) {
17190
17397
  const sources = kindDescriptor?.identifiers;
17191
17398
  if (!sources || sources.length === 0) return [];
@@ -17209,16 +17416,16 @@ function readFrontmatterName(node) {
17209
17416
  return raw.length > 0 ? raw : null;
17210
17417
  }
17211
17418
  function readFilenameBasename(node) {
17212
- const base = pathPosix4.basename(node.path);
17419
+ const base = pathPosix5.basename(node.path);
17213
17420
  if (!base) return null;
17214
- const ext = pathPosix4.extname(base);
17421
+ const ext = pathPosix5.extname(base);
17215
17422
  const stem = ext ? base.slice(0, -ext.length) : base;
17216
17423
  return stem.length > 0 ? stem : null;
17217
17424
  }
17218
17425
  function readDirname(node) {
17219
- const dir = pathPosix4.dirname(node.path);
17426
+ const dir = pathPosix5.dirname(node.path);
17220
17427
  if (!dir || dir === "." || dir === "/") return null;
17221
- const base = pathPosix4.basename(dir);
17428
+ const base = pathPosix5.basename(dir);
17222
17429
  return base.length > 0 ? base : null;
17223
17430
  }
17224
17431
  function collectNameCollisions(nodes, kindRegistry) {
@@ -17315,7 +17522,7 @@ function lookupAllowedKinds(link, _indexes, ctx) {
17315
17522
  }
17316
17523
  function stripTriggerSigil(normalized) {
17317
17524
  if (!normalized) return null;
17318
- const trimmed = normalized.replace(/^[/@]/, "").trim();
17525
+ const trimmed = normalized.replace(/^[/@$]/, "").trim();
17319
17526
  return trimmed.length === 0 ? null : trimmed;
17320
17527
  }
17321
17528
  function indexNode(node, ctx, byName) {
@@ -21780,6 +21987,23 @@ var PLUGINS_TEXTS = {
21780
21987
  toggleAppliedSingle: "{{verbPast}}: {{id}}\n",
21781
21988
  toggleAppliedManyHeader: "{{verbPast}}: {{count}} extension(s)\n",
21782
21989
  toggleAppliedManyRow: " - {{id}}\n",
21990
+ // --- trust / untrust -------------------------------------------------
21991
+ /**
21992
+ * Receipt printed after `sm plugins trust|untrust`. `verbPast` is
21993
+ * `trusted` / `untrusted`. Trust is per-plugin (bare id), so the rows
21994
+ * carry plugin ids, not qualified extension ids.
21995
+ */
21996
+ trustAppliedSingle: "{{verbPast}}: {{id}}\n",
21997
+ trustAppliedManyHeader: "{{verbPast}}: {{count}} plugin(s)\n",
21998
+ trustAppliedManyRow: " - {{id}}\n",
21999
+ /**
22000
+ * Rejection when a trust verb targets a built-in (or host-locked) id.
22001
+ * Those are never import-trust-gated, so a trust grant is meaningless.
22002
+ */
22003
+ trustBuiltInRejected: '{{glyph}} Plugin "{{id}}" is a built-in (or host-locked) and is never import-trust-gated.\n {{hint}}\n',
22004
+ trustBuiltInRejectedHint: "Import trust applies only to project-local drop-in plugins under .skill-map/plugins/.",
22005
+ /** `--all` found no project-local drop-in plugins to act on. */
22006
+ trustNoPlugins: "No project-local plugins discovered to {{verb}}.\n",
21783
22007
  /**
21784
22008
  * Macro expansion summary printed on stderr before the confirm
21785
22009
  * prompt (or before the `--yes` rejection). The block lists every
@@ -21911,7 +22135,7 @@ var PLUGINS_TEXTS = {
21911
22135
  * Success block printed after scaffolding. Kind-agnostic (the main stub
21912
22136
  * path is interpolated). Follows the no-em-dash rule across every line.
21913
22137
  */
21914
- createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{mainFile}}\n - Run sm plugins doctor to confirm it loads\n - sm plugins slots list: browse slots and input-types\n",
22138
+ createSuccess: "Created {{targetDir}}\nNext:\n - Edit {{mainFile}}\n - Run sm plugins doctor to confirm it loads\n - Run sm plugins trust {{pluginId}} to let its code run (project-local plugins are untrusted until you allow them)\n - sm plugins slots list: browse slots and input-types\n",
21915
22139
  // --- slots list verb -------------------------------------------------
21916
22140
  /** Section header for the view-slots catalogue. */
21917
22141
  slotsListHeaderViewSlots: " View slots ({{count}})\n",
@@ -21953,12 +22177,7 @@ function resolveSearchPaths2(opts, cwd) {
21953
22177
  async function buildResolver() {
21954
22178
  const ctx = defaultRuntimeContext();
21955
22179
  const { effective: cfg } = loadConfig({ cwd: ctx.cwd });
21956
- const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
21957
- const dbOverrides = await tryWithSqlite(
21958
- { databasePath: dbPath, autoBackup: false },
21959
- (adapter) => adapter.pluginConfig.loadOverrideMap()
21960
- ) ?? /* @__PURE__ */ new Map();
21961
- return makeEnabledResolver(cfg, dbOverrides);
22180
+ return makeEnabledResolver(cfg);
21962
22181
  }
21963
22182
  async function loadAll(opts) {
21964
22183
  const ctx = defaultRuntimeContext();
@@ -22954,6 +23173,9 @@ var TogglePluginsBase = class extends SmCommand {
22954
23173
  yes = Option25.Boolean("--yes,-y", false, {
22955
23174
  description: "Skip the interactive confirm when a bare plugin id (or --all) fans the toggle out across multiple extensions."
22956
23175
  });
23176
+ local = Option25.Boolean("--local", false, {
23177
+ description: "Write the enable toggle to the gitignored settings.local.json (per-checkout) instead of the team-shared settings.json."
23178
+ });
22957
23179
  ids = Option25.Rest({ name: "ids" });
22958
23180
  async toggle(enabled) {
22959
23181
  const verb = enabled ? "enable" : "disable";
@@ -23121,20 +23343,25 @@ var TogglePluginsBase = class extends SmCommand {
23121
23343
  return ExitCode.NotFound;
23122
23344
  }
23123
23345
  /**
23124
- * Persist every qualified id in `config_plugins`. On disable, also
23125
- * purge the plugin's `scan_contributions` rows immediately (matches
23126
- * the BFF route, see `server/routes/plugins.ts:applyChangeToAdapter`).
23127
- * Every key is `<plugin>/<ext>` shape so the contribution purge can
23128
- * split into `(pluginId, extensionId)` cleanly.
23346
+ * Persist the per-extension `enabled` toggle for every qualified id in
23347
+ * the config layers (`plugins.<plugin>.extensions.<ext>.enabled`),
23348
+ * targeting `settings.json` by default or `settings.local.json` with
23349
+ * `--local`. On disable, also purge the plugin's `scan_contributions`
23350
+ * rows immediately (matches the BFF route, see
23351
+ * `server/routes/plugins.ts:applyChangeToAdapter`). Every key is
23352
+ * `<plugin>/<ext>` shape so both the config dot-path and the
23353
+ * contribution purge split into `(pluginId, extensionId)` cleanly.
23129
23354
  */
23130
23355
  async #persistKeys(keys, enabled) {
23131
23356
  const ctx = defaultRuntimeContext();
23357
+ const target = this.local ? "project-local" : "project";
23358
+ for (const id of keys) {
23359
+ writeConfigValue(toEnableConfigKey(id), enabled, { target, cwd: ctx.cwd });
23360
+ }
23361
+ if (enabled) return;
23132
23362
  const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
23133
23363
  await withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
23134
- for (const id of keys) {
23135
- await adapter.pluginConfig.set(id, enabled);
23136
- if (!enabled) await purgeContributionsFor(adapter, id);
23137
- }
23364
+ for (const id of keys) await purgeContributionsFor(adapter, id);
23138
23365
  });
23139
23366
  }
23140
23367
  #renderSuccess(keys, enabled) {
@@ -23175,17 +23402,23 @@ async function purgeContributionsFor(adapter, id) {
23175
23402
  }
23176
23403
  await adapter.contributions.purgeByPlugin(id.slice(0, slash), id.slice(slash + 1));
23177
23404
  }
23405
+ function toEnableConfigKey(id) {
23406
+ const slash = id.indexOf("/");
23407
+ if (slash < 0) return `plugins.${id}.enabled`;
23408
+ return `plugins.${id.slice(0, slash)}.extensions.${id.slice(slash + 1)}.enabled`;
23409
+ }
23178
23410
  var PluginsEnableCommand = class extends TogglePluginsBase {
23179
23411
  static paths = [["plugins", "enable"]];
23180
23412
  static usage = Command26.Usage({
23181
23413
  category: "Plugins",
23182
- description: "Enable one or more extensions (or --all). Persists in config_plugins.",
23414
+ description: "Enable one or more extensions (or --all). Persists the per-extension enabled in the config layers.",
23183
23415
  details: `
23184
- Writes a row to config_plugins with enabled=1 per qualified
23185
- extension id. Takes precedence over the team-shared baseline at
23186
- settings.json#/plugins/<id>/enabled. Use sm plugins disable to
23187
- flip; sm config reset plugins.<id>.enabled drops the settings.json
23188
- baseline.
23416
+ Writes plugins.<plugin>.extensions.<ext>.enabled=true per qualified
23417
+ extension id to the team-shared settings.json (or settings.local.json
23418
+ with --local). This is the OPERATIONAL axis only; it does NOT grant
23419
+ import trust for a project-local plugin (use sm plugins trust).
23420
+ Use sm plugins disable to flip; sm config reset
23421
+ plugins.<plugin>.extensions.<ext>.enabled drops the override.
23189
23422
 
23190
23423
  Accepts qualified ids (\`claude/at-directive\`) and bare plugin
23191
23424
  ids (\`claude\`, which fans the toggle out across every extension
@@ -23207,10 +23440,11 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
23207
23440
  static paths = [["plugins", "disable"]];
23208
23441
  static usage = Command26.Usage({
23209
23442
  category: "Plugins",
23210
- description: "Disable one or more extensions (or --all). Persists in config_plugins; does not delete files.",
23443
+ description: "Disable one or more extensions (or --all). Persists the per-extension enabled in the config layers; does not delete files.",
23211
23444
  details: `
23212
- Writes a row to config_plugins with enabled=0 per qualified
23213
- extension id. Discovery still surfaces the plugin in
23445
+ Writes plugins.<plugin>.extensions.<ext>.enabled=false per qualified
23446
+ extension id to the team-shared settings.json (or settings.local.json
23447
+ with --local). Discovery still surfaces the plugin in
23214
23448
  sm plugins list, but with status=disabled; the kernel will not
23215
23449
  run any of its disabled extensions.
23216
23450
 
@@ -23259,10 +23493,176 @@ function resolveBareToggle(id, catalogue) {
23259
23493
  };
23260
23494
  }
23261
23495
 
23496
+ // cli/commands/plugins/trust.ts
23497
+ import { Command as Command27, Option as Option26 } from "clipanion";
23498
+ var TrustPluginsBase = class extends SmCommand {
23499
+ all = Option26.Boolean("--all", false);
23500
+ ids = Option26.Rest({ name: "ids" });
23501
+ async applyTrust(trusted) {
23502
+ const verb = trusted ? "trust" : "untrust";
23503
+ const stderrAnsi = this.ansiFor("stderr");
23504
+ const argError = this.#validateArgs(stderrAnsi, verb);
23505
+ if (argError !== null) return argError;
23506
+ const plugins = await loadAll({ pluginDir: void 0 });
23507
+ const discoveredIds = new Set(plugins.map((p) => p.id));
23508
+ const resolved = this.#resolvePluginIds(plugins, discoveredIds, verb, stderrAnsi);
23509
+ if (typeof resolved === "number") return resolved;
23510
+ if (resolved.length === 0) {
23511
+ this.printer.info(tx(PLUGINS_TEXTS.trustNoPlugins, { verb }));
23512
+ return ExitCode.Ok;
23513
+ }
23514
+ await this.#persist(resolved, trusted);
23515
+ this.#renderSuccess(resolved, trusted);
23516
+ return ExitCode.Ok;
23517
+ }
23518
+ /**
23519
+ * `--all` vs `<id>...` mutex check (one must be present, not both).
23520
+ * Reuses the toggle family's two-line rejection blocks so trust /
23521
+ * enable read in parallel.
23522
+ */
23523
+ #validateArgs(ansi, verb) {
23524
+ const errGlyph = ansi.red("\u2715");
23525
+ if (this.all && this.ids.length > 0) {
23526
+ this.printer.error(
23527
+ tx(PLUGINS_TEXTS.toggleBothIdAndAll, {
23528
+ glyph: errGlyph,
23529
+ hint: ansi.dim(tx(PLUGINS_TEXTS.toggleBothIdAndAllHint, { verb }))
23530
+ })
23531
+ );
23532
+ return ExitCode.Error;
23533
+ }
23534
+ if (!this.all && this.ids.length === 0) {
23535
+ this.printer.error(
23536
+ tx(PLUGINS_TEXTS.toggleNeitherIdNorAll, {
23537
+ glyph: errGlyph,
23538
+ hint: ansi.dim(tx(PLUGINS_TEXTS.toggleNeitherIdNorAllHint, { verb }))
23539
+ })
23540
+ );
23541
+ return ExitCode.Error;
23542
+ }
23543
+ return null;
23544
+ }
23545
+ /**
23546
+ * Resolve `<id>...` (or `--all`) into the deduped set of BARE plugin
23547
+ * ids to write. A qualified `<plugin>/<ext>` collapses to its plugin.
23548
+ * The first unresolvable id (built-in / host-locked, or unknown)
23549
+ * aborts the whole batch before any write so the operator never lands
23550
+ * in a partial state.
23551
+ */
23552
+ #resolvePluginIds(_plugins, discoveredIds, _verb, ansi) {
23553
+ if (this.all) {
23554
+ return [...discoveredIds];
23555
+ }
23556
+ const out = [];
23557
+ const seen = /* @__PURE__ */ new Set();
23558
+ for (const rawId of this.ids) {
23559
+ const bare = collapseToPluginId(rawId);
23560
+ if (isBuiltInOrLocked(bare)) {
23561
+ this.printer.error(
23562
+ tx(PLUGINS_TEXTS.trustBuiltInRejected, {
23563
+ glyph: ansi.red("\u2715"),
23564
+ id: sanitizeForTerminal(bare),
23565
+ hint: ansi.dim(PLUGINS_TEXTS.trustBuiltInRejectedHint)
23566
+ })
23567
+ );
23568
+ return ExitCode.NotFound;
23569
+ }
23570
+ if (!discoveredIds.has(bare)) {
23571
+ this.printer.error(
23572
+ tx(PLUGINS_TEXTS.pluginNotFound, {
23573
+ glyph: ansi.red("\u2715"),
23574
+ id: sanitizeForTerminal(bare),
23575
+ hint: ansi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
23576
+ })
23577
+ );
23578
+ return ExitCode.NotFound;
23579
+ }
23580
+ if (seen.has(bare)) continue;
23581
+ seen.add(bare);
23582
+ out.push(bare);
23583
+ }
23584
+ return out;
23585
+ }
23586
+ /**
23587
+ * Write the trust grant for every resolved bare plugin id. Single
23588
+ * SQLite open for the whole batch. `trusted` true grants import trust,
23589
+ * false revokes it (the next scan / restart reverts the plugin to
23590
+ * discovered-but-unexecuted).
23591
+ */
23592
+ async #persist(pluginIds, trusted) {
23593
+ const ctx = defaultRuntimeContext();
23594
+ const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
23595
+ await withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
23596
+ for (const id of pluginIds) await adapter.trust.set(id, trusted);
23597
+ });
23598
+ }
23599
+ #renderSuccess(pluginIds, trusted) {
23600
+ const verbPast = trusted ? "trusted" : "untrusted";
23601
+ if (pluginIds.length === 1) {
23602
+ this.printer.data(tx(PLUGINS_TEXTS.trustAppliedSingle, { verbPast, id: pluginIds[0] }));
23603
+ return;
23604
+ }
23605
+ this.printer.data(
23606
+ tx(PLUGINS_TEXTS.trustAppliedManyHeader, { verbPast, count: pluginIds.length })
23607
+ );
23608
+ for (const id of pluginIds) {
23609
+ this.printer.data(tx(PLUGINS_TEXTS.trustAppliedManyRow, { id }));
23610
+ }
23611
+ }
23612
+ };
23613
+ function collapseToPluginId(id) {
23614
+ const slash = id.indexOf("/");
23615
+ return slash < 0 ? id : id.slice(0, slash);
23616
+ }
23617
+ function isBuiltInOrLocked(id) {
23618
+ if (isPluginLocked(id)) return true;
23619
+ return builtInPlugins.some((p) => p.id === id);
23620
+ }
23621
+ var PluginsTrustCommand = class extends TrustPluginsBase {
23622
+ static paths = [["plugins", "trust"]];
23623
+ static usage = Command27.Usage({
23624
+ category: "Plugins",
23625
+ description: "Grant LOCAL import trust to one or more project-local plugins (or --all). Persists in the config_plugins trust store.",
23626
+ details: `
23627
+ Records this machine's consent to import and run the plugin's code.
23628
+ Trust is the SECURITY axis, distinct from enable: a project-local
23629
+ plugin runs only when it is BOTH enabled (config) and trusted (this
23630
+ DB store). Per-plugin (bare id); a qualified <plugin>/<ext> collapses
23631
+ to its plugin. Local only, never committed, so it cannot travel in a
23632
+ clone.
23633
+
23634
+ Accepts one or more ids, or --all (every discovered drop-in plugin).
23635
+ Batches are all-or-nothing: a built-in / host-locked / unknown id
23636
+ aborts before any write. Repeated ids are deduped. Granting trust
23637
+ lets an enabled plugin's code import on the next scan / sm serve
23638
+ restart.
23639
+ `
23640
+ });
23641
+ async run() {
23642
+ return this.applyTrust(true);
23643
+ }
23644
+ };
23645
+ var PluginsUntrustCommand = class extends TrustPluginsBase {
23646
+ static paths = [["plugins", "untrust"]];
23647
+ static usage = Command27.Usage({
23648
+ category: "Plugins",
23649
+ description: "Revoke LOCAL import trust from one or more project-local plugins (or --all). Does not delete files or change enable state.",
23650
+ details: `
23651
+ Drops the plugin's config_plugins trust row, so it reverts to
23652
+ discovered-but-unexecuted on the next scan / restart. Does NOT change
23653
+ the enable state and does NOT delete the plugin directory. Same
23654
+ id / batch semantics as sm plugins trust.
23655
+ `
23656
+ });
23657
+ async run() {
23658
+ return this.applyTrust(false);
23659
+ }
23660
+ };
23661
+
23262
23662
  // cli/commands/plugins/create.ts
23263
23663
  import { existsSync as existsSync26, mkdirSync as mkdirSync5, writeFileSync } from "fs";
23264
23664
  import { dirname as dirname18, join as join18, resolve as resolve37 } from "path";
23265
- import { Command as Command27, Option as Option26 } from "clipanion";
23665
+ import { Command as Command28, Option as Option27 } from "clipanion";
23266
23666
 
23267
23667
  // cli/commands/plugins/scaffold/action.ts
23268
23668
  function indexStub(extId) {
@@ -23618,6 +24018,7 @@ Generated by \`sm plugins create ${kind} ${pluginId}\`. Edit \`${mainFileRel}\`
23618
24018
 
23619
24019
  ## Verbs
23620
24020
 
24021
+ - \`sm plugins trust ${pluginId}\`: allow this project-local plugin's code to run (it is untrusted until you do)
23621
24022
  - \`sm plugins list ${pluginId}\`: manifest + extensions + load status
23622
24023
  - \`sm plugins doctor\`: full plugin diagnostic
23623
24024
  - \`sm scan\`: re-emit contributions / re-run analysis
@@ -23667,7 +24068,7 @@ function generateScaffold(kind, pluginId, specVersion) {
23667
24068
  // cli/commands/plugins/create.ts
23668
24069
  var PluginsCreateCommand = class extends SmCommand {
23669
24070
  static paths = [["plugins", "create"]];
23670
- static usage = Command27.Usage({
24071
+ static usage = Command28.Usage({
23671
24072
  category: "Plugins",
23672
24073
  description: "Scaffold a new plugin directory.",
23673
24074
  details: "Emits plugin.json + a per-kind extension stub + README. `<kind>` is one of: provider, extractor, analyzer, action, formatter, hook. The extractor stub ships one view contribution (slot `card.footer.left`) and one setting (`string-list`); edit to taste. Use `sm plugins slots list` to browse the slot / input-type catalog.",
@@ -23679,10 +24080,10 @@ var PluginsCreateCommand = class extends SmCommand {
23679
24080
  });
23680
24081
  // First positional: the extension kind (required). Declared before
23681
24082
  // `pluginId` so clipanion assigns it the first positional slot.
23682
- kind = Option26.String({ required: true, name: "kind" });
23683
- pluginId = Option26.String({ required: true, name: "plugin-id" });
23684
- at = Option26.String("--at", { required: false });
23685
- force = Option26.Boolean("--force", false);
24083
+ kind = Option27.String({ required: true, name: "kind" });
24084
+ pluginId = Option27.String({ required: true, name: "plugin-id" });
24085
+ at = Option27.String("--at", { required: false });
24086
+ force = Option27.Boolean("--force", false);
23686
24087
  async run() {
23687
24088
  const ansi = this.ansiFor("stderr");
23688
24089
  const errGlyph = ansi.red("\u2715");
@@ -23733,7 +24134,8 @@ var PluginsCreateCommand = class extends SmCommand {
23733
24134
  this.printer.data(
23734
24135
  tx(PLUGINS_TEXTS.createSuccess, {
23735
24136
  targetDir: sanitizeForTerminal(targetDir),
23736
- mainFile
24137
+ mainFile,
24138
+ pluginId: this.pluginId
23737
24139
  })
23738
24140
  );
23739
24141
  return ExitCode.Ok;
@@ -23741,7 +24143,7 @@ var PluginsCreateCommand = class extends SmCommand {
23741
24143
  };
23742
24144
 
23743
24145
  // cli/commands/plugins/slots.ts
23744
- import { Command as Command28 } from "clipanion";
24146
+ import { Command as Command29 } from "clipanion";
23745
24147
 
23746
24148
  // cli/commands/plugins/slots-catalog.ts
23747
24149
  var VIEW_SLOTS_CATALOG = [
@@ -23777,7 +24179,7 @@ var INPUT_TYPES_CATALOG = [
23777
24179
  // cli/commands/plugins/slots.ts
23778
24180
  var PluginsSlotsListCommand = class extends SmCommand {
23779
24181
  static paths = [["plugins", "slots", "list"]];
23780
- static usage = Command28.Usage({
24182
+ static usage = Command29.Usage({
23781
24183
  category: "Plugins",
23782
24184
  description: "Print the closed catalogs of view slots and input-types.",
23783
24185
  details: "Read-only. Use this when picking a slot / input-type for a new plugin."
@@ -23826,15 +24228,15 @@ var PluginsSlotsListCommand = class extends SmCommand {
23826
24228
  };
23827
24229
 
23828
24230
  // cli/commands/plugins/upgrade.ts
23829
- import { Command as Command29, Option as Option27 } from "clipanion";
24231
+ import { Command as Command30, Option as Option28 } from "clipanion";
23830
24232
  var PluginsUpgradeCommand = class extends SmCommand {
23831
24233
  static paths = [["plugins", "upgrade"]];
23832
- static usage = Command29.Usage({
24234
+ static usage = Command30.Usage({
23833
24235
  category: "Plugins",
23834
24236
  description: "Apply catalog migrations to plugin manifests.",
23835
24237
  details: "No migrations registered against catalog v1.0.0 yet; this verb is a no-op today. The structure exists so future slot renames / deprecations land without spec churn."
23836
24238
  });
23837
- pluginId = Option27.String({ required: false, name: "plugin-id" });
24239
+ pluginId = Option28.String({ required: false, name: "plugin-id" });
23838
24240
  async run() {
23839
24241
  this.printer.data(
23840
24242
  "sm plugins upgrade: no migrations registered for catalog v1.0.0.\n All loaded plugins are catalog-current.\n Run `sm plugins doctor` to surface any incompatible-catalog status.\n"
@@ -23844,7 +24246,7 @@ var PluginsUpgradeCommand = class extends SmCommand {
23844
24246
  };
23845
24247
 
23846
24248
  // cli/commands/plugins/config.ts
23847
- import { Command as Command30, Option as Option28 } from "clipanion";
24249
+ import { Command as Command31, Option as Option29 } from "clipanion";
23848
24250
 
23849
24251
  // cli/i18n/plugins-config.texts.ts
23850
24252
  var PLUGINS_CONFIG_TEXTS = {
@@ -23885,7 +24287,7 @@ var PLUGINS_CONFIG_TEXTS = {
23885
24287
  // cli/commands/plugins/config.ts
23886
24288
  var PluginsConfigCommand = class extends SmCommand {
23887
24289
  static paths = [["plugins", "config"]];
23888
- static usage = Command30.Usage({
24290
+ static usage = Command31.Usage({
23889
24291
  category: "Plugins",
23890
24292
  description: "Read or write an extension's declared settings.",
23891
24293
  details: `
@@ -23900,13 +24302,13 @@ var PluginsConfigCommand = class extends SmCommand {
23900
24302
  Secret values are shown as <redacted>. Run \`sm scan\` to apply.
23901
24303
  `
23902
24304
  });
23903
- id = Option28.String({ required: true });
23904
- settingId = Option28.String({ required: false });
23905
- value = Option28.String({ required: false });
23906
- reset = Option28.Boolean("--reset", false, {
24305
+ id = Option29.String({ required: true });
24306
+ settingId = Option29.String({ required: false });
24307
+ value = Option29.String({ required: false });
24308
+ reset = Option29.Boolean("--reset", false, {
23907
24309
  description: "Remove the override for <settingId> so the manifest default applies."
23908
24310
  });
23909
- pluginDir = Option28.String("--plugin-dir", { required: false });
24311
+ pluginDir = Option29.String("--plugin-dir", { required: false });
23910
24312
  // Read-only when listing; the write / reset paths emit their own
23911
24313
  // receipt. `sm config` exempts the config family from "done in <…>";
23912
24314
  // mirror that here for the read path. The write path keeps the line.
@@ -24219,6 +24621,8 @@ var PLUGIN_COMMANDS = [
24219
24621
  PluginsDoctorCommand,
24220
24622
  PluginsEnableCommand,
24221
24623
  PluginsDisableCommand,
24624
+ PluginsTrustCommand,
24625
+ PluginsUntrustCommand,
24222
24626
  PluginsCreateCommand,
24223
24627
  PluginsSlotsListCommand,
24224
24628
  PluginsUpgradeCommand,
@@ -24228,7 +24632,7 @@ var PLUGIN_COMMANDS = [
24228
24632
  // cli/commands/refresh.ts
24229
24633
  import { readFile as readFile4 } from "fs/promises";
24230
24634
  import { resolve as resolve38 } from "path";
24231
- import { Command as Command31, Option as Option29 } from "clipanion";
24635
+ import { Command as Command32, Option as Option30 } from "clipanion";
24232
24636
 
24233
24637
  // cli/i18n/refresh.texts.ts
24234
24638
  var REFRESH_TEXTS = {
@@ -24284,7 +24688,7 @@ var REFRESH_TEXTS = {
24284
24688
  // cli/commands/refresh.ts
24285
24689
  var RefreshCommand = class extends SmCommand {
24286
24690
  static paths = [["refresh"]];
24287
- static usage = Command31.Usage({
24691
+ static usage = Command32.Usage({
24288
24692
  category: "Scan",
24289
24693
  description: "Refresh enrichment rows: granular (single node) or batch (every stale row).",
24290
24694
  details: `
@@ -24306,11 +24710,11 @@ var RefreshCommand = class extends SmCommand {
24306
24710
  ["Refresh every node with stale enrichments", "$0 refresh --stale"]
24307
24711
  ]
24308
24712
  });
24309
- nodePath = Option29.String({ name: "node", required: false });
24310
- stale = Option29.Boolean("--stale", false, {
24713
+ nodePath = Option30.String({ name: "node", required: false });
24714
+ stale = Option30.Boolean("--stale", false, {
24311
24715
  description: "Refresh every node carrying a stale enrichment row (no-op in this revision; reserved for future Action-prob enrichments)."
24312
24716
  });
24313
- noPlugins = Option29.Boolean("--no-plugins", false, {
24717
+ noPlugins = Option30.Boolean("--no-plugins", false, {
24314
24718
  description: "Skip drop-in plugin discovery; use only the built-in extractor set."
24315
24719
  });
24316
24720
  // The remaining cyclomatic count comes from CLI ergonomics that don't
@@ -24614,7 +25018,7 @@ var IntentionalFailCommand = class extends SmCommand {
24614
25018
  };
24615
25019
 
24616
25020
  // cli/commands/scan.ts
24617
- import { Command as Command33, Option as Option31 } from "clipanion";
25021
+ import { Command as Command34, Option as Option32 } from "clipanion";
24618
25022
 
24619
25023
  // kernel/util/format-bytes.ts
24620
25024
  var UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
@@ -24773,22 +25177,17 @@ var SCAN_TEXTS = {
24773
25177
  };
24774
25178
 
24775
25179
  // cli/commands/watch.ts
24776
- import { Command as Command32, Option as Option30 } from "clipanion";
25180
+ import { Command as Command33, Option as Option31 } from "clipanion";
24777
25181
 
24778
25182
  // core/watcher/runtime.ts
24779
25183
  import { dirname as dirname19, isAbsolute as isAbsolute13, relative as relative9, resolve as resolve39, sep as sep6 } from "path";
24780
25184
 
24781
25185
  // core/runtime/fresh-resolver.ts
24782
25186
  async function buildFreshResolver(deps) {
24783
- const overrides = await tryWithSqlite(
24784
- { databasePath: deps.databasePath, autoBackup: false },
24785
- async (adapter) => adapter.pluginConfig.loadOverrideMap()
24786
- );
24787
- if (overrides === null) return deps.fallbackResolver;
24788
- return makeEnabledResolver(deps.effectiveConfig(), overrides);
25187
+ return makeEnabledResolver(deps.effectiveConfig());
24789
25188
  }
24790
- function composeResolver(effectiveConfig, overrides) {
24791
- return makeEnabledResolver(effectiveConfig, overrides);
25189
+ function composeResolver(effectiveConfig) {
25190
+ return makeEnabledResolver(effectiveConfig);
24792
25191
  }
24793
25192
 
24794
25193
  // core/watcher/i18n/runtime.texts.ts
@@ -24904,9 +25303,7 @@ function createWatcherRuntime(opts) {
24904
25303
  const runOnePass = async (changedPaths) => {
24905
25304
  notifyBatchStart();
24906
25305
  const resolveEnabledOverride = await buildFreshResolver({
24907
- databasePath: opts.dbPath,
24908
- effectiveConfig: () => cfg,
24909
- fallbackResolver: pluginRuntime.resolveEnabled
25306
+ effectiveConfig: () => cfg
24910
25307
  });
24911
25308
  const kernel = createKernel();
24912
25309
  registerEnabledExtensions(kernel, pluginRuntime, {
@@ -25370,7 +25767,7 @@ async function runWatchLoop(opts) {
25370
25767
  }
25371
25768
  var WatchCommand = class extends SmCommand {
25372
25769
  static paths = [["watch"]];
25373
- static usage = Command32.Usage({
25770
+ static usage = Command33.Usage({
25374
25771
  category: "Scan",
25375
25772
  description: "Watch roots and run an incremental scan after each debounced batch of filesystem events.",
25376
25773
  details: `
@@ -25394,25 +25791,25 @@ var WatchCommand = class extends SmCommand {
25394
25791
  ["Stream ScanResult per batch as ndjson", "$0 watch --json"]
25395
25792
  ]
25396
25793
  });
25397
- roots = Option30.Rest({ name: "roots" });
25398
- noTokens = Option30.Boolean("--no-tokens", false, {
25794
+ roots = Option31.Rest({ name: "roots" });
25795
+ noTokens = Option31.Boolean("--no-tokens", false, {
25399
25796
  description: "Skip per-node token counts (cl100k_base BPE)."
25400
25797
  });
25401
- strict = Option30.Boolean("--strict", false, {
25798
+ strict = Option31.Boolean("--strict", false, {
25402
25799
  description: "Promote frontmatter-validation findings from warn to error inside each batch. Does not change the watcher exit code."
25403
25800
  });
25404
- noPlugins = Option30.Boolean("--no-plugins", false, {
25801
+ noPlugins = Option31.Boolean("--no-plugins", false, {
25405
25802
  description: "Skip drop-in plugin discovery for the watcher session."
25406
25803
  });
25407
- maxConsecutiveFailures = Option30.String("--max-consecutive-failures", {
25804
+ maxConsecutiveFailures = Option31.String("--max-consecutive-failures", {
25408
25805
  required: false,
25409
25806
  description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
25410
25807
  });
25411
- maxScan = Option30.String("--max-scan", {
25808
+ maxScan = Option31.String("--max-scan", {
25412
25809
  required: false,
25413
25810
  description: "Per-batch override of scan.maxScan (default 50000), the WALK-INTAKE ceiling. The scan walks, parses, analyzes, and reference-validates the full corpus up to this number. Bidirectional: raises OR lowers the ceiling. When a batch hits it, additional files are dropped in stable order and the UI surfaces the persistent truncation banner. Validation: integer >= 1."
25414
25811
  });
25415
- maxNodes = Option30.String("--max-nodes", {
25812
+ maxNodes = Option31.String("--max-nodes", {
25416
25813
  required: false,
25417
25814
  description: "Per-batch override of scan.maxNodes (default 256), the MAP RENDER cap (pure metadata): it does NOT bound the scan, only the graph projection. Bidirectional: raises OR lowers the render cap. Validation: integer >= 1."
25418
25815
  });
@@ -25499,7 +25896,7 @@ function parseMaxNodesLimit(raw, stderr, noColor) {
25499
25896
  // cli/commands/scan.ts
25500
25897
  var ScanCommand = class extends SmCommand {
25501
25898
  static paths = [["scan"]];
25502
- static usage = Command33.Usage({
25899
+ static usage = Command34.Usage({
25503
25900
  category: "Scan",
25504
25901
  description: "Scan roots for markdown nodes, run extractors and analyzers.",
25505
25902
  details: `
@@ -25534,39 +25931,39 @@ var ScanCommand = class extends SmCommand {
25534
25931
  ["What would the next incremental scan persist?", "$0 scan --changed -n --json"]
25535
25932
  ]
25536
25933
  });
25537
- roots = Option31.Rest({ name: "roots" });
25538
- noBuiltIns = Option31.Boolean("--no-built-ins", false, {
25934
+ roots = Option32.Rest({ name: "roots" });
25935
+ noBuiltIns = Option32.Boolean("--no-built-ins", false, {
25539
25936
  description: "Skip the built-in extension set. Yields a zero-filled ScanResult (kernel-empty-boot parity); skips DB persistence."
25540
25937
  });
25541
- noPlugins = Option31.Boolean("--no-plugins", false, {
25938
+ noPlugins = Option32.Boolean("--no-plugins", false, {
25542
25939
  description: "Skip drop-in plugin discovery. Only the built-in set runs. Combine with --no-built-ins for a fully empty pipeline."
25543
25940
  });
25544
- noTokens = Option31.Boolean("--no-tokens", false, {
25941
+ noTokens = Option32.Boolean("--no-tokens", false, {
25545
25942
  description: "Skip per-node token counts (cl100k_base BPE). Leaves node.tokens undefined; spec-valid since the field is optional."
25546
25943
  });
25547
- dryRun = Option31.Boolean("-n,--dry-run", false, {
25944
+ dryRun = Option32.Boolean("-n,--dry-run", false, {
25548
25945
  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."
25549
25946
  });
25550
- changed = Option31.Boolean("--changed", false, {
25947
+ changed = Option32.Boolean("--changed", false, {
25551
25948
  description: "Incremental scan: reuse unchanged nodes from the persisted prior snapshot. Degrades to a full scan if no prior snapshot exists."
25552
25949
  });
25553
- allowEmpty = Option31.Boolean("--allow-empty", false, {
25950
+ allowEmpty = Option32.Boolean("--allow-empty", false, {
25554
25951
  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."
25555
25952
  });
25556
- strict = Option31.Boolean("--strict", false, {
25953
+ strict = Option32.Boolean("--strict", false, {
25557
25954
  description: "Promote frontmatter-validation findings from warn to error (exit code 1 on any violation). Overrides scan.strict from config when both are set."
25558
25955
  });
25559
- watch = Option31.Boolean("--watch", false, {
25956
+ watch = Option32.Boolean("--watch", false, {
25560
25957
  description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
25561
25958
  });
25562
- yes = Option31.Boolean("--yes", false, {
25959
+ yes = Option32.Boolean("--yes", false, {
25563
25960
  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."
25564
25961
  });
25565
- maxScan = Option31.String("--max-scan", {
25962
+ maxScan = Option32.String("--max-scan", {
25566
25963
  required: false,
25567
25964
  description: "Per-invocation override of `scan.maxScan` (default 50000). The WALK-INTAKE ceiling: the scan walks, parses, analyzes, and reference-validates the full corpus up to this number. Bidirectional: raises OR lowers the ceiling. When the walker hits it, additional files are dropped in stable order and the scan is marked truncated in scan_meta (the UI raises a persistent banner pointing at the .skillmapignore editor in Settings \u2192 Project). Validation: integer >= 1."
25568
25965
  });
25569
- maxNodes = Option31.String("--max-nodes", {
25966
+ maxNodes = Option32.String("--max-nodes", {
25570
25967
  required: false,
25571
25968
  description: "Per-invocation override of `scan.maxNodes` (default 256). The MAP RENDER cap (pure metadata): it does NOT bound the scan, only how many nodes the graph view projects onto the canvas. Bidirectional: raises OR lowers the render cap. Validation: integer >= 1."
25572
25969
  });
@@ -25927,10 +26324,10 @@ function capOverrides(caps) {
25927
26324
 
25928
26325
  // cli/commands/scan-compare.ts
25929
26326
  import { access, readFile as readFile5 } from "fs/promises";
25930
- import { Command as Command34, Option as Option32 } from "clipanion";
26327
+ import { Command as Command35, Option as Option33 } from "clipanion";
25931
26328
  var ScanCompareCommand = class extends SmCommand {
25932
26329
  static paths = [["scan", "compare-with"]];
25933
- static usage = Command34.Usage({
26330
+ static usage = Command35.Usage({
25934
26331
  category: "Scan",
25935
26332
  description: "Run a fresh scan in memory and emit a delta against the saved ScanResult dump at <dump>. Read-only.",
25936
26333
  details: `
@@ -25958,15 +26355,15 @@ var ScanCompareCommand = class extends SmCommand {
25958
26355
  ["JSON output for tooling", "$0 scan compare-with baseline.json --json"]
25959
26356
  ]
25960
26357
  });
25961
- dump = Option32.String({ required: true });
25962
- roots = Option32.Rest({ name: "roots" });
25963
- noTokens = Option32.Boolean("--no-tokens", false, {
26358
+ dump = Option33.String({ required: true });
26359
+ roots = Option33.Rest({ name: "roots" });
26360
+ noTokens = Option33.Boolean("--no-tokens", false, {
25964
26361
  description: "Skip per-node token counts during the fresh scan."
25965
26362
  });
25966
- strict = Option32.Boolean("--strict", false, {
26363
+ strict = Option33.Boolean("--strict", false, {
25967
26364
  description: "Promote layered-config warnings and frontmatter-validation findings from warn to error."
25968
26365
  });
25969
- noPlugins = Option32.Boolean("--no-plugins", false, {
26366
+ noPlugins = Option33.Boolean("--no-plugins", false, {
25970
26367
  description: "Skip drop-in plugin discovery."
25971
26368
  });
25972
26369
  // Cyclomatic count comes from CLI ergonomics: 3 distinct try/catch
@@ -26173,7 +26570,7 @@ function renderDeltaIssues(issues) {
26173
26570
  // cli/commands/serve.ts
26174
26571
  import { spawn as spawn2 } from "child_process";
26175
26572
  import { existsSync as existsSync32 } from "fs";
26176
- import { Command as Command35, Option as Option33 } from "clipanion";
26573
+ import { Command as Command36, Option as Option34 } from "clipanion";
26177
26574
 
26178
26575
  // kernel/util/dev-mode.ts
26179
26576
  import { sep as sep7 } from "path";
@@ -26420,6 +26817,14 @@ var SERVER_TEXTS = {
26420
26817
  pluginsBodyNotJson: "Request body must be valid JSON.",
26421
26818
  pluginsBodyNotObject: "Request body must be a JSON object.",
26422
26819
  pluginsEnabledRequired: "`enabled` is required and must be a boolean.",
26820
+ pluginsTrustedRequired: "`trusted` is required and must be a boolean.",
26821
+ // 403, trust toggle targeted a built-in (or host-locked) id. Those are
26822
+ // never import-trust-gated, so a trust grant is meaningless. Reuses the
26823
+ // 403 `locked` envelope code per the spec.
26824
+ pluginsTrustBuiltIn: 'Plugin "{{id}}" is a built-in (or host-locked) and is never import-trust-gated.',
26825
+ // 400, the trust route received a qualified `<plugin>/<ext>` id; trust
26826
+ // is per-plugin, so it must be a bare plugin id.
26827
+ pluginsTrustQualifiedRejected: 'Plugin id "{{id}}" contains "/"; import trust is per-plugin, pass the bare plugin id.',
26423
26828
  // 400, cascade route rejects qualified ids: the bare-id PATCH is the
26424
26829
  // bundle macro endpoint. Anything containing `/` needs the dedicated
26425
26830
  // per-extension route below.
@@ -26482,9 +26887,11 @@ var SERVER_TEXTS = {
26482
26887
  // silently widen the scan surface.
26483
26888
  projectPrefsBodyNotJson: "Request body must be valid JSON.",
26484
26889
  projectPrefsBodyNotObject: "Request body must be a JSON object.",
26485
- projectPrefsBodyEmpty: "Request body must contain `allowSidecarWriters` and/or a `scan` block with `referencePaths`.",
26890
+ projectPrefsBodyEmpty: "Request body must contain `allowSidecarWriters`, a `scan` block with `referencePaths`, and/or a `pluginTrust` block with `projectEnabled`.",
26486
26891
  projectPrefsConfirmNotBoolean: "`confirm` must be a boolean.",
26487
26892
  projectPrefsSidecarWritersNotBoolean: "`allowSidecarWriters` must be a boolean.",
26893
+ projectPrefsTrustNotObject: '`pluginTrust` must be an object (e.g. `{"pluginTrust": {"projectEnabled": true}}`).',
26894
+ projectPrefsTrustEnabledNotBoolean: "`pluginTrust.projectEnabled` must be a boolean.",
26488
26895
  // Server-stderr advisory after `PATCH /api/project-preferences`
26489
26896
  // toggles the committed sidecar-writer policy. Lets the operator see
26490
26897
  // the team-shared change land without opening settings.json.
@@ -26493,6 +26900,12 @@ var SERVER_TEXTS = {
26493
26900
  projectPrefsListNotArray: "`{{key}}` must be an array of strings.",
26494
26901
  projectPrefsListEntryNotString: "`{{key}}` entries must be strings.",
26495
26902
  projectPrefsConfirmRequired: "This change opens disk access outside the project: {{paths}}. Re-issue the request with `confirm: true` to proceed.",
26903
+ // 412, turning on the local plugin-trust opt-in. Expands the LOCAL
26904
+ // code-execution surface (every plugin the project enables becomes
26905
+ // trusted), so the route refuses without `confirm: true`.
26906
+ projectPrefsTrustConfirmRequired: "Turning on pluginTrust.projectEnabled trusts every plugin this project enables; their code may then import and run on this machine. Re-issue the request with `confirm: true` to proceed.",
26907
+ // Server-stderr advisory after the local plugin-trust opt-in changes.
26908
+ projectPrefsTrustSet: "project-prefs: pluginTrust.projectEnabled = {{value}}",
26496
26909
  projectPrefsPersistFailed: "Could not persist `{{key}}`: {{message}}",
26497
26910
  // Returned for every NEW entry that does not resolve to an existing
26498
26911
  // directory on disk. The list is comma-separated; pre-existing
@@ -27667,6 +28080,24 @@ var parsePatchBody = makeBodyValidator(SINGLE_PATCH_BODY_SCHEMA, {
27667
28080
  "/enabled:type:boolean": SERVER_TEXTS.pluginsEnabledRequired
27668
28081
  }
27669
28082
  });
28083
+ var TRUST_PATCH_BODY_SCHEMA = {
28084
+ type: "object",
28085
+ additionalProperties: false,
28086
+ required: ["trusted"],
28087
+ properties: {
28088
+ trusted: { type: "boolean" }
28089
+ }
28090
+ };
28091
+ var parseTrustPatchBody = makeBodyValidator(TRUST_PATCH_BODY_SCHEMA, {
28092
+ notJson: SERVER_TEXTS.pluginsBodyNotJson,
28093
+ notObject: SERVER_TEXTS.pluginsBodyNotObject,
28094
+ invalid: SERVER_TEXTS.pluginsTrustedRequired,
28095
+ mapping: {
28096
+ ":required:trusted": SERVER_TEXTS.pluginsTrustedRequired,
28097
+ "/trusted:required": SERVER_TEXTS.pluginsTrustedRequired,
28098
+ "/trusted:type:boolean": SERVER_TEXTS.pluginsTrustedRequired
28099
+ }
28100
+ });
27670
28101
  var BULK_PATCH_BODY_SCHEMA = {
27671
28102
  type: "object",
27672
28103
  additionalProperties: false,
@@ -27712,7 +28143,8 @@ var parseBulkPatchBody = makeBodyValidator(BULK_PATCH_BODY_SCHEMA, {
27712
28143
  function registerPluginsRoute(app, deps) {
27713
28144
  app.get("/api/plugins", async (c) => {
27714
28145
  const resolveEnabled = await buildFreshResolver2(deps);
27715
- const items = listItems(deps, resolveEnabled);
28146
+ const trust = await loadTrustState(deps);
28147
+ const items = listItems(deps, resolveEnabled, trust);
27716
28148
  const errorsByPlugin = await loadRuntimeContributionErrors(deps);
27717
28149
  attachRuntimeContributionErrors(items, errorsByPlugin);
27718
28150
  return c.json(
@@ -27771,7 +28203,28 @@ function registerPluginsRoute(app, deps) {
27771
28203
  });
27772
28204
  }
27773
28205
  const body = await parsePatchBody(c.req.raw);
27774
- return await persistAndProject(c, deps, qualified, body.enabled);
28206
+ return await persistManyAndProject(c, deps, [qualified], body.enabled);
28207
+ });
28208
+ app.patch("/api/plugins/:id/trust", async (c) => {
28209
+ const id = c.req.param("id");
28210
+ if (id.includes("/")) {
28211
+ throw new HTTPException10(400, {
28212
+ message: tx(SERVER_TEXTS.pluginsTrustQualifiedRejected, { id })
28213
+ });
28214
+ }
28215
+ const handle = findHandle(id, deps);
28216
+ if (!handle) {
28217
+ throw new HTTPException10(404, {
28218
+ message: tx(SERVER_TEXTS.pluginsUnknown, { id })
28219
+ });
28220
+ }
28221
+ if (handle.kind === "built-in" || isPluginLocked(id)) {
28222
+ throw new HTTPException10(403, {
28223
+ message: tx(SERVER_TEXTS.pluginsTrustBuiltIn, { id })
28224
+ });
28225
+ }
28226
+ const body = await parseTrustPatchBody(c.req.raw);
28227
+ return await persistTrustAndProject(c, deps, id, body.trusted);
27775
28228
  });
27776
28229
  app.patch("/api/plugins", async (c) => {
27777
28230
  const { changes } = await parseBulkPatchBody(c.req.raw);
@@ -27789,13 +28242,21 @@ function registerPluginsRoute(app, deps) {
27789
28242
  return await persistBulkAndProject(c, deps, changes);
27790
28243
  });
27791
28244
  }
27792
- function listItems(deps, resolveEnabled) {
28245
+ function listItems(deps, resolveEnabled, trust) {
27793
28246
  const config = deps.configService.effective();
27794
28247
  return [
27795
28248
  ...deps.options.noBuiltIns ? [] : buildBuiltInItems(resolveEnabled, config),
27796
- ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled, config)
28249
+ ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled, config, trust)
27797
28250
  ];
27798
28251
  }
28252
+ async function loadTrustState(deps) {
28253
+ const trustMap = await tryWithSqlite(
28254
+ { databasePath: deps.options.dbPath, autoBackup: false },
28255
+ (adapter) => adapter.trust.loadTrustMap()
28256
+ ) ?? /* @__PURE__ */ new Map();
28257
+ const trustProjectEnabled = deps.configService.effective().pluginTrust?.projectEnabled ?? false;
28258
+ return { trustMap, trustProjectEnabled };
28259
+ }
27799
28260
  function buildBuiltInItems(resolveEnabled, config) {
27800
28261
  return sortPluginsForPresentation(builtInPlugins).map((plugin) => {
27801
28262
  const pluginLocked = isPluginLocked(plugin.id);
@@ -27833,10 +28294,10 @@ function buildBuiltInItems(resolveEnabled, config) {
27833
28294
  };
27834
28295
  });
27835
28296
  }
27836
- function buildDiscoveredItems(discovered, deps, resolveEnabled, config) {
27837
- return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled, config));
28297
+ function buildDiscoveredItems(discovered, deps, resolveEnabled, config, trust) {
28298
+ return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled, config, trust));
27838
28299
  }
27839
- function buildDiscoveredItem(plugin, deps, resolveEnabled, config) {
28300
+ function buildDiscoveredItem(plugin, deps, resolveEnabled, config, trust) {
27840
28301
  const pluginLocked = isPluginLocked(plugin.id);
27841
28302
  const extensions = projectExtensionRows(plugin, resolveEnabled, pluginLocked, config);
27842
28303
  const optional = optionalDiscoveredFields(plugin, extensions);
@@ -27848,8 +28309,15 @@ function buildDiscoveredItem(plugin, deps, resolveEnabled, config) {
27848
28309
  reason: plugin.reason ?? null,
27849
28310
  source: classifyPluginSource(plugin.path, deps),
27850
28311
  ...optional,
28312
+ ...discoveredFlags(plugin, pluginLocked, trust)
28313
+ };
28314
+ }
28315
+ function discoveredFlags(plugin, pluginLocked, trust) {
28316
+ const trusted = trust.trustMap.get(plugin.id) === true || trust.trustProjectEnabled;
28317
+ return {
27851
28318
  ...pluginLocked ? { locked: true } : {},
27852
- ...plugin.status === "disabled" ? { startsAsDisabled: true } : {}
28319
+ ...trusted ? { trusted: true } : {},
28320
+ ...plugin.status === "disabled" && plugin.untrusted !== true ? { startsAsDisabled: true } : {}
27853
28321
  };
27854
28322
  }
27855
28323
  function optionalDiscoveredFields(plugin, extensions) {
@@ -27941,49 +28409,54 @@ function attachRuntimeContributionErrors(items, errorsByPlugin) {
27941
28409
  if (errors && errors.length > 0) item.runtimeContributionErrors = errors;
27942
28410
  }
27943
28411
  }
27944
- async function persistAndProject(c, deps, configKey, enabled) {
27945
- const overrides = await tryWithSqlite(
28412
+ async function persistManyAndProject(c, deps, keys, enabled) {
28413
+ const cwd = deps.runtimeContext.cwd;
28414
+ for (const key of keys) {
28415
+ writeConfigValue(toEnableConfigKey2(key), enabled, { target: "project", cwd });
28416
+ }
28417
+ if (!enabled && keys.length > 0) await purgeContributionsForKeys(deps, keys);
28418
+ if (keys.length > 0) deps.configService.reload();
28419
+ return await projectListResponse(c, deps);
28420
+ }
28421
+ async function persistTrustAndProject(c, deps, pluginId, trusted) {
28422
+ const ok = await tryWithSqlite(
27946
28423
  { databasePath: deps.options.dbPath, autoBackup: false },
27947
28424
  async (adapter) => {
27948
- await applyChangeToAdapter(adapter, configKey, enabled);
27949
- return await adapter.pluginConfig.loadOverrideMap();
28425
+ await adapter.trust.set(pluginId, trusted);
28426
+ return true;
27950
28427
  }
27951
28428
  );
27952
- return projectListResponse(c, deps, overrides);
28429
+ if (ok === null) {
28430
+ throw new DbMissingError(
28431
+ tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
28432
+ );
28433
+ }
28434
+ return await projectListResponse(c, deps);
27953
28435
  }
27954
- async function persistManyAndProject(c, deps, keys, enabled) {
27955
- const overrides = await tryWithSqlite(
28436
+ function toEnableConfigKey2(id) {
28437
+ const slash = id.indexOf("/");
28438
+ if (slash < 0) return `plugins.${id}.enabled`;
28439
+ return `plugins.${id.slice(0, slash)}.extensions.${id.slice(slash + 1)}.enabled`;
28440
+ }
28441
+ async function purgeContributionsForKeys(deps, keys) {
28442
+ await tryWithSqlite(
27956
28443
  { databasePath: deps.options.dbPath, autoBackup: false },
27957
28444
  async (adapter) => {
27958
28445
  for (const key of keys) {
27959
- await applyChangeToAdapter(adapter, key, enabled);
28446
+ const slash = key.indexOf("/");
28447
+ if (slash < 0) {
28448
+ await adapter.contributions.purgeByPlugin(key);
28449
+ } else {
28450
+ await adapter.contributions.purgeByPlugin(key.slice(0, slash), key.slice(slash + 1));
28451
+ }
27960
28452
  }
27961
- return await adapter.pluginConfig.loadOverrideMap();
27962
28453
  }
27963
28454
  );
27964
- return projectListResponse(c, deps, overrides);
27965
- }
27966
- async function applyChangeToAdapter(adapter, configKey, enabled) {
27967
- await adapter.pluginConfig.set(configKey, enabled);
27968
- if (enabled) return;
27969
- const slash = configKey.indexOf("/");
27970
- if (slash < 0) {
27971
- await adapter.contributions.purgeByPlugin(configKey);
27972
- return;
27973
- }
27974
- await adapter.contributions.purgeByPlugin(
27975
- configKey.slice(0, slash),
27976
- configKey.slice(slash + 1)
27977
- );
27978
28455
  }
27979
- function projectListResponse(c, deps, overrides) {
27980
- if (overrides === null) {
27981
- throw new DbMissingError(
27982
- tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
27983
- );
27984
- }
27985
- const freshResolver = composeResolver2(deps, overrides);
27986
- const items = listItems(deps, freshResolver);
28456
+ async function projectListResponse(c, deps) {
28457
+ const resolveEnabled = composeResolver2(deps);
28458
+ const trust = await loadTrustState(deps);
28459
+ const items = listItems(deps, resolveEnabled, trust);
27987
28460
  return c.json(
27988
28461
  buildListEnvelope({
27989
28462
  kind: "plugins",
@@ -28088,27 +28561,26 @@ function handleExtensionSettings(handle, extensionId) {
28088
28561
  return readManifestSettings(ext?.instance);
28089
28562
  }
28090
28563
  async function persistBulkAndProject(c, deps, changes) {
28091
- const overrides = await tryWithSqlite(
28092
- { databasePath: deps.options.dbPath, autoBackup: false },
28093
- async (adapter) => {
28094
- for (const change of changes) {
28095
- if (change.enabled === void 0) continue;
28096
- const writeKeys = expandBulkChangeKeys(change, deps);
28097
- for (const key of writeKeys) {
28098
- await applyChangeToAdapter(adapter, key, change.enabled);
28099
- }
28100
- }
28101
- return await adapter.pluginConfig.loadOverrideMap();
28564
+ const { disabledKeys, toggleTouched } = applyBulkEnableWrites(deps, changes);
28565
+ const settingsTouched = persistBulkSettings(deps, changes);
28566
+ if (disabledKeys.length > 0) await purgeContributionsForKeys(deps, disabledKeys);
28567
+ if (toggleTouched || settingsTouched) deps.configService.reload();
28568
+ return await projectListResponse(c, deps);
28569
+ }
28570
+ function applyBulkEnableWrites(deps, changes) {
28571
+ const cwd = deps.runtimeContext.cwd;
28572
+ const disabledKeys = [];
28573
+ let toggleTouched = false;
28574
+ for (const change of changes) {
28575
+ if (change.enabled === void 0) continue;
28576
+ const writeKeys = expandBulkChangeKeys(change, deps);
28577
+ for (const key of writeKeys) {
28578
+ writeConfigValue(toEnableConfigKey2(key), change.enabled, { target: "project", cwd });
28579
+ if (!change.enabled) disabledKeys.push(key);
28102
28580
  }
28103
- );
28104
- if (overrides === null) {
28105
- throw new DbMissingError(
28106
- tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
28107
- );
28581
+ if (writeKeys.length > 0) toggleTouched = true;
28108
28582
  }
28109
- const settingsTouched = persistBulkSettings(deps, changes);
28110
- if (settingsTouched) deps.configService.reload();
28111
- return projectListResponse(c, deps, overrides);
28583
+ return { disabledKeys, toggleTouched };
28112
28584
  }
28113
28585
  function persistBulkSettings(deps, changes) {
28114
28586
  const cwd = deps.runtimeContext.cwd;
@@ -28143,13 +28615,11 @@ function expandBulkChangeKeys(change, deps) {
28143
28615
  }
28144
28616
  async function buildFreshResolver2(deps) {
28145
28617
  return buildFreshResolver({
28146
- databasePath: deps.options.dbPath,
28147
- effectiveConfig: () => deps.configService.effective(),
28148
- fallbackResolver: deps.pluginRuntime.resolveEnabled
28618
+ effectiveConfig: () => deps.configService.effective()
28149
28619
  });
28150
28620
  }
28151
- function composeResolver2(deps, overrides) {
28152
- return composeResolver(deps.configService.effective(), overrides);
28621
+ function composeResolver2(deps) {
28622
+ return composeResolver(deps.configService.effective());
28153
28623
  }
28154
28624
  function findHandle(id, deps) {
28155
28625
  const builtIn = builtInPlugins.find((b) => b.id === id);
@@ -28467,6 +28937,12 @@ function buildEnvelope3(deps) {
28467
28937
  cwd,
28468
28938
  default: []
28469
28939
  }) ?? []
28940
+ },
28941
+ pluginTrust: {
28942
+ projectEnabled: readConfigValue("pluginTrust.projectEnabled", {
28943
+ cwd,
28944
+ default: false
28945
+ }) ?? false
28470
28946
  }
28471
28947
  };
28472
28948
  }
@@ -28474,8 +28950,32 @@ async function applyPatch3(deps, body) {
28474
28950
  const cwd = deps.runtimeContext.cwd;
28475
28951
  const policyChanged = typeof body.allowSidecarWriters === "boolean" && writeSidecarWritersPolicy(body.allowSidecarWriters, cwd);
28476
28952
  const scan = applyScanWrites(body, cwd);
28953
+ const trustChanged = applyTrustWrite(body, cwd);
28477
28954
  if (policyChanged || scan.mutated) await maybeRestartWatcher2(deps);
28478
- if (policyChanged || scan.attempted) deps.configService.reload();
28955
+ if (policyChanged || scan.attempted || trustChanged) deps.configService.reload();
28956
+ }
28957
+ function applyTrustWrite(body, cwd) {
28958
+ const next = body.pluginTrust?.projectEnabled;
28959
+ if (next === void 0) return false;
28960
+ const before = readConfigValue("pluginTrust.projectEnabled", { cwd, default: false }) ?? false;
28961
+ if (before === next) return false;
28962
+ if (projectTrustExposure({ value: next, cwd }).expandsSurface && body.confirm !== true) {
28963
+ throw new HTTPException13(412, {
28964
+ message: SERVER_TEXTS.projectPrefsTrustConfirmRequired
28965
+ });
28966
+ }
28967
+ try {
28968
+ writeConfigValue("pluginTrust.projectEnabled", next, { target: "project-local", cwd });
28969
+ } catch (err) {
28970
+ throw new HTTPException13(400, {
28971
+ message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
28972
+ key: "pluginTrust.projectEnabled",
28973
+ message: formatErrorMessage(err)
28974
+ })
28975
+ });
28976
+ }
28977
+ log.warn(tx(SERVER_TEXTS.projectPrefsTrustSet, { value: String(next) }));
28978
+ return true;
28479
28979
  }
28480
28980
  function applyScanWrites(body, cwd) {
28481
28981
  const writes = collectWrites(body);
@@ -28627,7 +29127,11 @@ function isExistingDirectory(entry, cwd) {
28627
29127
  var PATCH_BODY_SCHEMA3 = {
28628
29128
  type: "object",
28629
29129
  additionalProperties: false,
28630
- anyOf: [{ required: ["allowSidecarWriters"] }, { required: ["scan"] }],
29130
+ anyOf: [
29131
+ { required: ["allowSidecarWriters"] },
29132
+ { required: ["scan"] },
29133
+ { required: ["pluginTrust"] }
29134
+ ],
28631
29135
  properties: {
28632
29136
  confirm: { type: "boolean" },
28633
29137
  allowSidecarWriters: { type: "boolean" },
@@ -28641,6 +29145,14 @@ var PATCH_BODY_SCHEMA3 = {
28641
29145
  items: { type: "string", pattern: "^[^,]+$" }
28642
29146
  }
28643
29147
  }
29148
+ },
29149
+ pluginTrust: {
29150
+ type: "object",
29151
+ additionalProperties: false,
29152
+ minProperties: 1,
29153
+ properties: {
29154
+ projectEnabled: { type: "boolean" }
29155
+ }
28644
29156
  }
28645
29157
  }
28646
29158
  };
@@ -28652,6 +29164,9 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
28652
29164
  ":anyOf": SERVER_TEXTS.projectPrefsBodyEmpty,
28653
29165
  "/scan:minProperties": SERVER_TEXTS.projectPrefsBodyEmpty,
28654
29166
  "/scan:type:object": SERVER_TEXTS.projectPrefsScanNotObject,
29167
+ "/pluginTrust:minProperties": SERVER_TEXTS.projectPrefsBodyEmpty,
29168
+ "/pluginTrust:type:object": SERVER_TEXTS.projectPrefsTrustNotObject,
29169
+ "/pluginTrust/projectEnabled:type:boolean": SERVER_TEXTS.projectPrefsTrustEnabledNotBoolean,
28655
29170
  "/confirm:type:boolean": SERVER_TEXTS.projectPrefsConfirmNotBoolean,
28656
29171
  "/allowSidecarWriters:type:boolean": SERVER_TEXTS.projectPrefsSidecarWritersNotBoolean,
28657
29172
  "/scan/referencePaths:type:array": tx(SERVER_TEXTS.projectPrefsListNotArray, { key: "scan.referencePaths" }),
@@ -28694,9 +29209,7 @@ async function buildEnvelope4(deps) {
28694
29209
  }
28695
29210
  async function resolveSelectableProviders(deps) {
28696
29211
  const resolveEnabled = await buildFreshResolver({
28697
- databasePath: deps.options.dbPath,
28698
- effectiveConfig: () => deps.configService.effective(),
28699
- fallbackResolver: deps.pluginRuntime.resolveEnabled
29212
+ effectiveConfig: () => deps.configService.effective()
28700
29213
  });
28701
29214
  const selectable = /* @__PURE__ */ new Set();
28702
29215
  for (const provider of deps.providers) {
@@ -29263,9 +29776,7 @@ async function runPersistedScan(c, deps) {
29263
29776
  }
29264
29777
  async function buildBffResolverOverride(deps) {
29265
29778
  return buildFreshResolver({
29266
- databasePath: deps.options.dbPath,
29267
- effectiveConfig: () => deps.configService.effective(),
29268
- fallbackResolver: deps.pluginRuntime.resolveEnabled
29779
+ effectiveConfig: () => deps.configService.effective()
29269
29780
  });
29270
29781
  }
29271
29782
  async function loadPersistedScanMeta(deps) {
@@ -30047,6 +30558,7 @@ function presentationOptionals(ui) {
30047
30558
  if (ui.emoji !== void 0) out.emoji = ui.emoji;
30048
30559
  if (ui.icon !== void 0) out.icon = ui.icon;
30049
30560
  if (ui.hideChip !== void 0) out.hideChip = ui.hideChip;
30561
+ if (ui.invocationSigil !== void 0) out.invocationSigil = ui.invocationSigil;
30050
30562
  return out;
30051
30563
  }
30052
30564
  function resolveProviderBodyField(read) {
@@ -30604,7 +31116,7 @@ var SERVE_TEXTS = {
30604
31116
  // cli/commands/serve.ts
30605
31117
  var ServeCommand = class extends SmCommand {
30606
31118
  static paths = [["serve"]];
30607
- static usage = Command35.Usage({
31119
+ static usage = Command36.Usage({
30608
31120
  category: "Setup",
30609
31121
  description: "Start the Hono BFF (single-port: REST + WebSocket + SPA bundle).",
30610
31122
  details: `
@@ -30628,18 +31140,18 @@ var ServeCommand = class extends SmCommand {
30628
31140
  ["Point at a pre-built UI bundle", "$0 serve --ui-dist ./ui/dist/browser"]
30629
31141
  ]
30630
31142
  });
30631
- port = Option33.String("--port", {
31143
+ port = Option34.String("--port", {
30632
31144
  required: false,
30633
31145
  description: "Listening port (default 4242). 0 = OS-assigned."
30634
31146
  });
30635
- host = Option33.String("--host", {
31147
+ host = Option34.String("--host", {
30636
31148
  required: false,
30637
31149
  description: "Listening host (default 127.0.0.1). Loopback-only enforced when --dev-cors is set."
30638
31150
  });
30639
- noBuiltIns = Option33.Boolean("--no-built-ins", false, {
31151
+ noBuiltIns = Option34.Boolean("--no-built-ins", false, {
30640
31152
  description: "Skip built-in plugin registration (parity with sm scan --no-built-ins)."
30641
31153
  });
30642
- noPlugins = Option33.Boolean("--no-plugins", false, {
31154
+ noPlugins = Option34.Boolean("--no-plugins", false, {
30643
31155
  description: "Skip drop-in plugin discovery."
30644
31156
  });
30645
31157
  // `Option.Boolean('--open', true)`, Clipanion's parser auto-derives
@@ -30649,35 +31161,35 @@ var ServeCommand = class extends SmCommand {
30649
31161
  // two registrations for the same flag and rejects the invocation
30650
31162
  // with "Ambiguous Syntax Error". Same convention shipped by every
30651
31163
  // other `--no-...` flag in the CLI tree.
30652
- open = Option33.Boolean("--open", true, {
31164
+ open = Option34.Boolean("--open", true, {
30653
31165
  description: "Auto-open the SPA in the user's default browser after listen. --no-open opts out."
30654
31166
  });
30655
- devCors = Option33.Boolean("--dev-cors", false, {
31167
+ devCors = Option34.Boolean("--dev-cors", false, {
30656
31168
  description: "Enable permissive CORS for the Angular dev-server proxy workflow."
30657
31169
  });
30658
31170
  // `--ui-dist` is intentionally undocumented in the Usage block above
30659
31171
  // (the demo build pipeline + tests rely on it; everyday users never
30660
31172
  // need it). Clipanion still exposes it on the parser; the Usage
30661
31173
  // omission is the "hidden" contract per the 14.1 brief.
30662
- uiDist = Option33.String("--ui-dist", { required: false, hidden: true });
30663
- noUi = Option33.Boolean("--no-ui", false, {
31174
+ uiDist = Option34.String("--ui-dist", { required: false, hidden: true });
31175
+ noUi = Option34.Boolean("--no-ui", false, {
30664
31176
  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."
30665
31177
  });
30666
- noWatcher = Option33.Boolean("--no-watcher", false, {
31178
+ noWatcher = Option34.Boolean("--no-watcher", false, {
30667
31179
  description: "Disable the chokidar-fed scan-and-broadcast loop. Use only for CI / read-only deployments."
30668
31180
  });
30669
- yes = Option33.Boolean("--yes", false, {
31181
+ yes = Option34.Boolean("--yes", false, {
30670
31182
  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."
30671
31183
  });
30672
31184
  // `--watcher-debounce-ms` is undocumented sugar for advanced users
30673
31185
  // who want to tighten / relax the watcher's batching window without
30674
31186
  // editing settings.json. Hidden flag, the Usage block omits it.
30675
- watcherDebounceMs = Option33.String("--watcher-debounce-ms", { required: false, hidden: true });
30676
- maxScan = Option33.String("--max-scan", {
31187
+ watcherDebounceMs = Option34.String("--watcher-debounce-ms", { required: false, hidden: true });
31188
+ maxScan = Option34.String("--max-scan", {
30677
31189
  required: false,
30678
31190
  description: "Per-invocation override of scan.maxScan (default 50000), the WALK-INTAKE ceiling. The scan walks, parses, analyzes, and reference-validates the full corpus up to this number. Bidirectional: raises OR lowers the ceiling. 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`."
30679
31191
  });
30680
- maxNodes = Option33.String("--max-nodes", {
31192
+ maxNodes = Option34.String("--max-nodes", {
30681
31193
  required: false,
30682
31194
  description: "Per-invocation override of scan.maxNodes (default 256), the MAP RENDER cap (pure metadata): it does NOT bound the scan, only how many nodes the graph view projects onto the canvas. Bidirectional: raises OR lowers the render cap. Same flag is honoured on the bare `sm` invocation, which routes to `sm serve`."
30683
31195
  });
@@ -31041,7 +31553,7 @@ function tryOpenBrowser(url, stderr, warnGlyph) {
31041
31553
  }
31042
31554
 
31043
31555
  // cli/commands/show.ts
31044
- import { Command as Command36, Option as Option34 } from "clipanion";
31556
+ import { Command as Command37, Option as Option35 } from "clipanion";
31045
31557
 
31046
31558
  // cli/i18n/show.texts.ts
31047
31559
  var SHOW_TEXTS = {
@@ -31092,7 +31604,7 @@ var SHOW_TEXTS = {
31092
31604
  // cli/commands/show.ts
31093
31605
  var ShowCommand = class extends SmCommand {
31094
31606
  static paths = [["show"]];
31095
- static usage = Command36.Usage({
31607
+ static usage = Command37.Usage({
31096
31608
  category: "Browse",
31097
31609
  description: "Node detail: weight, frontmatter, links, issues.",
31098
31610
  details: `
@@ -31108,7 +31620,7 @@ var ShowCommand = class extends SmCommand {
31108
31620
  ["Machine-readable detail", "$0 show .claude/agents/architect.md --json"]
31109
31621
  ]
31110
31622
  });
31111
- nodePath = Option34.String({ required: true });
31623
+ nodePath = Option35.String({ required: true });
31112
31624
  async run() {
31113
31625
  const dbPath = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
31114
31626
  const exit = requireDbOrExit(dbPath, this.context.stderr);
@@ -31350,7 +31862,7 @@ function rankConfidenceForGrouping(c) {
31350
31862
  // cli/commands/sidecar.ts
31351
31863
  import { unlink as unlink3 } from "fs/promises";
31352
31864
  import { resolve as resolve43 } from "path";
31353
- import { Command as Command37, Option as Option35 } from "clipanion";
31865
+ import { Command as Command38, Option as Option36 } from "clipanion";
31354
31866
 
31355
31867
  // cli/i18n/sidecar.texts.ts
31356
31868
  var SIDECAR_TEXTS = {
@@ -31431,7 +31943,7 @@ async function runWithSidecarConsent(bag, ansi, dispatch) {
31431
31943
  }
31432
31944
  var SidecarRefreshCommand = class extends SmCommand {
31433
31945
  static paths = [["sidecar", "refresh"]];
31434
- static usage = Command37.Usage({
31946
+ static usage = Command38.Usage({
31435
31947
  category: "Actions",
31436
31948
  description: "Refresh a sidecar's `for.{bodyHash, frontmatterHash}` to match the live node. Does NOT bump the version.",
31437
31949
  details: `
@@ -31448,8 +31960,8 @@ var SidecarRefreshCommand = class extends SmCommand {
31448
31960
  ["Refresh a node's sidecar hashes", "$0 sidecar refresh .claude/agents/architect.md"]
31449
31961
  ]
31450
31962
  });
31451
- nodePath = Option35.String({ required: true });
31452
- yes = Option35.Boolean("--yes", false, {
31963
+ nodePath = Option36.String({ required: true });
31964
+ yes = Option36.Boolean("--yes", false, {
31453
31965
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
31454
31966
  });
31455
31967
  async run() {
@@ -31571,7 +32083,7 @@ var SidecarRefreshCommand = class extends SmCommand {
31571
32083
  };
31572
32084
  var SidecarPruneCommand = class extends SmCommand {
31573
32085
  static paths = [["sidecar", "prune"]];
31574
- static usage = Command37.Usage({
32086
+ static usage = Command38.Usage({
31575
32087
  category: "Actions",
31576
32088
  description: "Delete orphan .sm files (sidecars whose accompanying .md no longer exists).",
31577
32089
  details: `
@@ -31593,8 +32105,8 @@ var SidecarPruneCommand = class extends SmCommand {
31593
32105
  ["Delete every orphan .sm file (non-interactive)", "$0 sidecar prune --yes"]
31594
32106
  ]
31595
32107
  });
31596
- dryRun = Option35.Boolean("-n,--dry-run", false);
31597
- yes = Option35.Boolean("--yes,--force", false, {
32108
+ dryRun = Option36.Boolean("-n,--dry-run", false);
32109
+ yes = Option36.Boolean("--yes,--force", false, {
31598
32110
  description: "Skip the interactive confirmation prompt. Required for non-interactive callers (CI, pre-commit hooks)."
31599
32111
  });
31600
32112
  // Complexity is from per-orphan handling, empty-set / dry-run /
@@ -31714,7 +32226,7 @@ var SidecarPruneCommand = class extends SmCommand {
31714
32226
  };
31715
32227
  var SidecarAnnotateCommand = class extends SmCommand {
31716
32228
  static paths = [["sidecar", "annotate"]];
31717
- static usage = Command37.Usage({
32229
+ static usage = Command38.Usage({
31718
32230
  category: "Actions",
31719
32231
  description: "Scaffold an empty `<basename>.sm` next to a node ready for editing.",
31720
32232
  details: `
@@ -31732,9 +32244,9 @@ var SidecarAnnotateCommand = class extends SmCommand {
31732
32244
  ["Overwrite an existing one", "$0 sidecar annotate .claude/agents/architect.md --force"]
31733
32245
  ]
31734
32246
  });
31735
- nodePath = Option35.String({ required: true });
31736
- force = Option35.Boolean("--force", false);
31737
- yes = Option35.Boolean("--yes", false, {
32247
+ nodePath = Option36.String({ required: true });
32248
+ force = Option36.Boolean("--force", false);
32249
+ yes = Option36.Boolean("--yes", false, {
31738
32250
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
31739
32251
  });
31740
32252
  async run() {
@@ -31873,7 +32385,7 @@ var SIDECAR_COMMANDS = [
31873
32385
  ];
31874
32386
 
31875
32387
  // cli/commands/stubs.ts
31876
- import { Command as Command38, Option as Option36 } from "clipanion";
32388
+ import { Command as Command39, Option as Option37 } from "clipanion";
31877
32389
 
31878
32390
  // cli/i18n/stubs.texts.ts
31879
32391
  var STUBS_TEXTS = {
@@ -31899,7 +32411,7 @@ var StubCommand = class extends SmCommand {
31899
32411
  };
31900
32412
  var DoctorCommand = class extends StubCommand {
31901
32413
  static paths = [["doctor"]];
31902
- static usage = Command38.Usage({
32414
+ static usage = Command39.Usage({
31903
32415
  category: "Setup",
31904
32416
  description: planned("Diagnostic report: DB integrity, pending migrations, orphan rows, plugin status, runner availability.")
31905
32417
  });
@@ -31907,18 +32419,18 @@ var DoctorCommand = class extends StubCommand {
31907
32419
  };
31908
32420
  var FindingsCommand = class extends StubCommand {
31909
32421
  static paths = [["findings"]];
31910
- static usage = Command38.Usage({
32422
+ static usage = Command39.Usage({
31911
32423
  category: "Browse",
31912
32424
  description: planned("Probabilistic findings: injection, stale summaries, low confidence.")
31913
32425
  });
31914
- kind = Option36.String("--kind", { required: false });
31915
- since = Option36.String("--since", { required: false });
31916
- threshold = Option36.String("--threshold", { required: false });
32426
+ kind = Option37.String("--kind", { required: false });
32427
+ since = Option37.String("--since", { required: false });
32428
+ threshold = Option37.String("--threshold", { required: false });
31917
32429
  verbName = "findings";
31918
32430
  };
31919
32431
  var ActionsListCommand = class extends StubCommand {
31920
32432
  static paths = [["actions", "list"]];
31921
- static usage = Command38.Usage({
32433
+ static usage = Command39.Usage({
31922
32434
  category: "Jobs",
31923
32435
  description: planned("Registered action types (manifest view).")
31924
32436
  });
@@ -31926,103 +32438,103 @@ var ActionsListCommand = class extends StubCommand {
31926
32438
  };
31927
32439
  var ActionsShowCommand = class extends StubCommand {
31928
32440
  static paths = [["actions", "show"]];
31929
- static usage = Command38.Usage({
32441
+ static usage = Command39.Usage({
31930
32442
  category: "Jobs",
31931
32443
  description: planned("Full action manifest, including preconditions and expected duration.")
31932
32444
  });
31933
- id = Option36.String({ required: true });
32445
+ id = Option37.String({ required: true });
31934
32446
  verbName = "actions show";
31935
32447
  };
31936
32448
  var JobSubmitCommand = class extends StubCommand {
31937
32449
  static paths = [["job", "submit"]];
31938
- static usage = Command38.Usage({
32450
+ static usage = Command39.Usage({
31939
32451
  category: "Jobs",
31940
32452
  description: planned("Enqueue a single job or fan out to every matching node (--all).")
31941
32453
  });
31942
- action = Option36.String({ required: true });
31943
- node = Option36.String("-n", { required: false });
31944
- all = Option36.Boolean("--all", false);
32454
+ action = Option37.String({ required: true });
32455
+ node = Option37.String("-n", { required: false });
32456
+ all = Option37.Boolean("--all", false);
31945
32457
  // CLI flag stays `--run`; field name is `runFlag` per the
31946
32458
  // shadow-avoidance convention documented on `SmCommand`.
31947
- runFlag = Option36.Boolean("--run", false);
31948
- force = Option36.Boolean("--force", false);
31949
- ttl = Option36.String("--ttl", { required: false });
31950
- priority = Option36.String("--priority", { required: false });
32459
+ runFlag = Option37.Boolean("--run", false);
32460
+ force = Option37.Boolean("--force", false);
32461
+ ttl = Option37.String("--ttl", { required: false });
32462
+ priority = Option37.String("--priority", { required: false });
31951
32463
  verbName = "job submit";
31952
32464
  };
31953
32465
  var JobListCommand = class extends StubCommand {
31954
32466
  static paths = [["job", "list"]];
31955
- static usage = Command38.Usage({ category: "Jobs", description: planned("List jobs.") });
31956
- status = Option36.String("--status", { required: false });
31957
- action = Option36.String("--action", { required: false });
31958
- node = Option36.String("--node", { required: false });
32467
+ static usage = Command39.Usage({ category: "Jobs", description: planned("List jobs.") });
32468
+ status = Option37.String("--status", { required: false });
32469
+ action = Option37.String("--action", { required: false });
32470
+ node = Option37.String("--node", { required: false });
31959
32471
  verbName = "job list";
31960
32472
  };
31961
32473
  var JobShowCommand = class extends StubCommand {
31962
32474
  static paths = [["job", "show"]];
31963
- static usage = Command38.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
31964
- id = Option36.String({ required: true });
32475
+ static usage = Command39.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
32476
+ id = Option37.String({ required: true });
31965
32477
  verbName = "job show";
31966
32478
  };
31967
32479
  var JobPreviewCommand = class extends StubCommand {
31968
32480
  static paths = [["job", "preview"]];
31969
- static usage = Command38.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
31970
- id = Option36.String({ required: true });
32481
+ static usage = Command39.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
32482
+ id = Option37.String({ required: true });
31971
32483
  verbName = "job preview";
31972
32484
  };
31973
32485
  var JobClaimCommand = class extends StubCommand {
31974
32486
  static paths = [["job", "claim"]];
31975
- static usage = Command38.Usage({
32487
+ static usage = Command39.Usage({
31976
32488
  category: "Jobs",
31977
32489
  description: planned("Atomic primitive: return next queued job id, mark it running.")
31978
32490
  });
31979
- filter = Option36.String("--filter", { required: false });
32491
+ filter = Option37.String("--filter", { required: false });
31980
32492
  verbName = "job claim";
31981
32493
  };
31982
32494
  var JobRunCommand = class extends StubCommand {
31983
32495
  static paths = [["job", "run"]];
31984
- static usage = Command38.Usage({
32496
+ static usage = Command39.Usage({
31985
32497
  category: "Jobs",
31986
32498
  description: planned("Full CLI-runner loop: claim + spawn + record.")
31987
32499
  });
31988
- all = Option36.Boolean("--all", false);
31989
- max = Option36.String("--max", { required: false });
32500
+ all = Option37.Boolean("--all", false);
32501
+ max = Option37.String("--max", { required: false });
31990
32502
  verbName = "job run";
31991
32503
  };
31992
32504
  var JobStatusCommand = class extends StubCommand {
31993
32505
  static paths = [["job", "status"]];
31994
- static usage = Command38.Usage({
32506
+ static usage = Command39.Usage({
31995
32507
  category: "Jobs",
31996
32508
  description: planned("Counts (per status) or single-job status.")
31997
32509
  });
31998
- id = Option36.String({ required: false });
32510
+ id = Option37.String({ required: false });
31999
32511
  verbName = "job status";
32000
32512
  };
32001
32513
  var JobCancelCommand = class extends StubCommand {
32002
32514
  static paths = [["job", "cancel"]];
32003
- static usage = Command38.Usage({
32515
+ static usage = Command39.Usage({
32004
32516
  category: "Jobs",
32005
32517
  description: planned("Force a running job to failed with reason user-cancelled.")
32006
32518
  });
32007
- id = Option36.String({ required: false });
32008
- all = Option36.Boolean("--all", false);
32519
+ id = Option37.String({ required: false });
32520
+ all = Option37.Boolean("--all", false);
32009
32521
  verbName = "job cancel";
32010
32522
  };
32011
32523
  var RecordCommand = class extends StubCommand {
32012
32524
  static paths = [["record"]];
32013
- static usage = Command38.Usage({
32525
+ static usage = Command39.Usage({
32014
32526
  category: "Jobs",
32015
32527
  description: planned("Close a running job with success or failure. Nonce is the sole credential.")
32016
32528
  });
32017
- id = Option36.String("--id", { required: true });
32018
- nonce = Option36.String("--nonce", { required: true });
32019
- status = Option36.String("--status", { required: true });
32020
- report = Option36.String("--report", { required: false });
32021
- tokensIn = Option36.String("--tokens-in", { required: false });
32022
- tokensOut = Option36.String("--tokens-out", { required: false });
32023
- durationMs = Option36.String("--duration-ms", { required: false });
32024
- model = Option36.String("--model", { required: false });
32025
- error = Option36.String("--error", { required: false });
32529
+ id = Option37.String("--id", { required: true });
32530
+ nonce = Option37.String("--nonce", { required: true });
32531
+ status = Option37.String("--status", { required: true });
32532
+ report = Option37.String("--report", { required: false });
32533
+ tokensIn = Option37.String("--tokens-in", { required: false });
32534
+ tokensOut = Option37.String("--tokens-out", { required: false });
32535
+ durationMs = Option37.String("--duration-ms", { required: false });
32536
+ model = Option37.String("--model", { required: false });
32537
+ error = Option37.String("--error", { required: false });
32026
32538
  verbName = "record";
32027
32539
  };
32028
32540
  var STUB_COMMANDS = [
@@ -32046,7 +32558,7 @@ import { cpSync as cpSync3, existsSync as existsSync33, mkdirSync as mkdirSync6,
32046
32558
  import { dirname as dirname21, join as join21, resolve as resolve44 } from "path";
32047
32559
  import { createInterface as createInterface6 } from "readline";
32048
32560
  import { fileURLToPath as fileURLToPath8 } from "url";
32049
- import { Command as Command39, Option as Option37 } from "clipanion";
32561
+ import { Command as Command40, Option as Option38 } from "clipanion";
32050
32562
 
32051
32563
  // cli/i18n/tutorial.texts.ts
32052
32564
  var TUTORIAL_TEXTS = {
@@ -32110,7 +32622,7 @@ var TRIGGER_EN = "run the tutorial";
32110
32622
  var TRIGGER_ES = "ejecuta el tutorial";
32111
32623
  var TutorialCommand = class extends SmCommand {
32112
32624
  static paths = [["tutorial"]];
32113
- static usage = Command39.Usage({
32625
+ static usage = Command40.Usage({
32114
32626
  category: "Setup",
32115
32627
  description: "Materialize an interactive tester tutorial as a Claude Code skill folder under `<cwd>/.claude/skills/`.",
32116
32628
  details: `
@@ -32139,18 +32651,18 @@ var TutorialCommand = class extends SmCommand {
32139
32651
  // more. Accept one so a stale `sm tutorial master` lands on a friendly
32140
32652
  // usage error (guarded in `run()`) instead of clipanion's generic
32141
32653
  // "extraneous argument" message.
32142
- legacyPositional = Option37.String({ required: false });
32654
+ legacyPositional = Option38.String({ required: false });
32143
32655
  // Named `forProvider`, NOT `for` (reserved word). The CLI surface stays
32144
32656
  // `--for`; selects the destination Provider whose `scaffold.skillDir`
32145
32657
  // the skill is materialised under, skipping the interactive prompt.
32146
- forProvider = Option37.String("--for", {
32658
+ forProvider = Option38.String("--for", {
32147
32659
  required: false,
32148
32660
  description: "Destination provider id (e.g. claude). Skips the prompt."
32149
32661
  });
32150
- force = Option37.Boolean("--force", false, {
32662
+ force = Option38.Boolean("--force", false, {
32151
32663
  description: "Overwrite an existing target directory without prompting."
32152
32664
  });
32153
- experimental = Option37.Boolean("--experimental", false, {
32665
+ experimental = Option38.Boolean("--experimental", false, {
32154
32666
  description: "Offer experimental providers as destinations. They ship disabled; enable the chosen one with `sm plugins enable <id>`."
32155
32667
  });
32156
32668
  async run() {
@@ -32384,7 +32896,7 @@ function resolveSkillSourceDir() {
32384
32896
  }
32385
32897
 
32386
32898
  // cli/commands/version.ts
32387
- import { Command as Command40 } from "clipanion";
32899
+ import { Command as Command41 } from "clipanion";
32388
32900
 
32389
32901
  // cli/i18n/version.texts.ts
32390
32902
  var VERSION_TEXTS = {
@@ -32399,7 +32911,7 @@ var VERSION_TEXTS = {
32399
32911
  // cli/commands/version.ts
32400
32912
  var VersionCommand = class extends SmCommand {
32401
32913
  static paths = [["version"]];
32402
- static usage = Command40.Usage({
32914
+ static usage = Command41.Usage({
32403
32915
  category: "Introspection",
32404
32916
  description: "Print the CLI / spec / runtime / db-schema version matrix."
32405
32917
  });
@@ -32616,4 +33128,4 @@ function resolveBareDefault() {
32616
33128
  process.exit(ExitCode.Error);
32617
33129
  }
32618
33130
  //# sourceMappingURL=cli.js.map
32619
- //# debugId=a842c024-162c-5a3a-85db-1daea4b6735f
33131
+ //# debugId=ca8113af-a7ac-5cd7-b45c-03a5974bca86