@openagents-org/agent-launcher 0.2.128 → 0.2.130

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.128",
3
+ "version": "0.2.130",
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
@@ -209,49 +209,44 @@
209
209
  "order": 4,
210
210
  "builtin": true,
211
211
  "install": {
212
- "binary": "cursor",
213
- "verify": "cursor --version 2>/dev/null | grep -qi cursor",
214
- "verify_win": "cursor --version 2>nul | findstr /i cursor",
215
- "requires": [
216
- null
217
- ],
212
+ "binary": "agent",
213
+ "verify": "agent --version 2>/dev/null | head -1",
214
+ "verify_win": "agent --version 2>nul",
215
+ "requires": [],
218
216
  "macos": "curl https://cursor.com/install -fsSL | bash",
219
217
  "linux": "curl https://cursor.com/install -fsSL | bash",
220
- "windows": "echo 'Download Cursor from https://cursor.com/downloads'"
218
+ "windows": "irm 'https://cursor.com/install?win32=true' | iex",
219
+ "npm": "npm install -g @cursor/cli"
221
220
  },
222
221
  "adapter": {
223
222
  "module": "openagents.adapters.cursor",
224
223
  "class": "CursorAdapter"
225
224
  },
226
225
  "launch": {
227
- "args": [
228
- null
229
- ]
226
+ "args": []
230
227
  },
231
228
  "env_config": [
232
229
  {
233
- "name": "OPENAI_API_KEY",
234
- "description": "API key for LLM inference",
235
- "required": true,
230
+ "name": "CURSOR_API_KEY",
231
+ "description": "Cursor API key for CLI authentication",
232
+ "required": false,
236
233
  "password": true
234
+ },
235
+ {
236
+ "name": "CURSOR_MODEL",
237
+ "description": "Model to use (e.g. claude-sonnet-4-6, gpt-4o)",
238
+ "required": false
237
239
  }
238
240
  ],
239
241
  "check_ready": {
240
- "env_vars": [
241
- "OPENAI_API_KEY"
242
- ],
243
- "saved_env_key": "OPENAI_API_KEY",
244
- "not_ready_message": "No API key \u2014 press e to configure"
242
+ "binary": "agent",
243
+ "not_ready_message": "Cursor CLI not found \u2014 install with: curl https://cursor.com/install -fsSL | bash"
245
244
  },
