@openagents-org/agent-connector 0.1.10 → 0.2.0

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/src/daemon.js CHANGED
@@ -339,300 +339,63 @@ class Daemon {
339
339
  // ---------------------------------------------------------------------------
340
340
 
341
341
  async _adapterLoop(name, agentCfg, info, network) {
342
- const wsClient = new WorkspaceClient(network.endpoint || 'https://workspace-endpoint.openagents.org');
343
- const binary = this._resolveAgentBinary(agentCfg);
344
- const env = this._buildAgentEnv(agentCfg);
345
- let cursor = null;
346
- const processedIds = new Set();
342
+ const { createAdapter } = require('./adapters');
343
+ const agentType = agentCfg.type || 'openclaw';
344
+ const endpoint = network.endpoint || 'https://workspace-endpoint.openagents.org';
347
345
 
348
- // Skip existing events on startup
346
+ let adapter;
349
347
  try {
350
- while (true) {
351
- const { cursor: newCursor } = await wsClient.pollPending(
352
- network.id, name, network.token, { after: cursor, limit: 200 }
353
- );
354
- if (!newCursor || newCursor === cursor) break;
355
- cursor = newCursor;
356
- }
357
- this._log(`${name} skipped existing events, cursor=${cursor}`);
348
+ adapter = createAdapter(agentType, {
349
+ workspaceId: network.id,
350
+ channelName: 'general',
351
+ token: network.token,
352
+ agentName: name,
353
+ endpoint,
354
+ openclawAgentId: agentCfg.openclaw_agent_id || 'main',
355
+ disabledModules: new Set(),
356
+ });
358
357
  } catch (e) {
359
- this._log(`${name} skip-events failed: ${e.message}`);
358
+ this._log(`${name} failed to create ${agentType} adapter: ${e.message}`);
359
+ info.state = 'error';
360
+ info.lastError = e.message;
361
+ this._writeStatus();
362
+ return;
360
363
  }
361
364
 
362
- // Heartbeat interval
363
- const heartbeatInterval = setInterval(async () => {
364
- if (this._stoppedAgents.has(name) || this._shuttingDown) return;
365
- try {
366
- await wsClient.heartbeat(network.id, name, network.token);
367
- } catch (e) {
368
- this._log(`${name} heartbeat failed: ${e.message}`);
369
- }
370
- }, 30000);
371
-
372
- // Send initial heartbeat
373
- try { await wsClient.heartbeat(network.id, name, network.token); } catch {}
365
+ // Store adapter reference for stop
366
+ this._adapters = this._adapters || {};
367
+ this._adapters[name] = adapter;
374
368
 
375
369
  info.state = 'running';
376
370
  info.startedAt = new Date().toISOString();
377
371
  this._writeStatus();
378
- this._log(`${name} adapter online → ${network.slug}${binary ? ` (binary: ${binary})` : ''}`);
372
+ this._log(`${name} adapter online → ${network.slug} (type: ${agentType})`);
379
373
 
380
- let idleCount = 0;
381
-
382
- while (!this._shuttingDown && !this._stoppedAgents.has(name)) {
383
- try {
384
- const { messages, cursor: newCursor } = await wsClient.pollPending(
385
- network.id, name, network.token, { after: cursor }
386
- );
387
- if (newCursor) cursor = newCursor;
388
-
389
- // Filter already-processed messages
390
- const incoming = messages.filter((m) => {
391
- const id = m.messageId;
392
- if (!id || processedIds.has(id)) return false;
393
- if (m.messageType === 'status') return false;
394
- return true;
395
- });
396
-
397
- if (incoming.length > 0) {
398
- idleCount = 0;
399
- for (const msg of incoming) {
400
- if (msg.messageId) processedIds.add(msg.messageId);
401
- await this._handleAdapterMessage(name, agentCfg, msg, network, wsClient, binary, env);
402
- }
403
- // Cap dedup set
404
- if (processedIds.size > 2000) {
405
- const arr = [...processedIds];
406
- processedIds.clear();
407
- for (const id of arr.slice(-1000)) processedIds.add(id);
408
- }
409
- } else {
410
- idleCount++;
374
+ try {
375
+ // Run adapter poll loop — stops when adapter.stop() is called
376
+ // or when the daemon shuts down
377
+ const checkStop = setInterval(() => {
378
+ if (this._shuttingDown || this._stoppedAgents.has(name)) {
379
+ adapter.stop();
380
+ clearInterval(checkStop);
411
381
  }
382
+ }, 1000);
412
383
 
413
- // Adaptive polling: 2s active, up to 15s idle
414
- const delay = incoming.length > 0 ? 2000 : Math.min(2000 + idleCount * 1000, 15000);
415
- await this._sleep(delay);
416
- } catch (e) {
417
- if (this._stoppedAgents.has(name) || this._shuttingDown) break;
418
- info.lastError = (e.message || String(e)).slice(0, 200);
419
- this._log(`${name} poll error: ${info.lastError}`);
420
- await this._sleep(5000);
421
- }
384
+ await adapter.run();
385
+ clearInterval(checkStop);
386
+ } catch (e) {
387
+ info.lastError = (e.message || String(e)).slice(0, 200);
388
+ this._log(`${name} adapter error: ${info.lastError}`);
422
389
  }
423
390
 
424
- clearInterval(heartbeatInterval);
425
-
426
- // Disconnect from workspace
427
- try { await wsClient.disconnect(network.id, name, network.token); } catch {}
428
-
391
+ delete this._adapters[name];
429
392
  info.state = 'stopped';
430
393
  this._writeStatus();
431
394
  this._log(`${name} adapter stopped`);
432
395
  }
433
396
 
434
- async _handleAdapterMessage(name, agentCfg, msg, network, wsClient, binary, env) {
435
- const content = (msg.content || '').trim();
436
- if (!content) return;
437
-
438
- const channel = msg.sessionId || 'general';
439
- const sender = msg.senderName || msg.senderType || 'user';
440
- this._log(`${name} message from ${sender} in ${channel}: ${content.slice(0, 80)}`);
441
-
442
- // Send "thinking..." status
443
- try {
444
- await wsClient.sendMessage(network.id, channel, network.token, 'thinking...', {
445
- senderType: 'agent', senderName: name, messageType: 'status',
446
- });
447
- } catch {}
448
-
449
- try {
450
- let response;
451
- if (binary) {
452
- response = await this._runCliAgent(binary, content, channel, agentCfg, network, env);
453
- } else {
454
- response = `Agent type '${agentCfg.type}' has no CLI binary — cannot process message.`;
455
- }
456
-
457
- if (response) {
458
- await wsClient.sendMessage(network.id, channel, network.token, response, {
459
- senderType: 'agent', senderName: name,
460
- });
461
- this._log(`${name} responded in ${channel}: ${response.slice(0, 80)}...`);
462
- }
463
- } catch (e) {
464
- const errMsg = `Error: ${(e.message || String(e)).slice(0, 200)}`;
465
- this._log(`${name} error handling message: ${errMsg}`);
466
- try {
467
- await wsClient.sendMessage(network.id, channel, network.token, errMsg, {
468
- senderType: 'agent', senderName: name,
469
- });
470
- } catch {}
471
- }
472
- }
473
-
474
- /**
475
- * Build workspace context preamble for CLI agents.
476
- * Teaches the agent about shared workspace APIs (browser, files).
477
- */
478
- /**
479
- * On Windows, resolve a .cmd shim to the underlying node script
480
- * so we can spawn directly with node (avoiding cmd.exe argument limits).
481
- */
482
- _resolveWindowsBinary(binary, env) {
483
- const { execSync } = require('child_process');
484
- try {
485
- // Find the .cmd shim
486
- const cmdPath = execSync(`where ${binary}`, {
487
- encoding: 'utf-8', timeout: 5000, env,
488
- }).split(/\r?\n/)[0].trim();
489
-
490
- if (cmdPath.endsWith('.cmd')) {
491
- // Read the .cmd file to find the target JS script
492
- const cmdContent = fs.readFileSync(cmdPath, 'utf-8');
493
- // npm .cmd shims have: "%~dp0\node_modules\...\bin\cli.js" %*
494
- // or: @IF EXIST "%~dp0\node.exe" ... "%~dp0\node_modules\...\cli.js" %*
495
- const match = cmdContent.match(/"([^"]+\.js)"/);
496
- if (match) {
497
- const jsPath = match[1].replace('%~dp0\\', path.dirname(cmdPath) + '\\');
498
- return { binary: process.execPath, prefix: [jsPath] };
499
- }
500
- }
501
- } catch {}
502
-
503
- // Fallback: use cmd.exe /C (may truncate long args)
504
- return { binary: process.env.COMSPEC || 'cmd.exe', prefix: ['/C', binary] };
505
- }
506
-
507
- _buildWorkspaceContext(agentCfg, network) {
508
- const baseUrl = 'https://workspace-endpoint.openagents.org';
509
- const h = `Authorization: Bearer ${network.token}`;
510
- const wsId = network.id;
511
- const name = agentCfg.name;
512
-
513
- return [
514
- '=== WORKSPACE CONTEXT ===',
515
- `You are agent "${name}" in workspace "${network.slug}".`,
516
- 'You have access to shared workspace tools via HTTP API. Use your exec tool to run curl commands.',
517
- '',
518
- '## Shared Browser',
519
- 'The workspace has a shared browser visible to all users and agents.',
520
- `Open tab: curl -s -X POST ${baseUrl}/v1/browser/tabs -H "${h}" -H "Content-Type: application/json" -d '{"url":"URL","network":"${wsId}","source":"openagents:${name}"}'`,
521
- `Read page: curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID/snapshot`,
522
- `Screenshot: curl -s -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID/screenshot`,
523
- `Navigate: curl -s -X POST ${baseUrl}/v1/browser/tabs/TAB_ID/navigate -H "${h}" -H "Content-Type: application/json" -d '{"url":"URL"}'`,
524
- `Click: curl -s -X POST ${baseUrl}/v1/browser/tabs/TAB_ID/click -H "${h}" -H "Content-Type: application/json" -d '{"selector":"CSS_SELECTOR"}'`,
525
- `Type: curl -s -X POST ${baseUrl}/v1/browser/tabs/TAB_ID/type -H "${h}" -H "Content-Type: application/json" -d '{"selector":"CSS_SELECTOR","text":"TEXT"}'`,
526
- `Close: curl -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/TAB_ID`,
527
- `List tabs: curl -s -H "${h}" ${baseUrl}/v1/browser/tabs?network=${wsId}`,
528
- '(Replace TAB_ID with the id from the open response)',
529
- '',
530
- '## Workspace Files',
531
- `List: curl -s -H "${h}" ${baseUrl}/v1/files?network=${wsId}`,
532
- `Read: curl -s -H "${h}" ${baseUrl}/v1/files/FILE_PATH?network=${wsId}`,
533
- `Write: curl -s -X PUT ${baseUrl}/v1/files/FILE_PATH -H "${h}" -H "Content-Type: application/json" -d '{"content":"...","network":"${wsId}"}'`,
534
- '=== END WORKSPACE CONTEXT ===',
535
- '',
536
- ].join('\n');
537
- }
538
-
539
- _runCliAgent(binary, message, channel, agentCfg, network, env) {
540
- return new Promise((resolve, reject) => {
541
- const sessionKey = `openagents-${network.id.slice(0, 8)}-${channel.slice(-8)}`;
542
- const agentId = agentCfg.openclaw_agent_id || 'main';
543
-
544
- // Prepend workspace context on first message in a session
545
- const contextKey = `${agentCfg.name}:${sessionKey}`;
546
- let fullMessage = message;
547
- if (!this._sessionContextSent) this._sessionContextSent = new Set();
548
- if (!this._sessionContextSent.has(contextKey)) {
549
- fullMessage = this._buildWorkspaceContext(agentCfg, network) + message;
550
- this._sessionContextSent.add(contextKey);
551
- }
552
-
553
- // Write message to temp file to avoid cmd.exe argument length limits
554
- const msgFile = path.join(this.config.configDir, `msg-${Date.now()}.tmp`);
555
- fs.writeFileSync(msgFile, fullMessage, 'utf-8');
556
-
557
- // Use a small node wrapper to read the message file and exec openclaw
558
- // This avoids all cmd.exe quoting/length issues
559
- const wrapperCode = [
560
- `const msg = require("fs").readFileSync(${JSON.stringify(msgFile)}, "utf-8");`,
561
- `const cp = require("child_process");`,
562
- `const path = require("path");`,
563
- `const args = ${JSON.stringify(['agent', '--local', '--agent', agentId, '--session-id', sessionKey, '--json'])};`,
564
- `args.push("--message", msg);`,
565
- // Resolve the .cmd shim to find the actual JS entry point
566
- `let bin = ${JSON.stringify(binary)};`,
567
- `try {`,
568
- ` const w = cp.execSync("where " + bin, { encoding: "utf-8", timeout: 5000 }).split(/\\r?\\n/)[0].trim();`,
569
- ` if (w.endsWith(".cmd")) {`,
570
- ` const c = require("fs").readFileSync(w, "utf-8");`,
571
- ` const m = c.match(/"([^"]+\\.js)"/);`,
572
- ` if (m) { args.unshift(m[1].replace("%~dp0\\\\", path.dirname(w) + "\\\\")); bin = process.execPath; }`,
573
- ` }`,
574
- `} catch {}`,
575
- `const r = cp.spawnSync(bin, args, { stdio: ["ignore", "pipe", "pipe"], env: process.env, timeout: 600000 });`,
576
- `try { require("fs").unlinkSync(${JSON.stringify(msgFile)}); } catch {}`,
577
- `if (r.stdout) process.stdout.write(r.stdout);`,
578
- `if (r.stderr) process.stderr.write(r.stderr);`,
579
- `process.exit(r.status || 0);`,
580
- ].join('\n');
581
-
582
- this._log(`${agentCfg.name} CLI: ${binary} agent --local --agent ${agentId} ... (via wrapper, msg ${fullMessage.length} chars)`);
583
-
584
- const spawnEnv = { ...env };
585
- if (IS_WINDOWS) {
586
- const npmBin = path.join(process.env.APPDATA || '', 'npm');
587
- if (npmBin && !(spawnEnv.PATH || '').includes(npmBin)) {
588
- spawnEnv.PATH = npmBin + ';' + (spawnEnv.PATH || process.env.PATH || '');
589
- }
590
- }
591
-
592
- const spawnBinary = process.execPath; // node
593
- const spawnArgs = ['-e', wrapperCode];
594
- const spawnOpts = {
595
- stdio: ['ignore', 'pipe', 'pipe'],
596
- env: spawnEnv,
597
- timeout: 620000,
598
- };
599
-
600
- const proc = spawn(spawnBinary, spawnArgs, spawnOpts);
601
- let stdout = '';
602
- let stderr = '';
603
-
604
- if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d; });
605
- if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
606
-
607
- proc.on('error', (err) => reject(err));
608
- proc.on('exit', (code) => {
609
- if (code !== 0) {
610
- reject(new Error(`CLI exited ${code}: ${stderr.slice(0, 300)}`));
611
- return;
612
- }
613
-
614
- stdout = stdout.trim();
615
- if (!stdout) { resolve(''); return; }
616
-
617
- // Parse JSON output — find first '{'
618
- const jsonStart = stdout.indexOf('{');
619
- if (jsonStart < 0) { resolve(stdout); return; }
620
-
621
- try {
622
- const data = JSON.parse(stdout.slice(jsonStart));
623
- const payloads = data.payloads || [];
624
- if (payloads.length > 0) {
625
- const texts = payloads.filter((p) => p.text).map((p) => p.text);
626
- resolve(texts.join('\n\n'));
627
- } else {
628
- resolve(stdout.slice(0, jsonStart).trim() || '');
629
- }
630
- } catch {
631
- resolve(stdout);
632
- }
633
- });
634
- });
635
- }
397
+ // NOTE: Adapter-specific message handling (openclaw, claude, codex)
398
+ // has been moved to src/adapters/. The daemon delegates via createAdapter().
636
399
 
637
400
  _resolveAgentBinary(agentCfg) {
638
401
  const entry = this.registry.getEntry(agentCfg.type);
package/src/index.js CHANGED
@@ -195,4 +195,6 @@ class AgentConnector {
195
195
  }
196
196
  }
197
197
 
198
- module.exports = { AgentConnector, Daemon, WorkspaceClient };
198
+ const adapters = require('./adapters');
199
+
200
+ module.exports = { AgentConnector, Daemon, WorkspaceClient, adapters };