@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.
@@ -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: getEnhancedEnv(),
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
- fs.closeSync(logFd);
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 — nothing to do, mark as idle
264
- info.state = 'stopped';
279
+ // No workspace connected — agent is ready but not connected
280
+ info.state = 'idle';
265
281
  this._writeStatus();
266
- this._log(`${name} idle (no workspace connected)`);
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
 
@@ -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