@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 +305 -124
- package/dist/cli.js.map +1 -1
- package/dist/index.js +17 -3
- package/dist/index.js.map +1 -1
- package/dist/kernel/index.d.ts +28 -5
- package/dist/kernel/index.js +17 -3
- package/dist/kernel/index.js.map +1 -1
- package/dist/ui/chunk-5ZGVBIPP.js +1031 -0
- package/dist/ui/chunk-BWUDZKB6.js +247 -0
- package/dist/ui/chunk-LUDNWV6G.js +3091 -0
- package/dist/ui/chunk-WMWULWZX.js +237 -0
- package/dist/ui/index.html +2 -2
- package/dist/ui/main-7LR4JN4M.js +1 -0
- package/dist/ui/skill-map-mark-dark.svg +8 -0
- package/dist/ui/skill-map-mark-light.svg +8 -0
- package/dist/ui/{styles-TCK5JUQE.css → styles-CBPFNGXA.css} +1 -1
- package/package.json +6 -3
- package/dist/ui/chunk-7PFTODKS.js +0 -1031
- package/dist/ui/chunk-A4RBO3TD.js +0 -38
- package/dist/ui/chunk-KPEISNOV.js +0 -819
- package/dist/ui/chunk-NKC42FI7.js +0 -210
- package/dist/ui/chunk-S5C4U3I3.js +0 -2403
- package/dist/ui/chunk-TG6IWVEC.js +0 -54
- package/dist/ui/chunk-TGJQE3TH.js +0 -54
- package/dist/ui/chunk-UGEECDPV.js +0 -1
- package/dist/ui/main-XSGTD7FQ.js +0 -1
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.
|
|
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/
|
|
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/
|
|
698
|
-
var
|
|
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/
|
|
701
|
-
title: "
|
|
702
|
-
description:
|
|
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
|
|
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/
|
|
717
|
-
var
|
|
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/
|
|
720
|
-
title: "
|
|
721
|
-
description: "Frontmatter shape for nodes classified as `
|
|
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
|
-
|
|
763
|
+
tools: {
|
|
729
764
|
type: "array",
|
|
730
|
-
description: "
|
|
731
|
-
items: {
|
|
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
|
-
|
|
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: "
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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/
|
|
758
|
-
var
|
|
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/
|
|
761
|
-
title: "
|
|
762
|
-
description: "Frontmatter shape for nodes classified as `
|
|
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/
|
|
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,
|
|
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", "
|
|
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", "
|
|
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 /
|
|
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.
|
|
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
|
|
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.
|
|
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
|
-
|
|
12901
|
-
|
|
12902
|
-
|
|
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(
|
|
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,
|
|
12908
|
-
const indexPath = join13(uiDist, INDEX_HTML);
|
|
12909
|
-
if (!existsSync16(indexPath)) return htmlResponse(c,
|
|
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,
|
|
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
|
-
|
|
14005
|
-
if (!uiDistResult.ok) {
|
|
14167
|
+
if (this.noUi && this.uiDist !== void 0) {
|
|
14006
14168
|
this.context.stderr.write(
|
|
14007
|
-
tx(SERVE_TEXTS.
|
|
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:
|
|
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 =
|
|
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, {
|