@openagents-org/agent-launcher 0.2.69 → 0.2.72
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 +1 -1
- package/src/adapters/base.js +11 -0
- package/src/adapters/claude.js +45 -22
- package/src/daemon.js +1 -0
- package/src/installer.js +3 -2
- package/src/mcp-server.js +139 -0
package/package.json
CHANGED
package/src/adapters/base.js
CHANGED
|
@@ -313,6 +313,17 @@ class BaseAdapter {
|
|
|
313
313
|
} catch {}
|
|
314
314
|
}
|
|
315
315
|
|
|
316
|
+
async sendThinking(channel, content) {
|
|
317
|
+
try {
|
|
318
|
+
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
|
319
|
+
senderType: 'agent',
|
|
320
|
+
senderName: this.agentName,
|
|
321
|
+
messageType: 'thinking',
|
|
322
|
+
metadata: { agent_mode: this._mode },
|
|
323
|
+
});
|
|
324
|
+
} catch {}
|
|
325
|
+
}
|
|
326
|
+
|
|
316
327
|
async sendResponse(channel, content) {
|
|
317
328
|
await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
|
|
318
329
|
senderType: 'agent',
|
package/src/adapters/claude.js
CHANGED
|
@@ -192,7 +192,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
192
192
|
return null;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
_buildClaudeCmd(prompt, channelName) {
|
|
195
|
+
_buildClaudeCmd(prompt, channelName, { skipResume = false } = {}) {
|
|
196
196
|
const claudeBin = this._findClaudeBinary();
|
|
197
197
|
if (!claudeBin) {
|
|
198
198
|
throw new Error('claude CLI not found. Install with: curl -fsSL https://claude.ai/install.sh | bash');
|
|
@@ -252,9 +252,9 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
252
252
|
cmd.push('--allowedTools', ...allowed);
|
|
253
253
|
cmd.push('--disallowedTools', 'AskUserQuestion');
|
|
254
254
|
|
|
255
|
-
// Resume existing conversation
|
|
255
|
+
// Resume existing conversation (skipped on retry after stale session)
|
|
256
256
|
const sessionId = this._channelSessions[channelName];
|
|
257
|
-
if (sessionId) {
|
|
257
|
+
if (sessionId && !skipResume) {
|
|
258
258
|
cmd.push('--resume', sessionId);
|
|
259
259
|
}
|
|
260
260
|
|
|
@@ -389,20 +389,25 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
389
389
|
|
|
390
390
|
let mcpConfigFile = null;
|
|
391
391
|
let cmd;
|
|
392
|
-
try {
|
|
393
|
-
const result = this._buildClaudeCmd(content, msgChannel);
|
|
394
|
-
cmd = result.cmd;
|
|
395
|
-
mcpConfigFile = result.mcpConfigFile;
|
|
396
|
-
} catch (e) {
|
|
397
|
-
await this.sendError(msgChannel, e.message);
|
|
398
|
-
return;
|
|
399
|
-
}
|
|
400
392
|
|
|
401
393
|
// Clean env
|
|
402
394
|
const cleanEnv = { ...(this.agentEnv || process.env) };
|
|
403
395
|
delete cleanEnv.CLAUDECODE;
|
|
404
396
|
delete cleanEnv.CLAUDE_CODE_SESSION;
|
|
405
397
|
|
|
398
|
+
// Run up to 2 attempts: first with session resume, then fresh if stale session detected
|
|
399
|
+
let _shouldRetry = false;
|
|
400
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
401
|
+
if (mcpConfigFile) { try { fs.unlinkSync(mcpConfigFile); } catch {} mcpConfigFile = null; }
|
|
402
|
+
try {
|
|
403
|
+
const built = this._buildClaudeCmd(content, msgChannel, { skipResume: attempt > 0 });
|
|
404
|
+
cmd = built.cmd;
|
|
405
|
+
mcpConfigFile = built.mcpConfigFile;
|
|
406
|
+
} catch (e) {
|
|
407
|
+
await this.sendError(msgChannel, e.message);
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
406
411
|
try {
|
|
407
412
|
// Always resolve shim/symlink to node + JS entry point.
|
|
408
413
|
// On Windows: .cmd shims need cmd.exe which creates visible windows.
|
|
@@ -428,13 +433,14 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
428
433
|
let postedThinking = false;
|
|
429
434
|
let stderrBuf = '';
|
|
430
435
|
let lineBuffer = '';
|
|
436
|
+
let _pendingLines = Promise.resolve(); // chain of in-flight processLine calls
|
|
431
437
|
|
|
432
438
|
// Capture stderr for diagnostics
|
|
433
439
|
if (proc.stderr) {
|
|
434
440
|
proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
|
|
435
441
|
}
|
|
436
442
|
|
|
437
|
-
await new Promise((resolve, reject) => {
|
|
443
|
+
_shouldRetry = await new Promise((resolve, reject) => {
|
|
438
444
|
let consecutiveTimeouts = 0;
|
|
439
445
|
let lastDataTime = Date.now();
|
|
440
446
|
let timeoutTimer = null;
|
|
@@ -484,6 +490,8 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
484
490
|
}
|
|
485
491
|
lastResponseText.push(block.text.trim());
|
|
486
492
|
postedThinking = true;
|
|
493
|
+
// Stream text in real-time as "thinking" (same as Python adapter)
|
|
494
|
+
try { await this.sendThinking(msgChannel, block.text.trim()); } catch {}
|
|
487
495
|
} else if (block.type === 'tool_use') {
|
|
488
496
|
hasToolUseSinceLastText = true;
|
|
489
497
|
postedThinking = false;
|
|
@@ -530,6 +538,9 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
530
538
|
proc.on('exit', async (code) => {
|
|
531
539
|
if (timeoutTimer) clearInterval(timeoutTimer);
|
|
532
540
|
|
|
541
|
+
// Wait for all in-flight processLine calls to complete
|
|
542
|
+
try { await _pendingLines; } catch {}
|
|
543
|
+
|
|
533
544
|
// Process remaining buffer
|
|
534
545
|
const lines = lineBuffer.split('\n');
|
|
535
546
|
for (const line of lines) {
|
|
@@ -550,10 +561,19 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
550
561
|
if (fullResponse) {
|
|
551
562
|
try { await this.sendResponse(msgChannel, fullResponse); } catch {}
|
|
552
563
|
}
|
|
553
|
-
|
|
554
|
-
|
|
564
|
+
resolve(false); // done, no retry
|
|
565
|
+
} else if (code !== 0 && this._channelSessions[msgChannel]) {
|
|
566
|
+
// No output + error exit + had a resume session → stale session, signal retry
|
|
567
|
+
this._log(`Stale session detected for ${msgChannel}, clearing and retrying without resume`);
|
|
568
|
+
delete this._channelSessions[msgChannel];
|
|
569
|
+
this._saveSessions();
|
|
570
|
+
resolve(true); // retry=true
|
|
571
|
+
} else {
|
|
572
|
+
if (!postedThinking) {
|
|
573
|
+
try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
|
|
574
|
+
}
|
|
575
|
+
resolve(false);
|
|
555
576
|
}
|
|
556
|
-
resolve();
|
|
557
577
|
});
|
|
558
578
|
|
|
559
579
|
proc.on('error', (err) => {
|
|
@@ -561,26 +581,29 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
561
581
|
reject(err);
|
|
562
582
|
});
|
|
563
583
|
|
|
564
|
-
// Process lines as they arrive
|
|
584
|
+
// Process lines as they arrive (chained to preserve order)
|
|
565
585
|
proc.stdout.on('data', (chunk) => {
|
|
566
586
|
lineBuffer += chunk.toString('utf-8');
|
|
567
587
|
resetTimeout();
|
|
568
588
|
const lines = lineBuffer.split('\n');
|
|
569
589
|
lineBuffer = lines.pop(); // keep incomplete line
|
|
570
590
|
for (const line of lines) {
|
|
571
|
-
processLine(line).catch(() => {});
|
|
591
|
+
_pendingLines = _pendingLines.then(() => processLine(line)).catch(() => {});
|
|
572
592
|
}
|
|
573
593
|
});
|
|
574
594
|
});
|
|
575
595
|
} catch (e) {
|
|
576
596
|
this._log(`Error handling message: ${e.message}`);
|
|
577
597
|
await this.sendError(msgChannel, `Error processing message: ${e.message}`);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
598
|
+
break; // no retry on spawn error
|
|
599
|
+
}
|
|
600
|
+
if (!_shouldRetry) break; // exit loop if no retry needed
|
|
601
|
+
} // end for attempt
|
|
602
|
+
|
|
603
|
+
if (mcpConfigFile) {
|
|
604
|
+
try { fs.unlinkSync(mcpConfigFile); } catch {}
|
|
583
605
|
}
|
|
606
|
+
delete this._channelProcesses[msgChannel];
|
|
584
607
|
}
|
|
585
608
|
}
|
|
586
609
|
|
package/src/daemon.js
CHANGED
|
@@ -408,6 +408,7 @@ class Daemon {
|
|
|
408
408
|
openclawAgentId: agentCfg.openclaw_agent_id || 'main',
|
|
409
409
|
disabledModules: new Set(),
|
|
410
410
|
agentEnv: this._buildAgentEnv(agentCfg),
|
|
411
|
+
workingDir: agentCfg.path || undefined,
|
|
411
412
|
});
|
|
412
413
|
} catch (e) {
|
|
413
414
|
this._log(`${name} failed to create ${agentType} adapter: ${e.message}`);
|
package/src/installer.js
CHANGED
|
@@ -196,7 +196,7 @@ class Installer {
|
|
|
196
196
|
// Use bundled node/npm if system npm not available
|
|
197
197
|
if (cmd.startsWith('npm install')) {
|
|
198
198
|
const prefixDir = path.join(os.homedir(), '.openagents', 'nodejs');
|
|
199
|
-
const args = cmd.replace('npm install', 'install --ignore-scripts --no-save').replace(' -g ', ` --prefix "${prefixDir}" `);
|
|
199
|
+
const args = cmd.replace('npm install', 'install --ignore-scripts --no-save --install-strategy=nested').replace(' -g ', ` --prefix "${prefixDir}" `);
|
|
200
200
|
cmd = this._resolveNpmCommand(args);
|
|
201
201
|
}
|
|
202
202
|
|
|
@@ -232,7 +232,8 @@ class Installer {
|
|
|
232
232
|
let cmd = rawCmd;
|
|
233
233
|
if (rawCmd.startsWith('npm install')) {
|
|
234
234
|
const prefixDir2 = path.join(os.homedir(), '.openagents', 'nodejs');
|
|
235
|
-
|
|
235
|
+
// --install-strategy=nested prevents npm from pruning existing packages in the prefix
|
|
236
|
+
const args = rawCmd.replace('npm install', 'install --loglevel=verbose --ignore-scripts --no-save --install-strategy=nested').replace(' -g ', ` --prefix "${prefixDir2}" `);
|
|
236
237
|
cmd = this._resolveNpmCommand(args);
|
|
237
238
|
} else if (rawCmd.startsWith('pip install') || rawCmd.startsWith('pipx install')) {
|
|
238
239
|
cmd = rawCmd; // pip commands stay as-is
|
package/src/mcp-server.js
CHANGED
|
@@ -13,8 +13,13 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const readline = require('readline');
|
|
16
|
+
const { execSync, spawn: spawnChild } = require('child_process');
|
|
17
|
+
const net = require('net');
|
|
16
18
|
const { WorkspaceClient } = require('./workspace-client');
|
|
17
19
|
|
|
20
|
+
// Active tunnels: port → { proc, url }
|
|
21
|
+
const _activeTunnels = {};
|
|
22
|
+
|
|
18
23
|
// ── Tool definitions ────────────────────────────────────────────────────────
|
|
19
24
|
|
|
20
25
|
function buildToolDefs(disabledModules) {
|
|
@@ -185,6 +190,42 @@ function buildToolDefs(disabledModules) {
|
|
|
185
190
|
);
|
|
186
191
|
}
|
|
187
192
|
|
|
193
|
+
// -- Tunnel module --
|
|
194
|
+
if (!disabledModules.has('tunnel')) {
|
|
195
|
+
tools.push(
|
|
196
|
+
{
|
|
197
|
+
name: 'tunnel_expose',
|
|
198
|
+
description:
|
|
199
|
+
'Expose a local port as a public URL via Cloudflare tunnel. ' +
|
|
200
|
+
'Use this to let workspace users preview a local dev server ' +
|
|
201
|
+
'(e.g. React, Next.js, Flask on localhost). Returns the public URL.',
|
|
202
|
+
inputSchema: {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: {
|
|
205
|
+
port: { type: 'integer', description: 'Local port to expose (e.g. 3000)' },
|
|
206
|
+
},
|
|
207
|
+
required: ['port'],
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
name: 'tunnel_close',
|
|
212
|
+
description: 'Close a tunnel that was previously opened with tunnel_expose.',
|
|
213
|
+
inputSchema: {
|
|
214
|
+
type: 'object',
|
|
215
|
+
properties: {
|
|
216
|
+
port: { type: 'integer', description: 'Port of the tunnel to close' },
|
|
217
|
+
},
|
|
218
|
+
required: ['port'],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: 'tunnel_list',
|
|
223
|
+
description: 'List all active tunnels.',
|
|
224
|
+
inputSchema: { type: 'object', properties: {} },
|
|
225
|
+
},
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
188
229
|
return tools;
|
|
189
230
|
}
|
|
190
231
|
|
|
@@ -196,6 +237,10 @@ const MIME_MAP = {
|
|
|
196
237
|
'.html': 'text/html', '.css': 'text/css', '.xml': 'application/xml',
|
|
197
238
|
'.yaml': 'application/yaml', '.yml': 'application/yaml',
|
|
198
239
|
'.csv': 'text/csv', '.log': 'text/plain', '.sh': 'text/x-shellscript',
|
|
240
|
+
'.toml': 'application/toml', '.rs': 'text/x-rust', '.go': 'text/x-go',
|
|
241
|
+
'.java': 'text/x-java', '.rb': 'text/x-ruby', '.c': 'text/x-c',
|
|
242
|
+
'.cpp': 'text/x-c++', '.h': 'text/x-c', '.tsx': 'text/typescript',
|
|
243
|
+
'.jsx': 'text/javascript', '.sql': 'application/sql',
|
|
199
244
|
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
200
245
|
'.gif': 'image/gif', '.svg': 'image/svg+xml', '.pdf': 'application/pdf',
|
|
201
246
|
};
|
|
@@ -421,6 +466,100 @@ class McpServer {
|
|
|
421
466
|
return text(`Browser tab closed: ${args.tab_id}`);
|
|
422
467
|
}
|
|
423
468
|
|
|
469
|
+
// ── Tunnel ──
|
|
470
|
+
|
|
471
|
+
case 'tunnel_expose': {
|
|
472
|
+
const port = args.port;
|
|
473
|
+
if (!port) throw new Error('port is required');
|
|
474
|
+
|
|
475
|
+
if (_activeTunnels[port]) {
|
|
476
|
+
return text(`Tunnel already open for port ${port}: ${_activeTunnels[port].url}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Check cloudflared is available
|
|
480
|
+
let cfBin;
|
|
481
|
+
try {
|
|
482
|
+
cfBin = execSync('which cloudflared 2>/dev/null || echo ""', { encoding: 'utf-8' }).trim();
|
|
483
|
+
} catch {}
|
|
484
|
+
if (!cfBin) {
|
|
485
|
+
return text(
|
|
486
|
+
'Error: cloudflared is not installed. Install it:\n' +
|
|
487
|
+
' macOS: brew install cloudflared\n' +
|
|
488
|
+
' Linux: curl -fsSL https://github.com/cloudflare/cloudflared/' +
|
|
489
|
+
'releases/latest/download/cloudflared-linux-amd64 ' +
|
|
490
|
+
'-o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared'
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Pre-flight: check port is listening
|
|
495
|
+
const portOpen = await new Promise((resolve) => {
|
|
496
|
+
const sock = new net.Socket();
|
|
497
|
+
sock.setTimeout(2000);
|
|
498
|
+
sock.on('connect', () => { sock.destroy(); resolve(true); });
|
|
499
|
+
sock.on('error', () => resolve(false));
|
|
500
|
+
sock.on('timeout', () => { sock.destroy(); resolve(false); });
|
|
501
|
+
sock.connect(port, 'localhost');
|
|
502
|
+
});
|
|
503
|
+
if (!portOpen) {
|
|
504
|
+
return text(`Error: nothing is listening on localhost:${port}. Start a server on that port first.`);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Start cloudflared
|
|
508
|
+
const proc = spawnChild('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
|
|
509
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
510
|
+
detached: true,
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Wait for URL from stderr (cloudflared logs the URL there)
|
|
514
|
+
const url = await new Promise((resolve, reject) => {
|
|
515
|
+
const urlRe = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
516
|
+
let buf = '';
|
|
517
|
+
const timeout = setTimeout(() => {
|
|
518
|
+
proc.kill();
|
|
519
|
+
reject(new Error('cloudflared did not produce a URL within 20 seconds'));
|
|
520
|
+
}, 20000);
|
|
521
|
+
|
|
522
|
+
const onData = (chunk) => {
|
|
523
|
+
buf += chunk.toString();
|
|
524
|
+
const match = buf.match(urlRe);
|
|
525
|
+
if (match) {
|
|
526
|
+
clearTimeout(timeout);
|
|
527
|
+
proc.stderr.removeListener('data', onData);
|
|
528
|
+
resolve(match[0]);
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
proc.stderr.on('data', onData);
|
|
532
|
+
proc.on('exit', (code) => {
|
|
533
|
+
clearTimeout(timeout);
|
|
534
|
+
reject(new Error(`cloudflared exited with code ${code}: ${buf.slice(0, 200)}`));
|
|
535
|
+
});
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
_activeTunnels[port] = { proc, url };
|
|
539
|
+
proc.unref(); // don't block MCP server exit
|
|
540
|
+
return text(`Tunnel open: localhost:${port} → ${url}`);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
case 'tunnel_close': {
|
|
544
|
+
const port = args.port;
|
|
545
|
+
const tunnel = _activeTunnels[port];
|
|
546
|
+
if (!tunnel) return text(`No tunnel open for port ${port}`);
|
|
547
|
+
try { tunnel.proc.kill(); } catch {}
|
|
548
|
+
delete _activeTunnels[port];
|
|
549
|
+
return text(`Tunnel closed for port ${port}`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
case 'tunnel_list': {
|
|
553
|
+
const ports = Object.keys(_activeTunnels);
|
|
554
|
+
if (!ports.length) return text('No active tunnels.');
|
|
555
|
+
const lines = ports.map((p) => {
|
|
556
|
+
const t = _activeTunnels[p];
|
|
557
|
+
const running = t.proc && t.proc.exitCode === null;
|
|
558
|
+
return `- localhost:${p} → ${t.url} (${running ? 'running' : 'stopped'})`;
|
|
559
|
+
});
|
|
560
|
+
return text(lines.join('\n'));
|
|
561
|
+
}
|
|
562
|
+
|
|
424
563
|
default:
|
|
425
564
|
throw new Error(`Unknown tool: ${name}`);
|
|
426
565
|
}
|