@agentbean/daemon 0.1.34 → 0.1.35

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.
@@ -1,24 +1,44 @@
1
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
2
- import { homedir } from 'node:os';
1
+ import { existsSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
3
2
  import { join } from 'node:path';
4
- const AUTH_DIR = join(homedir(), '.agentbean');
5
- const AUTH_FILE = join(AUTH_DIR, 'auth.json');
6
- export function loadAuth() {
7
- if (!existsSync(AUTH_FILE))
3
+ import { agentbeanHome, authFile, ensureProfileRoot, profileIdForNetwork } from './profile-paths.js';
4
+ export function loadAuth(options = {}) {
5
+ const file = authFile(options.profileId);
6
+ if (!existsSync(file))
8
7
  return null;
9
8
  try {
10
- return JSON.parse(readFileSync(AUTH_FILE, 'utf8'));
9
+ return JSON.parse(readFileSync(file, 'utf8'));
11
10
  }
12
11
  catch {
13
12
  return null;
14
13
  }
15
14
  }
16
- export function saveAuth(data) {
17
- if (!existsSync(AUTH_DIR))
18
- mkdirSync(AUTH_DIR, { recursive: true });
19
- writeFileSync(AUTH_FILE, JSON.stringify(data, null, 2));
15
+ export function saveAuth(data, options = {}) {
16
+ const profileId = options.profileId ?? null;
17
+ ensureProfileRoot(profileId);
18
+ writeFileSync(authFile(profileId), JSON.stringify(data, null, 2));
20
19
  }
21
- export function clearAuth() {
22
- if (existsSync(AUTH_FILE))
23
- unlinkSync(AUTH_FILE);
20
+ export function clearAuth(options = {}) {
21
+ const file = authFile(options.profileId);
22
+ if (existsSync(file))
23
+ unlinkSync(file);
24
+ }
25
+ export function listAuthProfiles() {
26
+ const profiles = [];
27
+ const teamsDir = join(agentbeanHome(), 'teams');
28
+ if (existsSync(teamsDir)) {
29
+ for (const entry of readdirSync(teamsDir, { withFileTypes: true })) {
30
+ if (!entry.isDirectory())
31
+ continue;
32
+ const auth = loadAuth({ profileId: entry.name });
33
+ if (auth?.networkId)
34
+ profiles.push({ ...auth, profileId: entry.name });
35
+ }
36
+ }
37
+ const legacy = loadAuth();
38
+ if (legacy?.networkId) {
39
+ const profileId = profileIdForNetwork(legacy.networkId);
40
+ if (!profiles.some((profile) => profile.profileId === profileId))
41
+ profiles.push({ ...legacy, profileId });
42
+ }
43
+ return profiles;
24
44
  }
@@ -1,14 +1,14 @@
1
1
  import { io } from 'socket.io-client';
2
2
  import { execFile } from 'node:child_process';
3
3
  import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'node:fs';
4
- import { basename, isAbsolute, join } from 'node:path';
5
- import { homedir } from 'node:os';
4
+ import { basename, isAbsolute, dirname } from 'node:path';
6
5
  import { promisify } from 'node:util';
7
6
  import { logger } from './log.js';
8
7
  import { AgentInstance } from './agent-instance.js';
9
8
  import { pickAdapter } from './adapters/factory.js';
10
9
  import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, collectSystemInfo } from './scanner.js';
11
10
  import { syncWorkspaceArtifacts } from './workspace-sync.js';
11
+ import { scanCacheFile } from './profile-paths.js';
12
12
  const execFileAsync = promisify(execFile);
13
13
  function errorMessage(err) {
14
14
  if (err instanceof Error && err.message)
@@ -86,10 +86,6 @@ export function nativeDirectoryPickerCommands(platform = process.platform) {
86
86
  return [{
87
87
  command: 'osascript',
88
88
  args: [
89
- '-e',
90
- 'tell application "Finder" to activate',
91
- '-e',
92
- 'delay 0.2',
93
89
  '-e',
94
90
  'POSIX path of (choose folder with prompt "选择项目目录" default location (path to home folder))',
95
91
  ],
@@ -140,8 +136,6 @@ export async function selectNativeDirectory(commands = nativeDirectoryPickerComm
140
136
  }
141
137
  throw new Error(lastError ? `directory picker command not available: ${errorMessage(lastError)}` : 'directory picker command not available');
142
138
  }
143
- const CACHE_DIR = join(homedir(), '.agentbean');
144
- const CACHE_FILE = join(CACHE_DIR, 'scanned-agents.json');
145
139
  function isRuntimeEntry(entry) {
146
140
  return entry.category === 'executor-hosted' &&
147
141
  ['codex', 'claude-code', 'kimi-cli', 'Kimi-cli'].includes(entry.adapterKind);
@@ -164,11 +158,12 @@ function splitLegacyCache(entries) {
164
158
  }
165
159
  return { agents, runtimes };
166
160
  }
167
- function loadCache() {
161
+ function loadCache(profileId) {
168
162
  try {
169
- if (!existsSync(CACHE_FILE))
163
+ const cacheFile = scanCacheFile(profileId);
164
+ if (!existsSync(cacheFile))
170
165
  return null;
171
- const parsed = JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
166
+ const parsed = JSON.parse(readFileSync(cacheFile, 'utf-8'));
172
167
  if (Array.isArray(parsed))
173
168
  return splitLegacyCache(parsed);
174
169
  return {
@@ -180,11 +175,13 @@ function loadCache() {
180
175
  return null;
181
176
  }
182
177
  }
183
- function saveCache(payload) {
178
+ function saveCache(payload, profileId) {
184
179
  try {
185
- if (!existsSync(CACHE_DIR))
186
- mkdirSync(CACHE_DIR, { recursive: true });
187
- writeFileSync(CACHE_FILE, JSON.stringify(payload, null, 2));
180
+ const cacheFile = scanCacheFile(profileId);
181
+ const cacheDir = dirname(cacheFile);
182
+ if (!existsSync(cacheDir))
183
+ mkdirSync(cacheDir, { recursive: true });
184
+ writeFileSync(cacheFile, JSON.stringify(payload, null, 2));
188
185
  }
189
186
  catch (err) {
190
187
  logger.warn({ err: err?.message }, 'failed to save scan cache');
@@ -195,6 +192,8 @@ export function createDeviceSocketOptions(input) {
195
192
  auth: {
196
193
  token: input.token,
197
194
  deviceId: input.deviceId,
195
+ machineId: input.machineId,
196
+ profileId: input.profileId,
198
197
  networkId: input.networkId,
199
198
  agents: input.agents,
200
199
  systemInfo: input.systemInfo,
@@ -298,13 +297,13 @@ export function createDeviceDaemon(cfg, agents) {
298
297
  }
299
298
  async function scanAndRegister(sock, useCache) {
300
299
  if (useCache) {
301
- const cached = loadCache();
300
+ const cached = loadCache(cfg.profileId);
302
301
  if (cached) {
303
302
  logger.info({ count: cached.agents.length + cached.runtimes.length }, 'using cached scan results');
304
303
  emitRegister(sock, cached);
305
304
  // Background refresh — only emit if results differ
306
305
  scanAll().then((fresh) => {
307
- saveCache(fresh);
306
+ saveCache(fresh, cfg.profileId);
308
307
  const cachedKey = JSON.stringify([
309
308
  ...cached.agents.map((a) => a.command),
310
309
  ...cached.runtimes.map((rt) => rt.command),
@@ -326,7 +325,7 @@ export function createDeviceDaemon(cfg, agents) {
326
325
  // Full scan (no cache or cache miss)
327
326
  try {
328
327
  const scanned = await scanAll();
329
- saveCache(scanned);
328
+ saveCache(scanned, cfg.profileId);
330
329
  emitRegister(sock, scanned);
331
330
  }
332
331
  catch (err) {
@@ -339,6 +338,8 @@ export function createDeviceDaemon(cfg, agents) {
339
338
  socket = io(agentUrl, createDeviceSocketOptions({
340
339
  token: cfg.server.token,
341
340
  deviceId: cfg.deviceId,
341
+ machineId: cfg.machineId,
342
+ profileId: cfg.profileId,
342
343
  networkId: cfg.networkId,
343
344
  agents: publicAgents,
344
345
  systemInfo,
package/dist/index.js CHANGED
@@ -7,16 +7,17 @@ import { AgentInstance } from './agent-instance.js';
7
7
  import { pickAdapter } from './adapters/factory.js';
8
8
  import { logger } from './log.js';
9
9
  import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, getDeviceId } from './scanner.js';
10
- import { loadAuth, saveAuth } from './auth-store.js';
10
+ import { listAuthProfiles, loadAuth, saveAuth } from './auth-store.js';
11
+ import { deviceInstanceId, localAgentsDir, profileIdForNetwork } from './profile-paths.js';
11
12
  export function discoveredAgentId(name, deviceId) {
12
13
  const slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
13
14
  return deviceId ? `scan-${deviceId}-${slug}` : slug;
14
15
  }
15
- async function discoverAgents(deviceId) {
16
+ async function discoverAgents(deviceId, profileId) {
16
17
  const [_runtimes, agentos, local] = await Promise.all([
17
18
  scanRuntimes(),
18
19
  scanAgentOSAgents(),
19
- scanLocalAgents(),
20
+ scanLocalAgents(profileId ? localAgentsDir(profileId) : undefined),
20
21
  ]);
21
22
  const seen = new Set();
22
23
  const results = [];
@@ -60,6 +61,24 @@ async function startDeviceDaemon(cfg) {
60
61
  process.on('SIGINT', () => shutdown('SIGINT'));
61
62
  process.on('SIGTERM', () => shutdown('SIGTERM'));
62
63
  }
64
+ async function buildCliDeviceConfig(input) {
65
+ const machineId = input.machineId ?? await getDeviceId();
66
+ const deviceId = input.explicitDeviceId ?? deviceInstanceId(machineId, input.networkId);
67
+ logger.info({ serverUrl: input.serverUrl, deviceId, machineId, networkId: input.networkId, profileId: input.profileId }, 'CLI mode: auto-discovering agents');
68
+ const agents = await discoverAgents(deviceId, input.profileId);
69
+ if (agents.length === 0) {
70
+ logger.warn('no agents discovered on this machine. Daemon will start with no agents.');
71
+ }
72
+ return {
73
+ deviceId,
74
+ machineId,
75
+ profileId: input.profileId,
76
+ networkId: input.networkId,
77
+ server: { url: input.serverUrl, token: input.token },
78
+ heartbeatIntervalMs: 10_000,
79
+ agents,
80
+ };
81
+ }
63
82
  async function runDeviceMode(cfgPath) {
64
83
  let cfg;
65
84
  let scannedEntries;
@@ -129,38 +148,62 @@ async function runCliMode() {
129
148
  'invite': { type: 'string' },
130
149
  'device-id': { type: 'string' },
131
150
  'network-id': { type: 'string' },
151
+ 'profile': { type: 'string' },
152
+ 'all-profiles': { type: 'boolean' },
132
153
  'help': { type: 'boolean' },
133
154
  },
134
155
  strict: true,
135
156
  });
136
157
  if (values.help) {
137
- console.log(`Usage: agentbean-daemon --server-url <url> --token <token> [--device-id <id>] [--network-id <id>]
158
+ console.log(`Usage: agentbean-daemon --server-url <url> --token <token> [--device-id <id>] [--network-id <id>] [--profile <id>]
138
159
 
139
160
  Options:
140
161
  --server-url AgentBean Server URL (required)
141
162
  --token Authentication token (required)
142
163
  --device-id Device ID (default: auto-detected from hardware)
143
164
  --network-id Team ID (default: default)
165
+ --profile Team profile for local auth/cache isolation
166
+ --all-profiles Start one connection for every saved team profile
144
167
  `);
145
168
  process.exit(0);
146
169
  }
170
+ if (values['all-profiles']) {
171
+ const profiles = listAuthProfiles();
172
+ if (profiles.length === 0) {
173
+ console.error('Error: no saved AgentBean team profiles found.');
174
+ process.exit(1);
175
+ }
176
+ const machineId = values['device-id'] ?? await getDeviceId();
177
+ const configs = await Promise.all(profiles.map((profile) => buildCliDeviceConfig({
178
+ serverUrl: profile.serverUrl,
179
+ token: profile.token,
180
+ networkId: profile.networkId ?? networkIdFromToken(profile.token) ?? 'default',
181
+ machineId,
182
+ profileId: profile.profileId,
183
+ })));
184
+ await Promise.all(configs.map((cfg) => startDeviceDaemon(cfg)));
185
+ return;
186
+ }
147
187
  let serverUrl = values['server-url'] ?? process.env.AGENT_BEAN_SERVER_URL;
148
188
  let token = values['token'] ?? process.env.AGENT_BEAN_AGENT_TOKEN;
149
189
  let savedAuth = null;
150
190
  let networkId = values['network-id'] ?? 'default';
191
+ let profileId = values.profile ?? process.env.AGENTBEAN_PROFILE;
151
192
  if (values.invite) {
152
193
  if (!serverUrl) {
153
194
  console.error('Error: --server-url is required with --invite.');
154
195
  process.exit(1);
155
196
  }
156
- const inviteDeviceId = values['device-id'] ?? await getDeviceId();
157
- const auth = await runInviteMode(serverUrl, values.invite, inviteDeviceId);
197
+ const machineId = values['device-id'] ?? await getDeviceId();
198
+ const auth = await runInviteMode(serverUrl, values.invite, machineId);
158
199
  serverUrl = auth.serverUrl;
159
200
  token = auth.token;
160
201
  networkId = auth.networkId ?? networkId;
202
+ profileId = profileId ?? profileIdForNetwork(networkId);
203
+ saveAuth(auth, { profileId });
161
204
  }
162
205
  else if (!token) {
163
- savedAuth = loadAuth();
206
+ savedAuth = loadAuth({ profileId });
164
207
  if (savedAuth) {
165
208
  serverUrl = serverUrl ?? savedAuth.serverUrl;
166
209
  token = savedAuth.token;
@@ -183,19 +226,16 @@ Options:
183
226
  savedNetworkId: savedAuth?.networkId,
184
227
  fallbackNetworkId: networkId,
185
228
  });
186
- const deviceId = values['device-id'] ?? await getDeviceId();
187
- logger.info({ serverUrl, deviceId, networkId }, 'CLI mode: auto-discovering agents');
188
- const agents = await discoverAgents(deviceId);
189
- if (agents.length === 0) {
190
- logger.warn('no agents discovered on this machine. Daemon will start with no agents.');
191
- }
192
- const cfg = {
193
- deviceId,
229
+ profileId = profileId ?? profileIdForNetwork(networkId);
230
+ const machineId = values['device-id'] ?? await getDeviceId();
231
+ const cfg = await buildCliDeviceConfig({
232
+ serverUrl,
233
+ token,
194
234
  networkId,
195
- server: { url: serverUrl, token },
196
- heartbeatIntervalMs: 10_000,
197
- agents,
198
- };
235
+ machineId,
236
+ explicitDeviceId: values['device-id'] ? values['device-id'] : undefined,
237
+ profileId,
238
+ });
199
239
  await startDeviceDaemon(cfg);
200
240
  }
201
241
  function normalizeBaseUrl(serverUrl) {
@@ -291,7 +331,6 @@ async function runInviteMode(serverUrl, inviteCode, deviceId) {
291
331
  userId: payload.userId,
292
332
  networkId: payload.networkId,
293
333
  };
294
- saveAuth(auth);
295
334
  logger.info({ networkId: auth.networkId }, 'invite mode: token received and saved');
296
335
  console.log('Registration complete! Starting daemon...');
297
336
  socket.disconnect();
@@ -301,7 +340,7 @@ async function runInviteMode(serverUrl, inviteCode, deviceId) {
301
340
  }
302
341
  export async function main() {
303
342
  // Check for CLI flags first (npx mode)
304
- const hasCliFlags = process.argv.some((a) => a === '--server-url' || a === '--token' || a === '--invite' || a === '--help');
343
+ const hasCliFlags = process.argv.some((a) => a === '--server-url' || a === '--token' || a === '--invite' || a === '--profile' || a === '--all-profiles' || a === '--help');
305
344
  if (hasCliFlags) {
306
345
  await runCliMode();
307
346
  return;
@@ -0,0 +1,39 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ export function agentbeanHome() {
6
+ return process.env.AGENTBEAN_HOME?.trim() || join(homedir(), '.agentbean');
7
+ }
8
+ export function profileIdForNetwork(networkId) {
9
+ const source = networkId?.trim() || 'default';
10
+ const slug = source.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
11
+ return slug || 'default';
12
+ }
13
+ export function profileRoot(profileId) {
14
+ const explicitDir = process.env.AGENTBEAN_PROFILE_DIR?.trim();
15
+ if (explicitDir && profileId)
16
+ return explicitDir;
17
+ if (!profileId)
18
+ return agentbeanHome();
19
+ return join(agentbeanHome(), 'teams', profileIdForNetwork(profileId));
20
+ }
21
+ export function ensureProfileRoot(profileId) {
22
+ const root = profileRoot(profileId);
23
+ if (!existsSync(root))
24
+ mkdirSync(root, { recursive: true });
25
+ return root;
26
+ }
27
+ export function authFile(profileId) {
28
+ return join(profileRoot(profileId), 'auth.json');
29
+ }
30
+ export function scanCacheFile(profileId) {
31
+ return join(profileRoot(profileId), 'scanned-agents.json');
32
+ }
33
+ export function localAgentsDir(profileId) {
34
+ return join(profileRoot(profileId), 'agents');
35
+ }
36
+ export function deviceInstanceId(machineId, networkId) {
37
+ const hash = createHash('sha256').update(`${networkId}:${machineId}`).digest('hex');
38
+ return `dev_${hash.slice(0, 24)}`;
39
+ }
package/dist/scanner.js CHANGED
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { createHash } from "node:crypto";
5
5
  import * as os from "node:os";
6
6
  import { logger } from "./log.js";
7
+ import { agentbeanHome, localAgentsDir } from "./profile-paths.js";
7
8
  function readDaemonVersion() {
8
9
  try {
9
10
  const packageJson = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
@@ -126,7 +127,7 @@ export function parseOpenClawAgentId(output) {
126
127
  return null;
127
128
  }
128
129
  // --- Machine ID (stable per-device identifier) ---
129
- const MACHINE_ID_FILE = join(os.homedir(), ".agentbean", "device-id");
130
+ const MACHINE_ID_FILE = () => join(agentbeanHome(), "device-id");
130
131
  function getFirstMacAddress() {
131
132
  const ifaces = os.networkInterfaces();
132
133
  for (const [name, addrs] of Object.entries(ifaces)) {
@@ -185,8 +186,9 @@ async function readPlatformMachineId() {
185
186
  */
186
187
  export async function getDeviceId() {
187
188
  // 1. Read cached ID
188
- if (existsSync(MACHINE_ID_FILE)) {
189
- const cached = readFileSync(MACHINE_ID_FILE, "utf-8").trim();
189
+ const machineIdFile = MACHINE_ID_FILE();
190
+ if (existsSync(machineIdFile)) {
191
+ const cached = readFileSync(machineIdFile, "utf-8").trim();
190
192
  if (cached)
191
193
  return cached;
192
194
  }
@@ -221,10 +223,10 @@ export async function getDeviceId() {
221
223
  }
222
224
  // 3. Cache to file
223
225
  try {
224
- const dir = join(os.homedir(), ".agentbean");
226
+ const dir = agentbeanHome();
225
227
  if (!existsSync(dir))
226
228
  mkdirSync(dir, { recursive: true });
227
- writeFileSync(MACHINE_ID_FILE, deviceId);
229
+ writeFileSync(machineIdFile, deviceId);
228
230
  }
229
231
  catch {
230
232
  // non-fatal
@@ -308,7 +310,7 @@ export async function scanAgentOSAgents() {
308
310
  return [hermes, openclaw].filter((a) => a !== null);
309
311
  }
310
312
  // --- Scan local agent definitions from filesystem ---
311
- export async function scanLocalAgents(scanDir = join(os.homedir(), ".agentbean", "agents")) {
313
+ export async function scanLocalAgents(scanDir = localAgentsDir(process.env.AGENTBEAN_PROFILE)) {
312
314
  if (!existsSync(scanDir)) {
313
315
  return [];
314
316
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentbean/daemon",
3
3
  "private": false,
4
- "version": "0.1.34",
4
+ "version": "0.1.35",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {