@latentforce/shift 1.0.0 → 1.0.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.
@@ -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
+ });
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Tools Executor for MCP
3
+ * Handles execution of local file tools matching extension's tools-executor.js
4
+ */
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ const execAsync = promisify(exec);
10
+ export class ToolsExecutor {
11
+ wsClient;
12
+ workspaceRoot;
13
+ tools;
14
+ constructor(wsClient, workspaceRoot) {
15
+ this.wsClient = wsClient;
16
+ this.workspaceRoot = workspaceRoot;
17
+ this.tools = {
18
+ 'get_tree_struct': this.getTreeStruct.bind(this),
19
+ 'read_file': this.readFile.bind(this),
20
+ 'get_file_info': this.getFileInfo.bind(this),
21
+ 'get_repository_root': this.getRepositoryRoot.bind(this),
22
+ 'get_project_tree': this.getProjectTree.bind(this),
23
+ 'execute_command': this.executeCommand.bind(this),
24
+ };
25
+ }
26
+ /**
27
+ * Execute a tool by name
28
+ */
29
+ async executeTool(toolName, params) {
30
+ console.log(`[ToolsExecutor] Executing: ${toolName}`);
31
+ console.log(`[ToolsExecutor] Params:`, JSON.stringify(params, null, 2));
32
+ if (!this.tools[toolName]) {
33
+ throw new Error(`Unknown tool: ${toolName}`);
34
+ }
35
+ try {
36
+ const result = await this.tools[toolName](params);
37
+ console.log(`[ToolsExecutor] ✓ ${toolName} completed`);
38
+ return result;
39
+ }
40
+ catch (error) {
41
+ console.error(`[ToolsExecutor] ✗ ${toolName} failed:`, error);
42
+ throw error;
43
+ }
44
+ }
45
+ /**
46
+ * Get available tool names
47
+ */
48
+ getAvailableTools() {
49
+ return Object.keys(this.tools);
50
+ }
51
+ /**
52
+ * Execute command (with auto-approval for MCP - no UI)
53
+ */
54
+ async executeCommand(params) {
55
+ const { command, working_directory = '.' } = params;
56
+ if (!command) {
57
+ return {
58
+ status: 'error',
59
+ error: 'command is required'
60
+ };
61
+ }
62
+ console.log(`[ToolsExecutor] Executing command: ${command}`);
63
+ const fullWorkingDir = path.join(this.workspaceRoot, working_directory);
64
+ try {
65
+ const { stdout, stderr } = await execAsync(command, {
66
+ cwd: fullWorkingDir,
67
+ timeout: 30000,
68
+ maxBuffer: 1024 * 1024
69
+ });
70
+ return {
71
+ status: 'success',
72
+ command: command,
73
+ working_directory: working_directory,
74
+ stdout: stdout,
75
+ stderr: stderr,
76
+ exit_code: 0
77
+ };
78
+ }
79
+ catch (error) {
80
+ return {
81
+ status: 'error',
82
+ command: command,
83
+ working_directory: working_directory,
84
+ stdout: error.stdout || '',
85
+ stderr: error.stderr || '',
86
+ exit_code: error.code || 1,
87
+ error_message: error.message
88
+ };
89
+ }
90
+ }
91
+ /**
92
+ * Get tree structure of a directory
93
+ */
94
+ async getTreeStruct(params) {
95
+ const { target_path = params.path || '.', depth = 3, exclude_patterns = ['.git', 'node_modules', '__pycache__', '.vscode', 'dist', 'build'] } = params;
96
+ const fullPath = path.join(this.workspaceRoot, target_path);
97
+ let file_count = 0;
98
+ let dir_count = 0;
99
+ let total_size = 0;
100
+ const scanDirectory = async (dirPath, currentDepth = 0, relativePath = '') => {
101
+ if (currentDepth >= depth) {
102
+ return [];
103
+ }
104
+ const items = [];
105
+ try {
106
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
107
+ for (const entry of entries) {
108
+ if (exclude_patterns.some((pattern) => entry.name.includes(pattern))) {
109
+ continue;
110
+ }
111
+ const itemPath = path.join(dirPath, entry.name);
112
+ const itemRelativePath = path.join(relativePath, entry.name);
113
+ if (entry.isDirectory()) {
114
+ dir_count++;
115
+ const children = await scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
116
+ items.push({
117
+ name: entry.name,
118
+ type: 'directory',
119
+ path: itemRelativePath,
120
+ children: children
121
+ });
122
+ }
123
+ else if (entry.isFile()) {
124
+ file_count++;
125
+ try {
126
+ const stats = await fs.stat(itemPath);
127
+ total_size += stats.size;
128
+ items.push({
129
+ name: entry.name,
130
+ type: 'file',
131
+ path: itemRelativePath,
132
+ size: stats.size
133
+ });
134
+ }
135
+ catch {
136
+ // Skip files we can't read
137
+ }
138
+ }
139
+ }
140
+ }
141
+ catch (err) {
142
+ console.error(`Error scanning ${dirPath}:`, err.message);
143
+ }
144
+ return items;
145
+ };
146
+ const tree = await scanDirectory(fullPath, 0, target_path);
147
+ return {
148
+ status: 'success',
149
+ tree: tree,
150
+ file_count: file_count,
151
+ dir_count: dir_count,
152
+ total_size: `${(total_size / (1024 * 1024)).toFixed(2)} MB`,
153
+ scanned_path: target_path,
154
+ workspace_root: this.workspaceRoot
155
+ };
156
+ }
157
+ /**
158
+ * Read file contents
159
+ */
160
+ async readFile(params) {
161
+ const { file_path } = params;
162
+ if (!file_path) {
163
+ return {
164
+ status: 'error',
165
+ error: 'file_path is required'
166
+ };
167
+ }
168
+ console.log(`[ToolsExecutor] Reading file: ${file_path}`);
169
+ try {
170
+ const fullPath = path.join(this.workspaceRoot, file_path);
171
+ const content = await fs.readFile(fullPath, 'utf8');
172
+ const stats = await fs.stat(fullPath);
173
+ return {
174
+ status: 'success',
175
+ file_path: file_path,
176
+ content: content,
177
+ size: stats.size,
178
+ lines: content.split('\n').length
179
+ };
180
+ }
181
+ catch (error) {
182
+ return {
183
+ status: 'error',
184
+ file_path: file_path,
185
+ error: error.message
186
+ };
187
+ }
188
+ }
189
+ /**
190
+ * Get file info
191
+ */
192
+ async getFileInfo(params) {
193
+ const { file_path } = params;
194
+ if (!file_path) {
195
+ return {
196
+ status: 'error',
197
+ error: 'file_path is required'
198
+ };
199
+ }
200
+ try {
201
+ const fullPath = path.join(this.workspaceRoot, file_path);
202
+ const stats = await fs.stat(fullPath);
203
+ return {
204
+ status: 'success',
205
+ file_path: file_path,
206
+ size: stats.size,
207
+ created: stats.birthtime,
208
+ modified: stats.mtime,
209
+ is_directory: stats.isDirectory()
210
+ };
211
+ }
212
+ catch (error) {
213
+ return {
214
+ status: 'error',
215
+ file_path: file_path,
216
+ error: error.message
217
+ };
218
+ }
219
+ }
220
+ /**
221
+ * Get repository root directory
222
+ */
223
+ async getRepositoryRoot(params) {
224
+ const { start_path = '.' } = params;
225
+ console.log(`[ToolsExecutor] Finding repository root from: ${start_path}`);
226
+ let currentPath = path.join(this.workspaceRoot, start_path);
227
+ try {
228
+ const stats = await fs.stat(currentPath);
229
+ if (stats.isFile()) {
230
+ currentPath = path.dirname(currentPath);
231
+ }
232
+ }
233
+ catch {
234
+ currentPath = this.workspaceRoot;
235
+ }
236
+ let searchPath = currentPath;
237
+ const maxDepth = 20;
238
+ let depth = 0;
239
+ while (depth < maxDepth) {
240
+ try {
241
+ const gitPath = path.join(searchPath, '.git');
242
+ await fs.access(gitPath);
243
+ const entries = await fs.readdir(searchPath);
244
+ const has_package_json = entries.includes('package.json');
245
+ const has_requirements_txt = entries.includes('requirements.txt');
246
+ const has_cargo_toml = entries.includes('Cargo.toml');
247
+ const has_go_mod = entries.includes('go.mod');
248
+ let project_type = 'unknown';
249
+ if (has_package_json)
250
+ project_type = 'node';
251
+ else if (has_requirements_txt)
252
+ project_type = 'python';
253
+ else if (has_cargo_toml)
254
+ project_type = 'rust';
255
+ else if (has_go_mod)
256
+ project_type = 'go';
257
+ return {
258
+ status: 'success',
259
+ repository_root: searchPath,
260
+ relative_to_workspace: path.relative(this.workspaceRoot, searchPath),
261
+ has_git: true,
262
+ project_type: project_type,
263
+ files_in_root: entries.length
264
+ };
265
+ }
266
+ catch {
267
+ const parentPath = path.dirname(searchPath);
268
+ if (parentPath === searchPath) {
269
+ return {
270
+ status: 'success',
271
+ repository_root: null,
272
+ has_git: false,
273
+ message: 'No .git folder found in any parent directory'
274
+ };
275
+ }
276
+ searchPath = parentPath;
277
+ depth++;
278
+ }
279
+ }
280
+ return {
281
+ status: 'success',
282
+ repository_root: null,
283
+ has_git: false,
284
+ message: `No .git folder found after searching ${maxDepth} levels up`
285
+ };
286
+ }
287
+ /**
288
+ * Get project tree
289
+ */
290
+ async getProjectTree(params) {
291
+ const { depth = 0, exclude_patterns = [
292
+ '.git',
293
+ 'node_modules',
294
+ '__pycache__',
295
+ '.vscode',
296
+ 'dist',
297
+ 'build',
298
+ '.shift',
299
+ '.next',
300
+ '.cache',
301
+ 'coverage',
302
+ '.pytest_cache',
303
+ 'venv',
304
+ 'env',
305
+ '.env'
306
+ ] } = params;
307
+ console.log(`[ToolsExecutor] Getting project tree from workspace root: ${this.workspaceRoot}`);
308
+ console.log(`[ToolsExecutor] Depth limit: ${depth === 0 ? 'unlimited' : depth}`);
309
+ let file_count = 0;
310
+ let dir_count = 0;
311
+ let total_size = 0;
312
+ const max_depth = depth === 0 ? Infinity : depth;
313
+ const scanDirectory = async (dirPath, currentDepth = 0, relativePath = '') => {
314
+ if (currentDepth >= max_depth) {
315
+ return [];
316
+ }
317
+ const items = [];
318
+ try {
319
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
320
+ for (const entry of entries) {
321
+ if (exclude_patterns.some((pattern) => entry.name.includes(pattern))) {
322
+ continue;
323
+ }
324
+ const itemPath = path.join(dirPath, entry.name);
325
+ const itemRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
326
+ if (entry.isDirectory()) {
327
+ dir_count++;
328
+ const children = await scanDirectory(itemPath, currentDepth + 1, itemRelativePath);
329
+ items.push({
330
+ name: entry.name,
331
+ type: 'directory',
332
+ path: itemRelativePath,
333
+ depth: currentDepth,
334
+ children: children
335
+ });
336
+ }
337
+ else if (entry.isFile()) {
338
+ file_count++;
339
+ try {
340
+ const stats = await fs.stat(itemPath);
341
+ total_size += stats.size;
342
+ const ext = path.extname(entry.name).toLowerCase();
343
+ items.push({
344
+ name: entry.name,
345
+ type: 'file',
346
+ path: itemRelativePath,
347
+ depth: currentDepth,
348
+ size: stats.size,
349
+ extension: ext,
350
+ modified: stats.mtime
351
+ });
352
+ }
353
+ catch {
354
+ console.warn(`[ToolsExecutor] Cannot read: ${itemPath}`);
355
+ }
356
+ }
357
+ }
358
+ }
359
+ catch (err) {
360
+ console.error(`[ToolsExecutor] Error scanning ${dirPath}:`, err.message);
361
+ }
362
+ return items;
363
+ };
364
+ const tree = await scanDirectory(this.workspaceRoot, 0, '');
365
+ const total_size_mb = (total_size / (1024 * 1024)).toFixed(2);
366
+ console.log(`[ToolsExecutor] ✓ Scanned project tree:`);
367
+ console.log(`[ToolsExecutor] Files: ${file_count}`);
368
+ console.log(`[ToolsExecutor] Directories: ${dir_count}`);
369
+ console.log(`[ToolsExecutor] Total size: ${total_size_mb} MB`);
370
+ return {
371
+ status: 'success',
372
+ tree: tree,
373
+ file_count: file_count,
374
+ dir_count: dir_count,
375
+ total_size_bytes: total_size,
376
+ total_size_mb: total_size_mb,
377
+ scanned_from: this.workspaceRoot,
378
+ depth_limit: depth,
379
+ actual_max_depth: depth === 0 ? 'unlimited' : depth,
380
+ excluded_patterns: exclude_patterns
381
+ };
382
+ }
383
+ }