@jaggerxtrm/specialists 3.2.0 → 3.2.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/README.md +66 -257
- package/bin/install.js +152 -329
- package/dist/index.js +1286 -428
- package/package.json +2 -2
- package/specialists/codebase-explorer.specialist.yaml +1 -1
- package/specialists/planner.specialist.yaml +87 -0
- package/specialists/specialist-author.specialist.yaml +56 -0
- package/specialists/sync-docs.specialist.yaml +53 -0
- package/specialists/xt-merge.specialist.yaml +78 -0
- package/hooks/beads-close-memory-prompt.mjs +0 -47
- package/hooks/beads-commit-gate.mjs +0 -58
- package/hooks/beads-edit-gate.mjs +0 -53
- package/hooks/beads-stop-gate.mjs +0 -52
- package/hooks/specialists-main-guard.mjs +0 -90
package/bin/install.js
CHANGED
|
@@ -1,386 +1,209 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Specialists Installer
|
|
3
|
-
// Usage: npx --package=@jaggerxtrm/specialists install
|
|
4
2
|
|
|
5
3
|
import { spawnSync } from 'node:child_process';
|
|
6
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
chmodSync,
|
|
6
|
+
copyFileSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
writeFileSync,
|
|
12
|
+
} from 'node:fs';
|
|
7
13
|
import { homedir } from 'node:os';
|
|
8
14
|
import { join } from 'node:path';
|
|
9
15
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const MCP_NAME = 'specialists';
|
|
17
|
-
const GITHUB_PKG = '@jaggerxtrm/specialists';
|
|
18
|
-
|
|
19
|
-
// Bundled specialists dir — resolved relative to this file (bin/../specialists/)
|
|
16
|
+
const CWD = process.cwd();
|
|
17
|
+
const CLAUDE_DIR = join(CWD, '.claude');
|
|
18
|
+
const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
|
|
19
|
+
const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
|
|
20
|
+
const MCP_FILE = join(CWD, '.mcp.json');
|
|
21
|
+
const BUNDLED_HOOKS_DIR = new URL('../hooks', import.meta.url).pathname;
|
|
20
22
|
const BUNDLED_SPECIALISTS_DIR = new URL('../specialists', import.meta.url).pathname;
|
|
21
|
-
const
|
|
23
|
+
const USER_SPECIALISTS_DIR = join(homedir(), '.agents', 'specialists');
|
|
22
24
|
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
25
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
26
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
26
27
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
27
|
-
const bold
|
|
28
|
-
const red
|
|
28
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
29
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
29
30
|
|
|
30
31
|
function section(label) {
|
|
31
|
-
const line = '─'.repeat(Math.max(0,
|
|
32
|
+
const line = '─'.repeat(Math.max(0, 44 - label.length));
|
|
32
33
|
console.log(`\n${bold(`── ${label} ${line}`)}`);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function ok(label)
|
|
36
|
+
function ok(label) { console.log(` ${green('✓')} ${label}`); }
|
|
36
37
|
function skip(label) { console.log(` ${yellow('○')} ${label}`); }
|
|
37
38
|
function info(label) { console.log(` ${dim(label)}`); }
|
|
38
39
|
function fail(label) { console.log(` ${red('✗')} ${label}`); }
|
|
39
40
|
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
function run(cmd, args, options = {}) {
|
|
42
|
+
return spawnSync(cmd, args, {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
stdio: 'pipe',
|
|
45
|
+
...options,
|
|
46
|
+
});
|
|
43
47
|
}
|
|
44
48
|
|
|
45
|
-
function
|
|
46
|
-
const
|
|
47
|
-
|
|
49
|
+
function commandOk(cmd, args = ['--version']) {
|
|
50
|
+
const result = run(cmd, args);
|
|
51
|
+
return result.status === 0 && !result.error;
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
function
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
info('Installing dolt (requires sudo)...');
|
|
57
|
-
const r = spawnSync(
|
|
58
|
-
'sudo', ['bash', '-c', 'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash'],
|
|
59
|
-
{ stdio: 'inherit', encoding: 'utf8' }
|
|
60
|
-
);
|
|
61
|
-
if (r.status === 0) {
|
|
62
|
-
ok('dolt installed');
|
|
63
|
-
} else {
|
|
64
|
-
fail('dolt install failed — install manually:');
|
|
65
|
-
info(" sudo bash -c 'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash'");
|
|
66
|
-
}
|
|
54
|
+
function loadJson(path, fallback) {
|
|
55
|
+
if (!existsSync(path)) return structuredClone(fallback);
|
|
56
|
+
try {
|
|
57
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
58
|
+
} catch {
|
|
59
|
+
return structuredClone(fallback);
|
|
67
60
|
}
|
|
68
61
|
}
|
|
69
62
|
|
|
70
|
-
function
|
|
71
|
-
|
|
72
|
-
if (check.status === 0) return false;
|
|
73
|
-
|
|
74
|
-
npmInstallGlobal(GITHUB_PKG);
|
|
75
|
-
|
|
76
|
-
const r = spawnSync('claude', [
|
|
77
|
-
'mcp', 'add', '--scope', 'user', MCP_NAME, '--', MCP_NAME,
|
|
78
|
-
], { stdio: 'inherit', encoding: 'utf8' });
|
|
79
|
-
if (r.status !== 0) throw new Error('claude mcp add failed');
|
|
80
|
-
return true;
|
|
63
|
+
function saveJson(path, value) {
|
|
64
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + '\n', 'utf8');
|
|
81
65
|
}
|
|
82
66
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const HOOK_ENTRY = {
|
|
87
|
-
matcher: 'Edit|Write|MultiEdit|NotebookEdit|Bash',
|
|
88
|
-
hooks: [{ type: 'command', command: HOOK_FILE }],
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const BEADS_EDIT_GATE_FILE = join(HOOKS_DIR, 'beads-edit-gate.mjs');
|
|
93
|
-
const BEADS_COMMIT_GATE_FILE = join(HOOKS_DIR, 'beads-commit-gate.mjs');
|
|
94
|
-
const BEADS_STOP_GATE_FILE = join(HOOKS_DIR, 'beads-stop-gate.mjs');
|
|
95
|
-
|
|
96
|
-
const BEADS_EDIT_GATE_ENTRY = {
|
|
97
|
-
matcher: 'Edit|Write|MultiEdit|NotebookEdit|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol',
|
|
98
|
-
hooks: [{ type: 'command', command: BEADS_EDIT_GATE_FILE, timeout: 10000 }],
|
|
99
|
-
};
|
|
100
|
-
const BEADS_COMMIT_GATE_ENTRY = {
|
|
101
|
-
matcher: 'Bash',
|
|
102
|
-
hooks: [{ type: 'command', command: BEADS_COMMIT_GATE_FILE, timeout: 10000 }],
|
|
103
|
-
};
|
|
104
|
-
const BEADS_STOP_GATE_ENTRY = {
|
|
105
|
-
hooks: [{ type: 'command', command: BEADS_STOP_GATE_FILE, timeout: 10000 }],
|
|
106
|
-
};
|
|
107
|
-
const BEADS_CLOSE_MEMORY_PROMPT_FILE = join(HOOKS_DIR, 'beads-close-memory-prompt.mjs');
|
|
108
|
-
const BEADS_CLOSE_MEMORY_PROMPT_ENTRY = {
|
|
109
|
-
matcher: 'Bash',
|
|
110
|
-
hooks: [{ type: 'command', command: BEADS_CLOSE_MEMORY_PROMPT_FILE, timeout: 10000 }],
|
|
111
|
-
};
|
|
112
|
-
const SPECIALISTS_COMPLETE_FILE = join(HOOKS_DIR, 'specialists-complete.mjs');
|
|
113
|
-
const SPECIALISTS_COMPLETE_ENTRY = {
|
|
114
|
-
hooks: [{ type: 'command', command: SPECIALISTS_COMPLETE_FILE, timeout: 5000 }],
|
|
67
|
+
function sameFileContent(a, b) {
|
|
68
|
+
if (!existsSync(a) || !existsSync(b)) return false;
|
|
69
|
+
return readFileSync(a, 'utf8') === readFileSync(b, 'utf8');
|
|
115
70
|
}
|
|
116
|
-
const SPECIALISTS_SESSION_START_FILE = join(HOOKS_DIR, 'specialists-session-start.mjs');
|
|
117
|
-
const SPECIALISTS_SESSION_START_ENTRY = {
|
|
118
|
-
hooks: [{ type: 'command', command: SPECIALISTS_SESSION_START_FILE, timeout: 8000 }],
|
|
119
|
-
};
|
|
120
|
-
const BUNDLED_SKILLS_DIR = new URL('../skills', import.meta.url).pathname;
|
|
121
|
-
const CLAUDE_SKILLS_DIR = join(CLAUDE_DIR, 'skills');;
|
|
122
71
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
72
|
+
const HOOKS = [
|
|
73
|
+
{
|
|
74
|
+
event: 'UserPromptSubmit',
|
|
75
|
+
file: 'specialists-complete.mjs',
|
|
76
|
+
timeout: 5000,
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
event: 'SessionStart',
|
|
80
|
+
file: 'specialists-session-start.mjs',
|
|
81
|
+
timeout: 8000,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
133
84
|
|
|
134
|
-
function
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
[
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
['specialists-session-start.mjs', SPECIALISTS_SESSION_START_FILE],
|
|
143
|
-
];
|
|
144
|
-
return pairs
|
|
145
|
-
.map(([bundled, dest]) => ({
|
|
146
|
-
name: bundled,
|
|
147
|
-
dest,
|
|
148
|
-
missing: !existsSync(dest),
|
|
149
|
-
changed: existsSync(dest) &&
|
|
150
|
-
readFileSync(join(BUNDLED_HOOKS_DIR, bundled), 'utf8') !==
|
|
151
|
-
readFileSync(dest, 'utf8'),
|
|
152
|
-
}))
|
|
153
|
-
.filter(h => h.missing || h.changed);
|
|
85
|
+
function findHookCommands(settings, event, fileName) {
|
|
86
|
+
const entries = settings?.hooks?.[event];
|
|
87
|
+
if (!Array.isArray(entries)) return [];
|
|
88
|
+
return entries.flatMap((entry) =>
|
|
89
|
+
(entry.hooks ?? [])
|
|
90
|
+
.map((hook) => hook.command)
|
|
91
|
+
.filter((command) => typeof command === 'string' && command.includes(fileName)),
|
|
92
|
+
);
|
|
154
93
|
}
|
|
155
94
|
|
|
156
|
-
function
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
chmodSync(HOOK_FILE, 0o755);
|
|
162
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-edit-gate.mjs'), BEADS_EDIT_GATE_FILE);
|
|
163
|
-
chmodSync(BEADS_EDIT_GATE_FILE, 0o755);
|
|
164
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-commit-gate.mjs'), BEADS_COMMIT_GATE_FILE);
|
|
165
|
-
chmodSync(BEADS_COMMIT_GATE_FILE, 0o755);
|
|
166
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-stop-gate.mjs'), BEADS_STOP_GATE_FILE);
|
|
167
|
-
chmodSync(BEADS_STOP_GATE_FILE, 0o755);
|
|
168
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-close-memory-prompt.mjs'), BEADS_CLOSE_MEMORY_PROMPT_FILE);
|
|
169
|
-
chmodSync(BEADS_CLOSE_MEMORY_PROMPT_FILE, 0o755);
|
|
170
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'specialists-complete.mjs'), SPECIALISTS_COMPLETE_FILE);
|
|
171
|
-
chmodSync(SPECIALISTS_COMPLETE_FILE, 0o755);
|
|
172
|
-
copyFileSync(join(BUNDLED_HOOKS_DIR, 'specialists-session-start.mjs'), SPECIALISTS_SESSION_START_FILE);
|
|
173
|
-
chmodSync(SPECIALISTS_SESSION_START_FILE, 0o755);
|
|
95
|
+
function ensureHook(settings, hook) {
|
|
96
|
+
const dest = join(HOOKS_DIR, hook.file);
|
|
97
|
+
const source = join(BUNDLED_HOOKS_DIR, hook.file);
|
|
98
|
+
const existingCommands = findHookCommands(settings, hook.event, hook.file);
|
|
99
|
+
const externalOwner = existingCommands.find((command) => command !== dest);
|
|
174
100
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
101
|
+
if (externalOwner) {
|
|
102
|
+
skip(`${hook.file} already managed externally — deferring`);
|
|
103
|
+
info(`existing command: ${externalOwner}`);
|
|
104
|
+
return false;
|
|
178
105
|
}
|
|
179
106
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
)
|
|
190
|
-
);
|
|
191
|
-
settings.hooks.PreToolUse.push(HOOK_ENTRY);
|
|
192
|
-
settings.hooks.PreToolUse.push(BEADS_EDIT_GATE_ENTRY);
|
|
193
|
-
settings.hooks.PreToolUse.push(BEADS_COMMIT_GATE_ENTRY);
|
|
194
|
-
|
|
195
|
-
// PostToolUse — replace any existing beads-close-memory-prompt entry
|
|
196
|
-
if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
|
|
197
|
-
settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e =>
|
|
198
|
-
!e.hooks?.some(h => h.command?.includes('beads-close-memory-prompt'))
|
|
199
|
-
);
|
|
200
|
-
settings.hooks.PostToolUse.push(BEADS_CLOSE_MEMORY_PROMPT_ENTRY);
|
|
201
|
-
|
|
202
|
-
// Stop — replace any existing beads-stop-gate entry
|
|
203
|
-
if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
|
|
204
|
-
settings.hooks.Stop = settings.hooks.Stop.filter(e =>
|
|
205
|
-
!e.hooks?.some(h => h.command?.includes('beads-stop-gate'))
|
|
206
|
-
);
|
|
207
|
-
settings.hooks.Stop.push(BEADS_STOP_GATE_ENTRY);
|
|
208
|
-
|
|
209
|
-
// UserPromptSubmit — replace any existing specialists-complete entry
|
|
210
|
-
if (!Array.isArray(settings.hooks.UserPromptSubmit)) settings.hooks.UserPromptSubmit = [];
|
|
211
|
-
settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(e =>
|
|
212
|
-
!e.hooks?.some(h => h.command?.includes('specialists-complete'))
|
|
213
|
-
);
|
|
214
|
-
settings.hooks.UserPromptSubmit.push(SPECIALISTS_COMPLETE_ENTRY);
|
|
107
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
108
|
+
const changed = !sameFileContent(source, dest);
|
|
109
|
+
if (changed) {
|
|
110
|
+
copyFileSync(source, dest);
|
|
111
|
+
chmodSync(dest, 0o755);
|
|
112
|
+
ok(`${hook.file} installed in .claude/hooks/`);
|
|
113
|
+
} else {
|
|
114
|
+
skip(`${hook.file} already up to date`);
|
|
115
|
+
}
|
|
215
116
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
settings.hooks.
|
|
219
|
-
!
|
|
117
|
+
settings.hooks ??= {};
|
|
118
|
+
settings.hooks[hook.event] ??= [];
|
|
119
|
+
settings.hooks[hook.event] = settings.hooks[hook.event].filter(
|
|
120
|
+
(entry) => !(entry.hooks ?? []).some((h) => h.command === dest),
|
|
220
121
|
);
|
|
221
|
-
settings.hooks.
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
122
|
+
settings.hooks[hook.event].push({
|
|
123
|
+
hooks: [{ type: 'command', command: dest, timeout: hook.timeout }],
|
|
124
|
+
});
|
|
125
|
+
ok(`${hook.file} registered for ${hook.event}`);
|
|
126
|
+
return true;
|
|
225
127
|
}
|
|
226
128
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
let installed = 0;
|
|
233
|
-
let skippedCount = 0;
|
|
234
|
-
let skillNames;
|
|
235
|
-
try {
|
|
236
|
-
skillNames = readdirSync(BUNDLED_SKILLS_DIR, { withFileTypes: true })
|
|
237
|
-
.filter(d => d.isDirectory())
|
|
238
|
-
.map(d => d.name);
|
|
239
|
-
} catch {
|
|
240
|
-
return { installed: 0, skipped: 0 };
|
|
129
|
+
function installBundledSpecialists() {
|
|
130
|
+
if (!existsSync(BUNDLED_SPECIALISTS_DIR)) {
|
|
131
|
+
skip('bundled specialists dir not found — skipping');
|
|
132
|
+
return;
|
|
241
133
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
// Check if content matches bundled version
|
|
253
|
-
try {
|
|
254
|
-
if (readFileSync(skillFile, 'utf8') === readFileSync(destSkillFile, 'utf8')) {
|
|
255
|
-
skippedCount++;
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
} catch { /* fall through to copy */ }
|
|
134
|
+
mkdirSync(USER_SPECIALISTS_DIR, { recursive: true });
|
|
135
|
+
const files = readdirSync(BUNDLED_SPECIALISTS_DIR).filter(f => f.endsWith('.specialist.yaml'));
|
|
136
|
+
for (const file of files) {
|
|
137
|
+
const source = join(BUNDLED_SPECIALISTS_DIR, file);
|
|
138
|
+
const dest = join(USER_SPECIALISTS_DIR, file);
|
|
139
|
+
if (sameFileContent(source, dest)) {
|
|
140
|
+
skip(`${file} already up to date`);
|
|
141
|
+
} else {
|
|
142
|
+
copyFileSync(source, dest);
|
|
143
|
+
ok(`${file} installed in ~/.agents/specialists/`);
|
|
259
144
|
}
|
|
260
|
-
|
|
261
|
-
mkdirSync(destDir, { recursive: true });
|
|
262
|
-
copyFileSync(skillFile, destSkillFile);
|
|
263
|
-
installed++;
|
|
264
145
|
}
|
|
265
|
-
|
|
266
|
-
return { installed, skipped: skippedCount };
|
|
267
146
|
}
|
|
268
147
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
if (isInstalled('bd')) {
|
|
285
|
-
skip('bd already installed');
|
|
286
|
-
} else {
|
|
287
|
-
info('Installing @beads/bd...');
|
|
288
|
-
npmInstallGlobal('@beads/bd');
|
|
289
|
-
ok('bd installed');
|
|
290
|
-
}
|
|
148
|
+
function ensureMcpRegistration() {
|
|
149
|
+
const mcp = loadJson(MCP_FILE, { mcpServers: {} });
|
|
150
|
+
mcp.mcpServers ??= {};
|
|
151
|
+
const existing = mcp.mcpServers.specialists;
|
|
152
|
+
const desired = { command: 'specialists', args: [] };
|
|
153
|
+
|
|
154
|
+
if (
|
|
155
|
+
existing &&
|
|
156
|
+
existing.command === desired.command &&
|
|
157
|
+
Array.isArray(existing.args) &&
|
|
158
|
+
existing.args.length === 0
|
|
159
|
+
) {
|
|
160
|
+
skip('.mcp.json already registers specialists');
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
291
163
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
skip('dolt already installed');
|
|
296
|
-
} else {
|
|
297
|
-
installDolt();
|
|
164
|
+
mcp.mcpServers.specialists = desired;
|
|
165
|
+
saveJson(MCP_FILE, mcp);
|
|
166
|
+
ok('registered specialists in .mcp.json');
|
|
298
167
|
}
|
|
299
168
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const registered = registerMCP();
|
|
303
|
-
registered
|
|
304
|
-
? ok(`MCP '${MCP_NAME}' registered at user scope`)
|
|
305
|
-
: skip(`MCP '${MCP_NAME}' already registered`);
|
|
169
|
+
console.log(`\n${bold('Specialists installer')}`);
|
|
170
|
+
console.log(dim('Project scope: prerequisite check, bundled specialists, hooks, MCP registration'));
|
|
306
171
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
172
|
+
section('Prerequisite check');
|
|
173
|
+
const prereqs = [
|
|
174
|
+
{ name: 'pi', ok: commandOk('pi', ['--version']), required: true, help: 'Install pi first.' },
|
|
175
|
+
{ name: 'bd', ok: commandOk('bd', ['--version']), required: true, help: 'Install beads (bd) first.' },
|
|
176
|
+
{ name: 'xt', ok: commandOk('xt', ['--version']), required: true, help: 'xtrm-tools is required for hooks and workflow integration.' },
|
|
177
|
+
];
|
|
310
178
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
let installed = 0;
|
|
316
|
-
let skipped = 0;
|
|
317
|
-
for (const file of yamlFiles) {
|
|
318
|
-
const dest = join(SPECIALISTS_DIR, file);
|
|
319
|
-
if (existsSync(dest)) {
|
|
320
|
-
skipped++;
|
|
179
|
+
let prereqFailed = false;
|
|
180
|
+
for (const prereq of prereqs) {
|
|
181
|
+
if (prereq.ok) {
|
|
182
|
+
ok(`${prereq.name} available`);
|
|
321
183
|
} else {
|
|
322
|
-
|
|
323
|
-
|
|
184
|
+
prereqFailed = prereqFailed || prereq.required;
|
|
185
|
+
fail(`${prereq.name} not found`);
|
|
186
|
+
info(prereq.help);
|
|
324
187
|
}
|
|
325
188
|
}
|
|
326
189
|
|
|
327
|
-
if (
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
info('Edit any .specialist.yaml in ~/.agents/specialists/ to customise models, prompts, permissions');
|
|
331
|
-
|
|
332
|
-
// 6. Claude Code hooks
|
|
333
|
-
section('Claude Code hooks');
|
|
334
|
-
const drift = getHookDrift();
|
|
335
|
-
const hooksExist = existsSync(HOOK_FILE);
|
|
336
|
-
|
|
337
|
-
if (!hooksExist) {
|
|
338
|
-
installHook();
|
|
339
|
-
ok('hooks installed → ~/.claude/hooks/');
|
|
340
|
-
} else if (drift.length === 0) {
|
|
341
|
-
skip('hooks up to date');
|
|
342
|
-
} else {
|
|
343
|
-
const label = (h) => h.missing ? red('missing') : yellow('updated');
|
|
344
|
-
console.log(` ${yellow('○')} ${drift.length} of 7 hook(s) have changes:`);
|
|
345
|
-
for (const h of drift) info(` ${h.name} ${label(h)}`);
|
|
346
|
-
console.log();
|
|
347
|
-
const confirmed = promptYN(' Update hooks?');
|
|
348
|
-
if (confirmed) {
|
|
349
|
-
installHook();
|
|
350
|
-
ok('hooks updated');
|
|
351
|
-
} else {
|
|
352
|
-
skip('hooks update skipped');
|
|
353
|
-
}
|
|
190
|
+
if (prereqFailed) {
|
|
191
|
+
console.log(`\n${red('Install aborted: required prerequisites are missing.')}`);
|
|
192
|
+
process.exit(1);
|
|
354
193
|
}
|
|
355
|
-
info('main-guard: blocks file edits and direct master pushes (enforces PR workflow)');
|
|
356
|
-
info('beads-edit-gate: requires in_progress bead before editing files');
|
|
357
|
-
info('beads-commit-gate: requires issues closed before git commit');
|
|
358
|
-
info('beads-stop-gate: requires issues closed before session end');
|
|
359
|
-
info('beads-close-memory-prompt: nudges knowledge capture after bd close');
|
|
360
|
-
info('specialists-complete: injects completion banners for background jobs');
|
|
361
|
-
info('specialists-session-start: injects context (jobs, specialists, commands) at session start');
|
|
362
194
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
const
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
info("specialists-usage: teaches agents when/how to use specialists CLI and MCP tools");
|
|
195
|
+
section('Specialists hooks');
|
|
196
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
197
|
+
const settings = loadJson(SETTINGS_FILE, {});
|
|
198
|
+
for (const hook of HOOKS) ensureHook(settings, hook);
|
|
199
|
+
saveJson(SETTINGS_FILE, settings);
|
|
200
|
+
ok(`updated ${SETTINGS_FILE}`);
|
|
370
201
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
? ok('pi has at least one active provider')
|
|
377
|
-
: skip('No active provider — run pi config to set one up');
|
|
378
|
-
}
|
|
202
|
+
section('Bundled specialists');
|
|
203
|
+
installBundledSpecialists();
|
|
204
|
+
|
|
205
|
+
section('MCP registration');
|
|
206
|
+
ensureMcpRegistration();
|
|
379
207
|
|
|
380
|
-
|
|
381
|
-
console.log('
|
|
382
|
-
console.log('\n' + bold(' Next steps:'));
|
|
383
|
-
console.log(` 1. ${bold('Configure pi:')} run ${yellow('pi')} then ${yellow('pi config')} to enable model providers`);
|
|
384
|
-
console.log(` 2. ${bold('Restart Claude Code')} to load the MCP and hooks`);
|
|
385
|
-
console.log(` 3. ${bold('Customise specialists:')} edit files in ${yellow('~/.agents/specialists/')}`);
|
|
386
|
-
console.log(` 4. ${bold('Update later:')} re-run this installer (existing specialists preserved)\n`);
|
|
208
|
+
console.log(`\n${bold(green('Done!'))}`);
|
|
209
|
+
console.log(` ${dim('Hooks are project-local in .claude/, and MCP is project-local in .mcp.json.')}`);
|