@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 +1 -1
- package/registry.json +50 -0
- package/src/adapters/claude.js +98 -9
- package/src/adapters/cursor.js +22 -0
- package/src/adapters/index.js +10 -1
- package/src/adapters/llm-direct.js +180 -0
- package/src/adapters/nanoclaw.js +22 -0
- package/src/adapters/openclaw.js +0 -9
- package/src/adapters/opencode.js +380 -0
- package/src/adapters/workspace-prompt.js +37 -0
- package/src/daemon.js +16 -2
- package/src/identity.js +113 -0
- package/src/index.js +5 -0
- package/src/tui.js +147 -8
- package/src/workspace-client.js +311 -21
package/package.json
CHANGED
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
|
{
|
package/src/adapters/claude.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
314
|
-
|
|
315
|
-
|
|
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) =>
|
|
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;
|
package/src/adapters/index.js
CHANGED
|
@@ -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;
|
package/src/adapters/openclaw.js
CHANGED
|
@@ -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.');
|