@jaggerxtrm/specialists 3.0.2 → 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/bin/install.js CHANGED
@@ -1,319 +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 { existsSync, mkdirSync, writeFileSync, readFileSync, copyFileSync, readdirSync, chmodSync } from 'node:fs';
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 HOME = homedir();
11
- const SPECIALISTS_DIR = join(HOME, '.agents', 'specialists');
12
- const CLAUDE_DIR = join(HOME, '.claude');
13
- const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
14
- const SETTINGS_FILE = join(CLAUDE_DIR, 'settings.json');
15
- const HOOK_FILE = join(HOOKS_DIR, 'specialists-main-guard.mjs');
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 BUNDLED_HOOKS_DIR = new URL('../hooks', import.meta.url).pathname;
23
+ const USER_SPECIALISTS_DIR = join(homedir(), '.agents', 'specialists');
22
24
 
23
- // ── ANSI helpers ──────────────────────────────────────────────────────────────
24
- const dim = (s) => `\x1b[2m${s}\x1b[0m`;
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 = (s) => `\x1b[1m${s}\x1b[0m`;
28
- const red = (s) => `\x1b[31m${s}\x1b[0m`;
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, 40 - label.length));
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) { console.log(` ${green('✓')} ${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 isInstalled(cmd) {
41
- const r = spawnSync('which', [cmd], { encoding: 'utf8' });
42
- return r.status === 0 && r.stdout.trim().length > 0;
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 npmInstallGlobal(pkg) {
46
- const r = spawnSync('npm', ['install', '-g', pkg], { stdio: 'inherit', encoding: 'utf8' });
47
- if (r.status !== 0) throw new Error(`npm install -g ${pkg} failed`);
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 installDolt() {
51
- if (process.platform === 'darwin') {
52
- info('Installing dolt via brew...');
53
- const r = spawnSync('brew', ['install', 'dolt'], { stdio: 'inherit', encoding: 'utf8' });
54
- r.status === 0 ? ok('dolt installed') : fail('brew install dolt failed — install manually: brew install dolt');
55
- } else {
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 registerMCP() {
71
- const check = spawnSync('claude', ['mcp', 'get', MCP_NAME], { encoding: 'utf8' });
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
- // ── Hook installation ─────────────────────────────────────────────────────────
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 }],
115
- };
116
-
117
- function promptYN(question) {
118
- if (!process.stdin.isTTY) return true; // non-interactive: default yes
119
- process.stdout.write(`${question} [Y/n]: `);
120
- const r = spawnSync('/bin/sh', ['-c', 'read ans; printf "%s" "$ans"'], {
121
- stdio: ['inherit', 'pipe', 'inherit'],
122
- encoding: 'utf8',
123
- });
124
- const ans = (r.stdout ?? '').trim().toLowerCase();
125
- return ans === '' || ans === 'y' || ans === 'yes';
67
+ function sameFileContent(a, b) {
68
+ if (!existsSync(a) || !existsSync(b)) return false;
69
+ return readFileSync(a, 'utf8') === readFileSync(b, 'utf8');
126
70
  }
127
71
 
128
- function getHookDrift() {
129
- const pairs = [
130
- ['specialists-main-guard.mjs', HOOK_FILE],
131
- ['beads-edit-gate.mjs', BEADS_EDIT_GATE_FILE],
132
- ['beads-commit-gate.mjs', BEADS_COMMIT_GATE_FILE],
133
- ['beads-stop-gate.mjs', BEADS_STOP_GATE_FILE],
134
- ['beads-close-memory-prompt.mjs', BEADS_CLOSE_MEMORY_PROMPT_FILE],
135
- ['specialists-complete.mjs', SPECIALISTS_COMPLETE_FILE],
136
- ];
137
- return pairs
138
- .map(([bundled, dest]) => ({
139
- name: bundled,
140
- dest,
141
- missing: !existsSync(dest),
142
- changed: existsSync(dest) &&
143
- readFileSync(join(BUNDLED_HOOKS_DIR, bundled), 'utf8') !==
144
- readFileSync(dest, 'utf8'),
145
- }))
146
- .filter(h => h.missing || h.changed);
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
+ ];
84
+
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
+ );
147
93
  }
148
94
 
149
- function installHook() {
150
- mkdirSync(HOOKS_DIR, { recursive: true });
151
-
152
- // Copy hook files from bundled hooks/ directory
153
- copyFileSync(join(BUNDLED_HOOKS_DIR, 'specialists-main-guard.mjs'), HOOK_FILE);
154
- chmodSync(HOOK_FILE, 0o755);
155
- copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-edit-gate.mjs'), BEADS_EDIT_GATE_FILE);
156
- chmodSync(BEADS_EDIT_GATE_FILE, 0o755);
157
- copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-commit-gate.mjs'), BEADS_COMMIT_GATE_FILE);
158
- chmodSync(BEADS_COMMIT_GATE_FILE, 0o755);
159
- copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-stop-gate.mjs'), BEADS_STOP_GATE_FILE);
160
- chmodSync(BEADS_STOP_GATE_FILE, 0o755);
161
- copyFileSync(join(BUNDLED_HOOKS_DIR, 'beads-close-memory-prompt.mjs'), BEADS_CLOSE_MEMORY_PROMPT_FILE);
162
- chmodSync(BEADS_CLOSE_MEMORY_PROMPT_FILE, 0o755);
163
- copyFileSync(join(BUNDLED_HOOKS_DIR, 'specialists-complete.mjs'), SPECIALISTS_COMPLETE_FILE);
164
- chmodSync(SPECIALISTS_COMPLETE_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);
165
100
 
166
- let settings = {};
167
- if (existsSync(SETTINGS_FILE)) {
168
- try { settings = JSON.parse(readFileSync(SETTINGS_FILE, 'utf8')); } catch {}
101
+ if (externalOwner) {
102
+ skip(`${hook.file} already managed externally — deferring`);
103
+ info(`existing command: ${externalOwner}`);
104
+ return false;
169
105
  }
170
106
 
171
- settings.hooks = settings.hooks ?? {};
172
-
173
- // PreToolUse — replace any existing specialists-managed entries
174
- if (!Array.isArray(settings.hooks.PreToolUse)) settings.hooks.PreToolUse = [];
175
- settings.hooks.PreToolUse = settings.hooks.PreToolUse.filter(e =>
176
- !e.hooks?.some(h =>
177
- h.command?.includes('specialists-main-guard') ||
178
- h.command?.includes('beads-edit-gate') ||
179
- h.command?.includes('beads-commit-gate')
180
- )
181
- );
182
- settings.hooks.PreToolUse.push(HOOK_ENTRY);
183
- settings.hooks.PreToolUse.push(BEADS_EDIT_GATE_ENTRY);
184
- settings.hooks.PreToolUse.push(BEADS_COMMIT_GATE_ENTRY);
185
-
186
- // PostToolUse — replace any existing beads-close-memory-prompt entry
187
- if (!Array.isArray(settings.hooks.PostToolUse)) settings.hooks.PostToolUse = [];
188
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e =>
189
- !e.hooks?.some(h => h.command?.includes('beads-close-memory-prompt'))
190
- );
191
- settings.hooks.PostToolUse.push(BEADS_CLOSE_MEMORY_PROMPT_ENTRY);
192
-
193
- // Stop — replace any existing beads-stop-gate entry
194
- if (!Array.isArray(settings.hooks.Stop)) settings.hooks.Stop = [];
195
- settings.hooks.Stop = settings.hooks.Stop.filter(e =>
196
- !e.hooks?.some(h => h.command?.includes('beads-stop-gate'))
197
- );
198
- settings.hooks.Stop.push(BEADS_STOP_GATE_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
+ }
199
116
 
200
- // UserPromptSubmit — replace any existing specialists-complete entry
201
- if (!Array.isArray(settings.hooks.UserPromptSubmit)) settings.hooks.UserPromptSubmit = [];
202
- settings.hooks.UserPromptSubmit = settings.hooks.UserPromptSubmit.filter(e =>
203
- !e.hooks?.some(h => h.command?.includes('specialists-complete'))
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),
204
121
  );
205
- settings.hooks.UserPromptSubmit.push(SPECIALISTS_COMPLETE_ENTRY);
206
-
207
- mkdirSync(CLAUDE_DIR, { recursive: true });
208
- writeFileSync(SETTINGS_FILE, JSON.stringify(settings, null, 2) + '\n', 'utf8');
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;
209
127
  }
210
128
 
211
- // ── Main ──────────────────────────────────────────────────────────────────────
212
- console.log('\n' + bold(' Specialists — full-stack installer'));
213
-
214
- // 1. pi
215
- section('pi (coding agent runtime)');
216
- if (isInstalled('pi')) {
217
- skip('pi already installed');
218
- } else {
219
- info('Installing @mariozechner/pi-coding-agent...');
220
- npmInstallGlobal('@mariozechner/pi-coding-agent');
221
- ok('pi installed');
129
+ function installBundledSpecialists() {
130
+ if (!existsSync(BUNDLED_SPECIALISTS_DIR)) {
131
+ skip('bundled specialists dir not found — skipping');
132
+ return;
133
+ }
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/`);
144
+ }
145
+ }
222
146
  }
