deepdebug-local-agent 0.3.11 → 0.3.12

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/server.js +150 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepdebug-local-agent",
3
- "version": "0.3.11",
3
+ "version": "0.3.12",
4
4
  "description": "Insptech AI — DeepDebug Local Agent. Autonomous code debugging agent for production environments.",
5
5
  "type": "module",
6
6
  "main": "server.js",
package/server.js CHANGED
@@ -557,6 +557,7 @@ app.use(cors({
557
557
  // Cloud Run URLs (regex patterns)
558
558
  /https:\/\/.*\.run\.app$/,
559
559
  /https:\/\/.*\.web\.app$/,
560
+ // Production frontend
560
561
  "https://deepdebug.ai",
561
562
  "https://www.deepdebug.ai"
562
563
  ],
@@ -5082,10 +5083,138 @@ app.post("/workspace/:workspaceId/run", async (req, res) => {
5082
5083
  }
5083
5084
  });
5084
5085
 
5086
+
5087
+ // ============================================
5088
+ // šŸ”Œ WEBSOCKET REVERSE TUNNEL
5089
+ // Connects Local Agent to Gateway — no public URL needed
5090
+ // ============================================
5091
+ async function startWebSocketTunnel(port, gatewayUrl, apiKey, tenantId) {
5092
+ if (!gatewayUrl || !apiKey || !tenantId) {
5093
+ console.log('āš ļø WebSocket tunnel skipped: missing gatewayUrl, apiKey or tenantId in ~/.deepdebug/config.json');
5094
+ return;
5095
+ }
5096
+
5097
+ const wsUrl = gatewayUrl.replace(/^https:\/\//, 'wss://').replace(/^http:\/\//, 'ws://');
5098
+ const fullUrl = `${wsUrl}/api/v1/agent/ws?tenantId=${encodeURIComponent(tenantId)}&apiKey=${encodeURIComponent(apiKey)}`;
5099
+
5100
+ let reconnectDelay = 2000;
5101
+ let isShuttingDown = false;
5102
+ let ws = null;
5103
+
5104
+ async function connect() {
5105
+ if (isShuttingDown) return;
5106
+ try {
5107
+ const { default: WebSocket } = await import('ws');
5108
+ ws = new WebSocket(fullUrl);
5109
+
5110
+ ws.on('open', () => {
5111
+ console.log(`āœ… WebSocket tunnel connected to Gateway`);
5112
+ reconnectDelay = 2000;
5113
+ ws.send(JSON.stringify({ type: 'register', tenantId, port, workspacePath: WORKSPACE_ROOT || null }));
5114
+ const hb = setInterval(() => {
5115
+ if (ws.readyState === WebSocket.OPEN) ws.send('ping');
5116
+ else clearInterval(hb);
5117
+ }, 30000);
5118
+ });
5119
+
5120
+ ws.on('message', async (data) => {
5121
+ const text = data.toString();
5122
+ if (text === 'pong') return;
5123
+ let request;
5124
+ try { request = JSON.parse(text); } catch { return; }
5125
+ const { requestId, command, params } = request;
5126
+ if (!requestId || !command) return;
5127
+ console.log(`šŸ“Ø WS command: ${command}`);
5128
+ let result;
5129
+ try { result = await handleWsCommand(command, params || {}); }
5130
+ catch (err) { result = { error: err.message }; }
5131
+ ws.send(JSON.stringify({ requestId, command, ...result }));
5132
+ });
5133
+
5134
+ ws.on('close', () => {
5135
+ if (!isShuttingDown) {
5136
+ console.log(`šŸ”Œ WS disconnected. Reconnecting in ${reconnectDelay/1000}s...`);
5137
+ setTimeout(connect, reconnectDelay);
5138
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
5139
+ }
5140
+ });
5141
+
5142
+ ws.on('error', (err) => console.warn(`āš ļø WS error: ${err.message}`));
5143
+
5144
+ } catch (err) {
5145
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
5146
+ console.warn('āš ļø WebSocket tunnel requires "ws" package. Run: npm install ws');
5147
+ } else {
5148
+ console.warn(`āš ļø WS tunnel error: ${err.message}`);
5149
+ setTimeout(connect, reconnectDelay);
5150
+ reconnectDelay = Math.min(reconnectDelay * 2, 30000);
5151
+ }
5152
+ }
5153
+ }
5154
+
5155
+ async function handleWsCommand(command, params) {
5156
+ const localBase = `http://localhost:${port}`;
5157
+ switch (command) {
5158
+ case 'health':
5159
+ return { status: 'ok', workspace: WORKSPACE_ROOT, openWorkspaces: wsManager ? wsManager.list().length : 0 };
5160
+
5161
+ case 'workspace.open': {
5162
+ const { root } = params;
5163
+ if (!root) return { error: 'root is required' };
5164
+ await wsManager.open('default', root);
5165
+ WORKSPACE_ROOT = root;
5166
+ return { ok: true, root };
5167
+ }
5168
+
5169
+ case 'workspace.file-content': {
5170
+ const { path: filePath } = params;
5171
+ if (!filePath) return { error: 'path is required' };
5172
+ if (!WORKSPACE_ROOT) return { error: 'No workspace open' };
5173
+ const absPath = path.isAbsolute(filePath) ? filePath : path.join(WORKSPACE_ROOT, filePath);
5174
+ try {
5175
+ const content = await fsPromises.readFile(absPath, 'utf-8');
5176
+ return { content, path: filePath, found: true };
5177
+ } catch (err) {
5178
+ return { content: null, found: false, error: err.message };
5179
+ }
5180
+ }
5181
+
5182
+ case 'workspace.safe-patch':
5183
+ case 'workspace.compile':
5184
+ case 'workspace.rollback':
5185
+ case 'workspace.validate-diff':
5186
+ case 'workspace.backups': {
5187
+ const endpointMap = {
5188
+ 'workspace.safe-patch': '/workspace/safe-patch',
5189
+ 'workspace.compile': '/workspace/compile',
5190
+ 'workspace.rollback': '/workspace/rollback',
5191
+ 'workspace.validate-diff': '/workspace/validate-diff',
5192
+ 'workspace.backups': '/workspace/backups'
5193
+ };
5194
+ const endpoint = endpointMap[command];
5195
+ const method = command === 'workspace.backups' ? 'GET' : 'POST';
5196
+ const res = await fetch(`${localBase}${endpoint}`, {
5197
+ method,
5198
+ headers: { 'Content-Type': 'application/json' },
5199
+ ...(method === 'POST' ? { body: JSON.stringify(params) } : {})
5200
+ });
5201
+ return await res.json();
5202
+ }
5203
+
5204
+ default:
5205
+ return { error: `Unknown command: ${command}` };
5206
+ }
5207
+ }
5208
+
5209
+ process.on('SIGINT', () => { isShuttingDown = true; if (ws) ws.close(); });
5210
+ process.on('SIGTERM', () => { isShuttingDown = true; if (ws) ws.close(); });
5211
+ connect();
5212
+ }
5213
+
5085
5214
  // ============================================
5086
5215
  // START SERVER
5087
5216
  // ============================================
5088
- app.listen(PORT, '0.0.0.0', () => {
5217
+ app.listen(PORT, '0.0.0.0', async () => {
5089
5218
  console.log(`\nšŸ”Œ DeepDebug Local Agent listening on port ${PORT}`);
5090
5219
  console.log(`šŸ“¦ Environment: ${process.env.NODE_ENV || 'development'}`);
5091
5220
  console.log(`šŸ“¦ Process Manager initialized`);
@@ -5096,6 +5225,26 @@ app.listen(PORT, '0.0.0.0', () => {
5096
5225
  console.log(` Max Retries: ${aiEngine.maxRetries}`);
5097
5226
  }
5098
5227
  console.log(`\nšŸš€ Ready to receive requests!\n`);
5228
+
5229
+ // šŸ”Œ Start WebSocket reverse tunnel
5230
+ const homeDir = process.env.HOME || process.env.USERPROFILE;
5231
+ const configPath = path.join(homeDir, '.deepdebug', 'config.json');
5232
+ let cfg = {};
5233
+ try { cfg = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch {}
5234
+
5235
+ const gwUrl = cfg.gatewayUrl || process.env.GATEWAY_URL;
5236
+ const apiKey = cfg.apiKey || process.env.DEEPDEBUG_API_KEY;
5237
+ let tenantId = cfg.tenantId;
5238
+ if (!tenantId && apiKey) {
5239
+ try {
5240
+ const payload = JSON.parse(Buffer.from(apiKey.split('.')[1], 'base64').toString());
5241
+ tenantId = payload.tenantId;
5242
+ } catch {}
5243
+ }
5244
+
5245
+ if (process.env.DISABLE_WS_TUNNEL !== 'true') {
5246
+ startWebSocketTunnel(PORT, gwUrl, apiKey, tenantId);
5247
+ }
5099
5248
  });
5100
5249
 
5101
5250
  // Start MCP HTTP Bridge em paralelo (port 5056)