@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/setup.js CHANGED
@@ -1,206 +1,894 @@
1
1
  // =============================================================================
2
- // ClaudeTools Memory Interactive Setup Wizard
2
+ // ClaudeTools Interactive Setup Wizard
3
3
  // =============================================================================
4
- // Guides users through initial configuration with validation
5
- import readline from 'readline';
6
- import { stdin as input, stdout as output } from 'process';
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
- // Prompt Helpers
10
- // -----------------------------------------------------------------------------
11
- /**
12
- * Prompt user for input with optional default value
13
- */
14
- function prompt(question, defaultValue) {
15
- const rl = readline.createInterface({ input, output });
16
- return new Promise((resolve) => {
17
- const displayQuestion = defaultValue
18
- ? `${question} [${defaultValue}]: `
19
- : `${question}: `;
20
- rl.question(displayQuestion, (answer) => {
21
- rl.close();
22
- resolve(answer.trim() || defaultValue || '');
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
- * Prompt user for yes/no confirmation
28
- */
29
- async function confirm(question, defaultValue = true) {
30
- const defaultStr = defaultValue ? 'Y/n' : 'y/N';
31
- const answer = await prompt(`${question} (${defaultStr})`);
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
- // API Validation
112
+ // Authentication
39
113
  // -----------------------------------------------------------------------------
40
- /**
41
- * Validate API connection and authentication
42
- */
43
- async function validateApiConnection(apiUrl, apiKey) {
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 headers = {
46
- 'Content-Type': 'application/json',
47
- };
48
- if (apiKey) {
49
- headers['Authorization'] = `Bearer ${apiKey}`;
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 response = await fetch(`${apiUrl}/health`, {
52
- headers,
53
- signal: AbortSignal.timeout(10000), // 10 second timeout
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
- return response.ok;
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 (error) {
58
- return false;
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
- * Register this system with the ClaudeTools API
63
- */
64
- async function registerSystem(config) {
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 hostname = require('os').hostname();
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
- 'Content-Type': 'application/json',
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
- throw new Error(`Registration failed: ${response.status} ${response.statusText}`);
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
- return data.system_id;
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
- catch (error) {
88
- throw new Error(`Failed to register system: ${error instanceof Error ? error.message : String(error)}`);
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
- // Setup Wizard
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🧠 ClaudeTools Memory Setup\n');
96
- console.log('This wizard will guide you through the initial configuration.\n');
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
- // 1. Ensure config directory exists
723
+ // Ensure config directory exists
99
724
  await ensureConfigDir();
100
- // 2. Load existing config or use defaults
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
- // 3. API URL configuration
105
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
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
- console.log(`Current API key: ${config.apiKey.substring(0, 10)}...`);
121
- const replaceKey = await confirm('Replace existing API key?', false);
122
- if (replaceKey) {
123
- const newApiKey = await prompt('Enter new API key (from claudetools.dev/dashboard)');
124
- if (newApiKey) {
125
- config.apiKey = newApiKey;
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
- console.log('Get your API key from: https://claudetools.dev/dashboard\n');
131
- const apiKey = await prompt('Enter API key (leave blank to skip)');
132
- if (apiKey) {
133
- config.apiKey = apiKey;
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
- // 5. Validate API connection
137
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
138
- console.log('Validation');
139
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
140
- console.log('Validating API connection...');
141
- const isValid = await validateApiConnection(config.apiUrl || DEFAULT_CONFIG.apiUrl, config.apiKey);
142
- if (!isValid) {
143
- console.error('❌ Could not connect to API.');
144
- console.error(' Check your API URL and key, or continue without validation.\n');
145
- const continueAnyway = await confirm('Continue anyway?', false);
146
- if (!continueAnyway) {
147
- console.log('\nSetup cancelled.');
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
- console.log(' API connection validated');
153
- }
154
- // 6. Auto-inject context configuration
155
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
156
- console.log('Context Injection');
157
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
158
- console.log('Auto-inject context automatically adds relevant memory to every request.');
159
- console.log('This uses tokens but provides more contextual responses.\n');
160
- const enableAutoInject = await confirm('Enable automatic context injection?', config.autoInjectContext);
161
- config.autoInjectContext = enableAutoInject;
162
- // 7. System registration (optional)
163
- if (isValid && config.apiKey) {
164
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
165
- console.log('System Registration');
166
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
167
- const shouldRegister = await confirm('Register this system?', true);
168
- if (shouldRegister) {
169
- try {
170
- console.log('Registering system...');
171
- const systemId = await registerSystem(config);
172
- console.log(`✅ System registered: ${systemId}`);
173
- }
174
- catch (error) {
175
- console.error(`❌ Registration failed: ${error instanceof Error ? error.message : String(error)}`);
176
- console.error(' You can continue without registration.');
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
- // 8. Save configuration
181
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
182
- console.log('Saving Configuration');
183
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
184
- await saveConfig(config);
185
- console.log(`✅ Configuration saved to ${getConfigPath()}`);
186
- // 9. Show next steps
187
- console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
188
- console.log('Next Steps');
189
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');
190
- console.log('1. Add to Claude Code MCP settings:');
191
- console.log(' Location: ~/.claude/mcp.json\n');
192
- console.log(' {');
193
- console.log(' "mcpServers": {');
194
- console.log(' "claudetools-memory": {');
195
- console.log(' "command": "claudetools-memory"');
196
- console.log(' }');
197
- console.log(' }');
198
- console.log(' }\n');
199
- console.log('2. Restart Claude Code\n');
200
- console.log('Done! 🎉\n');
201
- }
202
- catch (error) {
203
- console.error('\n❌ Setup failed:', error instanceof Error ? error.message : String(error));
204
- process.exit(1);
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
  }