devbonzai 2.0.9 → 2.1.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,1729 @@
1
+ #!/usr/bin/env node
2
+
3
+ const express = require('./node_modules/express');
4
+ const cors = require('./node_modules/cors');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const { exec, spawn, execSync } = require('child_process');
8
+ let babelParser = null;
9
+ try {
10
+ babelParser = require('./node_modules/@babel/parser');
11
+ } catch (e) {
12
+ // Babel parser not available, will fall back gracefully
13
+ }
14
+
15
+ const app = express();
16
+ const ROOT = path.join(__dirname, '..');
17
+
18
+ app.use(cors());
19
+ app.use(express.json());
20
+
21
+ // Root route - simple API documentation
22
+ app.get('/', (req, res) => {
23
+ res.json({
24
+ message: 'Local File Server API',
25
+ endpoints: {
26
+ 'GET /list': 'List all files in the directory',
27
+ 'GET /read?path=<filepath>': 'Read file content',
28
+ 'GET /git-churn?path=<filepath>&commits=30': 'Get git commit churn for a file',
29
+ 'POST /write': 'Write file content (body: {path, content})',
30
+ 'POST /write_dir': 'Create directory (body: {path})',
31
+ 'POST /delete': 'Delete file or directory (body: {path})',
32
+ 'POST /move': 'Move file or folder (body: {source, destination})',
33
+ 'POST /open-cursor': 'Open Cursor (body: {path, line?})',
34
+ 'POST /analyze_prompt': 'Analyze what files would be modified (body: {prompt})',
35
+ 'POST /prompt_agent': 'Execute cursor-agent command (body: {prompt})',
36
+ 'POST /prompt_agent_stream': 'Execute cursor-agent with SSE streaming (body: {prompt})',
37
+ 'POST /revert_job': 'Revert to a previous commit (body: {beforeCommit})',
38
+ 'POST /shutdown': 'Gracefully shutdown the server'
39
+ },
40
+ example: 'Try: /list or /read?path=README.md'
41
+ });
42
+ });
43
+
44
+ // Read and parse ignore patterns from .ignore file
45
+ function getIgnorePatterns() {
46
+ try {
47
+ const ignorePath = path.join(__dirname, '.ignore');
48
+ if (fs.existsSync(ignorePath)) {
49
+ const content = fs.readFileSync(ignorePath, 'utf8');
50
+ return content
51
+ .split('\n')
52
+ .map(line => line.trim())
53
+ .filter(line => line && !line.startsWith('#'))
54
+ .map(pattern => {
55
+ // Convert simple glob patterns to regex
56
+ if (pattern.endsWith('/')) {
57
+ // Directory pattern
58
+ pattern = pattern.slice(0, -1);
59
+ }
60
+
61
+ // Simple approach: escape dots and convert globs
62
+ pattern = pattern.replace(/\./g, '\\.');
63
+ pattern = pattern.replace(/\*\*/g, '|||DOUBLESTAR|||');
64
+ pattern = pattern.replace(/\*/g, '[^/]*');
65
+ pattern = pattern.replace(/\|\|\|DOUBLESTAR\|\|\|/g, '.*');
66
+
67
+ return new RegExp('^' + pattern + '(/.*)?$');
68
+ });
69
+ }
70
+ } catch (e) {
71
+ console.warn('Could not read .ignore file:', e.message);
72
+ }
73
+
74
+ // Default ignore patterns if no .ignore file exists
75
+ return [
76
+ /^node_modules(\/.*)?$/,
77
+ /^\.git(\/.*)?$/,
78
+ /^\.DS_Store$/,
79
+ /^\.env$/,
80
+ /^bonzai(\/.*)?$/
81
+ ];
82
+ }
83
+
84
+ // Check if a path should be ignored
85
+ function shouldIgnore(relativePath, ignorePatterns) {
86
+ return ignorePatterns.some(pattern => pattern.test(relativePath));
87
+ }
88
+
89
+ // Extract functions, classes, and methods from a Python file
90
+ function extractPythonFunctions(filePath) {
91
+ try {
92
+ const content = fs.readFileSync(filePath, 'utf8');
93
+ const lines = content.split('\n');
94
+ const functions = [];
95
+ const classes = [];
96
+ let currentFunction = null;
97
+ let currentClass = null;
98
+ let decorators = [];
99
+ let classIndent = -1;
100
+
101
+ for (let i = 0; i < lines.length; i++) {
102
+ const line = lines[i];
103
+ const trimmed = line.trim();
104
+
105
+ // Calculate indentation level
106
+ const match = line.match(/^\s*/);
107
+ const currentIndent = match ? match[0].length : 0;
108
+
109
+ // Check for decorators (only at top level, before function/class)
110
+ if (trimmed.startsWith('@') && currentIndent === 0) {
111
+ decorators.push(line);
112
+ continue;
113
+ }
114
+
115
+ // Check if this is a top-level class definition
116
+ const classMatch = trimmed.match(/^class\s+(\w+)/);
117
+ if (classMatch && currentIndent === 0) {
118
+ // Save previous function/class if exists
119
+ if (currentFunction) {
120
+ currentFunction.content = currentFunction.content.trim();
121
+ functions.push(currentFunction);
122
+ currentFunction = null;
123
+ }
124
+ if (currentClass) {
125
+ currentClass.content = currentClass.content.trim();
126
+ classes.push(currentClass);
127
+ }
128
+
129
+ // Start new class
130
+ const className = classMatch[1];
131
+ let classContent = '';
132
+
133
+ // Add decorators if any
134
+ if (decorators.length > 0) {
135
+ classContent = decorators.join('\n') + '\n';
136
+ decorators = [];
137
+ }
138
+
139
+ classContent += line;
140
+ classIndent = currentIndent;
141
+
142
+ currentClass = {
143
+ name: className,
144
+ content: classContent,
145
+ methods: [],
146
+ startLine: i + 1,
147
+ endLine: i + 1
148
+ };
149
+ continue;
150
+ }
151
+
152
+ // Check if this is a method definition (inside a class)
153
+ const methodMatch = trimmed.match(/^def\s+(\w+)\s*\(/);
154
+ if (methodMatch && currentClass && currentIndent > classIndent) {
155
+ // Save previous method if exists
156
+ if (currentFunction) {
157
+ currentFunction.content = currentFunction.content.trim();
158
+ currentClass.methods.push(currentFunction);
159
+ currentFunction = null;
160
+ }
161
+
162
+ // Start new method
163
+ const methodName = methodMatch[1];
164
+ let methodContent = '';
165
+
166
+ // Add decorators if any
167
+ if (decorators.length > 0) {
168
+ methodContent = decorators.join('\n') + '\n';
169
+ decorators = [];
170
+ }
171
+
172
+ methodContent += line;
173
+
174
+ currentFunction = {
175
+ name: currentClass.name + '.' + methodName,
176
+ content: methodContent,
177
+ startLine: i + 1,
178
+ endLine: i + 1,
179
+ isMethod: true,
180
+ className: currentClass.name,
181
+ methodName: methodName
182
+ };
183
+ continue;
184
+ }
185
+
186
+ // Check if this is a top-level function definition
187
+ const funcMatch = trimmed.match(/^def\s+(\w+)\s*\(/);
188
+
189
+ if (funcMatch && currentIndent === 0) {
190
+ // Save previous function/class if exists
191
+ if (currentFunction) {
192
+ currentFunction.content = currentFunction.content.trim();
193
+ if (currentFunction.isMethod && currentClass) {
194
+ currentClass.methods.push(currentFunction);
195
+ } else {
196
+ functions.push(currentFunction);
197
+ }
198
+ currentFunction = null;
199
+ }
200
+ if (currentClass) {
201
+ currentClass.content = currentClass.content.trim();
202
+ classes.push(currentClass);
203
+ currentClass = null;
204
+ classIndent = -1;
205
+ }
206
+
207
+ // Start new function
208
+ const functionName = funcMatch[1];
209
+ let functionContent = '';
210
+
211
+ // Add decorators if any
212
+ if (decorators.length > 0) {
213
+ functionContent = decorators.join('\n') + '\n';
214
+ decorators = [];
215
+ }
216
+
217
+ functionContent += line;
218
+
219
+ currentFunction = {
220
+ name: functionName,
221
+ content: functionContent,
222
+ startLine: i + 1,
223
+ endLine: i + 1
224
+ };
225
+ } else if (currentFunction || currentClass) {
226
+ // We're processing lines after a function/class definition
227
+ if (currentIndent === 0 && trimmed && !trimmed.startsWith('#')) {
228
+ // Back to top level with non-comment content - function/class ended
229
+ if (currentFunction) {
230
+ currentFunction.content = currentFunction.content.trim();
231
+ if (currentFunction.isMethod && currentClass) {
232
+ currentClass.methods.push(currentFunction);
233
+ } else {
234
+ functions.push(currentFunction);
235
+ }
236
+ currentFunction = null;
237
+ }
238
+ if (currentClass) {
239
+ currentClass.content = currentClass.content.trim();
240
+ classes.push(currentClass);
241
+ currentClass = null;
242
+ classIndent = -1;
243
+ }
244
+
245
+ // Check if this line starts a new function/class
246
+ if (funcMatch) {
247
+ const functionName = funcMatch[1];
248
+ let functionContent = '';
249
+
250
+ if (decorators.length > 0) {
251
+ functionContent = decorators.join('\n') + '\n';
252
+ decorators = [];
253
+ }
254
+
255
+ functionContent += line;
256
+
257
+ currentFunction = {
258
+ name: functionName,
259
+ content: functionContent,
260
+ startLine: i + 1,
261
+ endLine: i + 1
262
+ };
263
+ } else if (classMatch) {
264
+ const className = classMatch[1];
265
+ let classContent = '';
266
+
267
+ if (decorators.length > 0) {
268
+ classContent = decorators.join('\n') + '\n';
269
+ decorators = [];
270
+ }
271
+
272
+ classContent += line;
273
+ classIndent = currentIndent;
274
+
275
+ currentClass = {
276
+ name: className,
277
+ content: classContent,
278
+ methods: [],
279
+ startLine: i + 1,
280
+ endLine: i + 1
281
+ };
282
+ } else if (trimmed.startsWith('@')) {
283
+ decorators.push(line);
284
+ }
285
+ } else {
286
+ // Still inside function/class (indented or empty/comment line)
287
+ if (currentFunction) {
288
+ currentFunction.content += '\n' + line;
289
+ currentFunction.endLine = i + 1;
290
+ }
291
+ if (currentClass) {
292
+ currentClass.content += '\n' + line;
293
+ currentClass.endLine = i + 1;
294
+ }
295
+ }
296
+ }
297
+ }
298
+
299
+ // Don't forget the last function/class
300
+ if (currentFunction) {
301
+ currentFunction.content = currentFunction.content.trim();
302
+ if (currentFunction.isMethod && currentClass) {
303
+ currentClass.methods.push(currentFunction);
304
+ } else {
305
+ functions.push(currentFunction);
306
+ }
307
+ }
308
+ if (currentClass) {
309
+ currentClass.content = currentClass.content.trim();
310
+ classes.push(currentClass);
311
+ }
312
+
313
+ return { functions, classes };
314
+ } catch (e) {
315
+ // If parsing fails (invalid Python, etc.), return empty arrays
316
+ console.warn('Failed to parse Python file:', filePath, e.message);
317
+ return { functions: [], classes: [] };
318
+ }
319
+ }
320
+
321
+ // Extract functions, classes, and methods from a JavaScript/TypeScript file
322
+ function extractJavaScriptFunctions(filePath) {
323
+ try {
324
+ if (!babelParser) {
325
+ return { functions: [], classes: [] };
326
+ }
327
+
328
+ // Skip .d.ts files, minified files, and node_modules
329
+ if (filePath.endsWith('.d.ts') || filePath.endsWith('.min.js') || filePath.includes('node_modules')) {
330
+ return { functions: [], classes: [] };
331
+ }
332
+
333
+ const content = fs.readFileSync(filePath, 'utf8');
334
+ const functions = [];
335
+ const classes = [];
336
+
337
+ // Determine if it's TypeScript
338
+ const isTypeScript = filePath.endsWith('.ts') || filePath.endsWith('.tsx');
339
+
340
+ try {
341
+ const ast = babelParser.parse(content, {
342
+ sourceType: 'module',
343
+ plugins: [
344
+ 'typescript',
345
+ 'jsx',
346
+ 'decorators-legacy',
347
+ 'classProperties',
348
+ 'objectRestSpread',
349
+ 'asyncGenerators',
350
+ 'functionBind',
351
+ 'exportDefaultFrom',
352
+ 'exportNamespaceFrom',
353
+ 'dynamicImport',
354
+ 'nullishCoalescingOperator',
355
+ 'optionalChaining'
356
+ ]
357
+ });
358
+
359
+ // Helper to extract code snippet from source
360
+ const getCode = (node) => {
361
+ return content.substring(node.start, node.end);
362
+ };
363
+
364
+ // Track visited nodes to avoid duplicates
365
+ const visitedNodes = new Set();
366
+
367
+ // Traverse AST
368
+ function traverse(node, parentType = null) {
369
+ if (!node) return;
370
+
371
+ // Skip if already visited (avoid processing same node twice)
372
+ if (visitedNodes.has(node)) return;
373
+ visitedNodes.add(node);
374
+
375
+ // Function declarations: function myFunc() {}
376
+ // Skip if inside ExportNamedDeclaration (will be handled below)
377
+ if (node.type === 'FunctionDeclaration' && node.id && parentType !== 'ExportNamedDeclaration') {
378
+ functions.push({
379
+ name: node.id.name,
380
+ content: getCode(node),
381
+ startLine: node.loc ? node.loc.start.line : 0,
382
+ endLine: node.loc ? node.loc.end.line : 0
383
+ });
384
+ }
385
+
386
+ // Arrow functions: const myFunc = () => {}
387
+ if (node.type === 'VariableDeclarator' &&
388
+ node.init &&
389
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression') &&
390
+ node.id && node.id.type === 'Identifier') {
391
+ const funcContent = getCode(node);
392
+ functions.push({
393
+ name: node.id.name,
394
+ content: funcContent,
395
+ startLine: node.loc ? node.loc.start.line : 0,
396
+ endLine: node.loc ? node.loc.end.line : 0
397
+ });
398
+ }
399
+
400
+ // Helper function to extract methods from a class body
401
+ const extractClassMethods = (classNode, className) => {
402
+ const methods = [];
403
+ if (classNode.body && classNode.body.body && Array.isArray(classNode.body.body)) {
404
+ for (const member of classNode.body.body) {
405
+ // Handle MethodDefinition (regular methods, constructors, getters, setters, static methods)
406
+ if (member && member.type === 'MethodDefinition' && member.key) {
407
+ let methodName;
408
+ if (member.key.type === 'Identifier') {
409
+ methodName = member.key.name;
410
+ } else if (member.key.type === 'PrivateName') {
411
+ methodName = '#' + member.key.id.name;
412
+ } else if (member.key.type === 'StringLiteral' || member.key.type === 'NumericLiteral') {
413
+ methodName = String(member.key.value);
414
+ } else {
415
+ methodName = String(member.key.value || member.key.name || 'unknown');
416
+ }
417
+
418
+ // Include kind (constructor, get, set, method) in the name for clarity
419
+ const kind = member.kind || 'method';
420
+ const isStatic = member.static || false;
421
+
422
+ // For getters and setters, include the kind in the method name to distinguish them
423
+ // e.g., "value" getter vs "value" setter -> "get value" and "set value"
424
+ let fullMethodName = methodName;
425
+ if (kind === 'get') {
426
+ fullMethodName = 'get ' + methodName;
427
+ } else if (kind === 'set') {
428
+ fullMethodName = 'set ' + methodName;
429
+ } else if (kind === 'constructor') {
430
+ fullMethodName = 'constructor';
431
+ } else if (isStatic) {
432
+ fullMethodName = 'static ' + methodName;
433
+ }
434
+
435
+ methods.push({
436
+ name: className + '.' + methodName,
437
+ content: getCode(member),
438
+ startLine: member.loc ? member.loc.start.line : 0,
439
+ endLine: member.loc ? member.loc.end.line : 0,
440
+ isMethod: true,
441
+ className: className,
442
+ methodName: methodName,
443
+ kind: kind,
444
+ static: isStatic
445
+ });
446
+ }
447
+ }
448
+ }
449
+ return methods;
450
+ };
451
+
452
+ // Class declarations: class User { ... }
453
+ // Skip if inside ExportNamedDeclaration or ExportDefaultDeclaration (will be handled below)
454
+ if (node.type === 'ClassDeclaration' && node.id &&
455
+ parentType !== 'ExportNamedDeclaration' && parentType !== 'ExportDefaultDeclaration') {
456
+ const className = node.id.name;
457
+ const methods = extractClassMethods(node, className);
458
+
459
+ classes.push({
460
+ name: className,
461
+ content: getCode(node),
462
+ methods: methods,
463
+ startLine: node.loc ? node.loc.start.line : 0,
464
+ endLine: node.loc ? node.loc.end.line : 0
465
+ });
466
+ }
467
+
468
+ // Export declarations: export function, export class
469
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
470
+ if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
471
+ functions.push({
472
+ name: node.declaration.id.name,
473
+ content: getCode(node.declaration),
474
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
475
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
476
+ isExported: true
477
+ });
478
+ // Mark as visited to avoid duplicate processing
479
+ visitedNodes.add(node.declaration);
480
+ } else if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
481
+ const className = node.declaration.id.name;
482
+ const methods = extractClassMethods(node.declaration, className);
483
+
484
+ classes.push({
485
+ name: className,
486
+ content: getCode(node.declaration),
487
+ methods: methods,
488
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
489
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
490
+ isExported: true
491
+ });
492
+ // Mark as visited to avoid duplicate processing
493
+ visitedNodes.add(node.declaration);
494
+ }
495
+ }
496
+
497
+ // Export default declarations: export default class
498
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
499
+ if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
500
+ const className = node.declaration.id.name;
501
+ const methods = extractClassMethods(node.declaration, className);
502
+
503
+ classes.push({
504
+ name: className,
505
+ content: getCode(node.declaration),
506
+ methods: methods,
507
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
508
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
509
+ isExported: true,
510
+ isDefaultExport: true
511
+ });
512
+ // Mark as visited to avoid duplicate processing
513
+ visitedNodes.add(node.declaration);
514
+ } else if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
515
+ functions.push({
516
+ name: node.declaration.id.name,
517
+ content: getCode(node.declaration),
518
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
519
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
520
+ isExported: true,
521
+ isDefaultExport: true
522
+ });
523
+ visitedNodes.add(node.declaration);
524
+ }
525
+ }
526
+
527
+ // Recursively traverse children
528
+ for (const key in node) {
529
+ if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
530
+ const child = node[key];
531
+ if (Array.isArray(child)) {
532
+ child.forEach(c => traverse(c, node.type));
533
+ } else if (child && typeof child === 'object' && child.type) {
534
+ traverse(child, node.type);
535
+ }
536
+ }
537
+ }
538
+
539
+ traverse(ast);
540
+ } catch (parseError) {
541
+ // Silently skip parsing errors - these are expected for some files
542
+ // Only log if it's not in node_modules or a known problematic file type
543
+ if (!filePath.includes('node_modules') && !filePath.endsWith('.d.ts') && !filePath.endsWith('.min.js')) {
544
+ // Suppress warnings for common parsing issues
545
+ const errorMsg = parseError.message || '';
546
+ if (!errorMsg.includes('outside of function') &&
547
+ !errorMsg.includes('Missing initializer') &&
548
+ !errorMsg.includes('Export') &&
549
+ !errorMsg.includes('Unexpected token')) {
550
+ // Only log unexpected errors
551
+ }
552
+ }
553
+ return { functions: [], classes: [] };
554
+ }
555
+
556
+ return { functions, classes };
557
+ } catch (e) {
558
+ console.warn('Failed to read JavaScript/TypeScript file:', filePath, e.message);
559
+ return { functions: [], classes: [] };
560
+ }
561
+ }
562
+
563
+ // Extract script content from Vue file and parse it
564
+ function extractVueFunctions(filePath) {
565
+ try {
566
+ const content = fs.readFileSync(filePath, 'utf8');
567
+
568
+ // Extract <script> section from Vue file
569
+ const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
570
+ if (!scriptMatch) {
571
+ return { functions: [], classes: [] };
572
+ }
573
+
574
+ const scriptContent = scriptMatch[1];
575
+
576
+ // Create a temporary file path for parsing (just for reference)
577
+ // Parse the script content as JavaScript/TypeScript
578
+ if (!babelParser) {
579
+ return { functions: [], classes: [] };
580
+ }
581
+
582
+ const functions = [];
583
+ const classes = [];
584
+
585
+ // Check if it's TypeScript
586
+ const isTypeScript = scriptMatch[0].includes('lang="ts"') || scriptMatch[0].includes("lang='ts'");
587
+
588
+ try {
589
+ const ast = babelParser.parse(scriptContent, {
590
+ sourceType: 'module',
591
+ plugins: [
592
+ 'typescript',
593
+ 'jsx',
594
+ 'decorators-legacy',
595
+ 'classProperties',
596
+ 'objectRestSpread',
597
+ 'asyncGenerators',
598
+ 'functionBind',
599
+ 'exportDefaultFrom',
600
+ 'exportNamespaceFrom',
601
+ 'dynamicImport',
602
+ 'nullishCoalescingOperator',
603
+ 'optionalChaining'
604
+ ]
605
+ });
606
+
607
+ // Helper to extract code snippet from source
608
+ const getCode = (node) => {
609
+ return scriptContent.substring(node.start, node.end);
610
+ };
611
+
612
+ // Track visited nodes to avoid duplicates
613
+ const visitedNodes = new Set();
614
+
615
+ // Traverse AST (same logic as JavaScript parser)
616
+ function traverse(node, parentType = null) {
617
+ if (!node) return;
618
+
619
+ // Skip if already visited (avoid processing same node twice)
620
+ if (visitedNodes.has(node)) return;
621
+ visitedNodes.add(node);
622
+
623
+ // Function declarations: function myFunc() {}
624
+ // Skip if inside ExportNamedDeclaration (will be handled below)
625
+ if (node.type === 'FunctionDeclaration' && node.id && parentType !== 'ExportNamedDeclaration') {
626
+ functions.push({
627
+ name: node.id.name,
628
+ content: getCode(node),
629
+ startLine: node.loc ? node.loc.start.line : 0,
630
+ endLine: node.loc ? node.loc.end.line : 0
631
+ });
632
+ }
633
+
634
+ // Arrow functions: const myFunc = () => {}
635
+ if (node.type === 'VariableDeclarator' &&
636
+ node.init &&
637
+ (node.init.type === 'ArrowFunctionExpression' || node.init.type === 'FunctionExpression') &&
638
+ node.id && node.id.type === 'Identifier') {
639
+ const funcContent = getCode(node);
640
+ functions.push({
641
+ name: node.id.name,
642
+ content: funcContent,
643
+ startLine: node.loc ? node.loc.start.line : 0,
644
+ endLine: node.loc ? node.loc.end.line : 0
645
+ });
646
+ }
647
+
648
+ // Helper function to extract methods from a class body
649
+ const extractClassMethods = (classNode, className) => {
650
+ const methods = [];
651
+ if (classNode.body && classNode.body.body && Array.isArray(classNode.body.body)) {
652
+ for (const member of classNode.body.body) {
653
+ // Handle MethodDefinition (regular methods, constructors, getters, setters, static methods)
654
+ if (member && member.type === 'MethodDefinition' && member.key) {
655
+ let methodName;
656
+ if (member.key.type === 'Identifier') {
657
+ methodName = member.key.name;
658
+ } else if (member.key.type === 'PrivateName') {
659
+ methodName = '#' + member.key.id.name;
660
+ } else if (member.key.type === 'StringLiteral' || member.key.type === 'NumericLiteral') {
661
+ methodName = String(member.key.value);
662
+ } else {
663
+ methodName = String(member.key.value || member.key.name || 'unknown');
664
+ }
665
+
666
+ // Include kind (constructor, get, set, method) in the name for clarity
667
+ const kind = member.kind || 'method';
668
+ const isStatic = member.static || false;
669
+
670
+ // For getters and setters, include the kind in the method name to distinguish them
671
+ // e.g., "value" getter vs "value" setter -> "get value" and "set value"
672
+ let fullMethodName = methodName;
673
+ if (kind === 'get') {
674
+ fullMethodName = 'get ' + methodName;
675
+ } else if (kind === 'set') {
676
+ fullMethodName = 'set ' + methodName;
677
+ } else if (kind === 'constructor') {
678
+ fullMethodName = 'constructor';
679
+ } else if (isStatic) {
680
+ fullMethodName = 'static ' + methodName;
681
+ }
682
+
683
+ methods.push({
684
+ name: className + '.' + methodName,
685
+ content: getCode(member),
686
+ startLine: member.loc ? member.loc.start.line : 0,
687
+ endLine: member.loc ? member.loc.end.line : 0,
688
+ isMethod: true,
689
+ className: className,
690
+ methodName: methodName,
691
+ kind: kind,
692
+ static: isStatic
693
+ });
694
+ }
695
+ }
696
+ }
697
+ return methods;
698
+ };
699
+
700
+ // Class declarations: class User { ... }
701
+ // Skip if inside ExportNamedDeclaration or ExportDefaultDeclaration (will be handled below)
702
+ if (node.type === 'ClassDeclaration' && node.id &&
703
+ parentType !== 'ExportNamedDeclaration' && parentType !== 'ExportDefaultDeclaration') {
704
+ const className = node.id.name;
705
+ const methods = extractClassMethods(node, className);
706
+
707
+ classes.push({
708
+ name: className,
709
+ content: getCode(node),
710
+ methods: methods,
711
+ startLine: node.loc ? node.loc.start.line : 0,
712
+ endLine: node.loc ? node.loc.end.line : 0
713
+ });
714
+ }
715
+
716
+ // Export declarations: export function, export class
717
+ if (node.type === 'ExportNamedDeclaration' && node.declaration) {
718
+ if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
719
+ functions.push({
720
+ name: node.declaration.id.name,
721
+ content: getCode(node.declaration),
722
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
723
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
724
+ isExported: true
725
+ });
726
+ // Mark as visited to avoid duplicate processing
727
+ visitedNodes.add(node.declaration);
728
+ } else if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
729
+ const className = node.declaration.id.name;
730
+ const methods = extractClassMethods(node.declaration, className);
731
+
732
+ classes.push({
733
+ name: className,
734
+ content: getCode(node.declaration),
735
+ methods: methods,
736
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
737
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
738
+ isExported: true
739
+ });
740
+ // Mark as visited to avoid duplicate processing
741
+ visitedNodes.add(node.declaration);
742
+ }
743
+ }
744
+
745
+ // Export default declarations: export default class
746
+ if (node.type === 'ExportDefaultDeclaration' && node.declaration) {
747
+ if (node.declaration.type === 'ClassDeclaration' && node.declaration.id) {
748
+ const className = node.declaration.id.name;
749
+ const methods = extractClassMethods(node.declaration, className);
750
+
751
+ classes.push({
752
+ name: className,
753
+ content: getCode(node.declaration),
754
+ methods: methods,
755
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
756
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
757
+ isExported: true,
758
+ isDefaultExport: true
759
+ });
760
+ // Mark as visited to avoid duplicate processing
761
+ visitedNodes.add(node.declaration);
762
+ } else if (node.declaration.type === 'FunctionDeclaration' && node.declaration.id) {
763
+ functions.push({
764
+ name: node.declaration.id.name,
765
+ content: getCode(node.declaration),
766
+ startLine: node.declaration.loc ? node.declaration.loc.start.line : 0,
767
+ endLine: node.declaration.loc ? node.declaration.loc.end.line : 0,
768
+ isExported: true,
769
+ isDefaultExport: true
770
+ });
771
+ visitedNodes.add(node.declaration);
772
+ }
773
+ }
774
+
775
+ // Recursively traverse children
776
+ for (const key in node) {
777
+ if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
778
+ const child = node[key];
779
+ if (Array.isArray(child)) {
780
+ child.forEach(c => traverse(c, node.type));
781
+ } else if (child && typeof child === 'object' && child.type) {
782
+ traverse(child, node.type);
783
+ }
784
+ }
785
+ }
786
+
787
+ traverse(ast);
788
+ } catch (parseError) {
789
+ // Silently skip parsing errors for Vue files
790
+ return { functions: [], classes: [] };
791
+ }
792
+
793
+ return { functions, classes };
794
+ } catch (e) {
795
+ console.warn('Failed to read Vue file:', filePath, e.message);
796
+ return { functions: [], classes: [] };
797
+ }
798
+ }
799
+
800
+ // Recursively list all files in a directory, respecting ignore patterns
801
+ function listAllFiles(dir, base = '', ignorePatterns = null) {
802
+ if (ignorePatterns === null) {
803
+ ignorePatterns = getIgnorePatterns();
804
+ }
805
+
806
+ let results = [];
807
+ const list = fs.readdirSync(dir);
808
+
809
+ for (const file of list) {
810
+ const fullPath = path.join(dir, file);
811
+ const relativePath = path.join(base, file);
812
+
813
+ // Check if this path should be ignored
814
+ if (shouldIgnore(relativePath, ignorePatterns)) {
815
+ continue;
816
+ }
817
+
818
+ const stat = fs.statSync(fullPath);
819
+ if (stat && stat.isDirectory()) {
820
+ // Skip node_modules directories explicitly
821
+ if (file === 'node_modules' || relativePath.includes('node_modules/')) {
822
+ continue;
823
+ }
824
+ // Add the directory itself to results
825
+ results.push(relativePath + '/');
826
+ // Recursively list files inside the directory
827
+ results = results.concat(listAllFiles(fullPath, relativePath, ignorePatterns));
828
+ } else {
829
+ // Skip files in node_modules explicitly
830
+ if (relativePath.includes('node_modules/') || fullPath.includes('node_modules')) {
831
+ continue;
832
+ }
833
+
834
+ results.push(relativePath);
835
+
836
+ // Helper function to add functions, classes, and methods as virtual files
837
+ const addVirtualFiles = (parseResult, filePath) => {
838
+ // Add functions
839
+ for (const func of parseResult.functions) {
840
+ const functionFileName = func.name + '.function';
841
+ const functionFilePath = path.join(filePath, functionFileName).replace(/\\/g, '/');
842
+ results.push(functionFilePath);
843
+ }
844
+
845
+ // Add classes and their methods
846
+ for (const cls of parseResult.classes) {
847
+ // Add class itself (optional, but useful)
848
+ const className = cls.name + '.class';
849
+ const classFilePath = path.join(filePath, className).replace(/\\/g, '/');
850
+ results.push(classFilePath);
851
+
852
+ // Add methods nested under the class: ClassName.methodName
853
+ if (cls.methods && cls.methods.length > 0) {
854
+ for (const method of cls.methods) {
855
+ const methodFileName = method.name + '.method';
856
+ const methodFilePath = path.join(classFilePath, methodFileName).replace(/\\/g, '/');
857
+ results.push(methodFilePath);
858
+ }
859
+ }
860
+ }
861
+ };
862
+
863
+ // Handle Python files
864
+ if (file.endsWith('.py')) {
865
+ const parseResult = extractPythonFunctions(fullPath);
866
+ addVirtualFiles(parseResult, relativePath);
867
+ }
868
+
869
+ // Handle JavaScript/TypeScript files
870
+ // Skip .d.ts files (TypeScript declaration files) and .min.js files (minified)
871
+ if ((file.endsWith('.js') || file.endsWith('.jsx') || file.endsWith('.ts') || file.endsWith('.tsx')) &&
872
+ !file.endsWith('.d.ts') && !file.endsWith('.min.js')) {
873
+ const parseResult = extractJavaScriptFunctions(fullPath);
874
+ addVirtualFiles(parseResult, relativePath);
875
+ }
876
+
877
+ // Handle Vue files
878
+ if (file.endsWith('.vue')) {
879
+ const parseResult = extractVueFunctions(fullPath);
880
+ addVirtualFiles(parseResult, relativePath);
881
+ }
882
+ }
883
+ }
884
+ return results;
885
+ }
886
+
887
+ app.get('/list', (req, res) => {
888
+ try {
889
+ const rootName = path.basename(ROOT);
890
+ const files = listAllFiles(ROOT, rootName);
891
+ res.json({ files });
892
+ } catch (e) {
893
+ res.status(500).send(e.message);
894
+ }
895
+ });
896
+
897
+ app.get('/read', (req, res) => {
898
+ try {
899
+ const requestedPath = req.query.path || '';
900
+ const filePath = path.join(ROOT, requestedPath);
901
+
902
+ if (!filePath.startsWith(ROOT)) {
903
+ return res.status(400).send('Invalid path');
904
+ }
905
+
906
+ // Helper function to find and return content from parse result
907
+ const findAndReturn = (parseResult, name, type) => {
908
+ if (type === 'function') {
909
+ const target = parseResult.functions.find(f => f.name === name);
910
+ if (target) return target.content;
911
+ } else if (type === 'method') {
912
+ // Method name format: ClassName.methodName
913
+ for (const cls of parseResult.classes) {
914
+ const method = cls.methods.find(m => m.name === name);
915
+ if (method) return method.content;
916
+ }
917
+ } else if (type === 'class') {
918
+ const target = parseResult.classes.find(c => c.name === name);
919
+ if (target) return target.content;
920
+ }
921
+ return null;
922
+ };
923
+
924
+ // Check if this is a virtual file request (.function, .method, or .class)
925
+ if (requestedPath.endsWith('.function') || requestedPath.endsWith('.method') || requestedPath.endsWith('.class')) {
926
+ // Traverse up the path to find the actual source file
927
+ let currentPath = filePath;
928
+ let sourceFilePath = null;
929
+ let parser = null;
930
+
931
+ // Keep going up until we find a source file (.py, .js, .jsx, .ts, .tsx, .vue)
932
+ while (currentPath !== ROOT && currentPath !== path.dirname(currentPath)) {
933
+ const stat = fs.existsSync(currentPath) ? fs.statSync(currentPath) : null;
934
+
935
+ // Check if current path is a file with a supported extension
936
+ if (stat && stat.isFile()) {
937
+ if (currentPath.endsWith('.py')) {
938
+ parser = extractPythonFunctions;
939
+ sourceFilePath = currentPath;
940
+ break;
941
+ } else if (currentPath.endsWith('.js') || currentPath.endsWith('.jsx') ||
942
+ currentPath.endsWith('.ts') || currentPath.endsWith('.tsx')) {
943
+ parser = extractJavaScriptFunctions;
944
+ sourceFilePath = currentPath;
945
+ break;
946
+ } else if (currentPath.endsWith('.vue')) {
947
+ parser = extractVueFunctions;
948
+ sourceFilePath = currentPath;
949
+ break;
950
+ }
951
+ }
952
+
953
+ // Move up one level
954
+ const parentPath = path.dirname(currentPath);
955
+ if (parentPath === currentPath) break; // Reached root
956
+ currentPath = parentPath;
957
+ }
958
+
959
+ if (!sourceFilePath || !parser) {
960
+ return res.status(404).send('Source file not found for virtual file');
961
+ }
962
+
963
+ // Extract the requested item name from the requested path
964
+ let itemName = '';
965
+ let itemType = '';
966
+
967
+ if (requestedPath.endsWith('.function')) {
968
+ itemName = path.basename(requestedPath, '.function');
969
+ itemType = 'function';
970
+ } else if (requestedPath.endsWith('.method')) {
971
+ itemName = path.basename(requestedPath, '.method');
972
+ itemType = 'method';
973
+ } else if (requestedPath.endsWith('.class')) {
974
+ itemName = path.basename(requestedPath, '.class');
975
+ itemType = 'class';
976
+ }
977
+
978
+ // Check if the source file exists
979
+ try {
980
+ if (!fs.existsSync(sourceFilePath)) {
981
+ return res.status(404).send('Source file not found');
982
+ }
983
+
984
+ // Parse the file
985
+ const parseResult = parser(sourceFilePath);
986
+
987
+ // Find and return the content
988
+ const content = findAndReturn(parseResult, itemName, itemType);
989
+
990
+ if (!content) {
991
+ return res.status(404).send(`${itemType} '${itemName}' not found in file`);
992
+ }
993
+
994
+ return res.json({ content });
995
+ } catch (e) {
996
+ const errorType = requestedPath.endsWith('.function') ? 'function' :
997
+ requestedPath.endsWith('.method') ? 'method' : 'class';
998
+ return res.status(500).send('Error reading ' + errorType + ': ' + e.message);
999
+ }
1000
+ }
1001
+
1002
+ // Regular file read
1003
+ const content = fs.readFileSync(filePath, 'utf8');
1004
+ res.json({ content });
1005
+ } catch (e) {
1006
+ res.status(500).send(e.message);
1007
+ }
1008
+ });
1009
+
1010
+ app.get('/git-churn', (req, res) => {
1011
+ try {
1012
+ const filePath = path.join(ROOT, req.query.path || '');
1013
+ if (!filePath.startsWith(ROOT)) {
1014
+ return res.status(400).json({ error: 'Invalid path' });
1015
+ }
1016
+
1017
+ // Get commits parameter, default to 30
1018
+ const commits = parseInt(req.query.commits) || 30;
1019
+
1020
+ // Get relative path from ROOT for git command
1021
+ const relativePath = path.relative(ROOT, filePath);
1022
+
1023
+ // Build git log command with relative path
1024
+ const gitCommand = `git log --oneline --all -${commits} -- "${relativePath}"`;
1025
+
1026
+ exec(gitCommand, { cwd: ROOT }, (error, stdout, stderr) => {
1027
+ // If git command fails (no repo, file not tracked, etc.), return 0 churn
1028
+ if (error) {
1029
+ // Check if it's because file is not in git or no git repo
1030
+ if (error.code === 128 || stderr.includes('not a git repository')) {
1031
+ return res.json({ churn: 0 });
1032
+ }
1033
+ // For other errors, still return 0 churn gracefully
1034
+ return res.json({ churn: 0 });
1035
+ }
1036
+
1037
+ // Count non-empty lines (each line is a commit)
1038
+ const commitCount = stdout.trim().split('\n').filter(line => line.trim().length > 0).length;
1039
+
1040
+ res.json({ churn: commitCount });
1041
+ });
1042
+ } catch (e) {
1043
+ // Handle any other errors gracefully
1044
+ res.status(500).json({ error: e.message, churn: 0 });
1045
+ }
1046
+ });
1047
+
1048
+ app.post('/write', (req, res) => {
1049
+ try {
1050
+ const filePath = path.join(ROOT, req.body.path || '');
1051
+ if (!filePath.startsWith(ROOT)) {
1052
+ return res.status(400).send('Invalid path');
1053
+ }
1054
+ fs.writeFileSync(filePath, req.body.content, 'utf8');
1055
+ res.json({ status: 'ok' });
1056
+ } catch (e) {
1057
+ res.status(500).send(e.message);
1058
+ }
1059
+ });
1060
+
1061
+ app.post('/write_dir', (req, res) => {
1062
+ try {
1063
+ const dirPath = path.join(ROOT, req.body.path || '');
1064
+ if (!dirPath.startsWith(ROOT)) {
1065
+ return res.status(400).send('Invalid path');
1066
+ }
1067
+ // Create directory recursively (creates parent directories if they don't exist)
1068
+ fs.mkdirSync(dirPath, { recursive: true });
1069
+ res.json({ status: 'ok' });
1070
+ } catch (e) {
1071
+ res.status(500).send(e.message);
1072
+ }
1073
+ });
1074
+
1075
+ app.post('/delete', (req, res) => {
1076
+ try {
1077
+ const targetPath = path.join(ROOT, req.body.path || '');
1078
+ if (!targetPath.startsWith(ROOT)) {
1079
+ return res.status(400).send('Invalid path');
1080
+ }
1081
+ // Delete file or directory recursively
1082
+ fs.rmSync(targetPath, { recursive: true, force: true });
1083
+ res.json({ status: 'ok' });
1084
+ } catch (e) {
1085
+ res.status(500).send(e.message);
1086
+ }
1087
+ });
1088
+
1089
+ app.post('/move', (req, res) => {
1090
+ try {
1091
+ const sourcePath = path.join(ROOT, req.body.source || '');
1092
+ const destinationPath = path.join(ROOT, req.body.destination || '');
1093
+
1094
+ // Validate both paths are within ROOT directory
1095
+ if (!sourcePath.startsWith(ROOT) || !destinationPath.startsWith(ROOT)) {
1096
+ return res.status(400).send('Invalid path');
1097
+ }
1098
+
1099
+ // Check if source exists
1100
+ if (!fs.existsSync(sourcePath)) {
1101
+ return res.status(400).send('Source path does not exist');
1102
+ }
1103
+
1104
+ // Ensure destination directory exists
1105
+ const destinationDir = path.dirname(destinationPath);
1106
+ if (!fs.existsSync(destinationDir)) {
1107
+ fs.mkdirSync(destinationDir, { recursive: true });
1108
+ }
1109
+
1110
+ // Move the file or folder
1111
+ fs.renameSync(sourcePath, destinationPath);
1112
+ res.json({ status: 'ok' });
1113
+ } catch (e) {
1114
+ res.status(500).send(e.message);
1115
+ }
1116
+ });
1117
+
1118
+ app.post('/open-cursor', (req, res) => {
1119
+ try {
1120
+ const requestedPath = req.body.path || '';
1121
+
1122
+ // Resolve path relative to ROOT (similar to other endpoints)
1123
+ // If path is absolute and within ROOT, use it directly
1124
+ // Otherwise, resolve it relative to ROOT
1125
+ let filePath;
1126
+ if (path.isAbsolute(requestedPath)) {
1127
+ // If absolute path, check if it's within ROOT
1128
+ if (requestedPath.startsWith(ROOT)) {
1129
+ filePath = requestedPath;
1130
+ } else {
1131
+ // Path might contain incorrect segments (like "codemaps")
1132
+ // Try to find ROOT in the path and extract the relative part
1133
+ const rootIndex = requestedPath.indexOf(ROOT);
1134
+ if (rootIndex !== -1) {
1135
+ // Extract the part after ROOT and remove leading slashes
1136
+ let relativePart = requestedPath.substring(rootIndex + ROOT.length);
1137
+ while (relativePart.startsWith('/')) {
1138
+ relativePart = relativePart.substring(1);
1139
+ }
1140
+ filePath = path.join(ROOT, relativePart);
1141
+ } else {
1142
+ return res.status(400).json({ error: 'Invalid path: path must be within project root' });
1143
+ }
1144
+ }
1145
+ } else {
1146
+ // Relative path - resolve relative to ROOT
1147
+ // Remove root directory name prefix if present (from /list endpoint format)
1148
+ const rootName = path.basename(ROOT);
1149
+ let relativePath = requestedPath;
1150
+ if (relativePath.startsWith(rootName + '/')) {
1151
+ relativePath = relativePath.substring(rootName.length + 1);
1152
+ }
1153
+ filePath = path.join(ROOT, relativePath);
1154
+ }
1155
+
1156
+ // Validate the resolved path is within ROOT
1157
+ if (!filePath.startsWith(ROOT)) {
1158
+ return res.status(400).json({ error: 'Invalid path' });
1159
+ }
1160
+
1161
+ const { line } = req.body;
1162
+
1163
+ // Always use cursor CLI command first (it handles line numbers correctly)
1164
+ const cursorCommands = [
1165
+ 'cursor',
1166
+ '/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
1167
+ '/usr/local/bin/cursor',
1168
+ 'code'
1169
+ ];
1170
+
1171
+ const tryCommand = (commandIndex = 0) => {
1172
+ if (commandIndex >= cursorCommands.length) {
1173
+ return res.status(500).json({
1174
+ error: 'Cursor not found. Please install Cursor CLI or check Cursor installation.'
1175
+ });
1176
+ }
1177
+
1178
+ // Use proper Cursor CLI syntax for line numbers
1179
+ const command = line
1180
+ ? `${cursorCommands[commandIndex]} --goto "${filePath}:${line}"`
1181
+ : `${cursorCommands[commandIndex]} "${filePath}"`;
1182
+
1183
+ exec(command, (error, stdout, stderr) => {
1184
+ if (error && error.code === 127) {
1185
+ // Command not found, try next one
1186
+ tryCommand(commandIndex + 1);
1187
+ } else if (error) {
1188
+ console.error('Error opening Cursor:', error);
1189
+ return res.status(500).json({ error: error.message });
1190
+ } else {
1191
+ // File opened successfully, now bring Cursor to front
1192
+ const isMac = process.platform === 'darwin';
1193
+ if (isMac) {
1194
+ // Use AppleScript to bring Cursor to the front
1195
+ exec('osascript -e "tell application \\"Cursor\\" to activate"', (activateError) => {
1196
+ if (activateError) {
1197
+ console.log('Could not activate Cursor, but file opened successfully');
1198
+ }
1199
+ });
1200
+
1201
+ // Additional command to ensure it's really in front
1202
+ setTimeout(() => {
1203
+ exec('osascript -e "tell application \\"System Events\\" to set frontmost of process \\"Cursor\\" to true"', () => {
1204
+ // Don't worry if this fails
1205
+ });
1206
+ }, 500);
1207
+ }
1208
+
1209
+ res.json({ success: true, message: 'Cursor opened and focused successfully' });
1210
+ }
1211
+ });
1212
+ };
1213
+
1214
+ tryCommand();
1215
+ } catch (e) {
1216
+ res.status(500).json({ error: e.message });
1217
+ }
1218
+ });
1219
+
1220
+ // Analyze prompt endpoint - asks agent what files it would modify without making changes
1221
+ app.post('/analyze_prompt', (req, res) => {
1222
+ console.log('🔵 [analyze_prompt] Endpoint hit');
1223
+ const { prompt } = req.body;
1224
+ console.log('🔵 [analyze_prompt] Received prompt:', prompt ? `${prompt.substring(0, 50)}...` : 'none');
1225
+
1226
+ if (!prompt || typeof prompt !== 'string') {
1227
+ console.log('❌ [analyze_prompt] Error: prompt required');
1228
+ return res.status(400).json({ error: 'prompt required' });
1229
+ }
1230
+
1231
+ // Configurable timeout (default 2 minutes for analysis)
1232
+ const timeoutMs = parseInt(req.body.timeout) || 2 * 60 * 1000;
1233
+ let timeoutId = null;
1234
+ let responseSent = false;
1235
+
1236
+ // Build analysis prompt - ask agent to list files without making changes
1237
+ const analysisPrompt = `You are analyzing a coding task. Do NOT make any changes to any files. Only analyze and list the files you would need to modify to complete this task.
1238
+
1239
+ Respond ONLY with valid JSON in this exact format (no other text):
1240
+ {"files": [{"path": "path/to/file.ext", "reason": "brief reason for modification"}]}
1241
+
1242
+ If no files need modification, respond with: {"files": []}
1243
+
1244
+ Task to analyze: ${prompt}`;
1245
+
1246
+ const args = ['--print', '--force', '--workspace', '.', analysisPrompt];
1247
+
1248
+ console.log('🔵 [analyze_prompt] Spawning cursor-agent process...');
1249
+ const proc = spawn(
1250
+ 'cursor-agent',
1251
+ args,
1252
+ {
1253
+ cwd: ROOT,
1254
+ env: process.env,
1255
+ stdio: ['ignore', 'pipe', 'pipe']
1256
+ }
1257
+ );
1258
+
1259
+ console.log('🔵 [analyze_prompt] Process spawned, PID:', proc.pid);
1260
+
1261
+ let stdout = '';
1262
+ let stderr = '';
1263
+
1264
+ timeoutId = setTimeout(() => {
1265
+ if (!responseSent && proc && !proc.killed) {
1266
+ console.log('⏱️ [analyze_prompt] Timeout reached, killing process...');
1267
+ proc.kill('SIGTERM');
1268
+ setTimeout(() => {
1269
+ if (!proc.killed) proc.kill('SIGKILL');
1270
+ }, 5000);
1271
+
1272
+ if (!responseSent) {
1273
+ responseSent = true;
1274
+ res.status(500).json({
1275
+ error: 'Process timeout',
1276
+ message: `Analysis exceeded timeout of ${timeoutMs / 1000} seconds`
1277
+ });
1278
+ }
1279
+ }
1280
+ }, timeoutMs);
1281
+
1282
+ proc.stdout.on('data', (d) => {
1283
+ stdout += d.toString();
1284
+ });
1285
+
1286
+ proc.stderr.on('data', (d) => {
1287
+ stderr += d.toString();
1288
+ });
1289
+
1290
+ proc.on('error', (error) => {
1291
+ console.log('❌ [analyze_prompt] Process error:', error.message);
1292
+ if (timeoutId) clearTimeout(timeoutId);
1293
+ if (!responseSent) {
1294
+ responseSent = true;
1295
+ return res.status(500).json({ error: error.message });
1296
+ }
1297
+ });
1298
+
1299
+ proc.on('close', (code, signal) => {
1300
+ console.log('🔵 [analyze_prompt] Process closed with code:', code);
1301
+ if (timeoutId) clearTimeout(timeoutId);
1302
+
1303
+ if (!responseSent) {
1304
+ responseSent = true;
1305
+
1306
+ // Try to parse JSON from the output
1307
+ try {
1308
+ // Look for JSON in the output - it might be wrapped in other text
1309
+ const jsonMatch = stdout.match(/\{[\s\S]*"files"[\s\S]*\}/);
1310
+ if (jsonMatch) {
1311
+ const parsed = JSON.parse(jsonMatch[0]);
1312
+ console.log('✅ [analyze_prompt] Parsed files:', parsed.files);
1313
+ res.json({ files: parsed.files || [] });
1314
+ } else {
1315
+ console.log('⚠️ [analyze_prompt] No JSON found in output, returning raw');
1316
+ res.json({
1317
+ files: [],
1318
+ raw: stdout,
1319
+ warning: 'Could not parse structured response'
1320
+ });
1321
+ }
1322
+ } catch (parseError) {
1323
+ console.log('⚠️ [analyze_prompt] JSON parse error:', parseError.message);
1324
+ res.json({
1325
+ files: [],
1326
+ raw: stdout,
1327
+ warning: 'Could not parse JSON: ' + parseError.message
1328
+ });
1329
+ }
1330
+ }
1331
+ });
1332
+ });
1333
+
1334
+ app.post('/prompt_agent', (req, res) => {
1335
+ console.log('🔵 [prompt_agent] Endpoint hit');
1336
+ const { prompt } = req.body;
1337
+ console.log('🔵 [prompt_agent] Received prompt:', prompt ? `${prompt.substring(0, 50)}...` : 'none');
1338
+
1339
+ if (!prompt || typeof prompt !== 'string') {
1340
+ console.log('❌ [prompt_agent] Error: prompt required');
1341
+ return res.status(400).json({ error: 'prompt required' });
1342
+ }
1343
+
1344
+ // Capture beforeCommit
1345
+ let beforeCommit = '';
1346
+ try {
1347
+ beforeCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
1348
+ console.log('🔵 [prompt_agent] beforeCommit:', beforeCommit);
1349
+ } catch (e) {
1350
+ console.log('⚠️ [prompt_agent] Could not get beforeCommit:', e.message);
1351
+ }
1352
+
1353
+ // Capture initial state of modified files (files already dirty before job starts)
1354
+ const initiallyModifiedFiles = new Set();
1355
+ try {
1356
+ const initialStatus = execSync('git status --short', { cwd: ROOT }).toString();
1357
+ initialStatus.split('\n').filter(Boolean).forEach(line => {
1358
+ const filePath = line.substring(3).trim();
1359
+ if (filePath) initiallyModifiedFiles.add(filePath);
1360
+ });
1361
+ console.log('🔵 [prompt_agent] Initially modified files:', Array.from(initiallyModifiedFiles));
1362
+ } catch (e) {
1363
+ console.log('⚠️ [prompt_agent] Could not get initial status:', e.message);
1364
+ }
1365
+
1366
+ // Set up file change tracking - only track NEW changes during job
1367
+ const changedFiles = new Set();
1368
+ const pollInterval = setInterval(() => {
1369
+ try {
1370
+ const status = execSync('git status --short', { cwd: ROOT }).toString();
1371
+ status.split('\n').filter(Boolean).forEach(line => {
1372
+ const filePath = line.substring(3).trim(); // Remove status prefix (XY + space)
1373
+ // Only add if this file was NOT already modified before the job started
1374
+ if (filePath && !initiallyModifiedFiles.has(filePath)) {
1375
+ const wasNew = !changedFiles.has(filePath);
1376
+ changedFiles.add(filePath);
1377
+ if (wasNew) {
1378
+ console.log('📁 [prompt_agent] New file changed:', filePath);
1379
+ }
1380
+ }
1381
+ });
1382
+ } catch (e) {
1383
+ // Ignore git status errors
1384
+ }
1385
+ }, 500);
1386
+
1387
+ // Configurable timeout (default 5 minutes)
1388
+ const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
1389
+ let timeoutId = null;
1390
+ let responseSent = false;
1391
+
1392
+ // Build command arguments
1393
+ const args = ['--print', '--force', '--workspace', '.', prompt];
1394
+
1395
+ console.log('🔵 [prompt_agent] Spawning cursor-agent process...');
1396
+ const proc = spawn(
1397
+ 'cursor-agent',
1398
+ args,
1399
+ {
1400
+ cwd: ROOT,
1401
+ env: process.env,
1402
+ stdio: ['ignore', 'pipe', 'pipe'] // Ignore stdin, pipe stdout/stderr
1403
+ }
1404
+ );
1405
+
1406
+ console.log('🔵 [prompt_agent] Process spawned, PID:', proc.pid);
1407
+
1408
+ let stdout = '';
1409
+ let stderr = '';
1410
+
1411
+ // Set up timeout to kill process if it takes too long
1412
+ timeoutId = setTimeout(() => {
1413
+ if (!responseSent && proc && !proc.killed) {
1414
+ console.log('⏱️ [prompt_agent] Timeout reached, killing process...');
1415
+ clearInterval(pollInterval);
1416
+ proc.kill('SIGTERM');
1417
+
1418
+ // Force kill after a short grace period if SIGTERM doesn't work
1419
+ setTimeout(() => {
1420
+ if (!proc.killed) {
1421
+ console.log('💀 [prompt_agent] Force killing process...');
1422
+ proc.kill('SIGKILL');
1423
+ }
1424
+ }, 5000);
1425
+
1426
+ if (!responseSent) {
1427
+ responseSent = true;
1428
+ res.status(500).json({
1429
+ error: 'Process timeout',
1430
+ message: `cursor-agent exceeded timeout of ${timeoutMs / 1000} seconds`,
1431
+ code: -1,
1432
+ stdout,
1433
+ stderr,
1434
+ changedFiles: Array.from(changedFiles),
1435
+ beforeCommit,
1436
+ afterCommit: ''
1437
+ });
1438
+ }
1439
+ }
1440
+ }, timeoutMs);
1441
+
1442
+ proc.stdout.on('data', (d) => {
1443
+ const data = d.toString();
1444
+ console.log('📤 [prompt_agent] stdout data received:', data.length, 'bytes');
1445
+ stdout += data;
1446
+ });
1447
+
1448
+ proc.stderr.on('data', (d) => {
1449
+ const data = d.toString();
1450
+ console.log('⚠️ [prompt_agent] stderr data received:', data.length, 'bytes');
1451
+ stderr += data;
1452
+ });
1453
+
1454
+ proc.on('error', (error) => {
1455
+ console.log('❌ [prompt_agent] Process error:', error.message);
1456
+ clearInterval(pollInterval);
1457
+ if (timeoutId) clearTimeout(timeoutId);
1458
+ if (!responseSent) {
1459
+ responseSent = true;
1460
+ return res.status(500).json({ error: error.message });
1461
+ }
1462
+ });
1463
+
1464
+ proc.on('close', (code, signal) => {
1465
+ console.log('🔵 [prompt_agent] Process closed with code:', code, 'signal:', signal);
1466
+ console.log('🔵 [prompt_agent] stdout length:', stdout.length);
1467
+ console.log('🔵 [prompt_agent] stderr length:', stderr.length);
1468
+
1469
+ // Stop polling for file changes
1470
+ clearInterval(pollInterval);
1471
+ if (timeoutId) clearTimeout(timeoutId);
1472
+
1473
+ // Capture afterCommit
1474
+ let afterCommit = '';
1475
+ try {
1476
+ afterCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
1477
+ console.log('🔵 [prompt_agent] afterCommit:', afterCommit);
1478
+ } catch (e) {
1479
+ console.log('⚠️ [prompt_agent] Could not get afterCommit:', e.message);
1480
+ }
1481
+
1482
+ if (!responseSent) {
1483
+ responseSent = true;
1484
+ // Check if process was killed due to timeout
1485
+ if (signal === 'SIGTERM' || signal === 'SIGKILL') {
1486
+ res.status(500).json({
1487
+ error: 'Process terminated',
1488
+ message: signal === 'SIGTERM' ? 'Process was terminated due to timeout' : 'Process was force killed',
1489
+ code: code || -1,
1490
+ stdout,
1491
+ stderr,
1492
+ changedFiles: Array.from(changedFiles),
1493
+ beforeCommit,
1494
+ afterCommit
1495
+ });
1496
+ } else {
1497
+ res.json({
1498
+ code,
1499
+ stdout,
1500
+ stderr,
1501
+ changedFiles: Array.from(changedFiles),
1502
+ beforeCommit,
1503
+ afterCommit
1504
+ });
1505
+ }
1506
+ }
1507
+ });
1508
+ });
1509
+
1510
+ // Streaming version of prompt_agent using Server-Sent Events
1511
+ app.post('/prompt_agent_stream', (req, res) => {
1512
+ console.log('🔵 [prompt_agent_stream] Endpoint hit');
1513
+ const { prompt } = req.body;
1514
+ console.log('🔵 [prompt_agent_stream] Received prompt:', prompt ? `${prompt.substring(0, 50)}...` : 'none');
1515
+
1516
+ if (!prompt || typeof prompt !== 'string') {
1517
+ console.log('❌ [prompt_agent_stream] Error: prompt required');
1518
+ return res.status(400).json({ error: 'prompt required' });
1519
+ }
1520
+
1521
+ // Set up SSE headers
1522
+ res.setHeader('Content-Type', 'text/event-stream');
1523
+ res.setHeader('Cache-Control', 'no-cache');
1524
+ res.setHeader('Connection', 'keep-alive');
1525
+ res.setHeader('Access-Control-Allow-Origin', '*');
1526
+ res.flushHeaders();
1527
+
1528
+ // Helper to send SSE events
1529
+ const sendEvent = (type, data) => {
1530
+ res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
1531
+ };
1532
+
1533
+ // Capture beforeCommit
1534
+ let beforeCommit = '';
1535
+ try {
1536
+ beforeCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
1537
+ console.log('🔵 [prompt_agent_stream] beforeCommit:', beforeCommit);
1538
+ } catch (e) {
1539
+ console.log('⚠️ [prompt_agent_stream] Could not get beforeCommit:', e.message);
1540
+ }
1541
+
1542
+ // Capture initial state of modified files
1543
+ const initiallyModifiedFiles = new Set();
1544
+ try {
1545
+ const initialStatus = execSync('git status --short', { cwd: ROOT }).toString();
1546
+ initialStatus.split('\n').filter(Boolean).forEach(line => {
1547
+ const filePath = line.substring(3).trim();
1548
+ if (filePath) initiallyModifiedFiles.add(filePath);
1549
+ });
1550
+ } catch (e) {
1551
+ // Ignore
1552
+ }
1553
+
1554
+ // Send starting event
1555
+ sendEvent('start', { beforeCommit });
1556
+
1557
+ // Set up file change tracking with real-time updates
1558
+ const changedFiles = new Set();
1559
+ const pollInterval = setInterval(() => {
1560
+ try {
1561
+ const status = execSync('git status --short', { cwd: ROOT }).toString();
1562
+ status.split('\n').filter(Boolean).forEach(line => {
1563
+ const filePath = line.substring(3).trim();
1564
+ if (filePath && !initiallyModifiedFiles.has(filePath)) {
1565
+ if (!changedFiles.has(filePath)) {
1566
+ changedFiles.add(filePath);
1567
+ console.log('📁 [prompt_agent_stream] File changed:', filePath);
1568
+ // Send real-time update to client
1569
+ sendEvent('file_changed', { path: filePath });
1570
+ }
1571
+ }
1572
+ });
1573
+ } catch (e) {
1574
+ // Ignore git status errors
1575
+ }
1576
+ }, 500);
1577
+
1578
+ const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
1579
+ let timeoutId = null;
1580
+ let responseSent = false;
1581
+
1582
+ const args = ['--print', '--force', '--workspace', '.', prompt];
1583
+
1584
+ console.log('🔵 [prompt_agent_stream] Spawning cursor-agent process...');
1585
+ const proc = spawn(
1586
+ 'cursor-agent',
1587
+ args,
1588
+ {
1589
+ cwd: ROOT,
1590
+ env: process.env,
1591
+ stdio: ['ignore', 'pipe', 'pipe']
1592
+ }
1593
+ );
1594
+
1595
+ console.log('🔵 [prompt_agent_stream] Process spawned, PID:', proc.pid);
1596
+
1597
+ let stdout = '';
1598
+ let stderr = '';
1599
+
1600
+ timeoutId = setTimeout(() => {
1601
+ if (!responseSent && proc && !proc.killed) {
1602
+ console.log('⏱️ [prompt_agent_stream] Timeout reached');
1603
+ clearInterval(pollInterval);
1604
+ proc.kill('SIGTERM');
1605
+
1606
+ setTimeout(() => {
1607
+ if (!proc.killed) proc.kill('SIGKILL');
1608
+ }, 5000);
1609
+
1610
+ if (!responseSent) {
1611
+ responseSent = true;
1612
+ sendEvent('error', {
1613
+ error: 'Process timeout',
1614
+ message: `cursor-agent exceeded timeout of ${timeoutMs / 1000} seconds`
1615
+ });
1616
+ sendEvent('complete', {
1617
+ code: -1,
1618
+ stdout,
1619
+ stderr,
1620
+ changedFiles: Array.from(changedFiles),
1621
+ beforeCommit,
1622
+ afterCommit: ''
1623
+ });
1624
+ res.end();
1625
+ }
1626
+ }
1627
+ }, timeoutMs);
1628
+
1629
+ proc.stdout.on('data', (d) => {
1630
+ stdout += d.toString();
1631
+ });
1632
+
1633
+ proc.stderr.on('data', (d) => {
1634
+ stderr += d.toString();
1635
+ });
1636
+
1637
+ proc.on('error', (error) => {
1638
+ console.log('❌ [prompt_agent_stream] Process error:', error.message);
1639
+ clearInterval(pollInterval);
1640
+ if (timeoutId) clearTimeout(timeoutId);
1641
+ if (!responseSent) {
1642
+ responseSent = true;
1643
+ sendEvent('error', { error: error.message });
1644
+ res.end();
1645
+ }
1646
+ });
1647
+
1648
+ proc.on('close', (code, signal) => {
1649
+ console.log('🔵 [prompt_agent_stream] Process closed with code:', code);
1650
+ clearInterval(pollInterval);
1651
+ if (timeoutId) clearTimeout(timeoutId);
1652
+
1653
+ let afterCommit = '';
1654
+ try {
1655
+ afterCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
1656
+ } catch (e) {
1657
+ // Ignore
1658
+ }
1659
+
1660
+ if (!responseSent) {
1661
+ responseSent = true;
1662
+ sendEvent('complete', {
1663
+ code,
1664
+ stdout,
1665
+ stderr,
1666
+ changedFiles: Array.from(changedFiles),
1667
+ beforeCommit,
1668
+ afterCommit
1669
+ });
1670
+ res.end();
1671
+ }
1672
+ });
1673
+
1674
+ // Handle client disconnect - DON'T kill the process, let it complete
1675
+ req.on('close', () => {
1676
+ console.log('🔵 [prompt_agent_stream] Client disconnected (process continues in background)');
1677
+ // Don't kill the process - let it complete
1678
+ // Just mark that we shouldn't try to send more events
1679
+ responseSent = true;
1680
+ });
1681
+ });
1682
+
1683
+ // Revert job endpoint to reset to a previous commit
1684
+ app.post('/revert_job', (req, res) => {
1685
+ console.log('🔵 [revert_job] Endpoint hit');
1686
+ const { beforeCommit } = req.body;
1687
+
1688
+ if (!beforeCommit || typeof beforeCommit !== 'string') {
1689
+ console.log('❌ [revert_job] Error: beforeCommit required');
1690
+ return res.status(400).json({ error: 'beforeCommit required' });
1691
+ }
1692
+
1693
+ // Validate commit hash format (basic sanitization to prevent command injection)
1694
+ if (!/^[a-f0-9]{7,40}$/i.test(beforeCommit)) {
1695
+ console.log('❌ [revert_job] Error: invalid commit hash format');
1696
+ return res.status(400).json({ error: 'Invalid commit hash format' });
1697
+ }
1698
+
1699
+ try {
1700
+ console.log('🔵 [revert_job] Resetting to commit:', beforeCommit);
1701
+ execSync(`git reset --hard ${beforeCommit}`, { cwd: ROOT });
1702
+ console.log('✅ [revert_job] Successfully reverted to commit:', beforeCommit);
1703
+ res.json({ success: true });
1704
+ } catch (e) {
1705
+ console.log('❌ [revert_job] Error:', e.message);
1706
+ res.status(500).json({ error: e.message });
1707
+ }
1708
+ });
1709
+
1710
+ // Shutdown endpoint to kill the server
1711
+ app.post('/shutdown', (req, res) => {
1712
+ console.log('🛑 Shutdown endpoint called - terminating server...');
1713
+
1714
+ res.json({
1715
+ success: true,
1716
+ message: 'Server shutting down...'
1717
+ });
1718
+
1719
+ // Close the server gracefully
1720
+ setTimeout(() => {
1721
+ process.exit(0);
1722
+ }, 100); // Small delay to ensure response is sent
1723
+ });
1724
+
1725
+ const port = 3001;
1726
+ app.listen(port, () => {
1727
+ console.log('📂 File server running on http://localhost:' + port);
1728
+ });
1729
+