@skill-map/cli 0.68.1 → 0.69.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 (31) hide show
  1. package/dist/cli/tutorial/sm-tutorial/references/_core.md +15 -6
  2. package/dist/cli/tutorial/sm-tutorial/references/_manifest.json +70 -70
  3. package/dist/cli/tutorial/sm-tutorial/references/_manifest.yml +38 -38
  4. package/dist/cli/tutorial/sm-tutorial/references/fixtures.md +3 -3
  5. package/dist/cli/tutorial/sm-tutorial/references/part-authoring.md +25 -8
  6. package/dist/cli/tutorial/sm-tutorial/references/part-basic-daily.md +77 -29
  7. package/dist/cli/tutorial/sm-tutorial/references/part-basic-fundamentals.md +1 -1
  8. package/dist/cli/tutorial/sm-tutorial/references/part-basic-kickoff.md +1 -1
  9. package/dist/cli/tutorial/sm-tutorial/references/part-cli.md +1 -1
  10. package/dist/cli/tutorial/sm-tutorial/references/part-daily-loop.md +77 -31
  11. package/dist/cli/tutorial/sm-tutorial/references/part-fundamentals.md +3 -3
  12. package/dist/cli/tutorial/sm-tutorial/references/part-plugins.md +1 -1
  13. package/dist/cli/tutorial/sm-tutorial/references/part-project-kickoff.md +2 -2
  14. package/dist/cli/tutorial/sm-tutorial/references/part-settings.md +2 -2
  15. package/dist/cli.js +699 -326
  16. package/dist/conformance/index.js +3 -3
  17. package/dist/index.js +11 -10
  18. package/dist/kernel/index.d.ts +27 -17
  19. package/dist/kernel/index.js +11 -10
  20. package/dist/migrations/001_initial.sql +7 -3
  21. package/dist/ui/chunk-E7GLGHVY.js +1 -0
  22. package/dist/ui/chunk-RLRSNHYG.js +3 -0
  23. package/dist/ui/{chunk-22EQLC23.js → chunk-RRRXQNG6.js} +1 -1
  24. package/dist/ui/{chunk-K3ZRQNN5.js → chunk-SI4MGFOW.js} +1 -1
  25. package/dist/ui/index.html +1 -1
  26. package/dist/ui/{main-R7BIU4HU.js → main-23NGLEUB.js} +3 -3
  27. package/migrations/001_initial.sql +7 -3
  28. package/package.json +2 -2
  29. package/dist/ui/chunk-PU5OP5RN.js +0 -1
  30. package/dist/ui/chunk-TLMV4LOQ.js +0 -3
  31. /package/dist/ui/{chunk-KMHXNOFZ.js → chunk-SXSNTF26.js} +0 -0
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]="75d0c2c2-d08f-5d7c-aa62-3e1d53dd4ef1")}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.69.0",
254
254
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
255
255
  license: "MIT",
256
256
  type: "module",
