@latentforce/latentgraph 1.0.0
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/README.md +320 -0
- package/build/cli/commands/add.js +350 -0
- package/build/cli/commands/config.js +142 -0
- package/build/cli/commands/init.js +285 -0
- package/build/cli/commands/start.js +65 -0
- package/build/cli/commands/status.js +76 -0
- package/build/cli/commands/stop.js +18 -0
- package/build/cli/commands/update-drg.js +194 -0
- package/build/daemon/daemon-manager.js +136 -0
- package/build/daemon/daemon.js +119 -0
- package/build/daemon/tools-executor.js +462 -0
- package/build/daemon/websocket-client.js +334 -0
- package/build/index.js +205 -0
- package/build/mcp-server.js +484 -0
- package/build/utils/api-client.js +147 -0
- package/build/utils/auth-resolver.js +184 -0
- package/build/utils/config.js +260 -0
- package/build/utils/machine-id.js +46 -0
- package/build/utils/prompts.js +114 -0
- package/build/utils/shiftignore.js +108 -0
- package/build/utils/tree-scanner.js +165 -0
- package/package.json +49 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { getApiKey, readProjectConfig } from '../../utils/config.js';
|
|
6
|
+
import { getProjectTree, extractAllFilePaths } from '../../utils/tree-scanner.js';
|
|
7
|
+
import { sendUpdateDrg } from '../../utils/api-client.js';
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
async function detectGitChanges(projectRoot, extensions) {
|
|
10
|
+
const changes = { added: [], modified: [], deleted: [] };
|
|
11
|
+
try {
|
|
12
|
+
const { stdout } = await execAsync('git status --porcelain -u', { cwd: projectRoot });
|
|
13
|
+
for (const line of stdout.split('\n')) {
|
|
14
|
+
if (!line.trim())
|
|
15
|
+
continue;
|
|
16
|
+
const statusCode = line.substring(0, 2);
|
|
17
|
+
let filePath = line.substring(3).trim();
|
|
18
|
+
// Handle renamed files (R status shows "old -> new")
|
|
19
|
+
if (filePath.includes(' -> ')) {
|
|
20
|
+
const parts = filePath.split(' -> ');
|
|
21
|
+
// Mark old path as deleted, new path as added
|
|
22
|
+
const oldPath = parts[0].trim();
|
|
23
|
+
const newPath = parts[1].trim();
|
|
24
|
+
const oldExt = path.extname(oldPath).toLowerCase();
|
|
25
|
+
const newExt = path.extname(newPath).toLowerCase();
|
|
26
|
+
if (extensions.has(oldExt))
|
|
27
|
+
changes.deleted.push(oldPath);
|
|
28
|
+
if (extensions.has(newExt))
|
|
29
|
+
changes.added.push(newPath);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Filter by extensions
|
|
33
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
34
|
+
if (!extensions.has(ext))
|
|
35
|
+
continue;
|
|
36
|
+
// Categorize based on git status codes
|
|
37
|
+
if (statusCode === '??' || statusCode.trimEnd() === 'A') {
|
|
38
|
+
changes.added.push(filePath);
|
|
39
|
+
}
|
|
40
|
+
else if (statusCode.includes('D')) {
|
|
41
|
+
changes.deleted.push(filePath);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
// M, MM, AM, etc. — treat as modified
|
|
45
|
+
changes.modified.push(filePath);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Not a git repo or git not available
|
|
51
|
+
}
|
|
52
|
+
return changes;
|
|
53
|
+
}
|
|
54
|
+
const LANGUAGE_CONFIG = {
|
|
55
|
+
js_ts: {
|
|
56
|
+
extensions: new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']),
|
|
57
|
+
label: 'JS/TS',
|
|
58
|
+
},
|
|
59
|
+
python: {
|
|
60
|
+
extensions: new Set(['.py']),
|
|
61
|
+
label: 'Python',
|
|
62
|
+
},
|
|
63
|
+
csharp: {
|
|
64
|
+
extensions: new Set(['.cs']),
|
|
65
|
+
label: 'C#',
|
|
66
|
+
},
|
|
67
|
+
cpp: {
|
|
68
|
+
extensions: new Set(['.cpp', '.cc', '.cxx', '.h', '.hpp', '.c']),
|
|
69
|
+
label: 'C/C++',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
export async function updateDrgCommand(options = {}) {
|
|
73
|
+
const projectRoot = process.cwd();
|
|
74
|
+
const mode = options.mode || 'incremental';
|
|
75
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
76
|
+
console.log('║ Updating Dependency Graph (DRG) ║');
|
|
77
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
78
|
+
// Step 1: Check API key
|
|
79
|
+
console.log('[DRG] Step 1/4: Checking API key...');
|
|
80
|
+
const apiKey = getApiKey();
|
|
81
|
+
if (!apiKey) {
|
|
82
|
+
console.error('\n❌ No API key found. Run "lgraph init" first to configure your API key.\n');
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
console.log('[DRG] ✓ API key found\n');
|
|
86
|
+
// Step 2: Read project config
|
|
87
|
+
console.log('[DRG] Step 2/4: Reading project configuration...');
|
|
88
|
+
const projectConfig = readProjectConfig(projectRoot);
|
|
89
|
+
if (!projectConfig) {
|
|
90
|
+
console.error('\n❌ No project configured for this directory.');
|
|
91
|
+
console.log('Run "lgraph start" first to configure the project.\n');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
console.log(`[DRG] ✓ Project: ${projectConfig.project_name} (${projectConfig.project_id})\n`);
|
|
95
|
+
// Step 3: Scan project for all supported language files
|
|
96
|
+
console.log('[DRG] Step 3/4: Scanning project for supported languages...');
|
|
97
|
+
const treeData = getProjectTree(projectRoot, {
|
|
98
|
+
depth: 0,
|
|
99
|
+
exclude_patterns: [
|
|
100
|
+
'.git',
|
|
101
|
+
'node_modules',
|
|
102
|
+
'__pycache__',
|
|
103
|
+
'.vscode',
|
|
104
|
+
'dist',
|
|
105
|
+
'build',
|
|
106
|
+
'.shift',
|
|
107
|
+
'.next',
|
|
108
|
+
'coverage',
|
|
109
|
+
'venv',
|
|
110
|
+
'env',
|
|
111
|
+
],
|
|
112
|
+
});
|
|
113
|
+
const allFiles = extractAllFilePaths(treeData.tree);
|
|
114
|
+
// Group files by language (each file counted in first matching language)
|
|
115
|
+
const filesByLanguage = {};
|
|
116
|
+
for (const lang of Object.keys(LANGUAGE_CONFIG)) {
|
|
117
|
+
const extSet = LANGUAGE_CONFIG[lang].extensions;
|
|
118
|
+
filesByLanguage[lang] = allFiles.filter((filePath) => extSet.has(path.extname(filePath).toLowerCase()));
|
|
119
|
+
}
|
|
120
|
+
const languagesWithFiles = Object.entries(filesByLanguage).filter(([_, files]) => files.length > 0);
|
|
121
|
+
console.log(`[DRG] Total files scanned: ${allFiles.length}`);
|
|
122
|
+
for (const [lang, files] of languagesWithFiles) {
|
|
123
|
+
console.log(`[DRG] ${LANGUAGE_CONFIG[lang].label} files: ${files.length}`);
|
|
124
|
+
}
|
|
125
|
+
console.log('');
|
|
126
|
+
if (languagesWithFiles.length === 0) {
|
|
127
|
+
console.log('⚠️ No supported language files found (JS/TS, Python, C#, C++). Nothing to update.\n');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
let anySent = false;
|
|
131
|
+
let lastError = null;
|
|
132
|
+
for (const [language, langFiles] of languagesWithFiles) {
|
|
133
|
+
const extSet = LANGUAGE_CONFIG[language].extensions;
|
|
134
|
+
const label = LANGUAGE_CONFIG[language].label;
|
|
135
|
+
// Read file contents for this language
|
|
136
|
+
const files = [];
|
|
137
|
+
let readErrors = 0;
|
|
138
|
+
for (const filePath of langFiles) {
|
|
139
|
+
const absolutePath = path.join(projectRoot, filePath);
|
|
140
|
+
try {
|
|
141
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
142
|
+
files.push({ file_path: filePath, content });
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
readErrors++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (readErrors > 0) {
|
|
149
|
+
console.log(`[DRG] [${label}] ⚠️ Could not read ${readErrors} file(s)\n`);
|
|
150
|
+
}
|
|
151
|
+
const gitChanges = await detectGitChanges(projectRoot, extSet);
|
|
152
|
+
const changedPaths = new Set([...gitChanges.added, ...gitChanges.modified]);
|
|
153
|
+
const filesToSend = mode === 'baseline'
|
|
154
|
+
? files
|
|
155
|
+
: files.filter((f) => changedPaths.has(f.file_path));
|
|
156
|
+
if (filesToSend.length === 0 && gitChanges.deleted.length === 0) {
|
|
157
|
+
console.log(`[DRG] [${label}] No changes. Skipping.\n`);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
console.log(`[DRG] [${label}] Git changes — added: ${gitChanges.added.length}, modified: ${gitChanges.modified.length}, deleted: ${gitChanges.deleted.length}`);
|
|
161
|
+
console.log(`[DRG] [${label}] Sending ${filesToSend.length} file(s) (mode: ${mode})...`);
|
|
162
|
+
const payload = {
|
|
163
|
+
project_id: projectConfig.project_id,
|
|
164
|
+
language,
|
|
165
|
+
mode,
|
|
166
|
+
changes: gitChanges,
|
|
167
|
+
files: filesToSend,
|
|
168
|
+
};
|
|
169
|
+
try {
|
|
170
|
+
const response = await sendUpdateDrg(apiKey, payload);
|
|
171
|
+
anySent = true;
|
|
172
|
+
console.log(`[DRG] [${label}] ✓ ${response.message}`);
|
|
173
|
+
if (response.stats) {
|
|
174
|
+
console.log(`[DRG] [${label}] Files sent: ${response.stats.total_files_provided}, Added: ${response.stats.added}, Modified: ${response.stats.modified}, Deleted: ${response.stats.deleted}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (error) {
|
|
178
|
+
lastError = error;
|
|
179
|
+
console.error(`[DRG] [${label}] ❌ ${error.message}`);
|
|
180
|
+
}
|
|
181
|
+
console.log('');
|
|
182
|
+
}
|
|
183
|
+
// Step 4: Summary
|
|
184
|
+
console.log('╔═══════════════════════════════════════════════╗');
|
|
185
|
+
console.log('║ ✓ DRG Update Complete ║');
|
|
186
|
+
console.log('╚═══════════════════════════════════════════════╝\n');
|
|
187
|
+
if (lastError && !anySent) {
|
|
188
|
+
console.error(`❌ All updates failed. Last error: ${lastError.message}`);
|
|
189
|
+
process.exit(1);
|
|
190
|
+
}
|
|
191
|
+
if (lastError) {
|
|
192
|
+
console.log(`⚠️ Some languages failed (see above).`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import { readDaemonPid, writeDaemonPid, removeDaemonPid, removeDaemonStatus, readDaemonStatus, isProcessRunning, } from '../utils/config.js';
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
function getDaemonScriptPath() {
|
|
9
|
+
// The daemon.js will be in the same directory as daemon-manager.js after compilation
|
|
10
|
+
return path.join(__dirname, 'daemon.js');
|
|
11
|
+
}
|
|
12
|
+
export async function startDaemon(projectRoot, projectId, apiKey) {
|
|
13
|
+
// Check if daemon is already running
|
|
14
|
+
const existingPid = readDaemonPid(projectRoot);
|
|
15
|
+
if (existingPid && isProcessRunning(existingPid)) {
|
|
16
|
+
return {
|
|
17
|
+
success: false,
|
|
18
|
+
error: `Daemon already running with PID ${existingPid}`,
|
|
19
|
+
pid: existingPid,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// Clean up stale files if process is not running
|
|
23
|
+
if (existingPid) {
|
|
24
|
+
removeDaemonPid(projectRoot);
|
|
25
|
+
removeDaemonStatus(projectRoot);
|
|
26
|
+
}
|
|
27
|
+
const daemonScript = getDaemonScriptPath();
|
|
28
|
+
if (!fs.existsSync(daemonScript)) {
|
|
29
|
+
return {
|
|
30
|
+
success: false,
|
|
31
|
+
error: `Daemon script not found at ${daemonScript}`,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
// Spawn detached daemon process
|
|
36
|
+
const child = spawn(process.execPath, [daemonScript, projectRoot, projectId, apiKey], {
|
|
37
|
+
detached: true,
|
|
38
|
+
stdio: 'ignore',
|
|
39
|
+
cwd: projectRoot,
|
|
40
|
+
});
|
|
41
|
+
if (!child.pid) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
error: 'Failed to spawn daemon process',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
// Detach from parent
|
|
48
|
+
child.unref();
|
|
49
|
+
// Write PID file
|
|
50
|
+
writeDaemonPid(child.pid, projectRoot);
|
|
51
|
+
return {
|
|
52
|
+
success: true,
|
|
53
|
+
pid: child.pid,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `Failed to start daemon: ${error.message}`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export async function stopDaemon(projectRoot) {
|
|
64
|
+
const pid = readDaemonPid(projectRoot);
|
|
65
|
+
if (!pid) {
|
|
66
|
+
return {
|
|
67
|
+
success: false,
|
|
68
|
+
error: 'No daemon PID file found',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
if (!isProcessRunning(pid)) {
|
|
72
|
+
// Clean up stale files
|
|
73
|
+
removeDaemonPid(projectRoot);
|
|
74
|
+
removeDaemonStatus(projectRoot);
|
|
75
|
+
return {
|
|
76
|
+
success: true,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
// Send SIGTERM for graceful shutdown
|
|
81
|
+
process.kill(pid, 'SIGTERM');
|
|
82
|
+
// Wait for process to exit (with timeout)
|
|
83
|
+
const maxWait = 5000;
|
|
84
|
+
const checkInterval = 100;
|
|
85
|
+
let waited = 0;
|
|
86
|
+
while (waited < maxWait && isProcessRunning(pid)) {
|
|
87
|
+
await new Promise((resolve) => setTimeout(resolve, checkInterval));
|
|
88
|
+
waited += checkInterval;
|
|
89
|
+
}
|
|
90
|
+
// If still running, force kill
|
|
91
|
+
if (isProcessRunning(pid)) {
|
|
92
|
+
try {
|
|
93
|
+
process.kill(pid, 'SIGKILL');
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// Process might have exited between check and kill
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Clean up files
|
|
100
|
+
removeDaemonPid(projectRoot);
|
|
101
|
+
removeDaemonStatus(projectRoot);
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
// Clean up files even on error
|
|
108
|
+
removeDaemonPid(projectRoot);
|
|
109
|
+
removeDaemonStatus(projectRoot);
|
|
110
|
+
return {
|
|
111
|
+
success: false,
|
|
112
|
+
error: `Failed to stop daemon: ${error.message}`,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export function getDaemonStatus(projectRoot) {
|
|
117
|
+
const pid = readDaemonPid(projectRoot);
|
|
118
|
+
const status = readDaemonStatus(projectRoot);
|
|
119
|
+
if (!pid) {
|
|
120
|
+
return { running: false };
|
|
121
|
+
}
|
|
122
|
+
const running = isProcessRunning(pid);
|
|
123
|
+
if (!running) {
|
|
124
|
+
// Clean up stale files
|
|
125
|
+
removeDaemonPid(projectRoot);
|
|
126
|
+
removeDaemonStatus(projectRoot);
|
|
127
|
+
return { running: false };
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
running: true,
|
|
131
|
+
pid,
|
|
132
|
+
connected: status?.connected ?? false,
|
|
133
|
+
projectId: status?.project_id,
|
|
134
|
+
startedAt: status?.started_at,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Daemon process entry point.
|
|
4
|
+
* This script runs as a detached background process and manages the WebSocket connection.
|
|
5
|
+
* Matching extension's behavior.
|
|
6
|
+
*
|
|
7
|
+
* Usage: node daemon.js <projectRoot> <projectId> <apiKey>
|
|
8
|
+
*/
|
|
9
|
+
import { WebSocketClient } from './websocket-client.js';
|
|
10
|
+
import { writeDaemonStatus, removeDaemonPid, removeDaemonStatus } from '../utils/config.js';
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
if (args.length < 3) {
|
|
13
|
+
console.error('Usage: daemon <projectRoot> <projectId> <apiKey>');
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
const [projectRoot, projectId, apiKey] = args;
|
|
17
|
+
let wsClient = null;
|
|
18
|
+
let isShuttingDown = false;
|
|
19
|
+
let startedAt = new Date().toISOString();
|
|
20
|
+
function updateStatus(connected) {
|
|
21
|
+
const status = {
|
|
22
|
+
pid: process.pid,
|
|
23
|
+
connected,
|
|
24
|
+
project_id: projectId,
|
|
25
|
+
started_at: startedAt,
|
|
26
|
+
};
|
|
27
|
+
writeDaemonStatus(status, projectRoot);
|
|
28
|
+
}
|
|
29
|
+
function cleanup() {
|
|
30
|
+
if (isShuttingDown)
|
|
31
|
+
return;
|
|
32
|
+
isShuttingDown = true;
|
|
33
|
+
console.log('[Daemon] Shutting down...');
|
|
34
|
+
if (wsClient) {
|
|
35
|
+
wsClient.disconnect();
|
|
36
|
+
wsClient = null;
|
|
37
|
+
}
|
|
38
|
+
removeDaemonPid(projectRoot);
|
|
39
|
+
removeDaemonStatus(projectRoot);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
// Handle graceful shutdown
|
|
43
|
+
process.on('SIGTERM', cleanup);
|
|
44
|
+
process.on('SIGINT', cleanup);
|
|
45
|
+
process.on('SIGHUP', cleanup);
|
|
46
|
+
// Handle uncaught errors
|
|
47
|
+
process.on('uncaughtException', (error) => {
|
|
48
|
+
console.error('[Daemon] Uncaught exception:', error);
|
|
49
|
+
cleanup();
|
|
50
|
+
});
|
|
51
|
+
process.on('unhandledRejection', (reason) => {
|
|
52
|
+
console.error('[Daemon] Unhandled rejection:', reason);
|
|
53
|
+
cleanup();
|
|
54
|
+
});
|
|
55
|
+
async function main() {
|
|
56
|
+
console.log('╔════════════════════════════════════════════╗');
|
|
57
|
+
console.log('║ Shift Daemon Starting ║');
|
|
58
|
+
console.log('╚════════════════════════════════════════════╝');
|
|
59
|
+
console.log(`[Daemon] Project ID: ${projectId}`);
|
|
60
|
+
console.log(`[Daemon] Project root: ${projectRoot}`);
|
|
61
|
+
console.log(`[Daemon] PID: ${process.pid}`);
|
|
62
|
+
// Initial status - not connected yet
|
|
63
|
+
updateStatus(false);
|
|
64
|
+
wsClient = new WebSocketClient({
|
|
65
|
+
projectId,
|
|
66
|
+
apiKey,
|
|
67
|
+
workspaceRoot: projectRoot,
|
|
68
|
+
});
|
|
69
|
+
// Set up event listeners - matching extension's setupWebSocketListeners
|
|
70
|
+
wsClient.on('connecting', () => {
|
|
71
|
+
console.log('[Daemon] WebSocket connecting...');
|
|
72
|
+
});
|
|
73
|
+
wsClient.on('connected', (projectInfo) => {
|
|
74
|
+
console.log('[Daemon] ✓ Connected to project:', projectInfo?.project_name);
|
|
75
|
+
updateStatus(true);
|
|
76
|
+
});
|
|
77
|
+
wsClient.on('disconnected', (code, reason) => {
|
|
78
|
+
console.log(`[Daemon] Disconnected (code: ${code}, reason: ${reason})`);
|
|
79
|
+
updateStatus(false);
|
|
80
|
+
});
|
|
81
|
+
wsClient.on('auth_failed', (message) => {
|
|
82
|
+
console.error(`[Daemon] ❌ Authentication failed: ${message}`);
|
|
83
|
+
updateStatus(false);
|
|
84
|
+
// Don't exit immediately, let the reconnect logic handle it
|
|
85
|
+
});
|
|
86
|
+
wsClient.on('error', (error) => {
|
|
87
|
+
console.error('[Daemon] WebSocket error:', error.message);
|
|
88
|
+
updateStatus(false);
|
|
89
|
+
});
|
|
90
|
+
wsClient.on('reconnecting', (attempt) => {
|
|
91
|
+
console.log(`[Daemon] Reconnecting... (attempt ${attempt})`);
|
|
92
|
+
});
|
|
93
|
+
wsClient.on('max_reconnects_reached', () => {
|
|
94
|
+
console.error('[Daemon] ❌ Max reconnection attempts reached. Stopping daemon.');
|
|
95
|
+
cleanup();
|
|
96
|
+
});
|
|
97
|
+
wsClient.on('message', (message) => {
|
|
98
|
+
console.log(`[Daemon] Received message: ${message.type}`);
|
|
99
|
+
});
|
|
100
|
+
wsClient.on('tool_executed', ({ tool, result, requestId }) => {
|
|
101
|
+
console.log(`[Daemon] ✓ Tool executed: ${tool} (${requestId})`);
|
|
102
|
+
});
|
|
103
|
+
wsClient.on('tool_error', ({ tool, error, requestId }) => {
|
|
104
|
+
console.error(`[Daemon] ✗ Tool failed: ${tool} (${requestId}):`, error.message);
|
|
105
|
+
});
|
|
106
|
+
// Connect to WebSocket
|
|
107
|
+
try {
|
|
108
|
+
await wsClient.connect();
|
|
109
|
+
console.log('[Daemon] ✓ Initial connection successful');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
console.error('[Daemon] Initial connection failed:', error.message);
|
|
113
|
+
// Don't exit - the WebSocket client will handle reconnection
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
main().catch((error) => {
|
|
117
|
+
console.error('[Daemon] Fatal error:', error);
|
|
118
|
+
cleanup();
|
|
119
|
+
});
|