@klhapp/skillmux 1.9.3 → 1.11.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 +46 -0
- package/README.md +19 -19
- package/docs/README.md +4 -4
- package/docs/assets/architecture-dark.svg +39 -32
- package/docs/assets/architecture-light.svg +25 -18
- package/docs/cli.md +147 -36
- package/docs/concepts.md +11 -11
- package/docs/configuration.md +7 -5
- package/docs/deployment.md +10 -6
- package/docs/getting-started.md +18 -14
- package/docs/mcp-routing.md +1 -1
- package/docs/skill-management.md +17 -11
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/src/adapters.ts +157 -11
- package/src/cli.ts +396 -1319
- package/src/commands/audit.ts +53 -56
- package/src/commands/config.ts +33 -26
- package/src/commands/context.ts +104 -0
- package/src/commands/core.ts +7 -3
- package/src/commands/doctor.ts +97 -0
- package/src/commands/eval.ts +22 -15
- package/src/commands/init.ts +672 -0
- package/src/commands/install.ts +132 -0
- package/src/commands/local-vault.ts +60 -0
- package/src/commands/models.ts +10 -0
- package/src/commands/outdated.ts +2 -1
- package/src/commands/project.ts +194 -51
- package/src/commands/report.ts +66 -0
- package/src/commands/scan.ts +61 -0
- package/src/commands/shared.ts +7 -14
- package/src/commands/skill.ts +33 -0
- package/src/commands/sync.ts +232 -0
- package/src/commands/target.ts +45 -15
- package/src/commands/update.ts +2 -1
- package/src/completions.ts +41 -15
- package/src/config-service.ts +4 -54
- package/src/context.ts +8 -3
- package/src/db-audit.ts +286 -0
- package/src/db-index.ts +238 -0
- package/src/db.ts +3 -521
- package/src/global-flags.ts +46 -0
- package/src/init-agents.ts +329 -0
- package/src/init-instructions.ts +47 -28
- package/src/logger.ts +26 -0
- package/src/mcp-registration.ts +89 -0
- package/src/output.ts +80 -18
- package/src/prompts.ts +75 -20
- package/src/router-core.ts +8 -27
- package/src/scan.ts +19 -19
- package/src/server.ts +161 -14
- package/src/toml-writer.ts +51 -0
- package/src/init-clients.ts +0 -220
package/docs/skill-management.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# Managing skills
|
|
2
2
|
|
|
3
3
|
Skillmux keeps skill content in a vault checkout and materializes selected
|
|
4
|
-
skills into
|
|
4
|
+
skills into agent directories. The vault source of truth is the logical
|
|
5
5
|
collection; a checkout is its physical copy. This guide covers the commands
|
|
6
6
|
that change or inspect that state.
|
|
7
7
|
|
|
8
|
-
Run these commands on the machine that owns the vault checkout and
|
|
8
|
+
Run these commands on the machine that owns the vault checkout and agent
|
|
9
9
|
directories. For a retrieval-only Docker service, manage the mounted checkout
|
|
10
10
|
on the host and keep the container mount read-only; the server image does not
|
|
11
11
|
manage host agent directories.
|
|
@@ -114,28 +114,28 @@ skill whose on-disk content has drifted from what was last installed —
|
|
|
114
114
|
someone may have hand-edited it. Pass `--force` to overwrite anyway. Like
|
|
115
115
|
`audit prune`, a non-interactive or `--json` run needs `--yes`.
|
|
116
116
|
|
|
117
|
-
## Plan
|
|
117
|
+
## Plan agent delivery
|
|
118
118
|
|
|
119
|
-
Use product names for common
|
|
119
|
+
Use product names for common agents:
|
|
120
120
|
|
|
121
121
|
```sh
|
|
122
|
-
skillmux init --
|
|
122
|
+
skillmux init --agent claude-code --agent codex --dry-run
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
For a directory that isn't tied to any supported agent, use `target add`
|
|
126
|
+
directly instead of `init`:
|
|
126
127
|
|
|
127
128
|
```sh
|
|
128
|
-
skillmux
|
|
129
|
-
skillmux init --target custom --dir /srv/my-agent/skills --yes
|
|
129
|
+
skillmux target add my-agent --dir /srv/my-agent/skills --yes
|
|
130
130
|
```
|
|
131
131
|
|
|
132
132
|
Skillmux refuses to adopt a target that points to the whole vault because sync
|
|
133
133
|
would reduce its visible skills. Review that migration first:
|
|
134
134
|
|
|
135
135
|
```sh
|
|
136
|
-
skillmux init --
|
|
136
|
+
skillmux init --agent claude-code --migrate-full-vault \
|
|
137
137
|
--core csv-formatter --dry-run
|
|
138
|
-
skillmux init --
|
|
138
|
+
skillmux init --agent claude-code --migrate-full-vault \
|
|
139
139
|
--core csv-formatter --yes
|
|
140
140
|
```
|
|
141
141
|
|
|
@@ -170,7 +170,7 @@ skillmux project list
|
|
|
170
170
|
skillmux project show my-project
|
|
171
171
|
skillmux project add-path my-project ~/code/my-project --yes
|
|
172
172
|
skillmux project pin my-project code-context --yes
|
|
173
|
-
skillmux project attach my-project --
|
|
173
|
+
skillmux project attach my-project --agent claude-code --agent codex --yes
|
|
174
174
|
skillmux project unpin my-project code-context --yes
|
|
175
175
|
skillmux project detach my-project --target codex --yes
|
|
176
176
|
skillmux project remove-path my-project ~/code/my-project --yes
|
|
@@ -267,6 +267,12 @@ true` with a token, or it refuses to start. `--stats-port` is rejected
|
|
|
267
267
|
alongside `--transport http`, since that transport already serves `/stats` on
|
|
268
268
|
`--port`.
|
|
269
269
|
|
|
270
|
+
`--context`/`--server` work the same way for `audit prune`, `eval`, `eval
|
|
271
|
+
promote`, and `doctor` — each hits the matching `/admin/v1/*` route on the
|
|
272
|
+
named deployment instead of the local vault and audit db. See [CLI
|
|
273
|
+
reference](cli.md#administrative-http-api-adminv1) for the full remote
|
|
274
|
+
command surface and route table.
|
|
275
|
+
|
|
270
276
|
## Target ownership and recovery
|
|
271
277
|
|
|
272
278
|
`skillmux target remove <name> --yes` removes the manifest record and preserves
|
package/docs/troubleshooting.md
CHANGED
|
@@ -113,8 +113,8 @@ also reveals local-overlay shadowing.
|
|
|
113
113
|
Skillmux will not change an existing unmarked directory. Adopt it first:
|
|
114
114
|
|
|
115
115
|
```sh
|
|
116
|
-
skillmux init --
|
|
117
|
-
skillmux init --
|
|
116
|
+
skillmux init --agent claude-code --dry-run
|
|
117
|
+
skillmux init --agent claude-code --yes
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
### Target points to the full vault
|
|
@@ -122,7 +122,7 @@ skillmux init --client claude-code --yes
|
|
|
122
122
|
Review the smaller pinned set before converting:
|
|
123
123
|
|
|
124
124
|
```sh
|
|
125
|
-
skillmux init --
|
|
125
|
+
skillmux init --agent claude-code \
|
|
126
126
|
--migrate-full-vault \
|
|
127
127
|
--core code-context \
|
|
128
128
|
--dry-run
|
|
@@ -239,7 +239,7 @@ skillmux --context prod config status
|
|
|
239
239
|
An MCP token for `/mcp` cannot authenticate this request, and the administrative
|
|
240
240
|
token cannot authenticate an MCP client. A named context administers the
|
|
241
241
|
deployed server configuration only; use Skillmux CLI on the machine that owns
|
|
242
|
-
|
|
242
|
+
agent directories for `install`, pinning, or `sync`.
|
|
243
243
|
|
|
244
244
|
### Client receives `429`
|
|
245
245
|
|
package/package.json
CHANGED
package/src/adapters.ts
CHANGED
|
@@ -14,8 +14,11 @@ import {
|
|
|
14
14
|
type ConfigStatusResponse,
|
|
15
15
|
type SetConfigResult,
|
|
16
16
|
} from "./config-service";
|
|
17
|
-
import type {
|
|
17
|
+
import type { ResolvedContext } from "./context";
|
|
18
18
|
import type { Clients, Config } from "./types";
|
|
19
|
+
import { getStats, parseSince, type StatsResponse } from "./stats";
|
|
20
|
+
import { countPrunable, openAudit, pruneAuditBefore, type PruneResult } from "./db";
|
|
21
|
+
import { buildPromotedCases, evalVault, queryPromotableFetches, type EvalCase, type EvalReport } from "./eval";
|
|
19
22
|
|
|
20
23
|
export interface Capabilities {
|
|
21
24
|
config_read: boolean;
|
|
@@ -25,13 +28,28 @@ export interface Capabilities {
|
|
|
25
28
|
restart_required_keys: string[];
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
export interface
|
|
31
|
+
export interface ContextAdapterOptions {
|
|
29
32
|
configPath?: string;
|
|
30
33
|
allowInsecure?: boolean;
|
|
31
34
|
clients?: Clients;
|
|
32
35
|
}
|
|
33
36
|
|
|
34
|
-
export interface
|
|
37
|
+
export interface AuditPruneOptions {
|
|
38
|
+
older_than?: string;
|
|
39
|
+
dry_run?: boolean;
|
|
40
|
+
confirm?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AuditPruneResult extends PruneResult {
|
|
44
|
+
dry_run: boolean;
|
|
45
|
+
cutoff: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Context adapter: `local` = this CLI process has the Skillmux runtime (vault, index,
|
|
50
|
+
* audit db, embeddings/reranker clients) in-process; `remote` = thin network client to an external process.
|
|
51
|
+
*/
|
|
52
|
+
export interface ContextAdapter {
|
|
35
53
|
getCapabilities(): Promise<Capabilities>;
|
|
36
54
|
getConfigShow(): Promise<{ effective: Config; sources: Record<string, string>; active_revision: string }>;
|
|
37
55
|
getConfigGet(key: string): Promise<unknown>;
|
|
@@ -39,6 +57,11 @@ export interface TargetAdapter {
|
|
|
39
57
|
configDiff(): Promise<{ diff: Record<string, { prior: unknown; resulting: unknown }> }>;
|
|
40
58
|
configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult>;
|
|
41
59
|
configStatus(): Promise<ConfigStatusResponse>;
|
|
60
|
+
getStats(since: string): Promise<StatsResponse>;
|
|
61
|
+
auditPrune(opts?: AuditPruneOptions): Promise<AuditPruneResult>;
|
|
62
|
+
auditCount(older_than?: string): Promise<AuditPruneResult>;
|
|
63
|
+
evalRun(): Promise<EvalReport>;
|
|
64
|
+
evalPromote(since: string): Promise<EvalCase[]>;
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
export function isLoopbackHost(hostname: string): boolean {
|
|
@@ -51,11 +74,11 @@ export function isLoopbackHost(hostname: string): boolean {
|
|
|
51
74
|
);
|
|
52
75
|
}
|
|
53
76
|
|
|
54
|
-
export class LocalAdapter implements
|
|
77
|
+
export class LocalAdapter implements ContextAdapter {
|
|
55
78
|
private configPath: string;
|
|
56
79
|
private clients?: Clients;
|
|
57
80
|
|
|
58
|
-
constructor(opts?:
|
|
81
|
+
constructor(opts?: ContextAdapterOptions) {
|
|
59
82
|
this.configPath = resolveConfigPath(opts?.configPath);
|
|
60
83
|
this.clients = opts?.clients;
|
|
61
84
|
}
|
|
@@ -109,21 +132,90 @@ export class LocalAdapter implements TargetAdapter {
|
|
|
109
132
|
return setDottedKey(key, rawValStr, {
|
|
110
133
|
configPath: this.configPath,
|
|
111
134
|
dryRun: opts?.dryRun,
|
|
112
|
-
|
|
135
|
+
contextName: "local",
|
|
113
136
|
});
|
|
114
137
|
}
|
|
115
138
|
|
|
116
139
|
async configStatus(): Promise<ConfigStatusResponse> {
|
|
117
140
|
return getLocalConfigStatus(this.configPath);
|
|
118
141
|
}
|
|
142
|
+
|
|
143
|
+
async getStats(since: string): Promise<StatsResponse> {
|
|
144
|
+
const config = await loadConfig(this.configPath);
|
|
145
|
+
const stateDir = expandHome(config.state_dir);
|
|
146
|
+
const db = openAudit(stateDir);
|
|
147
|
+
try {
|
|
148
|
+
return getStats(db, since);
|
|
149
|
+
} finally {
|
|
150
|
+
db.close();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async auditPrune(opts?: AuditPruneOptions): Promise<AuditPruneResult> {
|
|
155
|
+
const config = await loadConfig(this.configPath);
|
|
156
|
+
const stateDir = expandHome(config.state_dir);
|
|
157
|
+
const olderThan = opts?.older_than;
|
|
158
|
+
const dryRun = opts?.dry_run ?? false;
|
|
159
|
+
|
|
160
|
+
let cutoff: Date;
|
|
161
|
+
if (olderThan) {
|
|
162
|
+
cutoff = parseSince(olderThan);
|
|
163
|
+
} else {
|
|
164
|
+
const retentionDays = config.audit?.retention_days ?? 90;
|
|
165
|
+
if (retentionDays <= 0) {
|
|
166
|
+
return {
|
|
167
|
+
audit_deleted: 0,
|
|
168
|
+
fetch_deleted: 0,
|
|
169
|
+
admin_audit_deleted: 0,
|
|
170
|
+
dry_run: dryRun,
|
|
171
|
+
cutoff: null,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
cutoff = new Date(Date.now() - retentionDays * 86_400_000);
|
|
175
|
+
}
|
|
176
|
+
const cutoffIso = cutoff.toISOString();
|
|
177
|
+
|
|
178
|
+
const db = openAudit(stateDir);
|
|
179
|
+
try {
|
|
180
|
+
if (dryRun) {
|
|
181
|
+
const counts = countPrunable(db, cutoffIso);
|
|
182
|
+
return { ...counts, dry_run: true, cutoff: cutoffIso };
|
|
183
|
+
}
|
|
184
|
+
const counts = pruneAuditBefore(db, cutoffIso);
|
|
185
|
+
return { ...counts, dry_run: false, cutoff: cutoffIso };
|
|
186
|
+
} finally {
|
|
187
|
+
db.close();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async auditCount(older_than?: string): Promise<AuditPruneResult> {
|
|
192
|
+
return this.auditPrune({ older_than, dry_run: true });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async evalRun(): Promise<EvalReport> {
|
|
196
|
+
return evalVault();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async evalPromote(since: string): Promise<EvalCase[]> {
|
|
200
|
+
const sinceDate = parseSince(since);
|
|
201
|
+
const sinceIso = sinceDate.toISOString();
|
|
202
|
+
const config = await loadConfig(this.configPath);
|
|
203
|
+
const stateDir = expandHome(config.state_dir);
|
|
204
|
+
const db = openAudit(stateDir);
|
|
205
|
+
try {
|
|
206
|
+
return buildPromotedCases(queryPromotableFetches(db, sinceIso));
|
|
207
|
+
} finally {
|
|
208
|
+
db.close();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
119
211
|
}
|
|
120
212
|
|
|
121
|
-
export class RemoteAdapter implements
|
|
213
|
+
export class RemoteAdapter implements ContextAdapter {
|
|
122
214
|
private serverUrl: string;
|
|
123
215
|
private tokenEnv?: string;
|
|
124
216
|
private allowInsecure: boolean;
|
|
125
217
|
|
|
126
|
-
constructor(target: { server: string; token_env?: string }, opts?:
|
|
218
|
+
constructor(target: { server: string; token_env?: string }, opts?: ContextAdapterOptions) {
|
|
127
219
|
this.serverUrl = target.server.replace(/\/$/, "");
|
|
128
220
|
this.tokenEnv = target.token_env;
|
|
129
221
|
this.allowInsecure = opts?.allowInsecure ?? false;
|
|
@@ -281,12 +373,66 @@ export class RemoteAdapter implements TargetAdapter {
|
|
|
281
373
|
}
|
|
282
374
|
return data.runtime;
|
|
283
375
|
}
|
|
376
|
+
|
|
377
|
+
async getStats(since: string): Promise<StatsResponse> {
|
|
378
|
+
const searchParams = new URLSearchParams({ since });
|
|
379
|
+
const { status, data } = await this.fetchJson(`/stats?${searchParams.toString()}`);
|
|
380
|
+
if (status !== 200) {
|
|
381
|
+
throw new Error(`Remote stats fetch failed (${status}): ${typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data}`);
|
|
382
|
+
}
|
|
383
|
+
return data as StatsResponse;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async auditPrune(opts?: AuditPruneOptions): Promise<AuditPruneResult> {
|
|
387
|
+
const { status, data } = await this.fetchJson("/admin/v1/audit/prune", {
|
|
388
|
+
method: "POST",
|
|
389
|
+
headers: { "Content-Type": "application/json" },
|
|
390
|
+
body: JSON.stringify({
|
|
391
|
+
...(opts?.older_than ? { older_than: opts.older_than } : {}),
|
|
392
|
+
...(opts?.dry_run !== undefined ? { dry_run: opts.dry_run } : {}),
|
|
393
|
+
...(opts?.confirm !== undefined ? { confirm: opts.confirm } : {}),
|
|
394
|
+
}),
|
|
395
|
+
});
|
|
396
|
+
if (status !== 200) {
|
|
397
|
+
const message = typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data;
|
|
398
|
+
throw new Error(`Remote audit prune failed (${status}): ${message}`);
|
|
399
|
+
}
|
|
400
|
+
return data as AuditPruneResult;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
async auditCount(older_than?: string): Promise<AuditPruneResult> {
|
|
404
|
+
return this.auditPrune({ older_than, dry_run: true });
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
async evalRun(): Promise<EvalReport> {
|
|
408
|
+
const { status, data } = await this.fetchJson("/admin/v1/eval", {
|
|
409
|
+
method: "POST",
|
|
410
|
+
});
|
|
411
|
+
if (status !== 200) {
|
|
412
|
+
const message = typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data;
|
|
413
|
+
throw new Error(`Remote eval failed (${status}): ${message}`);
|
|
414
|
+
}
|
|
415
|
+
return data as EvalReport;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async evalPromote(since: string): Promise<EvalCase[]> {
|
|
419
|
+
const { status, data } = await this.fetchJson("/admin/v1/eval/promote", {
|
|
420
|
+
method: "POST",
|
|
421
|
+
headers: { "Content-Type": "application/json" },
|
|
422
|
+
body: JSON.stringify({ since }),
|
|
423
|
+
});
|
|
424
|
+
if (status !== 200) {
|
|
425
|
+
const message = typeof data === "object" ? data?.message || data?.error || JSON.stringify(data) : data;
|
|
426
|
+
throw new Error(`Remote eval promote failed (${status}): ${message}`);
|
|
427
|
+
}
|
|
428
|
+
return data.candidates as EvalCase[];
|
|
429
|
+
}
|
|
284
430
|
}
|
|
285
431
|
|
|
286
|
-
export function
|
|
287
|
-
if (
|
|
432
|
+
export function createContextAdapter(context: ResolvedContext, opts?: ContextAdapterOptions): ContextAdapter {
|
|
433
|
+
if (context.type === "local") {
|
|
288
434
|
return new LocalAdapter(opts);
|
|
289
435
|
} else {
|
|
290
|
-
return new RemoteAdapter({ server:
|
|
436
|
+
return new RemoteAdapter({ server: context.server, token_env: context.token_env }, opts);
|
|
291
437
|
}
|
|
292
438
|
}
|