@@ -5551,7 +5551,8 @@ var defaults_default = {
5551
5551
  // kernel/config/loader.ts
5552
5552
  var PROJECT_LOCAL_ONLY_KEYS = /* @__PURE__ */ new Set([
5553
5553
  "allowEditSmFiles",
5554
- "scan.referencePaths"
5554
+ "scan.referencePaths",
5555
+ "pluginTrust.projectEnabled"
5555
5556
  ]);
5556
5557
  var DEFAULTS = defaults_default;
5557
5558
  function loadConfig(opts) {
@@ -5832,7 +5833,8 @@ function enumerateConfigPaths(obj, prefix = "") {
5832
5833
 
5833
5834
  // core/config/helper.ts
5834
5835
  var PRIVACY_SENSITIVE_KEYS = /* @__PURE__ */ new Set([
5835
- "scan.referencePaths"
5836
+ "scan.referencePaths",
5837
+ "pluginTrust.projectEnabled"
5836
5838
  ]);
5837
5839
  var ProjectLocalOnlyKeyError = class extends Error {
5838
5840
  constructor(key) {
@@ -5919,6 +5921,14 @@ function projectPathExposure(inputs) {
5919
5921
  if (exposed.length === 0) return empty;
5920
5922
  return { expandsSurface: true, exposedPaths: exposed };
5921
5923
  }
5924
+ function projectTrustExposure(inputs) {
5925
+ if (inputs.value !== true) return { expandsSurface: false };
5926
+ const before = readConfigValue("pluginTrust.projectEnabled", {
5927
+ cwd: inputs.cwd,
5928
+ default: false
5929
+ }) ?? false;
5930
+ return { expandsSurface: before !== true };
5931
+ }
5922
5932
  function resolveScanPathForExposure(raw, cwd) {
5923
5933
  if (raw.startsWith("~/")) return resolve7(join4(osHomedir(), raw.slice(2)));
5924
5934
  if (raw === "~") return resolve7(osHomedir());
@@ -7567,40 +7577,38 @@ function formatMigrationName(m) {
7567
7577
  }
7568
7578
 
7569
7579
  // kernel/adapters/sqlite/plugins.ts
7570
- async function setPluginEnabled(db, pluginId, enabled, now = Date.now()) {
7580
+ async function setPluginTrusted(db, pluginId, trusted, now = Date.now()) {
7571
7581
  await db.insertInto("config_plugins").values({
7572
7582
  pluginId,
7573
- enabled: enabled ? 1 : 0,
7574
- configJson: null,
7583
+ trusted: trusted ? 1 : 0,
7575
7584
  updatedAt: now
7576
7585
  }).onConflict(
7577
7586
  (oc) => oc.column("pluginId").doUpdateSet({
7578
- enabled: enabled ? 1 : 0,
7587
+ trusted: trusted ? 1 : 0,
7579
7588
  updatedAt: now
7580
7589
  })
7581
7590
  ).execute();
7582
7591
  }
7583
- async function getPluginEnabled(db, pluginId) {
7584
- const row = await db.selectFrom("config_plugins").select(["enabled"]).where("pluginId", "=", pluginId).executeTakeFirst();
7592
+ async function getPluginTrusted(db, pluginId) {
7593
+ const row = await db.selectFrom("config_plugins").select(["trusted"]).where("pluginId", "=", pluginId).executeTakeFirst();
7585
7594
  if (!row) return void 0;
7586
- return row.enabled === 1;
7595
+ return row.trusted === 1;
7587
7596
  }
7588
- async function listPluginOverrides(db) {
7589
- const rows = await db.selectFrom("config_plugins").select(["pluginId", "enabled", "configJson", "updatedAt"]).orderBy("pluginId", "asc").execute();
7597
+ async function listPluginTrust(db) {
7598
+ const rows = await db.selectFrom("config_plugins").select(["pluginId", "trusted", "updatedAt"]).orderBy("pluginId", "asc").execute();
7590
7599
  return rows.map((r) => ({
7591
7600
  pluginId: r.pluginId,
7592
- enabled: r.enabled === 1,
7593
- configJson: r.configJson,
7601
+ trusted: r.trusted === 1,
7594
7602
  updatedAt: r.updatedAt
7595
7603
  }));
7596
7604
  }
7597
- async function deletePluginOverride(db, pluginId) {
7605
+ async function deletePluginTrust(db, pluginId) {
7598
7606
  await db.deleteFrom("config_plugins").where("pluginId", "=", pluginId).execute();
7599
7607
  }
7600
- async function loadPluginOverrideMap(db) {
7601
- const rows = await listPluginOverrides(db);
7608
+ async function loadPluginTrustMap(db) {
7609
+ const rows = await listPluginTrust(db);
7602
7610
  const out = /* @__PURE__ */ new Map();
7603
- for (const row of rows) out.set(row.pluginId, row.enabled);
7611
+ for (const row of rows) out.set(row.pluginId, row.trusted);
7604
7612
  return out;
7605
7613
  }
7606
7614
 
@@ -8755,7 +8763,7 @@ var SqliteStorageAdapter = class {
8755
8763
  jobs;
8756
8764
  favorites;
8757
8765
  preferences;
8758
- pluginConfig;
8766
+ trust;
8759
8767
  migrations;
8760
8768
  pluginMigrations;
8761
8769
  constructor(options) {
@@ -8872,12 +8880,12 @@ var SqliteStorageAdapter = class {
8872
8880
  loadUpdateCheckCache: () => loadUpdateCheckCache(this.db),
8873
8881
  saveUpdateCheckCache: (cache) => saveUpdateCheckCache(this.db, cache)
8874
8882
  };
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)
8883
+ this.trust = {
8884
+ set: (pluginId, trusted) => setPluginTrusted(this.db, pluginId, trusted),
8885
+ get: (pluginId) => getPluginTrusted(this.db, pluginId),
8886
+ list: () => listPluginTrust(this.db),
8887
+ delete: (pluginId) => deletePluginTrust(this.db, pluginId),
8888
+ loadTrustMap: () => loadPluginTrustMap(this.db)
8881
8889
  };
8882
8890
  const path = this.#options.databasePath;
8883
8891
  this.migrations = {
@@ -10064,21 +10072,22 @@ var PLUGIN_LOADER_TEXTS = {
10064
10072
  // schema. Both are GitHub blob URLs.
10065
10073
  invalidManifestExtensionShape: "{{relEntry}}: {{errors}}. See {{docUrl}}.",
10066
10074
  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",
10075
+ disabledByConfig: "disabled by settings.json (plugins.<id>.enabled)",
10068
10076
  /**
10069
10077
  * 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.
10078
+ * imported because the operator never granted local import trust.
10079
+ * Distinct from `disabledByConfig` (an explicit operational toggle-off
10080
+ * in the config layers): this id is enabled but carries no
10081
+ * `config_plugins` trust grant, so its code stays unexecuted until
10082
+ * `sm plugins trust` records local consent.
10074
10083
  */
10075
- untrustedNotLoaded: "not loaded: project-local plugin is untrusted until enabled. Run `sm plugins enable {{pluginId}}` to load it.",
10084
+ untrustedNotLoaded: "not loaded: project-local plugin is enabled but not trusted on this machine. Run `sm plugins trust {{pluginId}}` to load it.",
10076
10085
  /**
10077
10086
  * One-time aggregate notice the runtime emits when project-local
10078
10087
  * plugins were found on disk but left unloaded for lack of trust. The
10079
10088
  * `{{count}}` plugins ride the scan without executing any code.
10080
10089
  */
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>`.",
10090
+ 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
10091
  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
10092
  idCollision: "Plugin '{{id}}' at {{pathA}} collides with the plugin at {{pathB}}. Rename one and rerun.",
10084
10093
  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 +11040,33 @@ var SHIPS_DISABLED = /* @__PURE__ */ new Set([
11031
11040
  function installedDefaultEnabled(stability) {
11032
11041
  return stability === void 0 || !SHIPS_DISABLED.has(stability);
11033
11042
  }
11034
- function resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault = true) {
11043
+ function resolvePluginEnabled(pluginId, cfg, installedDefault = true) {
11035
11044
  if (isPluginLocked(pluginId)) return true;
11036
- if (dbOverrides.has(pluginId)) return dbOverrides.get(pluginId) === true;
11045
+ const slash = pluginId.indexOf("/");
11046
+ if (slash >= 0) {
11047
+ return resolveQualifiedEnabled(
11048
+ pluginId.slice(0, slash),
11049
+ pluginId.slice(slash + 1),
11050
+ cfg,
11051
+ installedDefault
11052
+ );
11053
+ }
11037
11054
  const settingsEntry = cfg.plugins[pluginId];
11038
11055
  if (settingsEntry?.enabled !== void 0) return settingsEntry.enabled;
11039
11056
  return installedDefault;
11040
11057
  }
11041
- function makeEnabledResolver(cfg, dbOverrides) {
11042
- return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, dbOverrides, installedDefault);
11058
+ function resolveQualifiedEnabled(plugin, ext, cfg, installedDefault) {
11059
+ const pluginEntry = cfg.plugins[plugin];
11060
+ const perExt = pluginEntry?.extensions?.[ext]?.enabled;
11061
+ if (perExt !== void 0) return perExt;
11062
+ if (pluginEntry?.enabled !== void 0) return pluginEntry.enabled;
11063
+ return installedDefault;
11043
11064
  }
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
- };
11065
+ function makeEnabledResolver(cfg) {
11066
+ return (pluginId, installedDefault) => resolvePluginEnabled(pluginId, cfg, installedDefault);
11067
+ }
11068
+ function makeTrustResolver(trustMap, trustProjectEnabled) {
11069
+ return (pluginId) => isPluginLocked(pluginId) || trustMap.get(pluginId) === true || trustProjectEnabled;
11054
11070
  }
11055
11071
 
11056
11072
  // core/runtime/plugin-runtime/resolver.ts
@@ -11075,11 +11091,15 @@ async function buildResolverInputs(ctx) {
11075
11091
  db: void 0,
11076
11092
  ...ctx
11077
11093
  });
11078
- const dbOverrides = await tryWithSqlite(
11094
+ const trustMap = await tryWithSqlite(
11079
11095
  { databasePath: dbPath, autoBackup: false },
11080
- (adapter) => adapter.pluginConfig.loadOverrideMap()
11096
+ (adapter) => adapter.trust.loadTrustMap()
11081
11097
  ) ?? /* @__PURE__ */ new Map();
11082
- return { resolveEnabled: makeEnabledResolver(cfg, dbOverrides), dbOverrides };
11098
+ return {
11099
+ resolveEnabled: makeEnabledResolver(cfg),
11100
+ trustMap,
11101
+ trustProjectEnabled: cfg.pluginTrust?.projectEnabled ?? false
11102
+ };
11083
11103
  }
11084
11104
 
11085
11105
  // kernel/scan/walk-content.ts
@@ -11616,11 +11636,13 @@ async function loadPluginRuntime(opts = {}) {
11616
11636
  const searchPaths = resolveSearchPaths(opts, ctx);
11617
11637
  const validators = loadSchemaValidators();
11618
11638
  let resolveEnabled;
11619
- let dbOverrides;
11639
+ let trustMap;
11640
+ let trustProjectEnabled;
11620
11641
  try {
11621
11642
  const inputs = await buildResolverInputs(ctx);
11622
11643
  resolveEnabled = inputs.resolveEnabled;
11623
- dbOverrides = inputs.dbOverrides;
11644
+ trustMap = inputs.trustMap;
11645
+ trustProjectEnabled = inputs.trustProjectEnabled;
11624
11646
  } catch {
11625
11647
  }
11626
11648
  const loaderOpts = {
@@ -11630,7 +11652,10 @@ async function loadPluginRuntime(opts = {}) {
11630
11652
  };
11631
11653
  if (resolveEnabled) loaderOpts.resolveEnabled = resolveEnabled;
11632
11654
  if (!opts.pluginDir) {
11633
- loaderOpts.resolveImportTrust = makeImportTrustResolver(dbOverrides ?? /* @__PURE__ */ new Map());
11655
+ loaderOpts.resolveImportTrust = makeTrustResolver(
11656
+ trustMap ?? /* @__PURE__ */ new Map(),
11657
+ trustProjectEnabled ?? false
11658
+ );
11634
11659
  }
11635
11660
  const loader = createPluginLoader(loaderOpts);
11636
11661
  const discovered = await loader.discoverAndLoadAll();
@@ -12269,6 +12294,18 @@ var CONFIG_TEXTS = {
12269
12294
  * screen what they just opted into.
12270
12295
  */
12271
12296
  privacyGateConfirmed: '{{glyph}} Opening disk access for "{{key}}":\n{{paths}}\n',
12297
+ /**
12298
+ * Surfaced when `sm config set pluginTrust.projectEnabled true` is run
12299
+ * without `--yes`. Turning the opt-in on expands the LOCAL
12300
+ * code-execution surface (every plugin the project enables becomes
12301
+ * trusted), so the verb refuses without confirmation.
12302
+ */
12303
+ 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',
12304
+ trustGateRequiredHint: "Rerun with --yes to confirm. Turning it off needs no flag. Prefer per-plugin `sm plugins trust <id>` for narrower consent.",
12305
+ /**
12306
+ * Receipt printed when the trust gate has been confirmed via `--yes`.
12307
+ */
12308
+ trustGateConfirmed: "{{glyph}} Local plugin trust opt-in enabled: every plugin this project enables is now trusted on this machine.\n",
12272
12309
  /**
12273
12310
  * Confirmation printed after `sm config set activeProvider <id>`
12274
12311
  * succeeds. The lens change atomically drops the scan_* zone (per
@@ -12665,7 +12702,7 @@ var ConfigSetCommand = class extends SmCommand {
12665
12702
  key = Option4.String({ required: true });
12666
12703
  value = Option4.String({ required: true });
12667
12704
  yes = Option4.Boolean("--yes", false, {
12668
- description: "Confirm a privacy-sensitive write that opens disk access outside the project (scan.referencePaths)."
12705
+ description: "Confirm a surface-expanding write: disk access outside the project (scan.referencePaths) or blanket local plugin trust (pluginTrust.projectEnabled)."
12669
12706
  });
12670
12707
  // CLI orchestrator: each branch is one validation gate (forbidden
12671
12708
  // segment / privacy guard / schema violation) or output dispatch.
@@ -12678,32 +12715,12 @@ var ConfigSetCommand = class extends SmCommand {
12678
12715
  const stderrAnsi = this.ansiFor("stderr");
12679
12716
  const errGlyph = stderrAnsi.red("\u2715");
12680
12717
  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
- }
12718
+ if (this.key === "pluginTrust.projectEnabled") {
12719
+ const trustGate = this.#applyTrustGate(value, ctx.cwd, errGlyph, stderrAnsi);
12720
+ if (trustGate !== null) return trustGate;
12721
+ } else if (PRIVACY_SENSITIVE_KEYS.has(this.key)) {
12722
+ const pathGate = this.#applyPathGate(value, ctx.cwd, errGlyph, stderrAnsi);
12723
+ if (pathGate !== null) return pathGate;
12707
12724
  }
12708
12725
  if (this.key === "activeProvider" && typeof value === "string") {
12709
12726
  const known = new Set(builtIns().providers.map((p) => p.id));
@@ -12781,6 +12798,58 @@ var ConfigSetCommand = class extends SmCommand {
12781
12798
  }
12782
12799
  return ExitCode.Ok;
12783
12800
  }
12801
+ /**
12802
+ * Disk-access privacy gate for `scan.referencePaths`-style keys.
12803
+ * Returns an exit code to bail with (gate refused) or `null` to
12804
+ * proceed. On a confirmed (`--yes`) expansion it prints the receipt
12805
+ * and returns `null`.
12806
+ */
12807
+ #applyPathGate(value, cwd, errGlyph, stderrAnsi) {
12808
+ const exposure = projectPathExposure({ key: this.key, value, cwd });
12809
+ if (!exposure.expandsSurface) return null;
12810
+ if (!this.yes) {
12811
+ this.printer.info(
12812
+ tx(CONFIG_TEXTS.privacyGateRequired, {
12813
+ glyph: errGlyph,
12814
+ key: this.key,
12815
+ paths: exposure.exposedPaths.map((p) => ` - ${p}`).join("\n"),
12816
+ hint: stderrAnsi.dim(CONFIG_TEXTS.privacyGateRequiredHint)
12817
+ })
12818
+ );
12819
+ return ExitCode.Error;
12820
+ }
12821
+ this.printer.info(
12822
+ tx(CONFIG_TEXTS.privacyGateConfirmed, {
12823
+ glyph: stderrAnsi.dim("\u24D8"),
12824
+ key: this.key,
12825
+ paths: exposure.exposedPaths.map((p) => ` - ${p}`).join("\n")
12826
+ })
12827
+ );
12828
+ return null;
12829
+ }
12830
+ /**
12831
+ * Code-execution-surface gate for `pluginTrust.projectEnabled`.
12832
+ * Turning the local opt-in ON trusts every plugin the project enables,
12833
+ * so it requires `--yes`. Returns an exit code to bail with, or `null`
12834
+ * to proceed (on a confirmed expansion it prints the receipt).
12835
+ */
12836
+ #applyTrustGate(value, cwd, errGlyph, stderrAnsi) {
12837
+ const exposure = projectTrustExposure({ value, cwd });
12838
+ if (!exposure.expandsSurface) return null;
12839
+ if (!this.yes) {
12840
+ this.printer.info(
12841
+ tx(CONFIG_TEXTS.trustGateRequired, {
12842
+ glyph: errGlyph,
12843
+ hint: stderrAnsi.dim(CONFIG_TEXTS.trustGateRequiredHint)
12844
+ })
12845
+ );
12846
+ return ExitCode.Error;
12847
+ }
12848
+ this.printer.info(
12849
+ tx(CONFIG_TEXTS.trustGateConfirmed, { glyph: stderrAnsi.dim("\u24D8") })
12850
+ );
12851
+ return null;
12852
+ }
12784
12853
  /**
12785
12854
  * Side effect of `sm config set activeProvider <id>`, atomically
12786
12855
  * drops the `scan_*` zone so the persisted graph never reflects the
@@ -13101,7 +13170,7 @@ function grantFixturePluginTrust(scope, binary, env) {
13101
13170
  const db = new DatabaseSync7(dbPath);
13102
13171
  try {
13103
13172
  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"
13173
+ "INSERT INTO config_plugins (plugin_id, trusted, updated_at) VALUES (?, 1, 0) ON CONFLICT(plugin_id) DO UPDATE SET trusted = 1"
13105
13174
  );
13106
13175
  for (const id of ids) stmt.run(id);
13107
13176
  } finally {
@@ -21780,6 +21849,23 @@ var PLUGINS_TEXTS = {
21780
21849
  toggleAppliedSingle: "{{verbPast}}: {{id}}\n",
21781
21850
  toggleAppliedManyHeader: "{{verbPast}}: {{count}} extension(s)\n",
21782
21851
  toggleAppliedManyRow: " - {{id}}\n",
21852
+ // --- trust / untrust -------------------------------------------------
21853
+ /**
21854
+ * Receipt printed after `sm plugins trust|untrust`. `verbPast` is
21855
+ * `trusted` / `untrusted`. Trust is per-plugin (bare id), so the rows
21856
+ * carry plugin ids, not qualified extension ids.
21857
+ */
21858
+ trustAppliedSingle: "{{verbPast}}: {{id}}\n",
21859
+ trustAppliedManyHeader: "{{verbPast}}: {{count}} plugin(s)\n",
21860
+ trustAppliedManyRow: " - {{id}}\n",
21861
+ /**
21862
+ * Rejection when a trust verb targets a built-in (or host-locked) id.
21863
+ * Those are never import-trust-gated, so a trust grant is meaningless.
21864
+ */
21865
+ trustBuiltInRejected: '{{glyph}} Plugin "{{id}}" is a built-in (or host-locked) and is never import-trust-gated.\n {{hint}}\n',
21866
+ trustBuiltInRejectedHint: "Import trust applies only to project-local drop-in plugins under .skill-map/plugins/.",
21867
+ /** `--all` found no project-local drop-in plugins to act on. */
21868
+ trustNoPlugins: "No project-local plugins discovered to {{verb}}.\n",
21783
21869
  /**
21784
21870
  * Macro expansion summary printed on stderr before the confirm
21785
21871
  * prompt (or before the `--yes` rejection). The block lists every
@@ -21911,7 +21997,7 @@ var PLUGINS_TEXTS = {
21911
21997
  * Success block printed after scaffolding. Kind-agnostic (the main stub
21912
21998
  * path is interpolated). Follows the no-em-dash rule across every line.
21913
21999
  */
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",
22000
+ 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
22001
  // --- slots list verb -------------------------------------------------
21916
22002
  /** Section header for the view-slots catalogue. */
21917
22003
  slotsListHeaderViewSlots: " View slots ({{count}})\n",
@@ -21953,12 +22039,7 @@ function resolveSearchPaths2(opts, cwd) {
21953
22039
  async function buildResolver() {
21954
22040
  const ctx = defaultRuntimeContext();
21955
22041
  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);
22042
+ return makeEnabledResolver(cfg);
21962
22043
  }
21963
22044
  async function loadAll(opts) {
21964
22045
  const ctx = defaultRuntimeContext();
@@ -22954,6 +23035,9 @@ var TogglePluginsBase = class extends SmCommand {
22954
23035
  yes = Option25.Boolean("--yes,-y", false, {
22955
23036
  description: "Skip the interactive confirm when a bare plugin id (or --all) fans the toggle out across multiple extensions."
22956
23037
  });
23038
+ local = Option25.Boolean("--local", false, {
23039
+ description: "Write the enable toggle to the gitignored settings.local.json (per-checkout) instead of the team-shared settings.json."
23040
+ });
22957
23041
  ids = Option25.Rest({ name: "ids" });
22958
23042
  async toggle(enabled) {
22959
23043
  const verb = enabled ? "enable" : "disable";
@@ -23121,20 +23205,25 @@ var TogglePluginsBase = class extends SmCommand {
23121
23205
  return ExitCode.NotFound;
23122
23206
  }
23123
23207
  /**
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.
23208
+ * Persist the per-extension `enabled` toggle for every qualified id in
23209
+ * the config layers (`plugins.<plugin>.extensions.<ext>.enabled`),
23210
+ * targeting `settings.json` by default or `settings.local.json` with
23211
+ * `--local`. On disable, also purge the plugin's `scan_contributions`
23212
+ * rows immediately (matches the BFF route, see
23213
+ * `server/routes/plugins.ts:applyChangeToAdapter`). Every key is
23214
+ * `<plugin>/<ext>` shape so both the config dot-path and the
23215
+ * contribution purge split into `(pluginId, extensionId)` cleanly.
23129
23216
  */
23130
23217
  async #persistKeys(keys, enabled) {
23131
23218
  const ctx = defaultRuntimeContext();
23219
+ const target = this.local ? "project-local" : "project";
23220
+ for (const id of keys) {
23221
+ writeConfigValue(toEnableConfigKey(id), enabled, { target, cwd: ctx.cwd });
23222
+ }
23223
+ if (enabled) return;
23132
23224
  const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
23133
23225
  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
- }
23226
+ for (const id of keys) await purgeContributionsFor(adapter, id);
23138
23227
  });
23139
23228
  }
23140
23229
  #renderSuccess(keys, enabled) {
@@ -23175,17 +23264,23 @@ async function purgeContributionsFor(adapter, id) {
23175
23264
  }
23176
23265
  await adapter.contributions.purgeByPlugin(id.slice(0, slash), id.slice(slash + 1));
23177
23266
  }
23267
+ function toEnableConfigKey(id) {
23268
+ const slash = id.indexOf("/");
23269
+ if (slash < 0) return `plugins.${id}.enabled`;
23270
+ return `plugins.${id.slice(0, slash)}.extensions.${id.slice(slash + 1)}.enabled`;
23271
+ }
23178
23272
  var PluginsEnableCommand = class extends TogglePluginsBase {
23179
23273
  static paths = [["plugins", "enable"]];
23180
23274
  static usage = Command26.Usage({
23181
23275
  category: "Plugins",
23182
- description: "Enable one or more extensions (or --all). Persists in config_plugins.",
23276
+ description: "Enable one or more extensions (or --all). Persists the per-extension enabled in the config layers.",
23183
23277
  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.
23278
+ Writes plugins.<plugin>.extensions.<ext>.enabled=true per qualified
23279
+ extension id to the team-shared settings.json (or settings.local.json
23280
+ with --local). This is the OPERATIONAL axis only; it does NOT grant
23281
+ import trust for a project-local plugin (use sm plugins trust).
23282
+ Use sm plugins disable to flip; sm config reset
23283
+ plugins.<plugin>.extensions.<ext>.enabled drops the override.
23189
23284
 
23190
23285
  Accepts qualified ids (\`claude/at-directive\`) and bare plugin
23191
23286
  ids (\`claude\`, which fans the toggle out across every extension
@@ -23207,10 +23302,11 @@ var PluginsDisableCommand = class extends TogglePluginsBase {
23207
23302
  static paths = [["plugins", "disable"]];
23208
23303
  static usage = Command26.Usage({
23209
23304
  category: "Plugins",
23210
- description: "Disable one or more extensions (or --all). Persists in config_plugins; does not delete files.",
23305
+ description: "Disable one or more extensions (or --all). Persists the per-extension enabled in the config layers; does not delete files.",
23211
23306
  details: `
23212
- Writes a row to config_plugins with enabled=0 per qualified
23213
- extension id. Discovery still surfaces the plugin in
23307
+ Writes plugins.<plugin>.extensions.<ext>.enabled=false per qualified
23308
+ extension id to the team-shared settings.json (or settings.local.json
23309
+ with --local). Discovery still surfaces the plugin in
23214
23310
  sm plugins list, but with status=disabled; the kernel will not
23215
23311
  run any of its disabled extensions.
23216
23312
 
@@ -23259,10 +23355,176 @@ function resolveBareToggle(id, catalogue) {
23259
23355
  };
23260
23356
  }
23261
23357
 
23358
+ // cli/commands/plugins/trust.ts
23359
+ import { Command as Command27, Option as Option26 } from "clipanion";
23360
+ var TrustPluginsBase = class extends SmCommand {
23361
+ all = Option26.Boolean("--all", false);
23362
+ ids = Option26.Rest({ name: "ids" });
23363
+ async applyTrust(trusted) {
23364
+ const verb = trusted ? "trust" : "untrust";
23365
+ const stderrAnsi = this.ansiFor("stderr");
23366
+ const argError = this.#validateArgs(stderrAnsi, verb);
23367
+ if (argError !== null) return argError;
23368
+ const plugins = await loadAll({ pluginDir: void 0 });
23369
+ const discoveredIds = new Set(plugins.map((p) => p.id));
23370
+ const resolved = this.#resolvePluginIds(plugins, discoveredIds, verb, stderrAnsi);
23371
+ if (typeof resolved === "number") return resolved;
23372
+ if (resolved.length === 0) {
23373
+ this.printer.info(tx(PLUGINS_TEXTS.trustNoPlugins, { verb }));
23374
+ return ExitCode.Ok;
23375
+ }
23376
+ await this.#persist(resolved, trusted);
23377
+ this.#renderSuccess(resolved, trusted);
23378
+ return ExitCode.Ok;
23379
+ }
23380
+ /**
23381
+ * `--all` vs `<id>...` mutex check (one must be present, not both).
23382
+ * Reuses the toggle family's two-line rejection blocks so trust /
23383
+ * enable read in parallel.
23384
+ */
23385
+ #validateArgs(ansi, verb) {
23386
+ const errGlyph = ansi.red("\u2715");
23387
+ if (this.all && this.ids.length > 0) {
23388
+ this.printer.error(
23389
+ tx(PLUGINS_TEXTS.toggleBothIdAndAll, {
23390
+ glyph: errGlyph,
23391
+ hint: ansi.dim(tx(PLUGINS_TEXTS.toggleBothIdAndAllHint, { verb }))
23392
+ })
23393
+ );
23394
+ return ExitCode.Error;
23395
+ }
23396
+ if (!this.all && this.ids.length === 0) {
23397
+ this.printer.error(
23398
+ tx(PLUGINS_TEXTS.toggleNeitherIdNorAll, {
23399
+ glyph: errGlyph,
23400
+ hint: ansi.dim(tx(PLUGINS_TEXTS.toggleNeitherIdNorAllHint, { verb }))
23401
+ })
23402
+ );
23403
+ return ExitCode.Error;
23404
+ }
23405
+ return null;
23406
+ }
23407
+ /**
23408
+ * Resolve `<id>...` (or `--all`) into the deduped set of BARE plugin
23409
+ * ids to write. A qualified `<plugin>/<ext>` collapses to its plugin.
23410
+ * The first unresolvable id (built-in / host-locked, or unknown)
23411
+ * aborts the whole batch before any write so the operator never lands
23412
+ * in a partial state.
23413
+ */
23414
+ #resolvePluginIds(_plugins, discoveredIds, _verb, ansi) {
23415
+ if (this.all) {
23416
+ return [...discoveredIds];
23417
+ }
23418
+ const out = [];
23419
+ const seen = /* @__PURE__ */ new Set();
23420
+ for (const rawId of this.ids) {
23421
+ const bare = collapseToPluginId(rawId);
23422
+ if (isBuiltInOrLocked(bare)) {
23423
+ this.printer.error(
23424
+ tx(PLUGINS_TEXTS.trustBuiltInRejected, {
23425
+ glyph: ansi.red("\u2715"),
23426
+ id: sanitizeForTerminal(bare),
23427
+ hint: ansi.dim(PLUGINS_TEXTS.trustBuiltInRejectedHint)
23428
+ })
23429
+ );
23430
+ return ExitCode.NotFound;
23431
+ }
23432
+ if (!discoveredIds.has(bare)) {
23433
+ this.printer.error(
23434
+ tx(PLUGINS_TEXTS.pluginNotFound, {
23435
+ glyph: ansi.red("\u2715"),
23436
+ id: sanitizeForTerminal(bare),
23437
+ hint: ansi.dim(PLUGINS_TEXTS.pluginNotFoundHint)
23438
+ })
23439
+ );
23440
+ return ExitCode.NotFound;
23441
+ }
23442
+ if (seen.has(bare)) continue;
23443
+ seen.add(bare);
23444
+ out.push(bare);
23445
+ }
23446
+ return out;
23447
+ }
23448
+ /**
23449
+ * Write the trust grant for every resolved bare plugin id. Single
23450
+ * SQLite open for the whole batch. `trusted` true grants import trust,
23451
+ * false revokes it (the next scan / restart reverts the plugin to
23452
+ * discovered-but-unexecuted).
23453
+ */
23454
+ async #persist(pluginIds, trusted) {
23455
+ const ctx = defaultRuntimeContext();
23456
+ const dbPath = resolveDbPath({ db: void 0, cwd: ctx.cwd });
23457
+ await withSqlite({ databasePath: dbPath, autoBackup: false }, async (adapter) => {
23458
+ for (const id of pluginIds) await adapter.trust.set(id, trusted);
23459
+ });
23460
+ }
23461
+ #renderSuccess(pluginIds, trusted) {
23462
+ const verbPast = trusted ? "trusted" : "untrusted";
23463
+ if (pluginIds.length === 1) {
23464
+ this.printer.data(tx(PLUGINS_TEXTS.trustAppliedSingle, { verbPast, id: pluginIds[0] }));
23465
+ return;
23466
+ }
23467
+ this.printer.data(
23468
+ tx(PLUGINS_TEXTS.trustAppliedManyHeader, { verbPast, count: pluginIds.length })
23469
+ );
23470
+ for (const id of pluginIds) {
23471
+ this.printer.data(tx(PLUGINS_TEXTS.trustAppliedManyRow, { id }));
23472
+ }
23473
+ }
23474
+ };
23475
+ function collapseToPluginId(id) {
23476
+ const slash = id.indexOf("/");
23477
+ return slash < 0 ? id : id.slice(0, slash);
23478
+ }
23479
+ function isBuiltInOrLocked(id) {
23480
+ if (isPluginLocked(id)) return true;
23481
+ return builtInPlugins.some((p) => p.id === id);
23482
+ }
23483
+ var PluginsTrustCommand = class extends TrustPluginsBase {
23484
+ static paths = [["plugins", "trust"]];
23485
+ static usage = Command27.Usage({
23486
+ category: "Plugins",
23487
+ description: "Grant LOCAL import trust to one or more project-local plugins (or --all). Persists in the config_plugins trust store.",
23488
+ details: `
23489
+ Records this machine's consent to import and run the plugin's code.
23490
+ Trust is the SECURITY axis, distinct from enable: a project-local
23491
+ plugin runs only when it is BOTH enabled (config) and trusted (this
23492
+ DB store). Per-plugin (bare id); a qualified <plugin>/<ext> collapses
23493
+ to its plugin. Local only, never committed, so it cannot travel in a
23494
+ clone.
23495
+
23496
+ Accepts one or more ids, or --all (every discovered drop-in plugin).
23497
+ Batches are all-or-nothing: a built-in / host-locked / unknown id
23498
+ aborts before any write. Repeated ids are deduped. Granting trust
23499
+ lets an enabled plugin's code import on the next scan / sm serve
23500
+ restart.
23501
+ `
23502
+ });
23503
+ async run() {
23504
+ return this.applyTrust(true);
23505
+ }
23506
+ };
23507
+ var PluginsUntrustCommand = class extends TrustPluginsBase {
23508
+ static paths = [["plugins", "untrust"]];
23509
+ static usage = Command27.Usage({
23510
+ category: "Plugins",
23511
+ description: "Revoke LOCAL import trust from one or more project-local plugins (or --all). Does not delete files or change enable state.",
23512
+ details: `
23513
+ Drops the plugin's config_plugins trust row, so it reverts to
23514
+ discovered-but-unexecuted on the next scan / restart. Does NOT change
23515
+ the enable state and does NOT delete the plugin directory. Same
23516
+ id / batch semantics as sm plugins trust.
23517
+ `
23518
+ });
23519
+ async run() {
23520
+ return this.applyTrust(false);
23521
+ }
23522
+ };
23523
+
23262
23524
  // cli/commands/plugins/create.ts
23263
23525
  import { existsSync as existsSync26, mkdirSync as mkdirSync5, writeFileSync } from "fs";
23264
23526
  import { dirname as dirname18, join as join18, resolve as resolve37 } from "path";
23265
- import { Command as Command27, Option as Option26 } from "clipanion";
23527
+ import { Command as Command28, Option as Option27 } from "clipanion";
23266
23528
 
23267
23529
  // cli/commands/plugins/scaffold/action.ts
23268
23530
  function indexStub(extId) {
@@ -23618,6 +23880,7 @@ Generated by \`sm plugins create ${kind} ${pluginId}\`. Edit \`${mainFileRel}\`
23618
23880
 
23619
23881
  ## Verbs
23620
23882
 
23883
+ - \`sm plugins trust ${pluginId}\`: allow this project-local plugin's code to run (it is untrusted until you do)
23621
23884
  - \`sm plugins list ${pluginId}\`: manifest + extensions + load status
23622
23885
  - \`sm plugins doctor\`: full plugin diagnostic
23623
23886
  - \`sm scan\`: re-emit contributions / re-run analysis
@@ -23667,7 +23930,7 @@ function generateScaffold(kind, pluginId, specVersion) {
23667
23930
  // cli/commands/plugins/create.ts
23668
23931
  var PluginsCreateCommand = class extends SmCommand {
23669
23932
  static paths = [["plugins", "create"]];
23670
- static usage = Command27.Usage({
23933
+ static usage = Command28.Usage({
23671
23934
  category: "Plugins",
23672
23935
  description: "Scaffold a new plugin directory.",
23673
23936
  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 +23942,10 @@ var PluginsCreateCommand = class extends SmCommand {
23679
23942
  });
23680
23943
  // First positional: the extension kind (required). Declared before
23681
23944
  // `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);
23945
+ kind = Option27.String({ required: true, name: "kind" });
23946
+ pluginId = Option27.String({ required: true, name: "plugin-id" });
23947
+ at = Option27.String("--at", { required: false });
23948
+ force = Option27.Boolean("--force", false);
23686
23949
  async run() {
23687
23950
  const ansi = this.ansiFor("stderr");
23688
23951
  const errGlyph = ansi.red("\u2715");
@@ -23733,7 +23996,8 @@ var PluginsCreateCommand = class extends SmCommand {
23733
23996
  this.printer.data(
23734
23997
  tx(PLUGINS_TEXTS.createSuccess, {
23735
23998
  targetDir: sanitizeForTerminal(targetDir),
23736
- mainFile
23999
+ mainFile,
24000
+ pluginId: this.pluginId
23737
24001
  })
23738
24002
  );
23739
24003
  return ExitCode.Ok;
@@ -23741,7 +24005,7 @@ var PluginsCreateCommand = class extends SmCommand {
23741
24005
  };
23742
24006
 
23743
24007
  // cli/commands/plugins/slots.ts
23744
- import { Command as Command28 } from "clipanion";
24008
+ import { Command as Command29 } from "clipanion";
23745
24009
 
23746
24010
  // cli/commands/plugins/slots-catalog.ts
23747
24011
  var VIEW_SLOTS_CATALOG = [
@@ -23777,7 +24041,7 @@ var INPUT_TYPES_CATALOG = [
23777
24041
  // cli/commands/plugins/slots.ts
23778
24042
  var PluginsSlotsListCommand = class extends SmCommand {
23779
24043
  static paths = [["plugins", "slots", "list"]];
23780
- static usage = Command28.Usage({
24044
+ static usage = Command29.Usage({
23781
24045
  category: "Plugins",
23782
24046
  description: "Print the closed catalogs of view slots and input-types.",
23783
24047
  details: "Read-only. Use this when picking a slot / input-type for a new plugin."
@@ -23826,15 +24090,15 @@ var PluginsSlotsListCommand = class extends SmCommand {
23826
24090
  };
23827
24091
 
23828
24092
  // cli/commands/plugins/upgrade.ts
23829
- import { Command as Command29, Option as Option27 } from "clipanion";
24093
+ import { Command as Command30, Option as Option28 } from "clipanion";
23830
24094
  var PluginsUpgradeCommand = class extends SmCommand {
23831
24095
  static paths = [["plugins", "upgrade"]];
23832
- static usage = Command29.Usage({
24096
+ static usage = Command30.Usage({
23833
24097
  category: "Plugins",
23834
24098
  description: "Apply catalog migrations to plugin manifests.",
23835
24099
  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
24100
  });
23837
- pluginId = Option27.String({ required: false, name: "plugin-id" });
24101
+ pluginId = Option28.String({ required: false, name: "plugin-id" });
23838
24102
  async run() {
23839
24103
  this.printer.data(
23840
24104
  "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 +24108,7 @@ var PluginsUpgradeCommand = class extends SmCommand {
23844
24108
  };
23845
24109
 
23846
24110
  // cli/commands/plugins/config.ts
23847
- import { Command as Command30, Option as Option28 } from "clipanion";
24111
+ import { Command as Command31, Option as Option29 } from "clipanion";
23848
24112
 
23849
24113
  // cli/i18n/plugins-config.texts.ts
23850
24114
  var PLUGINS_CONFIG_TEXTS = {
@@ -23885,7 +24149,7 @@ var PLUGINS_CONFIG_TEXTS = {
23885
24149
  // cli/commands/plugins/config.ts
23886
24150
  var PluginsConfigCommand = class extends SmCommand {
23887
24151
  static paths = [["plugins", "config"]];
23888
- static usage = Command30.Usage({
24152
+ static usage = Command31.Usage({
23889
24153
  category: "Plugins",
23890
24154
  description: "Read or write an extension's declared settings.",
23891
24155
  details: `
@@ -23900,13 +24164,13 @@ var PluginsConfigCommand = class extends SmCommand {
23900
24164
  Secret values are shown as <redacted>. Run \`sm scan\` to apply.
23901
24165
  `
23902
24166
  });
23903
- id = Option28.String({ required: true });
23904
- settingId = Option28.String({ required: false });
23905
- value = Option28.String({ required: false });
23906
- reset = Option28.Boolean("--reset", false, {
24167
+ id = Option29.String({ required: true });
24168
+ settingId = Option29.String({ required: false });
24169
+ value = Option29.String({ required: false });
24170
+ reset = Option29.Boolean("--reset", false, {
23907
24171
  description: "Remove the override for <settingId> so the manifest default applies."
23908
24172
  });
23909
- pluginDir = Option28.String("--plugin-dir", { required: false });
24173
+ pluginDir = Option29.String("--plugin-dir", { required: false });
23910
24174
  // Read-only when listing; the write / reset paths emit their own
23911
24175
  // receipt. `sm config` exempts the config family from "done in <…>";
23912
24176
  // mirror that here for the read path. The write path keeps the line.
@@ -24219,6 +24483,8 @@ var PLUGIN_COMMANDS = [
24219
24483
  PluginsDoctorCommand,
24220
24484
  PluginsEnableCommand,
24221
24485
  PluginsDisableCommand,
24486
+ PluginsTrustCommand,
24487
+ PluginsUntrustCommand,
24222
24488
  PluginsCreateCommand,
24223
24489
  PluginsSlotsListCommand,
24224
24490
  PluginsUpgradeCommand,
@@ -24228,7 +24494,7 @@ var PLUGIN_COMMANDS = [
24228
24494
  // cli/commands/refresh.ts
24229
24495
  import { readFile as readFile4 } from "fs/promises";
24230
24496
  import { resolve as resolve38 } from "path";
24231
- import { Command as Command31, Option as Option29 } from "clipanion";
24497
+ import { Command as Command32, Option as Option30 } from "clipanion";
24232
24498
 
24233
24499
  // cli/i18n/refresh.texts.ts
24234
24500
  var REFRESH_TEXTS = {
@@ -24284,7 +24550,7 @@ var REFRESH_TEXTS = {
24284
24550
  // cli/commands/refresh.ts
24285
24551
  var RefreshCommand = class extends SmCommand {
24286
24552
  static paths = [["refresh"]];
24287
- static usage = Command31.Usage({
24553
+ static usage = Command32.Usage({
24288
24554
  category: "Scan",
24289
24555
  description: "Refresh enrichment rows: granular (single node) or batch (every stale row).",
24290
24556
  details: `
@@ -24306,11 +24572,11 @@ var RefreshCommand = class extends SmCommand {
24306
24572
  ["Refresh every node with stale enrichments", "$0 refresh --stale"]
24307
24573
  ]
24308
24574
  });
24309
- nodePath = Option29.String({ name: "node", required: false });
24310
- stale = Option29.Boolean("--stale", false, {
24575
+ nodePath = Option30.String({ name: "node", required: false });
24576
+ stale = Option30.Boolean("--stale", false, {
24311
24577
  description: "Refresh every node carrying a stale enrichment row (no-op in this revision; reserved for future Action-prob enrichments)."
24312
24578
  });
24313
- noPlugins = Option29.Boolean("--no-plugins", false, {
24579
+ noPlugins = Option30.Boolean("--no-plugins", false, {
24314
24580
  description: "Skip drop-in plugin discovery; use only the built-in extractor set."
24315
24581
  });
24316
24582
  // The remaining cyclomatic count comes from CLI ergonomics that don't
@@ -24614,7 +24880,7 @@ var IntentionalFailCommand = class extends SmCommand {
24614
24880
  };
24615
24881
 
24616
24882
  // cli/commands/scan.ts
24617
- import { Command as Command33, Option as Option31 } from "clipanion";
24883
+ import { Command as Command34, Option as Option32 } from "clipanion";
24618
24884
 
24619
24885
  // kernel/util/format-bytes.ts
24620
24886
  var UNITS = ["B", "KiB", "MiB", "GiB", "TiB", "PiB"];
@@ -24773,22 +25039,17 @@ var SCAN_TEXTS = {
24773
25039
  };
24774
25040
 
24775
25041
  // cli/commands/watch.ts
24776
- import { Command as Command32, Option as Option30 } from "clipanion";
25042
+ import { Command as Command33, Option as Option31 } from "clipanion";
24777
25043
 
24778
25044
  // core/watcher/runtime.ts
24779
25045
  import { dirname as dirname19, isAbsolute as isAbsolute13, relative as relative9, resolve as resolve39, sep as sep6 } from "path";
24780
25046
 
24781
25047
  // core/runtime/fresh-resolver.ts
24782
25048
  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);
25049
+ return makeEnabledResolver(deps.effectiveConfig());
24789
25050
  }
24790
- function composeResolver(effectiveConfig, overrides) {
24791
- return makeEnabledResolver(effectiveConfig, overrides);
25051
+ function composeResolver(effectiveConfig) {
25052
+ return makeEnabledResolver(effectiveConfig);
24792
25053
  }
24793
25054
 
24794
25055
  // core/watcher/i18n/runtime.texts.ts
@@ -24904,9 +25165,7 @@ function createWatcherRuntime(opts) {
24904
25165
  const runOnePass = async (changedPaths) => {
24905
25166
  notifyBatchStart();
24906
25167
  const resolveEnabledOverride = await buildFreshResolver({
24907
- databasePath: opts.dbPath,
24908
- effectiveConfig: () => cfg,
24909
- fallbackResolver: pluginRuntime.resolveEnabled
25168
+ effectiveConfig: () => cfg
24910
25169
  });
24911
25170
  const kernel = createKernel();
24912
25171
  registerEnabledExtensions(kernel, pluginRuntime, {
@@ -25370,7 +25629,7 @@ async function runWatchLoop(opts) {
25370
25629
  }
25371
25630
  var WatchCommand = class extends SmCommand {
25372
25631
  static paths = [["watch"]];
25373
- static usage = Command32.Usage({
25632
+ static usage = Command33.Usage({
25374
25633
  category: "Scan",
25375
25634
  description: "Watch roots and run an incremental scan after each debounced batch of filesystem events.",
25376
25635
  details: `
@@ -25394,25 +25653,25 @@ var WatchCommand = class extends SmCommand {
25394
25653
  ["Stream ScanResult per batch as ndjson", "$0 watch --json"]
25395
25654
  ]
25396
25655
  });
25397
- roots = Option30.Rest({ name: "roots" });
25398
- noTokens = Option30.Boolean("--no-tokens", false, {
25656
+ roots = Option31.Rest({ name: "roots" });
25657
+ noTokens = Option31.Boolean("--no-tokens", false, {
25399
25658
  description: "Skip per-node token counts (cl100k_base BPE)."
25400
25659
  });
25401
- strict = Option30.Boolean("--strict", false, {
25660
+ strict = Option31.Boolean("--strict", false, {
25402
25661
  description: "Promote frontmatter-validation findings from warn to error inside each batch. Does not change the watcher exit code."
25403
25662
  });
25404
- noPlugins = Option30.Boolean("--no-plugins", false, {
25663
+ noPlugins = Option31.Boolean("--no-plugins", false, {
25405
25664
  description: "Skip drop-in plugin discovery for the watcher session."
25406
25665
  });
25407
- maxConsecutiveFailures = Option30.String("--max-consecutive-failures", {
25666
+ maxConsecutiveFailures = Option31.String("--max-consecutive-failures", {
25408
25667
  required: false,
25409
25668
  description: "Shut down with exit 2 after N consecutive batch failures (default 5; 0 disables the breaker)."
25410
25669
  });
25411
- maxScan = Option30.String("--max-scan", {
25670
+ maxScan = Option31.String("--max-scan", {
25412
25671
  required: false,
25413
25672
  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
25673
  });
25415
- maxNodes = Option30.String("--max-nodes", {
25674
+ maxNodes = Option31.String("--max-nodes", {
25416
25675
  required: false,
25417
25676
  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
25677
  });
@@ -25499,7 +25758,7 @@ function parseMaxNodesLimit(raw, stderr, noColor) {
25499
25758
  // cli/commands/scan.ts
25500
25759
  var ScanCommand = class extends SmCommand {
25501
25760
  static paths = [["scan"]];
25502
- static usage = Command33.Usage({
25761
+ static usage = Command34.Usage({
25503
25762
  category: "Scan",
25504
25763
  description: "Scan roots for markdown nodes, run extractors and analyzers.",
25505
25764
  details: `
@@ -25534,39 +25793,39 @@ var ScanCommand = class extends SmCommand {
25534
25793
  ["What would the next incremental scan persist?", "$0 scan --changed -n --json"]
25535
25794
  ]
25536
25795
  });
25537
- roots = Option31.Rest({ name: "roots" });
25538
- noBuiltIns = Option31.Boolean("--no-built-ins", false, {
25796
+ roots = Option32.Rest({ name: "roots" });
25797
+ noBuiltIns = Option32.Boolean("--no-built-ins", false, {
25539
25798
  description: "Skip the built-in extension set. Yields a zero-filled ScanResult (kernel-empty-boot parity); skips DB persistence."
25540
25799
  });
25541
- noPlugins = Option31.Boolean("--no-plugins", false, {
25800
+ noPlugins = Option32.Boolean("--no-plugins", false, {
25542
25801
  description: "Skip drop-in plugin discovery. Only the built-in set runs. Combine with --no-built-ins for a fully empty pipeline."
25543
25802
  });
25544
- noTokens = Option31.Boolean("--no-tokens", false, {
25803
+ noTokens = Option32.Boolean("--no-tokens", false, {
25545
25804
  description: "Skip per-node token counts (cl100k_base BPE). Leaves node.tokens undefined; spec-valid since the field is optional."
25546
25805
  });
25547
- dryRun = Option31.Boolean("-n,--dry-run", false, {
25806
+ dryRun = Option32.Boolean("-n,--dry-run", false, {
25548
25807
  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
25808
  });
25550
- changed = Option31.Boolean("--changed", false, {
25809
+ changed = Option32.Boolean("--changed", false, {
25551
25810
  description: "Incremental scan: reuse unchanged nodes from the persisted prior snapshot. Degrades to a full scan if no prior snapshot exists."
25552
25811
  });
25553
- allowEmpty = Option31.Boolean("--allow-empty", false, {
25812
+ allowEmpty = Option32.Boolean("--allow-empty", false, {
25554
25813
  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
25814
  });
25556
- strict = Option31.Boolean("--strict", false, {
25815
+ strict = Option32.Boolean("--strict", false, {
25557
25816
  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
25817
  });
25559
- watch = Option31.Boolean("--watch", false, {
25818
+ watch = Option32.Boolean("--watch", false, {
25560
25819
  description: "Long-running mode: watch the roots and trigger an incremental scan after each debounced batch of filesystem events. Alias of `sm watch`."
25561
25820
  });
25562
- yes = Option31.Boolean("--yes", false, {
25821
+ yes = Option32.Boolean("--yes", false, {
25563
25822
  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
25823
  });
25565
- maxScan = Option31.String("--max-scan", {
25824
+ maxScan = Option32.String("--max-scan", {
25566
25825
  required: false,
25567
25826
  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
25827
  });
25569
- maxNodes = Option31.String("--max-nodes", {
25828
+ maxNodes = Option32.String("--max-nodes", {
25570
25829
  required: false,
25571
25830
  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
25831
  });
@@ -25927,10 +26186,10 @@ function capOverrides(caps) {
25927
26186
 
25928
26187
  // cli/commands/scan-compare.ts
25929
26188
  import { access, readFile as readFile5 } from "fs/promises";
25930
- import { Command as Command34, Option as Option32 } from "clipanion";
26189
+ import { Command as Command35, Option as Option33 } from "clipanion";
25931
26190
  var ScanCompareCommand = class extends SmCommand {
25932
26191
  static paths = [["scan", "compare-with"]];
25933
- static usage = Command34.Usage({
26192
+ static usage = Command35.Usage({
25934
26193
  category: "Scan",
25935
26194
  description: "Run a fresh scan in memory and emit a delta against the saved ScanResult dump at <dump>. Read-only.",
25936
26195
  details: `
@@ -25958,15 +26217,15 @@ var ScanCompareCommand = class extends SmCommand {
25958
26217
  ["JSON output for tooling", "$0 scan compare-with baseline.json --json"]
25959
26218
  ]
25960
26219
  });
25961
- dump = Option32.String({ required: true });
25962
- roots = Option32.Rest({ name: "roots" });
25963
- noTokens = Option32.Boolean("--no-tokens", false, {
26220
+ dump = Option33.String({ required: true });
26221
+ roots = Option33.Rest({ name: "roots" });
26222
+ noTokens = Option33.Boolean("--no-tokens", false, {
25964
26223
  description: "Skip per-node token counts during the fresh scan."
25965
26224
  });
25966
- strict = Option32.Boolean("--strict", false, {
26225
+ strict = Option33.Boolean("--strict", false, {
25967
26226
  description: "Promote layered-config warnings and frontmatter-validation findings from warn to error."
25968
26227
  });
25969
- noPlugins = Option32.Boolean("--no-plugins", false, {
26228
+ noPlugins = Option33.Boolean("--no-plugins", false, {
25970
26229
  description: "Skip drop-in plugin discovery."
25971
26230
  });
25972
26231
  // Cyclomatic count comes from CLI ergonomics: 3 distinct try/catch
@@ -26173,7 +26432,7 @@ function renderDeltaIssues(issues) {
26173
26432
  // cli/commands/serve.ts
26174
26433
  import { spawn as spawn2 } from "child_process";
26175
26434
  import { existsSync as existsSync32 } from "fs";
26176
- import { Command as Command35, Option as Option33 } from "clipanion";
26435
+ import { Command as Command36, Option as Option34 } from "clipanion";
26177
26436
 
26178
26437
  // kernel/util/dev-mode.ts
26179
26438
  import { sep as sep7 } from "path";
@@ -26420,6 +26679,14 @@ var SERVER_TEXTS = {
26420
26679
  pluginsBodyNotJson: "Request body must be valid JSON.",
26421
26680
  pluginsBodyNotObject: "Request body must be a JSON object.",
26422
26681
  pluginsEnabledRequired: "`enabled` is required and must be a boolean.",
26682
+ pluginsTrustedRequired: "`trusted` is required and must be a boolean.",
26683
+ // 403, trust toggle targeted a built-in (or host-locked) id. Those are
26684
+ // never import-trust-gated, so a trust grant is meaningless. Reuses the
26685
+ // 403 `locked` envelope code per the spec.
26686
+ pluginsTrustBuiltIn: 'Plugin "{{id}}" is a built-in (or host-locked) and is never import-trust-gated.',
26687
+ // 400, the trust route received a qualified `<plugin>/<ext>` id; trust
26688
+ // is per-plugin, so it must be a bare plugin id.
26689
+ pluginsTrustQualifiedRejected: 'Plugin id "{{id}}" contains "/"; import trust is per-plugin, pass the bare plugin id.',
26423
26690
  // 400, cascade route rejects qualified ids: the bare-id PATCH is the
26424
26691
  // bundle macro endpoint. Anything containing `/` needs the dedicated
26425
26692
  // per-extension route below.
@@ -26482,9 +26749,11 @@ var SERVER_TEXTS = {
26482
26749
  // silently widen the scan surface.
26483
26750
  projectPrefsBodyNotJson: "Request body must be valid JSON.",
26484
26751
  projectPrefsBodyNotObject: "Request body must be a JSON object.",
26485
- projectPrefsBodyEmpty: "Request body must contain `allowSidecarWriters` and/or a `scan` block with `referencePaths`.",
26752
+ projectPrefsBodyEmpty: "Request body must contain `allowSidecarWriters`, a `scan` block with `referencePaths`, and/or a `pluginTrust` block with `projectEnabled`.",
26486
26753
  projectPrefsConfirmNotBoolean: "`confirm` must be a boolean.",
26487
26754
  projectPrefsSidecarWritersNotBoolean: "`allowSidecarWriters` must be a boolean.",
26755
+ projectPrefsTrustNotObject: '`pluginTrust` must be an object (e.g. `{"pluginTrust": {"projectEnabled": true}}`).',
26756
+ projectPrefsTrustEnabledNotBoolean: "`pluginTrust.projectEnabled` must be a boolean.",
26488
26757
  // Server-stderr advisory after `PATCH /api/project-preferences`
26489
26758
  // toggles the committed sidecar-writer policy. Lets the operator see
26490
26759
  // the team-shared change land without opening settings.json.
@@ -26493,6 +26762,12 @@ var SERVER_TEXTS = {
26493
26762
  projectPrefsListNotArray: "`{{key}}` must be an array of strings.",
26494
26763
  projectPrefsListEntryNotString: "`{{key}}` entries must be strings.",
26495
26764
  projectPrefsConfirmRequired: "This change opens disk access outside the project: {{paths}}. Re-issue the request with `confirm: true` to proceed.",
26765
+ // 412, turning on the local plugin-trust opt-in. Expands the LOCAL
26766
+ // code-execution surface (every plugin the project enables becomes
26767
+ // trusted), so the route refuses without `confirm: true`.
26768
+ 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.",
26769
+ // Server-stderr advisory after the local plugin-trust opt-in changes.
26770
+ projectPrefsTrustSet: "project-prefs: pluginTrust.projectEnabled = {{value}}",
26496
26771
  projectPrefsPersistFailed: "Could not persist `{{key}}`: {{message}}",
26497
26772
  // Returned for every NEW entry that does not resolve to an existing
26498
26773
  // directory on disk. The list is comma-separated; pre-existing
@@ -27667,6 +27942,24 @@ var parsePatchBody = makeBodyValidator(SINGLE_PATCH_BODY_SCHEMA, {
27667
27942
  "/enabled:type:boolean": SERVER_TEXTS.pluginsEnabledRequired
27668
27943
  }
27669
27944
  });
27945
+ var TRUST_PATCH_BODY_SCHEMA = {
27946
+ type: "object",
27947
+ additionalProperties: false,
27948
+ required: ["trusted"],
27949
+ properties: {
27950
+ trusted: { type: "boolean" }
27951
+ }
27952
+ };
27953
+ var parseTrustPatchBody = makeBodyValidator(TRUST_PATCH_BODY_SCHEMA, {
27954
+ notJson: SERVER_TEXTS.pluginsBodyNotJson,
27955
+ notObject: SERVER_TEXTS.pluginsBodyNotObject,
27956
+ invalid: SERVER_TEXTS.pluginsTrustedRequired,
27957
+ mapping: {
27958
+ ":required:trusted": SERVER_TEXTS.pluginsTrustedRequired,
27959
+ "/trusted:required": SERVER_TEXTS.pluginsTrustedRequired,
27960
+ "/trusted:type:boolean": SERVER_TEXTS.pluginsTrustedRequired
27961
+ }
27962
+ });
27670
27963
  var BULK_PATCH_BODY_SCHEMA = {
27671
27964
  type: "object",
27672
27965
  additionalProperties: false,
@@ -27712,7 +28005,8 @@ var parseBulkPatchBody = makeBodyValidator(BULK_PATCH_BODY_SCHEMA, {
27712
28005
  function registerPluginsRoute(app, deps) {
27713
28006
  app.get("/api/plugins", async (c) => {
27714
28007
  const resolveEnabled = await buildFreshResolver2(deps);
27715
- const items = listItems(deps, resolveEnabled);
28008
+ const trust = await loadTrustState(deps);
28009
+ const items = listItems(deps, resolveEnabled, trust);
27716
28010
  const errorsByPlugin = await loadRuntimeContributionErrors(deps);
27717
28011
  attachRuntimeContributionErrors(items, errorsByPlugin);
27718
28012
  return c.json(
@@ -27771,7 +28065,28 @@ function registerPluginsRoute(app, deps) {
27771
28065
  });
27772
28066
  }
27773
28067
  const body = await parsePatchBody(c.req.raw);
27774
- return await persistAndProject(c, deps, qualified, body.enabled);
28068
+ return await persistManyAndProject(c, deps, [qualified], body.enabled);
28069
+ });
28070
+ app.patch("/api/plugins/:id/trust", async (c) => {
28071
+ const id = c.req.param("id");
28072
+ if (id.includes("/")) {
28073
+ throw new HTTPException10(400, {
28074
+ message: tx(SERVER_TEXTS.pluginsTrustQualifiedRejected, { id })
28075
+ });
28076
+ }
28077
+ const handle = findHandle(id, deps);
28078
+ if (!handle) {
28079
+ throw new HTTPException10(404, {
28080
+ message: tx(SERVER_TEXTS.pluginsUnknown, { id })
28081
+ });
28082
+ }
28083
+ if (handle.kind === "built-in" || isPluginLocked(id)) {
28084
+ throw new HTTPException10(403, {
28085
+ message: tx(SERVER_TEXTS.pluginsTrustBuiltIn, { id })
28086
+ });
28087
+ }
28088
+ const body = await parseTrustPatchBody(c.req.raw);
28089
+ return await persistTrustAndProject(c, deps, id, body.trusted);
27775
28090
  });
27776
28091
  app.patch("/api/plugins", async (c) => {
27777
28092
  const { changes } = await parseBulkPatchBody(c.req.raw);
@@ -27789,13 +28104,21 @@ function registerPluginsRoute(app, deps) {
27789
28104
  return await persistBulkAndProject(c, deps, changes);
27790
28105
  });
27791
28106
  }
27792
- function listItems(deps, resolveEnabled) {
28107
+ function listItems(deps, resolveEnabled, trust) {
27793
28108
  const config = deps.configService.effective();
27794
28109
  return [
27795
28110
  ...deps.options.noBuiltIns ? [] : buildBuiltInItems(resolveEnabled, config),
27796
- ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled, config)
28111
+ ...buildDiscoveredItems(deps.pluginRuntime.discovered, deps, resolveEnabled, config, trust)
27797
28112
  ];
27798
28113
  }
28114
+ async function loadTrustState(deps) {
28115
+ const trustMap = await tryWithSqlite(
28116
+ { databasePath: deps.options.dbPath, autoBackup: false },
28117
+ (adapter) => adapter.trust.loadTrustMap()
28118
+ ) ?? /* @__PURE__ */ new Map();
28119
+ const trustProjectEnabled = deps.configService.effective().pluginTrust?.projectEnabled ?? false;
28120
+ return { trustMap, trustProjectEnabled };
28121
+ }
27799
28122
  function buildBuiltInItems(resolveEnabled, config) {
27800
28123
  return sortPluginsForPresentation(builtInPlugins).map((plugin) => {
27801
28124
  const pluginLocked = isPluginLocked(plugin.id);
@@ -27833,10 +28156,10 @@ function buildBuiltInItems(resolveEnabled, config) {
27833
28156
  };
27834
28157
  });
27835
28158
  }
27836
- function buildDiscoveredItems(discovered, deps, resolveEnabled, config) {
27837
- return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled, config));
28159
+ function buildDiscoveredItems(discovered, deps, resolveEnabled, config, trust) {
28160
+ return discovered.map((plugin) => buildDiscoveredItem(plugin, deps, resolveEnabled, config, trust));
27838
28161
  }
27839
- function buildDiscoveredItem(plugin, deps, resolveEnabled, config) {
28162
+ function buildDiscoveredItem(plugin, deps, resolveEnabled, config, trust) {
27840
28163
  const pluginLocked = isPluginLocked(plugin.id);
27841
28164
  const extensions = projectExtensionRows(plugin, resolveEnabled, pluginLocked, config);
27842
28165
  const optional = optionalDiscoveredFields(plugin, extensions);
@@ -27848,8 +28171,15 @@ function buildDiscoveredItem(plugin, deps, resolveEnabled, config) {
27848
28171
  reason: plugin.reason ?? null,
27849
28172
  source: classifyPluginSource(plugin.path, deps),
27850
28173
  ...optional,
28174
+ ...discoveredFlags(plugin, pluginLocked, trust)
28175
+ };
28176
+ }
28177
+ function discoveredFlags(plugin, pluginLocked, trust) {
28178
+ const trusted = trust.trustMap.get(plugin.id) === true || trust.trustProjectEnabled;
28179
+ return {
27851
28180
  ...pluginLocked ? { locked: true } : {},
27852
- ...plugin.status === "disabled" ? { startsAsDisabled: true } : {}
28181
+ ...trusted ? { trusted: true } : {},
28182
+ ...plugin.status === "disabled" && plugin.untrusted !== true ? { startsAsDisabled: true } : {}
27853
28183
  };
27854
28184
  }
27855
28185
  function optionalDiscoveredFields(plugin, extensions) {
@@ -27941,49 +28271,54 @@ function attachRuntimeContributionErrors(items, errorsByPlugin) {
27941
28271
  if (errors && errors.length > 0) item.runtimeContributionErrors = errors;
27942
28272
  }
27943
28273
  }
27944
- async function persistAndProject(c, deps, configKey, enabled) {
27945
- const overrides = await tryWithSqlite(
28274
+ async function persistManyAndProject(c, deps, keys, enabled) {
28275
+ const cwd = deps.runtimeContext.cwd;
28276
+ for (const key of keys) {
28277
+ writeConfigValue(toEnableConfigKey2(key), enabled, { target: "project", cwd });
28278
+ }
28279
+ if (!enabled && keys.length > 0) await purgeContributionsForKeys(deps, keys);
28280
+ if (keys.length > 0) deps.configService.reload();
28281
+ return await projectListResponse(c, deps);
28282
+ }
28283
+ async function persistTrustAndProject(c, deps, pluginId, trusted) {
28284
+ const ok = await tryWithSqlite(
27946
28285
  { databasePath: deps.options.dbPath, autoBackup: false },
27947
28286
  async (adapter) => {
27948
- await applyChangeToAdapter(adapter, configKey, enabled);
27949
- return await adapter.pluginConfig.loadOverrideMap();
28287
+ await adapter.trust.set(pluginId, trusted);
28288
+ return true;
27950
28289
  }
27951
28290
  );
27952
- return projectListResponse(c, deps, overrides);
28291
+ if (ok === null) {
28292
+ throw new DbMissingError(
28293
+ tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
28294
+ );
28295
+ }
28296
+ return await projectListResponse(c, deps);
27953
28297
  }
27954
- async function persistManyAndProject(c, deps, keys, enabled) {
27955
- const overrides = await tryWithSqlite(
28298
+ function toEnableConfigKey2(id) {
28299
+ const slash = id.indexOf("/");
28300
+ if (slash < 0) return `plugins.${id}.enabled`;
28301
+ return `plugins.${id.slice(0, slash)}.extensions.${id.slice(slash + 1)}.enabled`;
28302
+ }
28303
+ async function purgeContributionsForKeys(deps, keys) {
28304
+ await tryWithSqlite(
27956
28305
  { databasePath: deps.options.dbPath, autoBackup: false },
27957
28306
  async (adapter) => {
27958
28307
  for (const key of keys) {
27959
- await applyChangeToAdapter(adapter, key, enabled);
28308
+ const slash = key.indexOf("/");
28309
+ if (slash < 0) {
28310
+ await adapter.contributions.purgeByPlugin(key);
28311
+ } else {
28312
+ await adapter.contributions.purgeByPlugin(key.slice(0, slash), key.slice(slash + 1));
28313
+ }
27960
28314
  }
27961
- return await adapter.pluginConfig.loadOverrideMap();
27962
28315
  }
27963
28316
  );
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
28317
  }
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);
28318
+ async function projectListResponse(c, deps) {
28319
+ const resolveEnabled = composeResolver2(deps);
28320
+ const trust = await loadTrustState(deps);
28321
+ const items = listItems(deps, resolveEnabled, trust);
27987
28322
  return c.json(
27988
28323
  buildListEnvelope({
27989
28324
  kind: "plugins",
@@ -28088,27 +28423,26 @@ function handleExtensionSettings(handle, extensionId) {
28088
28423
  return readManifestSettings(ext?.instance);
28089
28424
  }
28090
28425
  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();
28426
+ const { disabledKeys, toggleTouched } = applyBulkEnableWrites(deps, changes);
28427
+ const settingsTouched = persistBulkSettings(deps, changes);
28428
+ if (disabledKeys.length > 0) await purgeContributionsForKeys(deps, disabledKeys);
28429
+ if (toggleTouched || settingsTouched) deps.configService.reload();
28430
+ return await projectListResponse(c, deps);
28431
+ }
28432
+ function applyBulkEnableWrites(deps, changes) {
28433
+ const cwd = deps.runtimeContext.cwd;
28434
+ const disabledKeys = [];
28435
+ let toggleTouched = false;
28436
+ for (const change of changes) {
28437
+ if (change.enabled === void 0) continue;
28438
+ const writeKeys = expandBulkChangeKeys(change, deps);
28439
+ for (const key of writeKeys) {
28440
+ writeConfigValue(toEnableConfigKey2(key), change.enabled, { target: "project", cwd });
28441
+ if (!change.enabled) disabledKeys.push(key);
28102
28442
  }
28103
- );
28104
- if (overrides === null) {
28105
- throw new DbMissingError(
28106
- tx(SERVER_TEXTS.pluginsDbMissing, { path: deps.options.dbPath })
28107
- );
28443
+ if (writeKeys.length > 0) toggleTouched = true;
28108
28444
  }
28109
- const settingsTouched = persistBulkSettings(deps, changes);
28110
- if (settingsTouched) deps.configService.reload();
28111
- return projectListResponse(c, deps, overrides);
28445
+ return { disabledKeys, toggleTouched };
28112
28446
  }
28113
28447
  function persistBulkSettings(deps, changes) {
28114
28448
  const cwd = deps.runtimeContext.cwd;
@@ -28143,13 +28477,11 @@ function expandBulkChangeKeys(change, deps) {
28143
28477
  }
28144
28478
  async function buildFreshResolver2(deps) {
28145
28479
  return buildFreshResolver({
28146
- databasePath: deps.options.dbPath,
28147
- effectiveConfig: () => deps.configService.effective(),
28148
- fallbackResolver: deps.pluginRuntime.resolveEnabled
28480
+ effectiveConfig: () => deps.configService.effective()
28149
28481
  });
28150
28482
  }
28151
- function composeResolver2(deps, overrides) {
28152
- return composeResolver(deps.configService.effective(), overrides);
28483
+ function composeResolver2(deps) {
28484
+ return composeResolver(deps.configService.effective());
28153
28485
  }
28154
28486
  function findHandle(id, deps) {
28155
28487
  const builtIn = builtInPlugins.find((b) => b.id === id);
@@ -28467,6 +28799,12 @@ function buildEnvelope3(deps) {
28467
28799
  cwd,
28468
28800
  default: []
28469
28801
  }) ?? []
28802
+ },
28803
+ pluginTrust: {
28804
+ projectEnabled: readConfigValue("pluginTrust.projectEnabled", {
28805
+ cwd,
28806
+ default: false
28807
+ }) ?? false
28470
28808
  }
28471
28809
  };
28472
28810
  }
@@ -28474,8 +28812,32 @@ async function applyPatch3(deps, body) {
28474
28812
  const cwd = deps.runtimeContext.cwd;
28475
28813
  const policyChanged = typeof body.allowSidecarWriters === "boolean" && writeSidecarWritersPolicy(body.allowSidecarWriters, cwd);
28476
28814
  const scan = applyScanWrites(body, cwd);
28815
+ const trustChanged = applyTrustWrite(body, cwd);
28477
28816
  if (policyChanged || scan.mutated) await maybeRestartWatcher2(deps);
28478
- if (policyChanged || scan.attempted) deps.configService.reload();
28817
+ if (policyChanged || scan.attempted || trustChanged) deps.configService.reload();
28818
+ }
28819
+ function applyTrustWrite(body, cwd) {
28820
+ const next = body.pluginTrust?.projectEnabled;
28821
+ if (next === void 0) return false;
28822
+ const before = readConfigValue("pluginTrust.projectEnabled", { cwd, default: false }) ?? false;
28823
+ if (before === next) return false;
28824
+ if (projectTrustExposure({ value: next, cwd }).expandsSurface && body.confirm !== true) {
28825
+ throw new HTTPException13(412, {
28826
+ message: SERVER_TEXTS.projectPrefsTrustConfirmRequired
28827
+ });
28828
+ }
28829
+ try {
28830
+ writeConfigValue("pluginTrust.projectEnabled", next, { target: "project-local", cwd });
28831
+ } catch (err) {
28832
+ throw new HTTPException13(400, {
28833
+ message: tx(SERVER_TEXTS.projectPrefsPersistFailed, {
28834
+ key: "pluginTrust.projectEnabled",
28835
+ message: formatErrorMessage(err)
28836
+ })
28837
+ });
28838
+ }
28839
+ log.warn(tx(SERVER_TEXTS.projectPrefsTrustSet, { value: String(next) }));
28840
+ return true;
28479
28841
  }
28480
28842
  function applyScanWrites(body, cwd) {
28481
28843
  const writes = collectWrites(body);
@@ -28627,7 +28989,11 @@ function isExistingDirectory(entry, cwd) {
28627
28989
  var PATCH_BODY_SCHEMA3 = {
28628
28990
  type: "object",
28629
28991
  additionalProperties: false,
28630
- anyOf: [{ required: ["allowSidecarWriters"] }, { required: ["scan"] }],
28992
+ anyOf: [
28993
+ { required: ["allowSidecarWriters"] },
28994
+ { required: ["scan"] },
28995
+ { required: ["pluginTrust"] }
28996
+ ],
28631
28997
  properties: {
28632
28998
  confirm: { type: "boolean" },
28633
28999
  allowSidecarWriters: { type: "boolean" },
@@ -28641,6 +29007,14 @@ var PATCH_BODY_SCHEMA3 = {
28641
29007
  items: { type: "string", pattern: "^[^,]+$" }
28642
29008
  }
28643
29009
  }
29010
+ },
29011
+ pluginTrust: {
29012
+ type: "object",
29013
+ additionalProperties: false,
29014
+ minProperties: 1,
29015
+ properties: {
29016
+ projectEnabled: { type: "boolean" }
29017
+ }
28644
29018
  }
28645
29019
  }
28646
29020
  };
@@ -28652,6 +29026,9 @@ var parsePatchBody4 = makeBodyValidator(PATCH_BODY_SCHEMA3, {
28652
29026
  ":anyOf": SERVER_TEXTS.projectPrefsBodyEmpty,
28653
29027
  "/scan:minProperties": SERVER_TEXTS.projectPrefsBodyEmpty,
28654
29028
  "/scan:type:object": SERVER_TEXTS.projectPrefsScanNotObject,
29029
+ "/pluginTrust:minProperties": SERVER_TEXTS.projectPrefsBodyEmpty,
29030
+ "/pluginTrust:type:object": SERVER_TEXTS.projectPrefsTrustNotObject,
29031
+ "/pluginTrust/projectEnabled:type:boolean": SERVER_TEXTS.projectPrefsTrustEnabledNotBoolean,
28655
29032
  "/confirm:type:boolean": SERVER_TEXTS.projectPrefsConfirmNotBoolean,
28656
29033
  "/allowSidecarWriters:type:boolean": SERVER_TEXTS.projectPrefsSidecarWritersNotBoolean,
28657
29034
  "/scan/referencePaths:type:array": tx(SERVER_TEXTS.projectPrefsListNotArray, { key: "scan.referencePaths" }),
@@ -28694,9 +29071,7 @@ async function buildEnvelope4(deps) {
28694
29071
  }
28695
29072
  async function resolveSelectableProviders(deps) {
28696
29073
  const resolveEnabled = await buildFreshResolver({
28697
- databasePath: deps.options.dbPath,
28698
- effectiveConfig: () => deps.configService.effective(),
28699
- fallbackResolver: deps.pluginRuntime.resolveEnabled
29074
+ effectiveConfig: () => deps.configService.effective()
28700
29075
  });
28701
29076
  const selectable = /* @__PURE__ */ new Set();
28702
29077
  for (const provider of deps.providers) {
@@ -29263,9 +29638,7 @@ async function runPersistedScan(c, deps) {
29263
29638
  }
29264
29639
  async function buildBffResolverOverride(deps) {
29265
29640
  return buildFreshResolver({
29266
- databasePath: deps.options.dbPath,
29267
- effectiveConfig: () => deps.configService.effective(),
29268
- fallbackResolver: deps.pluginRuntime.resolveEnabled
29641
+ effectiveConfig: () => deps.configService.effective()
29269
29642
  });
29270
29643
  }
29271
29644
  async function loadPersistedScanMeta(deps) {
@@ -30604,7 +30977,7 @@ var SERVE_TEXTS = {
30604
30977
  // cli/commands/serve.ts
30605
30978
  var ServeCommand = class extends SmCommand {
30606
30979
  static paths = [["serve"]];
30607
- static usage = Command35.Usage({
30980
+ static usage = Command36.Usage({
30608
30981
  category: "Setup",
30609
30982
  description: "Start the Hono BFF (single-port: REST + WebSocket + SPA bundle).",
30610
30983
  details: `
@@ -30628,18 +31001,18 @@ var ServeCommand = class extends SmCommand {
30628
31001
  ["Point at a pre-built UI bundle", "$0 serve --ui-dist ./ui/dist/browser"]
30629
31002
  ]
30630
31003
  });
30631
- port = Option33.String("--port", {
31004
+ port = Option34.String("--port", {
30632
31005
  required: false,
30633
31006
  description: "Listening port (default 4242). 0 = OS-assigned."
30634
31007
  });
30635
- host = Option33.String("--host", {
31008
+ host = Option34.String("--host", {
30636
31009
  required: false,
30637
31010
  description: "Listening host (default 127.0.0.1). Loopback-only enforced when --dev-cors is set."
30638
31011
  });
30639
- noBuiltIns = Option33.Boolean("--no-built-ins", false, {
31012
+ noBuiltIns = Option34.Boolean("--no-built-ins", false, {
30640
31013
  description: "Skip built-in plugin registration (parity with sm scan --no-built-ins)."
30641
31014
  });
30642
- noPlugins = Option33.Boolean("--no-plugins", false, {
31015
+ noPlugins = Option34.Boolean("--no-plugins", false, {
30643
31016
  description: "Skip drop-in plugin discovery."
30644
31017
  });
30645
31018
  // `Option.Boolean('--open', true)`, Clipanion's parser auto-derives
@@ -30649,35 +31022,35 @@ var ServeCommand = class extends SmCommand {
30649
31022
  // two registrations for the same flag and rejects the invocation
30650
31023
  // with "Ambiguous Syntax Error". Same convention shipped by every
30651
31024
  // other `--no-...` flag in the CLI tree.
30652
- open = Option33.Boolean("--open", true, {
31025
+ open = Option34.Boolean("--open", true, {
30653
31026
  description: "Auto-open the SPA in the user's default browser after listen. --no-open opts out."
30654
31027
  });
30655
- devCors = Option33.Boolean("--dev-cors", false, {
31028
+ devCors = Option34.Boolean("--dev-cors", false, {
30656
31029
  description: "Enable permissive CORS for the Angular dev-server proxy workflow."
30657
31030
  });
30658
31031
  // `--ui-dist` is intentionally undocumented in the Usage block above
30659
31032
  // (the demo build pipeline + tests rely on it; everyday users never
30660
31033
  // need it). Clipanion still exposes it on the parser; the Usage
30661
31034
  // 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, {
31035
+ uiDist = Option34.String("--ui-dist", { required: false, hidden: true });
31036
+ noUi = Option34.Boolean("--no-ui", false, {
30664
31037
  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
31038
  });
30666
- noWatcher = Option33.Boolean("--no-watcher", false, {
31039
+ noWatcher = Option34.Boolean("--no-watcher", false, {
30667
31040
  description: "Disable the chokidar-fed scan-and-broadcast loop. Use only for CI / read-only deployments."
30668
31041
  });
30669
- yes = Option33.Boolean("--yes", false, {
31042
+ yes = Option34.Boolean("--yes", false, {
30670
31043
  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
31044
  });
30672
31045
  // `--watcher-debounce-ms` is undocumented sugar for advanced users
30673
31046
  // who want to tighten / relax the watcher's batching window without
30674
31047
  // 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", {
31048
+ watcherDebounceMs = Option34.String("--watcher-debounce-ms", { required: false, hidden: true });
31049
+ maxScan = Option34.String("--max-scan", {
30677
31050
  required: false,
30678
31051
  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
31052
  });
30680
- maxNodes = Option33.String("--max-nodes", {
31053
+ maxNodes = Option34.String("--max-nodes", {
30681
31054
  required: false,
30682
31055
  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
31056
  });
@@ -31041,7 +31414,7 @@ function tryOpenBrowser(url, stderr, warnGlyph) {
31041
31414
  }
31042
31415
 
31043
31416
  // cli/commands/show.ts
31044
- import { Command as Command36, Option as Option34 } from "clipanion";
31417
+ import { Command as Command37, Option as Option35 } from "clipanion";
31045
31418
 
31046
31419
  // cli/i18n/show.texts.ts
31047
31420
  var SHOW_TEXTS = {
@@ -31092,7 +31465,7 @@ var SHOW_TEXTS = {
31092
31465
  // cli/commands/show.ts
31093
31466
  var ShowCommand = class extends SmCommand {
31094
31467
  static paths = [["show"]];
31095
- static usage = Command36.Usage({
31468
+ static usage = Command37.Usage({
31096
31469
  category: "Browse",
31097
31470
  description: "Node detail: weight, frontmatter, links, issues.",
31098
31471
  details: `
@@ -31108,7 +31481,7 @@ var ShowCommand = class extends SmCommand {
31108
31481
  ["Machine-readable detail", "$0 show .claude/agents/architect.md --json"]
31109
31482
  ]
31110
31483
  });
31111
- nodePath = Option34.String({ required: true });
31484
+ nodePath = Option35.String({ required: true });
31112
31485
  async run() {
31113
31486
  const dbPath = resolveDbPath({ db: this.db, ...defaultRuntimeContext() });
31114
31487
  const exit = requireDbOrExit(dbPath, this.context.stderr);
@@ -31350,7 +31723,7 @@ function rankConfidenceForGrouping(c) {
31350
31723
  // cli/commands/sidecar.ts
31351
31724
  import { unlink as unlink3 } from "fs/promises";
31352
31725
  import { resolve as resolve43 } from "path";
31353
- import { Command as Command37, Option as Option35 } from "clipanion";
31726
+ import { Command as Command38, Option as Option36 } from "clipanion";
31354
31727
 
31355
31728
  // cli/i18n/sidecar.texts.ts
31356
31729
  var SIDECAR_TEXTS = {
@@ -31431,7 +31804,7 @@ async function runWithSidecarConsent(bag, ansi, dispatch) {
31431
31804
  }
31432
31805
  var SidecarRefreshCommand = class extends SmCommand {
31433
31806
  static paths = [["sidecar", "refresh"]];
31434
- static usage = Command37.Usage({
31807
+ static usage = Command38.Usage({
31435
31808
  category: "Actions",
31436
31809
  description: "Refresh a sidecar's `for.{bodyHash, frontmatterHash}` to match the live node. Does NOT bump the version.",
31437
31810
  details: `
@@ -31448,8 +31821,8 @@ var SidecarRefreshCommand = class extends SmCommand {
31448
31821
  ["Refresh a node's sidecar hashes", "$0 sidecar refresh .claude/agents/architect.md"]
31449
31822
  ]
31450
31823
  });
31451
- nodePath = Option35.String({ required: true });
31452
- yes = Option35.Boolean("--yes", false, {
31824
+ nodePath = Option36.String({ required: true });
31825
+ yes = Option36.Boolean("--yes", false, {
31453
31826
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
31454
31827
  });
31455
31828
  async run() {
@@ -31571,7 +31944,7 @@ var SidecarRefreshCommand = class extends SmCommand {
31571
31944
  };
31572
31945
  var SidecarPruneCommand = class extends SmCommand {
31573
31946
  static paths = [["sidecar", "prune"]];
31574
- static usage = Command37.Usage({
31947
+ static usage = Command38.Usage({
31575
31948
  category: "Actions",
31576
31949
  description: "Delete orphan .sm files (sidecars whose accompanying .md no longer exists).",
31577
31950
  details: `
@@ -31593,8 +31966,8 @@ var SidecarPruneCommand = class extends SmCommand {
31593
31966
  ["Delete every orphan .sm file (non-interactive)", "$0 sidecar prune --yes"]
31594
31967
  ]
31595
31968
  });
31596
- dryRun = Option35.Boolean("-n,--dry-run", false);
31597
- yes = Option35.Boolean("--yes,--force", false, {
31969
+ dryRun = Option36.Boolean("-n,--dry-run", false);
31970
+ yes = Option36.Boolean("--yes,--force", false, {
31598
31971
  description: "Skip the interactive confirmation prompt. Required for non-interactive callers (CI, pre-commit hooks)."
31599
31972
  });
31600
31973
  // Complexity is from per-orphan handling, empty-set / dry-run /
@@ -31714,7 +32087,7 @@ var SidecarPruneCommand = class extends SmCommand {
31714
32087
  };
31715
32088
  var SidecarAnnotateCommand = class extends SmCommand {
31716
32089
  static paths = [["sidecar", "annotate"]];
31717
- static usage = Command37.Usage({
32090
+ static usage = Command38.Usage({
31718
32091
  category: "Actions",
31719
32092
  description: "Scaffold an empty `<basename>.sm` next to a node ready for editing.",
31720
32093
  details: `
@@ -31732,9 +32105,9 @@ var SidecarAnnotateCommand = class extends SmCommand {
31732
32105
  ["Overwrite an existing one", "$0 sidecar annotate .claude/agents/architect.md --force"]
31733
32106
  ]
31734
32107
  });
31735
- nodePath = Option35.String({ required: true });
31736
- force = Option35.Boolean("--force", false);
31737
- yes = Option35.Boolean("--yes", false, {
32108
+ nodePath = Option36.String({ required: true });
32109
+ force = Option36.Boolean("--force", false);
32110
+ yes = Option36.Boolean("--yes", false, {
31738
32111
  description: "Confirm writing .sm sidecar files in this project (sets allowEditSmFiles=true on first run)."
31739
32112
  });
31740
32113
  async run() {
@@ -31873,7 +32246,7 @@ var SIDECAR_COMMANDS = [
31873
32246
  ];
31874
32247
 
31875
32248
  // cli/commands/stubs.ts
31876
- import { Command as Command38, Option as Option36 } from "clipanion";
32249
+ import { Command as Command39, Option as Option37 } from "clipanion";
31877
32250
 
31878
32251
  // cli/i18n/stubs.texts.ts
31879
32252
  var STUBS_TEXTS = {
@@ -31899,7 +32272,7 @@ var StubCommand = class extends SmCommand {
31899
32272
  };
31900
32273
  var DoctorCommand = class extends StubCommand {
31901
32274
  static paths = [["doctor"]];
31902
- static usage = Command38.Usage({
32275
+ static usage = Command39.Usage({
31903
32276
  category: "Setup",
31904
32277
  description: planned("Diagnostic report: DB integrity, pending migrations, orphan rows, plugin status, runner availability.")
31905
32278
  });
@@ -31907,18 +32280,18 @@ var DoctorCommand = class extends StubCommand {
31907
32280
  };
31908
32281
  var FindingsCommand = class extends StubCommand {
31909
32282
  static paths = [["findings"]];
31910
- static usage = Command38.Usage({
32283
+ static usage = Command39.Usage({
31911
32284
  category: "Browse",
31912
32285
  description: planned("Probabilistic findings: injection, stale summaries, low confidence.")
31913
32286
  });
31914
- kind = Option36.String("--kind", { required: false });
31915
- since = Option36.String("--since", { required: false });
31916
- threshold = Option36.String("--threshold", { required: false });
32287
+ kind = Option37.String("--kind", { required: false });
32288
+ since = Option37.String("--since", { required: false });
32289
+ threshold = Option37.String("--threshold", { required: false });
31917
32290
  verbName = "findings";
31918
32291
  };
31919
32292
  var ActionsListCommand = class extends StubCommand {
31920
32293
  static paths = [["actions", "list"]];
31921
- static usage = Command38.Usage({
32294
+ static usage = Command39.Usage({
31922
32295
  category: "Jobs",
31923
32296
  description: planned("Registered action types (manifest view).")
31924
32297
  });
@@ -31926,103 +32299,103 @@ var ActionsListCommand = class extends StubCommand {
31926
32299
  };
31927
32300
  var ActionsShowCommand = class extends StubCommand {
31928
32301
  static paths = [["actions", "show"]];
31929
- static usage = Command38.Usage({
32302
+ static usage = Command39.Usage({
31930
32303
  category: "Jobs",
31931
32304
  description: planned("Full action manifest, including preconditions and expected duration.")
31932
32305
  });
31933
- id = Option36.String({ required: true });
32306
+ id = Option37.String({ required: true });
31934
32307
  verbName = "actions show";
31935
32308
  };
31936
32309
  var JobSubmitCommand = class extends StubCommand {
31937
32310
  static paths = [["job", "submit"]];
31938
- static usage = Command38.Usage({
32311
+ static usage = Command39.Usage({
31939
32312
  category: "Jobs",
31940
32313
  description: planned("Enqueue a single job or fan out to every matching node (--all).")
31941
32314
  });
31942
- action = Option36.String({ required: true });
31943
- node = Option36.String("-n", { required: false });
31944
- all = Option36.Boolean("--all", false);
32315
+ action = Option37.String({ required: true });
32316
+ node = Option37.String("-n", { required: false });
32317
+ all = Option37.Boolean("--all", false);
31945
32318
  // CLI flag stays `--run`; field name is `runFlag` per the
31946
32319
  // 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 });
32320
+ runFlag = Option37.Boolean("--run", false);
32321
+ force = Option37.Boolean("--force", false);
32322
+ ttl = Option37.String("--ttl", { required: false });
32323
+ priority = Option37.String("--priority", { required: false });
31951
32324
  verbName = "job submit";
31952
32325
  };
31953
32326
  var JobListCommand = class extends StubCommand {
31954
32327
  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 });
32328
+ static usage = Command39.Usage({ category: "Jobs", description: planned("List jobs.") });
32329
+ status = Option37.String("--status", { required: false });
32330
+ action = Option37.String("--action", { required: false });
32331
+ node = Option37.String("--node", { required: false });
31959
32332
  verbName = "job list";
31960
32333
  };
31961
32334
  var JobShowCommand = class extends StubCommand {
31962
32335
  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 });
32336
+ static usage = Command39.Usage({ category: "Jobs", description: planned("Job detail: state, claim time, TTL, runner, content hash.") });
32337
+ id = Option37.String({ required: true });
31965
32338
  verbName = "job show";
31966
32339
  };
31967
32340
  var JobPreviewCommand = class extends StubCommand {
31968
32341
  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 });
32342
+ static usage = Command39.Usage({ category: "Jobs", description: planned("Render the job MD file without executing.") });
32343
+ id = Option37.String({ required: true });
31971
32344
  verbName = "job preview";
31972
32345
  };
31973
32346
  var JobClaimCommand = class extends StubCommand {
31974
32347
  static paths = [["job", "claim"]];
31975
- static usage = Command38.Usage({
32348
+ static usage = Command39.Usage({
31976
32349
  category: "Jobs",
31977
32350
  description: planned("Atomic primitive: return next queued job id, mark it running.")
31978
32351
  });
31979
- filter = Option36.String("--filter", { required: false });
32352
+ filter = Option37.String("--filter", { required: false });
31980
32353
  verbName = "job claim";
31981
32354
  };
31982
32355
  var JobRunCommand = class extends StubCommand {
31983
32356
  static paths = [["job", "run"]];
31984
- static usage = Command38.Usage({
32357
+ static usage = Command39.Usage({
31985
32358
  category: "Jobs",
31986
32359
  description: planned("Full CLI-runner loop: claim + spawn + record.")
31987
32360
  });
31988
- all = Option36.Boolean("--all", false);
31989
- max = Option36.String("--max", { required: false });
32361
+ all = Option37.Boolean("--all", false);
32362
+ max = Option37.String("--max", { required: false });
31990
32363
  verbName = "job run";
31991
32364
  };
31992
32365
  var JobStatusCommand = class extends StubCommand {
31993
32366
  static paths = [["job", "status"]];
31994
- static usage = Command38.Usage({
32367
+ static usage = Command39.Usage({
31995
32368
  category: "Jobs",
31996
32369
  description: planned("Counts (per status) or single-job status.")
31997
32370
  });
31998
- id = Option36.String({ required: false });
32371
+ id = Option37.String({ required: false });
31999
32372
  verbName = "job status";
32000
32373
  };
32001
32374
  var JobCancelCommand = class extends StubCommand {
32002
32375
  static paths = [["job", "cancel"]];
32003
- static usage = Command38.Usage({
32376
+ static usage = Command39.Usage({
32004
32377
  category: "Jobs",
32005
32378
  description: planned("Force a running job to failed with reason user-cancelled.")
32006
32379
  });
32007
- id = Option36.String({ required: false });
32008
- all = Option36.Boolean("--all", false);
32380
+ id = Option37.String({ required: false });
32381
+ all = Option37.Boolean("--all", false);
32009
32382
  verbName = "job cancel";
32010
32383
  };
32011
32384
  var RecordCommand = class extends StubCommand {
32012
32385
  static paths = [["record"]];
32013
- static usage = Command38.Usage({
32386
+ static usage = Command39.Usage({
32014
32387
  category: "Jobs",
32015
32388
  description: planned("Close a running job with success or failure. Nonce is the sole credential.")
32016
32389
  });
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 });
32390
+ id = Option37.String("--id", { required: true });
32391
+ nonce = Option37.String("--nonce", { required: true });
32392
+ status = Option37.String("--status", { required: true });
32393
+ report = Option37.String("--report", { required: false });
32394
+ tokensIn = Option37.String("--tokens-in", { required: false });
32395
+ tokensOut = Option37.String("--tokens-out", { required: false });
32396
+ durationMs = Option37.String("--duration-ms", { required: false });
32397
+ model = Option37.String("--model", { required: false });
32398
+ error = Option37.String("--error", { required: false });
32026
32399
  verbName = "record";
32027
32400
  };
32028
32401
  var STUB_COMMANDS = [
@@ -32046,7 +32419,7 @@ import { cpSync as cpSync3, existsSync as existsSync33, mkdirSync as mkdirSync6,
32046
32419
  import { dirname as dirname21, join as join21, resolve as resolve44 } from "path";
32047
32420
  import { createInterface as createInterface6 } from "readline";
32048
32421
  import { fileURLToPath as fileURLToPath8 } from "url";
32049
- import { Command as Command39, Option as Option37 } from "clipanion";
32422
+ import { Command as Command40, Option as Option38 } from "clipanion";
32050
32423
 
32051
32424
  // cli/i18n/tutorial.texts.ts
32052
32425
  var TUTORIAL_TEXTS = {
@@ -32110,7 +32483,7 @@ var TRIGGER_EN = "run the tutorial";
32110
32483
  var TRIGGER_ES = "ejecuta el tutorial";
32111
32484
  var TutorialCommand = class extends SmCommand {
32112
32485
  static paths = [["tutorial"]];
32113
- static usage = Command39.Usage({
32486
+ static usage = Command40.Usage({
32114
32487
  category: "Setup",
32115
32488
  description: "Materialize an interactive tester tutorial as a Claude Code skill folder under `<cwd>/.claude/skills/`.",
32116
32489
  details: `
@@ -32139,18 +32512,18 @@ var TutorialCommand = class extends SmCommand {
32139
32512
  // more. Accept one so a stale `sm tutorial master` lands on a friendly
32140
32513
  // usage error (guarded in `run()`) instead of clipanion's generic
32141
32514
  // "extraneous argument" message.
32142
- legacyPositional = Option37.String({ required: false });
32515
+ legacyPositional = Option38.String({ required: false });
32143
32516
  // Named `forProvider`, NOT `for` (reserved word). The CLI surface stays
32144
32517
  // `--for`; selects the destination Provider whose `scaffold.skillDir`
32145
32518
  // the skill is materialised under, skipping the interactive prompt.
32146
- forProvider = Option37.String("--for", {
32519
+ forProvider = Option38.String("--for", {
32147
32520
  required: false,
32148
32521
  description: "Destination provider id (e.g. claude). Skips the prompt."
32149
32522
  });
32150
- force = Option37.Boolean("--force", false, {
32523
+ force = Option38.Boolean("--force", false, {
32151
32524
  description: "Overwrite an existing target directory without prompting."
32152
32525
  });
32153
- experimental = Option37.Boolean("--experimental", false, {
32526
+ experimental = Option38.Boolean("--experimental", false, {
32154
32527
  description: "Offer experimental providers as destinations. They ship disabled; enable the chosen one with `sm plugins enable <id>`."
32155
32528
  });
32156
32529
  async run() {
@@ -32384,7 +32757,7 @@ function resolveSkillSourceDir() {
32384
32757
  }
32385
32758
 
32386
32759
  // cli/commands/version.ts
32387
- import { Command as Command40 } from "clipanion";
32760
+ import { Command as Command41 } from "clipanion";
32388
32761
 
32389
32762
  // cli/i18n/version.texts.ts
32390
32763
  var VERSION_TEXTS = {
@@ -32399,7 +32772,7 @@ var VERSION_TEXTS = {
32399
32772
  // cli/commands/version.ts
32400
32773
  var VersionCommand = class extends SmCommand {
32401
32774
  static paths = [["version"]];
32402
- static usage = Command40.Usage({
32775
+ static usage = Command41.Usage({
32403
32776
  category: "Introspection",
32404
32777
  description: "Print the CLI / spec / runtime / db-schema version matrix."
32405
32778
  });
@@ -32616,4 +32989,4 @@ function resolveBareDefault() {
32616
32989
  process.exit(ExitCode.Error);
32617
32990
  }
32618
32991
  //# sourceMappingURL=cli.js.map
32619
- //# debugId=a842c024-162c-5a3a-85db-1daea4b6735f
32992
+ //# debugId=75d0c2c2-d08f-5d7c-aa62-3e1d53dd4ef1