@openagents-org/agent-launcher 0.2.28 → 0.2.30

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.28",
3
+ "version": "0.2.30",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -180,6 +180,7 @@ class OpenClawAdapter extends BaseAdapter {
180
180
  const sessionKey = `openagents-${this.workspaceId.slice(0, 8)}-${channel.slice(-8)}`;
181
181
 
182
182
  const args = [
183
+ '--log-level', 'trace',
183
184
  'agent', '--local',
184
185
  '--agent', this.openclawAgentId,
185
186
  '--session-id', sessionKey,
@@ -230,92 +231,147 @@ class OpenClawAdapter extends BaseAdapter {
230
231
  }
231
232
  };
232
233
 
233
- // Try node-pty for line-buffered output (real-time tool streaming)
234
- let pty;
235
- try { pty = require('node-pty'); } catch {}
236
-
237
- if (pty) {
238
- // On Windows, .cmd files can't be spawned directly by pty — use cmd.exe
239
- const ptyBin = IS_WINDOWS ? (process.env.COMSPEC || 'cmd.exe') : binary;
240
- const ptyArgs = IS_WINDOWS ? ['/C', binary, ...args] : args;
241
- this._log('Spawn: node-pty (line-buffered)');
242
- const proc = pty.spawn(ptyBin, ptyArgs, {
243
- name: 'xterm',
244
- cols: 200,
245
- rows: 50,
246
- cwd: process.cwd(),
247
- env: spawnEnv,
248
- });
249
-
250
- proc.onData((data) => {
251
- output += data;
252
- lineBuffer += data;
253
- const lines = lineBuffer.split('\n');
254
- lineBuffer = lines.pop() || '';
255
- for (const line of lines) processLine(line);
256
- });
257
-
258
- const timeout = setTimeout(() => {
259
- proc.kill();
260
- reject(new Error('CLI timed out after 600 seconds'));
261
- }, 600000);
262
-
263
- proc.onExit(({ exitCode }) => {
264
- clearTimeout(timeout);
265
- // Process remaining buffer
266
- if (lineBuffer) processLine(lineBuffer);
267
-
268
- if (exitCode !== 0) {
269
- reject(new Error(`CLI exited ${exitCode}: ${output.slice(-300)}`));
270
- return;
234
+ // Redirect stderr to temp file for real-time tool status polling.
235
+ // --log-level trace makes OpenClaw write diagnostic events to stderr
236
+ // even in non-TTY mode. We poll the temp file for new lines every 500ms.
237
+ const stderrFile = path.join(os.tmpdir(), `openclaw-stderr-${Date.now()}.log`);
238
+ const stderrFd = fs.openSync(stderrFile, 'w');
239
+ this._log('Spawn: stderr ' + stderrFile);
240
+
241
+ let spawnBin = binary;
242
+ let spawnArgs = args;
243
+ if (IS_WINDOWS) {
244
+ spawnBin = process.env.COMSPEC || 'cmd.exe';
245
+ spawnArgs = ['/C', binary, ...args.map(a => a.includes(' ') ? `"${a}"` : a)];
246
+ }
247
+ const proc = spawn(spawnBin, spawnArgs, {
248
+ stdio: ['ignore', 'pipe', stderrFd],
249
+ env: spawnEnv,
250
+ timeout: 600000,
251
+ windowsHide: true,
252
+ });
253
+ if (proc.stdout) proc.stdout.on('data', (d) => { output += d; });
254
+
255
+ // Poll stderr file every 500ms for tool events
256
+ let stderrOffset = 0;
257
+ const pollInterval = setInterval(() => {
258
+ try {
259
+ const stat = fs.statSync(stderrFile);
260
+ if (stat.size > stderrOffset) {
261
+ const fd = fs.openSync(stderrFile, 'r');
262
+ const buf = Buffer.alloc(stat.size - stderrOffset);
263
+ fs.readSync(fd, buf, 0, buf.length, stderrOffset);
264
+ fs.closeSync(fd);
265
+ stderrOffset = stat.size;
266
+ const chunk = buf.toString('utf-8');
267
+ const lines = chunk.split('\n');
268
+ for (const line of lines) processLine(line);
271
269
  }
272
- this._parseCliOutput(output, resolve);
273
- });
274
- } else {
275
- // Fallback: regular spawn (buffered on Windows)
276
- this._log('Spawn: fallback (no node-pty)');
277
- let spawnBin = binary;
278
- let spawnArgs = args;
279
- if (IS_WINDOWS) {
280
- spawnBin = process.env.COMSPEC || 'cmd.exe';
281
- spawnArgs = ['/C', binary, ...args.map(a => a.includes(' ') ? `"${a}"` : a)];
282
- }
283
- const proc = spawn(spawnBin, spawnArgs, {
284
- stdio: ['ignore', 'pipe', 'pipe'],
285
- env: spawnEnv,
286
- timeout: 600000,
287
- windowsHide: true,
288
- });
289
- if (proc.stdout) proc.stdout.on('data', (d) => { output += d; });
290
- if (proc.stderr) proc.stderr.on('data', (d) => { output += d; });
291
- proc.on('error', (err) => reject(err));
292
- proc.on('exit', (code) => {
293
- if (code !== 0) {
294
- reject(new Error(`CLI exited ${code}: ${output.slice(-300)}`));
295
- return;
270
+ } catch {}
271
+ }, 500);
272
+
273
+ const killTimeout = setTimeout(() => {
274
+ proc.kill();
275
+ reject(new Error('CLI timed out after 600 seconds'));
276
+ }, 600000);
277
+
278
+ proc.on('error', (err) => {
279
+ clearInterval(pollInterval);
280
+ clearTimeout(killTimeout);
281
+ fs.closeSync(stderrFd);
282
+ try { fs.unlinkSync(stderrFile); } catch {}
283
+ reject(err);
284
+ });
285
+ proc.on('exit', (code) => {
286
+ clearInterval(pollInterval);
287
+ clearTimeout(killTimeout);
288
+ fs.closeSync(stderrFd);
289
+ // Read full stderr content (contains JSON output + trace lines)
290
+ let stderrContent = '';
291
+ try {
292
+ stderrContent = fs.readFileSync(stderrFile, 'utf-8');
293
+ // Process any remaining lines for tool events
294
+ const remaining = stderrContent.slice(stderrOffset);
295
+ if (remaining) {
296
+ for (const line of remaining.split('\n')) processLine(line);
296
297
  }
297
- this._parseCliOutput(output, resolve);
298
- });
299
- }
298
+ } catch {}
299
+ try { fs.unlinkSync(stderrFile); } catch {}
300
+
301
+ // OpenClaw --json writes JSON to stderr, so combine stdout + stderr
302
+ const allOutput = output + '\n' + stderrContent;
303
+
304
+ if (code !== 0) {
305
+ reject(new Error(`CLI exited ${code}: ${allOutput.slice(-300)}`));
306
+ return;
307
+ }
308
+ this._parseCliOutput(allOutput, resolve);
309
+ });
300
310
  });
301
311
  }
