@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.
@@ -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.1.2",
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",