@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.
@@ -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
+ });