@claudetools/tools 0.2.0 → 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 CHANGED
@@ -9,6 +9,7 @@ import { fileURLToPath } from 'node:url';
9
9
  import { dirname, join } from 'node:path';
10
10
  import { runSetup, runUninstall } from './setup.js';
11
11
  import { startServer } from './index.js';
12
+ import { startWatcher, stopWatcher, watcherStatus } from './watcher.js';
12
13
  // Get version from package.json
13
14
  const __filename = fileURLToPath(import.meta.url);
14
15
  const __dirname = dirname(__filename);
@@ -16,14 +17,14 @@ const packagePath = join(__dirname, '..', 'package.json');
16
17
  const packageJson = JSON.parse(readFileSync(packagePath, 'utf-8'));
17
18
  const version = packageJson.version;
18
19
  // Parse command-line arguments
19
- const { values } = parseArgs({
20
+ const { values, positionals } = parseArgs({
20
21
  options: {
21
22
  setup: { type: 'boolean', short: 's' },
22
23
  uninstall: { type: 'boolean', short: 'u' },
23
24
  version: { type: 'boolean', short: 'v' },
24
25
  help: { type: 'boolean', short: 'h' },
25
26
  },
26
- allowPositionals: false,
27
+ allowPositionals: true,
27
28
  });
28
29
  // Handle version flag
29
30
  if (values.version) {
@@ -37,6 +38,7 @@ claudetools - Persistent AI memory for Claude Code
37
38
 
38
39
  Usage:
39
40
  claudetools [options]
41
+ claudetools [command]
40
42
 
41
43
  Options:
42
44
  -s, --setup Interactive setup wizard
@@ -44,7 +46,12 @@ Options:
44
46
  -v, --version Show version
45
47
  -h, --help Show this help
46
48
 
47
- Running without options starts the server.
49
+ Commands:
50
+ watch Start the file watcher daemon
51
+ watch --stop Stop the watcher daemon
52
+ watch --status Check watcher status
53
+
54
+ Running without options starts the MCP server.
48
55
 
49
56
  Documentation: https://github.com/claudetools/memory
50
57
  `);
@@ -63,6 +70,22 @@ else if (values.uninstall) {
63
70
  process.exit(1);
64
71
  });
65
72
  }
73
+ else if (positionals[0] === 'watch') {
74
+ // Handle watch command
75
+ const watchArgs = process.argv.slice(3); // Get args after 'watch'
76
+ if (watchArgs.includes('--stop')) {
77
+ stopWatcher();
78
+ }
79
+ else if (watchArgs.includes('--status')) {
80
+ watcherStatus();
81
+ }
82
+ else {
83
+ startWatcher().catch((error) => {
84
+ console.error('Watcher failed:', error);
85
+ process.exit(1);
86
+ });
87
+ }
88
+ }
66
89
  else {
67
90
  // Start MCP server
68
91
  startServer();
package/dist/setup.js CHANGED
@@ -5,16 +5,21 @@
5
5
  import prompts from 'prompts';
6
6
  import chalk from 'chalk';
7
7
  import ora from 'ora';
8
- import { homedir } from 'os';
9
- import { join } from 'path';
8
+ import { homedir, hostname, platform } from 'os';
9
+ import { join, basename } from 'path';
10
10
  import { existsSync, readFileSync, writeFileSync, mkdirSync, copyFileSync } from 'fs';
11
+ import { randomUUID } from 'crypto';
11
12
  import { loadConfigFromFile, saveConfig, ensureConfigDir, getConfigPath, DEFAULT_CONFIG, } from './helpers/config-manager.js';
12
13
  // -----------------------------------------------------------------------------
13
14
  // Constants
14
15
  // -----------------------------------------------------------------------------
15
16
  const CLAUDE_DIR = join(homedir(), '.claude');
17
+ const CLAUDETOOLS_DIR = join(homedir(), '.claudetools');
16
18
  const MCP_CONFIG_PATH = join(CLAUDE_DIR, 'mcp.json');
19
+ const SETTINGS_PATH = join(CLAUDE_DIR, 'settings.json');
17
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');
18
23
  // -----------------------------------------------------------------------------
19
24
  // Utility Functions
20
25
  // -----------------------------------------------------------------------------
@@ -33,6 +38,77 @@ function info(msg) {
33
38
  console.log(chalk.blue('ℹ ') + msg);
34
39
  }
35
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
+ }),
57
+ });
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;
102
+ }
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));
109
+ }
110
+ }
111
+ // -----------------------------------------------------------------------------
36
112
  // Authentication
37
113
  // -----------------------------------------------------------------------------
38
114
  async function authenticateWithEmailPassword(apiUrl) {
@@ -78,7 +154,6 @@ async function authenticateWithEmailPassword(apiUrl) {
78
154
  async function authenticateWithDeviceCode(apiUrl) {
79
155
  const spinner = ora('Requesting device code...').start();
80
156
  try {
81
- // Request device code
82
157
  const codeResponse = await fetch(`${apiUrl}/api/v1/auth/device/code`, {
83
158
  method: 'POST',
84
159
  headers: { 'Content-Type': 'application/json' },
@@ -93,7 +168,6 @@ async function authenticateWithDeviceCode(apiUrl) {
93
168
  console.log(` Open: ${chalk.underline(verification_uri)}`);
94
169
  console.log(` Enter the code above to authenticate.\n`);
95
170
  const pollSpinner = ora('Waiting for authentication...').start();
96
- // Poll for token
97
171
  const pollInterval = (interval || 5) * 1000;
98
172
  const expiresAt = Date.now() + (expires_in || 900) * 1000;
99
173
  while (Date.now() < expiresAt) {
@@ -206,6 +280,79 @@ async function runAuthFlow(apiUrl) {
206
280
  }
207
281
  }
208
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
+ }
329
+ }
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
+ }
351
+ }
352
+ success(`Projects directory: ${projectsDir}`);
353
+ return [projectsDir];
354
+ }
355
+ // -----------------------------------------------------------------------------
209
356
  // Service Configuration
210
357
  // -----------------------------------------------------------------------------
211
358
  async function configureServices() {
@@ -241,10 +388,10 @@ function ensureClaudeDir() {
241
388
  mkdirSync(CLAUDE_DIR, { recursive: true });
242
389
  }
243
390
  }
244
- function backupFile(path) {
245
- if (existsSync(path)) {
246
- const backupPath = `${path}.backup.${Date.now()}`;
247
- copyFileSync(path, backupPath);
391
+ function backupFile(filePath) {
392
+ if (existsSync(filePath)) {
393
+ const backupPath = `${filePath}.backup.${Date.now()}`;
394
+ copyFileSync(filePath, backupPath);
248
395
  return backupPath;
249
396
  }
250
397
  return null;
@@ -259,7 +406,7 @@ async function configureMcpSettings(services) {
259
406
  mcpConfig = JSON.parse(readFileSync(MCP_CONFIG_PATH, 'utf-8'));
260
407
  const backup = backupFile(MCP_CONFIG_PATH);
261
408
  if (backup) {
262
- info(`Backed up existing config to ${backup}`);
409
+ info(`Backed up existing config to ${basename(backup)}`);
263
410
  }
264
411
  }
265
412
  catch {
@@ -280,7 +427,7 @@ async function configureMcpSettings(services) {
280
427
  if (services.context7ApiKey) {
281
428
  servers['context7'] = {
282
429
  command: 'npx',
283
- args: ['-y', '@context7/mcp-server'],
430
+ args: ['-y', '@upstash/context7-mcp'],
284
431
  env: {
285
432
  CONTEXT7_API_KEY: services.context7ApiKey,
286
433
  },
@@ -305,6 +452,38 @@ async function installHooks() {
305
452
  if (!existsSync(HOOKS_DIR)) {
306
453
  mkdirSync(HOOKS_DIR, { recursive: true });
307
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');
308
487
  // User prompt submit hook - injects context before each message
309
488
  const userPromptHook = `#!/bin/bash
310
489
  # ClaudeTools Context Injection Hook
@@ -328,27 +507,39 @@ API_KEY=$(jq -r '.apiKey // empty' "$CONFIG_FILE")
328
507
 
329
508
  if [ -z "$API_KEY" ]; then exit 0; fi
330
509
 
331
- # Get current project
510
+ # Get current project from projects.json
332
511
  PROJECT_FILE="$HOME/.claudetools/projects.json"
333
512
  CWD=$(pwd)
334
513
  PROJECT_ID=""
335
514
 
336
515
  if [ -f "$PROJECT_FILE" ]; then
337
- PROJECT_ID=$(jq -r --arg cwd "$CWD" '.[$cwd] // empty' "$PROJECT_FILE")
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)
338
521
  fi
339
522
 
340
523
  # Inject context (silent fail)
341
- curl -s -X POST "$API_URL/api/v1/context/inject" \\
524
+ RESULT=$(curl -s -X POST "$API_URL/api/v1/context/inject" \\
342
525
  -H "Authorization: Bearer $API_KEY" \\
343
526
  -H "Content-Type: application/json" \\
344
527
  -d "{\\"project_id\\": \\"$PROJECT_ID\\", \\"cwd\\": \\"$CWD\\"}" \\
345
- 2>/dev/null || true
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
346
537
  `;
347
538
  const userPromptPath = join(HOOKS_DIR, 'user-prompt-submit.sh');
348
539
  if (existsSync(userPromptPath)) {
349
540
  const backup = backupFile(userPromptPath);
350
541
  if (backup)
351
- info(`Backed up existing hook to ${backup}`);
542
+ info(`Backed up existing hook to ${basename(backup)}`);
352
543
  }
353
544
  writeFileSync(userPromptPath, userPromptHook, { mode: 0o755 });
354
545
  success('Installed user-prompt-submit.sh hook');
@@ -389,11 +580,87 @@ curl -s -X POST "$API_URL/api/v1/tools/log" \\
389
580
  if (existsSync(postToolPath)) {
390
581
  const backup = backupFile(postToolPath);
391
582
  if (backup)
392
- info(`Backed up existing hook to ${backup}`);
583
+ info(`Backed up existing hook to ${basename(backup)}`);
393
584
  }
394
585
  writeFileSync(postToolPath, postToolHook, { mode: 0o755 });
395
586
  success('Installed post-tool-use.sh hook');
396
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
+ }
397
664
  // -----------------------------------------------------------------------------
398
665
  // Verification
399
666
  // -----------------------------------------------------------------------------
@@ -401,7 +668,7 @@ async function verifySetup(config) {
401
668
  header('Verification');
402
669
  const spinner = ora('Checking API connection...').start();
403
670
  try {
404
- const response = await fetch(`${config.apiUrl}/health`, {
671
+ const response = await fetch(`${config.apiUrl}/api/v1/health`, {
405
672
  headers: config.apiKey ? { Authorization: `Bearer ${config.apiKey}` } : {},
406
673
  signal: AbortSignal.timeout(10000),
407
674
  });
@@ -415,6 +682,13 @@ async function verifySetup(config) {
415
682
  catch {
416
683
  spinner.fail('Could not connect to API');
417
684
  }
685
+ // Check system registration
686
+ if (existsSync(SYSTEM_FILE)) {
687
+ success('System registered');
688
+ }
689
+ else {
690
+ error('System not registered');
691
+ }
418
692
  // Check MCP config exists
419
693
  if (existsSync(MCP_CONFIG_PATH)) {
420
694
  success('MCP config installed');
@@ -423,11 +697,20 @@ async function verifySetup(config) {
423
697
  error('MCP config not found');
424
698
  }
425
699
  // Check hooks installed
426
- if (existsSync(join(HOOKS_DIR, 'user-prompt-submit.sh'))) {
427
- success('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');
428
711
  }
429
712
  else {
430
- error('Hooks not found');
713
+ error('Settings not found');
431
714
  }
432
715
  }
433
716
  // -----------------------------------------------------------------------------
@@ -478,23 +761,47 @@ export async function runSetup() {
478
761
  }
479
762
  }
480
763
  }
481
- // Step 2: Service Configuration
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}`);
776
+ }
777
+ }
778
+ else {
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
482
786
  const services = await configureServices();
483
- // Store service configs in extended config
787
+ // Store all configs
484
788
  const extendedConfig = config;
485
789
  if (services.context7ApiKey) {
486
790
  extendedConfig.context7ApiKey = services.context7ApiKey;
487
791
  }
488
792
  extendedConfig.sequentialThinkingEnabled = services.sequentialThinkingEnabled;
489
- // Step 3: Save ClaudeTools config
793
+ extendedConfig.watchedDirectories = projectDirs;
794
+ // Step 5: Save ClaudeTools config
490
795
  header('Saving Configuration');
491
796
  await saveConfig(extendedConfig);
492
797
  success(`Configuration saved to ${getConfigPath()}`);
493
- // Step 4: Configure Claude Code MCP
798
+ // Step 6: Configure Claude Code MCP
494
799
  await configureMcpSettings(services);
495
- // Step 5: Install Hooks
800
+ // Step 7: Install Hooks
496
801
  await installHooks();
497
- // Step 6: Verify
802
+ // Step 8: Configure Settings
803
+ await configureSettings();
804
+ // Step 9: Verify
498
805
  await verifySetup(extendedConfig);
499
806
  // Done
500
807
  header('Setup Complete');
@@ -536,8 +843,27 @@ export async function runUninstall() {
536
843
  error('Could not update MCP config');
537
844
  }
538
845
  }
