@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.
- package/CHANGELOG.md +15 -0
- package/README.md +20 -10
- package/bin/skillmux.js +89 -0
- package/docs/README.md +4 -4
- package/docs/cli.md +15 -5
- package/docs/concepts.md +4 -3
- package/docs/configuration.md +22 -5
- package/docs/deployment.md +3 -3
- package/docs/getting-started.md +32 -7
- package/docs/releasing.md +17 -2
- package/docs/sdd/cli-remote-target-parity/schema-bucket-b.yaml +297 -0
- package/docs/sdd/cli-remote-target-parity/spec-bucket-b.md +95 -0
- package/docs/sdd/cli-remote-target-parity/spec.md +111 -0
- package/docs/sdd/cli-remote-target-parity/think.md +183 -0
- package/docs/sdd/cli-surface-consistency/think.md +149 -0
- package/docs/sdd/config-authority-rerank-resilience/plan.md +338 -0
- package/docs/sdd/config-authority-rerank-resilience/review.md +124 -0
- package/docs/sdd/config-authority-rerank-resilience/spec.md +78 -0
- package/docs/sdd/feat-packaging-deployment/review.md +95 -0
- package/docs/sdd/fetch-outcome-flywheel/review.md +103 -0
- package/docs/sdd/fetch-outcome-flywheel/schema.json +347 -0
- package/docs/sdd/fetch-outcome-flywheel/spec.md +185 -0
- package/docs/sdd/npm-native-binaries/review.md +103 -0
- package/docs/sdd/npm-native-binaries/spec.md +148 -0
- package/docs/sdd/ranked-shortlist-2/pr3-calibration-removal.md +56 -0
- package/docs/sdd/refactor-builtin-target-paths/spec.md +41 -0
- package/docs/sdd/runtime-resource-hardening/review.md +86 -0
- package/docs/sdd/runtime-resource-hardening/spec.md +43 -0
- package/docs/sdd/security-hardening-phase/think.md +129 -0
- package/docs/sdd/security-observability/review.md +96 -0
- package/docs/sdd/security-observability/spec.md +55 -0
- package/docs/sdd/skill-provenance-update/review.md +152 -0
- package/docs/sdd/skill-provenance-update/schema.json +193 -0
- package/docs/sdd/skill-provenance-update/spec.md +134 -0
- package/docs/sdd/supply-chain-hardening/review.md +50 -0
- package/docs/sdd/supply-chain-hardening/spec.md +42 -0
- package/docs/sdd/target-marker-rehome/spec.md +33 -0
- package/docs/skill-management.md +1 -1
- package/docs/troubleshooting.md +18 -2
- package/package.json +15 -8
- package/src/adapters.ts +0 -438
- package/src/audit.ts +0 -21
- package/src/cli.ts +0 -928
- package/src/clients.ts +0 -368
- package/src/commands/audit.ts +0 -85
- package/src/commands/config.ts +0 -224
- package/src/commands/context.ts +0 -104
- package/src/commands/core.ts +0 -56
- package/src/commands/doctor.ts +0 -97
- package/src/commands/eval.ts +0 -88
- package/src/commands/init.ts +0 -672
- package/src/commands/install.ts +0 -155
- package/src/commands/local-vault.ts +0 -60
- package/src/commands/models.ts +0 -10
- package/src/commands/outdated.ts +0 -115
- package/src/commands/project.ts +0 -580
- package/src/commands/report.ts +0 -66
- package/src/commands/scan.ts +0 -71
- package/src/commands/shared.ts +0 -38
- package/src/commands/skill.ts +0 -33
- package/src/commands/sync.ts +0 -233
- package/src/commands/target.ts +0 -225
- package/src/commands/update.ts +0 -277
- package/src/completions.ts +0 -196
- package/src/concurrency-limiter.ts +0 -61
- package/src/config-service.ts +0 -398
- package/src/config-watcher.ts +0 -253
- package/src/config.ts +0 -583
- package/src/context.ts +0 -188
- package/src/db-audit.ts +0 -286
- package/src/db-index.ts +0 -238
- package/src/db.ts +0 -3
- package/src/deployment.ts +0 -39
- package/src/doctor.ts +0 -205
- package/src/eval.ts +0 -302
- package/src/global-flags.ts +0 -46
- package/src/init-agents.ts +0 -329
- package/src/init-instructions.ts +0 -192
- package/src/init.ts +0 -389
- package/src/install.ts +0 -232
- package/src/lifecycle.ts +0 -51
- package/src/logger.ts +0 -26
- package/src/manifest.ts +0 -352
- package/src/mcp-registration.ts +0 -89
- package/src/metrics.ts +0 -121
- package/src/models.ts +0 -20
- package/src/output.ts +0 -214
- package/src/project-setup.ts +0 -36
- package/src/prompts.ts +0 -124
- package/src/provenance.ts +0 -99
- package/src/rate-limiter.ts +0 -137
- package/src/readiness.ts +0 -30
- package/src/redact.ts +0 -52
- package/src/router-core.ts +0 -666
- package/src/rrf.ts +0 -31
- package/src/scan.ts +0 -300
- package/src/server.ts +0 -930
- package/src/setup.ts +0 -145
- package/src/snapshot.ts +0 -135
- package/src/stats.ts +0 -340
- package/src/sync.ts +0 -482
- package/src/toml-writer.ts +0 -51
- package/src/types.ts +0 -212
- package/src/vault.ts +0 -207
package/src/clients.ts
DELETED
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import type { Clients, Config, RemoteRerankerConfig } from "./types";
|
|
2
|
-
import { expandHome } from "./config";
|
|
3
|
-
import { assertHostAllowed } from "./install";
|
|
4
|
-
import type { pipeline as createPipeline } from "@huggingface/transformers";
|
|
5
|
-
|
|
6
|
-
export type RemoteErrorKind = "configuration" | "availability" | "protocol";
|
|
7
|
-
|
|
8
|
-
export class RemoteInferenceError extends Error {
|
|
9
|
-
constructor(
|
|
10
|
-
public readonly kind: RemoteErrorKind,
|
|
11
|
-
message: string,
|
|
12
|
-
) {
|
|
13
|
-
super(message);
|
|
14
|
-
this.name = "RemoteInferenceError";
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// Lazy-loaded model instances for in-process ONNX inference
|
|
19
|
-
type FeatureExtractor = Awaited<ReturnType<typeof createPipeline<"feature-extraction">>>;
|
|
20
|
-
|
|
21
|
-
let localEmbedder: FeatureExtractor | null = null;
|
|
22
|
-
|
|
23
|
-
function authorizationHeaders(
|
|
24
|
-
apiKeyEnv: string | undefined,
|
|
25
|
-
configKey: string,
|
|
26
|
-
): Record<string, string> {
|
|
27
|
-
if (apiKeyEnv === undefined) return {};
|
|
28
|
-
const apiKey = process.env[apiKeyEnv];
|
|
29
|
-
if (apiKey === undefined || apiKey === "") {
|
|
30
|
-
throw new RemoteInferenceError(
|
|
31
|
-
"configuration",
|
|
32
|
-
`${configKey} names environment variable "${apiKeyEnv}", but it is unset or empty.`,
|
|
33
|
-
);
|
|
34
|
-
}
|
|
35
|
-
return { authorization: `Bearer ${apiKey}` };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function assertInferenceHostAllowed(url: string, allowedHosts: string[] | undefined): void {
|
|
39
|
-
try {
|
|
40
|
-
assertHostAllowed(url, allowedHosts);
|
|
41
|
-
} catch (error) {
|
|
42
|
-
throw new RemoteInferenceError(
|
|
43
|
-
"configuration",
|
|
44
|
-
error instanceof Error ? error.message : String(error),
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function httpFailure(surface: string, status: number): RemoteInferenceError {
|
|
50
|
-
const kind: RemoteErrorKind =
|
|
51
|
-
status === 401 || status === 403
|
|
52
|
-
? "configuration"
|
|
53
|
-
: status === 408 || status === 429 || status >= 500
|
|
54
|
-
? "availability"
|
|
55
|
-
: "protocol";
|
|
56
|
-
return new RemoteInferenceError(kind, `${surface} returned HTTP ${status}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function finiteFloat32(
|
|
60
|
-
values: unknown[],
|
|
61
|
-
error: () => Error,
|
|
62
|
-
): Float32Array {
|
|
63
|
-
const result = new Float32Array(values.length);
|
|
64
|
-
for (let index = 0; index < values.length; index++) {
|
|
65
|
-
const value = values[index];
|
|
66
|
-
if (typeof value !== "number" || !Number.isFinite(value)) throw error();
|
|
67
|
-
result[index] = value;
|
|
68
|
-
if (!Number.isFinite(result[index]!)) throw error();
|
|
69
|
-
}
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function parseEmbeddingVectors(
|
|
74
|
-
value: unknown,
|
|
75
|
-
inputCount: number,
|
|
76
|
-
dimension: number,
|
|
77
|
-
): Float32Array[] {
|
|
78
|
-
const response = value as { data?: unknown };
|
|
79
|
-
if (
|
|
80
|
-
typeof response !== "object" ||
|
|
81
|
-
response === null ||
|
|
82
|
-
!Array.isArray(response.data) ||
|
|
83
|
-
response.data.length !== inputCount
|
|
84
|
-
) {
|
|
85
|
-
throw new RemoteInferenceError(
|
|
86
|
-
"protocol",
|
|
87
|
-
"embedding endpoint returned an incomplete data array",
|
|
88
|
-
);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const vectors = new Array<Float32Array>(inputCount);
|
|
92
|
-
const seen = new Set<number>();
|
|
93
|
-
for (const value of response.data) {
|
|
94
|
-
const entry = value as { index?: unknown; embedding?: unknown };
|
|
95
|
-
if (
|
|
96
|
-
typeof entry !== "object" ||
|
|
97
|
-
entry === null ||
|
|
98
|
-
!Number.isInteger(entry.index) ||
|
|
99
|
-
(entry.index as number) < 0 ||
|
|
100
|
-
(entry.index as number) >= inputCount ||
|
|
101
|
-
seen.has(entry.index as number) ||
|
|
102
|
-
!Array.isArray(entry.embedding) ||
|
|
103
|
-
entry.embedding.length !== dimension
|
|
104
|
-
) {
|
|
105
|
-
throw new RemoteInferenceError(
|
|
106
|
-
"protocol",
|
|
107
|
-
"embedding endpoint returned invalid indexed vectors",
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
seen.add(entry.index as number);
|
|
111
|
-
vectors[entry.index as number] = finiteFloat32(
|
|
112
|
-
entry.embedding,
|
|
113
|
-
() => new RemoteInferenceError("protocol", "embedding endpoint returned invalid vector values"),
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
return vectors;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function parseLocalEmbeddingVectors(
|
|
120
|
-
value: unknown,
|
|
121
|
-
inputCount: number,
|
|
122
|
-
dimension: number,
|
|
123
|
-
): Float32Array[] {
|
|
124
|
-
if (!Array.isArray(value) || value.length !== inputCount) {
|
|
125
|
-
throw new Error("Embedding model returned an unexpected batch size.");
|
|
126
|
-
}
|
|
127
|
-
return value.map((row) => {
|
|
128
|
-
if (!Array.isArray(row) || row.length !== dimension) {
|
|
129
|
-
throw new Error("Embedding model returned unexpected vector dimensions.");
|
|
130
|
-
}
|
|
131
|
-
return finiteFloat32(
|
|
132
|
-
row,
|
|
133
|
-
() => new Error("Embedding model returned invalid vector values."),
|
|
134
|
-
);
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function rerankerRequestBody(
|
|
139
|
-
reranker: RemoteRerankerConfig,
|
|
140
|
-
query: string,
|
|
141
|
-
docs: { skill_id: string; text: string }[],
|
|
142
|
-
): Record<string, unknown> {
|
|
143
|
-
if (reranker.adapter === "jina-v1") {
|
|
144
|
-
return {
|
|
145
|
-
model: reranker.model,
|
|
146
|
-
query,
|
|
147
|
-
documents: docs.map((doc) => doc.text),
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
model: reranker.model,
|
|
152
|
-
query,
|
|
153
|
-
documents: docs.map((doc) => ({
|
|
154
|
-
text: doc.text,
|
|
155
|
-
id: doc.skill_id,
|
|
156
|
-
meta: {},
|
|
157
|
-
})),
|
|
158
|
-
top_n: docs.length,
|
|
159
|
-
return_documents: false,
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function parseRerankerScores(
|
|
164
|
-
adapter: RemoteRerankerConfig["adapter"],
|
|
165
|
-
value: unknown,
|
|
166
|
-
documentCount: number,
|
|
167
|
-
): number[] {
|
|
168
|
-
const response = value as { results?: unknown };
|
|
169
|
-
if (
|
|
170
|
-
typeof response !== "object" ||
|
|
171
|
-
response === null ||
|
|
172
|
-
!Array.isArray(response.results) ||
|
|
173
|
-
response.results.length !== documentCount
|
|
174
|
-
) {
|
|
175
|
-
throw new RemoteInferenceError(
|
|
176
|
-
"protocol",
|
|
177
|
-
`reranker adapter "${adapter}" returned an incomplete results array`,
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const scores = new Array<number>(documentCount);
|
|
182
|
-
const seen = new Set<number>();
|
|
183
|
-
for (const value of response.results) {
|
|
184
|
-
const result = value as { index?: unknown; relevance_score?: unknown };
|
|
185
|
-
if (
|
|
186
|
-
typeof result !== "object" ||
|
|
187
|
-
result === null ||
|
|
188
|
-
!Number.isInteger(result.index) ||
|
|
189
|
-
(result.index as number) < 0 ||
|
|
190
|
-
(result.index as number) >= documentCount ||
|
|
191
|
-
seen.has(result.index as number) ||
|
|
192
|
-
typeof result.relevance_score !== "number" ||
|
|
193
|
-
!Number.isFinite(result.relevance_score)
|
|
194
|
-
) {
|
|
195
|
-
throw new RemoteInferenceError(
|
|
196
|
-
"protocol",
|
|
197
|
-
`reranker adapter "${adapter}" returned invalid indexed scores`,
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
seen.add(result.index as number);
|
|
201
|
-
scores[result.index as number] = result.relevance_score;
|
|
202
|
-
}
|
|
203
|
-
return scores;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function fetchRerankerScores(
|
|
207
|
-
reranker: RemoteRerankerConfig,
|
|
208
|
-
timeoutMs: number,
|
|
209
|
-
query: string,
|
|
210
|
-
docs: { skill_id: string; text: string }[],
|
|
211
|
-
allowedHosts: string[] | undefined,
|
|
212
|
-
): Promise<number[]> {
|
|
213
|
-
if (docs.length === 0) return [];
|
|
214
|
-
|
|
215
|
-
assertInferenceHostAllowed(reranker.endpoint, allowedHosts);
|
|
216
|
-
|
|
217
|
-
let response: Response;
|
|
218
|
-
try {
|
|
219
|
-
response = await fetch(reranker.endpoint, {
|
|
220
|
-
method: "POST",
|
|
221
|
-
headers: {
|
|
222
|
-
"content-type": "application/json",
|
|
223
|
-
...authorizationHeaders(reranker.api_key_env, "inference.reranker.api_key_env"),
|
|
224
|
-
},
|
|
225
|
-
body: JSON.stringify(rerankerRequestBody(reranker, query, docs)),
|
|
226
|
-
signal: AbortSignal.timeout(timeoutMs),
|
|
227
|
-
});
|
|
228
|
-
} catch (error) {
|
|
229
|
-
if (error instanceof RemoteInferenceError) throw error;
|
|
230
|
-
const isTimeout =
|
|
231
|
-
(error as { name?: string })?.name === "TimeoutError" ||
|
|
232
|
-
(error as { name?: string })?.name === "AbortError" ||
|
|
233
|
-
String(error).toLowerCase().includes("timeout") ||
|
|
234
|
-
String(error).toLowerCase().includes("aborted");
|
|
235
|
-
if (isTimeout) {
|
|
236
|
-
throw new RemoteInferenceError(
|
|
237
|
-
"availability",
|
|
238
|
-
`reranker adapter "${reranker.adapter}" request timed out`,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
throw new RemoteInferenceError(
|
|
242
|
-
"availability",
|
|
243
|
-
`reranker adapter "${reranker.adapter}" request failed`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
if (!response.ok) {
|
|
247
|
-
throw httpFailure(`reranker adapter "${reranker.adapter}"`, response.status);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
let parsed: unknown;
|
|
251
|
-
try {
|
|
252
|
-
parsed = await response.json();
|
|
253
|
-
} catch {
|
|
254
|
-
throw new RemoteInferenceError(
|
|
255
|
-
"protocol",
|
|
256
|
-
`reranker adapter "${reranker.adapter}" returned malformed JSON`,
|
|
257
|
-
);
|
|
258
|
-
}
|
|
259
|
-
return parseRerankerScores(reranker.adapter, parsed, docs.length);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
function localInference(config: Config) {
|
|
263
|
-
if (config.inference.mode !== "local") throw new Error("Local inference is not configured.");
|
|
264
|
-
return config.inference;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
async function setupTransformers(cacheDir: string) {
|
|
268
|
-
process.env.HF_HUB_CACHE = cacheDir;
|
|
269
|
-
process.env.HF_HOME = cacheDir;
|
|
270
|
-
|
|
271
|
-
const { env, pipeline } = await import("@huggingface/transformers");
|
|
272
|
-
env.cacheDir = cacheDir;
|
|
273
|
-
return pipeline;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
async function getLocalEmbedder(config: Config): Promise<FeatureExtractor> {
|
|
277
|
-
if (localEmbedder) return localEmbedder;
|
|
278
|
-
|
|
279
|
-
const inference = localInference(config);
|
|
280
|
-
const cacheDir = expandHome(inference.models_dir);
|
|
281
|
-
const pipeline = await setupTransformers(cacheDir);
|
|
282
|
-
|
|
283
|
-
localEmbedder = await pipeline("feature-extraction", inference.embedding.model, {
|
|
284
|
-
device: inference.embedding.device || "cpu",
|
|
285
|
-
dtype: inference.embedding.dtype || "q8",
|
|
286
|
-
});
|
|
287
|
-
return localEmbedder;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
/**
|
|
291
|
-
* Real HTTP clients or in-process local ONNX inference clients.
|
|
292
|
-
* Every remote HTTP call is bounded by inference.timeout_ms; timeouts and transport
|
|
293
|
-
* errors reject so resolveSkill can fall back to the strongest available retrieval lane.
|
|
294
|
-
* Local ONNX calls run in-process using @huggingface/transformers.
|
|
295
|
-
*/
|
|
296
|
-
export function createClients(config: Config): Clients {
|
|
297
|
-
const clients: Clients = {
|
|
298
|
-
async embed(texts: string[]): Promise<Float32Array[]> {
|
|
299
|
-
if (config.inference.mode === "local") {
|
|
300
|
-
const pipe = await getLocalEmbedder(config);
|
|
301
|
-
const output = await pipe(texts, { pooling: "mean", normalize: true });
|
|
302
|
-
const dim = output.dims[1];
|
|
303
|
-
if (
|
|
304
|
-
dim === undefined ||
|
|
305
|
-
output.dims.length !== 2 ||
|
|
306
|
-
output.dims[0] !== texts.length ||
|
|
307
|
-
dim !== config.inference.embedding.dimension
|
|
308
|
-
) {
|
|
309
|
-
throw new Error(`Embedding model returned unexpected dimensions: ${output.dims.join("x")}`);
|
|
310
|
-
}
|
|
311
|
-
const rows = Array.from({ length: texts.length }, (_, index) =>
|
|
312
|
-
output.slice(index, null).tolist(),
|
|
313
|
-
);
|
|
314
|
-
return parseLocalEmbeddingVectors(rows, texts.length, dim);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const embedding = config.inference.embedding;
|
|
318
|
-
assertInferenceHostAllowed(embedding.endpoint, config.egress?.allowed_hosts);
|
|
319
|
-
let response: Response;
|
|
320
|
-
try {
|
|
321
|
-
response = await fetch(embedding.endpoint, {
|
|
322
|
-
method: "POST",
|
|
323
|
-
headers: {
|
|
324
|
-
"content-type": "application/json",
|
|
325
|
-
...authorizationHeaders(embedding.api_key_env, "inference.embedding.api_key_env"),
|
|
326
|
-
},
|
|
327
|
-
body: JSON.stringify({ model: embedding.model, input: texts }),
|
|
328
|
-
signal: AbortSignal.timeout(config.inference.timeout_ms),
|
|
329
|
-
});
|
|
330
|
-
} catch (error) {
|
|
331
|
-
if (error instanceof RemoteInferenceError) throw error;
|
|
332
|
-
const isTimeout =
|
|
333
|
-
(error as { name?: string })?.name === "TimeoutError" ||
|
|
334
|
-
(error as { name?: string })?.name === "AbortError" ||
|
|
335
|
-
String(error).toLowerCase().includes("timeout") ||
|
|
336
|
-
String(error).toLowerCase().includes("aborted");
|
|
337
|
-
if (isTimeout) {
|
|
338
|
-
throw new RemoteInferenceError("availability", "embedding endpoint request timed out");
|
|
339
|
-
}
|
|
340
|
-
throw new RemoteInferenceError("availability", "embedding endpoint request failed");
|
|
341
|
-
}
|
|
342
|
-
if (!response.ok) throw httpFailure("embedding endpoint", response.status);
|
|
343
|
-
|
|
344
|
-
let parsed: unknown;
|
|
345
|
-
try {
|
|
346
|
-
parsed = await response.json();
|
|
347
|
-
} catch {
|
|
348
|
-
throw new RemoteInferenceError("protocol", "embedding endpoint returned malformed JSON");
|
|
349
|
-
}
|
|
350
|
-
return parseEmbeddingVectors(parsed, texts.length, embedding.dimension);
|
|
351
|
-
},
|
|
352
|
-
};
|
|
353
|
-
if (config.inference.mode === "remote" && config.inference.reranker) {
|
|
354
|
-
const inference = config.inference;
|
|
355
|
-
clients.rerank = async (query, docs) => {
|
|
356
|
-
const reranker = inference.reranker;
|
|
357
|
-
if (!reranker) throw new Error("Reranker is not configured.");
|
|
358
|
-
return fetchRerankerScores(
|
|
359
|
-
reranker,
|
|
360
|
-
inference.timeout_ms,
|
|
361
|
-
query,
|
|
362
|
-
docs,
|
|
363
|
-
config.egress?.allowed_hosts,
|
|
364
|
-
);
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
return clients;
|
|
368
|
-
}
|
package/src/commands/audit.ts
DELETED
|
@@ -1,85 +0,0 @@
|
|
|
1
|
-
import { emitSuccess } from "../output";
|
|
2
|
-
import { confirmIfNeeded } from "./shared";
|
|
3
|
-
import type { ContextAdapter } from "../adapters";
|
|
4
|
-
import type { ResolvedContext } from "../context";
|
|
5
|
-
import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
|
|
6
|
-
|
|
7
|
-
export async function runAudit(
|
|
8
|
-
subCommand: string,
|
|
9
|
-
args: string[],
|
|
10
|
-
options: { isJson: boolean; dryRun: boolean; context: ResolvedContext; adapter: ContextAdapter },
|
|
11
|
-
): Promise<void> {
|
|
12
|
-
if (subCommand !== "prune") {
|
|
13
|
-
throw new Error("usage: skillmux audit prune [--older-than <window>] [--dry-run] [--yes] [--json]");
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
let olderThan: string | undefined;
|
|
17
|
-
let dryRun = options.dryRun;
|
|
18
|
-
let yes = false;
|
|
19
|
-
for (let i = 0; i < args.length; i++) {
|
|
20
|
-
const arg = args[i];
|
|
21
|
-
if (arg === "--older-than") olderThan = args[++i];
|
|
22
|
-
else if (arg === "--dry-run") dryRun = true;
|
|
23
|
-
else if (arg === "--yes") yes = true;
|
|
24
|
-
else if (isGlobalFlag(arg, "--json", "--allow-insecure", "--verbose")) {
|
|
25
|
-
// handled globally by main()'s flags; recognized here so it isn't rejected
|
|
26
|
-
} else if (isGlobalFlagWithValue(arg)) {
|
|
27
|
-
i++; // skip flag value
|
|
28
|
-
} else if (arg?.startsWith("--")) {
|
|
29
|
-
throw new Error(`unknown audit prune option: ${arg}`);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (dryRun) {
|
|
34
|
-
const counts = await options.adapter.auditCount(olderThan);
|
|
35
|
-
emitSuccess(
|
|
36
|
-
{ isJson: options.isJson },
|
|
37
|
-
counts,
|
|
38
|
-
() =>
|
|
39
|
-
console.log(
|
|
40
|
-
counts.cutoff === null
|
|
41
|
-
? "prune: audit.retention_days is 0 (pruning disabled); nothing to do"
|
|
42
|
-
: `prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} admin_audit=${counts.admin_audit_deleted} (dry-run)`,
|
|
43
|
-
),
|
|
44
|
-
);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Pre-fetch count / cutoff info for confirmation prompt if local or remote
|
|
49
|
-
const countResult = await options.adapter.auditCount(olderThan);
|
|
50
|
-
if (countResult.cutoff === null) {
|
|
51
|
-
emitSuccess(
|
|
52
|
-
{ isJson: options.isJson },
|
|
53
|
-
{ audit_deleted: 0, fetch_deleted: 0, admin_audit_deleted: 0, dry_run: false, cutoff: null },
|
|
54
|
-
() => console.log("prune: audit.retention_days is 0 (pruning disabled); nothing to do"),
|
|
55
|
-
);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
!(await confirmIfNeeded({
|
|
61
|
-
confirmed: yes,
|
|
62
|
-
isJson: options.isJson,
|
|
63
|
-
prompt: `prune audit rows older than ${countResult.cutoff}?`,
|
|
64
|
-
nonInteractiveError: "skillmux audit prune requires --yes when run non-interactively",
|
|
65
|
-
}))
|
|
66
|
-
)
|
|
67
|
-
return;
|
|
68
|
-
|
|
69
|
-
// Reuse the exact cutoff already shown in the confirmation prompt — recomputing
|
|
70
|
-
// it here (if olderThan was left unset) could drift from what was confirmed,
|
|
71
|
-
// e.g. if audit.retention_days changed between the two calls on a remote target.
|
|
72
|
-
const counts = await options.adapter.auditPrune({
|
|
73
|
-
older_than: countResult.cutoff,
|
|
74
|
-
dry_run: false,
|
|
75
|
-
confirm: true,
|
|
76
|
-
});
|
|
77
|
-
emitSuccess(
|
|
78
|
-
{ isJson: options.isJson },
|
|
79
|
-
counts,
|
|
80
|
-
() =>
|
|
81
|
-
console.log(
|
|
82
|
-
`prune: audit=${counts.audit_deleted} fetch=${counts.fetch_deleted} admin_audit=${counts.admin_audit_deleted}`,
|
|
83
|
-
),
|
|
84
|
-
);
|
|
85
|
-
}
|
package/src/commands/config.ts
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { expandHome, migrateLegacyPaths, resolveConfigPath } from "../config";
|
|
2
|
-
import { type ContextAdapter } from "../adapters";
|
|
3
|
-
import { type ResolvedContext } from "../context";
|
|
4
|
-
import { applyConfigInit, planConfigInit, type ConfigInitPlan } from "../setup";
|
|
5
|
-
import { emitSuccess, isInteractive, renderContextBanner, unknownSubcommandError } from "../output";
|
|
6
|
-
import { confirmAction } from "./shared";
|
|
7
|
-
import { isGlobalFlag } from "../global-flags";
|
|
8
|
-
function emitConfigInitOutcome(
|
|
9
|
-
ctx: { isJson: boolean },
|
|
10
|
-
opts: {
|
|
11
|
-
phase: "plan" | "result";
|
|
12
|
-
dryRun: boolean;
|
|
13
|
-
applied: boolean;
|
|
14
|
-
plan: ConfigInitPlan;
|
|
15
|
-
action: "create" | "preserve";
|
|
16
|
-
text: string;
|
|
17
|
-
},
|
|
18
|
-
): void {
|
|
19
|
-
if (ctx.isJson) {
|
|
20
|
-
console.log(
|
|
21
|
-
JSON.stringify({
|
|
22
|
-
schema_version: 1,
|
|
23
|
-
ok: true,
|
|
24
|
-
command: "config init",
|
|
25
|
-
phase: opts.phase,
|
|
26
|
-
dry_run: opts.dryRun,
|
|
27
|
-
applied: opts.applied,
|
|
28
|
-
plan: {
|
|
29
|
-
config_path: opts.plan.configPath,
|
|
30
|
-
vault_path: opts.plan.vaultPath,
|
|
31
|
-
action: opts.action,
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
);
|
|
35
|
-
return;
|
|
36
|
-
}
|
|
37
|
-
console.log(opts.text);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export async function handleConfigCommand(
|
|
41
|
-
adapter: ContextAdapter,
|
|
42
|
-
sub: string,
|
|
43
|
-
args: string[],
|
|
44
|
-
ctx: { context: ResolvedContext; isJson: boolean; dryRun: boolean },
|
|
45
|
-
) {
|
|
46
|
-
if (sub === "init") {
|
|
47
|
-
let vaultPath: string | undefined;
|
|
48
|
-
let yes = false;
|
|
49
|
-
for (let i = 0; i < args.length; i++) {
|
|
50
|
-
const option = args[i];
|
|
51
|
-
if (option === "--vault") {
|
|
52
|
-
vaultPath = args[++i];
|
|
53
|
-
if (!vaultPath)
|
|
54
|
-
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
55
|
-
} else if (option === "--yes") {
|
|
56
|
-
yes = true;
|
|
57
|
-
} else if (isGlobalFlag(option, "--dry-run", "--json")) {
|
|
58
|
-
continue;
|
|
59
|
-
} else {
|
|
60
|
-
throw new Error(`unknown config init option: ${option}`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (!vaultPath) {
|
|
64
|
-
if (isInteractive() && !ctx.isJson) {
|
|
65
|
-
vaultPath = "~/skills";
|
|
66
|
-
} else {
|
|
67
|
-
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
migrateLegacyPaths();
|
|
72
|
-
const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
|
|
73
|
-
if (plan.action === "preserve") {
|
|
74
|
-
emitConfigInitOutcome(ctx, {
|
|
75
|
-
phase: "result",
|
|
76
|
-
dryRun: ctx.dryRun,
|
|
77
|
-
applied: false,
|
|
78
|
-
plan,
|
|
79
|
-
action: "preserve",
|
|
80
|
-
text: `preserved existing config: ${plan.configPath}`,
|
|
81
|
-
});
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (ctx.dryRun) {
|
|
85
|
-
emitConfigInitOutcome(ctx, {
|
|
86
|
-
phase: "plan",
|
|
87
|
-
dryRun: true,
|
|
88
|
-
applied: false,
|
|
89
|
-
plan,
|
|
90
|
-
action: "create",
|
|
91
|
-
text: `config create: ${plan.configPath} (dry-run)`,
|
|
92
|
-
});
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (!yes) {
|
|
96
|
-
if (!ctx.isJson && isInteractive()) {
|
|
97
|
-
if (
|
|
98
|
-
!(await confirmAction(
|
|
99
|
-
`create ${plan.configPath} with vault_path ${plan.vaultPath}?`,
|
|
100
|
-
))
|
|
101
|
-
) {
|
|
102
|
-
console.log("config init cancelled; nothing written");
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
} else {
|
|
106
|
-
throw new Error(
|
|
107
|
-
"config initialization requires --yes in noninteractive mode",
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const result = applyConfigInit(plan);
|
|
113
|
-
emitConfigInitOutcome(ctx, {
|
|
114
|
-
phase: "result",
|
|
115
|
-
dryRun: false,
|
|
116
|
-
applied: result === "created",
|
|
117
|
-
plan,
|
|
118
|
-
action: plan.action,
|
|
119
|
-
text:
|
|
120
|
-
result === "created"
|
|
121
|
-
? `created ${plan.configPath}`
|
|
122
|
-
: `preserved existing config: ${plan.configPath}`,
|
|
123
|
-
});
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (sub === "show") {
|
|
128
|
-
const withSources = args.includes("--sources");
|
|
129
|
-
const data = await adapter.getConfigShow();
|
|
130
|
-
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, data, () => {
|
|
131
|
-
renderContextBanner(ctx.context);
|
|
132
|
-
if (withSources) {
|
|
133
|
-
const policy =
|
|
134
|
-
data.effective.config?.environment_overrides === false
|
|
135
|
-
? "strict (TOML authoritative)"
|
|
136
|
-
: "permissive (environment overrides enabled)";
|
|
137
|
-
console.log(`Policy: ${policy}`);
|
|
138
|
-
console.log("\nSources:");
|
|
139
|
-
for (const [k, src] of Object.entries(data.sources)) {
|
|
140
|
-
console.log(` ${k}: ${src}`);
|
|
141
|
-
}
|
|
142
|
-
console.log("\nEffective Configuration:");
|
|
143
|
-
}
|
|
144
|
-
console.log(JSON.stringify(data.effective, null, 2));
|
|
145
|
-
});
|
|
146
|
-
return;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (sub === "get") {
|
|
150
|
-
const key = args[0];
|
|
151
|
-
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
152
|
-
const val = await adapter.getConfigGet(key);
|
|
153
|
-
emitSuccess(
|
|
154
|
-
{ isJson: ctx.isJson, context: ctx.context },
|
|
155
|
-
{ key, value: val },
|
|
156
|
-
() => {
|
|
157
|
-
console.log(
|
|
158
|
-
typeof val === "object" ? JSON.stringify(val) : String(val),
|
|
159
|
-
);
|
|
160
|
-
},
|
|
161
|
-
);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (sub === "validate") {
|
|
166
|
-
const res = await adapter.configValidate();
|
|
167
|
-
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
168
|
-
console.log(res.valid ? "configuration is valid" : "configuration is invalid");
|
|
169
|
-
});
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
if (sub === "diff") {
|
|
174
|
-
const res = await adapter.configDiff();
|
|
175
|
-
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
176
|
-
renderContextBanner(ctx.context);
|
|
177
|
-
console.log(JSON.stringify(res.diff, null, 2));
|
|
178
|
-
});
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (sub === "set") {
|
|
183
|
-
const key = args[0];
|
|
184
|
-
const value = args[1];
|
|
185
|
-
if (!key || value === undefined) {
|
|
186
|
-
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
187
|
-
}
|
|
188
|
-
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
189
|
-
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
190
|
-
renderContextBanner(ctx.context);
|
|
191
|
-
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
192
|
-
console.log(
|
|
193
|
-
`${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`,
|
|
194
|
-
);
|
|
195
|
-
console.log(
|
|
196
|
-
`Persistence: ${res.persistence}, Application: ${res.application}`,
|
|
197
|
-
);
|
|
198
|
-
});
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (sub === "status") {
|
|
203
|
-
const res = await adapter.configStatus();
|
|
204
|
-
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
205
|
-
renderContextBanner(ctx.context);
|
|
206
|
-
console.log(`runtime: ${res.runtime}`);
|
|
207
|
-
console.log(`deployment runtime: ${res.deployment_runtime}`);
|
|
208
|
-
console.log(`image variant: ${res.image_variant ?? "none"}`);
|
|
209
|
-
console.log(`active revision: ${res.active_revision}`);
|
|
210
|
-
console.log(`readiness: ${res.readiness.status}`);
|
|
211
|
-
});
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
throw unknownSubcommandError("config", sub, [
|
|
216
|
-
"init",
|
|
217
|
-
"show",
|
|
218
|
-
"get",
|
|
219
|
-
"set",
|
|
220
|
-
"validate",
|
|
221
|
-
"diff",
|
|
222
|
-
"status",
|
|
223
|
-
]);
|
|
224
|
-
}
|