@skill-map/cli 0.16.5 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -660,129 +660,187 @@ var skill_schema_default = {
660
660
  $schema: "https://json-schema.org/draft/2020-12/schema",
661
661
  $id: "https://skill-map.dev/providers/claude/v1/frontmatter/skill.schema.json",
662
662
  title: "FrontmatterSkill",
663
- description: "Frontmatter shape for nodes classified as `skill` by the Claude Provider. Extends the spec's universal `frontmatter/base.schema.json` via $ref-by-$id. Kind-specific fields: `inputs`, `outputs`. Stability: experimental \u2014 the parameter shape may tighten once summarizer needs emerge. Owned by the Claude Provider; relocated from `spec/schemas/frontmatter/` in spec 0.8.0 (Phase 3 of plug-in model overhaul).",
663
+ description: "Frontmatter shape for nodes classified as `skill` by the Claude Provider. Today identical to `command` \u2014 both extend the shared `skill-base.schema.json` per Anthropic's documented merger (https://code.claude.com/docs/en/skills.md \u2014 \"Custom commands have been merged into skills\"). The two are kept SPLIT (not aliased) because skill-map's registry differentiates them in `IProviderKind.ui` (separate label / icon / color) and `defaultRefreshAction`. Splitting also reserves room for future divergence when one kind diverges from the other. No skill-only fields today; `additionalProperties: true` so the file is ready for them.",
664
664
  allOf: [
665
- { $ref: "https://skill-map.dev/spec/v0/frontmatter/base.schema.json" }
665
+ { $ref: "https://skill-map.dev/providers/claude/v1/frontmatter/skill-base.schema.json" }
666
666
  ],
667
667
  type: "object",
668
668
  additionalProperties: true,
669
- properties: {
670
- inputs: {
671
- type: "array",
672
- description: "Declared inputs for this skill. Optional structured. Stability: experimental.",
673
- items: { $ref: "#/$defs/Parameter" }
674
- },
675
- outputs: {
676
- type: "array",
677
- description: "Declared outputs. Optional structured. Stability: experimental.",
678
- items: { $ref: "#/$defs/Parameter" }
679
- }
680
- },
681
- $defs: {
682
- Parameter: {
683
- type: "object",
684
- required: ["name"],
685
- additionalProperties: true,
686
- properties: {
687
- name: { type: "string", minLength: 1 },
688
- type: { type: "string", description: "Free-form type hint (e.g. `string`, `integer`, `path`, `glob`)." },
689
- description: { type: "string" },
690
- required: { type: "boolean", default: false },
691
- default: { description: "Any JSON value, or a templated string." }
692
- }
693
- }
694
- }
669
+ properties: {}
695
670
  };
696
671
 
