@openagents-org/agent-launcher 0.2.68 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.68",
3
+ "version": "0.2.71",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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',
@@ -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
- } else if (!postedThinking) {
554
- try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
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
- } finally {
579
- if (mcpConfigFile) {
580
- try { fs.unlinkSync(mcpConfigFile); } catch {}
581
- }
582
- delete this._channelProcesses[msgChannel];
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/installer.js CHANGED
@@ -163,6 +163,16 @@ class Installer {
163
163
  }
164
164
  } catch {}
165
165
  }
166
+ // Also check OAuth credentials (Claude Code stores tokens in .credentials.json)
167
+ if (!ready) {
168
+ try {
169
+ const oauthFile = path.join(os.homedir(), '.claude', '.credentials.json');
170
+ if (fs.existsSync(oauthFile)) {
171
+ const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf-8'));
172
+ if (creds.claudeAiOauth?.accessToken) ready = true;
173
+ }
174
+ } catch {}
175
+ }
166
176
  }
167
177
 
168
178
  return { installed: true, binary, version, ready };
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
  }