@claudetools/tools 0.1.2 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +38 -7
- package/dist/setup.d.ts +1 -0
- package/dist/setup.js +847 -159
- package/dist/watcher.d.ts +3 -0
- package/dist/watcher.js +307 -0
- package/package.json +6 -2
package/dist/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
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@claudetools/tools",
|
|
3
|
-
"version": "0.1
|
|
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",
|
|
@@ -46,10 +46,14 @@
|
|
|
46
46
|
"access": "public"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
49
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
50
|
+
"chalk": "^5.6.2",
|
|
51
|
+
"ora": "^9.0.0",
|
|
52
|
+
"prompts": "^2.4.2"
|
|
50
53
|
},
|
|
51
54
|
"devDependencies": {
|
|
52
55
|
"@types/node": "^20.10.0",
|
|
56
|
+
"@types/prompts": "^2.4.9",
|
|
53
57
|
"@vitest/ui": "^4.0.15",
|
|
54
58
|
"tsx": "^4.7.0",
|
|
55
59
|
"typescript": "^5.3.0",
|