@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
|
@@ -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
|
@@ -160,17 +160,31 @@ class Daemon {
|
|
|
160
160
|
fs.mkdirSync(configDir, { recursive: true });
|
|
161
161
|
const logFd = fs.openSync(logFile, 'a');
|
|
162
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
|
+
|
|
163
171
|
const opts = {
|
|
164
172
|
detached: true,
|
|
165
173
|
stdio: ['ignore', logFd, logFd],
|
|
166
|
-
env
|
|
174
|
+
env,
|
|
175
|
+
cwd: configDir,
|
|
167
176
|
};
|
|
168
177
|
if (IS_WINDOWS) opts.windowsHide = true;
|
|
169
178
|
|
|
170
179
|
const proc = spawn(bin, foregroundArgs, opts);
|
|
171
180
|
proc.unref();
|
|
172
181
|
fs.writeFileSync(pidFile, String(proc.pid), 'utf-8');
|
|
173
|
-
|
|
182
|
+
|
|
183
|
+
// Give child a moment to start before closing the log fd
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
try { fs.closeSync(logFd); } catch {}
|
|
186
|
+
}, 1000);
|
|
187
|
+
|
|
174
188
|
console.log(`Daemon started (PID ${proc.pid})`);
|
|
175
189
|
console.log(`Logs: ${logFile}`);
|
|
176
190
|
console.log('Stop: agent-connector down');
|
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
|
|