@openagents-org/agent-launcher 0.2.69 → 0.2.71
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 +39 -20
- package/src/daemon.js +1 -0
- 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.
|
|
@@ -434,7 +439,7 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
434
439
|
proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
|
|
435
440
|
}
|
|
436
441
|
|
|
437
|
-
await new Promise((resolve, reject) => {
|
|
442
|
+
_shouldRetry = await new Promise((resolve, reject) => {
|
|
438
443
|
let consecutiveTimeouts = 0;
|
|
439
444
|
let lastDataTime = Date.now();
|
|
440
445
|
let timeoutTimer = null;
|
|
@@ -484,6 +489,8 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
484
489
|
}
|
|
485
490
|
lastResponseText.push(block.text.trim());
|
|
486
491
|
postedThinking = true;
|
|
492
|
+
// Stream text in real-time as "thinking" (same as Python adapter)
|
|
493
|
+
try { await this.sendThinking(msgChannel, block.text.trim()); } catch {}
|
|
487
494
|
} else if (block.type === 'tool_use') {
|
|
488
495
|
hasToolUseSinceLastText = true;
|
|
489
496
|
postedThinking = false;
|
|
@@ -550,10 +557,19 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
550
557
|
if (fullResponse) {
|
|
551
558
|
try { await this.sendResponse(msgChannel, fullResponse); } catch {}
|
|
552
559
|
}
|
|
553
|
-
|
|
554
|
-
|
|
560
|
+
resolve(false); // done, no retry
|
|
561
|
+
} else if (code !== 0 && this._channelSessions[msgChannel]) {
|
|
562
|
+
// No output + error exit + had a resume session → stale session, signal retry
|
|
563
|
+
this._log(`Stale session detected for ${msgChannel}, clearing and retrying without resume`);
|
|
564
|
+
delete this._channelSessions[msgChannel];
|
|
565
|
+
this._saveSessions();
|
|
566
|
+
resolve(true); // retry=true
|
|
567
|
+
} else {
|
|
568
|
+
if (!postedThinking) {
|
|
569
|
+
try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
|
|
570
|
+
}
|
|
571
|
+
resolve(false);
|
|
555
572
|
}
|
|
556
|
-
resolve();
|
|
557
573
|
});
|
|
558
574
|
|
|
559
575
|
proc.on('error', (err) => {
|
|
@@ -575,12 +591,15 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
575
591
|
} catch (e) {
|
|
576
592
|
this._log(`Error handling message: ${e.message}`);
|
|
577
593
|
await this.sendError(msgChannel, `Error processing message: ${e.message}`);
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
594
|
+
break; // no retry on spawn error
|
|
595
|
+
}
|
|
596
|
+
if (!_shouldRetry) break; // exit loop if no retry needed
|
|
597
|
+
} // end for attempt
|
|
598
|
+
|
|
599
|
+
if (mcpConfigFile) {
|
|
600
|
+
try { fs.unlinkSync(mcpConfigFile); } catch {}
|
|
583
601
|
}
|
|
602
|
+
delete this._channelProcesses[msgChannel];
|
|
584
603
|
}
|
|
585
604
|
}
|
|
586
605
|
|
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/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
|
}
|