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