223
147
 
224
- // 2. beads
225
- section('beads (issue tracker)');
226
- if (isInstalled('bd')) {
227
- skip('bd already installed');
228
- } else {
229
- info('Installing @beads/bd...');
230
- npmInstallGlobal('@beads/bd');
231
- ok('bd installed');
232
- }
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
+ }
233
163
 
234
- // 3. dolt
235
- section('dolt (beads sync backend)');
236
- if (isInstalled('dolt')) {
237
- skip('dolt already installed');
238
- } else {
239
- installDolt();
164
+ mcp.mcpServers.specialists = desired;
165
+ saveJson(MCP_FILE, mcp);
166
+ ok('registered specialists in .mcp.json');
240
167
  }
241
168
 
242
- // 4. Specialists MCP
243
- section('Specialists MCP');
244
- const registered = registerMCP();
245
- registered
246
- ? ok(`MCP '${MCP_NAME}' registered at user scope`)
247
- : skip(`MCP '${MCP_NAME}' already registered`);
248
-
249
- // 5. Scaffold + copy built-in specialists
250
- section('Specialists');
251
- mkdirSync(SPECIALISTS_DIR, { recursive: true });
169
+ console.log(`\n${bold('Specialists installer')}`);
170
+ console.log(dim('Project scope: prerequisite check, bundled specialists, hooks, MCP registration'));
252
171
 
