@agentbean/daemon 0.1.3 → 0.1.5

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/README.md CHANGED
@@ -14,7 +14,7 @@ npm test # 运行测试
14
14
  ### 带配置文件启动
15
15
 
16
16
  ```bash
17
- npx tsx src/index.ts ~/.agentbean/device-agent.yaml
17
+ npx tsx src/bin.ts ~/.agentbean/device-agent.yaml
18
18
  ```
19
19
 
20
20
  ### 自动扫描模式
@@ -22,7 +22,7 @@ npx tsx src/index.ts ~/.agentbean/device-agent.yaml
22
22
  如果不提供配置文件,或配置文件中 `agents` 数组为空,Daemon 会自动扫描本机 Agent:
23
23
 
24
24
  ```bash
25
- npx tsx src/index.ts
25
+ npx tsx src/bin.ts
26
26
  # 扫描 Coding Agent (which claude-code, codex, kimi...)
27
27
  # 扫描 AgentOS Gateway (localhost:PORT)
28
28
  # 扫描 ~/.agentbean/agents/ 目录
@@ -34,7 +34,7 @@ npx tsx src/index.ts
34
34
 
35
35
  ```yaml
36
36
  deviceId: my-macbook-pro # 设备标识
37
- networkId: default # 所属网络
37
+ networkId: default # 所属团队
38
38
  server:
39
39
  url: http://localhost:3000/agent # Server Socket.IO 地址
40
40
  token: default:default:dev-token-change-me # 三截 token
@@ -155,4 +155,4 @@ interface CliAdapter {
155
155
  | `SERVER_URL` | Server WebSocket 地址 |
156
156
  | `SERVER_TOKEN` | 接入令牌 |
157
157
  | `DEVICE_ID` | 设备标识 |
158
- | `NETWORK_ID` | 所属网络 |
158
+ | `NETWORK_ID` | 所属团队 |
@@ -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)
@@ -9,6 +25,40 @@ function buildPrompt(input, systemPrompt) {
9
25
  parts.push(input.prompt);
10
26
  return parts.join('\n\n---\n\n');
11
27
  }
