@klhapp/skillmux 1.12.0 → 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 (103) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +18 -9
  3. package/bin/skillmux.js +89 -0
  4. package/docs/README.md +4 -4
  5. package/docs/cli.md +14 -4
  6. package/docs/concepts.md +2 -2
  7. package/docs/configuration.md +18 -3
  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/troubleshooting.md +18 -2
  39. package/package.json +15 -8
  40. package/src/adapters.ts +0 -438
  41. package/src/audit.ts +0 -21
  42. package/src/cli.ts +0 -928
  43. package/src/clients.ts +0 -368
  44. package/src/commands/audit.ts +0 -85
  45. package/src/commands/config.ts +0 -224
  46. package/src/commands/context.ts +0 -104
  47. package/src/commands/core.ts +0 -56
  48. package/src/commands/doctor.ts +0 -97
  49. package/src/commands/eval.ts +0 -88
  50. package/src/commands/init.ts +0 -672
  51. package/src/commands/install.ts +0 -155
  52. package/src/commands/local-vault.ts +0 -60
  53. package/src/commands/models.ts +0 -10
  54. package/src/commands/outdated.ts +0 -115
  55. package/src/commands/project.ts +0 -580
  56. package/src/commands/report.ts +0 -66
  57. package/src/commands/scan.ts +0 -71
  58. package/src/commands/shared.ts +0 -38
  59. package/src/commands/skill.ts +0 -33
  60. package/src/commands/sync.ts +0 -233
  61. package/src/commands/target.ts +0 -225
  62. package/src/commands/update.ts +0 -277
  63. package/src/completions.ts +0 -196
  64. package/src/concurrency-limiter.ts +0 -61
  65. package/src/config-service.ts +0 -398
  66. package/src/config-watcher.ts +0 -253
  67. package/src/config.ts +0 -583
  68. package/src/context.ts +0 -188
  69. package/src/db-audit.ts +0 -286
  70. package/src/db-index.ts +0 -238
  71. package/src/db.ts +0 -3
  72. package/src/deployment.ts +0 -39
  73. package/src/doctor.ts +0 -205
  74. package/src/eval.ts +0 -302
  75. package/src/global-flags.ts +0 -46
  76. package/src/init-agents.ts +0 -329
  77. package/src/init-instructions.ts +0 -192
  78. package/src/init.ts +0 -390
  79. package/src/install.ts +0 -232
  80. package/src/lifecycle.ts +0 -51
  81. package/src/logger.ts +0 -26
  82. package/src/manifest.ts +0 -359
  83. package/src/mcp-registration.ts +0 -89
  84. package/src/metrics.ts +0 -121
  85. package/src/models.ts +0 -20
  86. package/src/output.ts +0 -214
  87. package/src/project-setup.ts +0 -36
  88. package/src/prompts.ts +0 -124
  89. package/src/provenance.ts +0 -99
  90. package/src/rate-limiter.ts +0 -137
  91. package/src/readiness.ts +0 -30
  92. package/src/redact.ts +0 -52
  93. package/src/router-core.ts +0 -666
  94. package/src/rrf.ts +0 -31
  95. package/src/scan.ts +0 -300
  96. package/src/server.ts +0 -930
  97. package/src/setup.ts +0 -145
  98. package/src/snapshot.ts +0 -135
  99. package/src/stats.ts +0 -340
  100. package/src/sync.ts +0 -482
  101. package/src/toml-writer.ts +0 -51
  102. package/src/types.ts +0 -212
  103. 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
- }