@klhapp/skillmux 1.11.2 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +20 -10
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +15 -5
  6. package/docs/concepts.md +4 -3
  7. package/docs/configuration.md +22 -5
  8. package/docs/deployment.md +3 -3
  9. package/docs/getting-started.md +32 -7
  10. package/docs/releasing.md +17 -2
  11. package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
  12. package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
  13. package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
  14. package/docs/sdd/cli-remote-target-parity/think.md +183 -0
  15. package/docs/sdd/cli-surface-consistency/think.md +149 -0
  16. package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
  17. package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
  18. package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
  19. package/docs/sdd/feat-packaging-deployment/review.md +95 -0
  20. package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
  21. package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
  22. package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
  23. package/docs/sdd/npm-native-binaries/review.md +103 -0
  24. package/docs/sdd/npm-native-binaries/spec.md +148 -0
  25. package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
  26. package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
  27. package/docs/sdd/runtime-resource-hardening/review.md +86 -0
  28. package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
  29. package/docs/sdd/security-hardening-phase/think.md +129 -0
  30. package/docs/sdd/security-observability/review.md +96 -0
  31. package/docs/sdd/security-observability/spec.md +55 -0
  32. package/docs/sdd/skill-provenance-update/review.md +152 -0
  33. package/docs/sdd/skill-provenance-update/schema.json +193 -0
  34. package/docs/sdd/skill-provenance-update/spec.md +134 -0
  35. package/docs/sdd/supply-chain-hardening/review.md +50 -0
  36. package/docs/sdd/supply-chain-hardening/spec.md +42 -0
  37. package/docs/sdd/target-marker-rehome/spec.md +33 -0
  38. package/docs/skill-management.md +1 -1
  39. package/docs/troubleshooting.md +18 -2
  40. package/package.json +15 -8
  41. package/src/adapters.ts +0 -438
  42. package/src/audit.ts +0 -21
  43. package/src/cli.ts +0 -928
  44. package/src/clients.ts +0 -368
  45. package/src/commands/audit.ts +0 -85
  46. package/src/commands/config.ts +0 -224
  47. package/src/commands/context.ts +0 -104
  48. package/src/commands/core.ts +0 -56
  49. package/src/commands/doctor.ts +0 -97
  50. package/src/commands/eval.ts +0 -88
  51. package/src/commands/init.ts +0 -672
  52. package/src/commands/install.ts +0 -155
  53. package/src/commands/local-vault.ts +0 -60
  54. package/src/commands/models.ts +0 -10
  55. package/src/commands/outdated.ts +0 -115
  56. package/src/commands/project.ts +0 -580
  57. package/src/commands/report.ts +0 -66
  58. package/src/commands/scan.ts +0 -71
  59. package/src/commands/shared.ts +0 -38
  60. package/src/commands/skill.ts +0 -33
  61. package/src/commands/sync.ts +0 -233
  62. package/src/commands/target.ts +0 -225
  63. package/src/commands/update.ts +0 -277
  64. package/src/completions.ts +0 -196
  65. package/src/concurrency-limiter.ts +0 -61
  66. package/src/config-service.ts +0 -398
  67. package/src/config-watcher.ts +0 -253
  68. package/src/config.ts +0 -583
  69. package/src/context.ts +0 -188
  70. package/src/db-audit.ts +0 -286
  71. package/src/db-index.ts +0 -238
  72. package/src/db.ts +0 -3
  73. package/src/deployment.ts +0 -39
  74. package/src/doctor.ts +0 -205
  75. package/src/eval.ts +0 -302
  76. package/src/global-flags.ts +0 -46
  77. package/src/init-agents.ts +0 -329
  78. package/src/init-instructions.ts +0 -192
  79. package/src/init.ts +0 -389
  80. package/src/install.ts +0 -232
  81. package/src/lifecycle.ts +0 -51
  82. package/src/logger.ts +0 -26
  83. package/src/manifest.ts +0 -352
  84. package/src/mcp-registration.ts +0 -89
  85. package/src/metrics.ts +0 -121
  86. package/src/models.ts +0 -20
  87. package/src/output.ts +0 -214
  88. package/src/project-setup.ts +0 -36
  89. package/src/prompts.ts +0 -124
  90. package/src/provenance.ts +0 -99
  91. package/src/rate-limiter.ts +0 -137
  92. package/src/readiness.ts +0 -30
  93. package/src/redact.ts +0 -52
  94. package/src/router-core.ts +0 -666
  95. package/src/rrf.ts +0 -31
  96. package/src/scan.ts +0 -300
  97. package/src/server.ts +0 -930
  98. package/src/setup.ts +0 -145
  99. package/src/snapshot.ts +0 -135
  100. package/src/stats.ts +0 -340
  101. package/src/sync.ts +0 -482
  102. package/src/toml-writer.ts +0 -51
  103. package/src/types.ts +0 -212
  104. package/src/vault.ts +0 -207