28
+ const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
29
+ const BOX_ONLY_RE = /^[\s─━═╭╮╰╯│┃┌┐└┘├┤┬┴┼]+$/;
30
+ export function extractHermesReply(output) {
31
+ const lines = output
32
+ .replace(ANSI_RE, '')
33
+ .replace(/\r\n?/g, '\n')
34
+ .split('\n');
35
+ const cleaned = lines
36
+ .map((line) => line.trimEnd())
37
+ .filter((line) => {
38
+ const trimmed = line.trim();
39
+ if (!trimmed)
40
+ return true;
41
+ if (trimmed.startsWith('Query:'))
42
+ return false;
43
+ if (trimmed === 'Initializing agent...' || trimmed === 'Initializing agent…')
44
+ return false;
45
+ if (trimmed.startsWith('Resume this session with:'))
46
+ return false;
47
+ if (/^hermes\s+--resume\b/.test(trimmed))
48
+ return false;
49
+ if (/^(Session|Duration|Messages):\s+/.test(trimmed))
50
+ return false;
51
+ if (trimmed.startsWith('╭') || trimmed.startsWith('╰'))
52
+ return false;
53
+ if (BOX_ONLY_RE.test(trimmed))
54
+ return false;
55
+ return true;
56
+ })
57
+ .map((line) => line.replace(/^[│┃]\s?/, '').replace(/\s?[│┃]$/, '').replace(/^\s{2,}/, ''))
58
+ .join('\n')
59
+ .trim();
60
+ return cleaned || output.trim();
61
+ }
12
62
  export class HermesAdapter {
13
63
  opts;
14
64
  kind = 'hermes';
@@ -19,7 +69,7 @@ export class HermesAdapter {
19
69
  return new Promise((resolve, reject) => {
20
70
  const prompt = buildPrompt(input, this.opts.systemPrompt ?? input.systemPrompt);
21
71
  const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
22
- const child = spawn(this.opts.command, ['-z', prompt, ...(this.opts.args ?? [])], {
72
+ const child = spawn(this.opts.command, buildArgs(runtimeArgs(this.opts.args), prompt), {
23
73
  cwd,
24
74
  stdio: ['ignore', 'pipe', 'pipe'],
25
75
  });
@@ -63,10 +113,17 @@ export class HermesAdapter {
63
113
  return reject(new Error('aborted'));
64
114
  const out = Buffer.concat(stdoutChunks).toString('utf8');
65
115
  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)}`));
116
+ const stdout = out.trim();
117
+ const stderr = err.trim();
118
+ if (code !== 0 && stdout.length === 0) {
119
+ const detail = stderr.length > 0 ? stderr.slice(0, 400) : 'no stderr';
120
+ return reject(new Error(`hermes exit ${code}: ${detail}`));
121
+ }
122
+ const reply = extractHermesReply(stdout || stderr);
123
+ if (!reply) {
124
+ return reject(new Error('hermes produced empty output'));
68
125
  }
69
- resolve(out.trim() || err.trim());
126
+ resolve(reply);
70
127
  });
71
128
  });
72
129
  }
@@ -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
@@ -1,6 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ import { pathToFileURL } from 'node:url';
2
3
  import { main } from './index.js';
3
- main().catch((err) => {
4
- console.error('fatal:', err.message);
5
- process.exit(1);
6
- });
4
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
5
+ main().catch((err) => {
6
+ console.error('fatal:', err.message);
7
+ process.exit(1);
8
+ });
9
+ }
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) => {
@@ -125,6 +201,9 @@ export function createDeviceDaemon(cfg, agents) {
125
201
  networkId: cfg.networkId,
126
202
  agents: publicAgents,
127
203
  systemInfo,
204
+ capabilities: {
205
+ customAgentDispatch: true,
206
+ },
128
207
  },
129
208
  transports: ['websocket'],
130
209
  reconnection: true,
@@ -145,12 +224,46 @@ export function createDeviceDaemon(cfg, agents) {
145
224
  heartbeatTimer = setInterval(() => {
146
225
  socket?.emit('heartbeat');
147
226
  }, cfg.heartbeatIntervalMs);
227
+ // Periodic re-scan to update agent availability
228
+ if (rescanTimer)
229
+ clearInterval(rescanTimer);
230
+ rescanTimer = setInterval(() => {
231
+ if (!socket?.connected)
232
+ return;
233
+ scanAndRegister(socket, false);
234
+ }, RESCAN_INTERVAL_MS);
148
235
  });
149
236
  socket.on('connect_error', (err) => {
150
237
  logger.error({ err: err.message }, 'connect_error');
151
238
  });
152
239
  socket.on('dispatch', (req) => {
153
- const agent = agents.get(req.agentId);
240
+ let agent = agents.get(req.agentId);
241
+ if (!agent && req.customAgent) {
242
+ const custom = req.customAgent;
243
+ const entry = {
244
+ id: custom.id,
245
+ name: custom.name,
246
+ role: custom.role ?? 'executor-agent',
247
+ category: 'executor-hosted',
248
+ adapter: {
249
+ kind: custom.adapterKind,
250
+ command: custom.command,
251
+ args: custom.args ?? [],
252
+ cwd: custom.cwd ?? undefined,
253
+ workspace: custom.cwd ?? undefined,
254
+ systemPrompt: custom.description ?? undefined,
255
+ },
256
+ visibility: 'public',
257
+ };
258
+ try {
259
+ agent = new AgentInstance(entry, pickAdapter(entry.adapter));
260
+ agents.set(req.agentId, agent);
261
+ logger.info({ agentId: req.agentId, kind: entry.adapter.kind, cwd: entry.adapter.cwd }, 'custom agent instance created for dispatch');
262
+ }
263
+ catch (err) {
264
+ logger.warn({ agentId: req.agentId, err: errorMessage(err) }, 'failed to create custom dispatch agent');
265
+ }
266
+ }
154
267
  if (!agent) {
155
268
  logger.warn({ agentId: req.agentId, requestId: req.requestId }, 'dispatch for unknown agent');
156
269
  socket?.emit('error_event', {
@@ -178,11 +291,12 @@ export function createDeviceDaemon(cfg, agents) {
178
291
  networkId: cfg.networkId,
179
292
  });
180
293
  }).catch((err) => {
181
- logger.error({ err: err?.message, agentId: req.agentId }, 'dispatch queue error');
294
+ const message = errorMessage(err);
295
+ logger.error({ err: message, agentId: req.agentId }, 'dispatch queue error');
182
296
  currentSocket.emit('error_event', {
183
297
  agentId: req.agentId,
184
298
  at: Date.now(),
185
- message: err?.message ?? 'unknown',
299
+ message,
186
300
  scope: 'reply',
187
301
  requestId: req.requestId,
188
302
  });
@@ -198,6 +312,10 @@ export function createDeviceDaemon(cfg, agents) {
198
312
  clearInterval(heartbeatTimer);
199
313
  heartbeatTimer = null;
200
314
  }
315
+ if (rescanTimer) {
316
+ clearInterval(rescanTimer);
317
+ rescanTimer = null;
318
+ }
201
319
  });
202
320
  },
203
321
  async stop() {
@@ -205,6 +323,10 @@ export function createDeviceDaemon(cfg, agents) {
205
323
  clearInterval(heartbeatTimer);
206
324
  heartbeatTimer = null;
207
325
  }
326
+ if (rescanTimer) {
327
+ clearInterval(rescanTimer);
328
+ rescanTimer = null;
329
+ }
208
330
  socket?.close();
209
331
  socket = null;
210
332
  },
package/dist/index.js CHANGED
@@ -1,50 +1,14 @@
1
1
  import { parseArgs } from 'node:util';
2
+ import { pathToFileURL } from 'node:url';
2
3
  import { loadConfig, loadDeviceConfig } from './config.js';
3
4
  import { createConnection } from './connection.js';
4
5
  import { createDeviceDaemon } from './device-daemon.js';
5
6
  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';
7
+ import { pickAdapter } from './adapters/factory.js';
10
8
  import { logger } from './log.js';
11
9
  import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, getDeviceId } from './scanner.js';
12
10
  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() {
11
+ async function discoverAgents(deviceId) {
48
12
  const [_runtimes, agentos, local] = await Promise.all([
49
13
  scanRuntimes(),
50
14
  scanAgentOSAgents(),
@@ -56,11 +20,10 @@ async function discoverAgents() {
56
20
  if (seen.has(s.command))
57
21
  continue;
58
22
  seen.add(s.command);
59
- const id = s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-');
60
23
  results.push({
61
- id,
24
+ id: s.name.toLowerCase().replace(/[^a-z0-9]+/g, '-'),
62
25
  name: s.name,
63
- role: s.category === 'executor-hosted' ? 'executor-agent' : s.category === 'agentos-hosted' ? 'gateway-agent' : 'standalone-agent',
26
+ role: s.category === 'executor-hosted' ? 'executor-agent' : 'gateway-agent',
64
27
  category: s.category,
65
28
  adapter: {
66
29
  kind: s.adapterKind,
@@ -102,10 +65,6 @@ async function runDeviceMode(cfgPath) {
102
65
  const shouldScan = err.message?.includes('agents array is required');
103
66
  if (!shouldScan)
104
67
  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
68
  let fileSettings = {};
110
69
  try {
111
70
  const { readFileSync } = await import('node:fs');
@@ -119,8 +78,13 @@ async function runDeviceMode(cfgPath) {
119
78
  };
120
79
  }
121
80
  catch { /* ignore */ }
81
+ const deviceId = fileSettings.deviceId ?? process.env.DEVICE_ID ?? await getDeviceId();
82
+ scannedEntries = await discoverAgents(deviceId);
83
+ if (scannedEntries.length === 0) {
84
+ throw new Error('device config missing and no agents discovered via scanning');
85
+ }
122
86
  cfg = {
123
- deviceId: fileSettings.deviceId ?? process.env.DEVICE_ID ?? await getDeviceId(),
87
+ deviceId,
124
88
  networkId: fileSettings.networkId ?? process.env.NETWORK_ID ?? 'default',
125
89
  server: fileSettings.server ?? {
126
90
  url: process.env.SERVER_URL ?? 'http://localhost:3000/agent',
@@ -131,7 +95,7 @@ async function runDeviceMode(cfgPath) {
131
95
  };
132
96
  }
133
97
  if (cfg.scan === true) {
134
- scannedEntries = await discoverAgents();
98
+ scannedEntries = await discoverAgents(cfg.deviceId);
135
99
  if (scannedEntries.length > 0) {
136
100
  cfg = { ...cfg, agents: scannedEntries };
137
101
  }
@@ -171,7 +135,7 @@ Options:
171
135
  --server-url AgentBean Server URL (required)
172
136
  --token Authentication token (required)
173
137
  --device-id Device ID (default: auto-detected from hardware)
174
- --network-id Network ID (default: default)
138
+ --network-id Team ID (default: default)
175
139
  `);
176
140
  process.exit(0);
177
141
  }
@@ -203,7 +167,7 @@ Options:
203
167
  }
204
168
  const deviceId = values['device-id'] ?? await getDeviceId();
205
169
  logger.info({ serverUrl, deviceId, networkId }, 'CLI mode: auto-discovering agents');
206
- const agents = await discoverAgents();
170
+ const agents = await discoverAgents(deviceId);
207
171
  if (agents.length === 0) {
208
172
  logger.warn('no agents discovered on this machine. Daemon will start with no agents.');
209
173
  }
@@ -300,3 +264,9 @@ export async function main() {
300
264
  }
301
265
  }
302
266
  }
