@agentbean/daemon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,296 @@
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) {
8
+ return new Promise((resolve) => {
9
+ const child = execFile('which', [bin], { timeout: 5_000 }, (err, stdout) => {
10
+ if (err) {
11
+ resolve(null);
12
+ return;
13
+ }
14
+ const path = stdout.trim();
15
+ resolve(path.length > 0 ? path : null);
16
+ });
17
+ child.on('error', () => resolve(null));
18
+ });
19
+ }
20
+ function run(bin, args) {
21
+ return new Promise((resolve) => {
22
+ const child = execFile(bin, args, { timeout: 10_000 }, (err, stdout) => {
23
+ resolve(stdout?.trim() ?? '');
24
+ });
25
+ child.on('error', () => resolve(''));
26
+ });
27
+ }
28
+ // --- Machine ID (stable per-device identifier) ---
29
+ const MACHINE_ID_FILE = join(os.homedir(), '.agentbean', 'device-id');
30
+ function getFirstMacAddress() {
31
+ const ifaces = os.networkInterfaces();
32
+ for (const [name, addrs] of Object.entries(ifaces)) {
33
+ if (!addrs)
34
+ continue;
35
+ for (const addr of addrs) {
36
+ // Skip internal (loopback) and zero MAC
37
+ if (addr.internal)
38
+ continue;
39
+ if (addr.mac === '00:00:00:00:00:00')
40
+ continue;
41
+ return addr.mac;
42
+ }
43
+ }
44
+ return null;
45
+ }
46
+ async function readPlatformMachineId() {
47
+ const platform = os.platform();
48
+ try {
49
+ if (platform === 'linux') {
50
+ if (existsSync('/etc/machine-id')) {
51
+ return readFileSync('/etc/machine-id', 'utf-8').trim() || null;
52
+ }
53
+ }
54
+ else if (platform === 'darwin') {
55
+ const output = await run('ioreg', ['-rd1', '-c', 'IOPlatformExpertDevice']);
56
+ const match = output.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
57
+ if (match)
58
+ return match[1] ?? null;
59
+ }
60
+ else if (platform === 'win32') {
61
+ const output = await run('reg', ['query', 'HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Cryptography', '/v', 'MachineGuid']);
62
+ const match = output.match(/MachineGuid\s+REG_SZ\s+(\S+)/);
63
+ if (match)
64
+ return match[1] ?? null;
65
+ }
66
+ }
67
+ catch {
68
+ // fall through
69
+ }
70
+ return null;
71
+ }
72
+ /**
73
+ * Get a stable device ID unique to this machine.
74
+ * Priority: cached file > platform machine-id > MAC address > random UUID
75
+ * Result is cached to ~/.agentbean/device-id
76
+ */
77
+ export async function getDeviceId() {
78
+ // 1. Read cached ID
79
+ if (existsSync(MACHINE_ID_FILE)) {
80
+ const cached = readFileSync(MACHINE_ID_FILE, 'utf-8').trim();
81
+ if (cached)
82
+ return cached;
83
+ }
84
+ // 2. Collect hardware fingerprint
85
+ const parts = [];
86
+ const platformId = await readPlatformMachineId();
87
+ if (platformId)
88
+ parts.push(`platform:${platformId}`);
89
+ const mac = getFirstMacAddress();
90
+ if (mac)
91
+ parts.push(`mac:${mac}`);
92
+ parts.push(`hostname:${os.hostname()}`);
93
+ parts.push(`arch:${os.arch()}`);
94
+ parts.push(`platform:${os.platform()}`);
95
+ let deviceId;
96
+ if (parts.length > 2) {
97
+ // We have enough hardware info — generate deterministic ID
98
+ const hash = createHash('sha256').update(parts.join('|')).digest('hex');
99
+ // Format as UUID: 8-4-4-4-12
100
+ deviceId = [
101
+ hash.slice(0, 8),
102
+ hash.slice(8, 12),
103
+ hash.slice(12, 16),
104
+ hash.slice(16, 20),
105
+ hash.slice(20, 32),
106
+ ].join('-');
107
+ }
108
+ else {
109
+ // Fallback: random UUID
110
+ const { randomUUID } = await import('node:crypto');
111
+ deviceId = randomUUID();
112
+ }
113
+ // 3. Cache to file
114
+ try {
115
+ const dir = join(os.homedir(), '.agentbean');
116
+ if (!existsSync(dir))
117
+ mkdirSync(dir, { recursive: true });
118
+ writeFileSync(MACHINE_ID_FILE, deviceId);
119
+ }
120
+ catch {
121
+ // non-fatal
122
+ }
123
+ return deviceId;
124
+ }
125
+ // --- Scan Coding Agent Runtimes (Claude Code, Codex, Kimi) ---
126
+ export async function scanRuntimes() {
127
+ 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' },
133
+ ];
134
+ const results = [];
135
+ for (const s of checks) {
136
+ const path = await which(s.bin);
137
+ results.push({
138
+ name: s.name,
139
+ adapterKind: s.adapterKind,
140
+ command: path ?? '',
141
+ installed: path !== null,
142
+ });
143
+ }
144
+ return results;
145
+ }
146
+ // --- Scan AgentOS Gateways (Hermes, OpenClaw) ---
147
+ async function checkHermesGateway() {
148
+ const path = await which('hermes');
149
+ if (!path)
150
+ return null;
151
+ const status = await run('hermes', ['gateway', 'status']);
152
+ const running = status.includes('running') || status.includes('✓');
153
+ if (running) {
154
+ return {
155
+ category: 'agentos-hosted',
156
+ name: 'Hermes Agent',
157
+ adapterKind: 'hermes',
158
+ command: path,
159
+ args: ['gateway', 'run'],
160
+ source: 'gateway',
161
+ };
162
+ }
163
+ return null;
164
+ }
165
+ async function checkOpenClawGateway() {
166
+ const path = await which('openclaw');
167
+ if (!path)
168
+ return null;
169
+ const status = await run('openclaw', ['gateway', 'status']);
170
+ const running = status.includes('running') || status.includes('✓');
171
+ if (running) {
172
+ return {
173
+ category: 'agentos-hosted',
174
+ name: 'OpenClaw Agent',
175
+ adapterKind: 'openclaw',
176
+ command: path,
177
+ args: ['gateway', 'run'],
178
+ source: 'gateway',
179
+ };
180
+ }
181
+ return null;
182
+ }
183
+ export async function scanAgentOSAgents() {
184
+ const [hermes, openclaw] = await Promise.all([
185
+ checkHermesGateway(),
186
+ checkOpenClawGateway(),
187
+ ]);
188
+ return [hermes, openclaw].filter((a) => a !== null);
189
+ }
190
+ // --- Scan local agent definitions from filesystem ---
191
+ export async function scanLocalAgents(scanDir = join(os.homedir(), '.agentbean', 'agents')) {
192
+ if (!existsSync(scanDir)) {
193
+ return [];
194
+ }
195
+ const results = [];
196
+ let entries;
197
+ try {
198
+ entries = readdirSync(scanDir);
199
+ }
200
+ catch (err) {
201
+ logger?.warn?.({ err: err?.message }, 'scan failed');
202
+ return [];
203
+ }
204
+ for (const entry of entries) {
205
+ const subdir = join(scanDir, entry);
206
+ let st;
207
+ try {
208
+ st = statSync(subdir);
209
+ }
210
+ catch {
211
+ continue;
212
+ }
213
+ if (!st.isDirectory())
214
+ continue;
215
+ const jsonPath = join(subdir, 'agent.json');
216
+ const yamlPath = join(subdir, 'agent.yaml');
217
+ const ymlPath = join(subdir, 'agent.yml');
218
+ let raw = null;
219
+ let ext = null;
220
+ if (existsSync(jsonPath)) {
221
+ raw = readFileSync(jsonPath, 'utf8');
222
+ ext = 'json';
223
+ }
224
+ else if (existsSync(yamlPath)) {
225
+ raw = readFileSync(yamlPath, 'utf8');
226
+ ext = 'yaml';
227
+ }
228
+ else if (existsSync(ymlPath)) {
229
+ raw = readFileSync(ymlPath, 'utf8');
230
+ ext = 'yaml';
231
+ }
232
+ if (raw === null || ext === null)
233
+ continue;
234
+ let parsed = null;
235
+ try {
236
+ if (ext === 'json') {
237
+ parsed = JSON.parse(raw);
238
+ }
239
+ else {
240
+ const { load: parseYaml } = await import('js-yaml');
241
+ parsed = parseYaml(raw);
242
+ }
243
+ }
244
+ catch {
245
+ continue;
246
+ }
247
+ if (!parsed || typeof parsed !== 'object')
248
+ 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) : [];
252
+ let category;
253
+ if (typeof parsed.category === 'string' && ['executor-hosted', 'agentos-hosted', 'standalone-cli'].includes(parsed.category)) {
254
+ category = parsed.category;
255
+ }
256
+ else if ('executor' in parsed) {
257
+ category = 'executor-hosted';
258
+ }
259
+ else {
260
+ category = 'standalone-cli';
261
+ }
262
+ const adapterKind = typeof parsed.adapterKind === 'string' && ['codex', 'claude-code', 'openclaw', 'hermes', 'standalone'].includes(parsed.adapterKind)
263
+ ? parsed.adapterKind
264
+ : 'standalone';
265
+ results.push({
266
+ category,
267
+ name,
268
+ adapterKind,
269
+ command,
270
+ args,
271
+ source: 'filesystem',
272
+ });
273
+ }
274
+ return results;
275
+ }
276
+ export function collectSystemInfo() {
277
+ const totalMem = os.totalmem();
278
+ const freeMem = os.freemem();
279
+ const cpus = os.cpus();
280
+ const platform = os.platform();
281
+ let osVersion = `${os.type()} ${os.release()}`;
282
+ if (platform === 'darwin') {
283
+ osVersion = `macOS ${os.release()}`;
284
+ }
285
+ return {
286
+ platform,
287
+ arch: os.arch(),
288
+ osVersion,
289
+ hostname: os.hostname(),
290
+ cpuModel: cpus[0]?.model ?? 'unknown',
291
+ cpuCores: cpus.length,
292
+ totalMemoryGB: Math.round(totalMem / 1024 / 1024 / 1024 * 10) / 10,
293
+ freeMemoryGB: Math.round(freeMem / 1024 / 1024 / 1024 * 10) / 10,
294
+ nodeVersion: process.version,
295
+ };
296
+ }
@@ -0,0 +1,46 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { basename } from 'node:path';
3
+ import { logger } from './log.js';
4
+ export async function uploadArtifact(input) {
5
+ const { serverUrl, token, networkId, filePath, channelId, uploaderId, metaJson } = input;
6
+ const filename = basename(filePath);
7
+ let buffer;
8
+ let size;
9
+ try {
10
+ const st = statSync(filePath);
11
+ size = st.size;
12
+ buffer = readFileSync(filePath);
13
+ }
14
+ catch (err) {
15
+ logger.warn({ err: err.message, filePath }, 'artifact read failed');
16
+ return null;
17
+ }
18
+ // Node Buffer is a Uint8Array; cast to satisfy strict DOM types
19
+ const blob = new Blob([buffer]);
20
+ const form = new FormData();
21
+ form.append('channelId', channelId);
22
+ form.append('file', blob, filename);
23
+ if (uploaderId)
24
+ form.append('uploaderId', uploaderId);
25
+ if (metaJson)
26
+ form.append('metaJson', metaJson);
27
+ try {
28
+ const resp = await fetch(`${serverUrl}/api/networks/${networkId}/artifacts/upload`, {
29
+ method: 'POST',
30
+ headers: { Authorization: `Bearer ${token}` },
31
+ body: form,
32
+ });
33
+ if (!resp.ok) {
34
+ const text = await resp.text();
35
+ logger.warn({ status: resp.status, body: text, filePath }, 'artifact upload rejected');
36
+ return null;
37
+ }
38
+ const result = (await resp.json());
39
+ logger.info({ id: result.id, filename, sizeBytes: size }, 'artifact uploaded');
40
+ return result;
41
+ }
42
+ catch (err) {
43
+ logger.warn({ err: err.message, filePath }, 'artifact upload failed');
44
+ return null;
45
+ }
46
+ }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@agentbean/daemon",
3
+ "private": false,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "bin": {
7
+ "agentbean-daemon": "./dist/bin.js",
8
+ "@agentbean/daemon": "./dist/bin.js"
9
+ },
10
+ "files": [
11
+ "dist/**/*"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "dev": "tsx watch src/index.ts",
16
+ "start": "node dist/bin.js",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
19
+ "prepublishOnly": "npm run build"
20
+ },
21
+ "dependencies": {
22
+ "js-yaml": "^4.1.0",
23
+ "node-pty": "^1.1.0",
24
+ "pino": "^9.4.0",
25
+ "pino-pretty": "^11.2.2",
26
+ "socket.io-client": "^4.7.5",
27
+ "ulid": "^2.3.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/js-yaml": "^4.0.9",
31
+ "@types/node": "^20.16.5",
32
+ "tsx": "^4.19.0",
33
+ "typescript": "^5.6.2",
34
+ "vitest": "^2.0.5"
35
+ }
36
+ }