package/src/adapters.ts DELETED
@@ -1,438 +0,0 @@
1
- import { createClients } from "./clients";
2
- import { expandHome, loadConfig, resolveConfigPath } from "./config";
3
- import { CliError } from "./output";
4
- import {
5
- computeHash,
6
- getDottedKey,
7
- getEffectiveConfig,
8
- getLocalConfigStatus,
9
- getNestedValue,
10
- RELOADABLE_KEYS,
11
- RESTART_REQUIRED_KEYS,
12
- setDottedKey,
13
- validateDottedKey,
14
- type ConfigStatusResponse,
15
- type SetConfigResult,
16
- } from "./config-service";
17
- import type { ResolvedContext } from "./context";
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";
22
-
23
- export interface Capabilities {
24
- config_read: boolean;
25
- config_write: boolean;
26
- persistence: "writable" | "externally_managed";
27
- reloadable_keys: string[];
28
- restart_required_keys: string[];
29
- }
30
-
31
- export interface ContextAdapterOptions {
32
- configPath?: string;
33
- allowInsecure?: boolean;
34
- clients?: Clients;
35
- }
36
-
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 {
53
- getCapabilities(): Promise<Capabilities>;
54
- getConfigShow(): Promise<{ effective: Config; sources: Record<string, string>; active_revision: string }>;
55
- getConfigGet(key: string): Promise<unknown>;
56
- configValidate(): Promise<{ valid: boolean; readiness: unknown }>;
57
- configDiff(): Promise<{ diff: Record<string, { prior: unknown; resulting: unknown }> }>;
58
- configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult>;
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[]>;
65
- }
66
-
67
- export function isLoopbackHost(hostname: string): boolean {
68
- return (
69
- hostname === "localhost" ||
70
- hostname === "127.0.0.1" ||
71
- hostname === "::1" ||
72
- hostname === "0.0.0.0" ||
73
- hostname.startsWith("127.")
74
- );
75
- }
76
-
77
- export class LocalAdapter implements ContextAdapter {
78
- private configPath: string;
79
- private clients?: Clients;
80
-
81
- constructor(opts?: ContextAdapterOptions) {
82
- this.configPath = resolveConfigPath(opts?.configPath);
83
- this.clients = opts?.clients;
84
- }
85
-
86
- async getCapabilities(): Promise<Capabilities> {
87
- const isExternallyManaged = process.env.SKILLMUX_CONFIG_READONLY === "true";
88
- return {
89
- config_read: true,
90
- config_write: !isExternallyManaged,
91
- persistence: isExternallyManaged ? "externally_managed" : "writable",
92
- reloadable_keys: RELOADABLE_KEYS,
93
- restart_required_keys: RESTART_REQUIRED_KEYS,
94
- };
95
- }
96
-
97
- async getConfigShow(): Promise<{ effective: Config; sources: Record<string, string>; active_revision: string }> {
98
- const { effective, sources } = await getEffectiveConfig(this.configPath);
99
- return {
100
- effective,
101
- sources,
102
- active_revision: computeHash(effective),
103
- };
104
- }
105
-
106
- async getConfigGet(key: string): Promise<unknown> {
107
- return getDottedKey(key, this.configPath);
108
- }
109
-
110
- async configValidate(): Promise<{ valid: boolean; readiness: unknown }> {
111
- const { effective } = await getEffectiveConfig(this.configPath);
112
- return { valid: !!effective, readiness: { status: "ready", capability: "hybrid" } };
113
- }
114
-
115
- async configDiff(): Promise<{ diff: Record<string, { prior: unknown; resulting: unknown }> }> {
116
- const { effective, sources } = await getEffectiveConfig(this.configPath);
117
- const diff: Record<string, { prior: unknown; resulting: unknown }> = {};
118
- for (const [k, src] of Object.entries(sources)) {
119
- if (src !== "default") {
120
- diff[k] = { prior: "default", resulting: getNestedValue(effective as any, k) };
121
- }
122
- }
123
- return { diff };
124
- }
125
-
126
- async configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult> {
127
- const caps = await this.getCapabilities();
128
- if (caps.persistence === "externally_managed") {
129
- throw new CliError("Configuration is externally managed and cannot be modified", 4);
130
- }
131
- validateDottedKey(key);
132
- return setDottedKey(key, rawValStr, {
133
- configPath: this.configPath,
134
- dryRun: opts?.dryRun,
135
- contextName: "local",
136
- });
137
- }
138
-
139
- async configStatus(): Promise<ConfigStatusResponse> {
140
- return getLocalConfigStatus(this.configPath);
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
- }
211
- }
212
-
213
- export class RemoteAdapter implements ContextAdapter {
214
- private serverUrl: string;
215
- private tokenEnv?: string;
216
- private allowInsecure: boolean;
217
-
218
- constructor(target: { server: string; token_env?: string }, opts?: ContextAdapterOptions) {
219
- this.serverUrl = target.server.replace(/\/$/, "");
220
- this.tokenEnv = target.token_env;
221
- this.allowInsecure = opts?.allowInsecure ?? false;
222
-
223
- this.validateSecurity();
224
- }
225
-
226
- private validateSecurity(): void {
227
- try {
228
- const url = new URL(this.serverUrl);
229
- if (url.protocol === "http:" && !isLoopbackHost(url.hostname) && !this.allowInsecure) {
230
- throw new Error(
231
- `Plaintext HTTP admin targets are not allowed for non-loopback server "${this.serverUrl}". Pass --allow-insecure to bypass.`
232
- );
233
- }
234
- } catch (err: any) {
235
- if (err.message.includes("Plaintext HTTP")) throw err;
236
- throw new Error(`Invalid server URL "${this.serverUrl}"`);
237
- }
238
- }
239
-
240
- private getAuthHeader(): Record<string, string> {
241
- const envVar = this.tokenEnv || "SKILLMUX_ADMIN_TOKEN";
242
- const token = process.env[envVar];
243
- if (!token) {
244
- throw new Error(`Environment variable "${envVar}" for administrative authentication is empty`);
245
- }
246
- return { Authorization: `Bearer ${token}` };
247
- }
248
-
249
- private async fetchJson(path: string, options: RequestInit = {}): Promise<{ status: number; headers: Headers; data: any }> {
250
- const url = `${this.serverUrl}${path}`;
251
- const headers = new Headers(options.headers);
252
- for (const [k, v] of Object.entries(this.getAuthHeader())) {
253
- headers.set(k, v);
254
- }
255
-
256
- try {
257
- const res = await fetch(url, { ...options, headers });
258
- const text = await res.text();
259
- let data: any = text;
260
- try {
261
- data = JSON.parse(text);
262
- } catch {
263
- // text
264
- }
265
- if (res.status === 401 || res.status === 403) {
266
- const message = typeof data === "object" && data ? data.message || data.error || data : data;
267
- throw new CliError(`Remote server rejected the request (${res.status}): ${message}`, 3);
268
- }
269
- return { status: res.status, headers: res.headers, data };
270
- } catch (err: any) {
271
- if (err instanceof CliError) throw err;
272
- throw new CliError(`Failed to reach remote server "${this.serverUrl}": ${err.message}`, 3);
273
- }
274
- }
275
-
276
- async getCapabilities(): Promise<Capabilities> {
277
- const { status, data } = await this.fetchJson("/admin/v1/capabilities");
278
- if (status !== 200) {
279
- throw new Error(`Remote capability discovery failed (${status}): ${typeof data === "object" ? data.message || data.error : data}`);
280
- }
281
- return data as Capabilities;
282
- }
283
-
284
- async getConfigShow(): Promise<{ effective: Config; sources: Record<string, string>; active_revision: string }> {
285
- const { status, data } = await this.fetchJson("/admin/v1/config");
286
- if (status !== 200) {
287
- throw new Error(`Remote config fetch failed (${status}): ${typeof data === "object" ? data.message || data.error : data}`);
288
- }
289
- return data;
290
- }
291
-
292
- async getConfigGet(key: string): Promise<unknown> {
293
- validateDottedKey(key);
294
- const { effective } = await this.getConfigShow();
295
- return getNestedValue(effective as any, key);
296
- }
297
-
298
- async configValidate(): Promise<{ valid: boolean; readiness: unknown }> {
299
- const show = await this.getConfigShow();
300
- return { valid: true, readiness: show.effective };
301
- }
302
-
303
- async configDiff(): Promise<{ diff: Record<string, { prior: unknown; resulting: unknown }> }> {
304
- const { effective, sources } = await this.getConfigShow();
305
- const diff: Record<string, { prior: unknown; resulting: unknown }> = {};
306
- for (const [k, src] of Object.entries(sources)) {
307
- if (src !== "default") {
308
- diff[k] = { prior: "default", resulting: getNestedValue(effective as any, k) };
309
- }
310
- }
311
- return { diff };
312
- }
313
-
314
- async configSet(key: string, rawValStr: string, opts?: { dryRun?: boolean }): Promise<SetConfigResult> {
315
- const caps = await this.getCapabilities();
316
- if (!caps.config_write || caps.persistence === "externally_managed") {
317
- throw new CliError("Remote server configuration is externally managed or read-only", 4);
318
- }
319
-
320
- const { status: showStatus, headers: showHeaders, data: showData } = await this.fetchJson("/admin/v1/config");
321
- if (showStatus !== 200) {
322
- throw new Error(`Remote config fetch failed (${showStatus})`);
323
- }
324
-
325
- const etag = showHeaders.get("etag") || `"${showData.active_revision}"`;
326
-
327
- if (opts?.dryRun) {
328
- const priorVal = getNestedValue(showData.effective, key);
329
- return {
330
- ok: true,
331
- key,
332
- prior_val: priorVal,
333
- resulting_val: rawValStr,
334
- target: "remote",
335
- prior_revision: showData.active_revision,
336
- resulting_revision: showData.active_revision,
337
- persistence: "not_persisted",
338
- application: "activated",
339
- readiness: { status: "ready", capability: "hybrid" },
340
- restart_required_keys: [],
341
- };
342
- }
343
-
344
- const { status, data } = await this.fetchJson("/admin/v1/config", {
345
- method: "PATCH",
346
- headers: {
347
- "Content-Type": "application/json",
348
- "If-Match": etag,
349
- },
350
- body: JSON.stringify({ changes: { [key]: rawValStr } }),
351
- });
352
-
353
- if (status === 409) {
354
- if (data?.error === "CONFIG_REVISION_CONFLICT") {
355
- throw new CliError(`Revision conflict: ${data.message || "Remote configuration was modified concurrently"}`, 4);
356
- }
357
- if (data?.error === "CONFIG_EXTERNALLY_MANAGED") {
358
- throw new CliError("Configuration is externally managed on the remote server", 4);
359
- }
360
- }
361
-
362
- if (status !== 200) {
363
- throw new Error(`Remote config set failed (${status}): ${data?.message || data?.error || data}`);
364
- }
365
-
366
- return data;
367
- }
368
-
369
- async configStatus(): Promise<ConfigStatusResponse> {
370
- const { status, data } = await this.fetchJson("/admin/v1/config");
371
- if (status !== 200) {
372
- throw new Error(`Remote status fetch failed (${status}): ${data?.message || data}`);
373
- }
374
- return data.runtime;
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
- }
430
- }
431
-
432
- export function createContextAdapter(context: ResolvedContext, opts?: ContextAdapterOptions): ContextAdapter {
433
- if (context.type === "local") {
434
- return new LocalAdapter(opts);
435
- } else {
436
- return new RemoteAdapter({ server: context.server, token_env: context.token_env }, opts);
437
- }
438
- }
package/src/audit.ts DELETED
@@ -1,21 +0,0 @@
1
- import type { AuditRow } from "./types";
2
-
3
- /** Shape an audit row to exactly the AuditRow fields — nothing extra survives. */
4
- export function buildAuditRow(row: AuditRow): AuditRow {
5
- const built: AuditRow = {
6
- id: row.id,
7
- ts: row.ts,
8
- request_id: row.request_id,
9
- query: row.query,
10
- retrieval: row.retrieval,
11
- candidates: row.candidates.map((c) => ({ skill_id: c.skill_id, score: c.score })),
12
- latency_ms: row.latency_ms,
13
- };
14
- if (row.degraded_from !== undefined && row.degraded_from !== null) {
15
- built.degraded_from = row.degraded_from;
16
- }
17
- if (row.degradation_reason !== undefined && row.degradation_reason !== null) {
18
- built.degradation_reason = row.degradation_reason;
19
- }
20
- return built;
21
- }