@klhapp/skillmux 1.10.0 → 1.11.1
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 +44 -0
- package/README.md +26 -19
- package/docs/README.md +3 -3
- package/docs/assets/architecture-dark.svg +39 -32
- package/docs/assets/architecture-light.svg +25 -18
- package/docs/cli.md +80 -33
- package/docs/concepts.md +15 -11
- package/docs/configuration.md +6 -4
- package/docs/deployment.md +1 -1
- package/docs/getting-started.md +21 -13
- package/docs/mcp-routing.md +1 -1
- package/docs/skill-management.md +39 -19
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/src/adapters.ts +11 -11
- package/src/cli.ts +255 -72
- package/src/commands/audit.ts +2 -2
- package/src/commands/config.ts +23 -15
- package/src/commands/context.ts +11 -10
- package/src/commands/core.ts +2 -2
- package/src/commands/doctor.ts +31 -10
- package/src/commands/eval.ts +14 -4
- package/src/commands/init.ts +175 -124
- package/src/commands/install.ts +36 -13
- package/src/commands/project.ts +177 -44
- package/src/commands/report.ts +3 -3
- package/src/commands/scan.ts +18 -8
- package/src/commands/shared.ts +7 -14
- package/src/commands/skill.ts +2 -1
- package/src/commands/target.ts +27 -9
- package/src/commands/update.ts +16 -9
- package/src/completions.ts +41 -15
- package/src/config-service.ts +3 -3
- package/src/init-agents.ts +329 -0
- package/src/init-instructions.ts +47 -28
- package/src/mcp-registration.ts +89 -0
- package/src/output.ts +53 -16
- package/src/prompts.ts +75 -20
- package/src/scan.ts +53 -19
- package/src/server.ts +1 -1
- package/src/init-clients.ts +0 -220
package/src/commands/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { expandHome, migrateLegacyPaths, resolveConfigPath } from "../config";
|
|
2
|
-
import { type
|
|
2
|
+
import { type ContextAdapter } from "../adapters";
|
|
3
3
|
import { type ResolvedContext } from "../context";
|
|
4
4
|
import { applyConfigInit, planConfigInit, type ConfigInitPlan } from "../setup";
|
|
5
|
-
import { emitSuccess, isInteractive,
|
|
5
|
+
import { emitSuccess, isInteractive, renderContextBanner, unknownSubcommandError } from "../output";
|
|
6
6
|
import { confirmAction } from "./shared";
|
|
7
7
|
import { isGlobalFlag } from "../global-flags";
|
|
8
8
|
function emitConfigInitOutcome(
|
|
@@ -38,10 +38,10 @@ function emitConfigInitOutcome(
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export async function handleConfigCommand(
|
|
41
|
-
adapter:
|
|
41
|
+
adapter: ContextAdapter,
|
|
42
42
|
sub: string,
|
|
43
43
|
args: string[],
|
|
44
|
-
ctx: {
|
|
44
|
+
ctx: { context: ResolvedContext; isJson: boolean; dryRun: boolean },
|
|
45
45
|
) {
|
|
46
46
|
if (sub === "init") {
|
|
47
47
|
let vaultPath: string | undefined;
|
|
@@ -127,8 +127,8 @@ export async function handleConfigCommand(
|
|
|
127
127
|
if (sub === "show") {
|
|
128
128
|
const withSources = args.includes("--sources");
|
|
129
129
|
const data = await adapter.getConfigShow();
|
|
130
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
131
|
-
|
|
130
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, data, () => {
|
|
131
|
+
renderContextBanner(ctx.context);
|
|
132
132
|
if (withSources) {
|
|
133
133
|
const policy =
|
|
134
134
|
data.effective.config?.environment_overrides === false
|
|
@@ -151,7 +151,7 @@ export async function handleConfigCommand(
|
|
|
151
151
|
if (!key) throw new Error("usage: skillmux config get <key>");
|
|
152
152
|
const val = await adapter.getConfigGet(key);
|
|
153
153
|
emitSuccess(
|
|
154
|
-
{ isJson: ctx.isJson,
|
|
154
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
155
155
|
{ key, value: val },
|
|
156
156
|
() => {
|
|
157
157
|
console.log(
|
|
@@ -164,7 +164,7 @@ export async function handleConfigCommand(
|
|
|
164
164
|
|
|
165
165
|
if (sub === "validate") {
|
|
166
166
|
const res = await adapter.configValidate();
|
|
167
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
167
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
168
168
|
console.log(res.valid ? "configuration is valid" : "configuration is invalid");
|
|
169
169
|
});
|
|
170
170
|
return;
|
|
@@ -172,8 +172,8 @@ export async function handleConfigCommand(
|
|
|
172
172
|
|
|
173
173
|
if (sub === "diff") {
|
|
174
174
|
const res = await adapter.configDiff();
|
|
175
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
176
|
-
|
|
175
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
176
|
+
renderContextBanner(ctx.context);
|
|
177
177
|
console.log(JSON.stringify(res.diff, null, 2));
|
|
178
178
|
});
|
|
179
179
|
return;
|
|
@@ -186,8 +186,8 @@ export async function handleConfigCommand(
|
|
|
186
186
|
throw new Error("usage: skillmux config set <key> <value> [--dry-run]");
|
|
187
187
|
}
|
|
188
188
|
const res = await adapter.configSet(key, value, { dryRun: ctx.dryRun });
|
|
189
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
190
|
-
|
|
189
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
190
|
+
renderContextBanner(ctx.context);
|
|
191
191
|
const prefix = ctx.dryRun ? "[dry-run] " : "";
|
|
192
192
|
console.log(
|
|
193
193
|
`${prefix}${key}: ${JSON.stringify(res.prior_val)} -> ${JSON.stringify(res.resulting_val)}`,
|
|
@@ -201,8 +201,8 @@ export async function handleConfigCommand(
|
|
|
201
201
|
|
|
202
202
|
if (sub === "status") {
|
|
203
203
|
const res = await adapter.configStatus();
|
|
204
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
205
|
-
|
|
204
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, res, () => {
|
|
205
|
+
renderContextBanner(ctx.context);
|
|
206
206
|
console.log(`runtime: ${res.runtime}`);
|
|
207
207
|
console.log(`deployment runtime: ${res.deployment_runtime}`);
|
|
208
208
|
console.log(`image variant: ${res.image_variant ?? "none"}`);
|
|
@@ -212,5 +212,13 @@ export async function handleConfigCommand(
|
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
throw
|
|
215
|
+
throw unknownSubcommandError("config", sub, [
|
|
216
|
+
"init",
|
|
217
|
+
"show",
|
|
218
|
+
"get",
|
|
219
|
+
"set",
|
|
220
|
+
"validate",
|
|
221
|
+
"diff",
|
|
222
|
+
"status",
|
|
223
|
+
]);
|
|
216
224
|
}
|
package/src/commands/context.ts
CHANGED
|
@@ -9,18 +9,19 @@ import {
|
|
|
9
9
|
import {
|
|
10
10
|
emitSuccess,
|
|
11
11
|
renderTable,
|
|
12
|
-
|
|
12
|
+
renderContextBanner,
|
|
13
|
+
unknownSubcommandError,
|
|
13
14
|
} from "../output";
|
|
14
15
|
|
|
15
16
|
export async function handleContextCommand(
|
|
16
17
|
sub: string,
|
|
17
18
|
args: string[],
|
|
18
|
-
ctx: {
|
|
19
|
+
ctx: { context: ResolvedContext; isJson: boolean },
|
|
19
20
|
) {
|
|
20
21
|
if (sub === "list") {
|
|
21
22
|
const contexts = await listContexts();
|
|
22
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
23
|
-
|
|
23
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, contexts, () => {
|
|
24
|
+
renderContextBanner(ctx.context);
|
|
24
25
|
renderTable(
|
|
25
26
|
[
|
|
26
27
|
{ key: "name", header: "NAME" },
|
|
@@ -40,8 +41,8 @@ export async function handleContextCommand(
|
|
|
40
41
|
|
|
41
42
|
if (sub === "current") {
|
|
42
43
|
const current = await getCurrentContext();
|
|
43
|
-
emitSuccess({ isJson: ctx.isJson,
|
|
44
|
-
|
|
44
|
+
emitSuccess({ isJson: ctx.isJson, context: ctx.context }, current, () => {
|
|
45
|
+
renderContextBanner(ctx.context);
|
|
45
46
|
console.log(`Current context: ${current.name} (${current.server})`);
|
|
46
47
|
});
|
|
47
48
|
return;
|
|
@@ -62,7 +63,7 @@ export async function handleContextCommand(
|
|
|
62
63
|
}
|
|
63
64
|
await addContext(name, { server, token_env: tokenEnv });
|
|
64
65
|
emitSuccess(
|
|
65
|
-
{ isJson: ctx.isJson,
|
|
66
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
66
67
|
{ name, server, token_env: tokenEnv },
|
|
67
68
|
() => {
|
|
68
69
|
console.log(`Added context "${name}" -> ${server}`);
|
|
@@ -76,7 +77,7 @@ export async function handleContextCommand(
|
|
|
76
77
|
if (!name) throw new Error("usage: skillmux context use <name>");
|
|
77
78
|
await useContext(name);
|
|
78
79
|
emitSuccess(
|
|
79
|
-
{ isJson: ctx.isJson,
|
|
80
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
80
81
|
{ default_context: name },
|
|
81
82
|
() => {
|
|
82
83
|
console.log(`Switched default context to "${name}"`);
|
|
@@ -90,7 +91,7 @@ export async function handleContextCommand(
|
|
|
90
91
|
if (!name) throw new Error("usage: skillmux context remove <name>");
|
|
91
92
|
await removeContext(name);
|
|
92
93
|
emitSuccess(
|
|
93
|
-
{ isJson: ctx.isJson,
|
|
94
|
+
{ isJson: ctx.isJson, context: ctx.context },
|
|
94
95
|
{ removed: name },
|
|
95
96
|
() => {
|
|
96
97
|
console.log(`Removed context "${name}"`);
|
|
@@ -99,5 +100,5 @@ export async function handleContextCommand(
|
|
|
99
100
|
return;
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
throw
|
|
103
|
+
throw unknownSubcommandError("context", sub, ["add", "list", "current", "use", "remove"]);
|
|
103
104
|
}
|
package/src/commands/core.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { expandHome } from "../config";
|
|
2
2
|
import { pinCore, unpinCore, validateManifest, writeManifestAtomic } from "../manifest";
|
|
3
|
-
import { emitSuccess } from "../output";
|
|
3
|
+
import { emitSuccess, unknownSubcommandError } from "../output";
|
|
4
4
|
import { confirmIfNeeded, loadManifestContext } from "./shared";
|
|
5
5
|
export async function runCore(
|
|
6
6
|
subCommand: string,
|
|
@@ -8,7 +8,7 @@ export async function runCore(
|
|
|
8
8
|
options: { isJson: boolean; dryRun: boolean },
|
|
9
9
|
): Promise<void> {
|
|
10
10
|
if (subCommand !== "pin" && subCommand !== "unpin") {
|
|
11
|
-
throw
|
|
11
|
+
throw unknownSubcommandError("core", subCommand, ["pin", "unpin"]);
|
|
12
12
|
}
|
|
13
13
|
const skillIds = args.filter((arg) => !arg.startsWith("-"));
|
|
14
14
|
if (skillIds.length === 0) {
|
package/src/commands/doctor.ts
CHANGED
|
@@ -1,29 +1,50 @@
|
|
|
1
1
|
import { resolveConfigPath } from "../config";
|
|
2
2
|
import { diagnose } from "../doctor";
|
|
3
3
|
import { getEffectiveConfig } from "../config-service";
|
|
4
|
-
import type {
|
|
4
|
+
import type { ContextAdapter } from "../adapters";
|
|
5
5
|
import type { ResolvedContext } from "../context";
|
|
6
|
+
import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
|
|
6
7
|
import {
|
|
7
8
|
emitSuccess,
|
|
8
9
|
green,
|
|
9
10
|
red,
|
|
10
|
-
|
|
11
|
+
renderContextBanner,
|
|
11
12
|
} from "../output";
|
|
12
13
|
|
|
14
|
+
/**
|
|
15
|
+
* doctor takes no options of its own, but it still has to reject unknown ones
|
|
16
|
+
* rather than silently ignoring them the way every other command does.
|
|
17
|
+
*/
|
|
18
|
+
export function parseDoctorArgs(args: readonly string[]): void {
|
|
19
|
+
for (let i = 0; i < args.length; i++) {
|
|
20
|
+
const option = args[i];
|
|
21
|
+
if (isGlobalFlag(option, "--json", "--allow-insecure", "--verbose")) {
|
|
22
|
+
// handled globally by main(); recognized here so it isn't rejected
|
|
23
|
+
} else if (isGlobalFlagWithValue(option)) {
|
|
24
|
+
// handled globally by main()'s resolveContext(); skip its value too
|
|
25
|
+
i++;
|
|
26
|
+
} else {
|
|
27
|
+
throw new Error(`unknown doctor option: ${option}`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
13
32
|
export async function runDoctor(options: {
|
|
14
33
|
isJson: boolean;
|
|
15
|
-
|
|
16
|
-
adapter:
|
|
34
|
+
context: ResolvedContext;
|
|
35
|
+
adapter: ContextAdapter;
|
|
36
|
+
args?: readonly string[];
|
|
17
37
|
}): Promise<void> {
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
parseDoctorArgs(options.args ?? []);
|
|
39
|
+
if (options.context.type === "remote") {
|
|
40
|
+
const context = options.context;
|
|
20
41
|
const [status, caps] = await Promise.all([
|
|
21
42
|
options.adapter.configStatus(),
|
|
22
43
|
options.adapter.getCapabilities(),
|
|
23
44
|
]);
|
|
24
45
|
const remoteReport = {
|
|
25
|
-
target:
|
|
26
|
-
server:
|
|
46
|
+
target: context.name || context.server,
|
|
47
|
+
server: context.server,
|
|
27
48
|
version: status.version,
|
|
28
49
|
deployment_runtime: status.deployment_runtime,
|
|
29
50
|
image_variant: status.image_variant ?? null,
|
|
@@ -34,8 +55,8 @@ export async function runDoctor(options: {
|
|
|
34
55
|
restart_required_keys: status.restart_required_keys,
|
|
35
56
|
last_reload_error: status.last_reload_error,
|
|
36
57
|
};
|
|
37
|
-
emitSuccess({ isJson: options.isJson, target: options.
|
|
38
|
-
|
|
58
|
+
emitSuccess({ isJson: options.isJson, target: options.context }, remoteReport, () => {
|
|
59
|
+
renderContextBanner(options.context);
|
|
39
60
|
console.log(`server: ${remoteReport.server}`);
|
|
40
61
|
console.log(`version: ${remoteReport.version}`);
|
|
41
62
|
console.log(`deployment runtime: ${remoteReport.deployment_runtime}`);
|
package/src/commands/eval.ts
CHANGED
|
@@ -5,20 +5,22 @@ import { excludeExistingCases, parseEvalCases } from "../eval";
|
|
|
5
5
|
import { emitSuccess, warn } from "../output";
|
|
6
6
|
import { parseSince } from "../stats";
|
|
7
7
|
import { confirmIfNeeded } from "./shared";
|
|
8
|
-
import type {
|
|
8
|
+
import type { ContextAdapter } from "../adapters";
|
|
9
9
|
import { isGlobalFlag, isGlobalFlagWithValue } from "../global-flags";
|
|
10
10
|
|
|
11
11
|
export async function runEvalPromote(
|
|
12
12
|
args: string[],
|
|
13
|
-
options: { isJson: boolean; dryRun: boolean; adapter:
|
|
13
|
+
options: { isJson: boolean; dryRun: boolean; adapter: ContextAdapter },
|
|
14
14
|
): Promise<void> {
|
|
15
15
|
let since: string | undefined;
|
|
16
|
+
let out: string | undefined;
|
|
16
17
|
let target: string | undefined;
|
|
17
18
|
let dryRun = options.dryRun;
|
|
18
19
|
let yes = false;
|
|
19
20
|
for (let i = 0; i < args.length; i++) {
|
|
20
21
|
const arg = args[i];
|
|
21
22
|
if (arg === "--since") since = args[++i];
|
|
23
|
+
else if (arg === "--out") out = args[++i];
|
|
22
24
|
else if (arg === "--target") target = args[++i];
|
|
23
25
|
else if (arg === "--dry-run") dryRun = true;
|
|
24
26
|
else if (arg === "--yes") yes = true;
|
|
@@ -31,12 +33,20 @@ export async function runEvalPromote(
|
|
|
31
33
|
}
|
|
32
34
|
}
|
|
33
35
|
if (!since) {
|
|
34
|
-
throw new Error("usage: skillmux eval promote --since <window> [--
|
|
36
|
+
throw new Error("usage: skillmux eval promote --since <window> [--out <path>] [--dry-run] [--yes] [--json]");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (out !== undefined && target !== undefined && out !== target) {
|
|
40
|
+
throw new Error("cannot specify conflicting --out and --target");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (target !== undefined) {
|
|
44
|
+
warn("--target is deprecated, use --out instead");
|
|
35
45
|
}
|
|
36
46
|
|
|
37
47
|
const config = await loadConfig();
|
|
38
48
|
const stateDir = expandHome(config.state_dir);
|
|
39
|
-
const targetPath = target ?? join(stateDir, "eval-observed.json");
|
|
49
|
+
const targetPath = out ?? target ?? join(stateDir, "eval-observed.json");
|
|
40
50
|
const sinceDate = parseSince(since);
|
|
41
51
|
const sinceIso = sinceDate.toISOString();
|
|
42
52
|
|