@klhapp/skillmux 1.10.0 → 1.11.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 +31 -0
- package/README.md +18 -18
- 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 +73 -33
- package/docs/concepts.md +10 -10
- package/docs/configuration.md +6 -4
- package/docs/deployment.md +1 -1
- package/docs/getting-started.md +17 -13
- package/docs/mcp-routing.md +1 -1
- package/docs/skill-management.md +11 -11
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/src/adapters.ts +11 -11
- package/src/cli.ts +173 -63
- 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/project.ts +161 -44
- package/src/commands/report.ts +3 -3
- package/src/commands/shared.ts +7 -14
- package/src/commands/skill.ts +2 -1
- package/src/commands/target.ts +27 -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 +19 -19
- package/src/server.ts +1 -1
- package/src/init-clients.ts +0 -220
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
|
|
package/src/commands/init.ts
CHANGED
|
@@ -15,21 +15,25 @@ import {
|
|
|
15
15
|
surfaceCandidates,
|
|
16
16
|
} from "../init";
|
|
17
17
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
type ClientId,
|
|
18
|
+
assessAgentReadiness,
|
|
19
|
+
detectInstalledAgents,
|
|
20
|
+
planAgentSurfaces,
|
|
21
|
+
SUPPORTED_AGENT_IDS,
|
|
22
|
+
type AgentId,
|
|
24
23
|
type ReadinessAxis,
|
|
25
|
-
} from "../init-
|
|
24
|
+
} from "../init-agents";
|
|
26
25
|
import {
|
|
27
26
|
applyInstructionPlan,
|
|
28
27
|
planInstructionSetup,
|
|
29
28
|
rollbackInstructionPlan,
|
|
30
29
|
} from "../init-instructions";
|
|
31
30
|
import { parseManifest, resolveManifestPath } from "../manifest";
|
|
32
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
MCP_REGISTRABLE_AGENTS,
|
|
33
|
+
registerMcpServer,
|
|
34
|
+
type McpRegistrationResult,
|
|
35
|
+
} from "../mcp-registration";
|
|
36
|
+
import { isInteractive } from "../output";
|
|
33
37
|
import { isGlobalFlag } from "../global-flags";
|
|
34
38
|
import {
|
|
35
39
|
parseCommaList,
|
|
@@ -49,47 +53,41 @@ import { confirmAction } from "./shared";
|
|
|
49
53
|
import { runSync } from "./sync";
|
|
50
54
|
|
|
51
55
|
function parseInitArgs(args: string[]): {
|
|
52
|
-
|
|
53
|
-
clients: string[];
|
|
56
|
+
agents: string[];
|
|
54
57
|
coreSkillIds: string[];
|
|
55
|
-
customPath?: string;
|
|
56
58
|
migrateFullVault: boolean;
|
|
59
|
+
showMcpSetup: boolean;
|
|
60
|
+
registerMcp: boolean;
|
|
57
61
|
skipInstructions: boolean;
|
|
58
62
|
sync: boolean;
|
|
59
63
|
vaultPath?: string;
|
|
60
64
|
yes: boolean;
|
|
61
65
|
} {
|
|
62
|
-
const
|
|
63
|
-
const clients: string[] = [];
|
|
66
|
+
const agents: string[] = [];
|
|
64
67
|
const coreSkillIds: string[] = [];
|
|
65
|
-
let customPath: string | undefined;
|
|
66
68
|
let migrateFullVault = false;
|
|
69
|
+
let showMcpSetup = false;
|
|
70
|
+
let registerMcp = false;
|
|
67
71
|
let skipInstructions = false;
|
|
68
72
|
let sync = true;
|
|
69
73
|
let vaultPath: string | undefined;
|
|
70
74
|
let yes = false;
|
|
71
75
|
for (let i = 0; i < args.length; i++) {
|
|
72
76
|
const option = args[i];
|
|
73
|
-
if (option === "--target") {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
} else if (option === "--client") {
|
|
77
|
+
if (option === "--target" || option === "--dir" || option === "--client") {
|
|
78
|
+
throw new Error(
|
|
79
|
+
`${option} was removed; select a specific supported agent with --agent instead (see "skillmux init --help")`,
|
|
80
|
+
);
|
|
81
|
+
} else if (option === "--agent") {
|
|
79
82
|
const value = args[i + 1];
|
|
80
|
-
if (!value) throw new Error("--
|
|
81
|
-
|
|
83
|
+
if (!value) throw new Error("--agent requires a name");
|
|
84
|
+
agents.push(value);
|
|
82
85
|
i++;
|
|
83
86
|
} else if (option === "--vault") {
|
|
84
87
|
const value = args[i + 1];
|
|
85
88
|
if (!value) throw new Error("--vault requires a path");
|
|
86
89
|
vaultPath = value;
|
|
87
90
|
i++;
|
|
88
|
-
} else if (option === "--dir") {
|
|
89
|
-
const value = args[i + 1];
|
|
90
|
-
if (!value) throw new Error("--dir requires a directory");
|
|
91
|
-
customPath = value;
|
|
92
|
-
i++;
|
|
93
91
|
} else if (option === "--core") {
|
|
94
92
|
const value = args[i + 1];
|
|
95
93
|
if (!value) throw new Error("--core requires a skill_id");
|
|
@@ -102,6 +100,10 @@ function parseInitArgs(args: string[]): {
|
|
|
102
100
|
continue;
|
|
103
101
|
} else if (option === "--migrate-full-vault") {
|
|
104
102
|
migrateFullVault = true;
|
|
103
|
+
} else if (option === "--show-mcp-setup") {
|
|
104
|
+
showMcpSetup = true;
|
|
105
|
+
} else if (option === "--register-mcp") {
|
|
106
|
+
registerMcp = true;
|
|
105
107
|
} else if (option === "--no-instructions") {
|
|
106
108
|
skipInstructions = true;
|
|
107
109
|
} else if (option === "--no-sync") {
|
|
@@ -113,11 +115,11 @@ function parseInitArgs(args: string[]): {
|
|
|
113
115
|
}
|
|
114
116
|
}
|
|
115
117
|
return {
|
|
116
|
-
|
|
117
|
-
clients,
|
|
118
|
+
agents,
|
|
118
119
|
coreSkillIds,
|
|
119
|
-
customPath,
|
|
120
120
|
migrateFullVault,
|
|
121
|
+
showMcpSetup,
|
|
122
|
+
registerMcp,
|
|
121
123
|
skipInstructions,
|
|
122
124
|
sync,
|
|
123
125
|
vaultPath,
|
|
@@ -125,16 +127,49 @@ function parseInitArgs(args: string[]): {
|
|
|
125
127
|
};
|
|
126
128
|
}
|
|
127
129
|
|
|
130
|
+
interface InitJsonPayload {
|
|
131
|
+
command: "init";
|
|
132
|
+
phase: "plan" | "result";
|
|
133
|
+
dry_run: boolean;
|
|
134
|
+
applied: boolean;
|
|
135
|
+
plan: unknown;
|
|
136
|
+
result?: unknown;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* init predates the shared emitSuccess envelope and shipped its own top-level
|
|
141
|
+
* keys, which existing automation reads. Emit the documented envelope
|
|
142
|
+
* (`target`/`data`/`error`, see docs/cli.md) so generic consumers work, while
|
|
143
|
+
* keeping the original keys so nothing breaks. init's error path already goes
|
|
144
|
+
* through the shared handler and needs no bridging.
|
|
145
|
+
*
|
|
146
|
+
* DEPRECATED: the duplicated top-level `command`/`phase`/`dry_run`/`applied`/
|
|
147
|
+
* `plan`/`result` keys should be dropped in the next major version, leaving
|
|
148
|
+
* only the standard envelope. init is local-only, so `target` is always
|
|
149
|
+
* "local".
|
|
150
|
+
*/
|
|
151
|
+
function initJsonEnvelope(payload: InitJsonPayload): string {
|
|
152
|
+
return JSON.stringify({
|
|
153
|
+
schema_version: 1,
|
|
154
|
+
ok: true,
|
|
155
|
+
context: "local",
|
|
156
|
+
target: "local",
|
|
157
|
+
data: payload,
|
|
158
|
+
error: null,
|
|
159
|
+
...payload,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
128
163
|
export async function runInit(
|
|
129
164
|
args: string[],
|
|
130
165
|
options: { isJson: boolean; dryRun: boolean },
|
|
131
166
|
): Promise<void> {
|
|
132
167
|
const {
|
|
133
|
-
|
|
134
|
-
clients: requestedClients,
|
|
168
|
+
agents: requestedAgents,
|
|
135
169
|
coreSkillIds,
|
|
136
|
-
customPath,
|
|
137
170
|
migrateFullVault,
|
|
171
|
+
showMcpSetup,
|
|
172
|
+
registerMcp: requestedRegisterMcp,
|
|
138
173
|
skipInstructions,
|
|
139
174
|
sync,
|
|
140
175
|
vaultPath: requestedVaultPath,
|
|
@@ -150,9 +185,14 @@ export async function runInit(
|
|
|
150
185
|
let configPlan: ConfigInitPlan | undefined;
|
|
151
186
|
let vaultPath: string;
|
|
152
187
|
if (!existsSync(configPath)) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
188
|
+
let bootstrapVaultPath = requestedVaultPath;
|
|
189
|
+
if (!bootstrapVaultPath) {
|
|
190
|
+
if (guided) {
|
|
191
|
+
bootstrapVaultPath = await promptText("Skill vault path", "~/skills");
|
|
192
|
+
} else if (!options.isJson && isInteractive()) {
|
|
193
|
+
bootstrapVaultPath = "~/skills";
|
|
194
|
+
}
|
|
195
|
+
}
|
|
156
196
|
if (!bootstrapVaultPath) {
|
|
157
197
|
throw new Error(
|
|
158
198
|
`machine config does not exist: ${configPath}; re-run with --vault <path>`,
|
|
@@ -161,7 +201,7 @@ export async function runInit(
|
|
|
161
201
|
configPlan = planConfigInit(configPath, expandHome(bootstrapVaultPath));
|
|
162
202
|
vaultPath = configPlan.vaultPath;
|
|
163
203
|
if (!options.isJson) {
|
|
164
|
-
console.log(`config create: ${configPath}`);
|
|
204
|
+
console.log(`config create: ${configPath} (vault: ${vaultPath})`);
|
|
165
205
|
}
|
|
166
206
|
} else {
|
|
167
207
|
const config = await loadConfig();
|
|
@@ -178,25 +218,25 @@ export async function runInit(
|
|
|
178
218
|
throw new Error(vaultHealth.message);
|
|
179
219
|
}
|
|
180
220
|
|
|
181
|
-
let
|
|
221
|
+
let selectedAgents = requestedAgents;
|
|
182
222
|
if (guided) {
|
|
183
|
-
const detected =
|
|
223
|
+
const detected = detectInstalledAgents({
|
|
184
224
|
codexHome: process.env.CODEX_HOME
|
|
185
225
|
? expandHome(process.env.CODEX_HOME)
|
|
186
226
|
: undefined,
|
|
187
227
|
});
|
|
188
228
|
const evidence = new Map(
|
|
189
|
-
detected.map((item) => [item.
|
|
229
|
+
detected.map((item) => [item.agent, item.evidence]),
|
|
190
230
|
);
|
|
191
|
-
|
|
192
|
-
"Which
|
|
193
|
-
|
|
194
|
-
value:
|
|
195
|
-
label:
|
|
196
|
-
detail: evidence.has(
|
|
197
|
-
? `detected: ${evidence.get(
|
|
231
|
+
selectedAgents = await promptMultiSelect(
|
|
232
|
+
"Which agents do you use?",
|
|
233
|
+
SUPPORTED_AGENT_IDS.map((agent) => ({
|
|
234
|
+
value: agent,
|
|
235
|
+
label: agent,
|
|
236
|
+
detail: evidence.has(agent)
|
|
237
|
+
? `detected: ${evidence.get(agent)}`
|
|
198
238
|
: undefined,
|
|
199
|
-
selected: evidence.has(
|
|
239
|
+
selected: evidence.has(agent) || requestedAgents.includes(agent),
|
|
200
240
|
})),
|
|
201
241
|
);
|
|
202
242
|
}
|
|
@@ -210,77 +250,79 @@ export async function runInit(
|
|
|
210
250
|
);
|
|
211
251
|
}
|
|
212
252
|
|
|
213
|
-
|
|
253
|
+
// Native pins and MCP registration are independent choices — this stays
|
|
254
|
+
// opt-in and only offered for agents whose own CLI we've verified, so a
|
|
255
|
+
// user who only wants native skill management sees nothing new here.
|
|
256
|
+
const registrableAgents = selectedAgents.filter((agent) =>
|
|
257
|
+
MCP_REGISTRABLE_AGENTS.includes(agent as AgentId),
|
|
258
|
+
) as AgentId[];
|
|
259
|
+
let registerMcp = requestedRegisterMcp;
|
|
260
|
+
if (guided && registrableAgents.length > 0) {
|
|
261
|
+
registerMcp = await confirmAction(
|
|
262
|
+
`Also register skillmux as an MCP server for ${registrableAgents.join(", ")}?`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const agentPlan = planAgentSurfaces(selectedAgents, {
|
|
214
267
|
codexHome: process.env.CODEX_HOME
|
|
215
268
|
? expandHome(process.env.CODEX_HOME)
|
|
216
269
|
: undefined,
|
|
217
270
|
});
|
|
271
|
+
// The managed instruction block only teaches an agent to call resolve_skill
|
|
272
|
+
// /fetch_skill (MCP tools) — writing it for an agent with no MCP registered
|
|
273
|
+
// or requested would tell that agent to call tools that don't exist. Only
|
|
274
|
+
// agents actually getting MCP this run (or whose snippet the user asked to
|
|
275
|
+
// see) get the block; native-only setups get no instruction writes at all.
|
|
276
|
+
const mcpInstructionAgentIds = agentPlan.agents
|
|
277
|
+
.map((agent) => agent.id)
|
|
278
|
+
.filter(
|
|
279
|
+
(agentId) =>
|
|
280
|
+
showMcpSetup || (registerMcp && registrableAgents.includes(agentId)),
|
|
281
|
+
);
|
|
218
282
|
const instructionPlan = planInstructionSetup(
|
|
219
|
-
skipInstructions ? [] :
|
|
283
|
+
skipInstructions ? [] : mcpInstructionAgentIds,
|
|
220
284
|
{
|
|
221
285
|
codexHome: process.env.CODEX_HOME
|
|
222
286
|
? expandHome(process.env.CODEX_HOME)
|
|
223
287
|
: undefined,
|
|
224
288
|
},
|
|
225
289
|
);
|
|
226
|
-
const instructionReadiness: Partial<Record<
|
|
290
|
+
const instructionReadiness: Partial<Record<AgentId, ReadinessAxis>> = {};
|
|
227
291
|
for (const change of instructionPlan.changes) {
|
|
228
|
-
for (const
|
|
229
|
-
instructionReadiness[
|
|
292
|
+
for (const agent of change.agents) {
|
|
293
|
+
instructionReadiness[agent] = {
|
|
230
294
|
status: change.status === "unchanged" ? "ready" : "planned",
|
|
231
295
|
detail: change.path,
|
|
232
296
|
};
|
|
233
297
|
}
|
|
234
298
|
}
|
|
235
299
|
for (const manual of instructionPlan.manual) {
|
|
236
|
-
instructionReadiness[manual.
|
|
300
|
+
instructionReadiness[manual.agent] = {
|
|
237
301
|
status: "manual",
|
|
238
302
|
detail: manual.reason,
|
|
239
303
|
};
|
|
240
304
|
}
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
]);
|
|
249
|
-
const explicitSurfaceTargets = explicitTargets
|
|
250
|
-
.filter((name) => builtInNames.has(name))
|
|
251
|
-
.map((name) =>
|
|
252
|
-
resolveBuiltInTarget(name, {
|
|
253
|
-
codexHome: process.env.CODEX_HOME
|
|
254
|
-
? expandHome(process.env.CODEX_HOME)
|
|
255
|
-
: undefined,
|
|
256
|
-
customPath: customPath ? expandHome(customPath) : undefined,
|
|
257
|
-
}),
|
|
258
|
-
);
|
|
259
|
-
if (customPath && !explicitTargets.includes("custom")) {
|
|
260
|
-
throw new Error("--dir may only be used with --target custom");
|
|
261
|
-
}
|
|
262
|
-
for (const target of explicitSurfaceTargets) {
|
|
263
|
-
if (target.warning) warn(target.warning);
|
|
305
|
+
for (const agent of agentPlan.agents) {
|
|
306
|
+
if (mcpInstructionAgentIds.includes(agent.id)) continue;
|
|
307
|
+
if (instructionReadiness[agent.id]) continue;
|
|
308
|
+
instructionReadiness[agent.id] = {
|
|
309
|
+
status: "not-applicable",
|
|
310
|
+
detail: "no MCP requested — see --show-mcp-setup / --register-mcp",
|
|
311
|
+
};
|
|
264
312
|
}
|
|
265
|
-
const targetByPath = new Map(
|
|
266
|
-
explicitSurfaceTargets.map(
|
|
267
|
-
(target) => [target.path, target.targetName] as const,
|
|
268
|
-
),
|
|
269
|
-
);
|
|
270
313
|
const existingManifestPath = resolveManifestPath(vaultPath);
|
|
271
314
|
const existingManifest = existingManifestPath
|
|
272
315
|
? parseManifest(await Bun.file(existingManifestPath).text())
|
|
273
316
|
: undefined;
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
317
|
+
const targetByPath = new Map<string, string>();
|
|
318
|
+
for (const surface of agentPlan.surfaces) {
|
|
319
|
+
targetByPath.set(
|
|
320
|
+
surface.path,
|
|
321
|
+
existingManifest
|
|
322
|
+
? (configuredTargetForSurface(existingManifest, surface) ??
|
|
323
|
+
surface.targetName)
|
|
324
|
+
: surface.targetName,
|
|
325
|
+
);
|
|
284
326
|
}
|
|
285
327
|
const candidatePaths = [
|
|
286
328
|
...new Set([
|
|
@@ -327,11 +369,11 @@ export async function runInit(
|
|
|
327
369
|
`${name} (${candidate.path}): ${kind}, ${candidate.skillCount} skills${marked}`,
|
|
328
370
|
);
|
|
329
371
|
}
|
|
330
|
-
for (const readiness of
|
|
331
|
-
|
|
372
|
+
for (const readiness of assessAgentReadiness(
|
|
373
|
+
agentPlan,
|
|
332
374
|
instructionReadiness,
|
|
333
375
|
)) {
|
|
334
|
-
console.log(`\n${readiness.
|
|
376
|
+
console.log(`\n${readiness.agent} readiness:`);
|
|
335
377
|
console.log(
|
|
336
378
|
` skill surface: ${readiness.skillSurface.status} — ${readiness.skillSurface.detail}`,
|
|
337
379
|
);
|
|
@@ -344,20 +386,15 @@ export async function runInit(
|
|
|
344
386
|
}
|
|
345
387
|
for (const change of instructionPlan.changes) {
|
|
346
388
|
console.log(
|
|
347
|
-
`instructions ${change.status}: ${change.path} (${change.
|
|
389
|
+
`instructions ${change.status}: ${change.path} (${change.agents.join(", ")})`,
|
|
348
390
|
);
|
|
349
391
|
}
|
|
350
392
|
for (const manual of instructionPlan.manual) {
|
|
351
|
-
console.log(`instructions manual: ${manual.
|
|
393
|
+
console.log(`instructions manual: ${manual.agent} — ${manual.reason}`);
|
|
352
394
|
}
|
|
353
395
|
}
|
|
354
396
|
|
|
355
|
-
const requestedTargets = [
|
|
356
|
-
...new Set([
|
|
357
|
-
...explicitTargets.filter((name) => !builtInNames.has(name)),
|
|
358
|
-
...targetByPath.values(),
|
|
359
|
-
]),
|
|
360
|
-
];
|
|
397
|
+
const requestedTargets = [...new Set(targetByPath.values())];
|
|
361
398
|
const hasInstructionWrites = instructionPlan.changes.some(
|
|
362
399
|
(change) => change.status !== "unchanged",
|
|
363
400
|
);
|
|
@@ -402,7 +439,7 @@ export async function runInit(
|
|
|
402
439
|
);
|
|
403
440
|
}
|
|
404
441
|
throw new Error(
|
|
405
|
-
`
|
|
442
|
+
`target "${name}" not among detected surfaces`,
|
|
406
443
|
);
|
|
407
444
|
}
|
|
408
445
|
}
|
|
@@ -425,22 +462,21 @@ export async function runInit(
|
|
|
425
462
|
config: configPlan
|
|
426
463
|
? { path: configPlan.configPath, action: configPlan.action }
|
|
427
464
|
: { path: configPath, action: "preserve" },
|
|
428
|
-
|
|
465
|
+
agents: agentPlan.agents.map((agent) => agent.id),
|
|
429
466
|
targets: confirmedTargets,
|
|
430
467
|
core: plannedManifest.core.skills,
|
|
431
|
-
instructions: instructionPlan.changes.map(({ path,
|
|
468
|
+
instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
|
|
432
469
|
path,
|
|
433
|
-
|
|
470
|
+
agents,
|
|
434
471
|
status,
|
|
435
472
|
})),
|
|
436
473
|
manual: instructionPlan.manual,
|
|
474
|
+
register_mcp_for: registerMcp ? registrableAgents : [],
|
|
437
475
|
};
|
|
438
476
|
if (!hasChanges) {
|
|
439
477
|
if (options.isJson) {
|
|
440
478
|
console.log(
|
|
441
|
-
|
|
442
|
-
schema_version: 1,
|
|
443
|
-
ok: true,
|
|
479
|
+
initJsonEnvelope({
|
|
444
480
|
command: "init",
|
|
445
481
|
phase: "plan",
|
|
446
482
|
dry_run: options.dryRun,
|
|
@@ -466,9 +502,7 @@ export async function runInit(
|
|
|
466
502
|
if (options.dryRun) {
|
|
467
503
|
if (options.isJson) {
|
|
468
504
|
console.log(
|
|
469
|
-
|
|
470
|
-
schema_version: 1,
|
|
471
|
-
ok: true,
|
|
505
|
+
initJsonEnvelope({
|
|
472
506
|
command: "init",
|
|
473
507
|
phase: "plan",
|
|
474
508
|
dry_run: true,
|
|
@@ -480,7 +514,8 @@ export async function runInit(
|
|
|
480
514
|
console.log(
|
|
481
515
|
`\ndry-run: ${confirmedTargets.length} target(s), ` +
|
|
482
516
|
`${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} instruction file(s), ` +
|
|
483
|
-
`core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}
|
|
517
|
+
`core: ${plannedManifest.core.skills.join(", ") || "(unchanged)"}, ` +
|
|
518
|
+
`MCP registration: ${registerMcp ? registrableAgents.join(", ") || "(none)" : "(none)"}`,
|
|
484
519
|
);
|
|
485
520
|
}
|
|
486
521
|
return;
|
|
@@ -490,7 +525,7 @@ export async function runInit(
|
|
|
490
525
|
if (!options.isJson && isInteractive()) {
|
|
491
526
|
if (guided) {
|
|
492
527
|
console.log("\nReview");
|
|
493
|
-
console.log(`
|
|
528
|
+
console.log(` agents: ${selectedAgents.join(", ") || "(none)"}`);
|
|
494
529
|
console.log(
|
|
495
530
|
` targets: ${confirmedTargets.map((target) => `${target.name} -> ${target.dir}`).join(", ") || "(none)"}`,
|
|
496
531
|
);
|
|
@@ -500,6 +535,9 @@ export async function runInit(
|
|
|
500
535
|
console.log(
|
|
501
536
|
` core: ${plannedManifest.core.skills.join(", ") || "(none)"}`,
|
|
502
537
|
);
|
|
538
|
+
console.log(
|
|
539
|
+
` MCP registration: ${registerMcp ? registrableAgents.join(", ") || "(none)" : "(none)"}`,
|
|
540
|
+
);
|
|
503
541
|
console.log(` sync: ${sync ? "yes" : "no"}`);
|
|
504
542
|
if (!(await confirmAction("apply this setup plan?"))) {
|
|
505
543
|
console.log("init cancelled");
|
|
@@ -572,11 +610,19 @@ export async function runInit(
|
|
|
572
610
|
);
|
|
573
611
|
}
|
|
574
612
|
|
|
613
|
+
// Best-effort and outside the rollback above: this mutates another tool's
|
|
614
|
+
// own config, not skillmux's, so a registration failure is reported, never
|
|
615
|
+
// rolled back — the successful native setup above still stands either way.
|
|
616
|
+
const mcpRegistrations: McpRegistrationResult[] = [];
|
|
617
|
+
if (registerMcp) {
|
|
618
|
+
for (const agent of registrableAgents) {
|
|
619
|
+
mcpRegistrations.push(await registerMcpServer(agent));
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
575
623
|
if (options.isJson) {
|
|
576
624
|
console.log(
|
|
577
|
-
|
|
578
|
-
schema_version: 1,
|
|
579
|
-
ok: true,
|
|
625
|
+
initJsonEnvelope({
|
|
580
626
|
command: "init",
|
|
581
627
|
phase: "result",
|
|
582
628
|
dry_run: false,
|
|
@@ -589,6 +635,7 @@ export async function runInit(
|
|
|
589
635
|
.filter((change) => change.status !== "unchanged")
|
|
590
636
|
.map((change) => change.path),
|
|
591
637
|
core: plannedManifest.core.skills,
|
|
638
|
+
mcp_registrations: mcpRegistrations,
|
|
592
639
|
},
|
|
593
640
|
}),
|
|
594
641
|
);
|
|
@@ -605,11 +652,15 @@ export async function runInit(
|
|
|
605
652
|
if (plannedManifest.core.skills.length === 0 && confirmedTargets.length > 0) {
|
|
606
653
|
console.log("next: skillmux core pin <skill_id> --yes");
|
|
607
654
|
}
|
|
608
|
-
if (confirmedTargets.length > 0) console.log("next: skillmux sync");
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
655
|
+
if (!sync && confirmedTargets.length > 0) console.log("next: skillmux sync");
|
|
656
|
+
for (const registration of mcpRegistrations) {
|
|
657
|
+
console.log(
|
|
658
|
+
registration.ok
|
|
659
|
+
? `registered skillmux as an MCP server for ${registration.agent}`
|
|
660
|
+
: `failed to register skillmux as an MCP server for ${registration.agent}: ${registration.error}`,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
if (selectedAgents.length === 0 || showMcpSetup) {
|
|
613
664
|
console.log(`\n${printLastMile()}`);
|
|
614
665
|
}
|
|
615
666
|
// Reaching this point already required approval above (--yes, or an accepted
|
|
@@ -617,5 +668,5 @@ export async function runInit(
|
|
|
617
668
|
// new target directories this init just adopted, so runSync's own new-target
|
|
618
669
|
// confirmation gate would just be a redundant (and non-interactively,
|
|
619
670
|
// silently-skipping) re-ask.
|
|
620
|
-
if (
|
|
671
|
+
if (sync && confirmedTargets.length > 0) await runSync(["--yes"]);
|
|
621
672
|
}
|