@claudetools/tools 0.2.0 → 0.2.2
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 +26 -3
- package/dist/helpers/project-registration.js +8 -2
- package/dist/setup.js +369 -29
- package/dist/watcher.d.ts +3 -0
- package/dist/watcher.js +307 -0
- package/package.json +1 -1
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:
|
|
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
|
-
|
|
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();
|
|
@@ -85,15 +85,21 @@ async function ensureSystemRegistered() {
|
|
|
85
85
|
*/
|
|
86
86
|
function detectGitRemote(localPath) {
|
|
87
87
|
try {
|
|
88
|
+
// Resolve symlinks and validate path to prevent path traversal attacks
|
|
89
|
+
const resolvedPath = fs.realpathSync(localPath);
|
|
90
|
+
// Ensure path is absolute and doesn't contain path traversal
|
|
91
|
+
if (!path.isAbsolute(resolvedPath) || resolvedPath.includes('..')) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
88
94
|
const gitRemote = execSync('git config --get remote.origin.url', {
|
|
89
|
-
cwd:
|
|
95
|
+
cwd: resolvedPath,
|
|
90
96
|
encoding: 'utf-8',
|
|
91
97
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
92
98
|
}).trim();
|
|
93
99
|
return gitRemote || undefined;
|
|
94
100
|
}
|
|
95
101
|
catch {
|
|
96
|
-
// Not a git repo
|
|
102
|
+
// Not a git repo, no remote configured, or invalid path
|
|
97
103
|
return undefined;
|
|
98
104
|
}
|
|
99
105
|
}
|
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(
|
|
245
|
-
if (existsSync(
|
|
246
|
-
const backupPath = `${
|
|
247
|
-
copyFileSync(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
427
|
-
|
|
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('
|
|
713
|
+
error('Settings not found');
|
|
431
714
|
}
|
|
432
715
|
}
|
|
433
716
|
// -----------------------------------------------------------------------------
|
|
@@ -445,7 +728,7 @@ export async function runSetup() {
|
|
|
445
728
|
// Step 1: Authentication
|
|
446
729
|
header('Authentication');
|
|
447
730
|
if (config.apiKey) {
|
|
448
|
-
info(`Existing API key found: ${config.apiKey.substring(0,
|
|
731
|
+
info(`Existing API key found: ${config.apiKey.substring(0, 6)}...`);
|
|
449
732
|
const { replace } = await prompts({
|
|
450
733
|
type: 'confirm',
|
|
451
734
|
name: 'replace',
|
|
@@ -478,23 +761,47 @@ export async function runSetup() {
|
|
|
478
761
|
}
|
|
479
762
|
}
|
|
480
763
|
}
|
|
481
|
-
// Step 2:
|
|
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
|
|
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
|
-
|
|
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
|
|
798
|
+
// Step 6: Configure Claude Code MCP
|
|
494
799
|
await configureMcpSettings(services);
|
|
495
|
-
// Step
|
|
800
|
+
// Step 7: Install Hooks
|
|
496
801
|
await installHooks();
|
|
497
|
-
// Step
|
|
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
|
-
|
|
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
|
}
|
package/dist/watcher.js
ADDED
|
@@ -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
|
+
}
|