@klhapp/skillmux 1.10.0 → 1.11.1
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 +44 -0
- package/README.md +26 -19
- 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 +80 -33
- package/docs/concepts.md +15 -11
- package/docs/configuration.md +6 -4
- package/docs/deployment.md +1 -1
- package/docs/getting-started.md +21 -13
- package/docs/mcp-routing.md +1 -1
- package/docs/skill-management.md +39 -19
- package/docs/troubleshooting.md +4 -4
- package/package.json +1 -1
- package/src/adapters.ts +11 -11
- package/src/cli.ts +255 -72
- 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/install.ts +36 -13
- package/src/commands/project.ts +177 -44
- package/src/commands/report.ts +3 -3
- package/src/commands/scan.ts +18 -8
- package/src/commands/shared.ts +7 -14
- package/src/commands/skill.ts +2 -1
- package/src/commands/target.ts +27 -9
- package/src/commands/update.ts +16 -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 +53 -19
- package/src/server.ts +1 -1
- package/src/init-clients.ts +0 -220
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,
|
|
@@ -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`;
|