267
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
268
+ main().catch((err) => {
269
+ console.error('fatal:', err.message);
270
+ process.exit(1);
271
+ });
272
+ }
package/dist/scanner.js CHANGED
@@ -1,12 +1,38 @@
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
- function which(bin) {
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
+ function isExecutableFile(path) {
8
+ try {
9
+ return existsSync(path) && statSync(path).isFile();
10
+ }
11
+ catch {
12
+ return false;
13
+ }
14
+ }
15
+ function getExtraPathEntries() {
16
+ return [
17
+ '/usr/local/bin',
18
+ '/opt/homebrew/bin',
19
+ join(os.homedir(), '.local/bin'),
20
+ join(os.homedir(), '.bun/bin'),
21
+ join(os.homedir(), '.npm-global/bin'),
22
+ join(os.homedir(), '.asdf/shims'),
23
+ join(os.homedir(), '.local/share/mise/shims'),
24
+ ...getAllNodeVersions().map((version) => join(os.homedir(), '.nvm/versions/node', version, 'bin')),
25
+ ];
26
+ }
27
+ function which(bin, candidatePaths = []) {
8
28
  return new Promise((resolve) => {
9
- const child = execFile('which', [bin], { timeout: 5_000 }, (err, stdout) => {
29
+ for (const candidate of candidatePaths) {
30
+ if (isExecutableFile(candidate)) {
31
+ resolve(candidate);
32
+ return;
33
+ }
34
+ }
35
+ const child = execFile('which', [bin], { timeout: 5_000, env: { ...process.env, PATH: [process.env.PATH, ...getExtraPathEntries()].filter(Boolean).join(':') } }, (err, stdout) => {
10
36
  if (err) {
11
37
  resolve(null);
12
38
  return;
@@ -17,16 +43,51 @@ function which(bin) {
17
43
  child.on('error', () => resolve(null));
18
44
  });
19
45
  }
46
+ function getAllNodeVersions() {
47
+ try {
48
+ const nvmDir = join(os.homedir(), '.nvm/versions/node');
49
+ if (!existsSync(nvmDir))
50
+ return [];
51
+ return readdirSync(nvmDir);
52
+ }
53
+ catch {
54
+ return [];
55
+ }
56
+ }
57
+ function getClaudeCodeCandidates() {
58
+ const latestDir = join(os.homedir(), '.local/share/claude-latest');
59
+ const legacyDir = join(os.homedir(), '.local/share/claude');
60
+ const candidates = [
61
+ join(latestDir, 'current/claude'),
62
+ join(legacyDir, 'current/claude'),
63
+ '/opt/homebrew/lib/node_modules/@anthropic-ai/claude-code/cli.js',
64
+ '/usr/local/lib/node_modules/@anthropic-ai/claude-code/cli.js',
65
+ ];
66
+ for (const base of [latestDir, legacyDir]) {
67
+ const versionsDir = join(base, 'versions');
68
+ try {
69
+ if (!existsSync(versionsDir))
70
+ continue;
71
+ const versions = readdirSync(versionsDir).sort((a, b) => b.localeCompare(a, undefined, { numeric: true }));
72
+ for (const version of versions)
73
+ candidates.push(join(versionsDir, version, 'claude'));
74
+ }
75
+ catch {
76
+ // Ignore unreadable version directories and continue with other candidates.
77
+ }
78
+ }
79
+ return candidates;
80
+ }
20
81
  function run(bin, args) {
21
82
  return new Promise((resolve) => {
22
83
  const child = execFile(bin, args, { timeout: 10_000 }, (err, stdout) => {
23
- resolve(stdout?.trim() ?? '');
84
+ resolve(stdout?.trim() ?? "");
24
85
  });
25
- child.on('error', () => resolve(''));
86
+ child.on("error", () => resolve(""));
26
87
  });
27
88
  }
28
89
  // --- Machine ID (stable per-device identifier) ---
29
- const MACHINE_ID_FILE = join(os.homedir(), '.agentbean', 'device-id');
90
+ const MACHINE_ID_FILE = join(os.homedir(), ".agentbean", "device-id");
30
91
  function getFirstMacAddress() {
31
92
  const ifaces = os.networkInterfaces();
32
93
  for (const [name, addrs] of Object.entries(ifaces)) {
@@ -36,7 +97,7 @@ function getFirstMacAddress() {
36
97
  // Skip internal (loopback) and zero MAC
37
98
  if (addr.internal)
38
99
  continue;
39
- if (addr.mac === '00:00:00:00:00:00')
100
+ if (addr.mac === "00:00:00:00:00:00")
40
101
  continue;
41
102
  return addr.mac;
42
103
  }
@@ -46,19 +107,28 @@ function getFirstMacAddress() {
46
107
  async function readPlatformMachineId() {
47
108
  const platform = os.platform();
48
109
  try {
49
- if (platform === 'linux') {
50
- if (existsSync('/etc/machine-id')) {
51
- return readFileSync('/etc/machine-id', 'utf-8').trim() || null;
110
+ if (platform === "linux") {
111
+ if (existsSync("/etc/machine-id")) {
112
+ return readFileSync("/etc/machine-id", "utf-8").trim() || null;
52
113
  }
53
114
  }
54
- else if (platform === 'darwin') {
55
- const output = await run('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']);
115
+ else if (platform === "darwin") {
116
+ const output = await run("ioreg", [
117
+ "-rd1",
118
+ "-c",
119
+ "IOPlatformExpertDevice",
120
+ ]);
56
121
  const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
57
122
  if (match)
58
123
  return match[1] ?? null;
59
124
  }
60
- else if (platform === 'win32') {
61
- const output = await run('reg', ['query', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid']);
125
+ else if (platform === "win32") {
126
+ const output = await run("reg", [
127
+ "query",
128
+ "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography",
129
+ "/v",
130
+ "MachineGuid",
131
+ ]);
62
132
  const match = output.match(/MachineGuid\s+REG_SZ\s+(\S+)/);
63
133
  if (match)
64
134
  return match[1] ?? null;
@@ -77,7 +147,7 @@ async function readPlatformMachineId() {
77
147
  export async function getDeviceId() {
78
148
  // 1. Read cached ID
79
149
  if (existsSync(MACHINE_ID_FILE)) {
80
- const cached = readFileSync(MACHINE_ID_FILE, 'utf-8').trim();
150
+ const cached = readFileSync(MACHINE_ID_FILE, "utf-8").trim();
81
151
  if (cached)
82
152
  return cached;
83
153
  }
@@ -95,7 +165,7 @@ export async function getDeviceId() {
95
165
  let deviceId;
96
166
  if (parts.length > 2) {
97
167
  // We have enough hardware info — generate deterministic ID
98
- const hash = createHash('sha256').update(parts.join('|')).digest('hex');
168
+ const hash = createHash("sha256").update(parts.join("|")).digest("hex");
99
169
  // Format as UUID: 8-4-4-4-12
100
170
  deviceId = [
101
171
  hash.slice(0, 8),
@@ -103,16 +173,16 @@ export async function getDeviceId() {
103
173
  hash.slice(12, 16),
104
174
  hash.slice(16, 20),
105
175
  hash.slice(20, 32),
106
- ].join('-');
176
+ ].join("-");
107
177
  }
108
178
  else {
109
179
  // Fallback: random UUID
110
- const { randomUUID } = await import('node:crypto');
180
+ const { randomUUID } = await import("node:crypto");
111
181
  deviceId = randomUUID();
112
182
  }
113
183
  // 3. Cache to file
114
184
  try {
115
- const dir = join(os.homedir(), '.agentbean');
185
+ const dir = join(os.homedir(), ".agentbean");
116
186
  if (!existsSync(dir))
117
187
  mkdirSync(dir, { recursive: true });
118
188
  writeFileSync(MACHINE_ID_FILE, deviceId);
@@ -125,19 +195,27 @@ export async function getDeviceId() {
125
195
  // --- Scan Coding Agent Runtimes (Claude Code, Codex, Kimi) ---
126
196
  export async function scanRuntimes() {
127
197
  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' },
198
+ {
199
+ bin: "claude",
200
+ name: "Claude Code",
201
+ adapterKind: "claude-code",
202
+ candidates: getClaudeCodeCandidates(),
203
+ },
204
+ { bin: "codex", name: "Codex CLI", adapterKind: "codex", candidates: [] },
205
+ {
206
+ bin: "kimi-cli",
207
+ name: "Kimi CLI",
208
+ adapterKind: "Kimi-cli",
209
+ candidates: [],
210
+ },
133
211
  ];
134
212
  const results = [];
135
213
  for (const s of checks) {
136
- const path = await which(s.bin);
214
+ const path = await which(s.bin, s.candidates);
137
215
  results.push({
138
216
  name: s.name,
139
217
  adapterKind: s.adapterKind,
140
- command: path ?? '',
218
+ command: path ?? "",
141
219
  installed: path !== null,
142
220
  });
143
221
  }
@@ -145,37 +223,37 @@ export async function scanRuntimes() {
145
223
  }
146
224
  // --- Scan AgentOS Gateways (Hermes, OpenClaw) ---
147
225
  async function checkHermesGateway() {
148
- const path = await which('hermes');
226
+ const path = await which("hermes");
149
227
  if (!path)
150
228
  return null;
151
- const status = await run('hermes', ['gateway', 'status']);
152
- const running = status.includes('running') || status.includes('');
229
+ const status = await run("hermes", ["gateway", "status"]);
230
+ const running = status.includes("running") || status.includes("");
153
231
  if (running) {
154
232
  return {
155
- category: 'agentos-hosted',
156
- name: 'Hermes Agent',
157
- adapterKind: 'hermes',
233
+ category: "agentos-hosted",
234
+ name: "Hermes-Agent",
235
+ adapterKind: "hermes",
158
236
  command: path,
159
- args: ['gateway', 'run'],
160
- source: 'gateway',
237
+ args: [],
238
+ source: "gateway",
161
239
  };
162
240
  }
163
241
  return null;
164
242
  }
165
243
  async function checkOpenClawGateway() {
166
- const path = await which('openclaw');
244
+ const path = await which("openclaw");
167
245
  if (!path)
168
246
  return null;
169
- const status = await run('openclaw', ['gateway', 'status']);
170
- const running = status.includes('running') || status.includes('');
247
+ const status = await run("openclaw", ["gateway", "status"]);
248
+ const running = status.includes("running") || status.includes("");
171
249
  if (running) {
172
250
  return {
173
- category: 'agentos-hosted',
174
- name: 'OpenClaw Agent',
175
- adapterKind: 'openclaw',
251
+ category: "agentos-hosted",
252
+ name: "OpenClaw-Agent",
253
+ adapterKind: "openclaw",
176
254
  command: path,
177
- args: ['gateway', 'run'],
178
- source: 'gateway',
255
+ args: ["gateway", "run"],
256
+ source: "gateway",
179
257
  };
180
258
  }
181
259
  return null;
@@ -188,7 +266,7 @@ export async function scanAgentOSAgents() {
188
266
  return [hermes, openclaw].filter((a) => a !== null);
189
267
  }
190
268
  // --- Scan local agent definitions from filesystem ---
191
- export async function scanLocalAgents(scanDir = join(os.homedir(), '.agentbean', 'agents')) {
269
+ export async function scanLocalAgents(scanDir = join(os.homedir(), ".agentbean", "agents")) {
192
270
  if (!existsSync(scanDir)) {
193
271
  return [];
194
272
  }
@@ -198,7 +276,7 @@ export async function scanLocalAgents(scanDir = join(os.homedir(), '.agentbean',
198
276
  entries = readdirSync(scanDir);
199
277
  }
200
278
  catch (err) {
201
- logger?.warn?.({ err: err?.message }, 'scan failed');
279
+ logger?.warn?.({ err: err?.message }, "scan failed");
202
280
  return [];
203
281
  }
204
282
  for (const entry of entries) {
@@ -212,63 +290,67 @@ export async function scanLocalAgents(scanDir = join(os.homedir(), '.agentbean',
212
290
  }
213
291
  if (!st.isDirectory())
214
292
  continue;
215
- const jsonPath = join(subdir, 'agent.json');
216
- const yamlPath = join(subdir, 'agent.yaml');
217
- const ymlPath = join(subdir, 'agent.yml');
293
+ const jsonPath = join(subdir, "agent.json");
294
+ const yamlPath = join(subdir, "agent.yaml");
295
+ const ymlPath = join(subdir, "agent.yml");
218
296
  let raw = null;
219
297
  let ext = null;
220
298
  if (existsSync(jsonPath)) {
221
- raw = readFileSync(jsonPath, 'utf8');
222
- ext = 'json';
299
+ raw = readFileSync(jsonPath, "utf8");
300
+ ext = "json";
223
301
  }
224
302
  else if (existsSync(yamlPath)) {
225
- raw = readFileSync(yamlPath, 'utf8');
226
- ext = 'yaml';
303
+ raw = readFileSync(yamlPath, "utf8");
304
+ ext = "yaml";
227
305
  }
228
306
  else if (existsSync(ymlPath)) {
229
- raw = readFileSync(ymlPath, 'utf8');
230
- ext = 'yaml';
307
+ raw = readFileSync(ymlPath, "utf8");
308
+ ext = "yaml";
231
309
  }
232
310
  if (raw === null || ext === null)
233
311
  continue;
234
312
  let parsed = null;
235
313
  try {
236
- if (ext === 'json') {
314
+ if (ext === "json") {
237
315
  parsed = JSON.parse(raw);
238
316
  }
239
317
  else {
240
- const { load: parseYaml } = await import('js-yaml');
318
+ const { load: parseYaml } = await import("js-yaml");
241
319
  parsed = parseYaml(raw);
242
320
  }
243
321
  }
244
322
  catch {
245
323
  continue;
246
324
  }
247
- if (!parsed || typeof parsed !== 'object')
325
+ if (!parsed || typeof parsed !== "object")
248
326
  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) : [];
327
+ const name = (typeof parsed.name === "string" ? parsed.name : entry).replace(/\s+/g, "-");
328
+ const command = typeof parsed.command === "string" ? parsed.command : "";
329
+ const args = Array.isArray(parsed.args)
330
+ ? parsed.args.map(String)
331
+ : [];
252
332
  let category;
253
- if (typeof parsed.category === 'string' && ['executor-hosted', 'agentos-hosted', 'standalone-cli'].includes(parsed.category)) {
333
+ if (typeof parsed.category === "string" &&
334
+ ["executor-hosted", "agentos-hosted"].includes(parsed.category)) {
254
335
  category = parsed.category;
255
336
  }
256
- else if ('executor' in parsed) {
257
- category = 'executor-hosted';
337
+ else if ("executor" in parsed) {
338
+ category = "executor-hosted";
258
339
  }
259
340
  else {
260
- category = 'standalone-cli';
341
+ category = "executor-hosted";
261
342
  }
262
- const adapterKind = typeof parsed.adapterKind === 'string' && ['codex', 'claude-code', 'openclaw', 'hermes', 'standalone'].includes(parsed.adapterKind)
343
+ const adapterKind = typeof parsed.adapterKind === "string" &&
344
+ ["codex", "claude-code", "openclaw", "hermes"].includes(parsed.adapterKind)
263
345
  ? parsed.adapterKind
264
- : 'standalone';
346
+ : "codex";
265
347
  results.push({
266
348
  category,
267
349
  name,
268
350
  adapterKind,
269
351
  command,
270
352
  args,
271
- source: 'filesystem',
353
+ source: "filesystem",
272
354
  });
273
355
  }
274
356
  return results;
@@ -279,7 +361,7 @@ export function collectSystemInfo() {
279
361
  const cpus = os.cpus();
280
362
  const platform = os.platform();
281
363
  let osVersion = `${os.type()} ${os.release()}`;
282
- if (platform === 'darwin') {
364
+ if (platform === "darwin") {
283
365
  osVersion = `macOS ${os.release()}`;
284
366
  }
285
367
  return {
@@ -287,10 +369,10 @@ export function collectSystemInfo() {
287
369
  arch: os.arch(),
288
370
  osVersion,
289
371
  hostname: os.hostname(),
290
- cpuModel: cpus[0]?.model ?? 'unknown',
372
+ cpuModel: cpus[0]?.model ?? "unknown",
291
373
  cpuCores: cpus.length,
292
- totalMemoryGB: Math.round(totalMem / 1024 / 1024 / 1024 * 10) / 10,
293
- freeMemoryGB: Math.round(freeMem / 1024 / 1024 / 1024 * 10) / 10,
374
+ totalMemoryGB: Math.round((totalMem / 1024 / 1024 / 1024) * 10) / 10,
375
+ freeMemoryGB: Math.round((freeMem / 1024 / 1024 / 1024) * 10) / 10,
294
376
  nodeVersion: process.version,
295
377
  };
296
378
  }
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.5",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "build": "tsc",
15
- "dev": "tsx watch src/index.ts",
15
+ "dev": "tsx watch src/bin.ts",
16
16
  "start": "node dist/bin.js",
17
17
  "test": "vitest run",
18
18
  "test:watch": "vitest",