devbonzai 2.1.6 → 2.1.8

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,247 @@
1
+ const { spawn, execSync } = require('child_process');
2
+ const { ROOT } = require('../config');
3
+
4
+ function promptAgentStreamHandler(req, res) {
5
+ console.log('🔵 [prompt_agent_stream] Endpoint hit');
6
+ const { prompt } = req.body;
7
+ console.log('🔵 [prompt_agent_stream] Received prompt:', prompt ? `${prompt.substring(0, 50)}...` : 'none');
8
+
9
+ if (!prompt || typeof prompt !== 'string') {
10
+ console.log('❌ [prompt_agent_stream] Error: prompt required');
11
+ return res.status(400).json({ error: 'prompt required' });
12
+ }
13
+
14
+ // Set up SSE headers
15
+ res.setHeader('Content-Type', 'text/event-stream');
16
+ res.setHeader('Cache-Control', 'no-cache');
17
+ res.setHeader('Connection', 'keep-alive');
18
+ res.setHeader('Access-Control-Allow-Origin', '*');
19
+ res.flushHeaders();
20
+
21
+ // Helper to send SSE events with robustness checks
22
+ const sendEvent = (type, data) => {
23
+ try {
24
+ // Check if response is still writable - try to send even if clientDisconnected flag is set
25
+ // because the response stream might still be open
26
+ if (res.destroyed || res.closed) {
27
+ console.log(`⚠️ [prompt_agent_stream] Response already closed, cannot send ${type} event`);
28
+ return false;
29
+ }
30
+ res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
31
+ return true;
32
+ } catch (e) {
33
+ console.log(`⚠️ [prompt_agent_stream] Error sending ${type} event:`, e.message);
34
+ return false;
35
+ }
36
+ };
37
+
38
+ // Capture beforeCommit
39
+ let beforeCommit = '';
40
+ try {
41
+ beforeCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
42
+ console.log('🔵 [prompt_agent_stream] beforeCommit:', beforeCommit);
43
+ } catch (e) {
44
+ console.log('⚠️ [prompt_agent_stream] Could not get beforeCommit:', e.message);
45
+ }
46
+
47
+ // Capture initial state of modified files
48
+ const initiallyModifiedFiles = new Set();
49
+ try {
50
+ const initialStatus = execSync('git status --short', { cwd: ROOT }).toString();
51
+ initialStatus.split('\n').filter(Boolean).forEach(line => {
52
+ const filePath = line.substring(3).trim();
53
+ if (filePath) initiallyModifiedFiles.add(filePath);
54
+ });
55
+ } catch (e) {
56
+ // Ignore
57
+ }
58
+
59
+ // Send starting event
60
+ sendEvent('start', { beforeCommit });
61
+
62
+ // Set up file change tracking with real-time updates
63
+ const changedFiles = new Set();
64
+ const pollInterval = setInterval(() => {
65
+ try {
66
+ const status = execSync('git status --short', { cwd: ROOT }).toString();
67
+ status.split('\n').filter(Boolean).forEach(line => {
68
+ const filePath = line.substring(3).trim();
69
+ if (filePath && !initiallyModifiedFiles.has(filePath)) {
70
+ if (!changedFiles.has(filePath)) {
71
+ changedFiles.add(filePath);
72
+ console.log('📁 [prompt_agent_stream] File changed:', filePath);
73
+ // Send real-time update to client
74
+ sendEvent('file_changed', { path: filePath });
75
+ }
76
+ }
77
+ });
78
+ } catch (e) {
79
+ // Ignore git status errors
80
+ }
81
+ }, 500);
82
+
83
+ const timeoutMs = parseInt(req.body.timeout) || 5 * 60 * 1000;
84
+ let timeoutId = null;
85
+ let responseSent = false;
86
+
87
+ const args = ['--print', '--force', '--workspace', '.', prompt];
88
+
89
+ console.log('🔵 [prompt_agent_stream] Spawning cursor-agent process...');
90
+ const proc = spawn(
91
+ 'cursor-agent',
92
+ args,
93
+ {
94
+ cwd: ROOT,
95
+ env: process.env,
96
+ stdio: ['ignore', 'pipe', 'pipe']
97
+ }
98
+ );
99
+
100
+ console.log('🔵 [prompt_agent_stream] Process spawned, PID:', proc.pid);
101
+
102
+ let stdout = '';
103
+ let stderr = '';
104
+
105
+ timeoutId = setTimeout(() => {
106
+ if (!responseSent && proc && !proc.killed) {
107
+ console.log('⏱️ [prompt_agent_stream] Timeout reached');
108
+ clearInterval(pollInterval);
109
+ proc.kill('SIGTERM');
110
+
111
+ setTimeout(() => {
112
+ if (!proc.killed) proc.kill('SIGKILL');
113
+ }, 5000);
114
+
115
+ // Always try to send complete event when timeout occurs
116
+ // Only skip if we've already sent it
117
+ if (!responseSent) {
118
+ try {
119
+ // Check if response is still writable - try to send even if clientDisconnected flag is set
120
+ // because the response stream might still be open
121
+ if (res.destroyed || res.closed) {
122
+ console.log('⚠️ [prompt_agent_stream] Response already closed, cannot send timeout events');
123
+ } else {
124
+ responseSent = true;
125
+ sendEvent('error', {
126
+ error: 'Process timeout',
127
+ message: `cursor-agent exceeded timeout of ${timeoutMs / 1000} seconds`
128
+ });
129
+ sendEvent('complete', {
130
+ code: -1,
131
+ stdout,
132
+ stderr,
133
+ changedFiles: Array.from(changedFiles),
134
+ beforeCommit,
135
+ afterCommit: ''
136
+ });
137
+ // Send stop event after complete
138
+ sendEvent('stop', {});
139
+ res.end();
140
+ console.log('✅ [prompt_agent_stream] Sent timeout error, complete, and stop events');
141
+ }
142
+ } catch (e) {
143
+ console.log('⚠️ [prompt_agent_stream] Error sending timeout events:', e.message);
144
+ // Don't set responseSent = true on error, in case we can retry
145
+ // But realistically, if there's an error, the connection is probably dead
146
+ }
147
+ } else {
148
+ console.log('⚠️ [prompt_agent_stream] Timeout events already sent, skipping');
149
+ }
150
+ }
151
+ }, timeoutMs);
152
+
153
+ proc.stdout.on('data', (d) => {
154
+ stdout += d.toString();
155
+ });
156
+
157
+ proc.stderr.on('data', (d) => {
158
+ stderr += d.toString();
159
+ });
160
+
161
+ proc.on('error', (error) => {
162
+ console.log('❌ [prompt_agent_stream] Process error:', error.message);
163
+ clearInterval(pollInterval);
164
+ if (timeoutId) clearTimeout(timeoutId);
165
+
166
+ // Always try to send error event when process error occurs
167
+ // Only skip if we've already sent it
168
+ if (!responseSent) {
169
+ try {
170
+ // Check if response is still writable - try to send even if clientDisconnected flag is set
171
+ // because the response stream might still be open
172
+ if (res.destroyed || res.closed) {
173
+ console.log('⚠️ [prompt_agent_stream] Response already closed, cannot send error event');
174
+ } else {
175
+ responseSent = true;
176
+ sendEvent('error', { error: error.message });
177
+ // Send stop event after error
178
+ sendEvent('stop', {});
179
+ res.end();
180
+ console.log('✅ [prompt_agent_stream] Sent error and stop events');
181
+ }
182
+ } catch (e) {
183
+ console.log('⚠️ [prompt_agent_stream] Error sending error event:', e.message);
184
+ // Don't set responseSent = true on error, in case we can retry
185
+ // But realistically, if there's an error, the connection is probably dead
186
+ }
187
+ } else {
188
+ console.log('⚠️ [prompt_agent_stream] Error event already sent, skipping');
189
+ }
190
+ });
191
+
192
+ proc.on('close', (code, signal) => {
193
+ console.log('🔵 [prompt_agent_stream] Process closed with code:', code);
194
+ clearInterval(pollInterval);
195
+ if (timeoutId) clearTimeout(timeoutId);
196
+
197
+ let afterCommit = '';
198
+ try {
199
+ afterCommit = execSync('git rev-parse HEAD', { cwd: ROOT }).toString().trim();
200
+ } catch (e) {
201
+ // Ignore
202
+ }
203
+
204
+ // Always try to send complete event when process finishes
205
+ // Check actual response stream state rather than relying on responseSent flag
206
+ // Only skip if we've already sent it (responseSent flag prevents duplicates)
207
+ if (!responseSent) {
208
+ try {
209
+ // Check if response is still writable - check actual stream state
210
+ if (res.destroyed || res.closed) {
211
+ console.log('⚠️ [prompt_agent_stream] Response already closed, cannot send complete event');
212
+ } else {
213
+ // Send events and only set flag after successful send
214
+ sendEvent('complete', {
215
+ code,
216
+ stdout,
217
+ stderr,
218
+ changedFiles: Array.from(changedFiles),
219
+ beforeCommit,
220
+ afterCommit
221
+ });
222
+ // Send stop event after complete
223
+ sendEvent('stop', {});
224
+ res.end();
225
+ responseSent = true;
226
+ console.log('✅ [prompt_agent_stream] Sent complete and stop events');
227
+ }
228
+ } catch (e) {
229
+ console.log('⚠️ [prompt_agent_stream] Error sending complete event:', e.message);
230
+ // Don't set responseSent = true on error, in case we can retry
231
+ // But realistically, if there's an error, the connection is probably dead
232
+ }
233
+ } else {
234
+ console.log('⚠️ [prompt_agent_stream] Complete event already sent, skipping');
235
+ }
236
+ });
237
+
238
+ // Handle client disconnect - DON'T kill the process, let it complete
239
+ req.on('close', () => {
240
+ console.log('🔵 [prompt_agent_stream] Client disconnected (process continues in background)');
241
+ // Don't kill the process - let it complete
242
+ // Don't set responseSent here - let proc.on('close') check actual stream state
243
+ });
244
+ }
245
+
246
+ module.exports = promptAgentStreamHandler;
247
+
@@ -0,0 +1,120 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ROOT } = require('../config');
4
+ const { extractPythonFunctions, extractJavaScriptFunctions, extractVueFunctions } = require('../utils/parsers');
5
+
6
+ function readHandler(req, res) {
7
+ try {
8
+ const requestedPath = req.query.path || '';
9
+ const filePath = path.join(ROOT, requestedPath);
10
+
11
+ if (!filePath.startsWith(ROOT)) {
12
+ return res.status(400).send('Invalid path');
13
+ }
14
+
15
+ // Helper function to find and return content from parse result
16
+ const findAndReturn = (parseResult, name, type) => {
17
+ if (type === 'function') {
18
+ const target = parseResult.functions.find(f => f.name === name);
19
+ if (target) return target.content;
20
+ } else if (type === 'method') {
21
+ // Method name format: ClassName.methodName
22
+ for (const cls of parseResult.classes) {
23
+ const method = cls.methods.find(m => m.name === name);
24
+ if (method) return method.content;
25
+ }
26
+ } else if (type === 'class') {
27
+ const target = parseResult.classes.find(c => c.name === name);
28
+ if (target) return target.content;
29
+ }
30
+ return null;
31
+ };
32
+
33
+ // Check if this is a virtual file request (.function, .method, or .class)
34
+ if (requestedPath.endsWith('.function') || requestedPath.endsWith('.method') || requestedPath.endsWith('.class')) {
35
+ // Traverse up the path to find the actual source file
36
+ let currentPath = filePath;
37
+ let sourceFilePath = null;
38
+ let parser = null;
39
+
40
+ // Keep going up until we find a source file (.py, .js, .jsx, .ts, .tsx, .vue)
41
+ while (currentPath !== ROOT && currentPath !== path.dirname(currentPath)) {
42
+ const stat = fs.existsSync(currentPath) ? fs.statSync(currentPath) : null;
43
+
44
+ // Check if current path is a file with a supported extension
45
+ if (stat && stat.isFile()) {
46
+ if (currentPath.endsWith('.py')) {
47
+ parser = extractPythonFunctions;
48
+ sourceFilePath = currentPath;
49
+ break;
50
+ } else if (currentPath.endsWith('.js') || currentPath.endsWith('.jsx') ||
51
+ currentPath.endsWith('.ts') || currentPath.endsWith('.tsx')) {
52
+ parser = extractJavaScriptFunctions;
53
+ sourceFilePath = currentPath;
54
+ break;
55
+ } else if (currentPath.endsWith('.vue')) {
56
+ parser = extractVueFunctions;
57
+ sourceFilePath = currentPath;
58
+ break;
59
+ }
60
+ }
61
+
62
+ // Move up one level
63
+ const parentPath = path.dirname(currentPath);
64
+ if (parentPath === currentPath) break; // Reached root
65
+ currentPath = parentPath;
66
+ }
67
+
68
+ if (!sourceFilePath || !parser) {
69
+ return res.status(404).send('Source file not found for virtual file');
70
+ }
71
+
72
+ // Extract the requested item name from the requested path
73
+ let itemName = '';
74
+ let itemType = '';
75
+
76
+ if (requestedPath.endsWith('.function')) {
77
+ itemName = path.basename(requestedPath, '.function');
78
+ itemType = 'function';
79
+ } else if (requestedPath.endsWith('.method')) {
80
+ itemName = path.basename(requestedPath, '.method');
81
+ itemType = 'method';
82
+ } else if (requestedPath.endsWith('.class')) {
83
+ itemName = path.basename(requestedPath, '.class');
84
+ itemType = 'class';
85
+ }
86
+
87
+ // Check if the source file exists
88
+ try {
89
+ if (!fs.existsSync(sourceFilePath)) {
90
+ return res.status(404).send('Source file not found');
91
+ }
92
+
93
+ // Parse the file
94
+ const parseResult = parser(sourceFilePath);
95
+
96
+ // Find and return the content
97
+ const content = findAndReturn(parseResult, itemName, itemType);
98
+
99
+ if (!content) {
100
+ return res.status(404).send(`${itemType} '${itemName}' not found in file`);
101
+ }
102
+
103
+ return res.json({ content });
104
+ } catch (e) {
105
+ const errorType = requestedPath.endsWith('.function') ? 'function' :
106
+ requestedPath.endsWith('.method') ? 'method' : 'class';
107
+ return res.status(500).send('Error reading ' + errorType + ': ' + e.message);
108
+ }
109
+ }
110
+
111
+ // Regular file read
112
+ const content = fs.readFileSync(filePath, 'utf8');
113
+ res.json({ content });
114
+ } catch (e) {
115
+ res.status(500).send(e.message);
116
+ }
117
+ }
118
+
119
+ module.exports = readHandler;
120
+
@@ -0,0 +1,31 @@
1
+ const { execSync } = require('child_process');
2
+ const { ROOT } = require('../config');
3
+
4
+ function revertJobHandler(req, res) {
5
+ console.log('🔵 [revert_job] Endpoint hit');
6
+ const { beforeCommit } = req.body;
7
+
8
+ if (!beforeCommit || typeof beforeCommit !== 'string') {
9
+ console.log('❌ [revert_job] Error: beforeCommit required');
10
+ return res.status(400).json({ error: 'beforeCommit required' });
11
+ }
12
+
13
+ // Validate commit hash format (basic sanitization to prevent command injection)
14
+ if (!/^[a-f0-9]{7,40}$/i.test(beforeCommit)) {
15
+ console.log('❌ [revert_job] Error: invalid commit hash format');
16
+ return res.status(400).json({ error: 'Invalid commit hash format' });
17
+ }
18
+
19
+ try {
20
+ console.log('🔵 [revert_job] Resetting to commit:', beforeCommit);
21
+ execSync(`git reset --hard ${beforeCommit}`, { cwd: ROOT });
22
+ console.log('✅ [revert_job] Successfully reverted to commit:', beforeCommit);
23
+ res.json({ success: true });
24
+ } catch (e) {
25
+ console.log('❌ [revert_job] Error:', e.message);
26
+ res.status(500).json({ error: e.message });
27
+ }
28
+ }
29
+
30
+ module.exports = revertJobHandler;
31
+
@@ -0,0 +1,16 @@
1
+ function shutdownHandler(req, res) {
2
+ console.log('🛑 Shutdown endpoint called - terminating server...');
3
+
4
+ res.json({
5
+ success: true,
6
+ message: 'Server shutting down...'
7
+ });
8
+
9
+ // Close the server gracefully
10
+ setTimeout(() => {
11
+ process.exit(0);
12
+ }, 100); // Small delay to ensure response is sent
13
+ }
14
+
15
+ module.exports = shutdownHandler;
16
+
@@ -0,0 +1,19 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ROOT } = require('../config');
4
+
5
+ function writeHandler(req, res) {
6
+ try {
7
+ const filePath = path.join(ROOT, req.body.path || '');
8
+ if (!filePath.startsWith(ROOT)) {
9
+ return res.status(400).send('Invalid path');
10
+ }
11
+ fs.writeFileSync(filePath, req.body.content, 'utf8');
12
+ res.json({ status: 'ok' });
13
+ } catch (e) {
14
+ res.status(500).send(e.message);
15
+ }
16
+ }
17
+
18
+ module.exports = writeHandler;
19
+
@@ -0,0 +1,20 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { ROOT } = require('../config');
4
+
5
+ function writeDirHandler(req, res) {
6
+ try {
7
+ const dirPath = path.join(ROOT, req.body.path || '');
8
+ if (!dirPath.startsWith(ROOT)) {
9
+ return res.status(400).send('Invalid path');
10
+ }
11
+ // Create directory recursively (creates parent directories if they don't exist)
12
+ fs.mkdirSync(dirPath, { recursive: true });
13
+ res.json({ status: 'ok' });
14
+ } catch (e) {
15
+ res.status(500).send(e.message);
16
+ }
17
+ }
18
+
19
+ module.exports = writeDirHandler;
20
+