@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 +4 -4
- package/dist/adapters/factory.js +38 -0
- package/dist/adapters/hermes.js +61 -4
- package/dist/adapters/openclaw.js +48 -21
- package/dist/agent-instance.js +16 -2
- package/dist/bin.js +7 -4
- package/dist/config.js +4 -6
- package/dist/connection.js +16 -2
- package/dist/device-daemon.js +152 -30
- package/dist/index.js +20 -50
- package/dist/scanner.js +157 -75
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ npm test # 运行测试
|
|
|
14
14
|
### 带配置文件启动
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
npx tsx src/
|
|
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/
|
|
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
|
+
}
|
package/dist/adapters/hermes.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
67
|
-
|
|
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(
|
|
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
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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(() =>
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
55
|
-
|
|
82
|
+
const reply = stdout || stderr;
|
|
83
|
+
if (!reply) {
|
|
84
|
+
return reject(new Error('openclaw produced empty output'));
|
|
56
85
|
}
|
|
57
|
-
|
|
58
|
-
return reject(new Error(parsed.error));
|
|
59
|
-
resolve((parsed.reply ?? '').trim());
|
|
86
|
+
resolve(reply);
|
|
60
87
|
});
|
|
61
88
|
});
|
|
62
89
|
}
|
package/dist/agent-instance.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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({
|
package/dist/connection.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
101
|
+
message,
|
|
88
102
|
scope: 'reply',
|
|
89
103
|
requestId: req.requestId,
|
|
90
104
|
});
|
package/dist/device-daemon.js
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
97
|
+
agents.push({ ...ag, source: 'scanned' });
|
|
61
98
|
}
|
|
62
99
|
}
|
|
63
|
-
return
|
|
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,
|
|
76
|
-
if (
|
|
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
|
-
|
|
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(
|
|
97
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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' :
|
|
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
|
|
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
|
|
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
|
|
2
|
-
import { readdirSync, readFileSync, statSync, existsSync, writeFileSync, mkdirSync } from
|
|
3
|
-
import { join } from
|
|
4
|
-
import { createHash } from
|
|
5
|
-
import * as os from
|
|
6
|
-
import { logger } from
|
|
7
|
-
function
|
|
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
|
-
|
|
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(
|
|
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(),
|
|
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 ===
|
|
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 ===
|
|
50
|
-
if (existsSync(
|
|
51
|
-
return readFileSync(
|
|
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 ===
|
|
55
|
-
const output = await run(
|
|
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 ===
|
|
61
|
-
const output = await run(
|
|
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,
|
|
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(
|
|
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(
|
|
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(),
|
|
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
|
-
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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(
|
|
226
|
+
const path = await which("hermes");
|
|
149
227
|
if (!path)
|
|
150
228
|
return null;
|
|
151
|
-
const status = await run(
|
|
152
|
-
const 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:
|
|
156
|
-
name:
|
|
157
|
-
adapterKind:
|
|
233
|
+
category: "agentos-hosted",
|
|
234
|
+
name: "Hermes-Agent",
|
|
235
|
+
adapterKind: "hermes",
|
|
158
236
|
command: path,
|
|
159
|
-
args: [
|
|
160
|
-
source:
|
|
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(
|
|
244
|
+
const path = await which("openclaw");
|
|
167
245
|
if (!path)
|
|
168
246
|
return null;
|
|
169
|
-
const status = await run(
|
|
170
|
-
const 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:
|
|
174
|
-
name:
|
|
175
|
-
adapterKind:
|
|
251
|
+
category: "agentos-hosted",
|
|
252
|
+
name: "OpenClaw-Agent",
|
|
253
|
+
adapterKind: "openclaw",
|
|
176
254
|
command: path,
|
|
177
|
-
args: [
|
|
178
|
-
source:
|
|
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(),
|
|
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 },
|
|
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,
|
|
216
|
-
const yamlPath = join(subdir,
|
|
217
|
-
const ymlPath = join(subdir,
|
|
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,
|
|
222
|
-
ext =
|
|
299
|
+
raw = readFileSync(jsonPath, "utf8");
|
|
300
|
+
ext = "json";
|
|
223
301
|
}
|
|
224
302
|
else if (existsSync(yamlPath)) {
|
|
225
|
-
raw = readFileSync(yamlPath,
|
|
226
|
-
ext =
|
|
303
|
+
raw = readFileSync(yamlPath, "utf8");
|
|
304
|
+
ext = "yaml";
|
|
227
305
|
}
|
|
228
306
|
else if (existsSync(ymlPath)) {
|
|
229
|
-
raw = readFileSync(ymlPath,
|
|
230
|
-
ext =
|
|
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 ===
|
|
314
|
+
if (ext === "json") {
|
|
237
315
|
parsed = JSON.parse(raw);
|
|
238
316
|
}
|
|
239
317
|
else {
|
|
240
|
-
const { load: parseYaml } = await import(
|
|
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 !==
|
|
325
|
+
if (!parsed || typeof parsed !== "object")
|
|
248
326
|
continue;
|
|
249
|
-
const name = typeof parsed.name ===
|
|
250
|
-
const command = typeof parsed.command ===
|
|
251
|
-
const args = Array.isArray(parsed.args)
|
|
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 ===
|
|
333
|
+
if (typeof parsed.category === "string" &&
|
|
334
|
+
["executor-hosted", "agentos-hosted"].includes(parsed.category)) {
|
|
254
335
|
category = parsed.category;
|
|
255
336
|
}
|
|
256
|
-
else if (
|
|
257
|
-
category =
|
|
337
|
+
else if ("executor" in parsed) {
|
|
338
|
+
category = "executor-hosted";
|
|
258
339
|
}
|
|
259
340
|
else {
|
|
260
|
-
category =
|
|
341
|
+
category = "executor-hosted";
|
|
261
342
|
}
|
|
262
|
-
const adapterKind = typeof parsed.adapterKind ===
|
|
343
|
+
const adapterKind = typeof parsed.adapterKind === "string" &&
|
|
344
|
+
["codex", "claude-code", "openclaw", "hermes"].includes(parsed.adapterKind)
|
|
263
345
|
? parsed.adapterKind
|
|
264
|
-
:
|
|
346
|
+
: "codex";
|
|
265
347
|
results.push({
|
|
266
348
|
category,
|
|
267
349
|
name,
|
|
268
350
|
adapterKind,
|
|
269
351
|
command,
|
|
270
352
|
args,
|
|
271
|
-
source:
|
|
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 ===
|
|
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 ??
|
|
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.
|
|
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/
|
|
15
|
+
"dev": "tsx watch src/bin.ts",
|
|
16
16
|
"start": "node dist/bin.js",
|
|
17
17
|
"test": "vitest run",
|
|
18
18
|
"test:watch": "vitest",
|