@claudetools/tools 0.1.2 → 0.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/dist/cli.js +38 -7
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +847 -159
- package/dist/watcher.d.ts +3 -0
- package/dist/watcher.js +307 -0
- package/package.json +6 -2
package/dist/setup.js
CHANGED
|
@@ -1,206 +1,894 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// ClaudeTools
|
|
2
|
+
// ClaudeTools Interactive Setup Wizard
|
|
3
3
|
// =============================================================================
|
|
4
|
-
// Guides users through
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
// Guides users through authentication, service configuration, and Claude Code integration
|
|
5
|
+
import prompts from 'prompts';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import { homedir, hostname, platform } from 'os';
|
|
9
|
+
import { join, basename } from 'path';
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
|
|
11
|
+
import { randomUUID } from 'crypto';
|
|
7
12
|
import { loadConfigFromFile, saveConfig, ensureConfigDir, getConfigPath, DEFAULT_CONFIG, } from './helpers/config-manager.js';
|
|
8
13
|
// -----------------------------------------------------------------------------
|
|
9
|
-
//
|
|
10
|
-
// -----------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
// Constants
|
|
15
|
+
// -----------------------------------------------------------------------------
|
|
16
|
+
const CLAUDE_DIR = join(homedir(), '.claude');
|
|
17
|
+
const CLAUDETOOLS_DIR = join(homedir(), '.claudetools');
|
|
18
|
+
const MCP_CONFIG_PATH = join(CLAUDE_DIR, 'mcp.json');
|
|
19
|
+
const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
|
|
20
|
+
const HOOKS_DIR = join(CLAUDE_DIR, 'hooks');
|
|
21
|
+
const SYSTEM_FILE = join(CLAUDETOOLS_DIR, 'system.json');
|
|
22
|
+
const PROJECTS_FILE = join(CLAUDETOOLS_DIR, 'projects.json');
|
|
23
|
+
// -----------------------------------------------------------------------------
|
|
24
|
+
// Utility Functions
|
|
25
|
+
// -----------------------------------------------------------------------------
|
|
26
|
+
function header(title) {
|
|
27
|
+
console.log('\n' + chalk.cyan('━'.repeat(50)));
|
|
28
|
+
console.log(chalk.cyan.bold(title));
|
|
29
|
+
console.log(chalk.cyan('━'.repeat(50)) + '\n');
|
|
30
|
+
}
|
|
31
|
+
function success(msg) {
|
|
32
|
+
console.log(chalk.green('✓ ') + msg);
|
|
33
|
+
}
|
|
34
|
+
function error(msg) {
|
|
35
|
+
console.log(chalk.red('✗ ') + msg);
|
|
36
|
+
}
|
|
37
|
+
function info(msg) {
|
|
38
|
+
console.log(chalk.blue('ℹ ') + msg);
|
|
39
|
+
}
|
|
40
|
+
// -----------------------------------------------------------------------------
|
|
41
|
+
// System Registration
|
|
42
|
+
// -----------------------------------------------------------------------------
|
|
43
|
+
async function registerSystem(apiUrl, apiKey) {
|
|
44
|
+
const spinner = ora('Registering system...').start();
|
|
45
|
+
try {
|
|
46
|
+
// First try to register with the API
|
|
47
|
+
const response = await fetch(`${apiUrl}/api/v1/systems/register`, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
hostname: hostname(),
|
|
55
|
+
platform: platform(),
|
|
56
|
+
}),
|
|
23
57
|
});
|
|
24
|
-
|
|
58
|
+
if (response.ok) {
|
|
59
|
+
const data = await response.json();
|
|
60
|
+
const systemInfo = {
|
|
61
|
+
user_id: data.user_id,
|
|
62
|
+
system_id: data.system_id,
|
|
63
|
+
hostname: hostname(),
|
|
64
|
+
platform: platform(),
|
|
65
|
+
created_at: new Date().toISOString(),
|
|
66
|
+
};
|
|
67
|
+
spinner.succeed('System registered with API');
|
|
68
|
+
return systemInfo;
|
|
69
|
+
}
|
|
70
|
+
// If API fails, generate local UUIDs
|
|
71
|
+
spinner.warn('API registration failed, using local UUIDs');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
spinner.warn('Could not reach API, using local UUIDs');
|
|
75
|
+
}
|
|
76
|
+
// Generate local UUIDs as fallback
|
|
77
|
+
const systemInfo = {
|
|
78
|
+
user_id: `user_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
79
|
+
system_id: `sys_${randomUUID().replace(/-/g, '').slice(0, 12)}`,
|
|
80
|
+
hostname: hostname(),
|
|
81
|
+
platform: platform(),
|
|
82
|
+
created_at: new Date().toISOString(),
|
|
83
|
+
};
|
|
84
|
+
return systemInfo;
|
|
85
|
+
}
|
|
86
|
+
function saveSystemInfo(systemInfo) {
|
|
87
|
+
if (!existsSync(CLAUDETOOLS_DIR)) {
|
|
88
|
+
mkdirSync(CLAUDETOOLS_DIR, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
writeFileSync(SYSTEM_FILE, JSON.stringify(systemInfo, null, 2));
|
|
91
|
+
}
|
|
92
|
+
function loadSystemInfo() {
|
|
93
|
+
if (existsSync(SYSTEM_FILE)) {
|
|
94
|
+
try {
|
|
95
|
+
return JSON.parse(readFileSync(SYSTEM_FILE, 'utf-8'));
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
25
102
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!answer) {
|
|
33
|
-
return defaultValue;
|
|
103
|
+
function initializeProjectsFile() {
|
|
104
|
+
if (!existsSync(PROJECTS_FILE)) {
|
|
105
|
+
writeFileSync(PROJECTS_FILE, JSON.stringify({
|
|
106
|
+
bindings: [],
|
|
107
|
+
last_sync: new Date().toISOString(),
|
|
108
|
+
}, null, 2));
|
|
34
109
|
}
|
|
35
|
-
return answer.toLowerCase().startsWith('y');
|
|
36
110
|
}
|
|
37
111
|
// -----------------------------------------------------------------------------
|
|
38
|
-
//
|
|
112
|
+
// Authentication
|
|
39
113
|
// -----------------------------------------------------------------------------
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
114
|
+
async function authenticateWithEmailPassword(apiUrl) {
|
|
115
|
+
const { email, password } = await prompts([
|
|
116
|
+
{
|
|
117
|
+
type: 'text',
|
|
118
|
+
name: 'email',
|
|
119
|
+
message: 'Email:',
|
|
120
|
+
validate: (v) => v.includes('@') || 'Enter a valid email',
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
type: 'password',
|
|
124
|
+
name: 'password',
|
|
125
|
+
message: 'Password:',
|
|
126
|
+
validate: (v) => v.length >= 8 || 'Password must be at least 8 characters',
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
if (!email || !password)
|
|
130
|
+
return null;
|
|
131
|
+
const spinner = ora('Authenticating...').start();
|
|
44
132
|
try {
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
133
|
+
const response = await fetch(`${apiUrl}/api/v1/auth/login`, {
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
body: JSON.stringify({ email, password }),
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
const data = await response.json().catch(() => ({}));
|
|
140
|
+
spinner.fail('Authentication failed');
|
|
141
|
+
error(data.message || `HTTP ${response.status}`);
|
|
142
|
+
return null;
|
|
50
143
|
}
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
144
|
+
const data = await response.json();
|
|
145
|
+
spinner.succeed('Authenticated');
|
|
146
|
+
return data;
|
|
147
|
+
}
|
|
148
|
+
catch (err) {
|
|
149
|
+
spinner.fail('Connection failed');
|
|
150
|
+
error(err instanceof Error ? err.message : String(err));
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function authenticateWithDeviceCode(apiUrl) {
|
|
155
|
+
const spinner = ora('Requesting device code...').start();
|
|
156
|
+
try {
|
|
157
|
+
const codeResponse = await fetch(`${apiUrl}/api/v1/auth/device/code`, {
|
|
158
|
+
method: 'POST',
|
|
159
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
160
|
});
|
|
55
|
-
|
|
161
|
+
if (!codeResponse.ok) {
|
|
162
|
+
spinner.fail('Failed to get device code');
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
const { device_code, user_code, verification_uri, expires_in, interval } = await codeResponse.json();
|
|
166
|
+
spinner.stop();
|
|
167
|
+
console.log('\n' + chalk.yellow.bold(' ' + user_code) + '\n');
|
|
168
|
+
console.log(` Open: ${chalk.underline(verification_uri)}`);
|
|
169
|
+
console.log(` Enter the code above to authenticate.\n`);
|
|
170
|
+
const pollSpinner = ora('Waiting for authentication...').start();
|
|
171
|
+
const pollInterval = (interval || 5) * 1000;
|
|
172
|
+
const expiresAt = Date.now() + (expires_in || 900) * 1000;
|
|
173
|
+
while (Date.now() < expiresAt) {
|
|
174
|
+
await new Promise((r) => setTimeout(r, pollInterval));
|
|
175
|
+
const tokenResponse = await fetch(`${apiUrl}/api/v1/auth/device/token`, {
|
|
176
|
+
method: 'POST',
|
|
177
|
+
headers: { 'Content-Type': 'application/json' },
|
|
178
|
+
body: JSON.stringify({ device_code }),
|
|
179
|
+
});
|
|
180
|
+
if (tokenResponse.ok) {
|
|
181
|
+
const data = await tokenResponse.json();
|
|
182
|
+
pollSpinner.succeed('Authenticated');
|
|
183
|
+
return data;
|
|
184
|
+
}
|
|
185
|
+
const errorData = await tokenResponse.json().catch(() => ({ error: 'unknown' }));
|
|
186
|
+
if (errorData.error === 'authorization_pending') {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
else if (errorData.error === 'slow_down') {
|
|
190
|
+
await new Promise((r) => setTimeout(r, 5000));
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
pollSpinner.fail('Authentication failed');
|
|
195
|
+
error(errorData.error || 'Unknown error');
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
pollSpinner.fail('Authentication timed out');
|
|
200
|
+
return null;
|
|
56
201
|
}
|
|
57
|
-
catch (
|
|
58
|
-
|
|
202
|
+
catch (err) {
|
|
203
|
+
spinner.fail('Connection failed');
|
|
204
|
+
error(err instanceof Error ? err.message : String(err));
|
|
205
|
+
return null;
|
|
59
206
|
}
|
|
60
207
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
208
|
+
async function signUp(apiUrl) {
|
|
209
|
+
const { email, password, confirmPassword } = await prompts([
|
|
210
|
+
{
|
|
211
|
+
type: 'text',
|
|
212
|
+
name: 'email',
|
|
213
|
+
message: 'Email:',
|
|
214
|
+
validate: (v) => v.includes('@') || 'Enter a valid email',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: 'password',
|
|
218
|
+
name: 'password',
|
|
219
|
+
message: 'Password:',
|
|
220
|
+
validate: (v) => v.length >= 8 || 'Password must be at least 8 characters',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: 'password',
|
|
224
|
+
name: 'confirmPassword',
|
|
225
|
+
message: 'Confirm password:',
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
if (!email || !password)
|
|
229
|
+
return null;
|
|
230
|
+
if (password !== confirmPassword) {
|
|
231
|
+
error('Passwords do not match');
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
const spinner = ora('Creating account...').start();
|
|
65
235
|
try {
|
|
66
|
-
const
|
|
67
|
-
const platform = process.platform;
|
|
68
|
-
const cwd = process.cwd();
|
|
69
|
-
const response = await fetch(`${config.apiUrl}/api/v1/systems/register`, {
|
|
236
|
+
const response = await fetch(`${apiUrl}/api/v1/auth/signup`, {
|
|
70
237
|
method: 'POST',
|
|
71
|
-
headers: {
|
|
72
|
-
|
|
73
|
-
'Authorization': config.apiKey ? `Bearer ${config.apiKey}` : '',
|
|
74
|
-
},
|
|
75
|
-
body: JSON.stringify({
|
|
76
|
-
name: hostname,
|
|
77
|
-
platform,
|
|
78
|
-
working_directory: cwd,
|
|
79
|
-
}),
|
|
238
|
+
headers: { 'Content-Type': 'application/json' },
|
|
239
|
+
body: JSON.stringify({ email, password }),
|
|
80
240
|
});
|
|
81
241
|
if (!response.ok) {
|
|
82
|
-
|
|
242
|
+
const data = await response.json().catch(() => ({}));
|
|
243
|
+
spinner.fail('Sign up failed');
|
|
244
|
+
error(data.message || `HTTP ${response.status}`);
|
|
245
|
+
return null;
|
|
83
246
|
}
|
|
84
247
|
const data = await response.json();
|
|
85
|
-
|
|
248
|
+
spinner.succeed('Account created');
|
|
249
|
+
return data;
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
spinner.fail('Connection failed');
|
|
253
|
+
error(err instanceof Error ? err.message : String(err));
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
async function runAuthFlow(apiUrl) {
|
|
258
|
+
const { authMethod } = await prompts({
|
|
259
|
+
type: 'select',
|
|
260
|
+
name: 'authMethod',
|
|
261
|
+
message: 'How would you like to authenticate?',
|
|
262
|
+
choices: [
|
|
263
|
+
{ title: 'Sign up (new account)', value: 'signup' },
|
|
264
|
+
{ title: 'Login with email/password', value: 'email' },
|
|
265
|
+
{ title: 'Login with device code (browser)', value: 'device' },
|
|
266
|
+
{ title: 'Skip (use existing API key)', value: 'skip' },
|
|
267
|
+
],
|
|
268
|
+
});
|
|
269
|
+
switch (authMethod) {
|
|
270
|
+
case 'signup':
|
|
271
|
+
return signUp(apiUrl);
|
|
272
|
+
case 'email':
|
|
273
|
+
return authenticateWithEmailPassword(apiUrl);
|
|
274
|
+
case 'device':
|
|
275
|
+
return authenticateWithDeviceCode(apiUrl);
|
|
276
|
+
case 'skip':
|
|
277
|
+
return null;
|
|
278
|
+
default:
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
// -----------------------------------------------------------------------------
|
|
283
|
+
// Projects Directory Configuration
|
|
284
|
+
// -----------------------------------------------------------------------------
|
|
285
|
+
async function configureProjectsDirectory() {
|
|
286
|
+
header('Projects Directory');
|
|
287
|
+
info('Where do you keep your code projects?');
|
|
288
|
+
console.log(chalk.dim('The watcher will monitor this directory for new projects.\n'));
|
|
289
|
+
// Suggest common locations
|
|
290
|
+
const homeDir = homedir();
|
|
291
|
+
const suggestions = [
|
|
292
|
+
join(homeDir, 'Projects'),
|
|
293
|
+
join(homeDir, 'projects'),
|
|
294
|
+
join(homeDir, 'code'),
|
|
295
|
+
join(homeDir, 'Code'),
|
|
296
|
+
join(homeDir, 'dev'),
|
|
297
|
+
join(homeDir, 'Development'),
|
|
298
|
+
join(homeDir, 'workspace'),
|
|
299
|
+
].filter(existsSync);
|
|
300
|
+
let projectsDir;
|
|
301
|
+
if (suggestions.length > 0) {
|
|
302
|
+
const { selectedDir } = await prompts({
|
|
303
|
+
type: 'select',
|
|
304
|
+
name: 'selectedDir',
|
|
305
|
+
message: 'Select your projects directory:',
|
|
306
|
+
choices: [
|
|
307
|
+
...suggestions.map(dir => ({ title: dir, value: dir })),
|
|
308
|
+
{ title: 'Enter custom path...', value: 'custom' },
|
|
309
|
+
],
|
|
310
|
+
});
|
|
311
|
+
if (selectedDir === 'custom') {
|
|
312
|
+
const { customDir } = await prompts({
|
|
313
|
+
type: 'text',
|
|
314
|
+
name: 'customDir',
|
|
315
|
+
message: 'Enter path to your projects directory:',
|
|
316
|
+
initial: join(homeDir, 'Projects'),
|
|
317
|
+
validate: (v) => {
|
|
318
|
+
if (!v)
|
|
319
|
+
return 'Path required';
|
|
320
|
+
const expanded = v.replace(/^~/, homeDir);
|
|
321
|
+
return existsSync(expanded) || `Directory not found: ${expanded}`;
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
projectsDir = customDir.replace(/^~/, homeDir);
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
projectsDir = selectedDir;
|
|
328
|
+
}
|
|
86
329
|
}
|
|
87
|
-
|
|
88
|
-
|
|
330
|
+
else {
|
|
331
|
+
const { customDir } = await prompts({
|
|
332
|
+
type: 'text',
|
|
333
|
+
name: 'customDir',
|
|
334
|
+
message: 'Enter path to your projects directory:',
|
|
335
|
+
initial: join(homeDir, 'Projects'),
|
|
336
|
+
});
|
|
337
|
+
projectsDir = customDir.replace(/^~/, homeDir);
|
|
338
|
+
// Create if doesn't exist
|
|
339
|
+
if (!existsSync(projectsDir)) {
|
|
340
|
+
const { create } = await prompts({
|
|
341
|
+
type: 'confirm',
|
|
342
|
+
name: 'create',
|
|
343
|
+
message: `Directory doesn't exist. Create it?`,
|
|
344
|
+
initial: true,
|
|
345
|
+
});
|
|
346
|
+
if (create) {
|
|
347
|
+
mkdirSync(projectsDir, { recursive: true });
|
|
348
|
+
success(`Created ${projectsDir}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
89
351
|
}
|
|
352
|
+
success(`Projects directory: ${projectsDir}`);
|
|
353
|
+
return [projectsDir];
|
|
90
354
|
}
|
|
91
355
|
// -----------------------------------------------------------------------------
|
|
92
|
-
//
|
|
356
|
+
// Service Configuration
|
|
357
|
+
// -----------------------------------------------------------------------------
|
|
358
|
+
async function configureServices() {
|
|
359
|
+
header('Service Configuration');
|
|
360
|
+
info('Configure optional integrations for enhanced functionality.\n');
|
|
361
|
+
// Context7
|
|
362
|
+
console.log(chalk.bold('Context7') + ' - Library documentation fetching');
|
|
363
|
+
console.log(chalk.dim('Provides up-to-date docs for npm packages, frameworks, etc.\n'));
|
|
364
|
+
const { context7ApiKey } = await prompts({
|
|
365
|
+
type: 'text',
|
|
366
|
+
name: 'context7ApiKey',
|
|
367
|
+
message: 'Context7 API key (leave blank to skip):',
|
|
368
|
+
});
|
|
369
|
+
// Sequential Thinking
|
|
370
|
+
console.log('\n' + chalk.bold('Sequential Thinking') + ' - Planning and reasoning MCP');
|
|
371
|
+
console.log(chalk.dim('Enables structured problem-solving and task planning.\n'));
|
|
372
|
+
const { sequentialThinking } = await prompts({
|
|
373
|
+
type: 'confirm',
|
|
374
|
+
name: 'sequentialThinking',
|
|
375
|
+
message: 'Enable Sequential Thinking?',
|
|
376
|
+
initial: true,
|
|
377
|
+
});
|
|
378
|
+
return {
|
|
379
|
+
context7ApiKey: context7ApiKey || undefined,
|
|
380
|
+
sequentialThinkingEnabled: sequentialThinking,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
// -----------------------------------------------------------------------------
|
|
384
|
+
// Claude Code Integration
|
|
385
|
+
// -----------------------------------------------------------------------------
|
|
386
|
+
function ensureClaudeDir() {
|
|
387
|
+
if (!existsSync(CLAUDE_DIR)) {
|
|
388
|
+
mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
function backupFile(filePath) {
|
|
392
|
+
if (existsSync(filePath)) {
|
|
393
|
+
const backupPath = `${filePath}.backup.${Date.now()}`;
|
|
394
|
+
copyFileSync(filePath, backupPath);
|
|
395
|
+
return backupPath;
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
async function configureMcpSettings(services) {
|
|
400
|
+
header('Claude Code MCP Configuration');
|
|
401
|
+
ensureClaudeDir();
|
|
402
|
+
// Read existing config
|
|
403
|
+
let mcpConfig = { mcpServers: {} };
|
|
404
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
405
|
+
try {
|
|
406
|
+
mcpConfig = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8'));
|
|
407
|
+
const backup = backupFile(MCP_CONFIG_PATH);
|
|
408
|
+
if (backup) {
|
|
409
|
+
info(`Backed up existing config to ${basename(backup)}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
error('Could not parse existing mcp.json, creating new one');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
// Ensure mcpServers object exists
|
|
417
|
+
if (!mcpConfig.mcpServers || typeof mcpConfig.mcpServers !== 'object') {
|
|
418
|
+
mcpConfig.mcpServers = {};
|
|
419
|
+
}
|
|
420
|
+
const servers = mcpConfig.mcpServers;
|
|
421
|
+
// Add claudetools
|
|
422
|
+
servers['claudetools_memory'] = {
|
|
423
|
+
command: 'claudetools',
|
|
424
|
+
};
|
|
425
|
+
success('Added claudetools_memory server');
|
|
426
|
+
// Add context7 if configured
|
|
427
|
+
if (services.context7ApiKey) {
|
|
428
|
+
servers['context7'] = {
|
|
429
|
+
command: 'npx',
|
|
430
|
+
args: ['-y', '@upstash/context7-mcp'],
|
|
431
|
+
env: {
|
|
432
|
+
CONTEXT7_API_KEY: services.context7ApiKey,
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
success('Added context7 server');
|
|
436
|
+
}
|
|
437
|
+
// Add sequential-thinking if enabled
|
|
438
|
+
if (services.sequentialThinkingEnabled) {
|
|
439
|
+
servers['sequential-thinking'] = {
|
|
440
|
+
command: 'npx',
|
|
441
|
+
args: ['-y', '@modelcontextprotocol/server-sequential-thinking'],
|
|
442
|
+
};
|
|
443
|
+
success('Added sequential-thinking server');
|
|
444
|
+
}
|
|
445
|
+
// Write config
|
|
446
|
+
writeFileSync(MCP_CONFIG_PATH, JSON.stringify(mcpConfig, null, 2));
|
|
447
|
+
success(`Saved MCP config to ${MCP_CONFIG_PATH}`);
|
|
448
|
+
}
|
|
449
|
+
async function installHooks() {
|
|
450
|
+
header('Claude Code Hooks Installation');
|
|
451
|
+
ensureClaudeDir();
|
|
452
|
+
if (!existsSync(HOOKS_DIR)) {
|
|
453
|
+
mkdirSync(HOOKS_DIR, { recursive: true });
|
|
454
|
+
}
|
|
455
|
+
// Session start hook - ensures watcher is running
|
|
456
|
+
const sessionStartHook = `#!/bin/bash
|
|
457
|
+
# ClaudeTools Session Start Hook
|
|
458
|
+
# Ensures the code watcher is running when Claude Code starts
|
|
459
|
+
|
|
460
|
+
# Skip if disabled
|
|
461
|
+
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then exit 0; fi
|
|
462
|
+
|
|
463
|
+
# Check if watcher is already running
|
|
464
|
+
WATCHER_PID_FILE="/tmp/claudetools-watcher.pid"
|
|
465
|
+
if [ -f "$WATCHER_PID_FILE" ]; then
|
|
466
|
+
PID=$(cat "$WATCHER_PID_FILE")
|
|
467
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
468
|
+
# Watcher is running
|
|
469
|
+
exit 0
|
|
470
|
+
fi
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
# Start watcher in background if claudetools is installed
|
|
474
|
+
if command -v claudetools &> /dev/null; then
|
|
475
|
+
nohup claudetools watch > /tmp/claudetools-watcher.log 2>&1 &
|
|
476
|
+
echo $! > "$WATCHER_PID_FILE"
|
|
477
|
+
fi
|
|
478
|
+
`;
|
|
479
|
+
const sessionStartPath = join(HOOKS_DIR, 'session-start.sh');
|
|
480
|
+
if (existsSync(sessionStartPath)) {
|
|
481
|
+
const backup = backupFile(sessionStartPath);
|
|
482
|
+
if (backup)
|
|
483
|
+
info(`Backed up existing hook to ${basename(backup)}`);
|
|
484
|
+
}
|
|
485
|
+
writeFileSync(sessionStartPath, sessionStartHook, { mode: 0o755 });
|
|
486
|
+
success('Installed session-start.sh hook');
|
|
487
|
+
// User prompt submit hook - injects context before each message
|
|
488
|
+
const userPromptHook = `#!/bin/bash
|
|
489
|
+
# ClaudeTools Context Injection Hook
|
|
490
|
+
# Automatically injects relevant memory context before each prompt
|
|
491
|
+
|
|
492
|
+
# Prevent recursion
|
|
493
|
+
LOCK_FILE="/tmp/claude-prompt-hook.lock"
|
|
494
|
+
if [ -f "$LOCK_FILE" ]; then exit 0; fi
|
|
495
|
+
touch "$LOCK_FILE"
|
|
496
|
+
trap "rm -f $LOCK_FILE" EXIT
|
|
497
|
+
|
|
498
|
+
# Skip if disabled
|
|
499
|
+
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then exit 0; fi
|
|
500
|
+
|
|
501
|
+
# Read config
|
|
502
|
+
CONFIG_FILE="$HOME/.claudetools/config.json"
|
|
503
|
+
if [ ! -f "$CONFIG_FILE" ]; then exit 0; fi
|
|
504
|
+
|
|
505
|
+
API_URL=$(jq -r '.apiUrl // "https://api.claudetools.dev"' "$CONFIG_FILE")
|
|
506
|
+
API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE")
|
|
507
|
+
|
|
508
|
+
if [ -z "$API_KEY" ]; then exit 0; fi
|
|
509
|
+
|
|
510
|
+
# Get current project from projects.json
|
|
511
|
+
PROJECT_FILE="$HOME/.claudetools/projects.json"
|
|
512
|
+
CWD=$(pwd)
|
|
513
|
+
PROJECT_ID=""
|
|
514
|
+
|
|
515
|
+
if [ -f "$PROJECT_FILE" ]; then
|
|
516
|
+
# Try to find project by path prefix
|
|
517
|
+
PROJECT_ID=$(jq -r --arg cwd "$CWD" '
|
|
518
|
+
.bindings[]? | select(.local_path != null) |
|
|
519
|
+
select($cwd | startswith(.local_path)) |
|
|
520
|
+
.project_id' "$PROJECT_FILE" 2>/dev/null | head -1)
|
|
521
|
+
fi
|
|
522
|
+
|
|
523
|
+
# Inject context (silent fail)
|
|
524
|
+
RESULT=$(curl -s -X POST "$API_URL/api/v1/context/inject" \\
|
|
525
|
+
-H "Authorization: Bearer $API_KEY" \\
|
|
526
|
+
-H "Content-Type: application/json" \\
|
|
527
|
+
-d "{\\"project_id\\": \\"$PROJECT_ID\\", \\"cwd\\": \\"$CWD\\"}" \\
|
|
528
|
+
2>/dev/null)
|
|
529
|
+
|
|
530
|
+
# Output context if available
|
|
531
|
+
if [ -n "$RESULT" ] && [ "$RESULT" != "null" ]; then
|
|
532
|
+
CONTEXT=$(echo "$RESULT" | jq -r '.context // empty' 2>/dev/null)
|
|
533
|
+
if [ -n "$CONTEXT" ]; then
|
|
534
|
+
echo "$CONTEXT"
|
|
535
|
+
fi
|
|
536
|
+
fi
|
|
537
|
+
`;
|
|
538
|
+
const userPromptPath = join(HOOKS_DIR, 'user-prompt-submit.sh');
|
|
539
|
+
if (existsSync(userPromptPath)) {
|
|
540
|
+
const backup = backupFile(userPromptPath);
|
|
541
|
+
if (backup)
|
|
542
|
+
info(`Backed up existing hook to ${basename(backup)}`);
|
|
543
|
+
}
|
|
544
|
+
writeFileSync(userPromptPath, userPromptHook, { mode: 0o755 });
|
|
545
|
+
success('Installed user-prompt-submit.sh hook');
|
|
546
|
+
// Post tool use hook - logs tool usage for learning
|
|
547
|
+
const postToolHook = `#!/bin/bash
|
|
548
|
+
# ClaudeTools Tool Usage Logger
|
|
549
|
+
# Logs tool executions for pattern learning
|
|
550
|
+
|
|
551
|
+
# Prevent recursion
|
|
552
|
+
LOCK_FILE="/tmp/claude-tool-hook.lock"
|
|
553
|
+
if [ -f "$LOCK_FILE" ]; then exit 0; fi
|
|
554
|
+
touch "$LOCK_FILE"
|
|
555
|
+
trap "rm -f $LOCK_FILE" EXIT
|
|
556
|
+
|
|
557
|
+
# Skip if disabled
|
|
558
|
+
if [ "$CLAUDE_DISABLE_HOOKS" = "1" ]; then exit 0; fi
|
|
559
|
+
|
|
560
|
+
# Read input from stdin
|
|
561
|
+
INPUT=$(cat)
|
|
562
|
+
|
|
563
|
+
# Read config
|
|
564
|
+
CONFIG_FILE="$HOME/.claudetools/config.json"
|
|
565
|
+
if [ ! -f "$CONFIG_FILE" ]; then exit 0; fi
|
|
566
|
+
|
|
567
|
+
API_URL=$(jq -r '.apiUrl // "https://api.claudetools.dev"' "$CONFIG_FILE")
|
|
568
|
+
API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE")
|
|
569
|
+
|
|
570
|
+
if [ -z "$API_KEY" ]; then exit 0; fi
|
|
571
|
+
|
|
572
|
+
# Log tool usage (silent fail)
|
|
573
|
+
curl -s -X POST "$API_URL/api/v1/tools/log" \\
|
|
574
|
+
-H "Authorization: Bearer $API_KEY" \\
|
|
575
|
+
-H "Content-Type: application/json" \\
|
|
576
|
+
-d "$INPUT" \\
|
|
577
|
+
2>/dev/null || true
|
|
578
|
+
`;
|
|
579
|
+
const postToolPath = join(HOOKS_DIR, 'post-tool-use.sh');
|
|
580
|
+
if (existsSync(postToolPath)) {
|
|
581
|
+
const backup = backupFile(postToolPath);
|
|
582
|
+
if (backup)
|
|
583
|
+
info(`Backed up existing hook to ${basename(backup)}`);
|
|
584
|
+
}
|
|
585
|
+
writeFileSync(postToolPath, postToolHook, { mode: 0o755 });
|
|
586
|
+
success('Installed post-tool-use.sh hook');
|
|
587
|
+
}
|
|
588
|
+
async function configureSettings() {
|
|
589
|
+
header('Claude Code Settings');
|
|
590
|
+
// Read existing settings
|
|
591
|
+
let settings = {};
|
|
592
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
593
|
+
try {
|
|
594
|
+
settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
595
|
+
const backup = backupFile(SETTINGS_PATH);
|
|
596
|
+
if (backup) {
|
|
597
|
+
info(`Backed up existing settings to ${basename(backup)}`);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
// Start fresh
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
// Initialize hooks if not present
|
|
605
|
+
if (!settings.hooks) {
|
|
606
|
+
settings.hooks = {};
|
|
607
|
+
}
|
|
608
|
+
const hooks = settings.hooks;
|
|
609
|
+
// Add SessionStart hook
|
|
610
|
+
if (!hooks.SessionStart) {
|
|
611
|
+
hooks.SessionStart = [];
|
|
612
|
+
}
|
|
613
|
+
const sessionStartHooks = hooks.SessionStart;
|
|
614
|
+
const hasSessionStart = sessionStartHooks.some(h => h.hooks?.some(hk => hk.command?.includes('session-start.sh')));
|
|
615
|
+
if (!hasSessionStart) {
|
|
616
|
+
sessionStartHooks.push({
|
|
617
|
+
matcher: '',
|
|
618
|
+
hooks: [{
|
|
619
|
+
type: 'command',
|
|
620
|
+
command: join(HOOKS_DIR, 'session-start.sh'),
|
|
621
|
+
timeout: 5,
|
|
622
|
+
}],
|
|
623
|
+
});
|
|
624
|
+
success('Added SessionStart hook to settings');
|
|
625
|
+
}
|
|
626
|
+
// Add UserPromptSubmit hook
|
|
627
|
+
if (!hooks.UserPromptSubmit) {
|
|
628
|
+
hooks.UserPromptSubmit = [];
|
|
629
|
+
}
|
|
630
|
+
const promptHooks = hooks.UserPromptSubmit;
|
|
631
|
+
const hasPromptHook = promptHooks.some(h => h.hooks?.some(hk => hk.command?.includes('user-prompt-submit.sh')));
|
|
632
|
+
if (!hasPromptHook) {
|
|
633
|
+
promptHooks.push({
|
|
634
|
+
matcher: '',
|
|
635
|
+
hooks: [{
|
|
636
|
+
type: 'command',
|
|
637
|
+
command: join(HOOKS_DIR, 'user-prompt-submit.sh'),
|
|
638
|
+
timeout: 10,
|
|
639
|
+
}],
|
|
640
|
+
});
|
|
641
|
+
success('Added UserPromptSubmit hook to settings');
|
|
642
|
+
}
|
|
643
|
+
// Add PostToolUse hook
|
|
644
|
+
if (!hooks.PostToolUse) {
|
|
645
|
+
hooks.PostToolUse = [];
|
|
646
|
+
}
|
|
647
|
+
const toolHooks = hooks.PostToolUse;
|
|
648
|
+
const hasToolHook = toolHooks.some(h => h.hooks?.some(hk => hk.command?.includes('post-tool-use.sh')));
|
|
649
|
+
if (!hasToolHook) {
|
|
650
|
+
toolHooks.push({
|
|
651
|
+
matcher: 'Edit|Write|Bash',
|
|
652
|
+
hooks: [{
|
|
653
|
+
type: 'command',
|
|
654
|
+
command: join(HOOKS_DIR, 'post-tool-use.sh'),
|
|
655
|
+
timeout: 5,
|
|
656
|
+
}],
|
|
657
|
+
});
|
|
658
|
+
success('Added PostToolUse hook to settings');
|
|
659
|
+
}
|
|
660
|
+
// Write settings
|
|
661
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
662
|
+
success(`Saved settings to ${SETTINGS_PATH}`);
|
|
663
|
+
}
|
|
664
|
+
// -----------------------------------------------------------------------------
|
|
665
|
+
// Verification
|
|
666
|
+
// -----------------------------------------------------------------------------
|
|
667
|
+
async function verifySetup(config) {
|
|
668
|
+
header('Verification');
|
|
669
|
+
const spinner = ora('Checking API connection...').start();
|
|
670
|
+
try {
|
|
671
|
+
const response = await fetch(`${config.apiUrl}/api/v1/health`, {
|
|
672
|
+
headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
|
|
673
|
+
signal: AbortSignal.timeout(10000),
|
|
674
|
+
});
|
|
675
|
+
if (response.ok) {
|
|
676
|
+
spinner.succeed('API connection verified');
|
|
677
|
+
}
|
|
678
|
+
else {
|
|
679
|
+
spinner.warn('API returned non-OK status');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
spinner.fail('Could not connect to API');
|
|
684
|
+
}
|
|
685
|
+
// Check system registration
|
|
686
|
+
if (existsSync(SYSTEM_FILE)) {
|
|
687
|
+
success('System registered');
|
|
688
|
+
}
|
|
689
|
+
else {
|
|
690
|
+
error('System not registered');
|
|
691
|
+
}
|
|
692
|
+
// Check MCP config exists
|
|
693
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
694
|
+
success('MCP config installed');
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
error('MCP config not found');
|
|
698
|
+
}
|
|
699
|
+
// Check hooks installed
|
|
700
|
+
const requiredHooks = ['session-start.sh', 'user-prompt-submit.sh', 'post-tool-use.sh'];
|
|
701
|
+
const installedHooks = requiredHooks.filter(h => existsSync(join(HOOKS_DIR, h)));
|
|
702
|
+
if (installedHooks.length === requiredHooks.length) {
|
|
703
|
+
success('All hooks installed');
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
error(`Missing hooks: ${requiredHooks.filter(h => !installedHooks.includes(h)).join(', ')}`);
|
|
707
|
+
}
|
|
708
|
+
// Check settings configured
|
|
709
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
710
|
+
success('Settings configured');
|
|
711
|
+
}
|
|
712
|
+
else {
|
|
713
|
+
error('Settings not found');
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
// -----------------------------------------------------------------------------
|
|
717
|
+
// Main Setup Flow
|
|
93
718
|
// -----------------------------------------------------------------------------
|
|
94
719
|
export async function runSetup() {
|
|
95
|
-
console.log('\n
|
|
96
|
-
console.log('
|
|
720
|
+
console.log('\n' + chalk.bold.cyan(' ClaudeTools Setup Wizard') + '\n');
|
|
721
|
+
console.log(' ' + chalk.dim('Persistent AI memory for Claude Code') + '\n');
|
|
97
722
|
try {
|
|
98
|
-
//
|
|
723
|
+
// Ensure config directory exists
|
|
99
724
|
await ensureConfigDir();
|
|
100
|
-
//
|
|
725
|
+
// Load existing config
|
|
101
726
|
const loadedConfig = await loadConfigFromFile();
|
|
102
|
-
// Merge with defaults to ensure all fields exist
|
|
103
727
|
let config = { ...DEFAULT_CONFIG, ...loadedConfig };
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
console.log('API Configuration');
|
|
107
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
108
|
-
const useDefaultApi = await confirm(`Use default API URL (${DEFAULT_CONFIG.apiUrl})?`, true);
|
|
109
|
-
if (!useDefaultApi) {
|
|
110
|
-
const customApiUrl = await prompt('Enter custom API URL');
|
|
111
|
-
if (customApiUrl) {
|
|
112
|
-
config.apiUrl = customApiUrl;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
// 4. API Key configuration
|
|
116
|
-
console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
117
|
-
console.log('Authentication');
|
|
118
|
-
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
|
|
728
|
+
// Step 1: Authentication
|
|
729
|
+
header('Authentication');
|
|
119
730
|
if (config.apiKey) {
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
731
|
+
info(`Existing API key found: ${config.apiKey.substring(0, 10)}...`);
|
|
732
|
+
const { replace } = await prompts({
|
|
733
|
+
type: 'confirm',
|
|
734
|
+
name: 'replace',
|
|
735
|
+
message: 'Replace existing authentication?',
|
|
736
|
+
initial: false,
|
|
737
|
+
});
|
|
738
|
+
if (replace) {
|
|
739
|
+
const auth = await runAuthFlow(config.apiUrl || DEFAULT_CONFIG.apiUrl);
|
|
740
|
+
if (auth) {
|
|
741
|
+
config.apiKey = auth.token;
|
|
742
|
+
success(`Logged in as ${auth.email}`);
|
|
126
743
|
}
|
|
127
744
|
}
|
|
128
745
|
}
|
|
129
746
|
else {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
747
|
+
const auth = await runAuthFlow(config.apiUrl || DEFAULT_CONFIG.apiUrl);
|
|
748
|
+
if (auth) {
|
|
749
|
+
config.apiKey = auth.token;
|
|
750
|
+
success(`Logged in as ${auth.email}`);
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
// Manual API key entry
|
|
754
|
+
const { apiKey } = await prompts({
|
|
755
|
+
type: 'text',
|
|
756
|
+
name: 'apiKey',
|
|
757
|
+
message: 'Enter API key manually (from claudetools.dev/dashboard):',
|
|
758
|
+
});
|
|
759
|
+
if (apiKey) {
|
|
760
|
+
config.apiKey = apiKey;
|
|
761
|
+
}
|
|
134
762
|
}
|
|
135
763
|
}
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
process.exit(1);
|
|
764
|
+
// Step 2: System Registration
|
|
765
|
+
header('System Registration');
|
|
766
|
+
const existingSystem = loadSystemInfo();
|
|
767
|
+
if (existingSystem) {
|
|
768
|
+
info(`System already registered: ${existingSystem.system_id}`);
|
|
769
|
+
}
|
|
770
|
+
else if (config.apiKey) {
|
|
771
|
+
const systemInfo = await registerSystem(config.apiUrl || DEFAULT_CONFIG.apiUrl, config.apiKey);
|
|
772
|
+
if (systemInfo) {
|
|
773
|
+
saveSystemInfo(systemInfo);
|
|
774
|
+
success(`System ID: ${systemInfo.system_id}`);
|
|
775
|
+
success(`User ID: ${systemInfo.user_id}`);
|
|
149
776
|
}
|
|
150
777
|
}
|
|
151
778
|
else {
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
779
|
+
info('Skipping system registration (no API key)');
|
|
780
|
+
}
|
|
781
|
+
// Initialize projects file
|
|
782
|
+
initializeProjectsFile();
|
|
783
|
+
// Step 3: Projects Directory
|
|
784
|
+
const projectDirs = await configureProjectsDirectory();
|
|
785
|
+
// Step 4: Service Configuration
|
|
786
|
+
const services = await configureServices();
|
|
787
|
+
// Store all configs
|
|
788
|
+
const extendedConfig = config;
|
|
789
|
+
if (services.context7ApiKey) {
|
|
790
|
+
extendedConfig.context7ApiKey = services.context7ApiKey;
|
|
791
|
+
}
|
|
792
|
+
extendedConfig.sequentialThinkingEnabled = services.sequentialThinkingEnabled;
|
|
793
|
+
extendedConfig.watchedDirectories = projectDirs;
|
|
794
|
+
// Step 5: Save ClaudeTools config
|
|
795
|
+
header('Saving Configuration');
|
|
796
|
+
await saveConfig(extendedConfig);
|
|
797
|
+
success(`Configuration saved to ${getConfigPath()}`);
|
|
798
|
+
// Step 6: Configure Claude Code MCP
|
|
799
|
+
await configureMcpSettings(services);
|
|
800
|
+
// Step 7: Install Hooks
|
|
801
|
+
await installHooks();
|
|
802
|
+
// Step 8: Configure Settings
|
|
803
|
+
await configureSettings();
|
|
804
|
+
// Step 9: Verify
|
|
805
|
+
await verifySetup(extendedConfig);
|
|
806
|
+
// Done
|
|
807
|
+
header('Setup Complete');
|
|
808
|
+
console.log(chalk.green(' ClaudeTools is now configured!\n'));
|
|
809
|
+
console.log(' ' + chalk.bold('Next step:') + ' Restart Claude Code\n');
|
|
810
|
+
console.log(' The memory system will activate automatically.\n');
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
console.error('\n' + chalk.red('Setup failed:'), err instanceof Error ? err.message : String(err));
|
|
814
|
+
process.exit(1);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
// -----------------------------------------------------------------------------
|
|
818
|
+
// Uninstall
|
|
819
|
+
// -----------------------------------------------------------------------------
|
|
820
|
+
export async function runUninstall() {
|
|
821
|
+
console.log('\n' + chalk.bold.red(' ClaudeTools Uninstall') + '\n');
|
|
822
|
+
const { confirm } = await prompts({
|
|
823
|
+
type: 'confirm',
|
|
824
|
+
name: 'confirm',
|
|
825
|
+
message: 'Remove ClaudeTools from Claude Code?',
|
|
826
|
+
initial: false,
|
|
827
|
+
});
|
|
828
|
+
if (!confirm) {
|
|
829
|
+
console.log('Cancelled.');
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
// Remove from MCP config
|
|
833
|
+
if (existsSync(MCP_CONFIG_PATH)) {
|
|
834
|
+
try {
|
|
835
|
+
const mcpConfig = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8'));
|
|
836
|
+
if (mcpConfig.mcpServers) {
|
|
837
|
+
delete mcpConfig.mcpServers['claudetools_memory'];
|
|
838
|
+
writeFileSync(MCP_CONFIG_PATH, JSON.stringify(mcpConfig, null, 2));
|
|
839
|
+
success('Removed from MCP config');
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
error('Could not update MCP config');
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
// Remove hooks from settings
|
|
847
|
+
if (existsSync(SETTINGS_PATH)) {
|
|
848
|
+
try {
|
|
849
|
+
const settings = JSON.parse(readFileSync(SETTINGS_PATH, 'utf-8'));
|
|
850
|
+
if (settings.hooks) {
|
|
851
|
+
// Remove claudetools hooks
|
|
852
|
+
for (const hookType of ['SessionStart', 'UserPromptSubmit', 'PostToolUse']) {
|
|
853
|
+
if (settings.hooks[hookType]) {
|
|
854
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter((h) => !h.hooks?.some(hk => hk.command?.includes('.claudetools') || hk.command?.includes('claudetools')));
|
|
855
|
+
}
|
|
177
856
|
}
|
|
857
|
+
writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
858
|
+
success('Removed hooks from settings');
|
|
178
859
|
}
|
|
179
860
|
}
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
861
|
+
catch {
|
|
862
|
+
error('Could not update settings');
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
// Remove hook scripts
|
|
866
|
+
const hooks = ['session-start.sh', 'user-prompt-submit.sh', 'post-tool-use.sh'];
|
|
867
|
+
for (const hook of hooks) {
|
|
868
|
+
const hookPath = join(HOOKS_DIR, hook);
|
|
869
|
+
if (existsSync(hookPath)) {
|
|
870
|
+
const content = readFileSync(hookPath, 'utf-8');
|
|
871
|
+
if (content.includes('ClaudeTools')) {
|
|
872
|
+
const { unlinkSync } = await import('fs');
|
|
873
|
+
unlinkSync(hookPath);
|
|
874
|
+
success(`Removed ${hook}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// Stop watcher if running
|
|
879
|
+
const pidFile = '/tmp/claudetools-watcher.pid';
|
|
880
|
+
if (existsSync(pidFile)) {
|
|
881
|
+
try {
|
|
882
|
+
const pid = readFileSync(pidFile, 'utf-8').trim();
|
|
883
|
+
process.kill(parseInt(pid), 'SIGTERM');
|
|
884
|
+
const { unlinkSync } = await import('fs');
|
|
885
|
+
unlinkSync(pidFile);
|
|
886
|
+
success('Stopped watcher');
|
|
887
|
+
}
|
|
888
|
+
catch {
|
|
889
|
+
// Process might already be dead
|
|
890
|
+
}
|
|
205
891
|
}
|
|
892
|
+
console.log('\n' + chalk.green('ClaudeTools removed from Claude Code.'));
|
|
893
|
+
console.log(chalk.dim('Your ~/.claudetools/ config and data are preserved.\n'));
|
|
206
894
|
}
|