@openagents-org/agent-launcher 0.1.16 → 0.2.1
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/base.js +14 -1
- 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 +5 -10
- package/src/adapters/opencode.js +380 -0
- package/src/adapters/workspace-prompt.js +37 -0
- package/src/daemon.js +32 -6
- package/src/identity.js +113 -0
- package/src/index.js +5 -0
- package/src/tui.js +163 -18
- package/src/workspace-client.js +311 -21
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode adapter for OpenAgents workspace.
|
|
3
|
+
*
|
|
4
|
+
* Bridges OpenCode (opencode-ai) to an OpenAgents workspace by running
|
|
5
|
+
* `opencode run --format json` as a subprocess. OpenCode handles its own
|
|
6
|
+
* model configuration, provider selection, and tool chain.
|
|
7
|
+
*
|
|
8
|
+
* Port of Python PR #316: src/openagents/adapters/opencode.py
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const os = require('os');
|
|
16
|
+
const { execSync, spawn } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const BaseAdapter = require('./base');
|
|
19
|
+
const { formatAttachmentsForPrompt } = require('./utils');
|
|
20
|
+
const { buildOpenCodeSkillMd, buildOpenCodeSystemPrompt } = require('./workspace-prompt');
|
|
21
|
+
|
|
22
|
+
const IS_WINDOWS = process.platform === 'win32';
|
|
23
|
+
|
|
24
|
+
class OpenCodeAdapter extends BaseAdapter {
|
|
25
|
+
/**
|
|
26
|
+
* @param {object} opts - BaseAdapter opts plus:
|
|
27
|
+
* @param {Set} [opts.disabledModules]
|
|
28
|
+
* @param {string} [opts.workingDir]
|
|
29
|
+
*/
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
super(opts);
|
|
32
|
+
this.disabledModules = opts.disabledModules || new Set();
|
|
33
|
+
this.workingDir = opts.workingDir || undefined;
|
|
34
|
+
|
|
35
|
+
// Agent home directory: ~/.openagents/agents/{agentName}/
|
|
36
|
+
this.agentHome = path.join(os.homedir(), '.openagents', 'agents', this.agentName);
|
|
37
|
+
fs.mkdirSync(this.agentHome, { recursive: true });
|
|
38
|
+
|
|
39
|
+
this._channelSessions = {};
|
|
40
|
+
this._sessionsFile = path.join(this.agentHome, 'sessions.json');
|
|
41
|
+
this._migrateSessionsFile();
|
|
42
|
+
this._loadSessions();
|
|
43
|
+
|
|
44
|
+
this._opencodeBinary = this._findOpencodeBinary();
|
|
45
|
+
if (this._opencodeBinary) {
|
|
46
|
+
this._log(`Using OpenCode subprocess mode: ${this._opencodeBinary}`);
|
|
47
|
+
} else {
|
|
48
|
+
this._log('OpenCode binary not found. Install with: npm install -g opencode-ai@latest');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Migrate sessions file from old location to agent home.
|
|
54
|
+
*/
|
|
55
|
+
_migrateSessionsFile() {
|
|
56
|
+
const oldPath = path.join(
|
|
57
|
+
os.homedir(), '.openagents', 'sessions',
|
|
58
|
+
`${this.workspaceId}_${this.agentName}_opencode.json`
|
|
59
|
+
);
|
|
60
|
+
try {
|
|
61
|
+
if (fs.existsSync(oldPath) && !fs.existsSync(this._sessionsFile)) {
|
|
62
|
+
fs.copyFileSync(oldPath, this._sessionsFile);
|
|
63
|
+
fs.unlinkSync(oldPath);
|
|
64
|
+
this._log(`Migrated sessions file from ${oldPath}`);
|
|
65
|
+
}
|
|
66
|
+
} catch {}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
_loadSessions() {
|
|
70
|
+
try {
|
|
71
|
+
if (fs.existsSync(this._sessionsFile)) {
|
|
72
|
+
const data = JSON.parse(fs.readFileSync(this._sessionsFile, 'utf-8'));
|
|
73
|
+
if (data && typeof data === 'object') {
|
|
74
|
+
Object.assign(this._channelSessions, data);
|
|
75
|
+
this._log(`Loaded ${Object.keys(data).length} session(s)`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
this._log('Could not load sessions file, starting fresh');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
_saveSessions() {
|
|
84
|
+
try {
|
|
85
|
+
fs.mkdirSync(path.dirname(this._sessionsFile), { recursive: true });
|
|
86
|
+
fs.writeFileSync(this._sessionsFile, JSON.stringify(this._channelSessions));
|
|
87
|
+
} catch {}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Write workspace skill to OpenCode's skill directory for auto-discovery.
|
|
92
|
+
*/
|
|
93
|
+
_ensureWorkspaceSkill(channelName) {
|
|
94
|
+
const skillDir = path.join(this.agentHome, '.opencode', 'skills');
|
|
95
|
+
const skillFile = path.join(skillDir, 'openagents-workspace.md');
|
|
96
|
+
try {
|
|
97
|
+
const content = buildOpenCodeSkillMd({
|
|
98
|
+
endpoint: this.endpoint,
|
|
99
|
+
workspaceId: this.workspaceId,
|
|
100
|
+
token: this.token,
|
|
101
|
+
agentName: this.agentName,
|
|
102
|
+
channelName,
|
|
103
|
+
disabledModules: this.disabledModules,
|
|
104
|
+
});
|
|
105
|
+
fs.mkdirSync(skillDir, { recursive: true });
|
|
106
|
+
fs.writeFileSync(skillFile, content, 'utf-8');
|
|
107
|
+
} catch {}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_buildSystemContext(channelName) {
|
|
111
|
+
return buildOpenCodeSystemPrompt({
|
|
112
|
+
agentName: this.agentName,
|
|
113
|
+
workspaceId: this.workspaceId,
|
|
114
|
+
channelName,
|
|
115
|
+
endpoint: this.endpoint,
|
|
116
|
+
token: this.token,
|
|
117
|
+
mode: this._mode,
|
|
118
|
+
disabledModules: this.disabledModules,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ------------------------------------------------------------------
|
|
123
|
+
// Binary discovery
|
|
124
|
+
// ------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
_findOpencodeBinary() {
|
|
127
|
+
// Tier 1: PATH
|
|
128
|
+
try {
|
|
129
|
+
if (IS_WINDOWS) {
|
|
130
|
+
const r = execSync('where opencode.cmd 2>nul || where opencode.exe 2>nul || where opencode 2>nul', {
|
|
131
|
+
encoding: 'utf-8', timeout: 5000,
|
|
132
|
+
});
|
|
133
|
+
return r.split(/\r?\n/)[0].trim();
|
|
134
|
+
} else {
|
|
135
|
+
return execSync('which opencode', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
136
|
+
}
|
|
137
|
+
} catch {}
|
|
138
|
+
|
|
139
|
+
// Tier 2: Next to Node.js
|
|
140
|
+
const ext = IS_WINDOWS ? '.cmd' : '';
|
|
141
|
+
const nearNode = path.join(path.dirname(process.execPath), `opencode${ext}`);
|
|
142
|
+
if (fs.existsSync(nearNode)) return nearNode;
|
|
143
|
+
|
|
144
|
+
// Tier 3: Common locations
|
|
145
|
+
const home = os.homedir();
|
|
146
|
+
const candidates = IS_WINDOWS ? [
|
|
147
|
+
path.join(process.env.APPDATA || '', 'npm', 'opencode.cmd'),
|
|
148
|
+
] : [
|
|
149
|
+
path.join(home, '.openagents', 'npm-global', 'bin', 'opencode'),
|
|
150
|
+
path.join(home, '.npm-global', 'bin', 'opencode'),
|
|
151
|
+
path.join(home, '.local', 'bin', 'opencode'),
|
|
152
|
+
'/usr/local/bin/opencode',
|
|
153
|
+
];
|
|
154
|
+
for (const c of candidates) {
|
|
155
|
+
if (fs.existsSync(c)) return c;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ------------------------------------------------------------------
|
|
161
|
+
// Message handler
|
|
162
|
+
// ------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
async _handleMessage(msg) {
|
|
165
|
+
let content = (msg.content || '').trim();
|
|
166
|
+
const attachments = msg.attachments || [];
|
|
167
|
+
|
|
168
|
+
const attText = formatAttachmentsForPrompt(attachments);
|
|
169
|
+
if (attText) {
|
|
170
|
+
content = content ? content + attText : attText.trim();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!content) return;
|
|
174
|
+
|
|
175
|
+
const msgChannel = msg.sessionId || this.channelName;
|
|
176
|
+
const sender = msg.senderName || msg.senderType || 'user';
|
|
177
|
+
this._log(`Processing message from ${sender} in ${msgChannel}: ${content.slice(0, 80)}...`);
|
|
178
|
+
|
|
179
|
+
await this._autoTitleChannel(msgChannel, content);
|
|
180
|
+
await this.sendStatus(msgChannel, 'thinking...');
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
const responseText = await this._runOpencode(content, msgChannel);
|
|
184
|
+
|
|
185
|
+
if (responseText) {
|
|
186
|
+
await this.sendResponse(msgChannel, responseText);
|
|
187
|
+
} else {
|
|
188
|
+
await this.sendResponse(msgChannel, 'No response generated. Please try again.');
|
|
189
|
+
}
|
|
190
|
+
} catch (e) {
|
|
191
|
+
this._log(`Error handling message: ${e.message}`);
|
|
192
|
+
await this.sendError(msgChannel, `Error processing message: ${e.message}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ------------------------------------------------------------------
|
|
197
|
+
// JSON output parsing
|
|
198
|
+
// ------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Split a string containing concatenated JSON objects.
|
|
202
|
+
*/
|
|
203
|
+
static _splitJsonObjects(raw) {
|
|
204
|
+
const objects = [];
|
|
205
|
+
raw = raw.trim();
|
|
206
|
+
let pos = 0;
|
|
207
|
+
while (pos < raw.length) {
|
|
208
|
+
if (' \t\r\n'.includes(raw[pos])) { pos++; continue; }
|
|
209
|
+
if (raw[pos] !== '{') { pos++; continue; }
|
|
210
|
+
// Find matching brace
|
|
211
|
+
let depth = 0;
|
|
212
|
+
let inStr = false;
|
|
213
|
+
let escape = false;
|
|
214
|
+
let start = pos;
|
|
215
|
+
for (let i = pos; i < raw.length; i++) {
|
|
216
|
+
const ch = raw[i];
|
|
217
|
+
if (escape) { escape = false; continue; }
|
|
218
|
+
if (ch === '\\' && inStr) { escape = true; continue; }
|
|
219
|
+
if (ch === '"') { inStr = !inStr; continue; }
|
|
220
|
+
if (inStr) continue;
|
|
221
|
+
if (ch === '{') depth++;
|
|
222
|
+
else if (ch === '}') {
|
|
223
|
+
depth--;
|
|
224
|
+
if (depth === 0) {
|
|
225
|
+
try {
|
|
226
|
+
const obj = JSON.parse(raw.slice(start, i + 1));
|
|
227
|
+
if (typeof obj === 'object' && obj !== null) objects.push(obj);
|
|
228
|
+
} catch {}
|
|
229
|
+
pos = i + 1;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (i === raw.length - 1) pos = raw.length; // no match, skip
|
|
234
|
+
}
|
|
235
|
+
if (depth !== 0) break; // unbalanced, stop
|
|
236
|
+
}
|
|
237
|
+
return objects;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Extract user-visible text from a single opencode JSON event.
|
|
242
|
+
*/
|
|
243
|
+
static _extractTextFromEvent(event) {
|
|
244
|
+
const eventType = event.type || '';
|
|
245
|
+
if (['step_start', 'step_finish', 'tool_use'].includes(eventType)) return null;
|
|
246
|
+
|
|
247
|
+
const part = event.part;
|
|
248
|
+
if (part && typeof part === 'object') {
|
|
249
|
+
const text = part.text || part.content || '';
|
|
250
|
+
if (text) return text;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const item = event.item || event;
|
|
254
|
+
const text = item.text || item.content || '';
|
|
255
|
+
return text || null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Extract human-readable text from opencode --format json output.
|
|
260
|
+
*/
|
|
261
|
+
static _extractTextFromJson(raw) {
|
|
262
|
+
const events = OpenCodeAdapter._splitJsonObjects(raw);
|
|
263
|
+
if (!events.length) return raw.trim();
|
|
264
|
+
|
|
265
|
+
const texts = [];
|
|
266
|
+
for (const event of events) {
|
|
267
|
+
const text = OpenCodeAdapter._extractTextFromEvent(event);
|
|
268
|
+
if (text) texts.push(text);
|
|
269
|
+
}
|
|
270
|
+
return texts.length ? texts.join('\n').trim() : raw.trim();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Extract and persist session_id from OpenCode JSON events.
|
|
275
|
+
*/
|
|
276
|
+
_persistSessionId(channel, rawOutput) {
|
|
277
|
+
const events = OpenCodeAdapter._splitJsonObjects(rawOutput);
|
|
278
|
+
let sessionId = null;
|
|
279
|
+
for (const event of events) {
|
|
280
|
+
let sid = event.sessionID;
|
|
281
|
+
if (!sid && event.session && typeof event.session === 'object') {
|
|
282
|
+
sid = event.session.id;
|
|
283
|
+
}
|
|
284
|
+
if (!sid && event.part && typeof event.part === 'object') {
|
|
285
|
+
sid = event.part.sessionID;
|
|
286
|
+
}
|
|
287
|
+
if (sid && typeof sid === 'string') sessionId = sid;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (sessionId) {
|
|
291
|
+
const prev = this._channelSessions[channel];
|
|
292
|
+
this._channelSessions[channel] = sessionId;
|
|
293
|
+
this._saveSessions();
|
|
294
|
+
if (prev !== sessionId) {
|
|
295
|
+
this._log(`OpenCode session for channel ${channel}: ${sessionId}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ------------------------------------------------------------------
|
|
301
|
+
// Subprocess execution
|
|
302
|
+
// ------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
_runOpencode(content, msgChannel) {
|
|
305
|
+
const binary = this._opencodeBinary || this._findOpencodeBinary();
|
|
306
|
+
if (binary) this._opencodeBinary = binary;
|
|
307
|
+
if (!binary) {
|
|
308
|
+
return Promise.reject(new Error(
|
|
309
|
+
'opencode CLI not found. Install with: npm install -g opencode-ai@latest'
|
|
310
|
+
));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const cmd = [binary, 'run', '--format', 'json', '--dir', this.agentHome];
|
|
314
|
+
|
|
315
|
+
const sessionId = this._channelSessions[msgChannel];
|
|
316
|
+
let fullPrompt;
|
|
317
|
+
if (sessionId) {
|
|
318
|
+
fullPrompt = content;
|
|
319
|
+
cmd.push('--session', sessionId);
|
|
320
|
+
} else {
|
|
321
|
+
this._ensureWorkspaceSkill(msgChannel);
|
|
322
|
+
const context = this._buildSystemContext(msgChannel);
|
|
323
|
+
fullPrompt = `${context}\n\n---\n\n${content}`;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this._log(`CLI: ${binary} ${cmd.slice(1, 5).join(' ')} ...`);
|
|
327
|
+
|
|
328
|
+
const spawnEnv = { ...(this.agentEnv || process.env) };
|
|
329
|
+
|
|
330
|
+
let spawnBinary = cmd[0];
|
|
331
|
+
let spawnArgs = cmd.slice(1);
|
|
332
|
+
if (IS_WINDOWS && spawnBinary.toLowerCase().endsWith('.cmd')) {
|
|
333
|
+
spawnArgs = ['/C', spawnBinary, ...spawnArgs];
|
|
334
|
+
spawnBinary = process.env.COMSPEC || 'cmd.exe';
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return new Promise((resolve, reject) => {
|
|
338
|
+
const proc = spawn(spawnBinary, spawnArgs, {
|
|
339
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
340
|
+
env: spawnEnv,
|
|
341
|
+
cwd: this.agentHome,
|
|
342
|
+
timeout: 300000, // 5 minutes
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
let stdout = '';
|
|
346
|
+
let stderr = '';
|
|
347
|
+
|
|
348
|
+
if (proc.stdout) proc.stdout.on('data', (d) => { stdout += d; });
|
|
349
|
+
if (proc.stderr) proc.stderr.on('data', (d) => { stderr += d; });
|
|
350
|
+
|
|
351
|
+
// Send the prompt via stdin
|
|
352
|
+
if (proc.stdin) {
|
|
353
|
+
proc.stdin.write(fullPrompt, 'utf-8');
|
|
354
|
+
proc.stdin.end();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
proc.on('error', (err) => reject(err));
|
|
358
|
+
proc.on('exit', (code) => {
|
|
359
|
+
stdout = stdout.trim();
|
|
360
|
+
stderr = stderr.trim();
|
|
361
|
+
|
|
362
|
+
if (code !== 0) {
|
|
363
|
+
this._log(`opencode exited with code ${code}: ${stderr.slice(0, 300)}`);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (stdout) {
|
|
367
|
+
this._persistSessionId(msgChannel, stdout);
|
|
368
|
+
resolve(OpenCodeAdapter._extractTextFromJson(stdout));
|
|
369
|
+
} else {
|
|
370
|
+
if (stderr) {
|
|
371
|
+
this._log(`opencode stderr: ${stderr.slice(0, 300)}`);
|
|
372
|
+
}
|
|
373
|
+
resolve('');
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
module.exports = OpenCodeAdapter;
|
|
@@ -282,6 +282,41 @@ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channel
|
|
|
282
282
|
return frontmatter + identity + '\n' + collab + '\n' + body;
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Build system prompt for OpenCode adapter.
|
|
287
|
+
*/
|
|
288
|
+
function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules }) {
|
|
289
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode);
|
|
290
|
+
const collab = buildCollaborationPrompt();
|
|
291
|
+
const modePrompt = buildModePrompt(mode);
|
|
292
|
+
const api = buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channelName, disabledModules, mode });
|
|
293
|
+
return identity + '\n' + collab + '\n' + modePrompt + '\n' + api;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Build workspace skill markdown for OpenCode (written to .opencode/skills/).
|
|
298
|
+
*/
|
|
299
|
+
function buildOpenCodeSkillMd({ endpoint, workspaceId, token, agentName, channelName, disabledModules }) {
|
|
300
|
+
const api = buildApiSkillsPrompt({
|
|
301
|
+
endpoint, workspaceId, token, agentName,
|
|
302
|
+
channelName: channelName || 'general',
|
|
303
|
+
disabledModules,
|
|
304
|
+
mode: 'execute',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
const frontmatter =
|
|
308
|
+
'---\n' +
|
|
309
|
+
'name: openagents-workspace\n' +
|
|
310
|
+
'description: OpenAgents Workspace API — shared files, browser, and agent collaboration\n' +
|
|
311
|
+
'---\n\n';
|
|
312
|
+
|
|
313
|
+
const identity =
|
|
314
|
+
`You are agent '${agentName}' connected to OpenAgents workspace ${workspaceId}.\n` +
|
|
315
|
+
'Use these APIs via bash + curl to interact with the workspace.\n\n';
|
|
316
|
+
|
|
317
|
+
return frontmatter + identity + api;
|
|
318
|
+
}
|
|
319
|
+
|
|
285
320
|
module.exports = {
|
|
286
321
|
buildWorkspaceIdentity,
|
|
287
322
|
buildCollaborationPrompt,
|
|
@@ -290,4 +325,6 @@ module.exports = {
|
|
|
290
325
|
buildClaudeSystemPrompt,
|
|
291
326
|
buildOpenclawSystemPrompt,
|
|
292
327
|
buildOpenclawSkillMd,
|
|
328
|
+
buildOpenCodeSystemPrompt,
|
|
329
|
+
buildOpenCodeSkillMd,
|
|
293
330
|
};
|
package/src/daemon.js
CHANGED
|
@@ -67,6 +67,8 @@ class Daemon {
|
|
|
67
67
|
|
|
68
68
|
this._writeStatus();
|
|
69
69
|
this._cachedAgentNames = new Set(agents.map(a => a.name));
|
|
70
|
+
this._cachedAgentConfigs = {};
|
|
71
|
+
for (const a of agents) this._cachedAgentConfigs[a.name] = a.network || '';
|
|
70
72
|
this._log(`Daemon started with ${agents.length} agent(s)`);
|
|
71
73
|
|
|
72
74
|
// Block until shutdown
|
|
@@ -158,17 +160,31 @@ class Daemon {
|
|
|
158
160
|
fs.mkdirSync(configDir, { recursive: true });
|
|
159
161
|
const logFd = fs.openSync(logFile, 'a');
|
|
160
162
|
|
|
163
|
+
// Build env with enhanced PATH (ensures node/npm are findable)
|
|
164
|
+
const env = getEnhancedEnv();
|
|
165
|
+
// Ensure the directory containing the node binary is on PATH
|
|
166
|
+
const nodeBinDir = path.dirname(bin);
|
|
167
|
+
if (env.PATH && !env.PATH.includes(nodeBinDir)) {
|
|
168
|
+
env.PATH = nodeBinDir + path.delimiter + env.PATH;
|
|
169
|
+
}
|
|
170
|
+
|
|
161
171
|
const opts = {
|
|
162
172
|
detached: true,
|
|
163
173
|
stdio: ['ignore', logFd, logFd],
|
|
164
|
-
env
|
|
174
|
+
env,
|
|
175
|
+
cwd: configDir,
|
|
165
176
|
};
|
|
166
177
|
if (IS_WINDOWS) opts.windowsHide = true;
|
|
167
178
|
|
|
168
179
|
const proc = spawn(bin, foregroundArgs, opts);
|
|
169
180
|
proc.unref();
|
|
170
181
|
fs.writeFileSync(pidFile, String(proc.pid), 'utf-8');
|
|
171
|
-
|
|
182
|
+
|
|
183
|
+
// Give child a moment to start before closing the log fd
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
try { fs.closeSync(logFd); } catch {}
|
|
186
|
+
}, 1000);
|
|
187
|
+
|
|
172
188
|
console.log(`Daemon started (PID ${proc.pid})`);
|
|
173
189
|
console.log(`Logs: ${logFile}`);
|
|
174
190
|
console.log('Stop: agent-connector down');
|
|
@@ -260,10 +276,10 @@ class Daemon {
|
|
|
260
276
|
if (network) {
|
|
261
277
|
this._adapterLoop(name, agentCfg, info, network);
|
|
262
278
|
} else {
|
|
263
|
-
// No workspace connected —
|
|
264
|
-
info.state = '
|
|
279
|
+
// No workspace connected — agent is ready but not connected
|
|
280
|
+
info.state = 'idle';
|
|
265
281
|
this._writeStatus();
|
|
266
|
-
this._log(`${name}
|
|
282
|
+
this._log(`${name} ready (no workspace connected)`);
|
|
267
283
|
}
|
|
268
284
|
}
|
|
269
285
|
|
|
@@ -358,6 +374,7 @@ class Daemon {
|
|
|
358
374
|
token: network.token,
|
|
359
375
|
agentName: name,
|
|
360
376
|
endpoint,
|
|
377
|
+
agentType,
|
|
361
378
|
openclawAgentId: agentCfg.openclaw_agent_id || 'main',
|
|
362
379
|
disabledModules: new Set(),
|
|
363
380
|
agentEnv: this._buildAgentEnv(agentCfg),
|
|
@@ -577,9 +594,12 @@ class Daemon {
|
|
|
577
594
|
_reload() {
|
|
578
595
|
this._log('Reloading config...');
|
|
579
596
|
const oldNames = this._cachedAgentNames || new Set();
|
|
597
|
+
const oldConfigs = this._cachedAgentConfigs || {};
|
|
580
598
|
// Re-read config from disk
|
|
581
599
|
const newAgents = this.config.getAgents();
|
|
582
600
|
const newNames = new Set(newAgents.map(a => a.name));
|
|
601
|
+
const newConfigs = {};
|
|
602
|
+
for (const a of newAgents) newConfigs[a.name] = a.network || '';
|
|
583
603
|
|
|
584
604
|
// Stop removed agents
|
|
585
605
|
for (const name of oldNames) {
|
|
@@ -589,15 +609,21 @@ class Daemon {
|
|
|
589
609
|
}
|
|
590
610
|
}
|
|
591
611
|
|
|
592
|
-
// Start new agents
|
|
612
|
+
// Start new agents or restart agents whose network changed
|
|
593
613
|
for (const agent of newAgents) {
|
|
594
614
|
if (!oldNames.has(agent.name)) {
|
|
595
615
|
this._launchAgent(agent);
|
|
596
616
|
this._log(`Reload: started new agent '${agent.name}'`);
|
|
617
|
+
} else if ((oldConfigs[agent.name] || '') !== (agent.network || '')) {
|
|
618
|
+
// Network config changed — restart agent
|
|
619
|
+
this.stopAgent(agent.name);
|
|
620
|
+
this._launchAgent(agent);
|
|
621
|
+
this._log(`Reload: restarted '${agent.name}' (network changed)`);
|
|
597
622
|
}
|
|
598
623
|
}
|
|
599
624
|
|
|
600
625
|
this._cachedAgentNames = newNames;
|
|
626
|
+
this._cachedAgentConfigs = newConfigs;
|
|
601
627
|
this._writeStatus();
|
|
602
628
|
}
|
|
603
629
|
|
package/src/identity.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Local identity management (~/.openagents/identity.json).
|
|
5
|
+
*
|
|
6
|
+
* Port of Python: src/openagents/client/workspace_client.py (identity section)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
const IDENTITY_DIR = path.join(os.homedir(), '.openagents');
|
|
15
|
+
const IDENTITY_FILE = path.join(IDENTITY_DIR, 'identity.json');
|
|
16
|
+
|
|
17
|
+
function _loadIdentities() {
|
|
18
|
+
try {
|
|
19
|
+
if (fs.existsSync(IDENTITY_FILE)) {
|
|
20
|
+
return JSON.parse(fs.readFileSync(IDENTITY_FILE, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return { agents: {}, user_email: null };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function _saveIdentities(data) {
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(IDENTITY_DIR, { recursive: true });
|
|
29
|
+
fs.writeFileSync(IDENTITY_FILE, JSON.stringify(data, null, 2));
|
|
30
|
+
// Restrict permissions (best-effort)
|
|
31
|
+
try { fs.chmodSync(IDENTITY_FILE, 0o600); } catch {}
|
|
32
|
+
} catch {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get the saved identity for an agent type.
|
|
37
|
+
* @returns {{ agentName, agentType, apiKey, createdAt } | null}
|
|
38
|
+
*/
|
|
39
|
+
function getIdentity(agentType) {
|
|
40
|
+
const data = _loadIdentities();
|
|
41
|
+
const entry = (data.agents || {})[agentType];
|
|
42
|
+
if (entry) {
|
|
43
|
+
return {
|
|
44
|
+
agentName: entry.agent_name,
|
|
45
|
+
agentType,
|
|
46
|
+
apiKey: entry.api_key || null,
|
|
47
|
+
createdAt: entry.created_at || null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save an agent identity to local storage.
|
|
55
|
+
*/
|
|
56
|
+
function saveIdentity({ agentName, agentType, apiKey, createdAt }) {
|
|
57
|
+
const data = _loadIdentities();
|
|
58
|
+
if (!data.agents) data.agents = {};
|
|
59
|
+
data.agents[agentType] = {
|
|
60
|
+
agent_name: agentName,
|
|
61
|
+
api_key: apiKey || null,
|
|
62
|
+
created_at: createdAt || new Date().toISOString(),
|
|
63
|
+
};
|
|
64
|
+
_saveIdentities(data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get the logged-in user email.
|
|
69
|
+
*/
|
|
70
|
+
function getUserEmail() {
|
|
71
|
+
return _loadIdentities().user_email || null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Set the logged-in user email.
|
|
76
|
+
*/
|
|
77
|
+
function setUserEmail(email) {
|
|
78
|
+
const data = _loadIdentities();
|
|
79
|
+
data.user_email = email;
|
|
80
|
+
_saveIdentities(data);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Clear the logged-in user email (logout).
|
|
85
|
+
*/
|
|
86
|
+
function clearUserEmail() {
|
|
87
|
+
const data = _loadIdentities();
|
|
88
|
+
data.user_email = null;
|
|
89
|
+
_saveIdentities(data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generate an auto-name: {type}-{context}-{4hex} or {type}-{4hex}.
|
|
94
|
+
*/
|
|
95
|
+
function generateAgentName(agentType, context) {
|
|
96
|
+
const suffix = crypto.randomBytes(2).toString('hex');
|
|
97
|
+
if (context) {
|
|
98
|
+
const ctx = context.toLowerCase().replace(/\s+/g, '-').slice(0, 20);
|
|
99
|
+
return `${agentType}-${ctx}-${suffix}`;
|
|
100
|
+
}
|
|
101
|
+
return `${agentType}-${suffix}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
getIdentity,
|
|
106
|
+
saveIdentity,
|
|
107
|
+
getUserEmail,
|
|
108
|
+
setUserEmail,
|
|
109
|
+
clearUserEmail,
|
|
110
|
+
generateAgentName,
|
|
111
|
+
IDENTITY_DIR,
|
|
112
|
+
IDENTITY_FILE,
|
|
113
|
+
};
|
package/src/index.js
CHANGED
|
@@ -139,6 +139,11 @@ class AgentConnector {
|
|
|
139
139
|
* Start daemon in background (daemonize).
|
|
140
140
|
*/
|
|
141
141
|
startDaemon(foregroundArgs) {
|
|
142
|
+
if (!foregroundArgs) {
|
|
143
|
+
// Auto-detect the CLI entry point for foreground mode
|
|
144
|
+
const binPath = require.resolve('../bin/agent-connector.js');
|
|
145
|
+
foregroundArgs = [binPath, 'up', '--foreground'];
|
|
146
|
+
}
|
|
142
147
|
Daemon.daemonize(this._configDir, foregroundArgs);
|
|
143
148
|
}
|
|
144
149
|
|