@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export const SUPPORTED_AGENT_IDS = [
|
|
6
|
+
"claude-code",
|
|
7
|
+
"codex",
|
|
8
|
+
"opencode",
|
|
9
|
+
"github-copilot",
|
|
10
|
+
"windsurf",
|
|
11
|
+
"antigravity",
|
|
12
|
+
"goose",
|
|
13
|
+
"hermes",
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type AgentId = (typeof SUPPORTED_AGENT_IDS)[number];
|
|
17
|
+
export type DeliveryMode = "managed-pins" | "full-vault";
|
|
18
|
+
|
|
19
|
+
export interface DetectedAgent {
|
|
20
|
+
agent: AgentId;
|
|
21
|
+
evidence: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export type McpRegistrationScope = "user" | "project";
|
|
25
|
+
|
|
26
|
+
export interface McpRegistrationCommand {
|
|
27
|
+
command: string;
|
|
28
|
+
/** Returns undefined when this agent's CLI has no way to register at the requested scope. */
|
|
29
|
+
buildArgs: (scope: McpRegistrationScope) => string[] | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface AgentPathOptions {
|
|
33
|
+
home: string;
|
|
34
|
+
codexHome: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface AgentInstructionOptions extends AgentPathOptions {
|
|
38
|
+
claudeConfigDir: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The single source of truth for what each agent supports. Every other
|
|
43
|
+
* module (detection, skill-surface planning, readiness reporting, MCP
|
|
44
|
+
* registration, instruction files, shell completions, CLI help text) reads
|
|
45
|
+
* from this record instead of keeping its own copy — see git history for
|
|
46
|
+
* what it looked like before consolidation (agent support was duplicated
|
|
47
|
+
* across 6+ files and could silently drift, e.g. shell completions offering
|
|
48
|
+
* goose/hermes for `project init` when they can't actually be attached).
|
|
49
|
+
*/
|
|
50
|
+
interface AgentDefinition {
|
|
51
|
+
id: AgentId;
|
|
52
|
+
surfaceId?: "agent-skills" | "claude-code" | "codex" | "antigravity";
|
|
53
|
+
deliveryMode: DeliveryMode;
|
|
54
|
+
/** Filesystem evidence used by guided-mode agent detection. */
|
|
55
|
+
detectionPath?: (options: AgentPathOptions) => string;
|
|
56
|
+
/** Bespoke readiness message for full-vault agents with no managed-pins surface. */
|
|
57
|
+
manualSkillSurfaceMessage?: string;
|
|
58
|
+
instructions?: {
|
|
59
|
+
global?: (options: AgentInstructionOptions) => string;
|
|
60
|
+
project?: (projectRoot: string) => string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Only set for agents whose own CLI's exact registration command was
|
|
64
|
+
* verified against its own --help, not guessed from docs. Every other
|
|
65
|
+
* agent falls back to printing the registration snippet (see
|
|
66
|
+
* printLastMile in init.ts) rather than a guessed command.
|
|
67
|
+
*/
|
|
68
|
+
mcpRegistration?: McpRegistrationCommand;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface PlannedAgentSurface {
|
|
72
|
+
id: string;
|
|
73
|
+
targetName: string;
|
|
74
|
+
path: string;
|
|
75
|
+
deliveryMode: "managed-pins";
|
|
76
|
+
agents: AgentId[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface AgentSurfacePlan {
|
|
80
|
+
agents: AgentDefinition[];
|
|
81
|
+
surfaces: PlannedAgentSurface[];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
type ReadinessStatus = "ready" | "planned" | "manual" | "not-applicable";
|
|
85
|
+
|
|
86
|
+
export interface ReadinessAxis {
|
|
87
|
+
status: ReadinessStatus;
|
|
88
|
+
detail: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface AgentReadiness {
|
|
92
|
+
agent: AgentId;
|
|
93
|
+
skillSurface: ReadinessAxis;
|
|
94
|
+
mcpRegistration: ReadinessAxis;
|
|
95
|
+
instructionSetup: ReadinessAxis;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export interface ResolvedBuiltInTarget {
|
|
99
|
+
targetName: string;
|
|
100
|
+
path: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Target names whose install directory is deterministic — --dir is optional for these. */
|
|
104
|
+
export const BUILT_IN_TARGET_NAMES = new Set(["agent-skills", "claude-code", "codex"]);
|
|
105
|
+
|
|
106
|
+
const AGENTS: Record<AgentId, AgentDefinition> = {
|
|
107
|
+
"claude-code": {
|
|
108
|
+
id: "claude-code",
|
|
109
|
+
surfaceId: "claude-code",
|
|
110
|
+
deliveryMode: "managed-pins",
|
|
111
|
+
detectionPath: ({ home }) => join(home, ".claude"),
|
|
112
|
+
instructions: {
|
|
113
|
+
global: ({ claudeConfigDir }) => join(claudeConfigDir, "CLAUDE.md"),
|
|
114
|
+
project: (projectRoot) => join(projectRoot, "CLAUDE.md"),
|
|
115
|
+
},
|
|
116
|
+
mcpRegistration: {
|
|
117
|
+
command: "claude",
|
|
118
|
+
// claude mcp add --scope: local (default, unshared), project (writes a
|
|
119
|
+
// committed .mcp.json), or user (global). "project" is the only shared
|
|
120
|
+
// option, so that's what a project-scoped registration means here.
|
|
121
|
+
buildArgs: (scope) => [
|
|
122
|
+
"mcp",
|
|
123
|
+
"add",
|
|
124
|
+
"-s",
|
|
125
|
+
scope === "project" ? "project" : "user",
|
|
126
|
+
"skillmux",
|
|
127
|
+
"--",
|
|
128
|
+
"skillmux",
|
|
129
|
+
"serve",
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
codex: {
|
|
134
|
+
id: "codex",
|
|
135
|
+
surfaceId: "codex",
|
|
136
|
+
deliveryMode: "managed-pins",
|
|
137
|
+
detectionPath: ({ codexHome }) => codexHome,
|
|
138
|
+
instructions: {
|
|
139
|
+
global: ({ codexHome }) => join(codexHome, "AGENTS.md"),
|
|
140
|
+
},
|
|
141
|
+
mcpRegistration: {
|
|
142
|
+
command: "codex",
|
|
143
|
+
// codex mcp add has no --scope flag at all — it always writes to the
|
|
144
|
+
// global ~/.codex/config.toml, so project scope is not representable.
|
|
145
|
+
buildArgs: (scope) =>
|
|
146
|
+
scope === "project"
|
|
147
|
+
? undefined
|
|
148
|
+
: ["mcp", "add", "skillmux", "--", "skillmux", "serve"],
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
opencode: {
|
|
152
|
+
id: "opencode",
|
|
153
|
+
surfaceId: "agent-skills",
|
|
154
|
+
deliveryMode: "managed-pins",
|
|
155
|
+
detectionPath: ({ home }) => join(home, ".config", "opencode"),
|
|
156
|
+
instructions: {
|
|
157
|
+
global: ({ home }) => join(home, ".config", "opencode", "AGENTS.md"),
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
"github-copilot": {
|
|
161
|
+
id: "github-copilot",
|
|
162
|
+
surfaceId: "agent-skills",
|
|
163
|
+
deliveryMode: "managed-pins",
|
|
164
|
+
detectionPath: ({ home }) => join(home, ".config", "github-copilot"),
|
|
165
|
+
},
|
|
166
|
+
windsurf: {
|
|
167
|
+
id: "windsurf",
|
|
168
|
+
surfaceId: "agent-skills",
|
|
169
|
+
deliveryMode: "managed-pins",
|
|
170
|
+
detectionPath: ({ home }) => join(home, ".codeium", "windsurf"),
|
|
171
|
+
},
|
|
172
|
+
antigravity: {
|
|
173
|
+
id: "antigravity",
|
|
174
|
+
surfaceId: "antigravity",
|
|
175
|
+
deliveryMode: "managed-pins",
|
|
176
|
+
instructions: {
|
|
177
|
+
global: ({ home }) => join(home, ".gemini", "GEMINI.md"),
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
goose: {
|
|
181
|
+
id: "goose",
|
|
182
|
+
deliveryMode: "full-vault",
|
|
183
|
+
detectionPath: ({ home }) => join(home, ".config", "goose"),
|
|
184
|
+
manualSkillSurfaceMessage: "configure the full vault in Goose",
|
|
185
|
+
instructions: {
|
|
186
|
+
global: ({ home }) => join(home, ".config", "goose", ".goosehints"),
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
hermes: {
|
|
190
|
+
id: "hermes",
|
|
191
|
+
deliveryMode: "full-vault",
|
|
192
|
+
detectionPath: ({ home }) => join(home, ".hermes"),
|
|
193
|
+
manualSkillSurfaceMessage: "configure the full vault in Hermes external_dirs",
|
|
194
|
+
instructions: {
|
|
195
|
+
global: ({ home }) => join(home, ".hermes.md"),
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export function getAgentDefinition(agent: AgentId): AgentDefinition {
|
|
201
|
+
return AGENTS[agent];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** Agents that can be attached to a project group (managed-pins delivery, not full-vault). */
|
|
205
|
+
export const MANAGED_PINS_AGENT_IDS = SUPPORTED_AGENT_IDS.filter(
|
|
206
|
+
(id) => AGENTS[id].deliveryMode === "managed-pins",
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
export function detectInstalledAgents(
|
|
210
|
+
options: {
|
|
211
|
+
home?: string;
|
|
212
|
+
codexHome?: string;
|
|
213
|
+
exists?: (path: string) => boolean;
|
|
214
|
+
} = {},
|
|
215
|
+
): DetectedAgent[] {
|
|
216
|
+
const home = options.home ?? homedir();
|
|
217
|
+
const codexHome = options.codexHome ?? join(home, ".codex");
|
|
218
|
+
const exists = options.exists ?? existsSync;
|
|
219
|
+
return SUPPORTED_AGENT_IDS.flatMap((agent) => {
|
|
220
|
+
const evidence = AGENTS[agent].detectionPath?.({ home, codexHome });
|
|
221
|
+
return evidence && exists(evidence) ? [{ agent, evidence }] : [];
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function surfacePath(
|
|
226
|
+
surfaceId: NonNullable<AgentDefinition["surfaceId"]>,
|
|
227
|
+
options: { home: string; codexHome?: string },
|
|
228
|
+
): string {
|
|
229
|
+
if (surfaceId === "agent-skills") return join(options.home, ".agents", "skills");
|
|
230
|
+
if (surfaceId === "claude-code") return join(options.home, ".claude", "skills");
|
|
231
|
+
if (surfaceId === "codex") return join(options.codexHome ?? join(options.home, ".codex"), "skills");
|
|
232
|
+
return join(options.home, ".gemini", "config", "skills");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function resolveBuiltInTarget(
|
|
236
|
+
name: string,
|
|
237
|
+
options: { home?: string; codexHome?: string; customPath?: string } = {},
|
|
238
|
+
): ResolvedBuiltInTarget {
|
|
239
|
+
const home = options.home ?? homedir();
|
|
240
|
+
if (name === "custom") {
|
|
241
|
+
if (!options.customPath) throw new Error("--target custom requires --path <dir>");
|
|
242
|
+
return { targetName: name, path: options.customPath };
|
|
243
|
+
}
|
|
244
|
+
if (name === "agent-skills") {
|
|
245
|
+
return { targetName: name, path: surfacePath("agent-skills", { home }) };
|
|
246
|
+
}
|
|
247
|
+
if (name === "claude-code") {
|
|
248
|
+
return { targetName: name, path: surfacePath("claude-code", { home }) };
|
|
249
|
+
}
|
|
250
|
+
if (name === "codex") {
|
|
251
|
+
return {
|
|
252
|
+
targetName: name,
|
|
253
|
+
path: surfacePath("codex", { home, codexHome: options.codexHome }),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
throw new Error(
|
|
257
|
+
`unknown --target "${name}"; supported targets: agent-skills, claude-code, codex, custom`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function planAgentSurfaces(
|
|
262
|
+
requestedAgents: readonly string[],
|
|
263
|
+
options: { home?: string; codexHome?: string } = {},
|
|
264
|
+
): AgentSurfacePlan {
|
|
265
|
+
const agents = [...new Set(requestedAgents)].map((id) => {
|
|
266
|
+
if (!SUPPORTED_AGENT_IDS.includes(id as AgentId)) {
|
|
267
|
+
throw new Error(
|
|
268
|
+
`unsupported agent "${id}"; supported agents: ${SUPPORTED_AGENT_IDS.join(", ")}`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
return AGENTS[id as AgentId];
|
|
272
|
+
});
|
|
273
|
+
const home = options.home ?? homedir();
|
|
274
|
+
const surfaces = new Map<string, PlannedAgentSurface>();
|
|
275
|
+
|
|
276
|
+
for (const agent of agents) {
|
|
277
|
+
if (!agent.surfaceId) continue;
|
|
278
|
+
const path = surfacePath(agent.surfaceId, { home, codexHome: options.codexHome });
|
|
279
|
+
const existing = surfaces.get(path);
|
|
280
|
+
if (existing) {
|
|
281
|
+
if (!existing.agents.includes(agent.id)) existing.agents.push(agent.id);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
surfaces.set(path, {
|
|
285
|
+
id: agent.surfaceId,
|
|
286
|
+
targetName: agent.surfaceId,
|
|
287
|
+
path,
|
|
288
|
+
deliveryMode: "managed-pins",
|
|
289
|
+
agents: [agent.id],
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { agents, surfaces: [...surfaces.values()] };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function assessAgentReadiness(
|
|
297
|
+
plan: AgentSurfacePlan,
|
|
298
|
+
instructionReadiness: Partial<Record<AgentId, ReadinessAxis>> = {},
|
|
299
|
+
): AgentReadiness[] {
|
|
300
|
+
return plan.agents.map((agent) => {
|
|
301
|
+
const surface = plan.surfaces.find((candidate) => candidate.agents.includes(agent.id));
|
|
302
|
+
let skillSurface: ReadinessAxis;
|
|
303
|
+
if (surface) {
|
|
304
|
+
skillSurface = { status: "planned", detail: surface.path };
|
|
305
|
+
} else if (agent.manualSkillSurfaceMessage) {
|
|
306
|
+
skillSurface = { status: "manual", detail: agent.manualSkillSurfaceMessage };
|
|
307
|
+
} else {
|
|
308
|
+
skillSurface = {
|
|
309
|
+
status: "not-applicable",
|
|
310
|
+
detail: "skills resolve through Skillmux MCP",
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const mcpRegistration: ReadinessAxis = {
|
|
315
|
+
status: "not-applicable",
|
|
316
|
+
detail: "native skill loading",
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
return {
|
|
320
|
+
agent: agent.id,
|
|
321
|
+
skillSurface,
|
|
322
|
+
mcpRegistration,
|
|
323
|
+
instructionSetup: instructionReadiness[agent.id] ?? {
|
|
324
|
+
status: "manual",
|
|
325
|
+
detail: "instruction adapter not applied",
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|
package/src/init-instructions.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from "node:fs";
|
|
10
10
|
import { dirname, join } from "node:path";
|
|
11
11
|
import { DISCOVERY_PARAGRAPH } from "./init";
|
|
12
|
-
import type
|
|
12
|
+
import { getAgentDefinition, type AgentId } from "./init-agents";
|
|
13
13
|
|
|
14
14
|
export const INSTRUCTION_BLOCK_START = "<!-- skillmux:discovery:start -->";
|
|
15
15
|
export const INSTRUCTION_BLOCK_END = "<!-- skillmux:discovery:end -->";
|
|
@@ -22,7 +22,7 @@ const MANAGED_BLOCK = [
|
|
|
22
22
|
|
|
23
23
|
export interface InstructionChange {
|
|
24
24
|
path: string;
|
|
25
|
-
|
|
25
|
+
agents: AgentId[];
|
|
26
26
|
status: "create" | "update" | "unchanged";
|
|
27
27
|
before: string | null;
|
|
28
28
|
after: string;
|
|
@@ -30,7 +30,7 @@ export interface InstructionChange {
|
|
|
30
30
|
|
|
31
31
|
export interface InstructionPlan {
|
|
32
32
|
changes: InstructionChange[];
|
|
33
|
-
manual: Array<{
|
|
33
|
+
manual: Array<{ agent: AgentId; reason: string }>;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
interface InstructionPlanOptions {
|
|
@@ -41,18 +41,10 @@ interface InstructionPlanOptions {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function instructionPath(
|
|
44
|
-
|
|
44
|
+
agent: AgentId,
|
|
45
45
|
options: Required<Pick<InstructionPlanOptions, "home" | "codexHome" | "claudeConfigDir">>,
|
|
46
46
|
): string | undefined {
|
|
47
|
-
|
|
48
|
-
if (client === "codex") return join(options.codexHome, "AGENTS.md");
|
|
49
|
-
if (client === "gemini-cli" || client === "antigravity") {
|
|
50
|
-
return join(options.home, ".gemini", "GEMINI.md");
|
|
51
|
-
}
|
|
52
|
-
if (client === "opencode") return join(options.home, ".config", "opencode", "AGENTS.md");
|
|
53
|
-
if (client === "goose") return join(options.home, ".config", "goose", ".goosehints");
|
|
54
|
-
if (client === "hermes") return join(options.home, ".hermes.md");
|
|
55
|
-
return undefined;
|
|
47
|
+
return getAgentDefinition(agent).instructions?.global?.(options);
|
|
56
48
|
}
|
|
57
49
|
|
|
58
50
|
function readInstructionFile(path: string): string | null {
|
|
@@ -85,29 +77,23 @@ function withManagedBlock(existing: string | null, path: string): string {
|
|
|
85
77
|
return `${existing.slice(0, start)}${MANAGED_BLOCK}${existing.slice(end + INSTRUCTION_BLOCK_END.length)}`;
|
|
86
78
|
}
|
|
87
79
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
80
|
+
function planInstructionSetupWithResolver(
|
|
81
|
+
requestedAgents: readonly AgentId[],
|
|
82
|
+
resolvePath: (agent: AgentId) => string | undefined,
|
|
83
|
+
readFile: (path: string) => string | null,
|
|
91
84
|
): InstructionPlan {
|
|
92
|
-
const home = options.home ?? process.env.HOME ?? "";
|
|
93
|
-
const resolvedOptions = {
|
|
94
|
-
home,
|
|
95
|
-
codexHome: options.codexHome ?? process.env.CODEX_HOME ?? join(home, ".codex"),
|
|
96
|
-
claudeConfigDir: options.claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"),
|
|
97
|
-
};
|
|
98
|
-
const readFile = options.readFile ?? readInstructionFile;
|
|
99
85
|
const changesByPath = new Map<string, InstructionChange>();
|
|
100
86
|
const manual: InstructionPlan["manual"] = [];
|
|
101
87
|
|
|
102
|
-
for (const
|
|
103
|
-
const path =
|
|
88
|
+
for (const agent of [...new Set(requestedAgents)]) {
|
|
89
|
+
const path = resolvePath(agent);
|
|
104
90
|
if (!path) {
|
|
105
|
-
manual.push({
|
|
91
|
+
manual.push({ agent, reason: "no safe durable user instruction file is known" });
|
|
106
92
|
continue;
|
|
107
93
|
}
|
|
108
94
|
const existingChange = changesByPath.get(path);
|
|
109
95
|
if (existingChange) {
|
|
110
|
-
existingChange.
|
|
96
|
+
existingChange.agents.push(agent);
|
|
111
97
|
continue;
|
|
112
98
|
}
|
|
113
99
|
|
|
@@ -115,7 +101,7 @@ export function planInstructionSetup(
|
|
|
115
101
|
const after = withManagedBlock(before, path);
|
|
116
102
|
changesByPath.set(path, {
|
|
117
103
|
path,
|
|
118
|
-
|
|
104
|
+
agents: [agent],
|
|
119
105
|
status: before === null ? "create" : before === after ? "unchanged" : "update",
|
|
120
106
|
before,
|
|
121
107
|
after,
|
|
@@ -125,6 +111,39 @@ export function planInstructionSetup(
|
|
|
125
111
|
return { changes: [...changesByPath.values()], manual };
|
|
126
112
|
}
|
|
127
113
|
|
|
114
|
+
export function planInstructionSetup(
|
|
115
|
+
requestedAgents: readonly AgentId[],
|
|
116
|
+
options: InstructionPlanOptions = {},
|
|
117
|
+
): InstructionPlan {
|
|
118
|
+
const home = options.home ?? process.env.HOME ?? "";
|
|
119
|
+
const resolvedOptions = {
|
|
120
|
+
home,
|
|
121
|
+
codexHome: options.codexHome ?? process.env.CODEX_HOME ?? join(home, ".codex"),
|
|
122
|
+
claudeConfigDir: options.claudeConfigDir ?? process.env.CLAUDE_CONFIG_DIR ?? join(home, ".claude"),
|
|
123
|
+
};
|
|
124
|
+
return planInstructionSetupWithResolver(
|
|
125
|
+
requestedAgents,
|
|
126
|
+
(agent) => instructionPath(agent, resolvedOptions),
|
|
127
|
+
options.readFile ?? readInstructionFile,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function projectInstructionPath(agent: AgentId, projectRoot: string): string | undefined {
|
|
132
|
+
return getAgentDefinition(agent).instructions?.project?.(projectRoot);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function planProjectInstructionSetup(
|
|
136
|
+
requestedAgents: readonly AgentId[],
|
|
137
|
+
projectRoot: string,
|
|
138
|
+
options: Pick<InstructionPlanOptions, "readFile"> = {},
|
|
139
|
+
): InstructionPlan {
|
|
140
|
+
return planInstructionSetupWithResolver(
|
|
141
|
+
requestedAgents,
|
|
142
|
+
(agent) => projectInstructionPath(agent, projectRoot),
|
|
143
|
+
options.readFile ?? readInstructionFile,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
128
147
|
function atomicWrite(path: string, content: string, mode: number): void {
|
|
129
148
|
mkdirSync(dirname(path), { recursive: true });
|
|
130
149
|
const temporaryPath = `${path}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getAgentDefinition,
|
|
3
|
+
SUPPORTED_AGENT_IDS,
|
|
4
|
+
type AgentId,
|
|
5
|
+
type McpRegistrationScope,
|
|
6
|
+
} from "./init-agents";
|
|
7
|
+
|
|
8
|
+
export type { McpRegistrationScope };
|
|
9
|
+
|
|
10
|
+
export const MCP_REGISTRABLE_AGENTS = SUPPORTED_AGENT_IDS.filter(
|
|
11
|
+
(agent) => getAgentDefinition(agent).mcpRegistration !== undefined,
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
export const MCP_PROJECT_REGISTRABLE_AGENTS = MCP_REGISTRABLE_AGENTS.filter(
|
|
15
|
+
(agent) => getAgentDefinition(agent).mcpRegistration!.buildArgs("project") !== undefined,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export function isMcpRegistrable(agent: AgentId): boolean {
|
|
19
|
+
return getAgentDefinition(agent).mcpRegistration !== undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface McpRegistrationResult {
|
|
23
|
+
agent: AgentId;
|
|
24
|
+
ok: boolean;
|
|
25
|
+
error?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type SpawnFn = (
|
|
29
|
+
cmd: string[],
|
|
30
|
+
opts?: { cwd?: string },
|
|
31
|
+
) => {
|
|
32
|
+
exited: Promise<number>;
|
|
33
|
+
stderr: ReadableStream<Uint8Array> | number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Runs the agent's own CLI to register skillmux as an MCP server. Never
|
|
38
|
+
* throws — registration failure (tool not installed, command rejected,
|
|
39
|
+
* etc.) is reported in the result, not fatal to the caller's larger flow.
|
|
40
|
+
*/
|
|
41
|
+
export async function registerMcpServer(
|
|
42
|
+
agent: AgentId,
|
|
43
|
+
options: {
|
|
44
|
+
spawn?: SpawnFn;
|
|
45
|
+
scope?: McpRegistrationScope;
|
|
46
|
+
cwd?: string;
|
|
47
|
+
} = {},
|
|
48
|
+
): Promise<McpRegistrationResult> {
|
|
49
|
+
const entry = getAgentDefinition(agent).mcpRegistration;
|
|
50
|
+
const scope = options.scope ?? "user";
|
|
51
|
+
const args = entry?.buildArgs(scope);
|
|
52
|
+
if (!entry || !args) {
|
|
53
|
+
return {
|
|
54
|
+
agent,
|
|
55
|
+
ok: false,
|
|
56
|
+
error: `no ${scope === "project" ? "project-scoped " : ""}MCP registration command known for agent "${agent}"`,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const spawn: SpawnFn =
|
|
60
|
+
options.spawn ??
|
|
61
|
+
((cmd, opts) => Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe", cwd: opts?.cwd }));
|
|
62
|
+
try {
|
|
63
|
+
const proc = spawn([entry.command, ...args], { cwd: options.cwd });
|
|
64
|
+
const stderrText =
|
|
65
|
+
typeof proc.stderr === "number"
|
|
66
|
+
? ""
|
|
67
|
+
: await new Response(proc.stderr).text();
|
|
68
|
+
const exitCode = await proc.exited;
|
|
69
|
+
if (exitCode !== 0) {
|
|
70
|
+
return {
|
|
71
|
+
agent,
|
|
72
|
+
ok: false,
|
|
73
|
+
error:
|
|
74
|
+
stderrText.trim() || `${entry.command} exited with code ${exitCode}`,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return { agent, ok: true };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
80
|
+
const notFound = /ENOENT|not found|no such file/i.test(message);
|
|
81
|
+
return {
|
|
82
|
+
agent,
|
|
83
|
+
ok: false,
|
|
84
|
+
error: notFound
|
|
85
|
+
? `"${entry.command}" is not installed or not on PATH`
|
|
86
|
+
: message,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
}
|
package/src/output.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { ResolvedContext } from "./context";
|
|
|
3
3
|
export interface JsonEnvelope<T = any> {
|
|
4
4
|
schema_version: 1;
|
|
5
5
|
ok: boolean;
|
|
6
|
+
context: string | { name: string; server: string };
|
|
7
|
+
/** @deprecated Slated for removal in the next major version. Use `context` instead. */
|
|
6
8
|
target: string | { name: string; server: string };
|
|
7
9
|
data: T | null;
|
|
8
10
|
error: { code: string; message: string; details?: any } | null;
|
|
@@ -10,27 +12,34 @@ export interface JsonEnvelope<T = any> {
|
|
|
10
12
|
|
|
11
13
|
export function formatJsonEnvelope<T>(opts: {
|
|
12
14
|
ok: boolean;
|
|
13
|
-
|
|
15
|
+
/** @deprecated Slated for removal in the next major version. Use `context` instead. */
|
|
16
|
+
target?: ResolvedContext | string | { name: string; server: string };
|
|
17
|
+
context?: ResolvedContext | string | { name: string; server: string };
|
|
14
18
|
data?: T;
|
|
15
19
|
error?: { code: string; message: string; details?: any } | null;
|
|
16
20
|
}): JsonEnvelope<T> {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const input: ResolvedContext | string | { name: string; server: string } =
|
|
22
|
+
opts.context ?? opts.target ?? "local";
|
|
23
|
+
let contextVal: string | { name: string; server: string };
|
|
24
|
+
if (typeof input === "string") {
|
|
25
|
+
contextVal = input;
|
|
26
|
+
} else if (typeof input === "object" && input !== null) {
|
|
27
|
+
if ("type" in input && (input as any).type === "local") {
|
|
28
|
+
contextVal = "local";
|
|
29
|
+
} else if ("name" in input && "server" in input) {
|
|
30
|
+
contextVal = { name: input.name, server: input.server };
|
|
23
31
|
} else {
|
|
24
|
-
|
|
32
|
+
contextVal = "local";
|
|
25
33
|
}
|
|
26
34
|
} else {
|
|
27
|
-
|
|
35
|
+
contextVal = "local";
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
return {
|
|
31
39
|
schema_version: 1,
|
|
32
40
|
ok: opts.ok,
|
|
33
|
-
|
|
41
|
+
context: contextVal,
|
|
42
|
+
target: contextVal,
|
|
34
43
|
data: opts.data ?? null,
|
|
35
44
|
error: opts.error ?? null,
|
|
36
45
|
};
|
|
@@ -51,12 +60,18 @@ export class CliError extends Error {
|
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
export function emitSuccess<T>(
|
|
54
|
-
ctx: {
|
|
63
|
+
ctx: {
|
|
64
|
+
isJson: boolean;
|
|
65
|
+
/** @deprecated Slated for removal in the next major version. Use `context` instead. */
|
|
66
|
+
target?: ResolvedContext | string | { name: string; server: string };
|
|
67
|
+
context?: ResolvedContext | string | { name: string; server: string };
|
|
68
|
+
},
|
|
55
69
|
data: T,
|
|
56
70
|
renderText: () => void,
|
|
57
71
|
): void {
|
|
58
72
|
if (ctx.isJson) {
|
|
59
|
-
|
|
73
|
+
const contextVal = ctx.context ?? ctx.target ?? "local";
|
|
74
|
+
console.log(JSON.stringify(formatJsonEnvelope({ ok: true, context: contextVal, target: contextVal, data })));
|
|
60
75
|
} else {
|
|
61
76
|
renderText();
|
|
62
77
|
}
|
|
@@ -112,6 +127,28 @@ export function suggestCorrection(input: string, candidates: string[]): string |
|
|
|
112
127
|
return bestMatch;
|
|
113
128
|
}
|
|
114
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Builds the error for an unrecognized subcommand: "did you mean X" when
|
|
132
|
+
* close to a valid one, otherwise the full <a|b|c> usage list — never a
|
|
133
|
+
* fixed, possibly-unrelated usage string for just one of several valid
|
|
134
|
+
* subcommands (that's what `config`'s fallback used to do before this
|
|
135
|
+
* existed: any invalid subcommand got told "usage: skillmux config show",
|
|
136
|
+
* silently omitting get/set/validate/diff/status/init).
|
|
137
|
+
*/
|
|
138
|
+
export function unknownSubcommandError(
|
|
139
|
+
command: string,
|
|
140
|
+
subCommand: string,
|
|
141
|
+
validSubcommands: string[],
|
|
142
|
+
): Error {
|
|
143
|
+
const suggestion = subCommand ? suggestCorrection(subCommand, validSubcommands) : null;
|
|
144
|
+
if (suggestion) {
|
|
145
|
+
return new Error(
|
|
146
|
+
`Unknown "${command} ${subCommand}" subcommand. Did you mean "${command} ${suggestion}"?`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return new Error(`usage: skillmux ${command} <${validSubcommands.join("|")}>`);
|
|
150
|
+
}
|
|
151
|
+
|
|
115
152
|
export function isInteractive(
|
|
116
153
|
env: NodeJS.ProcessEnv = process.env,
|
|
117
154
|
stdoutIsTTY = process.stdout.isTTY,
|
|
@@ -144,12 +181,12 @@ export function warn(line: string): void {
|
|
|
144
181
|
console.error(yellow(`warning: ${line}`));
|
|
145
182
|
}
|
|
146
183
|
|
|
147
|
-
export function
|
|
184
|
+
export function renderContextBanner(context: ResolvedContext): void {
|
|
148
185
|
if (!isInteractive()) return;
|
|
149
|
-
if (
|
|
150
|
-
console.log(`
|
|
186
|
+
if (context.type === "local") {
|
|
187
|
+
console.log(`Context: local`);
|
|
151
188
|
} else {
|
|
152
|
-
console.log(`
|
|
189
|
+
console.log(`Context: remote (${context.name} -> ${context.server})`);
|
|
153
190
|
}
|
|
154
191
|
}
|
|
155
192
|
|