@agentbean/daemon 0.1.3 → 0.1.4

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,38 @@
1
+ import { CodexAdapter } from './codex.js';
2
+ import { ClaudeCodeAdapter } from './claude-code.js';
3
+ import { OpenClawAdapter } from './openclaw.js';
4
+ import { HermesAdapter } from './hermes.js';
5
+ export function pickAdapter(cfg) {
6
+ switch (cfg.kind) {
7
+ case 'codex':
8
+ return new CodexAdapter({
9
+ command: cfg.command,
10
+ args: cfg.args,
11
+ cwd: cfg.cwd,
12
+ systemPrompt: cfg.systemPrompt,
13
+ });
14
+ case 'claude-code':
15
+ return new ClaudeCodeAdapter({
16
+ command: cfg.command,
17
+ args: cfg.args,
18
+ cwd: cfg.cwd,
19
+ systemPrompt: cfg.systemPrompt,
20
+ });
21
+ case 'openclaw':
22
+ return new OpenClawAdapter({
23
+ command: cfg.command,
24
+ args: cfg.args,
25
+ cwd: cfg.cwd,
26
+ systemPrompt: cfg.systemPrompt,
27
+ });
28
+ case 'hermes':
29
+ return new HermesAdapter({
30
+ command: cfg.command,
31
+ args: cfg.args,
32
+ cwd: cfg.cwd,
33
+ systemPrompt: cfg.systemPrompt,
34
+ });
35
+ default:
36
+ throw new Error(`adapter '${cfg.kind}' not yet implemented`);
37
+ }
38
+ }
@@ -1,4 +1,20 @@
1
1
  import { spawn } from 'node:child_process';
