@klhapp/skillmux 1.9.3 → 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 +46 -0
- package/README.md +19 -19
- package/docs/README.md +4 -4
- package/docs/assets/architecture-dark.svg +39 -32
- package/docs/assets/architecture-light.svg +25 -18
- package/docs/cli.md +147 -36
- package/docs/concepts.md +11 -11
- package/docs/configuration.md +7 -5
- package/docs/deployment.md +10 -6
- package/docs/getting-started.md +18 -14
- package/docs/mcp-routing.md +1 -1
- package/docs/skill-management.md +17 -11
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/src/adapters.ts +157 -11
- package/src/cli.ts +396 -1319
- package/src/commands/audit.ts +53 -56
- package/src/commands/config.ts +33 -26
- package/src/commands/context.ts +104 -0
- package/src/commands/core.ts +7 -3
- package/src/commands/doctor.ts +97 -0
- package/src/commands/eval.ts +22 -15
- package/src/commands/init.ts +672 -0
- package/src/commands/install.ts +132 -0
- package/src/commands/local-vault.ts +60 -0
- package/src/commands/models.ts +10 -0
- package/src/commands/outdated.ts +2 -1
- package/src/commands/project.ts +194 -51
- package/src/commands/report.ts +66 -0
- package/src/commands/scan.ts +61 -0
- package/src/commands/shared.ts +7 -14
- package/src/commands/skill.ts +33 -0
- package/src/commands/sync.ts +232 -0
- package/src/commands/target.ts +45 -15
- package/src/commands/update.ts +2 -1
- package/src/completions.ts +41 -15
- package/src/config-service.ts +4 -54
- package/src/context.ts +8 -3
- package/src/db-audit.ts +286 -0
- package/src/db-index.ts +238 -0
- package/src/db.ts +3 -521
- package/src/global-flags.ts +46 -0
- package/src/init-agents.ts +329 -0
- package/src/init-instructions.ts +47 -28
- package/src/logger.ts +26 -0
- package/src/mcp-registration.ts +89 -0
- package/src/output.ts +80 -18
- package/src/prompts.ts +75 -20
- package/src/router-core.ts +8 -27
- package/src/scan.ts +19 -19
- package/src/server.ts +161 -14
- package/src/toml-writer.ts +51 -0
- package/src/init-clients.ts +0 -220
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { expandHome, loadConfig } from "../config";
|
|
4
|
+
import {
|
|
5
|
+
assertHostAllowed,
|
|
6
|
+
cloneToTemp,
|
|
7
|
+
deriveRepoName,
|
|
8
|
+
installIntoVault,
|
|
9
|
+
isLocalFileUrl,
|
|
10
|
+
resolveCloneCommit,
|
|
11
|
+
resolveRepoSource,
|
|
12
|
+
resolveSkillDir,
|
|
13
|
+
validateSkillCandidate,
|
|
14
|
+
} from "../install";
|
|
15
|
+
import { emitSuccess } from "../output";
|
|
16
|
+
import { hashSkillContent, writeSkillOrigin } from "../provenance";
|
|
17
|
+
import { renderScanText, scanExitCode, type ScanSeverity } from "../scan";
|
|
18
|
+
import { isGlobalFlag } from "../global-flags";
|
|
19
|
+
|
|
20
|
+
function parseInstallArgs(args: string[]): {
|
|
21
|
+
repo?: string;
|
|
22
|
+
force: boolean;
|
|
23
|
+
dryRun: boolean;
|
|
24
|
+
failOn?: ScanSeverity;
|
|
25
|
+
allowLocalSource: boolean;
|
|
26
|
+
} {
|
|
27
|
+
let repo: string | undefined;
|
|
28
|
+
let force = false;
|
|
29
|
+
let dryRun = false;
|
|
30
|
+
let failOn: ScanSeverity | undefined;
|
|
31
|
+
let allowLocalSource = false;
|
|
32
|
+
for (let i = 0; i < args.length; i++) {
|
|
33
|
+
const option = args[i];
|
|
34
|
+
if (option === "--force") force = true;
|
|
35
|
+
else if (option === "--dry-run") dryRun = true;
|
|
36
|
+
else if (option === "--allow-local-source") allowLocalSource = true;
|
|
37
|
+
else if (option === "--fail-on") {
|
|
38
|
+
const value = args[++i];
|
|
39
|
+
if (value !== "low" && value !== "medium" && value !== "high") {
|
|
40
|
+
throw new Error("--fail-on must be low, medium, or high");
|
|
41
|
+
}
|
|
42
|
+
failOn = value;
|
|
43
|
+
} else if (isGlobalFlag(option, "--json", "--verbose")) {
|
|
44
|
+
// handled globally by main()'s isJson/isVerbose flags; recognized here so they aren't rejected
|
|
45
|
+
} else if (option?.startsWith("--")) {
|
|
46
|
+
throw new Error(`unknown install option: ${option}`);
|
|
47
|
+
} else if (repo !== undefined) {
|
|
48
|
+
throw new Error("skillmux install accepts at most one <repo> argument");
|
|
49
|
+
} else {
|
|
50
|
+
repo = option;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { repo, force, dryRun, failOn, allowLocalSource };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runInstall(
|
|
57
|
+
args: string[],
|
|
58
|
+
options: { isJson: boolean },
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
const { repo, force, dryRun, failOn, allowLocalSource } = parseInstallArgs(args);
|
|
61
|
+
if (!repo) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
"usage: skillmux install <repo>[/path] [--force] [--fail-on low|medium|high] [--dry-run] [--allow-local-source] [--json]",
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const source = resolveRepoSource(repo);
|
|
68
|
+
if (!allowLocalSource && isLocalFileUrl(source.url)) {
|
|
69
|
+
throw new Error(
|
|
70
|
+
`"${repo}" is a local (file://) source — pass --allow-local-source to install from it`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const config = await loadConfig();
|
|
74
|
+
assertHostAllowed(source.url, config.egress?.allowed_hosts);
|
|
75
|
+
const cloneDir = await cloneToTemp(source.url);
|
|
76
|
+
try {
|
|
77
|
+
const resolved = resolveSkillDir(
|
|
78
|
+
cloneDir,
|
|
79
|
+
deriveRepoName(source.url),
|
|
80
|
+
source.skillPath,
|
|
81
|
+
);
|
|
82
|
+
const { findings } = await validateSkillCandidate(
|
|
83
|
+
resolved.skillId,
|
|
84
|
+
resolved.dir,
|
|
85
|
+
);
|
|
86
|
+
if (!options.isJson) console.log(renderScanText({ scanned: 1, findings }));
|
|
87
|
+
|
|
88
|
+
if (scanExitCode(findings, failOn) !== 0) {
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
console.error(
|
|
91
|
+
`aborting install: a finding met the --fail-on ${failOn} threshold`,
|
|
92
|
+
);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const vaultPath = expandHome(config.vault_path);
|
|
97
|
+
if (dryRun) {
|
|
98
|
+
const plannedPath = join(vaultPath, resolved.skillId);
|
|
99
|
+
emitSuccess(
|
|
100
|
+
{ isJson: options.isJson },
|
|
101
|
+
{ skill_id: resolved.skillId, would_install_at: plannedPath },
|
|
102
|
+
() =>
|
|
103
|
+
console.log(
|
|
104
|
+
`dry-run: would install "${resolved.skillId}" into ${plannedPath}`,
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const commit = resolveCloneCommit(cloneDir);
|
|
111
|
+
const targetDir = installIntoVault(
|
|
112
|
+
vaultPath,
|
|
113
|
+
resolved.skillId,
|
|
114
|
+
resolved.dir,
|
|
115
|
+
force,
|
|
116
|
+
);
|
|
117
|
+
writeSkillOrigin(targetDir, {
|
|
118
|
+
source_url: source.url,
|
|
119
|
+
skill_path: source.skillPath,
|
|
120
|
+
commit,
|
|
121
|
+
installed_at: new Date().toISOString(),
|
|
122
|
+
content_hash: hashSkillContent(targetDir),
|
|
123
|
+
});
|
|
124
|
+
emitSuccess(
|
|
125
|
+
{ isJson: options.isJson },
|
|
126
|
+
{ skill_id: resolved.skillId, installed_at: targetDir },
|
|
127
|
+
() => console.log(`installed "${resolved.skillId}" into ${targetDir}`),
|
|
128
|
+
);
|
|
129
|
+
} finally {
|
|
130
|
+
rmSync(cloneDir, { recursive: true, force: true });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { expandHome, loadConfig } from "../config";
|
|
4
|
+
import { emitSuccess } from "../output";
|
|
5
|
+
import { writeLocalVaultMarker } from "../sync";
|
|
6
|
+
import { confirmIfNeeded } from "./shared";
|
|
7
|
+
|
|
8
|
+
export async function runLocalVaultInit(
|
|
9
|
+
args: string[],
|
|
10
|
+
options: { isJson: boolean; dryRun: boolean },
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const path = args[0];
|
|
13
|
+
if (!path) throw new Error("usage: skillmux local-vault init <path> --yes");
|
|
14
|
+
const expanded = expandHome(path);
|
|
15
|
+
const config = await loadConfig();
|
|
16
|
+
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
17
|
+
if (!localVaultPaths.includes(expanded)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`"${path}" is not one of the configured local_vault_paths — add it to config.toml first`,
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
if (!existsSync(expanded)) throw new Error(`"${path}" does not exist`);
|
|
23
|
+
const markerPath = join(expanded, ".skillmux");
|
|
24
|
+
if (options.dryRun) {
|
|
25
|
+
emitSuccess(
|
|
26
|
+
{ isJson: options.isJson },
|
|
27
|
+
{
|
|
28
|
+
marker_path: markerPath,
|
|
29
|
+
vault_path: expandHome(config.vault_path),
|
|
30
|
+
},
|
|
31
|
+
() =>
|
|
32
|
+
console.log(
|
|
33
|
+
`local-vault init: ${markerPath} (role: local_vault, vault_path: ${expandHome(config.vault_path)}) (dry-run)`,
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
!(await confirmIfNeeded({
|
|
40
|
+
confirmed: args.includes("--yes"),
|
|
41
|
+
isJson: options.isJson,
|
|
42
|
+
prompt: `mark ${expanded} as a local_vault (role: local_vault, vault_path: ${expandHome(config.vault_path)})?`,
|
|
43
|
+
nonInteractiveError:
|
|
44
|
+
"skillmux local-vault init requires --yes when run non-interactively",
|
|
45
|
+
}))
|
|
46
|
+
)
|
|
47
|
+
return;
|
|
48
|
+
writeLocalVaultMarker(expanded, expandHome(config.vault_path));
|
|
49
|
+
emitSuccess(
|
|
50
|
+
{ isJson: options.isJson },
|
|
51
|
+
{
|
|
52
|
+
marker_path: markerPath,
|
|
53
|
+
vault_path: expandHome(config.vault_path),
|
|
54
|
+
},
|
|
55
|
+
() =>
|
|
56
|
+
console.log(
|
|
57
|
+
`wrote ${markerPath} (role: local_vault, vault_path: ${expandHome(config.vault_path)})`,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { loadConfig } from "../config";
|
|
2
|
+
import { downloadLocalModels } from "../models";
|
|
3
|
+
import { emitSuccess } from "../output";
|
|
4
|
+
|
|
5
|
+
export async function runModelDownload(options: { isJson: boolean }): Promise<void> {
|
|
6
|
+
const cacheDir = await downloadLocalModels(await loadConfig());
|
|
7
|
+
emitSuccess({ isJson: options.isJson }, { cache_dir: cacheDir }, () =>
|
|
8
|
+
console.log(`models ready in ${cacheDir}`),
|
|
9
|
+
);
|
|
10
|
+
}
|
package/src/commands/outdated.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { assertHostAllowed, isLocalFileUrl, remoteHeadCommit } from "../install"
|
|
|
5
5
|
import { emitSuccess } from "../output";
|
|
6
6
|
import { readSkillOrigin } from "../provenance";
|
|
7
7
|
import { SKILL_ID_PATTERN } from "../vault";
|
|
8
|
+
import { isGlobalFlag } from "../global-flags";
|
|
8
9
|
|
|
9
10
|
export interface OutdatedCheckResult {
|
|
10
11
|
skill_id: string;
|
|
@@ -87,7 +88,7 @@ export async function checkOutdated(
|
|
|
87
88
|
export async function runOutdated(args: string[], options: { isJson: boolean }): Promise<void> {
|
|
88
89
|
let allowLocalSource = false;
|
|
89
90
|
for (const arg of args) {
|
|
90
|
-
if (arg
|
|
91
|
+
if (isGlobalFlag(arg, "--json")) continue;
|
|
91
92
|
if (arg === "--allow-local-source") {
|
|
92
93
|
allowLocalSource = true;
|
|
93
94
|
continue;
|
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,17 +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";
|
|
33
|
+
import { isGlobalFlag } from "../global-flags";
|
|
24
34
|
const PROJECT_INIT_USAGE =
|
|
25
|
-
"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]";
|
|
26
36
|
|
|
27
37
|
interface ProjectInitArgs {
|
|
28
38
|
path: string;
|
|
29
39
|
name: string;
|
|
30
40
|
skills: string[];
|
|
31
|
-
|
|
41
|
+
agents: string[];
|
|
32
42
|
targets: string[];
|
|
43
|
+
registerMcp: boolean;
|
|
33
44
|
yes: boolean;
|
|
34
45
|
sync: boolean;
|
|
35
46
|
}
|
|
@@ -44,16 +55,16 @@ export function configuredTargetForSurface(
|
|
|
44
55
|
)?.[0];
|
|
45
56
|
}
|
|
46
57
|
|
|
47
|
-
function
|
|
58
|
+
function configuredTargetsForAgents(
|
|
48
59
|
manifest: ReturnType<typeof parseManifest>,
|
|
49
|
-
|
|
60
|
+
agents: readonly string[],
|
|
50
61
|
): string[] {
|
|
51
|
-
return
|
|
62
|
+
return planAgentSurfaces(agents).surfaces.map((surface) => {
|
|
52
63
|
const target = configuredTargetForSurface(manifest, surface);
|
|
53
64
|
if (target) return target;
|
|
54
|
-
const
|
|
65
|
+
const agent = surface.agents[0]!;
|
|
55
66
|
throw new Error(
|
|
56
|
-
`
|
|
67
|
+
`agent target for "${agent}" is not configured; run "skillmux init --agent ${agent} --yes" first`,
|
|
57
68
|
);
|
|
58
69
|
});
|
|
59
70
|
}
|
|
@@ -62,8 +73,9 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
62
73
|
let projectPath: string | undefined;
|
|
63
74
|
let name: string | undefined;
|
|
64
75
|
const skills: string[] = [];
|
|
65
|
-
const
|
|
76
|
+
const agents: string[] = [];
|
|
66
77
|
const targets: string[] = [];
|
|
78
|
+
let registerMcp = false;
|
|
67
79
|
let yes = false;
|
|
68
80
|
let sync = true;
|
|
69
81
|
|
|
@@ -80,17 +92,18 @@ function parseProjectInitArgs(args: string[]): ProjectInitArgs {
|
|
|
80
92
|
const target = args[++i];
|
|
81
93
|
if (!target) throw new Error("--target requires a name");
|
|
82
94
|
targets.push(target);
|
|
83
|
-
} else if (arg === "--
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
86
|
-
|
|
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;
|
|
87
101
|
} else if (arg === "--yes") {
|
|
88
102
|
yes = true;
|
|
89
103
|
} else if (arg === "--no-sync") {
|
|
90
104
|
sync = false;
|
|
91
105
|
} else if (
|
|
92
|
-
arg
|
|
93
|
-
arg === "--json" ||
|
|
106
|
+
isGlobalFlag(arg, "--dry-run", "--json") ||
|
|
94
107
|
arg === "--interactive"
|
|
95
108
|
) {
|
|
96
109
|
continue;
|
|
@@ -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
|
};
|
|
@@ -184,7 +198,11 @@ export async function runProject(
|
|
|
184
198
|
config.local_vault_paths.map(expandHome),
|
|
185
199
|
);
|
|
186
200
|
if (options.dryRun) {
|
|
187
|
-
|
|
201
|
+
emitSuccess(
|
|
202
|
+
{ isJson: options.isJson },
|
|
203
|
+
{ subcommand: subCommand, group, path: projectPath },
|
|
204
|
+
() => console.log(`${subCommand}: [project.${group}] ${projectPath} (dry-run)`),
|
|
205
|
+
);
|
|
188
206
|
return;
|
|
189
207
|
}
|
|
190
208
|
if (
|
|
@@ -197,7 +215,11 @@ export async function runProject(
|
|
|
197
215
|
)
|
|
198
216
|
return;
|
|
199
217
|
writeManifestAtomic(manifestPath, updated);
|
|
200
|
-
|
|
218
|
+
emitSuccess(
|
|
219
|
+
{ isJson: options.isJson },
|
|
220
|
+
{ subcommand: subCommand, group, path: projectPath },
|
|
221
|
+
() => console.log(`${subCommand}: [project.${group}] ${projectPath}`),
|
|
222
|
+
);
|
|
201
223
|
return;
|
|
202
224
|
}
|
|
203
225
|
if (subCommand === "pin" || subCommand === "unpin") {
|
|
@@ -224,8 +246,13 @@ export async function runProject(
|
|
|
224
246
|
config.local_vault_paths.map(expandHome),
|
|
225
247
|
);
|
|
226
248
|
if (options.dryRun) {
|
|
227
|
-
|
|
228
|
-
|
|
249
|
+
emitSuccess(
|
|
250
|
+
{ isJson: options.isJson },
|
|
251
|
+
{ subcommand: subCommand, group, skill_ids: skills },
|
|
252
|
+
() =>
|
|
253
|
+
console.log(
|
|
254
|
+
`${subCommand}: [project.${group}] ${skills.join(", ")} (dry-run)`,
|
|
255
|
+
),
|
|
229
256
|
);
|
|
230
257
|
return;
|
|
231
258
|
}
|
|
@@ -239,22 +266,26 @@ export async function runProject(
|
|
|
239
266
|
)
|
|
240
267
|
return;
|
|
241
268
|
writeManifestAtomic(manifestPath, updated);
|
|
242
|
-
|
|
269
|
+
emitSuccess(
|
|
270
|
+
{ isJson: options.isJson },
|
|
271
|
+
{ subcommand: subCommand, group, skill_ids: skills },
|
|
272
|
+
() => console.log(`${subCommand}: [project.${group}] ${skills.join(", ")}`),
|
|
273
|
+
);
|
|
243
274
|
return;
|
|
244
275
|
}
|
|
245
276
|
if (subCommand === "attach" || subCommand === "detach") {
|
|
246
277
|
const group = args[0];
|
|
247
278
|
if (!group)
|
|
248
279
|
throw new Error(
|
|
249
|
-
`usage: skillmux project ${subCommand} <group> (--
|
|
280
|
+
`usage: skillmux project ${subCommand} <group> (--agent <id>... | --target <name>...) --yes`,
|
|
250
281
|
);
|
|
251
|
-
const
|
|
282
|
+
const agents: string[] = [];
|
|
252
283
|
const requestedTargets: string[] = [];
|
|
253
284
|
for (let i = 1; i < args.length; i++) {
|
|
254
|
-
if (args[i] === "--
|
|
285
|
+
if (args[i] === "--agent") {
|
|
255
286
|
const value = args[++i];
|
|
256
|
-
if (!value) throw new Error("--
|
|
257
|
-
|
|
287
|
+
if (!value) throw new Error("--agent requires a name");
|
|
288
|
+
agents.push(value);
|
|
258
289
|
} else if (args[i] === "--target") {
|
|
259
290
|
const value = args[++i];
|
|
260
291
|
if (!value) throw new Error("--target requires a name");
|
|
@@ -269,11 +300,20 @@ export async function runProject(
|
|
|
269
300
|
}
|
|
270
301
|
const { config, vaultPath, manifestPath, manifest } =
|
|
271
302
|
await loadManifestContext();
|
|
272
|
-
const
|
|
273
|
-
const targets = [...new Set([...requestedTargets, ...
|
|
303
|
+
const agentTargets = configuredTargetsForAgents(manifest, agents);
|
|
304
|
+
const targets = [...new Set([...requestedTargets, ...agentTargets])];
|
|
274
305
|
if (targets.length === 0) {
|
|
275
|
-
throw new Error(`project ${subCommand} requires --
|
|
306
|
+
throw new Error(`project ${subCommand} requires --agent or --target`);
|
|
276
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(", ");
|
|
277
317
|
const updated = updateProjectTargets(manifest, group, {
|
|
278
318
|
...(subCommand === "attach" ? { attach: targets } : { detach: targets }),
|
|
279
319
|
});
|
|
@@ -283,8 +323,13 @@ export async function runProject(
|
|
|
283
323
|
config.local_vault_paths.map(expandHome),
|
|
284
324
|
);
|
|
285
325
|
if (options.dryRun) {
|
|
286
|
-
|
|
287
|
-
|
|
326
|
+
emitSuccess(
|
|
327
|
+
{ isJson: options.isJson },
|
|
328
|
+
{ subcommand: subCommand, group, targets, target_dirs: targetDirs },
|
|
329
|
+
() =>
|
|
330
|
+
console.log(
|
|
331
|
+
`${subCommand}: [project.${group}] ${targetsDisplay} (dry-run)`,
|
|
332
|
+
),
|
|
288
333
|
);
|
|
289
334
|
return;
|
|
290
335
|
}
|
|
@@ -292,16 +337,31 @@ export async function runProject(
|
|
|
292
337
|
!(await confirmIfNeeded({
|
|
293
338
|
confirmed: args.includes("--yes"),
|
|
294
339
|
isJson: options.isJson,
|
|
295
|
-
prompt: `${subCommand} [project.${group}] to ${
|
|
340
|
+
prompt: `${subCommand} [project.${group}] to ${targetsDisplay}?`,
|
|
296
341
|
nonInteractiveError: `skillmux project ${subCommand} requires --yes when run non-interactively`,
|
|
297
342
|
}))
|
|
298
343
|
)
|
|
299
344
|
return;
|
|
300
345
|
writeManifestAtomic(manifestPath, updated);
|
|
301
|
-
|
|
346
|
+
emitSuccess(
|
|
347
|
+
{ isJson: options.isJson },
|
|
348
|
+
{ subcommand: subCommand, group, targets, target_dirs: targetDirs },
|
|
349
|
+
() => console.log(`${subCommand}: [project.${group}] ${targetsDisplay}`),
|
|
350
|
+
);
|
|
302
351
|
return;
|
|
303
352
|
}
|
|
304
|
-
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
|
+
]);
|
|
305
365
|
let request = parseProjectInitArgs(args);
|
|
306
366
|
const guided = shouldUseWizard(args, {
|
|
307
367
|
interactive: isInteractive(),
|
|
@@ -319,20 +379,20 @@ export async function runProject(
|
|
|
319
379
|
const localVaultPaths = config.local_vault_paths.map(expandHome);
|
|
320
380
|
if (guided) {
|
|
321
381
|
const name = await promptText("Project group", request.name);
|
|
322
|
-
const
|
|
323
|
-
const surface =
|
|
382
|
+
const availableAgents = SUPPORTED_AGENT_IDS.filter((agent) => {
|
|
383
|
+
const surface = planAgentSurfaces([agent]).surfaces[0];
|
|
324
384
|
return (
|
|
325
385
|
surface !== undefined &&
|
|
326
386
|
configuredTargetForSurface(manifest, surface) !== undefined
|
|
327
387
|
);
|
|
328
388
|
});
|
|
329
|
-
const
|
|
330
|
-
"Which
|
|
331
|
-
|
|
332
|
-
value:
|
|
333
|
-
label:
|
|
389
|
+
const agents = await promptMultiSelect(
|
|
390
|
+
"Which agents should receive project skills?",
|
|
391
|
+
availableAgents.map((agent) => ({
|
|
392
|
+
value: agent,
|
|
393
|
+
label: agent,
|
|
334
394
|
selected:
|
|
335
|
-
request.
|
|
395
|
+
request.agents.length === 0 || request.agents.includes(agent),
|
|
336
396
|
})),
|
|
337
397
|
);
|
|
338
398
|
const skills = parseCommaList(
|
|
@@ -341,10 +401,25 @@ export async function runProject(
|
|
|
341
401
|
request.skills.join(","),
|
|
342
402
|
),
|
|
343
403
|
);
|
|
344
|
-
request = { ...request, name,
|
|
404
|
+
request = { ...request, name, agents, skills };
|
|
345
405
|
}
|
|
346
|
-
|
|
347
|
-
|
|
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])];
|
|
348
423
|
const updated = upsertProject(manifest, {
|
|
349
424
|
name: request.name,
|
|
350
425
|
paths: [request.path],
|
|
@@ -352,15 +427,34 @@ export async function runProject(
|
|
|
352
427
|
targets,
|
|
353
428
|
});
|
|
354
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
|
+
|
|
355
443
|
const plan = {
|
|
356
444
|
mode: "project",
|
|
357
445
|
project: request.name,
|
|
358
446
|
path: request.path,
|
|
359
447
|
skills: request.skills,
|
|
360
|
-
|
|
448
|
+
agents: request.agents,
|
|
361
449
|
targets,
|
|
362
450
|
sync: request.sync,
|
|
363
451
|
notes,
|
|
452
|
+
instructions: instructionPlan.changes.map(({ path, agents, status }) => ({
|
|
453
|
+
path,
|
|
454
|
+
agents,
|
|
455
|
+
status,
|
|
456
|
+
})),
|
|
457
|
+
register_mcp_for: mcpInstructionAgents,
|
|
364
458
|
};
|
|
365
459
|
|
|
366
460
|
if (options.dryRun) {
|
|
@@ -375,13 +469,19 @@ export async function runProject(
|
|
|
375
469
|
console.log("\nReview");
|
|
376
470
|
console.log(` project: ${request.name}`);
|
|
377
471
|
console.log(` path: ${request.path}`);
|
|
378
|
-
console.log(`
|
|
472
|
+
console.log(` agents: ${request.agents.join(", ") || "(none)"}`);
|
|
379
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
|
+
);
|
|
380
480
|
console.log(` sync: ${request.sync ? "yes" : "no"}`);
|
|
381
481
|
}
|
|
382
482
|
if (
|
|
383
483
|
!(await confirmAction(
|
|
384
|
-
`
|
|
484
|
+
`apply project setup for ${request.name} at ${request.path}?`,
|
|
385
485
|
))
|
|
386
486
|
) {
|
|
387
487
|
console.log("project setup cancelled");
|
|
@@ -395,6 +495,17 @@ export async function runProject(
|
|
|
395
495
|
}
|
|
396
496
|
|
|
397
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
|
+
}
|
|
398
509
|
if (request.sync) {
|
|
399
510
|
try {
|
|
400
511
|
// Reaching here already required approval above (request.yes, or an
|
|
@@ -411,7 +522,39 @@ export async function runProject(
|
|
|
411
522
|
);
|
|
412
523
|
}
|
|
413
524
|
}
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
},
|
|
416
559
|
);
|
|
417
560
|
}
|