@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,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`;
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized logging helpers, all writing to stderr via console.error.
|
|
3
|
+
* `log` emits structured JSON ({ level, stage, ...payload }) for machine-
|
|
4
|
+
* readable signals like router-core's retrieval-degradation warnings.
|
|
5
|
+
* `redactedErrorLog` builds a plain-text [prefix, message] pair for
|
|
6
|
+
* operator-facing runtime errors that need secret redaction.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const log = {
|
|
10
|
+
warn(stage: string, payload: Record<string, unknown>): void {
|
|
11
|
+
console.error(JSON.stringify({ level: "warn", stage, ...payload }));
|
|
12
|
+
},
|
|
13
|
+
error(stage: string, payload: Record<string, unknown>): void {
|
|
14
|
+
console.error(JSON.stringify({ level: "error", stage, ...payload }));
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Pairs a fixed log prefix with a redacted error message, for console.error. */
|
|
19
|
+
export function redactedErrorLog(
|
|
20
|
+
prefix: string,
|
|
21
|
+
err: unknown,
|
|
22
|
+
redact: (text: string) => string,
|
|
23
|
+
): [string, string] {
|
|
24
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
25
|
+
return [prefix, redact(msg)];
|
|
26
|
+
}
|
|
@@ -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
|
+
}
|