253
- const yamlFiles = existsSync(BUNDLED_SPECIALISTS_DIR)
254
- ? readdirSync(BUNDLED_SPECIALISTS_DIR).filter(f => f.endsWith('.specialist.yaml'))
255
- : [];
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
+ ];
256
178
 
257
- let installed = 0;
258
- let skipped = 0;
259
- for (const file of yamlFiles) {
260
- const dest = join(SPECIALISTS_DIR, file);
261
- if (existsSync(dest)) {
262
- skipped++;
179
+ let prereqFailed = false;
180
+ for (const prereq of prereqs) {
181
+ if (prereq.ok) {
182
+ ok(`${prereq.name} available`);
263
183
  } else {
264
- copyFileSync(join(BUNDLED_SPECIALISTS_DIR, file), dest);
265
- installed++;
184
+ prereqFailed = prereqFailed || prereq.required;
185
+ fail(`${prereq.name} not found`);
186
+ info(prereq.help);
266
187
  }
267
188
  }
268
189
 
269
- if (installed > 0) ok(`${installed} specialist(s) installed → ~/.agents/specialists/`);
270
- if (skipped > 0) skip(`${skipped} specialist(s) already exist (user-modified, keeping)`);
271
- if (installed === 0 && skipped === 0) skip('No built-in specialists found');
272
- info('Edit any .specialist.yaml in ~/.agents/specialists/ to customise models, prompts, permissions');
190
+ if (prereqFailed) {
191
+ console.log(`\n${red('Install aborted: required prerequisites are missing.')}`);
192
+ process.exit(1);
193
+ }
273
194
 
274
- // 6. Claude Code hooks
275
- section('Claude Code hooks');
276
- const drift = getHookDrift();
277
- const hooksExist = existsSync(HOOK_FILE);
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}`);
278
201
 
279
- if (!hooksExist) {
280
- installHook();
281
- ok('hooks installed → ~/.claude/hooks/');
282
- } else if (drift.length === 0) {
283
- skip('hooks up to date');
284
- } else {
285
- const label = (h) => h.missing ? red('missing') : yellow('updated');
286
- console.log(` ${yellow('○')} ${drift.length} of 6 hook(s) have changes:`);
287
- for (const h of drift) info(` ${h.name} ${label(h)}`);
288
- console.log();
289
- const confirmed = promptYN(' Update hooks?');
290
- if (confirmed) {
291
- installHook();
292
- ok('hooks updated');
293
- } else {
294
- skip('hooks update skipped');
295
- }
296
- }
297
- info('main-guard: blocks file edits and direct master pushes (enforces PR workflow)');
298
- info('beads-edit-gate: requires in_progress bead before editing files');
299
- info('beads-commit-gate: requires issues closed before git commit');
300
- info('beads-stop-gate: requires issues closed before session end');
301
- info('beads-close-memory-prompt: nudges knowledge capture after bd close');
302
- info('specialists-complete: injects completion banners for background jobs');
202
+ section('Bundled specialists');
203
+ installBundledSpecialists();
303
204
 
304
- // 7. Health check
305
- section('Health check');
306
- if (isInstalled('pi')) {
307
- const r = spawnSync('pi', ['--list-models'], { encoding: 'utf8' });
308
- r.status === 0
309
- ? ok('pi has at least one active provider')
310
- : skip('No active provider — run pi config to set one up');
311
- }
205
+ section('MCP registration');
206
+ ensureMcpRegistration();
312
207
 
313
- // 8. Done
314
- console.log('\n' + bold(green(' Done!')));
315
- console.log('\n' + bold(' Next steps:'));
316
- console.log(` 1. ${bold('Configure pi:')} run ${yellow('pi')} then ${yellow('pi config')} to enable model providers`);
317
- console.log(` 2. ${bold('Restart Claude Code')} to load the MCP and hooks`);
318
- console.log(` 3. ${bold('Customise specialists:')} edit files in ${yellow('~/.agents/specialists/')}`);
319
- 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.')}`);