@klhapp/skillmux 1.0.0 → 1.1.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 +52 -0
- package/README.md +77 -28
- package/config.remote.example.toml +3 -3
- package/docs/configuration.md +39 -4
- package/docs/schema.json +13 -5
- package/package.json +1 -1
- package/src/adapters.ts +98 -18
- package/src/calibrate.ts +468 -178
- package/src/cli.ts +75 -815
- package/src/clients.ts +264 -48
- package/src/commands/config.ts +202 -0
- package/src/commands/core.ts +52 -0
- package/src/commands/project.ts +412 -0
- package/src/commands/shared.ts +45 -0
- package/src/commands/target.ts +110 -0
- package/src/config-mutation.ts +65 -0
- package/src/config-service.ts +25 -8
- package/src/config-watcher.ts +7 -0
- package/src/config.ts +106 -25
- package/src/decision.ts +4 -1
- package/src/doctor.ts +16 -5
- package/src/eval.ts +2 -1
- package/src/router-core.ts +73 -42
- package/src/server.ts +8 -5
- package/src/types.ts +3 -3
package/src/clients.ts
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import type { Clients, Config } from "./types";
|
|
1
|
+
import type { Clients, Config, RemoteRerankerConfig } from "./types";
|
|
2
2
|
import { expandHome } from "./config";
|
|
3
3
|
import type { pipeline as createPipeline } from "@huggingface/transformers";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
data: { index: number; embedding: number[] }[];
|
|
7
|
-
}
|
|
5
|
+
export type RemoteErrorKind = "configuration" | "availability" | "protocol";
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export class RemoteInferenceError extends Error {
|
|
8
|
+
constructor(
|
|
9
|
+
public readonly kind: RemoteErrorKind,
|
|
10
|
+
message: string,
|
|
11
|
+
) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "RemoteInferenceError";
|
|
14
|
+
}
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
// Lazy-loaded model instances for in-process ONNX inference
|
|
@@ -15,6 +19,220 @@ type FeatureExtractor = Awaited<ReturnType<typeof createPipeline<"feature-extrac
|
|
|
15
19
|
|
|
16
20
|
let localEmbedder: FeatureExtractor | null = null;
|
|
17
21
|
|
|
22
|
+
function authorizationHeaders(
|
|
23
|
+
apiKeyEnv: string | undefined,
|
|
24
|
+
configKey: string,
|
|
25
|
+
): Record<string, string> {
|
|
26
|
+
if (apiKeyEnv === undefined) return {};
|
|
27
|
+
const apiKey = process.env[apiKeyEnv];
|
|
28
|
+
if (apiKey === undefined || apiKey === "") {
|
|
29
|
+
throw new RemoteInferenceError(
|
|
30
|
+
"configuration",
|
|
31
|
+
`${configKey} names environment variable "${apiKeyEnv}", but it is unset or empty.`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return { authorization: `Bearer ${apiKey}` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function httpFailure(surface: string, status: number): RemoteInferenceError {
|
|
38
|
+
const kind: RemoteErrorKind =
|
|
39
|
+
status === 401 || status === 403
|
|
40
|
+
? "configuration"
|
|
41
|
+
: status === 408 || status === 429 || status >= 500
|
|
42
|
+
? "availability"
|
|
43
|
+
: "protocol";
|
|
44
|
+
return new RemoteInferenceError(kind, `${surface} returned HTTP ${status}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function finiteFloat32(
|
|
48
|
+
values: unknown[],
|
|
49
|
+
error: () => Error,
|
|
50
|
+
): Float32Array {
|
|
51
|
+
const result = new Float32Array(values.length);
|
|
52
|
+
for (let index = 0; index < values.length; index++) {
|
|
53
|
+
const value = values[index];
|
|
54
|
+
if (typeof value !== "number" || !Number.isFinite(value)) throw error();
|
|
55
|
+
result[index] = value;
|
|
56
|
+
if (!Number.isFinite(result[index]!)) throw error();
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseEmbeddingVectors(
|
|
62
|
+
value: unknown,
|
|
63
|
+
inputCount: number,
|
|
64
|
+
dimension: number,
|
|
65
|
+
): Float32Array[] {
|
|
66
|
+
const response = value as { data?: unknown };
|
|
67
|
+
if (
|
|
68
|
+
typeof response !== "object" ||
|
|
69
|
+
response === null ||
|
|
70
|
+
!Array.isArray(response.data) ||
|
|
71
|
+
response.data.length !== inputCount
|
|
72
|
+
) {
|
|
73
|
+
throw new RemoteInferenceError(
|
|
74
|
+
"protocol",
|
|
75
|
+
"embedding endpoint returned an incomplete data array",
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const vectors = new Array<Float32Array>(inputCount);
|
|
80
|
+
const seen = new Set<number>();
|
|
81
|
+
for (const value of response.data) {
|
|
82
|
+
const entry = value as { index?: unknown; embedding?: unknown };
|
|
83
|
+
if (
|
|
84
|
+
typeof entry !== "object" ||
|
|
85
|
+
entry === null ||
|
|
86
|
+
!Number.isInteger(entry.index) ||
|
|
87
|
+
(entry.index as number) < 0 ||
|
|
88
|
+
(entry.index as number) >= inputCount ||
|
|
89
|
+
seen.has(entry.index as number) ||
|
|
90
|
+
!Array.isArray(entry.embedding) ||
|
|
91
|
+
entry.embedding.length !== dimension
|
|
92
|
+
) {
|
|
93
|
+
throw new RemoteInferenceError(
|
|
94
|
+
"protocol",
|
|
95
|
+
"embedding endpoint returned invalid indexed vectors",
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
seen.add(entry.index as number);
|
|
99
|
+
vectors[entry.index as number] = finiteFloat32(
|
|
100
|
+
entry.embedding,
|
|
101
|
+
() => new RemoteInferenceError("protocol", "embedding endpoint returned invalid vector values"),
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return vectors;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function parseLocalEmbeddingVectors(
|
|
108
|
+
value: unknown,
|
|
109
|
+
inputCount: number,
|
|
110
|
+
dimension: number,
|
|
111
|
+
): Float32Array[] {
|
|
112
|
+
if (!Array.isArray(value) || value.length !== inputCount) {
|
|
113
|
+
throw new Error("Embedding model returned an unexpected batch size.");
|
|
114
|
+
}
|
|
115
|
+
return value.map((row) => {
|
|
116
|
+
if (!Array.isArray(row) || row.length !== dimension) {
|
|
117
|
+
throw new Error("Embedding model returned unexpected vector dimensions.");
|
|
118
|
+
}
|
|
119
|
+
return finiteFloat32(
|
|
120
|
+
row,
|
|
121
|
+
() => new Error("Embedding model returned invalid vector values."),
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function rerankerRequestBody(
|
|
127
|
+
reranker: RemoteRerankerConfig,
|
|
128
|
+
query: string,
|
|
129
|
+
docs: { skill_id: string; text: string }[],
|
|
130
|
+
): Record<string, unknown> {
|
|
131
|
+
if (reranker.adapter === "jina-v1") {
|
|
132
|
+
return {
|
|
133
|
+
model: reranker.model,
|
|
134
|
+
query,
|
|
135
|
+
documents: docs.map((doc) => doc.text),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
model: reranker.model,
|
|
140
|
+
query,
|
|
141
|
+
documents: docs.map((doc) => ({
|
|
142
|
+
text: doc.text,
|
|
143
|
+
id: doc.skill_id,
|
|
144
|
+
meta: {},
|
|
145
|
+
})),
|
|
146
|
+
top_n: docs.length,
|
|
147
|
+
return_documents: false,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseRerankerScores(
|
|
152
|
+
adapter: RemoteRerankerConfig["adapter"],
|
|
153
|
+
value: unknown,
|
|
154
|
+
documentCount: number,
|
|
155
|
+
): number[] {
|
|
156
|
+
const response = value as { results?: unknown };
|
|
157
|
+
if (
|
|
158
|
+
typeof response !== "object" ||
|
|
159
|
+
response === null ||
|
|
160
|
+
!Array.isArray(response.results) ||
|
|
161
|
+
response.results.length !== documentCount
|
|
162
|
+
) {
|
|
163
|
+
throw new RemoteInferenceError(
|
|
164
|
+
"protocol",
|
|
165
|
+
`reranker adapter "${adapter}" returned an incomplete results array`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const scores = new Array<number>(documentCount);
|
|
170
|
+
const seen = new Set<number>();
|
|
171
|
+
for (const value of response.results) {
|
|
172
|
+
const result = value as { index?: unknown; relevance_score?: unknown };
|
|
173
|
+
if (
|
|
174
|
+
typeof result !== "object" ||
|
|
175
|
+
result === null ||
|
|
176
|
+
!Number.isInteger(result.index) ||
|
|
177
|
+
(result.index as number) < 0 ||
|
|
178
|
+
(result.index as number) >= documentCount ||
|
|
179
|
+
seen.has(result.index as number) ||
|
|
180
|
+
typeof result.relevance_score !== "number" ||
|
|
181
|
+
!Number.isFinite(result.relevance_score)
|
|
182
|
+
) {
|
|
183
|
+
throw new RemoteInferenceError(
|
|
184
|
+
"protocol",
|
|
185
|
+
`reranker adapter "${adapter}" returned invalid indexed scores`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
seen.add(result.index as number);
|
|
189
|
+
scores[result.index as number] = result.relevance_score;
|
|
190
|
+
}
|
|
191
|
+
return scores;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function fetchRerankerScores(
|
|
195
|
+
reranker: RemoteRerankerConfig,
|
|
196
|
+
timeoutMs: number,
|
|
197
|
+
query: string,
|
|
198
|
+
docs: { skill_id: string; text: string }[],
|
|
199
|
+
): Promise<number[]> {
|
|
200
|
+
if (docs.length === 0) return [];
|
|
201
|
+
|
|
202
|
+
let response: Response;
|
|
203
|
+
try {
|
|
204
|
+
response = await fetch(reranker.endpoint, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: {
|
|
207
|
+
"content-type": "application/json",
|
|
208
|
+
...authorizationHeaders(reranker.api_key_env, "inference.reranker.api_key_env"),
|
|
209
|
+
},
|
|
210
|
+
body: JSON.stringify(rerankerRequestBody(reranker, query, docs)),
|
|
211
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
212
|
+
});
|
|
213
|
+
} catch (error) {
|
|
214
|
+
if (error instanceof RemoteInferenceError) throw error;
|
|
215
|
+
throw new RemoteInferenceError(
|
|
216
|
+
"availability",
|
|
217
|
+
`reranker adapter "${reranker.adapter}" request failed`,
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
if (!response.ok) {
|
|
221
|
+
throw httpFailure(`reranker adapter "${reranker.adapter}"`, response.status);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
let parsed: unknown;
|
|
225
|
+
try {
|
|
226
|
+
parsed = await response.json();
|
|
227
|
+
} catch {
|
|
228
|
+
throw new RemoteInferenceError(
|
|
229
|
+
"protocol",
|
|
230
|
+
`reranker adapter "${reranker.adapter}" returned malformed JSON`,
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
return parseRerankerScores(reranker.adapter, parsed, docs.length);
|
|
234
|
+
}
|
|
235
|
+
|
|
18
236
|
function localInference(config: Config) {
|
|
19
237
|
if (config.inference.mode !== "local") throw new Error("Local inference is not configured.");
|
|
20
238
|
return config.inference;
|
|
@@ -56,37 +274,45 @@ export function createClients(config: Config): Clients {
|
|
|
56
274
|
const pipe = await getLocalEmbedder(config);
|
|
57
275
|
const output = await pipe(texts, { pooling: "mean", normalize: true });
|
|
58
276
|
const dim = output.dims[1];
|
|
59
|
-
if (
|
|
277
|
+
if (
|
|
278
|
+
dim === undefined ||
|
|
279
|
+
output.dims.length !== 2 ||
|
|
280
|
+
output.dims[0] !== texts.length ||
|
|
281
|
+
dim !== config.inference.embedding.dimension
|
|
282
|
+
) {
|
|
60
283
|
throw new Error(`Embedding model returned unexpected dimensions: ${output.dims.join("x")}`);
|
|
61
284
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
throw new Error("Embedding model returned non-numeric values.");
|
|
67
|
-
}
|
|
68
|
-
result.push(Float32Array.from(row));
|
|
69
|
-
}
|
|
70
|
-
return result;
|
|
285
|
+
const rows = Array.from({ length: texts.length }, (_, index) =>
|
|
286
|
+
output.slice(index, null).tolist(),
|
|
287
|
+
);
|
|
288
|
+
return parseLocalEmbeddingVectors(rows, texts.length, dim);
|
|
71
289
|
}
|
|
72
290
|
|
|
73
291
|
const embedding = config.inference.embedding;
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
292
|
+
let response: Response;
|
|
293
|
+
try {
|
|
294
|
+
response = await fetch(embedding.endpoint, {
|
|
295
|
+
method: "POST",
|
|
296
|
+
headers: {
|
|
297
|
+
"content-type": "application/json",
|
|
298
|
+
...authorizationHeaders(embedding.api_key_env, "inference.embedding.api_key_env"),
|
|
299
|
+
},
|
|
300
|
+
body: JSON.stringify({ model: embedding.model, input: texts }),
|
|
301
|
+
signal: AbortSignal.timeout(config.inference.timeout_ms),
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (error instanceof RemoteInferenceError) throw error;
|
|
305
|
+
throw new RemoteInferenceError("availability", "embedding endpoint request failed");
|
|
306
|
+
}
|
|
307
|
+
if (!response.ok) throw httpFailure("embedding endpoint", response.status);
|
|
308
|
+
|
|
309
|
+
let parsed: unknown;
|
|
310
|
+
try {
|
|
311
|
+
parsed = await response.json();
|
|
312
|
+
} catch {
|
|
313
|
+
throw new RemoteInferenceError("protocol", "embedding endpoint returned malformed JSON");
|
|
314
|
+
}
|
|
315
|
+
return parseEmbeddingVectors(parsed, texts.length, embedding.dimension);
|
|
90
316
|
},
|
|
91
317
|
};
|
|
92
318
|
if (config.inference.mode === "remote" && config.inference.reranker) {
|
|
@@ -94,22 +320,12 @@ export function createClients(config: Config): Clients {
|
|
|
94
320
|
clients.rerank = async (query, docs) => {
|
|
95
321
|
const reranker = inference.reranker;
|
|
96
322
|
if (!reranker) throw new Error("Reranker is not configured.");
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
query,
|
|
104
|
-
documents: docs.map((d) => d.text),
|
|
105
|
-
}),
|
|
106
|
-
signal: AbortSignal.timeout(inference.timeout_ms),
|
|
107
|
-
});
|
|
108
|
-
if (!response.ok) throw new Error(`rerank endpoint returned ${response.status}`);
|
|
109
|
-
const parsed = (await response.json()) as RerankResponse;
|
|
110
|
-
const scores = new Array<number>(docs.length).fill(0);
|
|
111
|
-
for (const result of parsed.results) scores[result.index] = result.relevance_score;
|
|
112
|
-
return scores;
|
|
323
|
+
return fetchRerankerScores(
|
|
324
|
+
reranker,
|
|
325
|
+
inference.timeout_ms,
|
|
326
|
+
query,
|
|
327
|
+
docs,
|
|
328
|
+
);
|
|
113
329
|
};
|
|
114
330
|
}
|
|
115
331
|
return clients;
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { expandHome, migrateLegacyPaths, resolveConfigPath } from "../config";
|
|
2
|
+
import { type TargetAdapter } from "../adapters";
|
|
3
|
+
import { type ResolvedTarget } from "../context";
|
|
4
|
+
import { applyConfigInit, planConfigInit, type ConfigInitPlan } from "../setup";
|
|
5
|
+
import { emitSuccess, isInteractive, renderTargetBanner } from "../output";
|
|
6
|
+
import { confirmAction } from "./shared";
|
|
7
|
+
function emitConfigInitOutcome(
|
|
8
|
+
ctx: { isJson: boolean },
|
|
9
|
+
opts: {
|
|
10
|
+
phase: "plan" | "result";
|
|
11
|
+
dryRun: boolean;
|
|
12
|
+
applied: boolean;
|
|
13
|
+
plan: ConfigInitPlan;
|
|
14
|
+
action: "create" | "preserve";
|
|
15
|
+
text: string;
|
|
16
|
+
},
|
|
17
|
+
): void {
|
|
18
|
+
if (ctx.isJson) {
|
|
19
|
+
console.log(
|
|
20
|
+
JSON.stringify({
|
|
21
|
+
schema_version: 1,
|
|
22
|
+
ok: true,
|
|
23
|
+
command: "config init",
|
|
24
|
+
phase: opts.phase,
|
|
25
|
+
dry_run: opts.dryRun,
|
|
26
|
+
applied: opts.applied,
|
|
27
|
+
plan: {
|
|
28
|
+
config_path: opts.plan.configPath,
|
|
29
|
+
vault_path: opts.plan.vaultPath,
|
|
30
|
+
action: opts.action,
|
|
31
|
+
},
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(opts.text);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function handleConfigCommand(
|
|
40
|
+
adapter: TargetAdapter,
|
|
41
|
+
sub: string,
|
|
42
|
+
args: string[],
|
|
43
|
+
ctx: { target: ResolvedTarget; isJson: boolean; dryRun: boolean },
|
|
44
|
+
) {
|
|
45
|
+
if (sub === "init") {
|
|
46
|
+
let vaultPath: string | undefined;
|
|
47
|
+
let yes = false;
|
|
48
|
+
for (let i = 0; i < args.length; i++) {
|
|
49
|
+
const option = args[i];
|
|
50
|
+
if (option === "--vault") {
|
|
51
|
+
vaultPath = args[++i];
|
|
52
|
+
if (!vaultPath)
|
|
53
|
+
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
54
|
+
} else if (option === "--yes") {
|
|
55
|
+
yes = true;
|
|
56
|
+
} else if (option === "--dry-run" || option === "--json") {
|
|
57
|
+
continue;
|
|
58
|
+
} else {
|
|
59
|
+
throw new Error(`unknown config init option: ${option}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (!vaultPath) {
|
|
63
|
+
if (isInteractive() && !ctx.isJson) {
|
|
64
|
+
vaultPath = "~/skills";
|
|
65
|
+
} else {
|
|
66
|
+
throw new Error("usage: skillmux config init --vault <path> --yes");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
migrateLegacyPaths();
|
|
71
|
+
const plan = planConfigInit(resolveConfigPath(), expandHome(vaultPath));
|
|
72
|
+
if (plan.action === "preserve") {
|
|
73
|
+
emitConfigInitOutcome(ctx, {
|
|
74
|
+
phase: "result",
|
|
75
|
+
dryRun: ctx.dryRun,
|
|
76
|
+
applied: false,
|
|
77
|
+
plan,
|
|
78
|
+
action: "preserve",
|
|
79
|
+
text: `preserved existing config: ${plan.configPath}`,
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (ctx.dryRun) {
|
|
84
|
+
emitConfigInitOutcome(ctx, {
|
|
85
|
+
phase: "plan",
|
|
86
|
+
dryRun: true,
|
|
87
|
+
applied: false,
|
|
88
|
+
plan,
|
|
89
|
+
action: "create",
|
|
90
|
+
text: `config create: ${plan.configPath} (dry-run)`,
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!yes) {
|
|
95
|
+
if (!ctx.isJson && isInteractive()) {
|
|
96
|
+
if (
|
|
97
|
+
!(await confirmAction(
|
|
98
|
+
`Create ${plan.configPath} with vault_path ${plan.vaultPath}?`,
|
|
99
|
+
))
|
|
100
|
+
) {
|
|
101
|
+
console.log("config init cancelled; nothing written");
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
} else {
|
|
105
|
+
throw new Error(
|
|
106
|
+
"config initialization requires --yes in noninteractive mode",
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const result = applyConfigInit(plan);
|
|
112
|
+
emitConfigInitOutcome(ctx, {
|
|
113
|
+
phase: "result",
|
|
114
|
+
dryRun: false,
|
|
115
|
+
applied: result === "created",
|
|
116
|
+
plan,
|
|
117
|
+
action: plan.action,
|
|
118
|
+
text:
|
|
119
|
+
result === "created"
|
|
120
|
+
? `created ${plan.configPath}`
|
|
121
|
+
: `preserved existing config: ${plan.configPath}`,
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (sub === "show") {
|
|
127
|
+
const data = await adapter.getConfigShow();
|
|
128
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, data, () => {
|
|
129
|
+
renderTargetBanner(ctx.target);
|
|
130
|
+
console.log(JSON.stringify(data.effective, null, 2));
|
|
131
|
+
});
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (sub === "get") {
|
|
136
|
+
const key = args[0];
|
|
137
|
+
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
138
|
+
const val = await adapter.getConfigGet(key);
|
|
139
|
+
emitSuccess(
|
|
140
|
+
{ isJson: ctx.isJson, target: ctx.target },
|
|
141
|
+
{ key, value: val },
|
|
142
|
+
() => {
|
|
143
|
+
console.log(
|
|
144
|
+
typeof val === "object" ? JSON.stringify(val) : String(val),
|
|
145
|
+
);
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (sub === "validate") {
|
|
152
|
+
const res = await adapter.configValidate();
|
|
153
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
154
|
+
console.log(
|
|
155
|
+
res.valid ? "Configuration is valid." : "Configuration is invalid.",
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (sub === "diff") {
|
|
162
|
+
const res = await adapter.configDiff();
|
|
163
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
164
|
+
renderTargetBanner(ctx.target);
|
|
165
|
+
console.log(JSON.stringify(res.diff, null, 2));
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (sub === "set") {
|
|
171
|
+
const key = args[0];
|
|
172
|
+
const value = args[1];
|
|
173
|
+
if (!key || value === undefined) {
|
|
174
|
+
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
175
|
+
}
|
|
176
|
+
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
177
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
178
|
+
renderTargetBanner(ctx.target);
|
|
179
|
+
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
180
|
+
console.log(
|
|
181
|
+
`${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`,
|
|
182
|
+
);
|
|
183
|
+
console.log(
|
|
184
|
+
`Persistence: ${res.persistence}, Application: ${res.application}`,
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (sub === "status") {
|
|
191
|
+
const res = await adapter.configStatus();
|
|
192
|
+
emitSuccess({ isJson: ctx.isJson, target: ctx.target }, res, () => {
|
|
193
|
+
renderTargetBanner(ctx.target);
|
|
194
|
+
console.log(`Runtime: ${res.runtime}`);
|
|
195
|
+
console.log(`Active revision: ${res.active_revision}`);
|
|
196
|
+
console.log(`Readiness: ${res.readiness.status}`);
|
|
197
|
+
});
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
throw new Error("usage: skillmux config show");
|
|
202
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { expandHome } from "../config";
|
|
2
|
+
import { pinCore, unpinCore, validateManifest, writeManifestAtomic } from "../manifest";
|
|
3
|
+
import { emitSuccess } from "../output";
|
|
4
|
+
import { confirmIfNeeded, loadManifestContext } from "./shared";
|
|
5
|
+
export async function runCore(
|
|
6
|
+
subCommand: string,
|
|
7
|
+
args: string[],
|
|
8
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
if (subCommand !== "pin" && subCommand !== "unpin") {
|
|
11
|
+
throw new Error("usage: skillmux core <pin|unpin>");
|
|
12
|
+
}
|
|
13
|
+
const skillIds = args.filter((arg) => !arg.startsWith("-"));
|
|
14
|
+
if (skillIds.length === 0) {
|
|
15
|
+
throw new Error(`usage: skillmux core ${subCommand} <skill_id>... --yes`);
|
|
16
|
+
}
|
|
17
|
+
const yes = args.includes("--yes");
|
|
18
|
+
const { config, vaultPath, manifestPath, manifest } =
|
|
19
|
+
await loadManifestContext();
|
|
20
|
+
let updated = manifest;
|
|
21
|
+
for (const skillId of skillIds) {
|
|
22
|
+
updated =
|
|
23
|
+
subCommand === "pin"
|
|
24
|
+
? pinCore(updated, skillId)
|
|
25
|
+
: unpinCore(updated, skillId);
|
|
26
|
+
}
|
|
27
|
+
validateManifest(
|
|
28
|
+
updated,
|
|
29
|
+
vaultPath,
|
|
30
|
+
config.local_vault_paths.map(expandHome),
|
|
31
|
+
);
|
|
32
|
+
if (options.dryRun) {
|
|
33
|
+
emitSuccess(
|
|
34
|
+
{ isJson: options.isJson },
|
|
35
|
+
{ subcommand: subCommand, skill_ids: skillIds },
|
|
36
|
+
() =>
|
|
37
|
+
console.log(`${subCommand}: [core] ${skillIds.join(", ")} (dry-run)`),
|
|
38
|
+
);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
!(await confirmIfNeeded({
|
|
43
|
+
confirmed: yes,
|
|
44
|
+
isJson: options.isJson,
|
|
45
|
+
prompt: `${subCommand} ${skillIds.join(", ")} in [core]?`,
|
|
46
|
+
nonInteractiveError: `skillmux core ${subCommand} requires --yes when run non-interactively`,
|
|
47
|
+
}))
|
|
48
|
+
)
|
|
49
|
+
return;
|
|
50
|
+
writeManifestAtomic(manifestPath, updated);
|
|
51
|
+
console.log(`${subCommand}: [core] ${skillIds.join(", ")}`);
|
|
52
|
+
}
|