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