@openagents-org/agent-launcher 0.1.17 → 0.2.2

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.1.17",
3
+ "version": "0.2.2",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/registry.json CHANGED
@@ -433,6 +433,7 @@
433
433
  "cli",
434
434
  "terminal"
435
435
  ],
436
+ "builtin": true,
436
437
  "install": {
437
438
  "binary": "opencode",
438
439
  "requires": [
@@ -441,6 +442,55 @@
441
442
  "macos": "npm install -g opencode-ai@latest",
442
443
  "linux": "npm install -g opencode-ai@latest",
443
444
  "windows": "npm install -g opencode-ai@latest"
445
+ },
446
+ "adapter": {
447
+ "module": "openagents.adapters.opencode",
448
+ "class": "OpenCodeAdapter"
449
+ },
450
+ "env_config": [
451
+ {
452
+ "name": "LLM_API_KEY",
453
+ "description": "API key",
454
+ "required": true,
455
+ "password": true
456
+ },
457
+ {
458
+ "name": "LLM_BASE_URL",
459
+ "description": "API base URL (OpenAI-compatible endpoint)",
460
+ "required": false,
461
+ "default": "https://api.openai.com/v1",
462
+ "placeholder": "https://api.openai.com/v1"
463
+ },
464
+ {
465
+ "name": "LLM_MODEL",
466
+ "description": "Model name",
467
+ "required": false,
468
+ "placeholder": "gpt-4o, claude-sonnet-4-20250514, etc."
469
+ }
470
+ ],
471
+ "resolve_env": {
472
+ "rules": [
473
+ {
474
+ "from": "LLM_API_KEY",
475
+ "to": "OPENAI_API_KEY"
476
+ },
477
+ {
478
+ "from": "LLM_BASE_URL",
479
+ "to": "OPENAI_BASE_URL"
480
+ },
481
+ {
482
+ "from": "LLM_MODEL",
483
+ "to": "OPENCODE_MODEL"
484
+ }
485
+ ]
486
+ },
487
+ "check_ready": {
488
+ "env_vars": [
489
+ "OPENAI_API_KEY",
490
+ "ANTHROPIC_API_KEY"
491
+ ],
492
+ "saved_env_key": "LLM_API_KEY",
493
+ "not_ready_message": "Not configured — press e to configure"
444
494
  }
445
495
  },
