@klhapp/skillmux 0.2.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/CHANGELOG.md +147 -0
- package/LICENSE +21 -0
- package/README.md +410 -0
- package/config.example.toml +5 -0
- package/config.remote.example.toml +25 -0
- package/docs/configuration.md +100 -0
- package/docs/releasing.md +62 -0
- package/docs/schema.json +314 -0
- package/package.json +55 -0
- package/src/audit.ts +15 -0
- package/src/cli.ts +482 -0
- package/src/clients.ts +114 -0
- package/src/config.ts +338 -0
- package/src/db.ts +280 -0
- package/src/decision.ts +42 -0
- package/src/doctor.ts +68 -0
- package/src/eval.ts +74 -0
- package/src/init.ts +122 -0
- package/src/install.ts +113 -0
- package/src/lifecycle.ts +51 -0
- package/src/manifest.ts +105 -0
- package/src/metrics.ts +101 -0
- package/src/models.ts +20 -0
- package/src/rate-limiter.ts +110 -0
- package/src/readiness.ts +30 -0
- package/src/router-core.ts +435 -0
- package/src/rrf.ts +31 -0
- package/src/scan.ts +260 -0
- package/src/server.ts +318 -0
- package/src/stats.ts +165 -0
- package/src/sync.ts +182 -0
- package/src/types.ts +178 -0
- package/src/vault.ts +109 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Skillmux defaults to FTS5 plus local GTE-small semantic retrieval. Most users need no config file.
|
|
4
|
+
|
|
5
|
+
## Local mode
|
|
6
|
+
|
|
7
|
+
```toml
|
|
8
|
+
[inference]
|
|
9
|
+
mode = "local"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The versioned `gte-small-v1` bundle uses normalized, mean-pooled `Xenova/gte-small` embeddings (384 dimensions), quantized to q8 on CPU. Models are cached in `~/.cache/skillmux/models`. FTS5 and cosine result lists are combined with reciprocal-rank fusion; without a reranker the calling LLM selects from the ordered shortlist.
|
|
13
|
+
|
|
14
|
+
Advanced local overrides:
|
|
15
|
+
|
|
16
|
+
```toml
|
|
17
|
+
[inference]
|
|
18
|
+
mode = "local"
|
|
19
|
+
models_dir = "~/.cache/skillmux/models"
|
|
20
|
+
|
|
21
|
+
[inference.embedding]
|
|
22
|
+
model = "Xenova/gte-small"
|
|
23
|
+
dimension = 384
|
|
24
|
+
device = "cpu"
|
|
25
|
+
dtype = "q8"
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Use `skillmux models download` to prefetch models and `skillmux doctor` to verify readiness.
|
|
30
|
+
|
|
31
|
+
## Remote mode
|
|
32
|
+
|
|
33
|
+
See [`config.remote.example.toml`](../config.remote.example.toml). Embeddings must implement the OpenAI-compatible `POST /v1/embeddings` API. Optional reranking must implement Infinity's `POST /rerank` API. Credentials are read from the environment variables named by `api_key_env`.
|
|
34
|
+
|
|
35
|
+
Remote embedding `dimension` is required. Changing the provider, model, or dimension invalidates stored vectors and safely rebuilds them.
|
|
36
|
+
|
|
37
|
+
## Advanced retrieval
|
|
38
|
+
|
|
39
|
+
Candidate-generation depth is configurable but normally does not need tuning:
|
|
40
|
+
|
|
41
|
+
```toml
|
|
42
|
+
[recall]
|
|
43
|
+
k_lexical = 20
|
|
44
|
+
k_vector = 20
|
|
45
|
+
|
|
46
|
+
[thresholds]
|
|
47
|
+
candidate_limit = 5
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The router considers up to 20 candidates from each retrieval lane but returns at most 5 to the calling LLM.
|
|
51
|
+
|
|
52
|
+
Reranker thresholds have no universal default because score distributions are model-specific. When configuring a reranker, provide calibrated `inference.thresholds.match_score`, `inference.thresholds.match_margin`, and `inference.thresholds.candidate_floor`; otherwise configuration is rejected rather than silently applying unsuitable values.
|
|
53
|
+
|
|
54
|
+
## HTTP server
|
|
55
|
+
|
|
56
|
+
```toml
|
|
57
|
+
[server]
|
|
58
|
+
hostname = "127.0.0.1"
|
|
59
|
+
auth_enabled = false
|
|
60
|
+
auth_token_env = "SKILLMUX_AUTH_TOKEN"
|
|
61
|
+
allowed_origins = []
|
|
62
|
+
|
|
63
|
+
[server.rate_limit]
|
|
64
|
+
enabled = false
|
|
65
|
+
requests_per_minute = 60
|
|
66
|
+
trust_proxy = false
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Defaults are loopback-only (`hostname = "127.0.0.1"`) with CORS deny-by-default (`allowed_origins = []`) — a zero-config `skillmux serve --transport http` is not reachable from the network or from a browser tab on another origin. Docker sets `hostname` to `0.0.0.0` automatically (`RUNNING_IN_DOCKER=true`) since port-mapping needs the container to accept connections on all interfaces.
|
|
70
|
+
|
|
71
|
+
Before exposing HTTP beyond localhost, set `hostname` to a reachable interface, `auth_enabled = true` with a token, and populate `allowed_origins` with the specific origins that need browser access. `rate_limit.trust_proxy` should stay `false` unless a trusted reverse proxy sets `X-Forwarded-For` — it's otherwise a client-controlled, spoofable header and trusting it defeats per-client rate limiting.
|
|
72
|
+
|
|
73
|
+
## Tiers and the manifest
|
|
74
|
+
|
|
75
|
+
`skillmux init`/`sync` manage an optional second delivery path — pinning a subset of skills as real symlinks inside an agent's own skill directory, instead of routing every request through `resolve_skill`. See the README's [Tiers](../README.md#tiers-routed-vs-pinned) section for the concept and a walkthrough; this is the manifest reference.
|
|
76
|
+
|
|
77
|
+
### `skillmux.toml`
|
|
78
|
+
|
|
79
|
+
Lives at the vault root (a legacy `skr.toml` is still read if present, never written):
|
|
80
|
+
|
|
81
|
+
```toml
|
|
82
|
+
[core]
|
|
83
|
+
skills = ["csv-formatter"] # pinned into every [targets.*] dir; capped at 25
|
|
84
|
+
|
|
85
|
+
[project.repo1]
|
|
86
|
+
repos = ["/Users/you/code/repo1"] # only synced for repos paths that exist locally
|
|
87
|
+
skills = ["pdf-extractor"] # must not overlap [core]
|
|
88
|
+
|
|
89
|
+
[targets.claude]
|
|
90
|
+
dir = "/Users/you/.claude/skills"
|
|
91
|
+
project = false # true = also apply [project.*] groups scoped to this target
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
- `[core].skills` — symlinked into every `[targets.*]` dir on `sync`. Capped at 25 skills; `sync` fails if a listed skill id isn't actually in the vault.
|
|
95
|
+
- `[project.<group>].skills` — symlinked only into `<repo>/<relative path from $HOME to the target dir>`, for each `repos` entry, and only for targets with `project = true`. `repos` entries must resolve under `$HOME` (that's how the pin path is derived). A skill can't appear in both `[core]` and the same `[project.*]` group.
|
|
96
|
+
- `[targets.<name>]` — one entry per adopted surface. `skillmux init --target <name> --yes` writes these; hand-editing is fine as long as `sync` is still allowed to own the directory (see below).
|
|
97
|
+
|
|
98
|
+
### Ownership marker
|
|
99
|
+
|
|
100
|
+
Every directory `sync` manages gets a `.skillmux` marker file (`{"managed_by": "skillmux", "target": "<name>", "created_at": ...}`). `sync` refuses to touch a directory that exists but has no marker — run `skillmux init --target <name> --yes` first, which either creates the directory fresh or adopts an existing one in place (contents untouched). This is also why `sync --restore-monolith` (which deletes the marker and replaces the whole directory with one symlink straight to the vault) requires re-running `init` before that target can be `sync`'d again.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Releases are created from version tags after `main` is green.
|
|
4
|
+
|
|
5
|
+
## Prepare
|
|
6
|
+
|
|
7
|
+
1. Update `version` in `package.json`.
|
|
8
|
+
2. Move relevant entries from `Unreleased` into a dated version section in `CHANGELOG.md`.
|
|
9
|
+
3. Merge those changes through a pull request and confirm CI passes.
|
|
10
|
+
|
|
11
|
+
## Publish
|
|
12
|
+
|
|
13
|
+
Create and push the matching tag:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
git switch main
|
|
17
|
+
git pull --ff-only
|
|
18
|
+
git tag v0.1.1
|
|
19
|
+
git push origin v0.1.1
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The tag must exactly match `v` plus the `package.json` version. A mismatch stops the release.
|
|
23
|
+
|
|
24
|
+
The release workflow publishes:
|
|
25
|
+
|
|
26
|
+
- `skillmux-linux-amd64`
|
|
27
|
+
- `skillmux-linux-arm64`
|
|
28
|
+
- `SHA256SUMS`
|
|
29
|
+
- GitHub build provenance attestations when the repository is public
|
|
30
|
+
- Full image: `:<version>`, `:<major>.<minor>`, `:<major>`, and `:latest`
|
|
31
|
+
- Slim image: `:<version>-slim`, `:<major>.<minor>-slim`, `:<major>-slim`, and `:latest-slim`
|
|
32
|
+
- Multi-architecture `linux/amd64` and `linux/arm64` images with SBOM and provenance
|
|
33
|
+
|
|
34
|
+
Each Docker tag is a multi-architecture manifest. Users run the same tag on AMD64 and ARM64; Docker automatically pulls the matching image.
|
|
35
|
+
|
|
36
|
+
Container images are published to GitHub Container Registry only; the release workflow does not require external registry credentials.
|
|
37
|
+
Private repositories still publish BuildKit SBOM/provenance with container images, but GitHub artifact attestations are skipped because GitHub does not support them for user-owned private repositories.
|
|
38
|
+
|
|
39
|
+
Verify downloaded binaries with:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
sha256sum --check SHA256SUMS
|
|
43
|
+
./skillmux-linux-amd64 config show
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Verify release assets from GitHub without cloning the repository:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
gh release download v0.1.1 --repo klhq/skillmux
|
|
50
|
+
sha256sum --check SHA256SUMS
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Verify the container with a read-only vault mount:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
docker run --rm \
|
|
57
|
+
-v ~/.agents/skills:/vault:ro \
|
|
58
|
+
-p 3000:3000 \
|
|
59
|
+
ghcr.io/klhq/skillmux:latest
|
|
60
|
+
|
|
61
|
+
curl --fail http://127.0.0.1:3000/health/ready
|
|
62
|
+
```
|
package/docs/schema.json
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://klh.app/schemas/skillmux/router-core.json",
|
|
4
|
+
"title": "Skillmux — router-core contract",
|
|
5
|
+
"description": "Typed contract for the skillmux MCP server: tool inputs/outputs, config.toml, and audit-log rows. The SKILL.md body must appear exactly once on the wire.",
|
|
6
|
+
"$defs": {
|
|
7
|
+
"SkillId": {
|
|
8
|
+
"type": "string",
|
|
9
|
+
"description": "Vault directory name of the skill (~/.agents/skills/<skill_id>/SKILL.md).",
|
|
10
|
+
"pattern": "^[a-z0-9][a-z0-9-]{1,127}$"
|
|
11
|
+
},
|
|
12
|
+
"Sha256Hex": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"description": "Lowercase hex SHA-256 of the exact SKILL.md bytes.",
|
|
15
|
+
"pattern": "^[a-f0-9]{64}$"
|
|
16
|
+
},
|
|
17
|
+
"RelativePath": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Path of a supporting file relative to the skill directory (e.g. 'references/palette.md'). Never absolute, never contains '..'.",
|
|
20
|
+
"pattern": "^(?!/)(?!.*\\.\\.).+$",
|
|
21
|
+
"minLength": 1,
|
|
22
|
+
"maxLength": 512
|
|
23
|
+
},
|
|
24
|
+
"Candidate": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"description": "One shortlist entry in an ambiguous result.",
|
|
27
|
+
"properties": {
|
|
28
|
+
"skill_id": { "$ref": "#/$defs/SkillId" },
|
|
29
|
+
"title": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
30
|
+
"description": { "type": "string", "minLength": 1 }
|
|
31
|
+
},
|
|
32
|
+
"required": ["skill_id", "title", "description"],
|
|
33
|
+
"additionalProperties": false
|
|
34
|
+
},
|
|
35
|
+
"ResolveSkillInput": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"properties": {
|
|
38
|
+
"query": {
|
|
39
|
+
"type": "string",
|
|
40
|
+
"description": "Natural-language task description to route. Normalized (NFC) by the server; never semantically rewritten.",
|
|
41
|
+
"minLength": 1,
|
|
42
|
+
"maxLength": 8192
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"required": ["query"],
|
|
46
|
+
"additionalProperties": false
|
|
47
|
+
},
|
|
48
|
+
"MatchedResult": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"description": "One reranked candidate clearly dominates. Full SKILL.md is delivered inline.",
|
|
51
|
+
"properties": {
|
|
52
|
+
"outcome": { "const": "matched" },
|
|
53
|
+
"retrieval": { "enum": ["exact", "reranked"] },
|
|
54
|
+
"skill_id": { "$ref": "#/$defs/SkillId" },
|
|
55
|
+
"title": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
56
|
+
"content_sha256": { "$ref": "#/$defs/Sha256Hex" },
|
|
57
|
+
"score": {
|
|
58
|
+
"type": "number",
|
|
59
|
+
"description": "Top configured-reranker score. Satisfies score >= config thresholds.match_score."
|
|
60
|
+
},
|
|
61
|
+
"margin": {
|
|
62
|
+
"type": "number",
|
|
63
|
+
"description": "Top minus second configured-reranker score. Equals score when there is exactly one candidate.",
|
|
64
|
+
"minimum": 0
|
|
65
|
+
},
|
|
66
|
+
"body": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Verbatim UTF-8 content of SKILL.md. sha256(body bytes) == content_sha256 == hash of the on-disk file read at delivery time.",
|
|
69
|
+
"minLength": 1
|
|
70
|
+
},
|
|
71
|
+
"files": {
|
|
72
|
+
"type": "array",
|
|
73
|
+
"description": "Relative paths of the skill's supporting files (contents NOT included). Same semantics as FetchSkillResult.files.",
|
|
74
|
+
"items": { "$ref": "#/$defs/RelativePath" },
|
|
75
|
+
"uniqueItems": true
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"required": ["outcome", "retrieval", "skill_id", "title", "content_sha256", "score", "margin", "body", "files"],
|
|
79
|
+
"additionalProperties": false
|
|
80
|
+
},
|
|
81
|
+
"AmbiguousResult": {
|
|
82
|
+
"type": "object",
|
|
83
|
+
"description": "Several plausible candidates; the calling LLM picks, then calls fetch_skill.",
|
|
84
|
+
"properties": {
|
|
85
|
+
"outcome": { "const": "ambiguous" },
|
|
86
|
+
"retrieval": { "enum": ["exact", "reranked", "hybrid", "lexical"] },
|
|
87
|
+
"candidates": {
|
|
88
|
+
"type": "array",
|
|
89
|
+
"items": { "$ref": "#/$defs/Candidate" },
|
|
90
|
+
"minItems": 1,
|
|
91
|
+
"maxItems": 5
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"required": ["outcome", "retrieval", "candidates"],
|
|
95
|
+
"additionalProperties": false
|
|
96
|
+
},
|
|
97
|
+
"NoMatchResult": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"description": "AC4: no skill passed the relevance floor. Caller proceeds under its normal workflow; must not load an unrelated skill.",
|
|
100
|
+
"properties": {
|
|
101
|
+
"outcome": { "const": "no_match" },
|
|
102
|
+
"retrieval": { "enum": ["exact", "reranked", "hybrid", "lexical"] },
|
|
103
|
+
"message": {
|
|
104
|
+
"type": "string",
|
|
105
|
+
"description": "Fixed guidance string telling the caller to proceed normally.",
|
|
106
|
+
"minLength": 1
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
"required": ["outcome", "retrieval", "message"],
|
|
110
|
+
"additionalProperties": false
|
|
111
|
+
},
|
|
112
|
+
"ResolveSkillResult": {
|
|
113
|
+
"description": "AC1: exactly three outcomes, discriminated by 'outcome'.",
|
|
114
|
+
"oneOf": [
|
|
115
|
+
{ "$ref": "#/$defs/MatchedResult" },
|
|
116
|
+
{ "$ref": "#/$defs/AmbiguousResult" },
|
|
117
|
+
{ "$ref": "#/$defs/NoMatchResult" }
|
|
118
|
+
]
|
|
119
|
+
},
|
|
120
|
+
"FetchSkillInput": {
|
|
121
|
+
"type": "object",
|
|
122
|
+
"properties": {
|
|
123
|
+
"skill_id": { "$ref": "#/$defs/SkillId" }
|
|
124
|
+
},
|
|
125
|
+
"required": ["skill_id"],
|
|
126
|
+
"additionalProperties": false
|
|
127
|
+
},
|
|
128
|
+
"FetchSkillResult": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"description": "AC5: verbatim delivery independent of any prior resolve outcome. Unknown skill_id raises ToolError with code SKILL_NOT_FOUND instead of returning this shape.",
|
|
131
|
+
"properties": {
|
|
132
|
+
"skill_id": { "$ref": "#/$defs/SkillId" },
|
|
133
|
+
"title": { "type": "string", "minLength": 1, "maxLength": 512 },
|
|
134
|
+
"content_sha256": { "$ref": "#/$defs/Sha256Hex" },
|
|
135
|
+
"body": { "type": "string", "minLength": 1 },
|
|
136
|
+
"files": {
|
|
137
|
+
"type": "array",
|
|
138
|
+
"description": "Relative paths of supporting files under the skill directory (references/, scripts/, ...). Paths only; callers with filesystem access read contents directly.",
|
|
139
|
+
"items": { "$ref": "#/$defs/RelativePath" },
|
|
140
|
+
"uniqueItems": true
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
"required": ["skill_id", "title", "content_sha256", "body", "files"],
|
|
144
|
+
"additionalProperties": false
|
|
145
|
+
},
|
|
146
|
+
"ErrorCode": {
|
|
147
|
+
"type": "string",
|
|
148
|
+
"description": "Machine-readable code carried in MCP ToolError messages as 'CODE: human message'.",
|
|
149
|
+
"enum": ["SKILL_NOT_FOUND", "VAULT_UNAVAILABLE", "INTERNAL"]
|
|
150
|
+
},
|
|
151
|
+
"Config": {
|
|
152
|
+
"type": "object",
|
|
153
|
+
"description": "config.toml, validated at startup. Local ONNX is the default; remote inference is explicit.",
|
|
154
|
+
"properties": {
|
|
155
|
+
"vault_path": {
|
|
156
|
+
"type": "string",
|
|
157
|
+
"description": "Skill vault root. Default: ~/.agents/skills",
|
|
158
|
+
"default": "~/.agents/skills"
|
|
159
|
+
},
|
|
160
|
+
"state_dir": {
|
|
161
|
+
"type": "string",
|
|
162
|
+
"description": "All router writes are confined here (AC9): index SQLite (incl. audit log), vector matrix.",
|
|
163
|
+
"default": "~/.local/state/skillmux"
|
|
164
|
+
},
|
|
165
|
+
"recall": {
|
|
166
|
+
"type": "object",
|
|
167
|
+
"properties": {
|
|
168
|
+
"k_lexical": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 },
|
|
169
|
+
"k_vector": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }
|
|
170
|
+
},
|
|
171
|
+
"required": ["k_lexical", "k_vector"],
|
|
172
|
+
"additionalProperties": false
|
|
173
|
+
},
|
|
174
|
+
"thresholds": {
|
|
175
|
+
"type": "object",
|
|
176
|
+
"description": "Caller-facing shortlist policy. Reranker thresholds live under inference.thresholds.",
|
|
177
|
+
"properties": {
|
|
178
|
+
"candidate_limit": {
|
|
179
|
+
"type": "integer",
|
|
180
|
+
"minimum": 1,
|
|
181
|
+
"maximum": 50,
|
|
182
|
+
"default": 5,
|
|
183
|
+
"description": "Maximum number of candidates returned on ambiguous outcomes (top-k limit)."
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
"required": ["candidate_limit"],
|
|
187
|
+
"additionalProperties": false
|
|
188
|
+
},
|
|
189
|
+
"inference": { "$ref": "#/$defs/InferenceConfig" },
|
|
190
|
+
"server": { "$ref": "#/$defs/ServerConfig" }
|
|
191
|
+
},
|
|
192
|
+
"required": ["vault_path", "state_dir", "recall", "thresholds", "inference"],
|
|
193
|
+
"additionalProperties": false
|
|
194
|
+
},
|
|
195
|
+
"InferenceConfig": {
|
|
196
|
+
"oneOf": [
|
|
197
|
+
{
|
|
198
|
+
"type": "object",
|
|
199
|
+
"properties": {
|
|
200
|
+
"mode": { "const": "local" },
|
|
201
|
+
"bundle": { "const": "gte-small-v1" },
|
|
202
|
+
"models_dir": { "type": "string", "default": "~/.cache/skillmux/models" },
|
|
203
|
+
"embedding": {
|
|
204
|
+
"type": "object",
|
|
205
|
+
"properties": {
|
|
206
|
+
"model": { "const": "Xenova/gte-small" },
|
|
207
|
+
"dimension": { "const": 384 },
|
|
208
|
+
"device": { "type": "string" },
|
|
209
|
+
"dtype": { "type": "string" }
|
|
210
|
+
},
|
|
211
|
+
"required": ["model", "dimension"],
|
|
212
|
+
"additionalProperties": false
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
"required": ["mode", "bundle", "models_dir", "embedding"],
|
|
216
|
+
"additionalProperties": false
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
"type": "object",
|
|
220
|
+
"properties": {
|
|
221
|
+
"mode": { "const": "remote" },
|
|
222
|
+
"timeout_ms": { "type": "integer", "minimum": 100, "maximum": 30000, "default": 2000 },
|
|
223
|
+
"embedding": {
|
|
224
|
+
"type": "object",
|
|
225
|
+
"properties": {
|
|
226
|
+
"provider": { "const": "openai" },
|
|
227
|
+
"base_url": { "type": "string", "format": "uri" },
|
|
228
|
+
"model": { "type": "string", "minLength": 1 },
|
|
229
|
+
"dimension": { "type": "integer", "minimum": 1 },
|
|
230
|
+
"api_key_env": { "type": "string", "minLength": 1 }
|
|
231
|
+
},
|
|
232
|
+
"required": ["provider", "base_url", "model", "dimension"],
|
|
233
|
+
"additionalProperties": false
|
|
234
|
+
},
|
|
235
|
+
"reranker": {
|
|
236
|
+
"type": "object",
|
|
237
|
+
"properties": {
|
|
238
|
+
"provider": { "const": "infinity" },
|
|
239
|
+
"base_url": { "type": "string", "format": "uri" },
|
|
240
|
+
"model": { "type": "string", "minLength": 1 },
|
|
241
|
+
"api_key_env": { "type": "string", "minLength": 1 }
|
|
242
|
+
},
|
|
243
|
+
"required": ["provider", "base_url", "model"],
|
|
244
|
+
"additionalProperties": false
|
|
245
|
+
},
|
|
246
|
+
"thresholds": {
|
|
247
|
+
"type": "object",
|
|
248
|
+
"description": "Model-specific calibrated values; required when reranker is configured.",
|
|
249
|
+
"properties": {
|
|
250
|
+
"match_score": { "type": "number" },
|
|
251
|
+
"match_margin": { "type": "number", "minimum": 0 },
|
|
252
|
+
"candidate_floor": { "type": "number" }
|
|
253
|
+
},
|
|
254
|
+
"required": ["match_score", "match_margin", "candidate_floor"],
|
|
255
|
+
"additionalProperties": false
|
|
256
|
+
}
|
|
257
|
+
},
|
|
258
|
+
"required": ["mode", "timeout_ms", "embedding"],
|
|
259
|
+
"additionalProperties": false
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
},
|
|
263
|
+
"ServerConfig": {
|
|
264
|
+
"type": "object",
|
|
265
|
+
"properties": {
|
|
266
|
+
"auth_enabled": { "type": "boolean", "default": false },
|
|
267
|
+
"auth_token_env": { "type": "string", "default": "SKILLMUX_AUTH_TOKEN" },
|
|
268
|
+
"allowed_origins": { "type": "array", "items": { "type": "string" }, "default": ["*"] },
|
|
269
|
+
"rate_limit": {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"properties": {
|
|
272
|
+
"enabled": { "type": "boolean", "default": false },
|
|
273
|
+
"requests_per_minute": { "type": "integer", "minimum": 1, "default": 60 }
|
|
274
|
+
},
|
|
275
|
+
"required": ["enabled", "requests_per_minute"],
|
|
276
|
+
"additionalProperties": false
|
|
277
|
+
}
|
|
278
|
+
},
|
|
279
|
+
"required": ["auth_enabled", "auth_token_env", "allowed_origins"],
|
|
280
|
+
"additionalProperties": false
|
|
281
|
+
},
|
|
282
|
+
"AuditRow": {
|
|
283
|
+
"type": "object",
|
|
284
|
+
"description": "AC10: one row per resolve_skill call, appended to the audit table in the state-dir SQLite. Raw query stored deliberately (single-user private homelab).",
|
|
285
|
+
"properties": {
|
|
286
|
+
"id": { "type": "integer", "minimum": 1 },
|
|
287
|
+
"ts": { "type": "string", "format": "date-time" },
|
|
288
|
+
"query": { "type": "string" },
|
|
289
|
+
"outcome": { "type": "string", "enum": ["matched", "ambiguous", "no_match"] },
|
|
290
|
+
"retrieval": { "enum": ["exact", "reranked", "hybrid", "lexical"] },
|
|
291
|
+
"candidates": {
|
|
292
|
+
"type": "array",
|
|
293
|
+
"description": "skill_id + score of every reranked (or FTS5-ranked) candidate considered, best first.",
|
|
294
|
+
"items": {
|
|
295
|
+
"type": "object",
|
|
296
|
+
"properties": {
|
|
297
|
+
"skill_id": { "$ref": "#/$defs/SkillId" },
|
|
298
|
+
"score": { "type": ["number", "null"] }
|
|
299
|
+
},
|
|
300
|
+
"required": ["skill_id", "score"],
|
|
301
|
+
"additionalProperties": false
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
"selected_skill_id": {
|
|
305
|
+
"oneOf": [{ "$ref": "#/$defs/SkillId" }, { "type": "null" }],
|
|
306
|
+
"description": "Set iff outcome=matched."
|
|
307
|
+
},
|
|
308
|
+
"latency_ms": { "type": "integer", "minimum": 0 }
|
|
309
|
+
},
|
|
310
|
+
"required": ["id", "ts", "query", "outcome", "retrieval", "candidates", "selected_skill_id", "latency_ms"],
|
|
311
|
+
"additionalProperties": false
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@klhapp/skillmux",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Local read-only MCP server routing natural-language task queries to skills in a SKILL.md vault, with zero-loss delivery",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"author": "Lance Hsu <lance@klh.app>",
|
|
8
|
+
"license": "MIT",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/klhq/skillmux.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/klhq/skillmux/issues"
|
|
15
|
+
},
|
|
16
|
+
"homepage": "https://github.com/klhq/skillmux#readme",
|
|
17
|
+
"bin": {
|
|
18
|
+
"skillmux": "./src/cli.ts"
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"src",
|
|
25
|
+
"docs/schema.json",
|
|
26
|
+
"docs/configuration.md",
|
|
27
|
+
"docs/releasing.md",
|
|
28
|
+
"README.md",
|
|
29
|
+
"LICENSE",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"config.example.toml",
|
|
32
|
+
"config.remote.example.toml"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "bun test",
|
|
36
|
+
"serve": "bun run src/cli.ts serve",
|
|
37
|
+
"index": "bun run src/cli.ts index",
|
|
38
|
+
"eval": "bun run src/cli.ts eval",
|
|
39
|
+
"doctor": "bun run src/cli.ts doctor",
|
|
40
|
+
"models:download": "bun run src/cli.ts models download",
|
|
41
|
+
"build": "bun build --compile src/cli.ts --outfile dist/skillmux"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"bun-types": "^1.3.14"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@huggingface/transformers": "^4.2.0",
|
|
48
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
49
|
+
"zod": "^4.4.3"
|
|
50
|
+
},
|
|
51
|
+
"trustedDependencies": [
|
|
52
|
+
"onnxruntime-node",
|
|
53
|
+
"protobufjs"
|
|
54
|
+
]
|
|
55
|
+
}
|
package/src/audit.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AuditRow } from "./types";
|
|
2
|
+
|
|
3
|
+
/** Shape an audit row to exactly the schema's AuditRow fields — nothing extra survives. */
|
|
4
|
+
export function buildAuditRow(row: AuditRow): AuditRow {
|
|
5
|
+
return {
|
|
6
|
+
id: row.id,
|
|
7
|
+
ts: row.ts,
|
|
8
|
+
query: row.query,
|
|
9
|
+
outcome: row.outcome,
|
|
10
|
+
retrieval: row.retrieval,
|
|
11
|
+
candidates: row.candidates.map((c) => ({ skill_id: c.skill_id, score: c.score })),
|
|
12
|
+
selected_skill_id: row.selected_skill_id,
|
|
13
|
+
latency_ms: row.latency_ms,
|
|
14
|
+
};
|
|
15
|
+
}
|