302
312
 
303
313
  _parseCliOutput(output, resolve) {
304
314
  const text = output.trim();
305
315
  if (!text) { resolve(''); return; }
306
- const jsonStart = text.indexOf('{');
307
- if (jsonStart < 0) { resolve(text); return; }
308
- try {
309
- const data = JSON.parse(text.slice(jsonStart));
310
- const payloads = data.payloads || [];
311
- if (payloads.length > 0) {
312
- resolve(payloads.filter(p => p.text).map(p => p.text).join('\n\n'));
313
- } else {
314
- resolve('');
316
+
317
+ // OpenClaw --json outputs a JSON blob with {"payloads":[...]} structure.
318
+ // With --log-level trace, stderr also contains diagnostic lines.
319
+ // Find the JSON by looking for '{"payloads"' or the last complete JSON object.
320
+ let jsonStr = null;
321
+
322
+ // Strategy 1: find {"payloads" directly
323
+ const payloadsIdx = text.indexOf('{"payloads"');
324
+ if (payloadsIdx >= 0) {
325
+ // Find the matching closing brace by counting braces
326
+ let depth = 0;
327
+ for (let i = payloadsIdx; i < text.length; i++) {
328
+ if (text[i] === '{') depth++;
329
+ else if (text[i] === '}') { depth--; if (depth === 0) { jsonStr = text.slice(payloadsIdx, i + 1); break; } }
330
+ }
331
+ }
332
+
333
+ // Strategy 2: find last '{' that starts a valid JSON with "payloads"
334
+ if (!jsonStr) {
335
+ for (let i = text.length - 1; i >= 0; i--) {
336
+ if (text[i] === '{') {
337
+ const candidate = text.slice(i);
338
+ try {
339
+ const d = JSON.parse(candidate);
340
+ if (d.payloads) { jsonStr = candidate; break; }
341
+ } catch {}
342
+ }
343
+ }
344
+ }
345
+
346
+ // Strategy 3: try each line that starts with '{'
347
+ if (!jsonStr) {
348
+ for (const line of text.split('\n')) {
349
+ const trimmed = line.trim();
350
+ if (trimmed.startsWith('{')) {
351
+ try {
352
+ const d = JSON.parse(trimmed);
353
+ if (d.payloads) { jsonStr = trimmed; break; }
354
+ } catch {}
355
+ }
315
356
  }
316
- } catch {
317
- resolve(text);
318
357
  }
358
+
359
+ if (jsonStr) {
360
+ try {
361
+ const data = JSON.parse(jsonStr);
362
+ const payloads = data.payloads || [];
363
+ if (payloads.length > 0) {
364
+ resolve(payloads.filter(p => p.text).map(p => p.text).join('\n\n'));
365
+ return;
366
+ }
367
+ } catch {}
368
+ }
369
+
370
+ // Fallback: return non-diagnostic text
371
+ const cleanLines = text.split('\n').filter(l =>
372
+ !l.includes('[diagnostic]') && !l.includes('[agent/embedded]') && !l.includes('Registered plugin')
373
+ ).map(l => l.trim()).filter(Boolean);
374
+ resolve(cleanLines.join('\n') || '');
319
375
  }
320
376
  // ------------------------------------------------------------------
321
377
  // Static: configure OpenClaw's native auth from LLM env vars