446
496
  {
@@ -106,6 +106,7 @@ class ClaudeAdapter extends BaseAdapter {
106
106
  }
107
107
 
108
108
  _findClaudeBinary() {
109
+ // Tier 1: PATH search
109
110
  try {
110
111
  if (IS_WINDOWS) {
111
112
  const r = execSync('where claude.cmd 2>nul || where claude.exe 2>nul || where claude 2>nul', {
@@ -115,9 +116,29 @@ class ClaudeAdapter extends BaseAdapter {
115
116
  } else {
116
117
  return execSync('which claude', { encoding: 'utf-8', timeout: 5000 }).trim();
117
118
  }
118
- } catch {
119
- return null;
119
+ } catch {}
120
+
121
+ // Tier 2: Next to current Node.js interpreter (npm global)
122
+ const nodeBinDir = path.dirname(process.execPath);
123
+ const ext = IS_WINDOWS ? '.cmd' : '';
124
+ const nearNode = path.join(nodeBinDir, `claude${ext}`);
125
+ if (fs.existsSync(nearNode)) return nearNode;
126
+
127
+ // Tier 3: Common install locations
128
+ const home = os.homedir();
129
+ const candidates = IS_WINDOWS ? [
130
+ path.join(process.env.APPDATA || '', 'npm', 'claude.cmd'),
131
+ ] : [
132
+ path.join(home, '.local', 'bin', 'claude'),
133
+ path.join(home, '.npm-global', 'bin', 'claude'),
134
+ '/opt/homebrew/bin/claude',
135
+ '/usr/local/bin/claude',
136
+ ];
137
+ for (const c of candidates) {
138
+ if (fs.existsSync(c)) return c;
120
139
  }
140
+
141
+ return null;
121
142
  }
122
143
 
123
144
  _buildClaudeCmd(prompt, channelName) {
@@ -197,8 +218,9 @@ class ClaudeAdapter extends BaseAdapter {
197
218
  if (this.disabledModules.has('files')) mcpArgs.push('--disable-files');
198
219
  if (this.disabledModules.has('browser')) mcpArgs.push('--disable-browser');
199
220
 
200
- // Find openagents binary
201
- let oaBin;
221
+ // Find openagents binary (multi-tier)
222
+ let oaBin = null;
223
+ // Tier 1: PATH
202
224
  try {
203
225
  if (IS_WINDOWS) {
204
226
  oaBin = execSync('where openagents.cmd 2>nul || where openagents.exe 2>nul || where openagents 2>nul', {
@@ -207,7 +229,31 @@ class ClaudeAdapter extends BaseAdapter {
207
229
  } else {
208
230
  oaBin = execSync('which openagents', { encoding: 'utf-8', timeout: 5000 }).trim();
209
231
  }
210
- } catch {
232
+ } catch {}
233
+ // Tier 2: Next to Node.js
234
+ if (!oaBin) {
235
+ const nodeBinDir2 = path.dirname(process.execPath);
236
+ const oaExt = IS_WINDOWS ? '.cmd' : '';
237
+ const nearNode2 = path.join(nodeBinDir2, `openagents${oaExt}`);
238
+ if (fs.existsSync(nearNode2)) oaBin = nearNode2;
239
+ }
240
+ // Tier 3: Common locations
241
+ if (!oaBin) {
242
+ const home2 = os.homedir();
243
+ const oaCandidates = IS_WINDOWS ? [
244
+ path.join(process.env.APPDATA || '', 'npm', 'openagents.cmd'),
245
+ ] : [
246
+ path.join(home2, '.openagents', 'npm-global', 'bin', 'openagents'),
247
+ path.join(home2, '.local', 'bin', 'openagents'),
248
+ path.join(home2, '.npm-global', 'bin', 'openagents'),
249
+ '/opt/homebrew/bin/openagents',
250
+ '/usr/local/bin/openagents',
251
+ ];
252
+ for (const c of oaCandidates) {
253
+ if (fs.existsSync(c)) { oaBin = c; break; }
254
+ }
255
+ }
256
+ if (!oaBin) {
211
257
  oaBin = 'openagents';
212
258
  this._log('Could not find openagents binary — MCP tools may not be available');
213
259
  }
@@ -309,15 +355,47 @@ class ClaudeAdapter extends BaseAdapter {
309
355
  const lastResponseText = [];
310
356
  let hasToolUseSinceLastText = false;
311
357
  let postedThinking = false;
358
+ let stderrBuf = '';
312
359
 
313
- // Read stream-json output line by line
314
- let buffer = '';
315
- proc.stdout.on('data', (chunk) => { buffer += chunk.toString('utf-8'); });
360
+ // Capture stderr for diagnostics
361
+ if (proc.stderr) {
362
+ proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
363
+ }
316
364
 
317
365
  await new Promise((resolve, reject) => {
366
+ let consecutiveTimeouts = 0;
367
+ let lastDataTime = Date.now();
368
+ let timeoutTimer = null;
369
+
370
+ const resetTimeout = () => {
371
+ consecutiveTimeouts = 0;
372
+ lastDataTime = Date.now();
373
+ };
374
+
375
+ // 15-second idle timeout monitoring
376
+ const startTimeoutMonitor = () => {
377
+ timeoutTimer = setInterval(async () => {
378
+ const elapsed = Date.now() - lastDataTime;
379
+ if (elapsed >= 15000) {
380
+ consecutiveTimeouts++;
381
+ lastDataTime = Date.now(); // reset for next interval
382
+ if (consecutiveTimeouts === 2) {
383
+ try { await this.sendStatus(msgChannel, 'Compacting conversation...'); } catch {}
384
+ }
385
+ // Kill after 20 consecutive timeouts (~5 minutes of no output)
386
+ if (consecutiveTimeouts >= 20) {
387
+ this._log(`Process idle for ${consecutiveTimeouts * 15}s, killing...`);
388
+ await this._stopProcess(proc);
389
+ }
390
+ }
391
+ }, 15000);
392
+ };
393
+ startTimeoutMonitor();
394
+
318
395
  const processLine = async (line) => {
319
396
  line = line.trim();
320
397
  if (!line) return;
398
+ resetTimeout();
321
399
 
322
400
  let event;
323
401
  try { event = JSON.parse(line); } catch { return; }
@@ -363,10 +441,14 @@ class ClaudeAdapter extends BaseAdapter {
363
441
  if (subtype.includes('compact') || String(message).toLowerCase().includes('compact')) {
364
442
  await this.sendStatus(msgChannel, String(message) || 'Compacting conversation...');
365
443
  }
444
+ } else if (eventType === 'rate_limit_event') {
445
+ this._log(`Rate limited: ${JSON.stringify(event).slice(0, 200)}`);
366
446
  }
367
447
  };
368
448
 
369
449
  proc.on('exit', async (code) => {
450
+ if (timeoutTimer) clearInterval(timeoutTimer);
451
+
370
452
  // Process remaining buffer
371
453
  const lines = buffer.split('\n');
372
454
  for (const line of lines) {
@@ -377,6 +459,9 @@ class ClaudeAdapter extends BaseAdapter {
377
459
 
378
460
  if (code !== 0) {
379
461
  this._log(`CLI exited with code ${code}`);
462
+ if (stderrBuf.trim()) {
463
+ this._log(`stderr: ${stderrBuf.trim().slice(0, 500)}`);
464
+ }
380
465
  }
381
466
 
382
467
  if (lastResponseText.length > 0) {
@@ -390,12 +475,16 @@ class ClaudeAdapter extends BaseAdapter {
390
475
  resolve();
391
476
  });
392
477
 
393
- proc.on('error', (err) => reject(err));
478
+ proc.on('error', (err) => {
479
+ if (timeoutTimer) clearInterval(timeoutTimer);
480
+ reject(err);
481
+ });
394
482
 
395
483
  // Process lines as they arrive
396
484
  let lineBuffer = '';
397
485
  proc.stdout.on('data', (chunk) => {
398
486
  lineBuffer += chunk.toString('utf-8');
487
+ resetTimeout();
399
488
  const lines = lineBuffer.split('\n');
400
489
  lineBuffer = lines.pop(); // keep incomplete line
401
490
  for (const line of lines) {
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Cursor adapter — AI-powered code editor agent mode.
3
+ *
4
+ * Uses direct LLM API mode (OpenAI-compatible chat completions).
5
+ * Port of Python: src/openagents/adapters/cursor.py
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const LlmDirectAdapter = require('./llm-direct');
11
+
12
+ class CursorAdapter extends LlmDirectAdapter {
13
+ constructor(opts) {
14
+ super({
15
+ ...opts,
16
+ adapterLabel: 'Cursor',
17
+ modelEnvVar: 'CURSOR_MODEL',
18
+ });
19
+ }
20
+ }
21
+
22
+ module.exports = CursorAdapter;
@@ -8,16 +8,22 @@ const BaseAdapter = require('./base');
8
8
  const OpenClawAdapter = require('./openclaw');
9
9
  const ClaudeAdapter = require('./claude');
10
10
  const CodexAdapter = require('./codex');
11
+ const OpenCodeAdapter = require('./opencode');
12
+ const NanoClawAdapter = require('./nanoclaw');
13
+ const CursorAdapter = require('./cursor');
11
14
 
12
15
  const ADAPTER_MAP = {
13
16
  openclaw: OpenClawAdapter,
14
17
  claude: ClaudeAdapter,
15
18
  codex: CodexAdapter,
19
+ opencode: OpenCodeAdapter,
20
+ nanoclaw: NanoClawAdapter,
21
+ cursor: CursorAdapter,
16
22
  };
17
23
 
18
24
  /**
19
25
  * Create an adapter instance for the given agent type.
20
- * @param {string} type - Agent type (openclaw, claude, codex)
26
+ * @param {string} type - Agent type (openclaw, claude, codex, opencode, nanoclaw, cursor)
21
27
  * @param {object} opts - Adapter constructor options
22
28
  * @returns {BaseAdapter}
23
29
  */
@@ -34,6 +40,9 @@ module.exports = {
34
40
  OpenClawAdapter,
35
41
  ClaudeAdapter,
36
42
  CodexAdapter,
43
+ OpenCodeAdapter,
44
+ NanoClawAdapter,
45
+ CursorAdapter,
37
46
  createAdapter,
38
47
  ADAPTER_MAP,
39
48
  };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Direct LLM API adapter — shared base for NanoClaw and Cursor.
3
+ *
4
+ * Calls OpenAI-compatible chat completions API directly with SSE streaming.
5
+ * No CLI binary needed — just OPENAI_API_KEY + OPENAI_BASE_URL.
6
+ *
7
+ * Port of Python: src/openagents/adapters/nanoclaw.py & cursor.py
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ const https = require('https');
13
+ const http = require('http');
14
+
15
+ const BaseAdapter = require('./base');
16
+ const { formatAttachmentsForPrompt } = require('./utils');
17
+ const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
18
+
19
+ const MAX_HISTORY = 50;
20
+
21
+ class LlmDirectAdapter extends BaseAdapter {
22
+ /**
23
+ * @param {object} opts - BaseAdapter opts plus:
24
+ * @param {Set} [opts.disabledModules]
25
+ * @param {string} opts.adapterLabel - e.g. "NanoClaw" or "Cursor"
26
+ * @param {string} opts.modelEnvVar - e.g. "NANOCLAW_MODEL" or "CURSOR_MODEL"
27
+ */
28
+ constructor(opts) {
29
+ super(opts);
30
+ this.disabledModules = opts.disabledModules || new Set();
31
+ this._adapterLabel = opts.adapterLabel || 'LLM';
32
+ this._modelEnvVar = opts.modelEnvVar || '';
33
+
34
+ const env = this.agentEnv || process.env;
35
+ this._apiKey = env.OPENAI_API_KEY || '';
36
+ this._baseUrl = (env.OPENAI_BASE_URL || '').replace(/\/$/, '');
37
+ this._model = env[this._modelEnvVar] || env.OPENCLAW_MODEL || '';
38
+ this._directMode = !!(this._apiKey && this._baseUrl);
39
+
40
+ if (this._directMode) {
41
+ this._log(`Direct LLM mode: ${this._baseUrl} model=${this._model || 'gpt-4o'}`);
42
+ } else {
43
+ this._log(
44
+ `${this._adapterLabel} adapter started without direct API config. ` +
45
+ 'Set OPENAI_API_KEY + OPENAI_BASE_URL for direct mode.'
46
+ );
47
+ }
48
+
49
+ this._conversationHistory = [];
50
+ }
51
+
52
+ _buildSystemPrompt(channelName) {
53
+ return buildOpenclawSystemPrompt({
54
+ agentName: this.agentName,
55
+ workspaceId: this.workspaceId,
56
+ channelName,
57
+ endpoint: this.endpoint,
58
+ token: this.token,
59
+ mode: this._mode,
60
+ disabledModules: this.disabledModules,
61
+ });
62
+ }
63
+
64
+ async _handleMessage(msg) {
65
+ let content = (msg.content || '').trim();
66
+ const attachments = msg.attachments || [];
67
+
68
+ const attText = formatAttachmentsForPrompt(attachments);
69
+ if (attText) content = content ? content + attText : attText.trim();
70
+ if (!content) return;
71
+
72
+ const msgChannel = msg.sessionId || this.channelName;
73
+ const sender = msg.senderName || msg.senderType || 'user';
74
+ this._log(`Processing message from ${sender} in ${msgChannel}: ${content.slice(0, 80)}...`);
75
+
76
+ await this._autoTitleChannel(msgChannel, content);
77
+ await this.sendStatus(msgChannel, 'thinking...');
78
+
79
+ try {
80
+ if (!this._directMode) {
81
+ await this.sendError(
82
+ msgChannel,
83
+ `${this._adapterLabel} direct API mode not configured. Set OPENAI_API_KEY + OPENAI_BASE_URL.`
84
+ );
85
+ return;
86
+ }
87
+
88
+ const responseText = await this._callCompletionApi(content, msgChannel);
89
+
90
+ if (responseText) {
91
+ this._conversationHistory.push({ role: 'user', content });
92
+ this._conversationHistory.push({ role: 'assistant', content: responseText });
93
+ if (this._conversationHistory.length > MAX_HISTORY * 2) {
94
+ this._conversationHistory = this._conversationHistory.slice(-MAX_HISTORY * 2);
95
+ }
96
+ await this.sendResponse(msgChannel, responseText);
97
+ } else {
98
+ await this.sendResponse(msgChannel, 'No response generated. Please try again.');
99
+ }
100
+ } catch (e) {
101
+ this._log(`Error handling message: ${e.message}`);
102
+ await this.sendError(msgChannel, `Error processing message: ${e.message}`);
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Call OpenAI-compatible chat completions API with SSE streaming.
108
+ */
109
+ _callCompletionApi(userMessage, channel) {
110
+ const systemPrompt = this._buildSystemPrompt(channel);
111
+
112
+ const messages = [{ role: 'system', content: systemPrompt }];
113
+ messages.push(...this._conversationHistory);
114
+ messages.push({ role: 'user', content: userMessage });
115
+
116
+ const url = `${this._baseUrl}/chat/completions`;
117
+ const headers = {
118
+ 'Content-Type': 'application/json',
119
+ 'Authorization': `Bearer ${this._apiKey}`,
120
+ };
121
+ const payload = JSON.stringify({
122
+ model: this._model || 'gpt-4o',
123
+ messages,
124
+ stream: true,
125
+ });
126
+
127
+ return new Promise((resolve, reject) => {
128
+ const parsedUrl = new URL(url);
129
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
130
+
131
+ const req = transport.request(url, {
132
+ method: 'POST',
133
+ headers: { ...headers, 'Content-Length': Buffer.byteLength(payload) },
134
+ timeout: 300000,
135
+ }, (res) => {
136
+ if (res.statusCode !== 200) {
137
+ let body = '';
138
+ res.on('data', (c) => { body += c; });
139
+ res.on('end', () => reject(new Error(`LLM API returned ${res.statusCode}: ${body.slice(0, 300)}`)));
140
+ return;
141
+ }
142
+
143
+ let fullText = '';
144
+ let lineBuf = '';
145
+
146
+ res.on('data', (chunk) => {
147
+ lineBuf += chunk.toString('utf-8');
148
+ const lines = lineBuf.split('\n');
149
+ lineBuf = lines.pop(); // keep incomplete line
150
+
151
+ for (const line of lines) {
152
+ const trimmed = line.trim();
153
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
154
+
155
+ const data = trimmed.slice(6);
156
+ if (data === '[DONE]') continue;
157
+
158
+ try {
159
+ const parsed = JSON.parse(data);
160
+ const choices = parsed.choices || [];
161
+ if (choices.length > 0) {
162
+ const delta = choices[0].delta || {};
163
+ if (delta.content) fullText += delta.content;
164
+ }
165
+ } catch {}
166
+ }
167
+ });
168
+
169
+ res.on('end', () => resolve(fullText.trim()));
170
+ });
171
+
172
+ req.on('error', reject);
173
+ req.on('timeout', () => { req.destroy(); reject(new Error('LLM API request timed out')); });
174
+ req.write(payload);
175
+ req.end();
176
+ });
177
+ }
178
+ }
179
+
180
+ module.exports = LlmDirectAdapter;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * NanoClaw adapter — lightweight containerized coding agent.
3
+ *
4
+ * Uses direct LLM API mode (OpenAI-compatible chat completions).
5
+ * Port of Python: src/openagents/adapters/nanoclaw.py
6
+ */
7
+
8
+ 'use strict';
9
+
10
+ const LlmDirectAdapter = require('./llm-direct');
11
+
12
+ class NanoClawAdapter extends LlmDirectAdapter {
13
+ constructor(opts) {
14
+ super({
15
+ ...opts,
16
+ adapterLabel: 'NanoClaw',
17
+ modelEnvVar: 'NANOCLAW_MODEL',
18
+ });
19
+ }
20
+ }
21
+
22
+ module.exports = NanoClawAdapter;
@@ -36,10 +36,6 @@ class OpenClawAdapter extends BaseAdapter {
36
36
  this.openclawAgentId = opts.openclawAgentId || 'main';
37
37
  this.disabledModules = opts.disabledModules || new Set();
38
38
 
39
- // Conversation history for multi-turn context
40
- this._conversationHistory = [];
41
- this._maxHistory = 50;
42
-
43
39
  // Find the openclaw binary
44
40
  this._openclawBinary = this._findOpenclawBinary();
45
41
  if (this._openclawBinary) {
@@ -157,11 +153,6 @@ class OpenClawAdapter extends BaseAdapter {
157
153
  const responseText = await this._runCliAgent(content, msgChannel);
158
154
 
159
155
  if (responseText) {
160
- this._conversationHistory.push({ role: 'user', content });
161
- this._conversationHistory.push({ role: 'assistant', content: responseText });
162
- if (this._conversationHistory.length > this._maxHistory * 2) {
163
- this._conversationHistory = this._conversationHistory.slice(-this._maxHistory * 2);
164
- }
165
156
  await this.sendResponse(msgChannel, responseText);
166
157
  } else {
167
158
  await this.sendResponse(msgChannel, 'No response generated. Please try again.');