@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/project.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { existsSync, lstatSync } from "node:fs";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { expandHome } from "../config";
|
|
4
|
-
import {
|
|
4
|
+
import { planAgentSurfaces, SUPPORTED_AGENT_IDS, type AgentId } from "../init-agents";
|
|
5
|
+
import {
|
|
6
|
+
applyInstructionPlan,
|
|
7
|
+
planProjectInstructionSetup,
|
|
8
|
+
} from "../init-instructions";
|
|
5
9
|
import {
|
|
6
10
|
parseManifest,
|
|
7
11
|
pinProject,
|
|
@@ -12,6 +16,11 @@ import {
|
|
|
12
16
|
validateManifest,
|
|
13
17
|
writeManifestAtomic,
|
|
14
18
|
} from "../manifest";
|
|
19
|
+
import {
|
|
20
|
+
MCP_PROJECT_REGISTRABLE_AGENTS,
|
|
21
|
+
registerMcpServer,
|
|
22
|
+
type McpRegistrationResult,
|
|
23
|
+
} from "../mcp-registration";
|
|
15
24
|
import { resolveProjectDirectory, suggestProjectName } from "../project-setup";
|
|
16
25
|
import {
|
|
17
26
|
parseCommaList,
|
|
@@ -19,18 +28,19 @@ import {
|
|
|
19
28
|
promptText,
|
|
20
29
|
shouldUseWizard,
|
|
21
30
|
} from "../prompts";
|
|
22
|
-
import { emitSuccess, isInteractive } from "../output";
|
|
31
|
+
import { emitSuccess, isInteractive, unknownSubcommandError } from "../output";
|
|
23
32
|
import { confirmAction, confirmIfNeeded, loadManifestContext } from "./shared";
|
|
24
33
|
import { isGlobalFlag } from "../global-flags";
|
|
25
34
|
const PROJECT_INIT_USAGE =
|
|
26
|
-
"usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--
|
|
35
|
+
"usage: skillmux project init [path] [--name <group>] [--skill <id>...] [--agent <id>...] [--target <name>...] [--register-mcp] [--yes] [--no-sync]";
|
|
27
36
|
|
|
28
37
|
interface ProjectInitArgs {
|
|
29
38
|
path: string;
|
|
30
39
|
name: string;
|
|
31
40
|
skills: string[];
|
|
32
|
-
|
|
41
|
+
agents: string[];
|
|
33
42
|
targets: string[];
|
|
43
|
+
registerMcp: boolean;
|
|
34
44
|
yes: boolean;
|
|
35
45
|
sync: boolean;
|
|
36
46
|
}
|
|
@@ -45,16 +55,16 @@ export function configuredTargetForSurface(
|
|
|
45
55
|
)?.[0];
|
|
46
56
|
}
|
|
47
57
|
|
|
48
|
-
function
|
|
58
|
+
function configuredTargetsForAgents(
|
|
49
59
|
manifest: ReturnType<typeof parseManifest>,
|
|
50
|
-
|
|
60
|
+
agents: readonly string[],
|
|
51
61
|
): string[] {
|
|
52
|
-
return
|
|
62
|
+
return planAgentSurfaces(agents).surfaces.map((surface) => {
|
|
53
63
|
const target = configuredTargetForSurface(manifest, surface);
|
|
54
64
|
if (target) return target;
|
|
55
|
-
const
|
|
65
|
+
const agent = surface.agents[0]!;
|
|
56
66
|
throw new Error(
|
|
57
|
-
`
|
|
67
|
+
`agent target for "${agent}" is not configured; run "skillmux init --agent ${agent} --yes" first`,
|
|
58
68
|
);
|
|
59
69
|
});
|
|
60
70
|
}
|
|
@@ -63,8 +73,9 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
63
73
|
let projectPath: string | undefined;
|
|
64
74
|
let name: string | undefined;
|
|
65
75
|
const skills: string[] = [];
|
|
66
|
-
const
|
|
76
|
+
const agents: string[] = [];
|
|
67
77
|
const targets: string[] = [];
|
|
78
|
+
let registerMcp = false;
|
|
68
79
|
let yes = false;
|
|
69
80
|
let sync = true;
|
|
70
81
|
|
|
@@ -81,10 +92,12 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
81
92
|
const target = args[++i];
|
|
82
93
|
if (!target) throw new Error("--target requires a name");
|
|
83
94
|
targets.push(target);
|
|
84
|
-
} else if (arg === "--
|
|
85
|
-
const
|
|
86
|
-
if (!
|
|
87
|
-
|
|
95
|
+
} else if (arg === "--agent") {
|
|
96
|
+
const agent = args[++i];
|
|
97
|
+
if (!agent) throw new Error("--agent requires a name");
|
|
98
|
+
agents.push(agent);
|
|
99
|
+
} else if (arg === "--register-mcp") {
|
|
100
|
+
registerMcp = true;
|
|
88
101
|
} else if (arg === "--yes") {
|
|
89
102
|
yes = true;
|
|
90
103
|
} else if (arg === "--no-sync") {
|
|
@@ -110,8 +123,9 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
110
123
|
path,
|
|
111
124
|
name: name ?? suggestProjectName(basename(path)),
|
|
112
125
|
skills,
|
|
113
|
-
|
|
126
|
+
agents,
|
|
114
127
|
targets,
|
|
128
|
+
registerMcp,
|
|
115
129
|
yes,
|
|
116
130
|
sync,
|
|
117
131
|
};
|
|
@@ -263,15 +277,15 @@ export async function runProject(
|
|
|
263
277
|
const group = args[0];
|
|
264
278
|
if (!group)
|
|
265
279
|
throw new Error(
|
|
266
|
-
`usage: skillmux project ${subCommand} <group> (--
|
|
280
|
+
`usage: skillmux project ${subCommand} <group> (--agent <id>... | --target <name>...) --yes`,
|
|
267
281
|
);
|
|
268
|
-
const
|
|
282
|
+
const agents: string[] = [];
|
|
269
283
|
const requestedTargets: string[] = [];
|
|
270
284
|
for (let i = 1; i < args.length; i++) {
|
|
271
|
-
if (args[i] === "--
|
|
285
|
+
if (args[i] === "--agent") {
|
|
272
286
|
const value = args[++i];
|
|
273
|
-
if (!value) throw new Error("--
|
|
274
|
-
|
|
287
|
+
if (!value) throw new Error("--agent requires a name");
|
|
288
|
+
agents.push(value);
|
|
275
289
|
} else if (args[i] === "--target") {
|
|
276
290
|
const value = args[++i];
|
|
277
291
|
if (!value) throw new Error("--target requires a name");
|
|
@@ -286,11 +300,20 @@ export async function runProject(
|
|
|
286
300
|
}
|
|
287
301
|
const { config, vaultPath, manifestPath, manifest } =
|
|
288
302
|
await loadManifestContext();
|
|
289
|
-
const
|
|
290
|
-
const targets = [...new Set([...requestedTargets, ...
|
|
303
|
+
const agentTargets = configuredTargetsForAgents(manifest, agents);
|
|
304
|
+
const targets = [...new Set([...requestedTargets, ...agentTargets])];
|
|
291
305
|
if (targets.length === 0) {
|
|
292
|
-
throw new Error(`project ${subCommand} requires --
|
|
306
|
+
throw new Error(`project ${subCommand} requires --agent or --target`);
|
|
293
307
|
}
|
|
308
|
+
// Several agents can share one target (e.g. opencode/windsurf both use
|
|
309
|
+
// agent-skills) — show the resolved directory, not just the target name,
|
|
310
|
+
// so it's clear at confirmation time which physical folder this affects.
|
|
311
|
+
const targetDirs = Object.fromEntries(
|
|
312
|
+
targets.map((t) => [t, manifest.targets[t]?.dir ?? "(unknown)"]),
|
|
313
|
+
);
|
|
314
|
+
const targetsDisplay = targets
|
|
315
|
+
.map((t) => `${t} (${targetDirs[t]})`)
|
|
316
|
+
.join(", ");
|
|
294
317
|
const updated = updateProjectTargets(manifest, group, {
|
|
295
318
|
...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
|
|
296
319
|
});
|
|
@@ -302,10 +325,10 @@ export async function runProject(
|
|
|
302
325
|
if (options.dryRun) {
|
|
303
326
|
emitSuccess(
|
|
304
327
|
{ isJson: options.isJson },
|
|
305
|
-
{ subcommand: subCommand, group, targets },
|
|
328
|
+
{ subcommand: subCommand, group, targets, target_dirs: targetDirs },
|
|
306
329
|
() =>
|
|
307
330
|
console.log(
|
|
308
|
-
`${subCommand}: [project.${group}] ${
|
|
331
|
+
`${subCommand}: [project.${group}] ${targetsDisplay} (dry-run)`,
|
|
309
332
|
),
|
|
310
333
|
);
|
|
311
334
|
return;
|
|
@@ -314,7 +337,7 @@ export async function runProject(
|
|
|
314
337
|
!(await confirmIfNeeded({
|
|
315
338
|
confirmed: args.includes("--yes"),
|
|
316
339
|
isJson: options.isJson,
|
|
317
|
-
prompt: `${subCommand} [project.${group}] to ${
|
|
340
|
+
prompt: `${subCommand} [project.${group}] to ${targetsDisplay}?`,
|
|
318
341
|
nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
|
|
319
342
|
}))
|
|
320
343
|
)
|
|
@@ -322,12 +345,23 @@ export async function runProject(
|
|
|
322
345
|
writeManifestAtomic(manifestPath, updated);
|
|
323
346
|
emitSuccess(
|
|
324
347
|
{ isJson: options.isJson },
|
|
325
|
-
{ subcommand: subCommand, group, targets },
|
|
326
|
-
() => console.log(`${subCommand}: [project.${group}] ${
|
|
348
|
+
{ subcommand: subCommand, group, targets, target_dirs: targetDirs },
|
|
349
|
+
() => console.log(`${subCommand}: [project.${group}] ${targetsDisplay}`),
|
|
327
350
|
);
|
|
328
351
|
return;
|
|
329
352
|
}
|
|
330
|
-
if (subCommand !== "init")
|
|
353
|
+
if (subCommand !== "init")
|
|
354
|
+
throw unknownSubcommandError("project", subCommand, [
|
|
355
|
+
"init",
|
|
356
|
+
"list",
|
|
357
|
+
"show",
|
|
358
|
+
"add-path",
|
|
359
|
+
"remove-path",
|
|
360
|
+
"pin",
|
|
361
|
+
"unpin",
|
|
362
|
+
"attach",
|
|
363
|
+
"detach",
|
|
364
|
+
]);
|
|
331
365
|
let request = parseProjectInitArgs(args);
|
|
332
366
|
const guided = shouldUseWizard(args, {
|
|
333
367
|
interactive: isInteractive(),
|
|
@@ -345,20 +379,20 @@ export async function runProject(
|
|
|
345
379
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
346
380
|
if (guided) {
|
|
347
381
|
const name = await promptText("Project group", request.name);
|
|
348
|
-
const
|
|
349
|
-
const surface =
|
|
382
|
+
const availableAgents = SUPPORTED_AGENT_IDS.filter((agent) => {
|
|
383
|
+
const surface = planAgentSurfaces([agent]).surfaces[0];
|
|
350
384
|
return (
|
|
351
385
|
surface !== undefined &&
|
|
352
386
|
configuredTargetForSurface(manifest, surface) !== undefined
|
|
353
387
|
);
|
|
354
388
|
});
|
|
355
|
-
const
|
|
356
|
-
"Which
|
|
357
|
-
|
|
358
|
-
value:
|
|
359
|
-
label:
|
|
389
|
+
const agents = await promptMultiSelect(
|
|
390
|
+
"Which agents should receive project skills?",
|
|
391
|
+
availableAgents.map((agent) => ({
|
|
392
|
+
value: agent,
|
|
393
|
+
label: agent,
|
|
360
394
|
selected:
|
|
361
|
-
request.
|
|
395
|
+
request.agents.length === 0 || request.agents.includes(agent),
|
|
362
396
|
})),
|
|
363
397
|
);
|
|
364
398
|
const skills = parseCommaList(
|
|
@@ -367,10 +401,25 @@ export async function runProject(
|
|
|
367
401
|
request.skills.join(","),
|
|
368
402
|
),
|
|
369
403
|
);
|
|
370
|
-
request = { ...request, name,
|
|
404
|
+
request = { ...request, name, agents, skills };
|
|
371
405
|
}
|
|
372
|
-
|
|
373
|
-
|
|
406
|
+
// Local MCP registration + instruction writing are independent of skill
|
|
407
|
+
// pins — only offered for agents with a verified project-scoped CLI
|
|
408
|
+
// command (currently just claude-code; see MCP_PROJECT_REGISTRABLE_AGENTS).
|
|
409
|
+
const registrableAgents = request.agents.filter((agent) =>
|
|
410
|
+
MCP_PROJECT_REGISTRABLE_AGENTS.includes(agent as AgentId),
|
|
411
|
+
) as AgentId[];
|
|
412
|
+
if (guided && registrableAgents.length > 0) {
|
|
413
|
+
request = {
|
|
414
|
+
...request,
|
|
415
|
+
registerMcp: await confirmAction(
|
|
416
|
+
`Also register skillmux as a project-scoped MCP server for ${registrableAgents.join(", ")}? ` +
|
|
417
|
+
`This writes ${request.path}/.mcp.json, shared via git.`,
|
|
418
|
+
),
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
const agentTargets = configuredTargetsForAgents(manifest, request.agents);
|
|
422
|
+
const targets = [...new Set([...request.targets, ...agentTargets])];
|
|
374
423
|
const updated = upsertProject(manifest, {
|
|
375
424
|
name: request.name,
|
|
376
425
|
paths: [request.path],
|
|
@@ -378,15 +427,34 @@ export async function runProject(
|
|
|
378
427
|
targets,
|
|
379
428
|
});
|
|
380
429
|
const { notes } = validateManifest(updated, vaultPath, localVaultPaths);
|
|
430
|
+
|
|
431
|
+
// The project-local instruction block only teaches an agent to call
|
|
432
|
+
// resolve_skill/fetch_skill (MCP tools) — write it only for agents that
|
|
433
|
+
// are actually getting a project-scoped MCP registration this run.
|
|
434
|
+
const mcpInstructionAgents = request.registerMcp ? registrableAgents : [];
|
|
435
|
+
const instructionPlan = planProjectInstructionSetup(
|
|
436
|
+
mcpInstructionAgents,
|
|
437
|
+
request.path,
|
|
438
|
+
);
|
|
439
|
+
const hasInstructionWrites = instructionPlan.changes.some(
|
|
440
|
+
(change) => change.status !== "unchanged",
|
|
441
|
+
);
|
|
442
|
+
|
|
381
443
|
const plan = {
|
|
382
444
|
mode: "project",
|
|
383
445
|
project: request.name,
|
|
384
446
|
path: request.path,
|
|
385
447
|
skills: request.skills,
|
|
386
|
-
|
|
448
|
+
agents: request.agents,
|
|
387
449
|
targets,
|
|
388
450
|
sync: request.sync,
|
|
389
451
|
notes,
|
|
452
|
+
instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
|
|
453
|
+
path,
|
|
454
|
+
agents,
|
|
455
|
+
status,
|
|
456
|
+
})),
|
|
457
|
+
register_mcp_for: mcpInstructionAgents,
|
|
390
458
|
};
|
|
391
459
|
|
|
392
460
|
if (options.dryRun) {
|
|
@@ -401,8 +469,14 @@ export async function runProject(
|
|
|
401
469
|
console.log("\nReview");
|
|
402
470
|
console.log(` project: ${request.name}`);
|
|
403
471
|
console.log(` path: ${request.path}`);
|
|
404
|
-
console.log(`
|
|
472
|
+
console.log(` agents: ${request.agents.join(", ") || "(none)"}`);
|
|
405
473
|
console.log(` skills: ${request.skills.join(", ") || "(none)"}`);
|
|
474
|
+
console.log(
|
|
475
|
+
` instructions: ${instructionPlan.changes.filter((change) => change.status !== "unchanged").length} file(s)`,
|
|
476
|
+
);
|
|
477
|
+
console.log(
|
|
478
|
+
` MCP registration: ${mcpInstructionAgents.join(", ") || "(none)"}`,
|
|
479
|
+
);
|
|
406
480
|
console.log(` sync: ${request.sync ? "yes" : "no"}`);
|
|
407
481
|
}
|
|
408
482
|
if (
|
|
@@ -421,6 +495,17 @@ export async function runProject(
|
|
|
421
495
|
}
|
|
422
496
|
|
|
423
497
|
writeManifestAtomic(manifestPath, updated);
|
|
498
|
+
if (hasInstructionWrites) {
|
|
499
|
+
try {
|
|
500
|
+
applyInstructionPlan(instructionPlan);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
throw new Error(
|
|
503
|
+
`project configuration was saved, but writing instruction files failed: ${
|
|
504
|
+
error instanceof Error ? error.message : String(error)
|
|
505
|
+
}`,
|
|
506
|
+
);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
424
509
|
if (request.sync) {
|
|
425
510
|
try {
|
|
426
511
|
// Reaching here already required approval above (request.yes, or an
|
|
@@ -437,7 +522,39 @@ export async function runProject(
|
|
|
437
522
|
);
|
|
438
523
|
}
|
|
439
524
|
}
|
|
440
|
-
|
|
441
|
-
|
|
525
|
+
|
|
526
|
+
// Best-effort and outside the checks above: this mutates another tool's
|
|
527
|
+
// own config, not skillmux's, so a registration failure is reported, never
|
|
528
|
+
// rolled back — the successful project setup above still stands either way.
|
|
529
|
+
const mcpRegistrations: McpRegistrationResult[] = [];
|
|
530
|
+
if (request.registerMcp) {
|
|
531
|
+
for (const agent of registrableAgents) {
|
|
532
|
+
mcpRegistrations.push(
|
|
533
|
+
await registerMcpServer(agent, { scope: "project", cwd: request.path }),
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
emitSuccess(
|
|
539
|
+
{ isJson: options.isJson },
|
|
540
|
+
{
|
|
541
|
+
result: {
|
|
542
|
+
...plan,
|
|
543
|
+
instructions_changed: instructionPlan.changes
|
|
544
|
+
.filter((change) => change.status !== "unchanged")
|
|
545
|
+
.map((change) => change.path),
|
|
546
|
+
mcp_registrations: mcpRegistrations,
|
|
547
|
+
},
|
|
548
|
+
},
|
|
549
|
+
() => {
|
|
550
|
+
console.log(`project "${request.name}" ready at ${request.path}`);
|
|
551
|
+
for (const registration of mcpRegistrations) {
|
|
552
|
+
console.log(
|
|
553
|
+
registration.ok
|
|
554
|
+
? `MCP registered: ${registration.agent} (project scope)`
|
|
555
|
+
: `MCP registration failed for ${registration.agent}: ${registration.error}`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
},
|
|
442
559
|
);
|
|
443
560
|
}
|
package/src/commands/report.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Database } from "bun:sqlite";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ContextAdapter } from "../adapters";
|
|
3
3
|
import type { ResolvedContext } from "../context";
|
|
4
4
|
import { emitSuccess } from "../output";
|
|
5
5
|
import { getStats, renderStatsText } from "../stats";
|
|
@@ -36,14 +36,14 @@ function parseReportArgs(args: string[]): {
|
|
|
36
36
|
|
|
37
37
|
export async function runReport(
|
|
38
38
|
args: string[],
|
|
39
|
-
options: { isJson: boolean;
|
|
39
|
+
options: { isJson: boolean; context: ResolvedContext; allowInsecure: boolean; adapter: ContextAdapter },
|
|
40
40
|
): Promise<void> {
|
|
41
41
|
const { db: dbPath, since } = parseReportArgs(args);
|
|
42
42
|
if (!since)
|
|
43
43
|
throw new Error(
|
|
44
44
|
"usage: skillmux report [--context <name> | --server <url> | --db <path>] --since <window> [--json]",
|
|
45
45
|
);
|
|
46
|
-
if (dbPath && options.
|
|
46
|
+
if (dbPath && options.context.type === "remote")
|
|
47
47
|
throw new Error("--db and --context/--server are mutually exclusive");
|
|
48
48
|
|
|
49
49
|
if (dbPath) {
|
package/src/commands/shared.ts
CHANGED
|
@@ -1,21 +1,14 @@
|
|
|
1
|
-
import { createInterface } from "node:readline/promises";
|
|
2
1
|
import { expandHome, loadConfig } from "../config";
|
|
3
2
|
import { parseManifest, resolveManifestPath } from "../manifest";
|
|
4
3
|
import { isInteractive } from "../output";
|
|
4
|
+
import { askQuestion, type PromptIO } from "../prompts";
|
|
5
5
|
|
|
6
|
-
export async function confirmAction(
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
const answer = (await readline.question(`${prompt} [y/N] `))
|
|
13
|
-
.trim()
|
|
14
|
-
.toLowerCase();
|
|
15
|
-
return answer === "y" || answer === "yes";
|
|
16
|
-
} finally {
|
|
17
|
-
readline.close();
|
|
18
|
-
}
|
|
6
|
+
export async function confirmAction(
|
|
7
|
+
prompt: string,
|
|
8
|
+
io: PromptIO = {},
|
|
9
|
+
): Promise<boolean> {
|
|
10
|
+
const answer = (await askQuestion(`${prompt} [y/N] `, io)).trim().toLowerCase();
|
|
11
|
+
return answer === "y" || answer === "yes";
|
|
19
12
|
}
|
|
20
13
|
|
|
21
14
|
export async function loadManifestContext() {
|
package/src/commands/skill.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { expandHome, loadConfig } from "../config";
|
|
4
|
+
import { unknownSubcommandError } from "../output";
|
|
4
5
|
import { vaultResolutionOrder } from "../vault";
|
|
5
6
|
|
|
6
7
|
export async function runSkill(subCommand: string, args: string[]): Promise<void> {
|
|
7
|
-
if (subCommand !== "which") throw
|
|
8
|
+
if (subCommand !== "which") throw unknownSubcommandError("skill", subCommand, ["which"]);
|
|
8
9
|
await runWhich(args);
|
|
9
10
|
}
|
|
10
11
|
|
package/src/commands/target.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { expandHome } from "../config";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
BUILT_IN_TARGET_NAMES,
|
|
4
|
+
planAgentSurfaces,
|
|
5
|
+
resolveBuiltInTarget,
|
|
6
|
+
SUPPORTED_AGENT_IDS,
|
|
7
|
+
} from "../init-agents";
|
|
3
8
|
import { planInitManifest, applyInit } from "../init";
|
|
4
9
|
import { writeManifestAtomic } from "../manifest";
|
|
5
|
-
import { emitSuccess } from "../output";
|
|
10
|
+
import { emitSuccess, unknownSubcommandError } from "../output";
|
|
6
11
|
import { confirmIfNeeded, loadManifestContext } from "./shared";
|
|
12
|
+
|
|
7
13
|
export async function runTarget(
|
|
8
14
|
subCommand: string,
|
|
9
15
|
args: string[],
|
|
@@ -19,11 +25,11 @@ export async function runTarget(
|
|
|
19
25
|
}
|
|
20
26
|
const targets = names.map((name) => {
|
|
21
27
|
const target = manifest.targets[name]!;
|
|
22
|
-
const
|
|
23
|
-
const surface =
|
|
28
|
+
const agents = SUPPORTED_AGENT_IDS.filter((agent) => {
|
|
29
|
+
const surface = planAgentSurfaces([agent]).surfaces[0];
|
|
24
30
|
return surface !== undefined && surface.path === expandHome(target.dir);
|
|
25
31
|
});
|
|
26
|
-
return { name, ...target,
|
|
32
|
+
return { name, ...target, agents };
|
|
27
33
|
});
|
|
28
34
|
emitSuccess({ isJson: options.isJson }, { targets }, () => {
|
|
29
35
|
if (targets.length === 0) {
|
|
@@ -33,7 +39,7 @@ export async function runTarget(
|
|
|
33
39
|
console.log(`${target.name}:`);
|
|
34
40
|
console.log(` dir: ${target.dir}`);
|
|
35
41
|
console.log(` host: ${target.host ?? "(global)"}`);
|
|
36
|
-
console.log(`
|
|
42
|
+
console.log(` agents: ${target.agents.join(", ") || "(custom)"}`);
|
|
37
43
|
console.log(
|
|
38
44
|
` projects: ${target.project_groups.join(", ") || "(none)"}`,
|
|
39
45
|
);
|
|
@@ -47,9 +53,21 @@ export async function runTarget(
|
|
|
47
53
|
const name = args[0];
|
|
48
54
|
const dirIndex = args.indexOf("--dir");
|
|
49
55
|
const rawPath = dirIndex === -1 ? undefined : args[dirIndex + 1];
|
|
50
|
-
if (!name
|
|
56
|
+
if (!name)
|
|
51
57
|
throw new Error("usage: skillmux target add <name> --dir <dir> --yes");
|
|
52
|
-
|
|
58
|
+
|
|
59
|
+
let path: string;
|
|
60
|
+
if (rawPath) {
|
|
61
|
+
path = expandHome(rawPath);
|
|
62
|
+
} else if (BUILT_IN_TARGET_NAMES.has(name)) {
|
|
63
|
+
path = resolveBuiltInTarget(name, {
|
|
64
|
+
codexHome: process.env.CODEX_HOME ? expandHome(process.env.CODEX_HOME) : undefined,
|
|
65
|
+
}).path;
|
|
66
|
+
} else {
|
|
67
|
+
throw new Error(
|
|
68
|
+
"usage: skillmux target add <name> --dir <dir> --yes (--dir may be omitted for built-in target names: agent-skills, claude-code, codex)",
|
|
69
|
+
);
|
|
70
|
+
}
|
|
53
71
|
if (options.dryRun) {
|
|
54
72
|
const planned = planInitManifest(vaultPath, [{ name, dir: path }], []);
|
|
55
73
|
emitSuccess(
|
|
@@ -118,5 +136,5 @@ export async function runTarget(
|
|
|
118
136
|
return;
|
|
119
137
|
}
|
|
120
138
|
|
|
121
|
-
throw
|
|
139
|
+
throw unknownSubcommandError("target", subCommand, ["list", "show", "add", "remove"]);
|
|
122
140
|
}
|
package/src/completions.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { MANAGED_PINS_AGENT_IDS, SUPPORTED_AGENT_IDS } from "./init-agents";
|
|
2
|
+
|
|
1
3
|
export type ShellType = "bash" | "zsh" | "fish";
|
|
2
4
|
|
|
3
5
|
const TOP_LEVEL_COMMANDS: { name: string; description: string }[] = [
|
|
@@ -6,7 +8,7 @@ const TOP_LEVEL_COMMANDS: { name: string; description: string }[] = [
|
|
|
6
8
|
{ name: "serve", description: "Start MCP server" },
|
|
7
9
|
{ name: "index", description: "Rebuild local search index" },
|
|
8
10
|
{ name: "sync", description: "Synchronize vault skills" },
|
|
9
|
-
{ name: "init", description: "Configure this machine and its
|
|
11
|
+
{ name: "init", description: "Configure this machine and its agents" },
|
|
10
12
|
{ name: "project", description: "Configure project-scoped skills" },
|
|
11
13
|
{ name: "target", description: "Manage advanced skill-delivery targets" },
|
|
12
14
|
{ name: "core", description: "Pin/unpin skills into [core]" },
|
|
@@ -62,18 +64,25 @@ _skillmux_completions() {
|
|
|
62
64
|
local-vault)
|
|
63
65
|
COMPREPLY=( $(compgen -W "init" -- "$cur") )
|
|
64
66
|
;;
|
|
65
|
-
|
|
66
|
-
COMPREPLY=( $(compgen -W "
|
|
67
|
+
eval)
|
|
68
|
+
COMPREPLY=( $(compgen -W "promote" -- "$cur") )
|
|
67
69
|
;;
|
|
68
|
-
--
|
|
69
|
-
|
|
70
|
+
--agent)
|
|
71
|
+
if [ "\${COMP_WORDS[1]}" = "project" ]; then
|
|
72
|
+
COMPREPLY=( $(compgen -W "${MANAGED_PINS_AGENT_IDS.join(" ")}" -- "$cur") )
|
|
73
|
+
else
|
|
74
|
+
COMPREPLY=( $(compgen -W "${SUPPORTED_AGENT_IDS.join(" ")}" -- "$cur") )
|
|
75
|
+
fi
|
|
70
76
|
;;
|
|
71
77
|
esac
|
|
72
78
|
if [ "\${COMP_WORDS[1]}" = "init" ] && [ "\${#COMPREPLY[@]}" -eq 0 ]; then
|
|
73
|
-
COMPREPLY=( $(compgen -W "--
|
|
79
|
+
COMPREPLY=( $(compgen -W "--agent --vault --core --migrate-full-vault --show-mcp-setup --register-mcp --no-instructions --no-sync --interactive --yes --dry-run --json" -- "$cur") )
|
|
74
80
|
fi
|
|
75
81
|
if [ "\${COMP_WORDS[1]}" = "project" ] && [ "\${COMP_WORDS[2]}" = "init" ]; then
|
|
76
|
-
COMPREPLY=( $(compgen -W "--name --skill --
|
|
82
|
+
COMPREPLY=( $(compgen -W "--name --skill --agent --target --register-mcp --no-sync --interactive --yes --dry-run --json" -- "$cur") )
|
|
83
|
+
fi
|
|
84
|
+
if [ "\${COMP_WORDS[1]}" = "eval" ] && [ "\${COMP_WORDS[2]}" = "promote" ]; then
|
|
85
|
+
COMPREPLY=( $(compgen -W "--since --out --dry-run --yes --json" -- "$cur") )
|
|
77
86
|
fi
|
|
78
87
|
}
|
|
79
88
|
complete -F _skillmux_completions skillmux
|
|
@@ -92,12 +101,12 @@ ${commands}
|
|
|
92
101
|
_describe -t commands 'skillmux command' commands
|
|
93
102
|
elif [[ "$words[2]" == "init" ]]; then
|
|
94
103
|
_arguments \\
|
|
95
|
-
'*--
|
|
96
|
-
'*--target[select a delivery target]:target:(agent-skills claude-code codex custom)' \\
|
|
97
|
-
'--dir[custom target directory]:directory:_directories' \\
|
|
104
|
+
'*--agent[select an agent]:agent:(${SUPPORTED_AGENT_IDS.join(" ")})' \\
|
|
98
105
|
'--vault[vault directory]:directory:_directories' \\
|
|
99
106
|
'*--core[seed a core skill]:skill id:' \\
|
|
100
107
|
'--migrate-full-vault[convert a full-vault symlink to managed pins]' \\
|
|
108
|
+
'--show-mcp-setup[also print the MCP registration snippet]' \\
|
|
109
|
+
'--register-mcp[register skillmux via the agent own CLI]' \\
|
|
101
110
|
'--no-instructions[skip managed instruction files]' \\
|
|
102
111
|
'--no-sync[save setup without synchronizing targets]' \\
|
|
103
112
|
'--interactive[force guided setup]' \\
|
|
@@ -109,8 +118,9 @@ ${commands}
|
|
|
109
118
|
'1:project directory:_directories' \\
|
|
110
119
|
'--name[project group name]:group:' \\
|
|
111
120
|
'*--skill[project skill]:skill id:' \\
|
|
112
|
-
'*--
|
|
121
|
+
'*--agent[select an agent]:agent:(${MANAGED_PINS_AGENT_IDS.join(" ")})' \\
|
|
113
122
|
'*--target[select an advanced target]:target:' \\
|
|
123
|
+
'--register-mcp[register a project-scoped MCP server for claude-code]' \\
|
|
114
124
|
'--no-sync[save setup without synchronizing targets]' \\
|
|
115
125
|
'--interactive[force guided setup]' \\
|
|
116
126
|
'--yes[apply without prompts]' \\
|
|
@@ -118,6 +128,15 @@ ${commands}
|
|
|
118
128
|
'--json[emit a JSON envelope]'
|
|
119
129
|
elif [[ "$words[2]" == "project" && CURRENT == 3 ]]; then
|
|
120
130
|
_values 'project command' init list show add-path remove-path pin unpin attach detach
|
|
131
|
+
elif [[ "$words[2]" == "eval" && "$words[3]" == "promote" ]]; then
|
|
132
|
+
_arguments \
|
|
133
|
+
'--since[time window]:window:' \
|
|
134
|
+
'--out[output file]:file:_files' \
|
|
135
|
+
'--dry-run[print the plan without writing]' \
|
|
136
|
+
'--yes[apply without prompts]' \
|
|
137
|
+
'--json[emit a JSON envelope]'
|
|
138
|
+
elif [[ "$words[2]" == "eval" && CURRENT == 3 ]]; then
|
|
139
|
+
_values 'eval command' promote
|
|
121
140
|
elif [[ "$words[2]" == "target" && CURRENT == 3 ]]; then
|
|
122
141
|
_values 'target command' list show add remove
|
|
123
142
|
elif [[ "$words[2]" == "skill" && CURRENT == 3 ]]; then
|
|
@@ -139,12 +158,12 @@ _skillmux "$@"
|
|
|
139
158
|
return `# fish completion for skillmux
|
|
140
159
|
complete -c skillmux -f
|
|
141
160
|
${topLevel}
|
|
142
|
-
complete -c skillmux -n "__fish_seen_subcommand_from init" -l
|
|
143
|
-
complete -c skillmux -n "__fish_seen_subcommand_from init" -l target -x -a "agent-skills claude-code codex custom" -d "Select a delivery target"
|
|
144
|
-
complete -c skillmux -n "__fish_seen_subcommand_from init" -l dir -r -d "Custom target directory"
|
|
161
|
+
complete -c skillmux -n "__fish_seen_subcommand_from init" -l agent -x -a "${SUPPORTED_AGENT_IDS.join(" ")}" -d "Select an agent"
|
|
145
162
|
complete -c skillmux -n "__fish_seen_subcommand_from init" -l vault -r -d "Vault directory"
|
|
146
163
|
complete -c skillmux -n "__fish_seen_subcommand_from init" -l core -x -d "Seed a core skill"
|
|
147
164
|
complete -c skillmux -n "__fish_seen_subcommand_from init" -l migrate-full-vault -d "Convert a full-vault symlink"
|
|
165
|
+
complete -c skillmux -n "__fish_seen_subcommand_from init" -l show-mcp-setup -d "Also print the MCP registration snippet"
|
|
166
|
+
complete -c skillmux -n "__fish_seen_subcommand_from init" -l register-mcp -d "Register skillmux via the agent's own CLI"
|
|
148
167
|
complete -c skillmux -n "__fish_seen_subcommand_from init" -l no-instructions -d "Skip managed instruction files"
|
|
149
168
|
complete -c skillmux -n "__fish_seen_subcommand_from init" -l no-sync -d "Save without synchronizing"
|
|
150
169
|
complete -c skillmux -n "__fish_seen_subcommand_from init" -l interactive -d "Force guided setup"
|
|
@@ -154,11 +173,18 @@ complete -c skillmux -n "__fish_seen_subcommand_from init" -l json -d "Emit a JS
|
|
|
154
173
|
complete -c skillmux -n "__fish_seen_subcommand_from project" -a "init list show add-path remove-path pin unpin attach detach" -d "Manage projects"
|
|
155
174
|
complete -c skillmux -n "__fish_seen_subcommand_from project" -l name -x -d "Project group name"
|
|
156
175
|
complete -c skillmux -n "__fish_seen_subcommand_from project" -l skill -x -d "Project skill"
|
|
157
|
-
complete -c skillmux -n "__fish_seen_subcommand_from project" -l
|
|
176
|
+
complete -c skillmux -n "__fish_seen_subcommand_from project" -l agent -x -a "${MANAGED_PINS_AGENT_IDS.join(" ")}" -d "Select an agent"
|
|
158
177
|
complete -c skillmux -n "__fish_seen_subcommand_from project" -l target -x -d "Select an advanced delivery target"
|
|
178
|
+
complete -c skillmux -n "__fish_seen_subcommand_from project" -l register-mcp -d "Register a project-scoped MCP server for claude-code"
|
|
159
179
|
complete -c skillmux -n "__fish_seen_subcommand_from project" -l no-sync -d "Save without synchronizing"
|
|
160
180
|
complete -c skillmux -n "__fish_seen_subcommand_from project" -l interactive -d "Force guided setup"
|
|
161
181
|
complete -c skillmux -n "__fish_seen_subcommand_from project" -l yes -d "Apply without prompts"
|
|
182
|
+
complete -c skillmux -n "__fish_seen_subcommand_from eval" -a "promote" -d "Promote correlated fetches into eval cases"
|
|
183
|
+
complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l since -x -d "Time window"
|
|
184
|
+
complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l out -r -d "Output file"
|
|
185
|
+
complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l dry-run -d "Print the plan without writing"
|
|
186
|
+
complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l yes -d "Apply without prompts"
|
|
187
|
+
complete -c skillmux -n "__fish_seen_subcommand_from eval; and __fish_seen_subcommand_from promote" -l json -d "Emit a JSON envelope"
|
|
162
188
|
complete -c skillmux -n "__fish_seen_subcommand_from target" -a "list show add remove" -d "Manage targets"
|
|
163
189
|
complete -c skillmux -n "__fish_seen_subcommand_from core" -a "pin unpin" -d "Manage [core] pins"
|
|
164
190
|
complete -c skillmux -n "__fish_seen_subcommand_from skill" -a "which" -d "Show which root resolves a skill_id"
|
package/src/config-service.ts
CHANGED
|
@@ -309,7 +309,7 @@ export async function getDottedKey(key: string, configPath?: string): Promise<un
|
|
|
309
309
|
export async function setDottedKey(
|
|
310
310
|
key: string,
|
|
311
311
|
rawValStr: string,
|
|
312
|
-
opts?: { configPath?: string; dryRun?: boolean;
|
|
312
|
+
opts?: { configPath?: string; dryRun?: boolean; contextName?: string }
|
|
313
313
|
): Promise<SetConfigResult> {
|
|
314
314
|
validateDottedKey(key);
|
|
315
315
|
if (isEnvMasked(key)) {
|
|
@@ -317,7 +317,7 @@ export async function setDottedKey(
|
|
|
317
317
|
}
|
|
318
318
|
|
|
319
319
|
const path = opts?.configPath ?? process.env.SKILLMUX_CONFIG ?? DEFAULT_CONFIG_PATH;
|
|
320
|
-
const
|
|
320
|
+
const contextName = opts?.contextName ?? "local";
|
|
321
321
|
|
|
322
322
|
const { effective: priorEffective, rawToml } = await getEffectiveConfig(path);
|
|
323
323
|
const priorVal = getNestedValue(priorEffective as Record<string, any>, key);
|
|
@@ -364,7 +364,7 @@ export async function setDottedKey(
|
|
|
364
364
|
key,
|
|
365
365
|
prior_val: priorVal,
|
|
366
366
|
resulting_val: parsedVal,
|
|
367
|
-
target:
|
|
367
|
+
target: contextName,
|
|
368
368
|
prior_revision: priorRevision,
|
|
369
369
|
resulting_revision: resultingRevision,
|
|
370
370
|
persistence,
|