539
- // Remove hooks
540
- const hooks = ['user-prompt-submit.sh', 'post-tool-use.sh'];
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
+ }
856
+ }
857
+ writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2));
858
+ success('Removed hooks from settings');
859
+ }
860
+ }
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'];
541
867
  for (const hook of hooks) {
542
868
  const hookPath = join(HOOKS_DIR, hook);
543
869
  if (existsSync(hookPath)) {
@@ -549,6 +875,20 @@ export async function runUninstall() {
549
875
  }
550
876
  }
551
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
+ }
891
+ }
552
892
  console.log('\n' + chalk.green('ClaudeTools removed from Claude Code.'));
553
893
  console.log(chalk.dim('Your ~/.claudetools/ config and data are preserved.\n'));
554
894
  }
@@ -0,0 +1,3 @@
1
+ export declare function startWatcher(): Promise<void>;
2
+ export declare function stopWatcher(): void;
3
+ export declare function watcherStatus(): void;
@@ -0,0 +1,307 @@
1
+ // =============================================================================
2
+ // ClaudeTools Code Watcher Daemon
3
+ // =============================================================================
4
+ // Monitors project directories for changes and syncs with the API
5
+ import { homedir } from 'os';
6
+ import { join } from 'path';
7
+ import { existsSync, readFileSync, writeFileSync, unlinkSync, watch, readdirSync, statSync } from 'fs';
8
+ // -----------------------------------------------------------------------------
9
+ // Constants
10
+ // -----------------------------------------------------------------------------
11
+ const PID_FILE = '/tmp/claudetools-watcher.pid';
12
+ const LOG_FILE = '/tmp/claudetools-watcher.log';
13
+ const CONFIG_FILE = join(homedir(), '.claudetools', 'config.json');
14
+ const PROJECTS_FILE = join(homedir(), '.claudetools', 'projects.json');
15
+ const SYSTEM_FILE = join(homedir(), '.claudetools', 'system.json');
16
+ // -----------------------------------------------------------------------------
17
+ // Logging
18
+ // -----------------------------------------------------------------------------
19
+ function log(level, message) {
20
+ const timestamp = new Date().toISOString();
21
+ const line = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`;
22
+ // Write to log file
23
+ try {
24
+ const fs = require('fs');
25
+ fs.appendFileSync(LOG_FILE, line);
26
+ }
27
+ catch {
28
+ // Ignore write errors
29
+ }
30
+ // Also output to console when running interactively
31
+ if (process.stdout.isTTY) {
32
+ const prefix = level === 'error' ? '\x1b[31m' : level === 'warn' ? '\x1b[33m' : '\x1b[36m';
33
+ console.log(`${prefix}[${level}]\x1b[0m ${message}`);
34
+ }
35
+ }
36
+ function loadConfig() {
37
+ try {
38
+ if (!existsSync(CONFIG_FILE)) {
39
+ log('error', `Config file not found: ${CONFIG_FILE}`);
40
+ return null;
41
+ }
42
+ const config = JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
43
+ if (!config.apiKey) {
44
+ log('error', 'No API key configured');
45
+ return null;
46
+ }
47
+ return {
48
+ apiUrl: config.apiUrl || 'https://api.claudetools.dev',
49
+ apiKey: config.apiKey,
50
+ watchedDirectories: config.watchedDirectories,
51
+ };
52
+ }
53
+ catch (err) {
54
+ log('error', `Failed to load config: ${err}`);
55
+ return null;
56
+ }
57
+ }
58
+ function loadSystemInfo() {
59
+ try {
60
+ if (!existsSync(SYSTEM_FILE)) {
61
+ return null;
62
+ }
63
+ return JSON.parse(readFileSync(SYSTEM_FILE, 'utf-8'));
64
+ }
65
+ catch {
66
+ return null;
67
+ }
68
+ }
69
+ // -----------------------------------------------------------------------------
70
+ // PID File Management
71
+ // -----------------------------------------------------------------------------
72
+ function writePidFile() {
73
+ writeFileSync(PID_FILE, String(process.pid));
74
+ }
75
+ function removePidFile() {
76
+ try {
77
+ if (existsSync(PID_FILE)) {
78
+ unlinkSync(PID_FILE);
79
+ }
80
+ }
81
+ catch {
82
+ // Ignore errors
83
+ }
84
+ }
85
+ function isWatcherRunning() {
86
+ if (!existsSync(PID_FILE)) {
87
+ return false;
88
+ }
89
+ try {
90
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim());
91
+ // Check if process is alive
92
+ process.kill(pid, 0);
93
+ return true;
94
+ }
95
+ catch {
96
+ // Process not running or permission denied
97
+ removePidFile();
98
+ return false;
99
+ }
100
+ }
101
+ function discoverProjects(directories) {
102
+ const projects = [];
103
+ for (const dir of directories) {
104
+ if (!existsSync(dir)) {
105
+ continue;
106
+ }
107
+ try {
108
+ const entries = readdirSync(dir);
109
+ for (const entry of entries) {
110
+ const fullPath = join(dir, entry);
111
+ try {
112
+ const stat = statSync(fullPath);
113
+ if (!stat.isDirectory() || entry.startsWith('.')) {
114
+ continue;
115
+ }
116
+ const hasGit = existsSync(join(fullPath, '.git'));
117
+ const hasPackageJson = existsSync(join(fullPath, 'package.json'));
118
+ // Consider it a project if it has git or package.json
119
+ if (hasGit || hasPackageJson) {
120
+ projects.push({
121
+ name: entry,
122
+ path: fullPath,
123
+ hasGit,
124
+ hasPackageJson,
125
+ });
126
+ }
127
+ }
128
+ catch {
129
+ // Skip inaccessible entries
130
+ }
131
+ }
132
+ }
133
+ catch (err) {
134
+ log('warn', `Could not read directory ${dir}: ${err}`);
135
+ }
136
+ }
137
+ return projects;
138
+ }
139
+ // -----------------------------------------------------------------------------
140
+ // API Sync
141
+ // -----------------------------------------------------------------------------
142
+ async function syncProjectsWithAPI(config, system, projects) {
143
+ try {
144
+ const response = await fetch(`${config.apiUrl}/api/v1/projects/sync`, {
145
+ method: 'POST',
146
+ headers: {
147
+ 'Content-Type': 'application/json',
148
+ 'Authorization': `Bearer ${config.apiKey}`,
149
+ },
150
+ body: JSON.stringify({
151
+ system_id: system.system_id,
152
+ projects: projects.map(p => ({
153
+ name: p.name,
154
+ local_path: p.path,
155
+ has_git: p.hasGit,
156
+ })),
157
+ }),
158
+ });
159
+ if (response.ok) {
160
+ const data = await response.json();
161
+ // Update local projects file
162
+ if (data.bindings) {
163
+ const projectsData = existsSync(PROJECTS_FILE)
164
+ ? JSON.parse(readFileSync(PROJECTS_FILE, 'utf-8'))
165
+ : { bindings: [] };
166
+ projectsData.bindings = data.bindings;
167
+ projectsData.last_sync = new Date().toISOString();
168
+ writeFileSync(PROJECTS_FILE, JSON.stringify(projectsData, null, 2));
169
+ }
170
+ log('info', `Synced ${projects.length} projects with API`);
171
+ }
172
+ else {
173
+ const text = await response.text();
174
+ log('warn', `API sync returned ${response.status}: ${text}`);
175
+ }
176
+ }
177
+ catch (err) {
178
+ log('error', `Failed to sync with API: ${err}`);
179
+ }
180
+ }
181
+ // -----------------------------------------------------------------------------
182
+ // File Watcher
183
+ // -----------------------------------------------------------------------------
184
+ function startWatching(directories, onChange) {
185
+ const watchers = [];
186
+ let debounceTimer = null;
187
+ const debouncedOnChange = () => {
188
+ if (debounceTimer) {
189
+ clearTimeout(debounceTimer);
190
+ }
191
+ debounceTimer = setTimeout(onChange, 5000); // 5 second debounce
192
+ };
193
+ for (const dir of directories) {
194
+ if (!existsSync(dir)) {
195
+ continue;
196
+ }
197
+ try {
198
+ const watcher = watch(dir, { recursive: false }, (eventType, filename) => {
199
+ if (filename && !filename.startsWith('.')) {
200
+ log('info', `Detected ${eventType} in ${dir}: ${filename}`);
201
+ debouncedOnChange();
202
+ }
203
+ });
204
+ watcher.on('error', (err) => {
205
+ log('error', `Watcher error for ${dir}: ${err}`);
206
+ });
207
+ watchers.push(watcher);
208
+ log('info', `Watching directory: ${dir}`);
209
+ }
210
+ catch (err) {
211
+ log('warn', `Could not watch directory ${dir}: ${err}`);
212
+ }
213
+ }
214
+ // Clean up on exit
215
+ const cleanup = () => {
216
+ for (const watcher of watchers) {
217
+ watcher.close();
218
+ }
219
+ removePidFile();
220
+ };
221
+ process.on('SIGINT', () => {
222
+ log('info', 'Received SIGINT, shutting down...');
223
+ cleanup();
224
+ process.exit(0);
225
+ });
226
+ process.on('SIGTERM', () => {
227
+ log('info', 'Received SIGTERM, shutting down...');
228
+ cleanup();
229
+ process.exit(0);
230
+ });
231
+ }
232
+ // -----------------------------------------------------------------------------
233
+ // Main
234
+ // -----------------------------------------------------------------------------
235
+ export async function startWatcher() {
236
+ // Check if already running
237
+ if (isWatcherRunning()) {
238
+ log('info', 'Watcher is already running');
239
+ console.log('Watcher is already running. Use "claudetools watch --stop" to stop it.');
240
+ return;
241
+ }
242
+ // Load config
243
+ const config = loadConfig();
244
+ if (!config) {
245
+ console.error('Failed to load config. Run "claudetools --setup" first.');
246
+ process.exit(1);
247
+ }
248
+ // Load system info
249
+ const system = loadSystemInfo();
250
+ if (!system) {
251
+ console.error('System not registered. Run "claudetools --setup" first.');
252
+ process.exit(1);
253
+ }
254
+ // Determine directories to watch
255
+ const directories = config.watchedDirectories || [join(homedir(), 'Projects')];
256
+ if (directories.length === 0) {
257
+ console.error('No directories configured to watch.');
258
+ process.exit(1);
259
+ }
260
+ // Write PID file
261
+ writePidFile();
262
+ log('info', `ClaudeTools watcher started (PID: ${process.pid})`);
263
+ console.log(`Watcher started (PID: ${process.pid})`);
264
+ console.log(`Watching: ${directories.join(', ')}`);
265
+ console.log(`Log file: ${LOG_FILE}`);
266
+ // Initial project discovery and sync
267
+ const projects = discoverProjects(directories);
268
+ log('info', `Discovered ${projects.length} projects`);
269
+ await syncProjectsWithAPI(config, system, projects);
270
+ // Set up file watching
271
+ const resync = async () => {
272
+ const updatedProjects = discoverProjects(directories);
273
+ await syncProjectsWithAPI(config, system, updatedProjects);
274
+ };
275
+ startWatching(directories, resync);
276
+ // Periodic resync every 5 minutes
277
+ setInterval(resync, 5 * 60 * 1000);
278
+ // Keep process alive
279
+ log('info', 'Watcher is running. Press Ctrl+C to stop.');
280
+ }
281
+ export function stopWatcher() {
282
+ if (!existsSync(PID_FILE)) {
283
+ console.log('Watcher is not running.');
284
+ return;
285
+ }
286
+ try {
287
+ const pid = parseInt(readFileSync(PID_FILE, 'utf-8').trim());
288
+ process.kill(pid, 'SIGTERM');
289
+ console.log(`Stopped watcher (PID: ${pid})`);
290
+ removePidFile();
291
+ }
292
+ catch (err) {
293
+ console.error(`Failed to stop watcher: ${err}`);
294
+ // Clean up stale PID file
295
+ removePidFile();
296
+ }
297
+ }
298
+ export function watcherStatus() {
299
+ if (isWatcherRunning()) {
300
+ const pid = readFileSync(PID_FILE, 'utf-8').trim();
301
+ console.log(`Watcher is running (PID: ${pid})`);
302
+ console.log(`Log file: ${LOG_FILE}`);
303
+ }
304
+ else {
305
+ console.log('Watcher is not running.');
306
+ }
307
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claudetools/tools",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Persistent AI memory, task management, and codebase intelligence for Claude Code",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",