2
+ function runtimeArgs(args = []) {
3
+ if (args[0] === 'gateway' && args[1] === 'run') {
4
+ return args.slice(2);
5
+ }
6
+ return args;
7
+ }
8
+ function buildArgs(baseArgs, prompt) {
9
+ // If user already configured args with chat -q, just append the prompt
10
+ // Otherwise default to: hermes chat -q "<prompt>"
11
+ const hasChat = baseArgs.includes('chat');
12
+ const hasQ = baseArgs.includes('-q');
13
+ if (hasChat && hasQ) {
14
+ return [...baseArgs, prompt];
15
+ }
16
+ return [...baseArgs, 'chat', '-q', prompt];
17
+ }
2
18
  function buildPrompt(input, systemPrompt) {
3
19
  const parts = [];
4
20
  if (systemPrompt)
@@ -19,7 +35,7 @@ export class HermesAdapter {
19
35
  return new Promise((resolve, reject) => {
20
36
  const prompt = buildPrompt(input, this.opts.systemPrompt ?? input.systemPrompt);
21
37
  const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
22
- const child = spawn(this.opts.command, ['-z', prompt, ...(this.opts.args ?? [])], {
38
+ const child = spawn(this.opts.command, buildArgs(runtimeArgs(this.opts.args), prompt), {
23
39
  cwd,
24
40
  stdio: ['ignore', 'pipe', 'pipe'],
25
41
  });
@@ -63,10 +79,17 @@ export class HermesAdapter {
63
79
  return reject(new Error('aborted'));
64
80
  const out = Buffer.concat(stdoutChunks).toString('utf8');
65
81
  const err = Buffer.concat(stderrChunks).toString('utf8');
66
- if (code !== 0 && out.length === 0) {
67
- return reject(new Error(`hermes exit ${code}: ${err.slice(0, 400)}`));
82
+ const stdout = out.trim();
83
+ const stderr = err.trim();
84
+ if (code !== 0 && stdout.length === 0) {
85
+ const detail = stderr.length > 0 ? stderr.slice(0, 400) : 'no stderr';
86
+ return reject(new Error(`hermes exit ${code}: ${detail}`));
87
+ }
88
+ const reply = stdout || stderr;
89
+ if (!reply) {
90
+ return reject(new Error('hermes produced empty output'));
68
91
  }
69
- resolve(out.trim() || err.trim());
92
+ resolve(reply);
70
93
  });
71
94
  });
72
95
  }
@@ -1,4 +1,24 @@
1
1
  import { spawn } from 'node:child_process';
2
+ function buildArgs(baseArgs, prompt) {
3
+ // If user already configured args with chat send --message, just append the prompt
4
+ // Otherwise default to: openclaw chat send --message "<prompt>"
5
+ const hasSend = baseArgs.includes('send');
6
+ const hasMessage = baseArgs.includes('--message');
7
+ if (hasSend && hasMessage) {
8
+ return [...baseArgs, prompt];
9
+ }
10
+ return [...baseArgs, 'chat', 'send', '--message', prompt];
11
+ }
12
+ function buildPrompt(input, systemPrompt) {
13
+ const parts = [];
14
+ if (systemPrompt)
15
+ parts.push(systemPrompt);
16
+ for (const h of input.history.slice(-10)) {
17
+ parts.push(`${h.speaker} (${h.role}): ${h.body}`);
18
+ }
19
+ parts.push(input.prompt);
20
+ return parts.join('\n\n---\n\n');
21
+ }
2
22
  export class OpenClawAdapter {
3
23
  opts;
4
24
  kind = 'openclaw';
@@ -7,31 +27,37 @@ export class OpenClawAdapter {
7
27
  }
8
28
  async ask(input, signal) {
9
29
  return new Promise((resolve, reject) => {
10
- const payload = JSON.stringify({
11
- system: this.opts.systemPrompt ?? input.systemPrompt,
12
- history: input.history.slice(-10),
13
- user: input.prompt,
14
- });
15
- const child = spawn(this.opts.command, this.opts.args ?? [], {
16
- cwd: this.opts.cwd,
17
- stdio: ['pipe', 'pipe', 'pipe'],
30
+ const prompt = buildPrompt(input, this.opts.systemPrompt ?? input.systemPrompt);
31
+ const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
32
+ const child = spawn(this.opts.command, buildArgs(this.opts.args ?? [], prompt), {
33
+ cwd,
34
+ stdio: ['ignore', 'pipe', 'pipe'],
18
35
  });
19
36
  const stdoutChunks = [];
20
37
  const stderrChunks = [];
38
+ let finished = false;
21
39
  const MAX_EXEC_MS = 600_000;
22
40
  const onAbort = () => {
41
+ if (finished)
42
+ return;
43
+ finished = true;
23
44
  child.kill('SIGTERM');
24
- setTimeout(() => child.kill('SIGKILL'), 2_000).unref();
45
+ setTimeout(() => { try {
46
+ child.kill('SIGKILL');
47
+ }
48
+ catch { } }, 2_000).unref();
25
49
  };
26
50
  signal.addEventListener('abort', onAbort);
27
51
  const maxTimer = setTimeout(() => {
52
+ if (finished)
53
+ return;
54
+ finished = true;
28
55
  child.kill('SIGKILL');
29
56
  signal.removeEventListener('abort', onAbort);
30
57
  reject(new Error('openclaw adapter timeout'));
31
58
  }, MAX_EXEC_MS).unref();
32
59
  child.stdout.on('data', (b) => stdoutChunks.push(b));
33
60
  child.stderr.on('data', (b) => stderrChunks.push(b));
34
- child.stdin.end(payload);
35
61
  child.on('error', (err) => {
36
62
  clearTimeout(maxTimer);
37
63
  signal.removeEventListener('abort', onAbort);
@@ -40,23 +66,24 @@ export class OpenClawAdapter {
40
66
  child.on('exit', (code) => {
41
67
  clearTimeout(maxTimer);
42
68
  signal.removeEventListener('abort', onAbort);
69
+ if (finished)
70
+ return;
71
+ finished = true;
43
72
  if (signal.aborted)
44
73
  return reject(new Error('aborted'));
45
74
  const out = Buffer.concat(stdoutChunks).toString('utf8');
46
75
  const err = Buffer.concat(stderrChunks).toString('utf8');
47
- if (code !== 0) {
48
- return reject(new Error(`openclaw exit ${code}: ${err.slice(0, 400)}`));
49
- }
50
- let parsed;
51
- try {
52
- parsed = JSON.parse(out);
76
+ const stdout = out.trim();
77
+ const stderr = err.trim();
78
+ if (code !== 0 && stdout.length === 0) {
79
+ const detail = stderr.length > 0 ? stderr.slice(0, 400) : 'no stderr';
80
+ return reject(new Error(`openclaw exit ${code}: ${detail}`));
53
81
  }
54
- catch {
55
- return reject(new Error(`openclaw produced non-JSON output: ${out.slice(0, 200)}`));
82
+ const reply = stdout || stderr;
83
+ if (!reply) {
84
+ return reject(new Error('openclaw produced empty output'));
56
85
  }
57
- if (parsed.error)
58
- return reject(new Error(parsed.error));
59
- resolve((parsed.reply ?? '').trim());
86
+ resolve(reply);
60
87
  });
61
88
  });
62
89
  }
@@ -2,6 +2,19 @@ import { logger } from './log.js';
2
2
  import { uploadArtifact } from './uploader.js';
3
3
  import { postProcess } from './post-process.js';
4
4
  import { generateSandboxProfile, getWorkspaceDir, isSandboxAvailable } from './sandbox.js';
5
+ function errorMessage(err) {
6
+ if (err instanceof Error && err.message)
7
+ return err.message;
8
+ if (typeof err === 'string' && err.trim())
9
+ return err;
10
+ try {
11
+ const serialized = JSON.stringify(err);
12
+ if (serialized && serialized !== '{}')
13
+ return serialized;
14
+ }
15
+ catch { }
16
+ return 'unknown error';
17
+ }
5
18
  export class AgentInstance {
6
19
  config;
7
20
  adapter;
@@ -71,11 +84,12 @@ export class AgentInstance {
71
84
  });
72
85
  }
73
86
  catch (err) {
74
- logger.error({ err: err.message, requestId: req.requestId, agentId: this.id }, 'dispatch failed');
87
+ const message = errorMessage(err);
88
+ logger.error({ err: message, requestId: req.requestId, agentId: this.id }, 'dispatch failed');
75
89
  socket.emit('error_event', {
76
90
  agentId: this.id,
77
91
  at: Date.now(),
78
- message: err.message ?? 'unknown',
92
+ message,
79
93
  scope: 'reply',
80
94
  requestId: req.requestId,
81
95
  });
package/dist/bin.js CHANGED
File without changes
package/dist/config.js CHANGED
@@ -46,9 +46,8 @@ export function loadConfig(path) {
46
46
  throw new Error('config: server.url and server.token are required');
47
47
  }
48
48
  const inferredCategory = a.kind === 'codex' || a.kind === 'claude-code' ? 'executor-hosted' :
49
- a.kind === 'openclaw' || a.kind === 'hermes' ? 'agentos-hosted' :
50
- 'standalone-cli';
51
- const category = typeof interp.category === 'string' && ['executor-hosted', 'agentos-hosted', 'standalone-cli'].includes(interp.category)
49
+ 'agentos-hosted';
50
+ const category = typeof interp.category === 'string' && ['executor-hosted', 'agentos-hosted'].includes(interp.category)
52
51
  ? interp.category
53
52
  : inferredCategory;
54
53
  return {
@@ -106,9 +105,8 @@ export function loadDeviceConfig(path) {
106
105
  throw new Error('config: adapter.command is required');
107
106
  }
108
107
  const inferredCategory = ad.kind === 'codex' || ad.kind === 'claude-code' ? 'executor-hosted' :
109
- ad.kind === 'openclaw' || ad.kind === 'hermes' ? 'agentos-hosted' :
110
- 'standalone-cli';
111
- const category = typeof a.category === 'string' && ['executor-hosted', 'agentos-hosted', 'standalone-cli'].includes(a.category)
108
+ 'agentos-hosted';
109
+ const category = typeof a.category === 'string' && ['executor-hosted', 'agentos-hosted'].includes(a.category)
112
110
  ? a.category
113
111
  : inferredCategory;
114
112
  parsedAgents.push({
@@ -2,6 +2,19 @@ import { io } from 'socket.io-client';
2
2
  import { logger } from './log.js';
3
3
  import { uploadArtifact } from './uploader.js';
4
4
  import { postProcess } from './post-process.js';
5
+ function errorMessage(err) {
6
+ if (err instanceof Error && err.message)
7
+ return err.message;
8
+ if (typeof err === 'string' && err.trim())
9
+ return err;
10
+ try {
11
+ const serialized = JSON.stringify(err);
12
+ if (serialized && serialized !== '{}')
13
+ return serialized;
14
+ }
15
+ catch { }
16
+ return 'unknown error';
17
+ }
5
18
  export function createConnection(cfg, adapter) {
6
19
  let socket = null;
7
20
  let heartbeatTimer = null;
@@ -81,10 +94,11 @@ export function createConnection(cfg, adapter) {
81
94
  });
82
95
  }
83
96
  catch (err) {
84
- logger.error({ err: err.message, requestId: req.requestId }, 'dispatch failed');
97
+ const message = errorMessage(err);
98
+ logger.error({ err: message, requestId: req.requestId }, 'dispatch failed');
85
99
  currentSocket.emit('error_event', {
86
100
  at: Date.now(),
87
- message: err.message ?? 'unknown',
101
+ message,
88
102
  scope: 'reply',
89
103
  requestId: req.requestId,
90
104
  });
@@ -3,24 +3,73 @@ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import { logger } from './log.js';
6
+ import { AgentInstance } from './agent-instance.js';
7
+ import { pickAdapter } from './adapters/factory.js';
6
8
  import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, collectSystemInfo } from './scanner.js';
9
+ function errorMessage(err) {
10
+ if (err instanceof Error && err.message)
11
+ return err.message;
12
+ if (typeof err === 'string' && err.trim())
13
+ return err;
14
+ try {
15
+ const serialized = JSON.stringify(err);
16
+ if (serialized && serialized !== '{}')
17
+ return serialized;
18
+ }
19
+ catch { }
20
+ return 'unknown error';
21
+ }
22
+ function agentSlug(name) {
23
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
24
+ }
25
+ function scannedAgentId(deviceId, name) {
26
+ return `scan-${deviceId}-${agentSlug(name)}`;
27
+ }
7
28
  const CACHE_DIR = join(homedir(), '.agentbean');
8
29
  const CACHE_FILE = join(CACHE_DIR, 'scanned-agents.json');
30
+ function isRuntimeEntry(entry) {
31
+ return entry.category === 'executor-hosted' &&
32
+ ['codex', 'claude-code', 'kimi-cli', 'Kimi-cli'].includes(entry.adapterKind);
33
+ }
34
+ function splitLegacyCache(entries) {
35
+ const agents = [];
36
+ const runtimes = [];
37
+ for (const entry of entries) {
38
+ if (isRuntimeEntry(entry)) {
39
+ runtimes.push({
40
+ name: entry.name,
41
+ adapterKind: entry.adapterKind,
42
+ command: entry.command,
43
+ installed: Boolean(entry.command),
44
+ });
45
+ }
46
+ else {
47
+ agents.push(entry);
48
+ }
49
+ }
50
+ return { agents, runtimes };
51
+ }
9
52
  function loadCache() {
10
53
  try {
11
54
  if (!existsSync(CACHE_FILE))
12
55
  return null;
13
- return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
56
+ const parsed = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
57
+ if (Array.isArray(parsed))
58
+ return splitLegacyCache(parsed);
59
+ return {
60
+ agents: parsed.agents ?? [],
61
+ runtimes: parsed.runtimes ?? [],
62
+ };
14
63
  }
15
64
  catch {
16
65
  return null;
17
66
  }
18
67
  }
19
- function saveCache(agents) {
68
+ function saveCache(payload) {
20
69
  try {
21
70
  if (!existsSync(CACHE_DIR))
22
71
  mkdirSync(CACHE_DIR, { recursive: true });
23
- writeFileSync(CACHE_FILE, JSON.stringify(agents, null, 2));
72
+ writeFileSync(CACHE_FILE, JSON.stringify(payload, null, 2));
24
73
  }
25
74
  catch (err) {
26
75
  logger.warn({ err: err?.message }, 'failed to save scan cache');
@@ -32,39 +81,29 @@ async function scanAll() {
32
81
  scanAgentOSAgents(),
33
82
  scanLocalAgents(),
34
83
  ]);
35
- const results = [];
36
- // Runtimes (executor-hosted) only installed ones
37
- for (const rt of runtimes) {
38
- if (rt.installed) {
39
- results.push({
40
- name: rt.name,
41
- category: 'executor-hosted',
42
- adapterKind: rt.adapterKind,
43
- command: rt.command,
44
- args: [],
45
- source: 'scanned',
46
- });
47
- }
48
- }
84
+ const agents = [];
85
+ const runtimeResults = runtimes.filter((rt) => rt.installed);
49
86
  // AgentOS + standalone (from gateway and filesystem scans)
50
87
  const seen = new Set();
51
88
  for (const ag of agentos) {
52
89
  if (!seen.has(ag.command)) {
53
90
  seen.add(ag.command);
54
- results.push({ ...ag, source: 'scanned' });
91
+ agents.push({ ...ag, source: 'scanned' });
55
92
  }
56
93
  }
57
94
  for (const ag of local) {
58
95
  if (!seen.has(ag.command)) {
59
96
  seen.add(ag.command);
60
- results.push({ ...ag, source: 'scanned' });
97
+ agents.push({ ...ag, source: 'scanned' });
61
98
  }
62
99
  }
63
- return results;
100
+ return { agents, runtimes: runtimeResults };
64
101
  }
65
102
  export function createDeviceDaemon(cfg, agents) {
66
103
  let socket = null;
67
104
  let heartbeatTimer = null;
105
+ let rescanTimer = null;
106
+ const RESCAN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
68
107
  const queues = new Map();
69
108
  const httpBase = cfg.server.url.replace(/\/agent$/, '');
70
109
  let firstConnect = true;
@@ -72,10 +111,41 @@ export function createDeviceDaemon(cfg, agents) {
72
111
  const publicAgents = Array.from(agents.values())
73
112
  .filter((a) => a.visibility === 'public')
74
113
  .map((a) => a.publicMeta);
75
- function emitRegister(sock, scanned) {
76
- if (scanned.length === 0)
114
+ function emitRegister(sock, payload) {
115
+ if (payload.runtimes.length > 0) {
116
+ sock.emit('device:register-runtimes', { runtimes: payload.runtimes }, (ack) => {
117
+ if (!ack?.ok)
118
+ logger.warn({ error: ack?.error }, 'failed to register runtimes');
119
+ });
120
+ }
121
+ if (payload.agents.length === 0)
77
122
  return;
78
- sock.emit('device:register-agents', { agents: scanned }, (ack) => {
123
+ for (const ag of payload.agents) {
124
+ const id = scannedAgentId(cfg.deviceId, ag.name);
125
+ if (agents.has(id))
126
+ continue;
127
+ const entry = {
128
+ id,
129
+ name: ag.name,
130
+ role: ag.category === 'executor-hosted' ? 'executor-agent' : 'gateway-agent',
131
+ category: ag.category,
132
+ adapter: {
133
+ kind: ag.adapterKind,
134
+ command: ag.command,
135
+ args: ag.args ?? [],
136
+ cwd: ag.cwd,
137
+ },
138
+ visibility: 'public',
139
+ };
140
+ try {
141
+ agents.set(id, new AgentInstance(entry, pickAdapter(entry.adapter)));
142
+ logger.info({ id, kind: entry.adapter.kind }, 'scanned agent instance created');
143
+ }
144
+ catch (err) {
145
+ logger.warn({ id, err: errorMessage(err) }, 'failed to create scanned agent instance');
146
+ }
147
+ }
148
+ sock.emit('device:register-agents', { agents: payload.agents }, (ack) => {
79
149
  if (ack?.ok) {
80
150
  logger.info({ count: ack.agents?.length }, 'scanned agents registered');
81
151
  }
@@ -88,15 +158,21 @@ export function createDeviceDaemon(cfg, agents) {
88
158
  if (useCache) {
89
159
  const cached = loadCache();
90
160
  if (cached) {
91
- logger.info({ count: cached.length }, 'using cached scan results');
161
+ logger.info({ count: cached.agents.length + cached.runtimes.length }, 'using cached scan results');
92
162
  emitRegister(sock, cached);
93
163
  // Background refresh — only emit if results differ
94
164
  scanAll().then((fresh) => {
95
165
  saveCache(fresh);
96
- const cachedKey = JSON.stringify(cached.map((a) => a.command).sort());
97
- const freshKey = JSON.stringify(fresh.map((a) => a.command).sort());
166
+ const cachedKey = JSON.stringify([
167
+ ...cached.agents.map((a) => a.command),
168
+ ...cached.runtimes.map((rt) => rt.command),
169
+ ].sort());
170
+ const freshKey = JSON.stringify([
171
+ ...fresh.agents.map((a) => a.command),
172
+ ...fresh.runtimes.map((rt) => rt.command),
173
+ ].sort());
98
174
  if (cachedKey !== freshKey) {
99
- logger.info({ count: fresh.length }, 'scan results changed, updating');
175
+ logger.info({ count: fresh.agents.length + fresh.runtimes.length }, 'scan results changed, updating');
100
176
  emitRegister(sock, fresh);
101
177
  }
102
178
  }).catch((err) => {
@@ -145,6 +221,14 @@ export function createDeviceDaemon(cfg, agents) {
145
221
  heartbeatTimer = setInterval(() => {
146
222
  socket?.emit('heartbeat');
147
223
  }, cfg.heartbeatIntervalMs);
224
+ // Periodic re-scan to update agent availability
225
+ if (rescanTimer)
226
+ clearInterval(rescanTimer);
227
+ rescanTimer = setInterval(() => {
228
+ if (!socket?.connected)
229
+ return;
230
+ scanAndRegister(socket, false);
231
+ }, RESCAN_INTERVAL_MS);
148
232
  });
149
233
  socket.on('connect_error', (err) => {
150
234
  logger.error({ err: err.message }, 'connect_error');
@@ -178,11 +262,12 @@ export function createDeviceDaemon(cfg, agents) {
178
262
  networkId: cfg.networkId,
179
263
  });
180
264
  }).catch((err) => {
181
- logger.error({ err: err?.message, agentId: req.agentId }, 'dispatch queue error');
265
+ const message = errorMessage(err);
266
+ logger.error({ err: message, agentId: req.agentId }, 'dispatch queue error');
182
267
  currentSocket.emit('error_event', {
183
268
  agentId: req.agentId,
184
269
  at: Date.now(),
185
- message: err?.message ?? 'unknown',
270
+ message,
186
271
  scope: 'reply',
187
272
  requestId: req.requestId,
188
273
  });
@@ -198,6 +283,10 @@ export function createDeviceDaemon(cfg, agents) {
198
283
  clearInterval(heartbeatTimer);
199
284
  heartbeatTimer = null;
200
285
  }
286
+ if (rescanTimer) {
287
+ clearInterval(rescanTimer);
288
+ rescanTimer = null;
289
+ }
201
290
  });
202
291
  },
203
292
  async stop() {
@@ -205,6 +294,10 @@ export function createDeviceDaemon(cfg, agents) {
205
294
  clearInterval(heartbeatTimer);
206
295
  heartbeatTimer = null;
207
296
  }
297
+ if (rescanTimer) {
298
+ clearInterval(rescanTimer);
299
+ rescanTimer = null;
300
+ }
208
301
  socket?.close();
209
302
  socket = null;
210
303
  },
package/dist/index.js CHANGED
@@ -3,48 +3,11 @@ import { loadConfig, loadDeviceConfig } from './config.js';
3
3
  import { createConnection } from './connection.js';
4
4
  import { createDeviceDaemon } from './device-daemon.js';
5
5
  import { AgentInstance } from './agent-instance.js';
6
- import { CodexAdapter } from './adapters/codex.js';
7
- import { ClaudeCodeAdapter } from './adapters/claude-code.js';
8
- import { OpenClawAdapter } from './adapters/openclaw.js';
9
- import { HermesAdapter } from './adapters/hermes.js';
6
+ import { pickAdapter } from './adapters/factory.js';
10
7
  import { logger } from './log.js';
11
8
  import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, getDeviceId } from './scanner.js';
12
9
  import { loadAuth, saveAuth } from './auth-store.js';
13
- function pickAdapter(cfg) {
14
- switch (cfg.kind) {
15
- case 'codex':
16
- return new CodexAdapter({
17
- command: cfg.command,
18
- args: cfg.args,
19
- cwd: cfg.cwd,
20
- systemPrompt: cfg.systemPrompt,
21
- });
22
- case 'claude-code':
23
- return new ClaudeCodeAdapter({
24
- command: cfg.command,
25
- args: cfg.args,
26
- cwd: cfg.cwd,
27
- systemPrompt: cfg.systemPrompt,
28
- });
29
- case 'openclaw':
30
- return new OpenClawAdapter({
31
- command: cfg.command,
32
- args: cfg.args,
33
- cwd: cfg.cwd,
34
- systemPrompt: cfg.systemPrompt,
35
- });
36
- case 'hermes':
37
- return new HermesAdapter({
38
- command: cfg.command,
39
- args: cfg.args,
40
- cwd: cfg.cwd,
41
- systemPrompt: cfg.systemPrompt,
42
- });
43
- default:
44
- throw new Error(`adapter '${cfg.kind}' not yet implemented`);
45
- }
46
- }
47
- async function discoverAgents() {
10
+ async function discoverAgents(deviceId) {
48
11
  const [_runtimes, agentos, local] = await Promise.all([
49
12
  scanRuntimes(),
50
13
  scanAgentOSAgents(),
@@ -56,11 +19,10 @@ async function discoverAgents() {
56
19
  if (seen.has(s.command))
57
20
  continue;
58
21
  seen.add(s.command);
59
- const id = s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
60
22
  results.push({
61
- id,
23
+ id: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
62
24
  name: s.name,
63
- role: s.category === 'executor-hosted' ? 'executor-agent' : s.category === 'agentos-hosted' ? 'gateway-agent' : 'standalone-agent',
25
+ role: s.category === 'executor-hosted' ? 'executor-agent' : 'gateway-agent',
64
26
  category: s.category,
65
27
  adapter: {
66
28
  kind: s.adapterKind,
@@ -102,10 +64,6 @@ async function runDeviceMode(cfgPath) {
102
64
  const shouldScan = err.message?.includes('agents array is required');
103
65
  if (!shouldScan)
104
66
  throw err;
105
- scannedEntries = await discoverAgents();
106
- if (scannedEntries.length === 0) {
107
- throw new Error('device config missing and no agents discovered via scanning');
108
- }
109
67
  let fileSettings = {};
110
68
  try {
111
69
  const { readFileSync } = await import('node:fs');
@@ -119,8 +77,13 @@ async function runDeviceMode(cfgPath) {
119
77
  };
120
78
  }
121
79
  catch { /* ignore */ }
80
+ const deviceId = fileSettings.deviceId ?? process.env.DEVICE_ID ?? await getDeviceId();
81
+ scannedEntries = await discoverAgents(deviceId);
82
+ if (scannedEntries.length === 0) {
83
+ throw new Error('device config missing and no agents discovered via scanning');
84
+ }
122
85
  cfg = {
123
- deviceId: fileSettings.deviceId ?? process.env.DEVICE_ID ?? await getDeviceId(),
86
+ deviceId,
124
87
  networkId: fileSettings.networkId ?? process.env.NETWORK_ID ?? 'default',
125
88
  server: fileSettings.server ?? {
126
89
  url: process.env.SERVER_URL ?? 'http://localhost:3000/agent',
@@ -131,7 +94,7 @@ async function runDeviceMode(cfgPath) {
131
94
  };
132
95
  }
133
96
  if (cfg.scan === true) {
134
- scannedEntries = await discoverAgents();
97
+ scannedEntries = await discoverAgents(cfg.deviceId);
135
98
  if (scannedEntries.length > 0) {
136
99
  cfg = { ...cfg, agents: scannedEntries };
137
100
  }
@@ -203,7 +166,7 @@ Options:
203
166
  }
204
167
  const deviceId = values['device-id'] ?? await getDeviceId();
205
168
  logger.info({ serverUrl, deviceId, networkId }, 'CLI mode: auto-discovering agents');
206
- const agents = await discoverAgents();
169
+ const agents = await discoverAgents(deviceId);
207
170
  if (agents.length === 0) {
208
171
  logger.warn('no agents discovered on this machine. Daemon will start with no agents.');
209
172
  }
package/dist/scanner.js CHANGED
@@ -1,12 +1,17 @@
1
- import { execFile } from 'node:child_process';
2
- import { readdirSync, readFileSync, statSync, existsSync, writeFileSync, mkdirSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { createHash } from 'node:crypto';
5
- import * as os from 'node:os';
6
- import { logger } from './log.js';
1
+ import { execFile } from "node:child_process";
2
+ import { readdirSync, readFileSync, statSync, existsSync, writeFileSync, mkdirSync, } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import * as os from "node:os";
6
+ import { logger } from "./log.js";
7
7
  function which(bin) {
8
8
  return new Promise((resolve) => {
9
- const child = execFile('which', [bin], { timeout: 5_000 }, (err, stdout) => {
9
+ const child = execFile('which', [bin], { timeout: 5_000, env: { ...process.env, PATH: [
10
+ process.env.PATH,
11
+ '/usr/local/bin',
12
+ '/opt/homebrew/bin',
13
+ join(os.homedir(), '.nvm/versions/node', ...getAllNodeVersions(), 'bin'),
14
+ ].filter(Boolean).join(':') } }, (err, stdout) => {
10
15
  if (err) {
11
16
  resolve(null);
12
17
  return;
@@ -17,16 +22,27 @@ function which(bin) {
17
22
  child.on('error', () => resolve(null));
18
23
  });
19
24
  }
25
+ function getAllNodeVersions() {
26
+ try {
27
+ const nvmDir = join(os.homedir(), '.nvm/versions/node');
28
+ if (!existsSync(nvmDir))
29
+ return [];
30
+ return readdirSync(nvmDir);
31
+ }
32
+ catch {
33
+ return [];
34
+ }
35
+ }
20
36
  function run(bin, args) {
21
37
  return new Promise((resolve) => {
22
38
  const child = execFile(bin, args, { timeout: 10_000 }, (err, stdout) => {
23
- resolve(stdout?.trim() ?? '');
39
+ resolve(stdout?.trim() ?? "");
24
40
  });
25
- child.on('error', () => resolve(''));
41
+ child.on("error", () => resolve(""));
26
42
  });
27
43
  }
28
44
  // --- Machine ID (stable per-device identifier) ---
29
- const MACHINE_ID_FILE = join(os.homedir(), '.agentbean', 'device-id');
45
+ const MACHINE_ID_FILE = join(os.homedir(), ".agentbean", "device-id");
30
46
  function getFirstMacAddress() {
31
47
  const ifaces = os.networkInterfaces();
32
48
  for (const [name, addrs] of Object.entries(ifaces)) {
@@ -36,7 +52,7 @@ function getFirstMacAddress() {
36
52
  // Skip internal (loopback) and zero MAC
37
53
  if (addr.internal)
38
54
  continue;
39
- if (addr.mac === '00:00:00:00:00:00')
55
+ if (addr.mac === "00:00:00:00:00:00")
40
56
  continue;
41
57
  return addr.mac;
42
58
  }
@@ -46,19 +62,28 @@ function getFirstMacAddress() {
46
62
  async function readPlatformMachineId() {
47
63
  const platform = os.platform();
48
64
  try {
49
- if (platform === 'linux') {
50
- if (existsSync('/etc/machine-id')) {
51
- return readFileSync('/etc/machine-id', 'utf-8').trim() || null;
65
+ if (platform === "linux") {
66
+ if (existsSync("/etc/machine-id")) {
67
+ return readFileSync("/etc/machine-id", "utf-8").trim() || null;
52
68
  }
53
69
  }
54
- else if (platform === 'darwin') {
55
- const output = await run('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']);
70
+ else if (platform === "darwin") {
71
+ const output = await run("ioreg", [
72
+ "-rd1",
73
+ "-c",
74
+ "IOPlatformExpertDevice",
75
+ ]);
56
76
  const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
57
77
  if (match)
58
78
  return match[1] ?? null;
59
79
  }
60
- else if (platform === 'win32') {
61
- const output = await run('reg', ['query', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid']);
80
+ else if (platform === "win32") {
81
+ const output = await run("reg", [
82
+ "query",
83
+ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
84
+ "/v",
85
+ "MachineGuid",
86
+ ]);
62
87
  const match = output.match(/MachineGuid\s+REG_SZ\s+(\S+)/);
63
88
  if (match)
64
89
  return match[1] ?? null;
@@ -77,7 +102,7 @@ async function readPlatformMachineId() {
77
102
  export async function getDeviceId() {
78
103
  // 1. Read cached ID
79
104
  if (existsSync(MACHINE_ID_FILE)) {
80
- const cached = readFileSync(MACHINE_ID_FILE, 'utf-8').trim();
105
+ const cached = readFileSync(MACHINE_ID_FILE, "utf-8").trim();
81
106
  if (cached)
82
107
  return cached;
83
108
  }
@@ -95,7 +120,7 @@ export async function getDeviceId() {
95
120
  let deviceId;
96
121
  if (parts.length > 2) {
97
122
  // We have enough hardware info — generate deterministic ID
98
- const hash = createHash('sha256').update(parts.join('|')).digest('hex');
123
+ const hash = createHash("sha256").update(parts.join("|")).digest("hex");
99
124
  // Format as UUID: 8-4-4-4-12
100
125
  deviceId = [
101
126
  hash.slice(0, 8),
@@ -103,16 +128,16 @@ export async function getDeviceId() {
103
128
  hash.slice(12, 16),
104
129
  hash.slice(16, 20),
105
130
  hash.slice(20, 32),
106
- ].join('-');
131
+ ].join("-");
107
132
  }
108
133
  else {
109
134
  // Fallback: random UUID
110
- const { randomUUID } = await import('node:crypto');
135
+ const { randomUUID } = await import("node:crypto");
111
136
  deviceId = randomUUID();
112
137
  }
113
138
  // 3. Cache to file
114
139
  try {
115
- const dir = join(os.homedir(), '.agentbean');
140
+ const dir = join(os.homedir(), ".agentbean");
116
141
  if (!existsSync(dir))
117
142
  mkdirSync(dir, { recursive: true });
118
143
  writeFileSync(MACHINE_ID_FILE, deviceId);
@@ -125,19 +150,25 @@ export async function getDeviceId() {
125
150
  // --- Scan Coding Agent Runtimes (Claude Code, Codex, Kimi) ---
126
151
  export async function scanRuntimes() {
127
152
  const checks = [
128
- { bin: 'claude', name: 'Claude Code', adapterKind: 'claude-code' },
129
- { bin: 'codex', name: 'Codex CLI', adapterKind: 'codex' },
130
- { bin: 'kimi-cli', name: 'Kimi CLI', adapterKind: 'codex' },
131
- { bin: 'manus', name: 'Manus', adapterKind: 'standalone' },
132
- { bin: 'anygen', name: 'Anygen', adapterKind: 'standalone' },
153
+ {
154
+ bin: "claude",
155
+ name: "Claude Code",
156
+ adapterKind: "claude-code",
157
+ },
158
+ { bin: "codex", name: "Codex CLI", adapterKind: "codex" },
159
+ {
160
+ bin: "kimi-cli",
161
+ name: "Kimi CLI",
162
+ adapterKind: "Kimi-cli",
163
+ },
133
164
  ];
134
165
  const results = [];
135
166
  for (const s of checks) {
136
167
  const path = await which(s.bin);
137
168
  results.push({
138
- name: s.name,
169
+ name: s.name.replace(/\s+/g, "-"),
139
170
  adapterKind: s.adapterKind,
140
- command: path ?? '',
171
+ command: path ?? "",
141
172
  installed: path !== null,
142
173
  });
143
174
  }
@@ -145,37 +176,37 @@ export async function scanRuntimes() {
145
176
  }
146
177
  // --- Scan AgentOS Gateways (Hermes, OpenClaw) ---
147
178
  async function checkHermesGateway() {
148
- const path = await which('hermes');
179
+ const path = await which("hermes");
149
180
  if (!path)
150
181
  return null;
151
- const status = await run('hermes', ['gateway', 'status']);
152
- const running = status.includes('running') || status.includes('');
182
+ const status = await run("hermes", ["gateway", "status"]);
183
+ const running = status.includes("running") || status.includes("");
153
184
  if (running) {
154
185
  return {
155
- category: 'agentos-hosted',
156
- name: 'Hermes Agent',
157
- adapterKind: 'hermes',
186
+ category: "agentos-hosted",
187
+ name: "Hermes-Agent",
188
+ adapterKind: "hermes",
158
189
  command: path,
159
- args: ['gateway', 'run'],
160
- source: 'gateway',
190
+ args: [],
191
+ source: "gateway",
161
192
  };
162
193
  }
163
194
  return null;
164
195
  }
165
196
  async function checkOpenClawGateway() {
166
- const path = await which('openclaw');
197
+ const path = await which("openclaw");
167
198
  if (!path)
168
199
  return null;
169
- const status = await run('openclaw', ['gateway', 'status']);
170
- const running = status.includes('running') || status.includes('');
200
+ const status = await run("openclaw", ["gateway", "status"]);
201
+ const running = status.includes("running") || status.includes("");
171
202
  if (running) {
172
203
  return {
173
- category: 'agentos-hosted',
174
- name: 'OpenClaw Agent',
175
- adapterKind: 'openclaw',
204
+ category: "agentos-hosted",
205
+ name: "OpenClaw-Agent",
206
+ adapterKind: "openclaw",
176
207
  command: path,
177
- args: ['gateway', 'run'],
178
- source: 'gateway',
208
+ args: ["gateway", "run"],
209
+ source: "gateway",
179
210
  };
180
211
  }
181
212
  return null;
@@ -188,7 +219,7 @@ export async function scanAgentOSAgents() {
188
219
  return [hermes, openclaw].filter((a) => a !== null);
189
220
  }
190
221
  // --- Scan local agent definitions from filesystem ---
191
- export async function scanLocalAgents(scanDir = join(os.homedir(), '.agentbean', 'agents')) {
222
+ export async function scanLocalAgents(scanDir = join(os.homedir(), ".agentbean", "agents")) {
192
223
  if (!existsSync(scanDir)) {
193
224
  return [];
194
225
  }
@@ -198,7 +229,7 @@ export async function scanLocalAgents(scanDir = join(os.homedir(), '.agentbean',
198
229
  entries = readdirSync(scanDir);
199
230
  }
200
231
  catch (err) {
201
- logger?.warn?.({ err: err?.message }, 'scan failed');
232
+ logger?.warn?.({ err: err?.message }, "scan failed");
202
233
  return [];
203
234
  }
204
235
  for (const entry of entries) {
@@ -212,63 +243,67 @@ export async function scanLocalAgents(scanDir = join(os.homedir(), '.agentbean',
212
243
  }
213
244
  if (!st.isDirectory())
214
245
  continue;
215
- const jsonPath = join(subdir, 'agent.json');
216
- const yamlPath = join(subdir, 'agent.yaml');
217
- const ymlPath = join(subdir, 'agent.yml');
246
+ const jsonPath = join(subdir, "agent.json");
247
+ const yamlPath = join(subdir, "agent.yaml");
248
+ const ymlPath = join(subdir, "agent.yml");
218
249
  let raw = null;
219
250
  let ext = null;
220
251
  if (existsSync(jsonPath)) {
221
- raw = readFileSync(jsonPath, 'utf8');
222
- ext = 'json';
252
+ raw = readFileSync(jsonPath, "utf8");
253
+ ext = "json";
223
254
  }
224
255
  else if (existsSync(yamlPath)) {
225
- raw = readFileSync(yamlPath, 'utf8');
226
- ext = 'yaml';
256
+ raw = readFileSync(yamlPath, "utf8");
257
+ ext = "yaml";
227
258
  }
228
259
  else if (existsSync(ymlPath)) {
229
- raw = readFileSync(ymlPath, 'utf8');
230
- ext = 'yaml';
260
+ raw = readFileSync(ymlPath, "utf8");
261
+ ext = "yaml";
231
262
  }
232
263
  if (raw === null || ext === null)
233
264
  continue;
234
265
  let parsed = null;
235
266
  try {
236
- if (ext === 'json') {
267
+ if (ext === "json") {
237
268
  parsed = JSON.parse(raw);
238
269
  }
239
270
  else {
240
- const { load: parseYaml } = await import('js-yaml');
271
+ const { load: parseYaml } = await import("js-yaml");
241
272
  parsed = parseYaml(raw);
242
273
  }
243
274
  }
244
275
  catch {
245
276
  continue;
246
277
  }
247
- if (!parsed || typeof parsed !== 'object')
278
+ if (!parsed || typeof parsed !== "object")
248
279
  continue;
249
- const name = typeof parsed.name === 'string' ? parsed.name : entry;
250
- const command = typeof parsed.command === 'string' ? parsed.command : '';
251
- const args = Array.isArray(parsed.args) ? parsed.args.map(String) : [];
280
+ const name = (typeof parsed.name === "string" ? parsed.name : entry).replace(/\s+/g, "-");
281
+ const command = typeof parsed.command === "string" ? parsed.command : "";
282
+ const args = Array.isArray(parsed.args)
283
+ ? parsed.args.map(String)
284
+ : [];
252
285
  let category;
253
- if (typeof parsed.category === 'string' && ['executor-hosted', 'agentos-hosted', 'standalone-cli'].includes(parsed.category)) {
286
+ if (typeof parsed.category === "string" &&
287
+ ["executor-hosted", "agentos-hosted"].includes(parsed.category)) {
254
288
  category = parsed.category;
255
289
  }
256
- else if ('executor' in parsed) {
257
- category = 'executor-hosted';
290
+ else if ("executor" in parsed) {
291
+ category = "executor-hosted";
258
292
  }
259
293
  else {
260
- category = 'standalone-cli';
294
+ category = "executor-hosted";
261
295
  }
262
- const adapterKind = typeof parsed.adapterKind === 'string' && ['codex', 'claude-code', 'openclaw', 'hermes', 'standalone'].includes(parsed.adapterKind)
296
+ const adapterKind = typeof parsed.adapterKind === "string" &&
297
+ ["codex", "claude-code", "openclaw", "hermes"].includes(parsed.adapterKind)
263
298
  ? parsed.adapterKind
264
- : 'standalone';
299
+ : "codex";
265
300
  results.push({
266
301
  category,
267
302
  name,
268
303
  adapterKind,
269
304
  command,
270
305
  args,
271
- source: 'filesystem',
306
+ source: "filesystem",
272
307
  });
273
308
  }
274
309
  return results;
@@ -279,7 +314,7 @@ export function collectSystemInfo() {
279
314
  const cpus = os.cpus();
280
315
  const platform = os.platform();
281
316
  let osVersion = `${os.type()} ${os.release()}`;
282
- if (platform === 'darwin') {
317
+ if (platform === "darwin") {
283
318
  osVersion = `macOS ${os.release()}`;
284
319
  }
285
320
  return {
@@ -287,10 +322,10 @@ export function collectSystemInfo() {
287
322
  arch: os.arch(),
288
323
  osVersion,
289
324
  hostname: os.hostname(),
290
- cpuModel: cpus[0]?.model ?? 'unknown',
325
+ cpuModel: cpus[0]?.model ?? "unknown",
291
326
  cpuCores: cpus.length,
292
- totalMemoryGB: Math.round(totalMem / 1024 / 1024 / 1024 * 10) / 10,
293
- freeMemoryGB: Math.round(freeMem / 1024 / 1024 / 1024 * 10) / 10,
327
+ totalMemoryGB: Math.round((totalMem / 1024 / 1024 / 1024) * 10) / 10,
328
+ freeMemoryGB: Math.round((freeMem / 1024 / 1024 / 1024) * 10) / 10,
294
329
  nodeVersion: process.version,
295
330
  };
296
331
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentbean/daemon",
3
3
  "private": false,
4
- "version": "0.1.3",
4
+ "version": "0.1.4",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {