@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/package.json +1 -1
- package/src/adapters/base.js +327 -0
- package/src/adapters/claude.js +420 -0
- package/src/adapters/codex.js +260 -0
- package/src/adapters/index.js +39 -0
- package/src/adapters/openclaw.js +259 -0
- package/src/adapters/utils.js +83 -0
- package/src/adapters/workspace-prompt.js +293 -0
- package/src/daemon.js +38 -275
- package/src/index.js +3 -1
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
|
|
343
|
-
const
|
|
344
|
-
const
|
|
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
|
-
|
|
346
|
+
let adapter;
|
|
349
347
|
try {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
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}
|
|
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
|
-
//
|
|
363
|
-
|
|
364
|
-
|
|
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}
|
|
372
|
+
this._log(`${name} adapter online → ${network.slug} (type: ${agentType})`);
|
|
379
373
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
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
|
-
|
|
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
|
-
|
|
435
|
-
|
|
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);
|