devbonzai 2.1.6 → 2.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "devbonzai",
3
- "version": "2.1.6",
3
+ "version": "2.1.7",
4
4
  "description": "Quickly set up a local file server in any repository for browser-based file access",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -0,0 +1,18 @@
1
+ const path = require('path');
2
+
3
+ // Root directory (one level up from templates/)
4
+ const ROOT = path.join(__dirname, '..');
5
+
6
+ // Initialize babelParser (optional dependency)
7
+ let babelParser = null;
8
+ try {
9
+ babelParser = require('./node_modules/@babel/parser');
10
+ } catch (e) {
11
+ // Babel parser not available, will fall back gracefully
12
+ }
13
+
14
+ module.exports = {
15
+ ROOT,
16
+ babelParser
17
+ };
18
+
@@ -0,0 +1,118 @@
1
+ const { spawn } = require('child_process');
2
+ const { ROOT } = require('../config');
3
+
4
+ function analyzePromptHandler(req, res) {
5
+ console.log('🔵 [analyze_prompt] Endpoint hit');
6
+ const { prompt } = req.body;
7
+ console.log('🔵 [analyze_prompt] Received prompt:', prompt ? `${prompt.substring(0, 50)}...` : 'none');
8
+
9
+ if (!prompt || typeof prompt !== 'string') {
10
+ console.log('❌ [analyze_prompt] Error: prompt required');
11
+ return res.status(400).json({ error: 'prompt required' });
12
+ }
13
+
14
+ // Configurable timeout (default 2 minutes for analysis)
15
+ const timeoutMs = parseInt(req.body.timeout) || 2 * 60 * 1000;
16
+ let timeoutId = null;
17
+ let responseSent = false;
18
+
19
+ // Build analysis prompt - ask agent to list files without making changes
20
+ 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.
21
+
22
+ Respond ONLY with valid JSON in this exact format (no other text):
23
+ {"files": [{"path": "path/to/file.ext", "reason": "brief reason for modification"}]}
24
+
25
+ If no files need modification, respond with: {"files": []}
26
+
27
+ Task to analyze: ${prompt}`;
28
+
29
+ const args = ['--print', '--force', '--workspace', '.', analysisPrompt];
30
+
31
+ console.log('🔵 [analyze_prompt] Spawning cursor-agent process...');
32
+ const proc = spawn(
33
+ 'cursor-agent',
34
+ args,
35
+ {
36
+ cwd: ROOT,
37
+ env: process.env,
38
+ stdio: ['ignore', 'pipe', 'pipe']
39
+ }
40
+ );
41
+
42
+ console.log('🔵 [analyze_prompt] Process spawned, PID:', proc.pid);
43
+
44
+ let stdout = '';
45
+ let stderr = '';
46
+
47
+ timeoutId = setTimeout(() => {
48
+ if (!responseSent && proc && !proc.killed) {
49
+ console.log('⏱️ [analyze_prompt] Timeout reached, killing process...');
50
+ proc.kill('SIGTERM');
51
+ setTimeout(() => {
52
+ if (!proc.killed) proc.kill('SIGKILL');
53
+ }, 5000);
54
+
55
+ if (!responseSent) {
56
+ responseSent = true;
57
+ res.status(500).json({
58
+ error: 'Process timeout',
59
+ message: `Analysis exceeded timeout of ${timeoutMs / 1000} seconds`
60
+ });
61
+ }
62
+ }
63
+ }, timeoutMs);
64
+
65
+ proc.stdout.on('data', (d) => {
66
+ stdout += d.toString();
67
+ });
68
+
69
+ proc.stderr.on('data', (d) => {
70
+ stderr += d.toString();
71
+ });
72
+
73
+ proc.on('error', (error) => {
74
+ console.log('❌ [analyze_prompt] Process error:', error.message);
75
+ if (timeoutId) clearTimeout(timeoutId);
76
+ if (!responseSent) {
77
+ responseSent = true;
78
+ return res.status(500).json({ error: error.message });
79
+ }
80
+ });
81
+
82
+ proc.on('close', (code, signal) => {
83
+ console.log('🔵 [analyze_prompt] Process closed with code:', code);
84
+ if (timeoutId) clearTimeout(timeoutId);
85
+
86
+ if (!responseSent) {
87
+ responseSent = true;
88
+
89
+ // Try to parse JSON from the output
90
+ try {
91
+ // Look for JSON in the output - it might be wrapped in other text
92
+ const jsonMatch = stdout.match(/\{[\s\S]*"files"[\s\S]*\}/);
93
+ if (jsonMatch) {
94
+ const parsed = JSON.parse(jsonMatch[0]);
95
+ console.log('✅ [analyze_prompt] Parsed files:', parsed.files);
96
+ res.json({ files: parsed.files || [] });
97
+ } else {
98
+ console.log('⚠️ [analyze_prompt] No JSON found in output, returning raw');
99
+ res.json({
100
+ files: [],
101
+ raw: stdout,
102
+ warning: 'Could not parse structured response'
103
+ });
104
+ }
105
+ } catch (parseError) {
106
+ console.log('⚠️ [analyze_prompt] JSON parse error:', parseError.message);
107
+ res.json({
108
+ files: [],
109
+ raw: stdout,
110
+ warning: 'Could not parse JSON: ' + parseError.message
111
+ });
112
+ }
113
+ }
114
+ });
115
+ }
116
+
117
+ module.exports = analyzePromptHandler;
118
+
@@ -0,0 +1,20 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ROOT } = require('../config');
4
+
5
+ function deleteHandler(req, res) {
6
+ try {
7
+ const targetPath = path.join(ROOT, req.body.path || '');
8
+ if (!targetPath.startsWith(ROOT)) {
9
+ return res.status(400).send('Invalid path');
10
+ }
11
+ // Delete file or directory recursively
12
+ fs.rmSync(targetPath, { recursive: true, force: true });
13
+ res.json({ status: 'ok' });
14
+ } catch (e) {
15
+ res.status(500).send(e.message);
16
+ }
17
+ }
18
+
19
+ module.exports = deleteHandler;
20
+
@@ -0,0 +1,44 @@
1
+ const path = require('path');
2
+ const { exec } = require('child_process');
3
+ const { ROOT } = require('../config');
4
+
5
+ function gitChurnHandler(req, res) {
6
+ try {
7
+ const filePath = path.join(ROOT, req.query.path || '');
8
+ if (!filePath.startsWith(ROOT)) {
9
+ return res.status(400).json({ error: 'Invalid path' });
10
+ }
11
+
12
+ // Get commits parameter, default to 30
13
+ const commits = parseInt(req.query.commits) || 30;
14
+
15
+ // Get relative path from ROOT for git command
16
+ const relativePath = path.relative(ROOT, filePath);
17
+
18
+ // Build git log command with relative path
19
+ const gitCommand = `git log --oneline --all -${commits} -- "${relativePath}"`;
20
+
21
+ exec(gitCommand, { cwd: ROOT }, (error, stdout, stderr) => {
22
+ // If git command fails (no repo, file not tracked, etc.), return 0 churn
23
+ if (error) {
24
+ // Check if it's because file is not in git or no git repo
25
+ if (error.code === 128 || stderr.includes('not a git repository')) {
26
+ return res.json({ churn: 0 });
27
+ }
28
+ // For other errors, still return 0 churn gracefully
29
+ return res.json({ churn: 0 });
30
+ }
31
+
32
+ // Count non-empty lines (each line is a commit)
33
+ const commitCount = stdout.trim().split('\n').filter(line => line.trim().length > 0).length;
34
+
35
+ res.json({ churn: commitCount });
36
+ });
37
+ } catch (e) {
38
+ // Handle any other errors gracefully
39
+ res.status(500).json({ error: e.message, churn: 0 });
40
+ }
41
+ }
42
+
43
+ module.exports = gitChurnHandler;
44
+
@@ -0,0 +1,25 @@
1
+ // Root route - simple API documentation
2
+ function indexHandler(req, res) {
3
+ res.json({
4
+ message: 'Local File Server API',
5
+ endpoints: {
6
+ 'GET /list': 'List all files in the directory',
7
+ 'GET /read?path=<filepath>': 'Read file content',
8
+ 'GET /git-churn?path=<filepath>&commits=30': 'Get git commit churn for a file',
9
+ 'POST /write': 'Write file content (body: {path, content})',
10
+ 'POST /write_dir': 'Create directory (body: {path})',
11
+ 'POST /delete': 'Delete file or directory (body: {path})',
12
+ 'POST /move': 'Move file or folder (body: {source, destination})',
13
+ 'POST /open-cursor': 'Open Cursor (body: {path, line?})',
14
+ 'POST /analyze_prompt': 'Analyze what files would be modified (body: {prompt})',
15
+ 'POST /prompt_agent': 'Execute cursor-agent command (body: {prompt})',
16
+ 'POST /prompt_agent_stream': 'Execute cursor-agent with SSE streaming (body: {prompt})',
17
+ 'POST /revert_job': 'Revert to a previous commit (body: {beforeCommit})',
18
+ 'POST /shutdown': 'Gracefully shutdown the server'
19
+ },
20
+ example: 'Try: /list or /read?path=README.md'
21
+ });
22
+ }
23
+
24
+ module.exports = indexHandler;
25
+
@@ -0,0 +1,16 @@
1
+ const path = require('path');
2
+ const { ROOT } = require('../config');
3
+ const { listAllFiles } = require('../utils/fileList');
4
+
5
+ function listHandler(req, res) {
6
+ try {
7
+ const rootName = path.basename(ROOT);
8
+ const files = listAllFiles(ROOT, rootName);
9
+ res.json({ files });
10
+ } catch (e) {
11
+ res.status(500).send(e.message);
12
+ }
13
+ }
14
+
15
+ module.exports = listHandler;
16
+
@@ -0,0 +1,35 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ROOT } = require('../config');
4
+
5
+ function moveHandler(req, res) {
6
+ try {
7
+ const sourcePath = path.join(ROOT, req.body.source || '');
8
+ const destinationPath = path.join(ROOT, req.body.destination || '');
9
+
10
+ // Validate both paths are within ROOT directory
11
+ if (!sourcePath.startsWith(ROOT) || !destinationPath.startsWith(ROOT)) {
12
+ return res.status(400).send('Invalid path');
13
+ }
14
+
15
+ // Check if source exists
16
+ if (!fs.existsSync(sourcePath)) {
17
+ return res.status(400).send('Source path does not exist');
18
+ }
19
+
20
+ // Ensure destination directory exists
21
+ const destinationDir = path.dirname(destinationPath);
22
+ if (!fs.existsSync(destinationDir)) {
23
+ fs.mkdirSync(destinationDir, { recursive: true });
24
+ }
25
+
26
+ // Move the file or folder
27
+ fs.renameSync(sourcePath, destinationPath);
28
+ res.json({ status: 'ok' });
29
+ } catch (e) {
30
+ res.status(500).send(e.message);
31
+ }
32
+ }
33
+
34
+ module.exports = moveHandler;
35
+
@@ -0,0 +1,108 @@
1
+ const path = require('path');
2
+ const { exec } = require('child_process');
3
+ const { ROOT } = require('../config');
4
+
5
+ function openCursorHandler(req, res) {
6
+ try {
7
+ const requestedPath = req.body.path || '';
8
+
9
+ // Resolve path relative to ROOT (similar to other endpoints)
10
+ // If path is absolute and within ROOT, use it directly
11
+ // Otherwise, resolve it relative to ROOT
12
+ let filePath;
13
+ if (path.isAbsolute(requestedPath)) {
14
+ // If absolute path, check if it's within ROOT
15
+ if (requestedPath.startsWith(ROOT)) {
16
+ filePath = requestedPath;
17
+ } else {
18
+ // Path might contain incorrect segments (like "codemaps")
19
+ // Try to find ROOT in the path and extract the relative part
20
+ const rootIndex = requestedPath.indexOf(ROOT);
21
+ if (rootIndex !== -1) {
22
+ // Extract the part after ROOT and remove leading slashes
23
+ let relativePart = requestedPath.substring(rootIndex + ROOT.length);
24
+ while (relativePart.startsWith('/')) {
25
+ relativePart = relativePart.substring(1);
26
+ }
27
+ filePath = path.join(ROOT, relativePart);
28
+ } else {
29
+ return res.status(400).json({ error: 'Invalid path: path must be within project root' });
30
+ }
31
+ }
32
+ } else {
33
+ // Relative path - resolve relative to ROOT
34
+ // Remove root directory name prefix if present (from /list endpoint format)
35
+ const rootName = path.basename(ROOT);
36
+ let relativePath = requestedPath;
37
+ if (relativePath.startsWith(rootName + '/')) {
38
+ relativePath = relativePath.substring(rootName.length + 1);
39
+ }
40
+ filePath = path.join(ROOT, relativePath);
41
+ }
42
+
43
+ // Validate the resolved path is within ROOT
44
+ if (!filePath.startsWith(ROOT)) {
45
+ return res.status(400).json({ error: 'Invalid path' });
46
+ }
47
+
48
+ const { line } = req.body;
49
+
50
+ // Always use cursor CLI command first (it handles line numbers correctly)
51
+ const cursorCommands = [
52
+ 'cursor',
53
+ '/Applications/Cursor.app/Contents/Resources/app/bin/cursor',
54
+ '/usr/local/bin/cursor',
55
+ 'code'
56
+ ];
57
+
58
+ const tryCommand = (commandIndex = 0) => {
59
+ if (commandIndex >= cursorCommands.length) {
60
+ return res.status(500).json({
61
+ error: 'Cursor not found. Please install Cursor CLI or check Cursor installation.'
62
+ });
63
+ }
64
+
65
+ // Use proper Cursor CLI syntax for line numbers
66
+ const command = line
67
+ ? `${cursorCommands[commandIndex]} --goto "${filePath}:${line}"`
68
+ : `${cursorCommands[commandIndex]} "${filePath}"`;
69
+
70
+ exec(command, (error, stdout, stderr) => {
71
+ if (error && error.code === 127) {
72
+ // Command not found, try next one
73
+ tryCommand(commandIndex + 1);
74
+ } else if (error) {
75
+ console.error('Error opening Cursor:', error);
76
+ return res.status(500).json({ error: error.message });
77
+ } else {
78
+ // File opened successfully, now bring Cursor to front
79
+ const isMac = process.platform === 'darwin';
80
+ if (isMac) {
81
+ // Use AppleScript to bring Cursor to the front
82
+ exec('osascript -e "tell application \\"Cursor\\" to activate"', (activateError) => {
83
+ if (activateError) {
84
+ console.log('Could not activate Cursor, but file opened successfully');
85
+ }
86
+ });
87
+
88
+ // Additional command to ensure it's really in front
89
+ setTimeout(() => {
90
+ exec('osascript -e "tell application \\"System Events\\" to set frontmost of process \\"Cursor\\" to true"', () => {
91
+ // Don't worry if this fails
92
+ });
93
+ }, 500);
94
+ }
95
+
96
+ res.json({ success: true, message: 'Cursor opened and focused successfully' });
97
+ }
98
+ });
99
+ };
100
+
101
+ tryCommand();
102
+ } catch (e) {
103
+ res.status(500).json({ error: e.message });
104
+ }
105
+ }
106
+
107
+ module.exports = openCursorHandler;
108
+
@@ -0,0 +1,181 @@
1
+ const { spawn, execSync } = require('child_process');
2
+ const { ROOT } = require('../config');
3
+
4
+ function promptAgentHandler(req, res) {
5
+ console.log('🔵 [prompt_agent] Endpoint hit');
6
+ const { prompt } = req.body;
7
+ console.log('🔵 [prompt_agent] Received prompt:', prompt ? `${prompt.substring(0, 50)}...` : 'none');
8
+
9
+ if (!prompt || typeof prompt !== 'string') {
10
+ console.log('❌ [prompt_agent] Error: prompt required');
11
+ return res.status(400).json({ error: 'prompt required' });
12
+ }
13
+
14
+ // Capture beforeCommit
15
+ let beforeCommit = '';
16
+ try {
17
+ beforeCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
18
+ console.log('🔵 [prompt_agent] beforeCommit:', beforeCommit);
19
+ } catch (e) {
20
+ console.log('⚠️ [prompt_agent] Could not get beforeCommit:', e.message);
21
+ }
22
+
23
+ // Capture initial state of modified files (files already dirty before job starts)
24
+ const initiallyModifiedFiles = new Set();
25
+ try {
26
+ const initialStatus = execSync('git status --short', { cwd: ROOT }).toString();
27
+ initialStatus.split('\n').filter(Boolean).forEach(line => {
28
+ const filePath = line.substring(3).trim();
29
+ if (filePath) initiallyModifiedFiles.add(filePath);
30
+ });
31
+ console.log('🔵 [prompt_agent] Initially modified files:', Array.from(initiallyModifiedFiles));
32
+ } catch (e) {
33
+ console.log('⚠️ [prompt_agent] Could not get initial status:', e.message);
34
+ }
35
+
36
+ // Set up file change tracking - only track NEW changes during job
37
+ const changedFiles = new Set();
38
+ const pollInterval = setInterval(() => {
39
+ try {
40
+ const status = execSync('git status --short', { cwd: ROOT }).toString();
41
+ status.split('\n').filter(Boolean).forEach(line => {
42
+ const filePath = line.substring(3).trim(); // Remove status prefix (XY + space)
43
+ // Only add if this file was NOT already modified before the job started
44
+ if (filePath && !initiallyModifiedFiles.has(filePath)) {
45
+ const wasNew = !changedFiles.has(filePath);
46
+ changedFiles.add(filePath);
47
+ if (wasNew) {
48
+ console.log('📁 [prompt_agent] New file changed:', filePath);
49
+ }
50
+ }
51
+ });
52
+ } catch (e) {
53
+ // Ignore git status errors
54
+ }
55
+ }, 500);
56
+
57
+ // Configurable timeout (default 5 minutes)
58
+ const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
59
+ let timeoutId = null;
60
+ let responseSent = false;
61
+
62
+ // Build command arguments
63
+ const args = ['--print', '--force', '--workspace', '.', prompt];
64
+
65
+ console.log('🔵 [prompt_agent] Spawning cursor-agent process...');
66
+ const proc = spawn(
67
+ 'cursor-agent',
68
+ args,
69
+ {
70
+ cwd: ROOT,
71
+ env: process.env,
72
+ stdio: ['ignore', 'pipe', 'pipe'] // Ignore stdin, pipe stdout/stderr
73
+ }
74
+ );
75
+
76
+ console.log('🔵 [prompt_agent] Process spawned, PID:', proc.pid);
77
+
78
+ let stdout = '';
79
+ let stderr = '';
80
+
81
+ // Set up timeout to kill process if it takes too long
82
+ timeoutId = setTimeout(() => {
83
+ if (!responseSent && proc && !proc.killed) {
84
+ console.log('⏱️ [prompt_agent] Timeout reached, killing process...');
85
+ clearInterval(pollInterval);
86
+ proc.kill('SIGTERM');
87
+
88
+ // Force kill after a short grace period if SIGTERM doesn't work
89
+ setTimeout(() => {
90
+ if (!proc.killed) {
91
+ console.log('💀 [prompt_agent] Force killing process...');
92
+ proc.kill('SIGKILL');
93
+ }
94
+ }, 5000);
95
+
96
+ if (!responseSent) {
97
+ responseSent = true;
98
+ res.status(500).json({
99
+ error: 'Process timeout',
100
+ message: `cursor-agent exceeded timeout of ${timeoutMs / 1000} seconds`,
101
+ code: -1,
102
+ stdout,
103
+ stderr,
104
+ changedFiles: Array.from(changedFiles),
105
+ beforeCommit,
106
+ afterCommit: ''
107
+ });
108
+ }
109
+ }
110
+ }, timeoutMs);
111
+
112
+ proc.stdout.on('data', (d) => {
113
+ const data = d.toString();
114
+ console.log('📤 [prompt_agent] stdout data received:', data.length, 'bytes');
115
+ stdout += data;
116
+ });
117
+
118
+ proc.stderr.on('data', (d) => {
119
+ const data = d.toString();
120
+ console.log('⚠️ [prompt_agent] stderr data received:', data.length, 'bytes');
121
+ stderr += data;
122
+ });
123
+
124
+ proc.on('error', (error) => {
125
+ console.log('❌ [prompt_agent] Process error:', error.message);
126
+ clearInterval(pollInterval);
127
+ if (timeoutId) clearTimeout(timeoutId);
128
+ if (!responseSent) {
129
+ responseSent = true;
130
+ return res.status(500).json({ error: error.message });
131
+ }
132
+ });
133
+
134
+ proc.on('close', (code, signal) => {
135
+ console.log('🔵 [prompt_agent] Process closed with code:', code, 'signal:', signal);
136
+ console.log('🔵 [prompt_agent] stdout length:', stdout.length);
137
+ console.log('🔵 [prompt_agent] stderr length:', stderr.length);
138
+
139
+ // Stop polling for file changes
140
+ clearInterval(pollInterval);
141
+ if (timeoutId) clearTimeout(timeoutId);
142
+
143
+ // Capture afterCommit
144
+ let afterCommit = '';
145
+ try {
146
+ afterCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
147
+ console.log('🔵 [prompt_agent] afterCommit:', afterCommit);
148
+ } catch (e) {
149
+ console.log('⚠️ [prompt_agent] Could not get afterCommit:', e.message);
150
+ }
151
+
152
+ if (!responseSent) {
153
+ responseSent = true;
154
+ // Check if process was killed due to timeout
155
+ if (signal === 'SIGTERM' || signal === 'SIGKILL') {
156
+ res.status(500).json({
157
+ error: 'Process terminated',
158
+ message: signal === 'SIGTERM' ? 'Process was terminated due to timeout' : 'Process was force killed',
159
+ code: code || -1,
160
+ stdout,
161
+ stderr,
162
+ changedFiles: Array.from(changedFiles),
163
+ beforeCommit,
164
+ afterCommit
165
+ });
166
+ } else {
167
+ res.json({
168
+ code,
169
+ stdout,
170
+ stderr,
171
+ changedFiles: Array.from(changedFiles),
172
+ beforeCommit,
173
+ afterCommit
174
+ });
175
+ }
176
+ }
177
+ });
178
+ }
179
+
180
+ module.exports = promptAgentHandler;
181
+