697
- // built-in-plugins/providers/claude/schemas/agent.schema.json
698
- var agent_schema_default = {
672
+ // built-in-plugins/providers/claude/schemas/skill-base.schema.json
673
+ var skill_base_schema_default = {
699
674
  $schema: "https://json-schema.org/draft/2020-12/schema",
700
- $id: "https://skill-map.dev/providers/claude/v1/frontmatter/agent.schema.json",
701
- title: "FrontmatterAgent",
702
- description: "Frontmatter shape for nodes classified as `agent` by the Claude Provider. Extends the spec's universal `frontmatter/base.schema.json` via $ref-by-$id. Kind-specific field: `model`. The `tools` and `allowedTools` fields live on the spec base and apply here unchanged. Color, when needed, lives in `metadata.color` (inherited from base). Owned by the Claude Provider; relocated from `spec/schemas/frontmatter/` in spec 0.8.0 (Phase 3 of plug-in model overhaul).",
675
+ $id: "https://skill-map.dev/providers/claude/v1/frontmatter/skill-base.schema.json",
676
+ title: "FrontmatterSkillBase",
677
+ description: 'Shared frontmatter base for `skill` and `command` nodes per Anthropic\'s documented merger (https://code.claude.com/docs/en/skills.md \u2014 "Custom commands have been merged into skills"). Both kinds carry the same vendor frontmatter today; skill-map keeps them as distinct `IProviderKind`s in the registry (different UI presentation, different `defaultRefreshAction`) but extends the same base via `allOf` + `$ref` so the field catalog is single-sourced. Field naming is reproduced verbatim from Anthropic \u2014 a deliberate mix of kebab-case (`argument-hint`, `disable-model-invocation`, `user-invocable`, `allowed-tools`), snake_case (`when_to_use`), and camelCase. `additionalProperties: true` so future Anthropic additions flow through unchanged until this schema catches up.',
703
678
  allOf: [
704
679
  { $ref: "https://skill-map.dev/spec/v0/frontmatter/base.schema.json" }
705
680
  ],
706
681
  type: "object",
707
682
  additionalProperties: true,
708
683
  properties: {
684
+ when_to_use: {
685
+ type: "string",
686
+ description: "When the host SHOULD activate this skill / command. Appended to `description` for autocomplete; counts toward the 1,536-character description cap."
687
+ },
688
+ "argument-hint": {
689
+ type: "string",
690
+ description: "Autocomplete hint shown when the user types a leading `/`, e.g. `[issue-number]`."
691
+ },
692
+ arguments: {
693
+ oneOf: [
694
+ { type: "string" },
695
+ { type: "array", items: { type: "string" } }
696
+ ],
697
+ description: "Named positional arguments for `$name` substitution in the skill body. String form for one argument, array for several."
698
+ },
699
+ "disable-model-invocation": {
700
+ type: "boolean",
701
+ description: "When true, the model cannot invoke this skill autonomously \u2014 only the user can trigger it via the `/` menu."
702
+ },
703
+ "user-invocable": {
704
+ type: "boolean",
705
+ description: "When false, this skill is hidden from the user's `/` menu (still invocable by the model unless `disable-model-invocation` is also set)."
706
+ },
707
+ "allowed-tools": {
708
+ oneOf: [
709
+ { type: "string" },
710
+ { type: "array", items: { type: "string" } }
711
+ ],
712
+ description: "Tools pre-approved for this skill (no per-use permission prompt). Argument-scoped patterns supported (`Bash(git add *)`)."
713
+ },
709
714
  model: {
710
715
  type: "string",
711
- description: "Model identifier for this agent (e.g. `sonnet`, `opus`, `haiku`, or a full `claude-*` id). Free-form string; Providers MAY validate against a platform-specific enum."
716
+ description: "Model alias (`sonnet`, `opus`, `haiku`), full Claude id, or the literal `inherit` to defer to the parent session's model."
717
+ },
718
+ effort: {
719
+ type: "string",
720
+ enum: ["low", "medium", "high", "xhigh", "max"],
721
+ description: "Effort budget for reasoning. Higher levels allocate more thinking time."
722
+ },
723
+ context: {
724
+ type: "string",
725
+ enum: ["fork"],
726
+ description: "Execution context. `fork` is the only documented value today (runs the skill in a subagent so the main session's context is preserved)."
727
+ },
728
+ agent: {
729
+ type: "string",
730
+ description: "Subagent type when `context: fork`. Built-ins include `Explore`, `Plan`, `general-purpose`; custom agent types resolve against the host's agent registry."
731
+ },
732
+ hooks: {
733
+ type: "object",
734
+ description: "Lifecycle hooks declared inline on this skill / command. Shape is platform-defined; preserved as an opaque object. `once: true` is honored ONLY here (not at the agent level). Note: Anthropic's `hooks` is NOT a separate node kind \u2014 it lives here, as a sub-object of skill frontmatter, or in `settings.json` (see https://code.claude.com/docs/en/hooks.md)."
735
+ },
736
+ paths: {
737
+ oneOf: [
738
+ { type: "string" },
739
+ { type: "array", items: { type: "string" } }
740
+ ],
741
+ description: "Glob patterns limiting auto-activation. The skill auto-activates only when the user is editing files matching one of these globs."
742
+ },
743
+ shell: {
744
+ type: "string",
745
+ enum: ["bash", "powershell"],
746
+ description: "Shell flavor for any embedded shell snippets. Default: `bash`."
712
747
  }
713
748
  }
714
749
  };
715
750
 
716
- // built-in-plugins/providers/claude/schemas/command.schema.json
717
- var command_schema_default = {
751
+ // built-in-plugins/providers/claude/schemas/agent.schema.json
752
+ var agent_schema_default = {
718
753
  $schema: "https://json-schema.org/draft/2020-12/schema",
719
- $id: "https://skill-map.dev/providers/claude/v1/frontmatter/command.schema.json",
720
- title: "FrontmatterCommand",
721
- description: "Frontmatter shape for nodes classified as `command` by the Claude Provider. Extends the spec's universal `frontmatter/base.schema.json` via $ref-by-$id. Kind-specific fields: `args`, `shortcut`. Owned by the Claude Provider; relocated from `spec/schemas/frontmatter/` in spec 0.8.0 (Phase 3 of plug-in model overhaul).",
754
+ $id: "https://skill-map.dev/providers/claude/v1/frontmatter/agent.schema.json",
755
+ title: "FrontmatterAgent",
756
+ description: "Frontmatter shape for nodes classified as `agent` by the Claude Provider. Mirrors Anthropic's documented agent frontmatter verbatim (https://code.claude.com/docs/en/agents.md): `name` and `description` come from the spec base; this schema adds the 14 vendor-specific fields. skill-map AGGREGATES the vendor spec, it does not curate it \u2014 keys are reproduced exactly as Anthropic publishes them (mix of camelCase and snake_case). `additionalProperties: true` so future Anthropic additions flow through unchanged until this schema catches up.",
722
757
  allOf: [
723
758
  { $ref: "https://skill-map.dev/spec/v0/frontmatter/base.schema.json" }
724
759
  ],
725
760
  type: "object",
726
761
  additionalProperties: true,
727
762
  properties: {
728
- args: {
763
+ tools: {
729
764
  type: "array",
730
- description: "Declared positional / named arguments for this command.",
731
- items: { $ref: "#/$defs/CommandArg" }
765
+ description: "Allowlist of tools this agent is permitted to invoke. Argument-scoped patterns supported (e.g. `Bash(git add *)`). Free-form strings \u2014 token vocabulary is platform-specific.",
766
+ items: { type: "string" }
732
767
  },
733
- shortcut: {
768
+ disallowedTools: {
769
+ type: "array",
770
+ description: "Denylist of tools this agent MUST NOT invoke. Free-form strings \u2014 token vocabulary is platform-specific.",
771
+ items: { type: "string" }
772
+ },
773
+ model: {
734
774
  type: "string",
735
- description: "Keyboard shortcut hint. Platform-specific notation (e.g. `cmd+shift+k`, `ctrl+alt+m`). Purely advisory."
736
- }
737
- },
738
- $defs: {
739
- CommandArg: {
775
+ description: "Model alias (`sonnet`, `opus`, `haiku`), full Claude id (e.g. `claude-3-7-sonnet-latest`), or the literal `inherit` to defer to the parent session's model."
776
+ },
777
+ permissionMode: {
778
+ type: "string",
779
+ enum: ["default", "acceptEdits", "auto", "dontAsk", "bypassPermissions", "plan"],
780
+ description: "How the agent handles permission prompts. See https://code.claude.com/docs/en/agents.md."
781
+ },
782
+ maxTurns: {
783
+ type: "integer",
784
+ minimum: 1,
785
+ description: "Hard cap on agentic turns this agent may take in one invocation."
786
+ },
787
+ skills: {
788
+ type: "array",
789
+ description: "Skills preloaded into this agent's context. Identifiers reference skill nodes available to the host.",
790
+ items: { type: "string" }
791
+ },
792
+ mcpServers: {
793
+ type: "array",
794
+ description: "MCP servers this agent connects to at startup. Shape is platform-defined; preserved as opaque objects.",
795
+ items: { type: "object" }
796
+ },
797
+ hooks: {
740
798
  type: "object",
741
- required: ["name"],
742
- additionalProperties: true,
743
- properties: {
744
- name: { type: "string", minLength: 1 },
745
- type: {
746
- type: "string",
747
- description: "Free-form type hint (e.g. `string`, `integer`, `path`, `boolean`, `enum:a|b|c`)."
748
- },
749
- required: { type: "boolean", default: false },
750
- description: { type: "string" },
751
- default: { description: "Any JSON value." }
752
- }
799
+ description: "Lifecycle hooks (`PreToolUse`, `PostToolUse`, `Stop`, etc.) declared inline on the agent. Shape is platform-defined; preserved as an opaque object. Note: Anthropic's `hooks` is NOT a separate node kind \u2014 it lives here, as a sub-object of agent frontmatter, or in `settings.json` (see https://code.claude.com/docs/en/hooks.md)."
800
+ },
801
+ memory: {
802
+ type: "string",
803
+ enum: ["user", "project", "local"],
804
+ description: "Memory scope this agent reads from and writes to."
805
+ },
806
+ background: {
807
+ type: "boolean",
808
+ description: "When true, the agent runs as a background task (no foreground UI presence)."
809
+ },
810
+ effort: {
811
+ type: "string",
812
+ enum: ["low", "medium", "high", "xhigh", "max"],
813
+ description: "Effort budget for reasoning. Higher levels allocate more thinking time."
814
+ },
815
+ isolation: {
816
+ type: "string",
817
+ enum: ["worktree"],
818
+ description: "Isolation strategy. `worktree` is the only documented value today (runs the agent in a separate git worktree)."
819
+ },
820
+ color: {
821
+ type: "string",
822
+ enum: ["red", "blue", "green", "yellow", "purple", "orange", "pink", "cyan"],
823
+ description: "Display color for this agent in the host UI."
824
+ },
825
+ initialPrompt: {
826
+ type: "string",
827
+ description: "Auto-submitted first turn \u2014 Anthropic dispatches this as if the user had typed it on agent activation."
753
828
  }
754
829
  }
755
830
  };
756
831
 
757
- // built-in-plugins/providers/claude/schemas/hook.schema.json
758
- var hook_schema_default = {
832
+ // built-in-plugins/providers/claude/schemas/command.schema.json
833
+ var command_schema_default = {
759
834
  $schema: "https://json-schema.org/draft/2020-12/schema",
760
- $id: "https://skill-map.dev/providers/claude/v1/frontmatter/hook.schema.json",
761
- title: "FrontmatterHook",
762
- description: "Frontmatter shape for nodes classified as `hook` by the Claude Provider. Extends the spec's universal `frontmatter/base.schema.json` via $ref-by-$id. Kind-specific fields: `event`, `condition`, `blocking`, `idempotent`. Owned by the Claude Provider; relocated from `spec/schemas/frontmatter/` in spec 0.8.0 (Phase 3 of plug-in model overhaul).",
835
+ $id: "https://skill-map.dev/providers/claude/v1/frontmatter/command.schema.json",
836
+ title: "FrontmatterCommand",
837
+ description: "Frontmatter shape for nodes classified as `command` by the Claude Provider. Today identical to `skill` per Anthropic's documented merger (https://code.claude.com/docs/en/skills.md \u2014 \"Custom commands have been merged into skills\"). Both extend the shared `skill-base.schema.json` via the same `allOf` pattern. The two are kept SPLIT (not aliased) because skill-map's registry differentiates them in `IProviderKind.ui` (separate label / icon / color) and `defaultRefreshAction`. Splitting also reserves room for future divergence. No command-only fields today; `additionalProperties: true` so the file is ready for them.",
763
838
  allOf: [
764
- { $ref: "https://skill-map.dev/spec/v0/frontmatter/base.schema.json" }
839
+ { $ref: "https://skill-map.dev/providers/claude/v1/frontmatter/skill-base.schema.json" }
765
840
  ],
766
841
  type: "object",
767
842
  additionalProperties: true,
768
- properties: {
769
- event: {
770
- type: "string",
771
- description: "Event name this hook reacts to (e.g. `PreToolUse`, `PostToolUse`, `SubagentStop`, `SessionStart`). Platform-specific enum; Providers MAY validate."
772
- },
773
- condition: {
774
- type: "string",
775
- description: "Free-form predicate expression evaluated by the host. Syntax is platform-specific; the spec does not constrain it."
776
- },
777
- blocking: {
778
- type: "boolean",
779
- description: "When true, the host MUST wait for the hook to finish before proceeding. When false, the hook runs fire-and-forget."
780
- },
781
- idempotent: {
782
- type: "boolean",
783
- description: "Author-declared: executing twice with the same inputs produces the same result. Consumed by runners for retry and dedup."
784
- }
785
- }
843
+ properties: {}
786
844
  };
787
845
 
788
846
  // built-in-plugins/providers/claude/schemas/note.schema.json
@@ -805,7 +863,7 @@ var claudeProvider = {
805
863
  pluginId: "claude",
806
864
  kind: "provider",
807
865
  version: "1.0.0",
808
- description: "Walks Claude Code scope conventions (.claude/{agents,commands,hooks,skills} + notes).",
866
+ description: "Walks Claude Code scope conventions (.claude/{agents,commands,skills} + notes).",
809
867
  stability: "stable",
810
868
  // The Claude Provider's content lives under `~/.claude` for the global
811
869
  // scope (and inside `.claude/` for project scope). `sm doctor` validates
@@ -857,20 +915,6 @@ var claudeProvider = {
857
915
  }
858
916
  }
859
917
  },
860
- hook: {
861
- schema: "./schemas/hook.schema.json",
862
- schemaJson: hook_schema_default,
863
- defaultRefreshAction: "claude/summarize-hook",
864
- ui: {
865
- label: "Hooks",
866
- color: "#8b5cf6",
867
- colorDark: "#a78bfa",
868
- icon: {
869
- kind: "svg",
870
- path: "M12 2 a3 3 0 1 0 0 6 a3 3 0 1 0 0 -6 M12 8 L12 22 M5 12 H2 a10 10 0 0 0 20 0 H19"
871
- }
872
- }
873
- },
874
918
  skill: {
875
919
  schema: "./schemas/skill.schema.json",
876
920
  schemaJson: skill_schema_default,
@@ -897,6 +941,12 @@ var claudeProvider = {
897
941
  }
898
942
  }
899
943
  },
944
+ // Auxiliary schemas the per-kind schemas $ref by $id. AJV needs them
945
+ // registered via addSchema BEFORE the per-kind schemas compile, so the
946
+ // validator builder pre-registers them. `skill-base.schema.json` is the
947
+ // shared base for `skill` + `command` per Anthropic's documented merger
948
+ // (https://code.claude.com/docs/en/skills.md).
949
+ schemas: [skill_base_schema_default],
900
950
  async *walk(roots, options = {}) {
901
951
  const filter = options.ignoreFilter ?? buildIgnoreFilter();
902
952
  for (const root of roots) {
@@ -917,7 +967,6 @@ var claudeProvider = {
917
967
  const lower = path.toLowerCase();
918
968
  if (lower.startsWith(".claude/agents/")) return "agent";
919
969
  if (lower.startsWith(".claude/commands/")) return "command";
920
- if (lower.startsWith(".claude/hooks/")) return "hook";
921
970
  if (lower.startsWith(".claude/skills/")) return "skill";
922
971
  return "note";
923
972
  }
@@ -1597,7 +1646,7 @@ var ASCII_FORMATTER_TEXTS = {
1597
1646
 
1598
1647
  // built-in-plugins/formatters/ascii/index.ts
1599
1648
  var ID10 = "ascii";
1600
- var KIND_ORDER = ["agent", "command", "hook", "skill", "note"];
1649
+ var KIND_ORDER = ["agent", "command", "skill", "note"];
1601
1650
  var asciiFormatter = {
1602
1651
  id: ID10,
1603
1652
  pluginId: "core",
@@ -1809,6 +1858,7 @@ function buildProviderFrontmatterValidator(providers) {
1809
1858
  const baseFile = resolve3(specRoot, "schemas/frontmatter/base.schema.json");
1810
1859
  const baseSchema = JSON.parse(readFileSync2(baseFile, "utf8"));
1811
1860
  ajv.addSchema(baseSchema);
1861
+ registerProviderAuxiliarySchemas(ajv, providers);
1812
1862
  const compiled = /* @__PURE__ */ new Map();
1813
1863
  for (const provider of providers) {
1814
1864
  for (const [kind, entry] of Object.entries(provider.kinds)) {
@@ -1833,6 +1883,16 @@ function formatError(err) {
1833
1883
  const path = err.instancePath || "(root)";
1834
1884
  return `${path} ${err.message ?? err.keyword}`;
1835
1885
  }
1886
+ function registerProviderAuxiliarySchemas(ajv, providers) {
1887
+ for (const provider of providers) {
1888
+ if (!provider.schemas) continue;
1889
+ for (const aux of provider.schemas) {
1890
+ const auxJson = aux;
1891
+ if (typeof auxJson.$id === "string" && ajv.getSchema(auxJson.$id)) continue;
1892
+ ajv.addSchema(aux);
1893
+ }
1894
+ }
1895
+ }
1836
1896
  function resolveSpecRoot() {
1837
1897
  const require2 = createRequire(import.meta.url);
1838
1898
  try {
@@ -6297,7 +6357,7 @@ function writeStreamSnippet(stream, header, text) {
6297
6357
  var CONFORMANCE_COMMANDS = [ConformanceRunCommand];
6298
6358
 
6299
6359
  // cli/commands/db.ts
6300
- import { spawnSync as spawnSync2 } from "child_process";
6360
+ import { spawn, spawnSync as spawnSync2 } from "child_process";
6301
6361
  import { chmod, copyFile, mkdir, rm } from "fs/promises";
6302
6362
  import { dirname as dirname8, join as join9, resolve as resolve12 } from "path";
6303
6363
  import { DatabaseSync as DatabaseSync4 } from "node:sqlite";
@@ -6349,6 +6409,11 @@ var DB_TEXTS = {
6349
6409
  migrateKernelAppliedWithBackup: "kernel \xB7 Applied {{count}} migration(s) \xB7 backup: {{backupPath}}\n",
6350
6410
  // --- shell (system sqlite3 binary required for the interactive REPL) ---
6351
6411
  shellSqlite3NotFound: "sqlite3 binary not found on PATH. Install it (macOS: brew install sqlite; Debian/Ubuntu: apt install sqlite3) or use `sm db dump` for read-only inspection.\n",
6412
+ // --- browser (system sqlitebrowser GUI required) ---------------------
6413
+ browserRunScanFirstHint: "Run `sm scan` first (or `sm init`) to create the project DB.\n",
6414
+ browserNotFound: "sqlitebrowser is not installed (or not on PATH).\n\nIf you want a GUI to inspect the DB, install it:\n Debian/Ubuntu: sudo apt install -y sqlitebrowser\n macOS: brew install --cask db-browser-for-sqlite\n Windows: https://sqlitebrowser.org/dl/\n",
6415
+ browserOpeningReadOnly: "Opening {{path}} (read-only)\n",
6416
+ browserOpeningReadWrite: "Opening {{path}} (read-write)\n",
6352
6417
  // --- dump (pure node:sqlite, no external binary) ----------------------
6353
6418
  dumpInvalidTable: "--tables: refusing non-identifier name {{table}}. Table names must match [a-zA-Z_][a-zA-Z0-9_]*\n",
6354
6419
  // --- plugin migration runner -----------------------------------------
@@ -6635,6 +6700,57 @@ var DbShellCommand = class extends SmCommand {
6635
6700
  return result.status ?? 0;
6636
6701
  }
6637
6702
  };
6703
+ var DbBrowserCommand = class extends SmCommand {
6704
+ static paths = [["db", "browser"]];
6705
+ static usage = Command5.Usage({
6706
+ category: "Database",
6707
+ description: "Open the DB in DB Browser for SQLite (sqlitebrowser GUI).",
6708
+ details: `
6709
+ Default: read-only (-R), so a concurrent \`sm scan\` writer is safe.
6710
+ Pass --rw to enable writes.
6711
+
6712
+ Resolution order for the DB path: positional arg > --db <path> >
6713
+ -g/--global > project default (cwd/.skill-map/skill-map.db).
6714
+
6715
+ Spawns sqlitebrowser detached so the terminal stays usable. If
6716
+ sqlitebrowser is not on PATH, a clear error points at the install
6717
+ hint (Debian/Ubuntu: sudo apt install -y sqlitebrowser).
6718
+ `,
6719
+ examples: [
6720
+ ["Open the project DB read-only", "sm db browser"],
6721
+ ["Open the project DB read-write", "sm db browser --rw"],
6722
+ ["Open an arbitrary DB file", "sm db browser path/to/other.db"]
6723
+ ]
6724
+ });
6725
+ // GUI launch: the spawned process is detached and unref'd; we exit
6726
+ // immediately. No `done in <…>` line — the user expects to see the
6727
+ // GUI window, not a follow-up trailer in the terminal.
6728
+ emitElapsed = false;
6729
+ rw = Option5.Boolean("--rw", false, {
6730
+ description: "Open in read-write mode. Default is read-only so a concurrent `sm scan` writer is safe."
6731
+ });
6732
+ positional = Option5.String({ required: false });
6733
+ async run() {
6734
+ const path = this.positional ? resolve12(this.positional) : resolveDbPath({ global: this.global, db: this.db, ...defaultRuntimeContext() });
6735
+ if (!assertDbExists(path, this.context.stderr)) {
6736
+ this.context.stderr.write(DB_TEXTS.browserRunScanFirstHint);
6737
+ return ExitCode.NotFound;
6738
+ }
6739
+ const which = spawnSync2("which", ["sqlitebrowser"], { stdio: "ignore" });
6740
+ if (which.status !== 0) {
6741
+ this.context.stderr.write(DB_TEXTS.browserNotFound);
6742
+ return ExitCode.Error;
6743
+ }
6744
+ const readOnly = !this.rw;
6745
+ const args2 = readOnly ? ["-R", path] : [path];
6746
+ this.context.stdout.write(
6747
+ tx(readOnly ? DB_TEXTS.browserOpeningReadOnly : DB_TEXTS.browserOpeningReadWrite, { path })
6748
+ );
6749
+ const child = spawn("sqlitebrowser", args2, { detached: true, stdio: "ignore" });
6750
+ child.unref();
6751
+ return ExitCode.Ok;
6752
+ }
6753
+ };
6638
6754
  var DbDumpCommand = class extends SmCommand {
6639
6755
  static paths = [["db", "dump"]];
6640
6756
  static usage = Command5.Usage({
@@ -6924,6 +7040,7 @@ var DB_COMMANDS = [
6924
7040
  DbRestoreCommand,
6925
7041
  DbResetCommand,
6926
7042
  DbShellCommand,
7043
+ DbBrowserCommand,
6927
7044
  DbDumpCommand,
6928
7045
  DbMigrateCommand
6929
7046
  ];
@@ -7081,7 +7198,7 @@ var EXPORT_TEXTS = {
7081
7198
  };
7082
7199
 
7083
7200
  // cli/commands/export.ts
7084
- var KIND_ORDER2 = ["agent", "command", "hook", "skill", "note"];
7201
+ var KIND_ORDER2 = ["agent", "command", "skill", "note"];
7085
7202
  var SUPPORTED_FORMATS = ["json", "md"];
7086
7203
  var DEFERRED_FORMATS = {
7087
7204
  mermaid: EXPORT_TEXTS.formatDeferredReasonMermaid
@@ -7097,7 +7214,7 @@ var ExportCommand = class extends SmCommand {
7097
7214
 
7098
7215
  Query syntax (v0.5.0): whitespace-separated key=value tokens; AND
7099
7216
  across keys, OR within comma-separated values. Keys: \`kind\`
7100
- (skill / agent / command / hook / note), \`has\` (issues), \`path\`
7217
+ (skill / agent / command / note), \`has\` (issues), \`path\`
7101
7218
  (POSIX glob \u2014 \`*\` matches a single segment, \`**\` matches across
7102
7219
  segments).
7103
7220
 
@@ -7374,7 +7491,7 @@ import { Command as Command8, Option as Option8 } from "clipanion";
7374
7491
  // package.json
7375
7492
  var package_default = {
7376
7493
  name: "@skill-map/cli",
7377
- version: "0.16.5",
7494
+ version: "0.17.0",
7378
7495
  description: "skill-map reference implementation \u2014 kernel + CLI + adapters.",
7379
7496
  license: "MIT",
7380
7497
  type: "module",
@@ -7421,10 +7538,13 @@ var package_default = {
7421
7538
  scripts: {
7422
7539
  build: "tsup",
7423
7540
  dev: "tsup --watch",
7424
- "dev:serve": "node ../scripts/dev-serve.js",
7541
+ "dev:serve": "node scripts/dev-serve.js",
7425
7542
  typecheck: "tsc --noEmit",
7426
7543
  lint: "eslint .",
7427
7544
  "lint:fix": "eslint . --fix",
7545
+ reference: "node scripts/build-reference.js",
7546
+ "reference:check": "node scripts/build-reference.js --check",
7547
+ validate: "npm run typecheck && npm run lint && npm run build && npm run test:ci && npm run reference:check",
7428
7548
  pretest: "tsup",
7429
7549
  "pretest:ci": "tsup",
7430
7550
  "pretest:coverage": "tsup",
@@ -7437,7 +7557,7 @@ var package_default = {
7437
7557
  },
7438
7558
  dependencies: {
7439
7559
  "@hono/node-server": "2.0.1",
7440
- "@skill-map/spec": "0.16.0",
7560
+ "@skill-map/spec": "0.17.0",
7441
7561
  ajv: "8.18.0",
7442
7562
  "ajv-formats": "3.0.1",
7443
7563
  chokidar: "5.0.0",
@@ -12240,7 +12360,7 @@ function renderDeltaIssues(issues) {
12240
12360
  }
12241
12361
 
12242
12362
  // cli/commands/serve.ts
12243
- import { spawn } from "child_process";
12363
+ import { spawn as spawn2 } from "child_process";
12244
12364
  import { existsSync as existsSync18 } from "fs";
12245
12365
  import { Command as Command19, Option as Option19 } from "clipanion";
12246
12366
 
@@ -12897,24 +13017,46 @@ var PLACEHOLDER_HTML = `<!doctype html>
12897
13017
  </body>
12898
13018
  </html>
12899
13019
  `;
12900
- function createStaticHandler(uiDist) {
12901
- if (uiDist === null) return placeholderRootMiddleware();
12902
- return serveStatic({ root: uiDist });
13020
+ var DEV_PLACEHOLDER_HTML = `<!doctype html>
13021
+ <html lang="en">
13022
+ <head>
13023
+ <meta charset="utf-8" />
13024
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13025
+ <meta name="skill-map-mode" content="live" />
13026
+ <title>skill-map BFF (dev)</title>
13027
+ <style>
13028
+ body { font-family: system-ui, sans-serif; margin: 2rem; max-width: 36rem; line-height: 1.5; }
13029
+ code { background: #f4f4f4; padding: 0.1rem 0.3rem; border-radius: 3px; }
13030
+ h1 { font-size: 1.4rem; }
13031
+ </style>
13032
+ </head>
13033
+ <body>
13034
+ <h1>skill-map BFF in dev mode \u2014 UI disabled</h1>
13035
+ <p>Run <code>npm run ui:dev</code> in another terminal and visit <a href="http://localhost:4200/">http://localhost:4200/</a> for the Angular SPA.</p>
13036
+ <p>The REST API on this port is reachable at <code>/api/health</code>.</p>
13037
+ </body>
13038
+ </html>
13039
+ `;
13040
+ function createStaticHandler(opts) {
13041
+ if (opts.uiDist === null) return placeholderRootMiddleware(opts.noUi === true);
13042
+ return serveStatic({ root: opts.uiDist });
12903
13043
  }
12904
- function createSpaFallback(uiDist) {
13044
+ function createSpaFallback(opts) {
13045
+ const placeholder = opts.noUi === true ? DEV_PLACEHOLDER_HTML : PLACEHOLDER_HTML;
12905
13046
  return async (c, _next) => {
12906
13047
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return c.notFound();
12907
- if (uiDist === null) return htmlResponse(c, PLACEHOLDER_HTML);
12908
- const indexPath = join13(uiDist, INDEX_HTML);
12909
- if (!existsSync16(indexPath)) return htmlResponse(c, PLACEHOLDER_HTML);
13048
+ if (opts.uiDist === null) return htmlResponse(c, placeholder);
13049
+ const indexPath = join13(opts.uiDist, INDEX_HTML);
13050
+ if (!existsSync16(indexPath)) return htmlResponse(c, placeholder);
12910
13051
  return fileResponse(c, indexPath);
12911
13052
  };
12912
13053
  }
12913
- function placeholderRootMiddleware() {
13054
+ function placeholderRootMiddleware(noUi) {
13055
+ const html = noUi ? DEV_PLACEHOLDER_HTML : PLACEHOLDER_HTML;
12914
13056
  return async (c, next) => {
12915
13057
  if (c.req.method !== "GET" && c.req.method !== "HEAD") return next();
12916
13058
  if (c.req.path === "/" || c.req.path === "/index.html") {
12917
- return htmlResponse(c, PLACEHOLDER_HTML);
13059
+ return htmlResponse(c, html);
12918
13060
  }
12919
13061
  return next();
12920
13062
  };
@@ -13018,8 +13160,8 @@ function createApp(deps) {
13018
13160
  throw new HTTPException5(404, { message: `Unknown API endpoint: ${c.req.path}` });
13019
13161
  });
13020
13162
  attachBroadcasterRoute(app, deps.broadcaster);
13021
- app.use("*", createStaticHandler(deps.options.uiDist));
13022
- app.get("*", createSpaFallback(deps.options.uiDist));
13163
+ app.use("*", createStaticHandler({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
13164
+ app.get("*", createSpaFallback({ uiDist: deps.options.uiDist, noUi: deps.options.noUi }));
13023
13165
  app.notFound((c) => {
13024
13166
  throw new HTTPException5(404, { message: `Not found: ${c.req.path}` });
13025
13167
  });
@@ -13481,12 +13623,15 @@ function validateServerOptions(input) {
13481
13623
  if (watcherError !== null) return { ok: false, error: watcherError };
13482
13624
  const debounceError = validateWatcherDebounce(input.watcherDebounceMs);
13483
13625
  if (debounceError !== null) return { ok: false, error: debounceError };
13626
+ const noUiError = validateNoUi(filled.noUi, filled.uiDist);
13627
+ if (noUiError !== null) return { ok: false, error: noUiError };
13484
13628
  const options = {
13485
13629
  port: filled.port,
13486
13630
  host: filled.host,
13487
13631
  scope: filled.scope,
13488
13632
  dbPath: input.dbPath,
13489
13633
  uiDist: filled.uiDist,
13634
+ noUi: filled.noUi,
13490
13635
  noBuiltIns: filled.noBuiltIns,
13491
13636
  noPlugins: filled.noPlugins,
13492
13637
  open: filled.open,
@@ -13504,6 +13649,7 @@ function applyDefaults(input) {
13504
13649
  host: input.host ?? DEFAULT_HOST,
13505
13650
  scope: input.scope ?? DEFAULT_SCOPE,
13506
13651
  uiDist: input.uiDist ?? null,
13652
+ noUi: input.noUi ?? false,
13507
13653
  noBuiltIns: input.noBuiltIns ?? false,
13508
13654
  noPlugins: input.noPlugins ?? false,
13509
13655
  open: input.open ?? true,
@@ -13562,6 +13708,16 @@ function validateWatcherDebounce(value) {
13562
13708
  }
13563
13709
  return null;
13564
13710
  }
13711
+ function validateNoUi(noUi, uiDist) {
13712
+ if (noUi && uiDist !== null) {
13713
+ return {
13714
+ code: "no-ui-conflicts-ui-dist",
13715
+ message: "--no-ui and --ui-dist <path> are mutually exclusive",
13716
+ value: uiDist
13717
+ };
13718
+ }
13719
+ return null;
13720
+ }
13565
13721
 
13566
13722
  // server/paths.ts
13567
13723
  import { existsSync as existsSync17, statSync as statSync5 } from "fs";
@@ -13757,6 +13913,10 @@ var SERVE_TEXTS = {
13757
13913
  // Watcher option failures — ExitCode.Error.
13758
13914
  watcherRequiresPipeline: "sm serve: --no-built-ins is incompatible with the watcher (would persist empty scans on every batch). Pass --no-watcher to opt out, or drop --no-built-ins.\n",
13759
13915
  watcherDebounceInvalid: "sm serve: --watcher-debounce-ms must be a non-negative integer (got {{value}}).\n",
13916
+ // --no-ui flag-validation failures — ExitCode.Error.
13917
+ noUiConflictsUiDist: "sm serve: --no-ui and --ui-dist {{path}} are mutually exclusive (drop one).\n",
13918
+ // --no-ui + --open is harmless but worth flagging — non-fatal stderr note.
13919
+ noUiOpenWarning: "sm serve: warning: --open with --no-ui will open the placeholder, not the live UI; pass --no-open if running alongside `ui:dev`.\n",
13760
13920
  // Generic operational error — surfaced when the server itself throws
13761
13921
  // before the listener binds (e.g. UI bundle missing under explicit
13762
13922
  // --ui-dist).
@@ -13961,6 +14121,9 @@ var ServeCommand = class extends SmCommand {
13961
14121
  // need it). Clipanion still exposes it on the parser; the Usage
13962
14122
  // omission is the "hidden" contract per the 14.1 brief.
13963
14123
  uiDist = Option19.String("--ui-dist", { required: false, hidden: true });
14124
+ noUi = Option19.Boolean("--no-ui", false, {
14125
+ 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."
14126
+ });
13964
14127
  noWatcher = Option19.Boolean("--no-watcher", false, {
13965
14128
  description: "Disable the chokidar-fed scan-and-broadcast loop. Use only for CI / read-only deployments."
13966
14129
  });
@@ -14001,13 +14164,28 @@ var ServeCommand = class extends SmCommand {
14001
14164
  );
14002
14165
  return ExitCode.NotFound;
14003
14166
  }
14004
- const uiDistResult = resolveUiDist(runtimeCtx, this.uiDist);
14005
- if (!uiDistResult.ok) {
14167
+ if (this.noUi && this.uiDist !== void 0) {
14006
14168
  this.context.stderr.write(
14007
- tx(SERVE_TEXTS.startupFailed, { message: sanitizeForTerminal(uiDistResult.message) })
14169
+ tx(SERVE_TEXTS.noUiConflictsUiDist, { path: sanitizeForTerminal(this.uiDist) })
14008
14170
  );
14009
14171
  return ExitCode.Error;
14010
14172
  }
14173
+ let resolvedUiDist;
14174
+ if (this.noUi) {
14175
+ resolvedUiDist = null;
14176
+ } else {
14177
+ const uiDistResult = resolveUiDist(runtimeCtx, this.uiDist);
14178
+ if (!uiDistResult.ok) {
14179
+ this.context.stderr.write(
14180
+ tx(SERVE_TEXTS.startupFailed, { message: sanitizeForTerminal(uiDistResult.message) })
14181
+ );
14182
+ return ExitCode.Error;
14183
+ }
14184
+ resolvedUiDist = uiDistResult.uiDist;
14185
+ }
14186
+ if (this.noUi && this.open) {
14187
+ this.context.stderr.write(SERVE_TEXTS.noUiOpenWarning);
14188
+ }
14011
14189
  const debounceResult = parseDebounce(this.watcherDebounceMs);
14012
14190
  if (!debounceResult.ok) {
14013
14191
  this.context.stderr.write(
@@ -14020,7 +14198,8 @@ var ServeCommand = class extends SmCommand {
14020
14198
  const input = {
14021
14199
  dbPath,
14022
14200
  scope,
14023
- uiDist: uiDistResult.uiDist,
14201
+ uiDist: resolvedUiDist,
14202
+ noUi: this.noUi,
14024
14203
  noBuiltIns: this.noBuiltIns,
14025
14204
  noPlugins: this.noPlugins,
14026
14205
  open: this.open,
@@ -14131,6 +14310,8 @@ function formatValidationError(err) {
14131
14310
  return tx(SERVE_TEXTS.watcherRequiresPipeline, { value: sanitizeForTerminal(err.value) });
14132
14311
  case "watcher-debounce-invalid":
14133
14312
  return tx(SERVE_TEXTS.watcherDebounceInvalid, { value: sanitizeForTerminal(err.value) });
14313
+ case "no-ui-conflicts-ui-dist":
14314
+ return tx(SERVE_TEXTS.noUiConflictsUiDist, { path: sanitizeForTerminal(err.value) });
14134
14315
  default:
14135
14316
  return tx(SERVE_TEXTS.startupFailed, { message: sanitizeForTerminal(err.message) });
14136
14317
  }
@@ -14161,7 +14342,7 @@ function tryOpenBrowser(url, stderr) {
14161
14342
  command = "xdg-open";
14162
14343
  args2 = [url];
14163
14344
  }
14164
- const child = spawn(command, args2, { detached: true, stdio: "ignore" });
14345
+ const child = spawn2(command, args2, { detached: true, stdio: "ignore" });
14165
14346
  child.on("error", (err) => {
14166
14347
  stderr.write(
14167
14348
  tx(SERVE_TEXTS.openFailed, {