246
245
  "resolve_env": {
247
246
  "rules": [
248
247
  {
249
248
  "from": "LLM_API_KEY",
250
- "to": "OPENAI_API_KEY"
251
- },
252
- {
253
- "from": "LLM_BASE_URL",
254
- "to": "OPENAI_BASE_URL"
249
+ "to": "CURSOR_API_KEY"
255
250
  },
256
251
  {
257
252
  "from": "LLM_MODEL",
@@ -42,6 +42,7 @@ class BaseAdapter {
42
42
  this.workingDir = workingDir || undefined;
43
43
  this.client = new WorkspaceClient(this.endpoint);
44
44
  this._lastEventId = null;
45
+ this._lastToolResultId = null;
45
46
  this._running = false;
46
47
  this._sessionId = null; // issued by server on /v1/join; used to prove liveness
47
48
  this._processedIds = new Set();
@@ -358,6 +359,31 @@ class BaseAdapter {
358
359
  idleCount++;
359
360
  }
360
361
 
362
+ // Sidecar poll: A2UI tool_result events. These are the user's response
363
+ // to a UI spec this agent (or any agent in the network) emitted. We
364
+ // surface each one as a synthetic user message so the LLM sees it as
365
+ // the next turn and can react. Failures here don't break the main
366
+ // message poll.
367
+ try {
368
+ const toolResult = await this.client.pollToolResults(
369
+ this.workspaceId, this.token,
370
+ { after: this._lastToolResultId }
371
+ );
372
+ if (toolResult.cursor) this._lastToolResultId = toolResult.cursor;
373
+ for (const event of toolResult.events || []) {
374
+ const msgId = event.id;
375
+ if (msgId && this._processedIds.has(msgId)) continue;
376
+ if (msgId) this._processedIds.add(msgId);
377
+ const synth = synthesizeToolResultMessage(event);
378
+ if (synth) await this._dispatchMessage(synth);
379
+ }
380
+ } catch (e) {
381
+ // Non-fatal — log once per poll if it fails
382
+ if (pollCount <= 3 || pollCount % 20 === 0) {
383
+ this._log(`tool_result poll #${pollCount} failed: ${e.message}`);
384
+ }
385
+ }
386
+
361
387
  // Adaptive polling: 2s active, up to 15s idle.
362
388
  // Each connected agent runs this loop, so faster rates multiply across
363
389
  // every workspace member — keep this conservative and tune separately
@@ -421,6 +447,9 @@ class BaseAdapter {
421
447
  const queue = this._channelQueues[channel];
422
448
  if (!queue || queue.length === 0) break;
423
449
  const nextMsg = queue.shift();
450
+ if (nextMsg._queueId) {
451
+ try { await this.sendStatus(channel, 'processing queued message', { queue_id: nextMsg._queueId, queue_status: 'processed' }); } catch {}
452
+ }
424
453
  try {
425
454
  await this._handleMessage(nextMsg);
426
455
  } catch (e) {
@@ -473,8 +502,15 @@ class BaseAdapter {
473
502
  }
474
503
 
475
504
  async sendThinking(channel, content) {
505
+ // Strip ```a2ui blocks if they leak into Claude's intermediate thinking
506
+ // trace — the real spec gets emitted via sendResponse with proper
507
+ // payload.spec extraction, so showing the raw block here is just noise
508
+ // (and a duplicate). If stripping leaves the thinking message empty,
509
+ // skip it entirely.
510
+ const { cleanContent } = extractA2UISpec(content);
511
+ if (!cleanContent || !cleanContent.trim()) return;
476
512
  try {
477
- await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
513
+ await this.client.sendMessage(this.workspaceId, channel, this.token, cleanContent, {
478
514
  senderType: 'agent',
479
515
  senderName: this.agentName,
480
516
  messageType: 'thinking',
@@ -487,11 +523,14 @@ class BaseAdapter {
487
523
  }
488
524
 
489
525
  async sendResponse(channel, content) {
526
+ const { cleanContent, spec, specToolCallId } = extractA2UISpec(content);
490
527
  try {
491
- await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
528
+ await this.client.sendMessage(this.workspaceId, channel, this.token, cleanContent, {
492
529
  senderType: 'agent',
493
530
  senderName: this.agentName,
494
531
  sessionId: this._sessionId,
532
+ spec,
533
+ specToolCallId,
495
534
  });
496
535
  } catch (e) {
497
536
  if (e instanceof SessionRevokedError) {
@@ -599,4 +638,74 @@ class BaseAdapter {
599
638
  }
600
639
  }
601
640
 
641
+ // ------------------------------------------------------------------
642
+ // A2UI helpers
643
+ // ------------------------------------------------------------------
644
+
645
+ /**
646
+ * Pull the first ```a2ui ... ``` fenced block out of LLM-produced content.
647
+ * Returns the content with the block stripped, the parsed spec, and a
648
+ * tool-call id derived from `spec.tool_call_id` (if present) or a new one.
649
+ * If no block is present or parsing fails, returns the content unchanged
650
+ * with null spec — the message still goes out as plain markdown.
651
+ */
652
+ /**
653
+ * Convert a workspace.tool_result event into a synthetic user-message
654
+ * shape that the agent's _handleMessage can dispatch. The LLM sees this
655
+ * as the next user turn — the content is a short, machine-readable line
656
+ * the LLM can parse without ambiguity. The original spec it emitted is
657
+ * already in the LLM's conversation history; the tool_call_id lets the
658
+ * LLM correlate this back.
659
+ */
660
+ function synthesizeToolResultMessage(event) {
661
+ if (!event || !event.payload) return null;
662
+ const p = event.payload;
663
+ const actionId = p.action_id || '';
664
+ const toolCallId = p.tool_call_id || '';
665
+ let valueStr = '';
666
+ if (p.value !== undefined && p.value !== null) {
667
+ try { valueStr = JSON.stringify(p.value); } catch (_) { valueStr = String(p.value); }
668
+ }
669
+ const lines = [
670
+ '[ui_action]',
671
+ `action=${actionId}`,
672
+ toolCallId ? `tool_call_id=${toolCallId}` : null,
673
+ valueStr ? `value=${valueStr}` : null,
674
+ ].filter(Boolean);
675
+ const content = lines.join(' ');
676
+ const target = event.target || '';
677
+ return {
678
+ messageId: event.id || '',
679
+ sessionId: target.startsWith('channel/') ? target.replace('channel/', '') : target,
680
+ senderType: 'human',
681
+ senderName: 'user',
682
+ content,
683
+ mentions: [],
684
+ messageType: 'chat',
685
+ metadata: event.metadata || {},
686
+ };
687
+ }
688
+
689
+ function extractA2UISpec(content) {
690
+ if (!content || typeof content !== 'string') {
691
+ return { cleanContent: content, spec: null, specToolCallId: null };
692
+ }
693
+ const match = content.match(/```a2ui\s*\n([\s\S]*?)\n```/);
694
+ if (!match) return { cleanContent: content, spec: null, specToolCallId: null };
695
+
696
+ let spec;
697
+ try {
698
+ spec = JSON.parse(match[1]);
699
+ } catch (_) {
700
+ return { cleanContent: content, spec: null, specToolCallId: null };
701
+ }
702
+
703
+ const specToolCallId = (spec && spec.tool_call_id) || `tc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
704
+ if (spec && spec.tool_call_id) delete spec.tool_call_id;
705
+
706
+ const cleanContent = content.replace(match[0], '').trim();
707
+ return { cleanContent, spec, specToolCallId };
708
+ }
709
+
602
710
  module.exports = BaseAdapter;
711
+ module.exports.extractA2UISpec = extractA2UISpec;
@@ -569,7 +569,7 @@ class ClaudeAdapter extends BaseAdapter {
569
569
  let content = (msg.content || '').trim();
570
570
  const attachments = msg.attachments || [];
571
571
 
572
- const attText = formatAttachmentsForPrompt(attachments);
572
+ const attText = formatAttachmentsForPrompt(attachments, this.toolMode);
573
573
  if (attText) {
574
574
  content = content ? content + attText : attText.trim();
575
575
  }