@agentbean/daemon 0.1.33 → 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.
@@ -27,11 +27,22 @@ function buildPrompt(input, systemPrompt) {
27
27
  }
28
28
  const ANSI_RE = /\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g;
29
29
  const BOX_ONLY_RE = /^[\s─━═╭╮╰╯│┃┌┐└┘├┤┬┴┼]+$/;
30
+ function stripEchoedQueryPreamble(lines) {
31
+ const initIdx = lines.findIndex((line) => {
32
+ const trimmed = line.trim();
33
+ return trimmed === 'Initializing agent...' || trimmed === 'Initializing agent…';
34
+ });
35
+ if (initIdx < 0)
36
+ return lines;
37
+ if (!lines.slice(0, initIdx).some((line) => line.trim().startsWith('Query:')))
38
+ return lines;
39
+ return lines.slice(initIdx + 1);
40
+ }
30
41
  export function extractHermesReply(output) {
31
- const lines = output
42
+ const lines = stripEchoedQueryPreamble(output
32
43
  .replace(ANSI_RE, '')
33
44
  .replace(/\r\n?/g, '\n')
34
- .split('\n');
45
+ .split('\n'));
35
46
  let boxStart = -1;
36
47
  for (let i = lines.length - 1; i >= 0; i -= 1) {
37
48
  if (lines[i]?.trim().startsWith('╭')) {
@@ -106,7 +106,7 @@ export class AgentInstance {
106
106
  systemPrompt: this.config.adapter.systemPrompt,
107
107
  workspace: projectWorkspace,
108
108
  sandboxProfilePath: req.sandboxed && isSandboxAvailable()
109
- ? generateSandboxProfile(this.id, this.config.adapter.command)
109
+ ? generateSandboxProfile(this.id, this.config.adapter.command, [run.runDir])
110
110
  : undefined,
111
111
  env: { ...(this.config.adapter.env ?? {}), ...workspaceEnv(run) },
112
112
  }, ctl.signal);
@@ -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,
@@ -205,6 +204,8 @@ export function createDeviceSocketOptions(input) {
205
204
  directoryPicker: true,
206
205
  },
207
206
  },
207
+ transports: ['websocket', 'polling'],
208
+ rememberUpgrade: true,
208
209
  reconnection: true,
209
210
  reconnectionDelay: 1_000,
210
211
  reconnectionDelayMax: 10_000,
@@ -296,13 +297,13 @@ export function createDeviceDaemon(cfg, agents) {
296
297
  }
297
298
  async function scanAndRegister(sock, useCache) {
298
299
  if (useCache) {
299
- const cached = loadCache();
300
+ const cached = loadCache(cfg.profileId);
300
301
  if (cached) {
301
302
  logger.info({ count: cached.agents.length + cached.runtimes.length }, 'using cached scan results');
302
303
  emitRegister(sock, cached);
303
304
  // Background refresh — only emit if results differ
304
305
  scanAll().then((fresh) => {
305
- saveCache(fresh);
306
+ saveCache(fresh, cfg.profileId);
306
307
  const cachedKey = JSON.stringify([
307
308
  ...cached.agents.map((a) => a.command),
308
309
  ...cached.runtimes.map((rt) => rt.command),
@@ -324,7 +325,7 @@ export function createDeviceDaemon(cfg, agents) {
324
325
  // Full scan (no cache or cache miss)
325
326
  try {
326
327
  const scanned = await scanAll();
327
- saveCache(scanned);
328
+ saveCache(scanned, cfg.profileId);
328
329
  emitRegister(sock, scanned);
329
330
  }
330
331
  catch (err) {
@@ -337,6 +338,8 @@ export function createDeviceDaemon(cfg, agents) {
337
338
  socket = io(agentUrl, createDeviceSocketOptions({
338
339
  token: cfg.server.token,
339
340
  deviceId: cfg.deviceId,
341
+ machineId: cfg.machineId,
342
+ profileId: cfg.profileId,
340
343
  networkId: cfg.networkId,
341
344
  agents: publicAgents,
342
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,37 +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 auth = await runInviteMode(serverUrl, values.invite);
197
+ const machineId = values['device-id'] ?? await getDeviceId();
198
+ const auth = await runInviteMode(serverUrl, values.invite, machineId);
157
199
  serverUrl = auth.serverUrl;
158
200
  token = auth.token;
159
201
  networkId = auth.networkId ?? networkId;
202
+ profileId = profileId ?? profileIdForNetwork(networkId);
203
+ saveAuth(auth, { profileId });
160
204
  }
161
205
  else if (!token) {
162
- savedAuth = loadAuth();
206
+ savedAuth = loadAuth({ profileId });
163
207
  if (savedAuth) {
164
208
  serverUrl = serverUrl ?? savedAuth.serverUrl;
165
209
  token = savedAuth.token;
@@ -182,19 +226,16 @@ Options:
182
226
  savedNetworkId: savedAuth?.networkId,
183
227
  fallbackNetworkId: networkId,
184
228
  });
185
- const deviceId = values['device-id'] ?? await getDeviceId();
186
- logger.info({ serverUrl, deviceId, networkId }, 'CLI mode: auto-discovering agents');
187
- const agents = await discoverAgents(deviceId);
188
- if (agents.length === 0) {
189
- logger.warn('no agents discovered on this machine. Daemon will start with no agents.');
190
- }
191
- const cfg = {
192
- deviceId,
229
+ profileId = profileId ?? profileIdForNetwork(networkId);
230
+ const machineId = values['device-id'] ?? await getDeviceId();
231
+ const cfg = await buildCliDeviceConfig({
232
+ serverUrl,
233
+ token,
193
234
  networkId,
194
- server: { url: serverUrl, token },
195
- heartbeatIntervalMs: 10_000,
196
- agents,
197
- };
235
+ machineId,
236
+ explicitDeviceId: values['device-id'] ? values['device-id'] : undefined,
237
+ profileId,
238
+ });
198
239
  await startDeviceDaemon(cfg);
199
240
  }
200
241
  function normalizeBaseUrl(serverUrl) {
@@ -238,7 +279,7 @@ export function socketErrorMessage(err) {
238
279
  .map((value) => value.trim());
239
280
  return [...new Set(details)].join(': ') || 'unknown socket error';
240
281
  }
241
- async function runInviteMode(serverUrl, inviteCode) {
282
+ async function runInviteMode(serverUrl, inviteCode, deviceId) {
242
283
  const { io } = await import('socket.io-client');
243
284
  const { execFile } = await import('node:child_process');
244
285
  const baseUrl = normalizeBaseUrl(serverUrl);
@@ -265,7 +306,7 @@ async function runInviteMode(serverUrl, inviteCode) {
265
306
  clearTimeout(connectTimer);
266
307
  logger.info('invite mode: connected, validating invite code');
267
308
  console.log('Connected. Validating invite code...');
268
- socket.emit('auth:invite:validate', { code: inviteCode }, (res) => {
309
+ socket.emit('auth:invite:validate', { code: inviteCode, deviceId }, (res) => {
269
310
  if (!res?.ok) {
270
311
  fail(new Error(res?.error ?? 'invalid invite code'));
271
312
  return;
@@ -290,7 +331,6 @@ async function runInviteMode(serverUrl, inviteCode) {
290
331
  userId: payload.userId,
291
332
  networkId: payload.networkId,
292
333
  };
293
- saveAuth(auth);
294
334
  logger.info({ networkId: auth.networkId }, 'invite mode: token received and saved');
295
335
  console.log('Registration complete! Starting daemon...');
296
336
  socket.disconnect();
@@ -300,7 +340,7 @@ async function runInviteMode(serverUrl, inviteCode) {
300
340
  }
301
341
  export async function main() {
302
342
  // Check for CLI flags first (npx mode)
303
- 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');
304
344
  if (hasCliFlags) {
305
345
  await runCliMode();
306
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/sandbox.js CHANGED
@@ -21,16 +21,21 @@ export function isSandboxAvailable() {
21
21
  return false;
22
22
  }
23
23
  }
24
- export function generateSandboxProfile(agentId, runtimePath) {
24
+ export function generateSandboxProfile(agentId, runtimePath, writableDirs = []) {
25
25
  const workspaceDir = getWorkspaceDir(agentId);
26
26
  const runtimeDir = runtimePath.includes('/') ? dirname(runtimePath) : '/usr/bin';
27
27
  const profilePath = `/tmp/agentbean-sandbox-${agentId}.sb`;
28
+ const extraWritableRules = writableDirs
29
+ .filter(Boolean)
30
+ .map((dir) => `(allow file-read* file-write*
31
+ (subpath "${escapeSchemeString(dir)}"))`)
32
+ .join('\n');
28
33
  const profile = `(version 1)
29
34
  (allow file-read* file-write*
30
35
  (subpath "${escapeSchemeString(workspaceDir)}"))
31
36
  (allow file-read* file-write*
32
37
  (subpath "/tmp"))
33
- (allow file-read*
38
+ ${extraWritableRules ? `${extraWritableRules}\n` : ''}(allow file-read*
34
39
  (subpath "${escapeSchemeString(runtimeDir)}"))
35
40
  (allow file-read*
36
41
  (subpath "/bin")
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
  }
@@ -34,6 +34,14 @@ function uniqueDestination(dir, filename) {
34
34
  }
35
35
  return candidate;
36
36
  }
37
+ function fileNamePreference(path) {
38
+ const name = basename(path).toLowerCase();
39
+ if (/^ig_[a-f0-9]{32,}\.(png|jpe?g|gif|webp)$/i.test(name))
40
+ return 0;
41
+ if (/^(image|output|generated)[._-]?\d*\.(png|jpe?g|gif|webp)$/i.test(name))
42
+ return 1;
43
+ return 2;
44
+ }
37
45
  function escapeRegExp(value) {
38
46
  return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
47
  }
@@ -94,12 +102,14 @@ export function workspaceEnv(run) {
94
102
  }
95
103
  export function archiveOutputFiles(run, files) {
96
104
  const archived = [];
97
- const seen = new Set();
105
+ const candidates = new Map();
106
+ const hashOrder = [];
107
+ const seenPaths = new Set();
98
108
  for (const file of files) {
99
109
  const abs = isAbsolute(file) ? file : resolve(file);
100
- if (seen.has(abs))
110
+ if (seenPaths.has(abs))
101
111
  continue;
102
- seen.add(abs);
112
+ seenPaths.add(abs);
103
113
  let st;
104
114
  try {
105
115
  st = statSync(abs);
@@ -109,6 +119,21 @@ export function archiveOutputFiles(run, files) {
109
119
  catch {
110
120
  continue;
111
121
  }
122
+ const hash = fileHash(abs);
123
+ const current = candidates.get(hash);
124
+ if (!current) {
125
+ candidates.set(hash, { abs, hash });
126
+ hashOrder.push(hash);
127
+ }
128
+ else if (fileNamePreference(abs) > fileNamePreference(current.abs)) {
129
+ candidates.set(hash, { abs, hash });
130
+ }
131
+ }
132
+ for (const hash of hashOrder) {
133
+ const candidate = candidates.get(hash);
134
+ if (!candidate)
135
+ continue;
136
+ const abs = candidate.abs;
112
137
  const alreadyInRun = relative(run.runDir, abs);
113
138
  const archivedPath = alreadyInRun && !alreadyInRun.startsWith('..') && !isAbsolute(alreadyInRun)
114
139
  ? abs
@@ -121,7 +146,7 @@ export function archiveOutputFiles(run, files) {
121
146
  archivedPath,
122
147
  relativePath: relative(run.agentDir, archivedPath),
123
148
  pathKind: 'output',
124
- sha256: fileHash(archivedPath),
149
+ sha256: candidate.hash,
125
150
  sizeBytes,
126
151
  });
127
152
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentbean/daemon",
3
3
  "private": false,
4
- "version": "0.1.33",
4
+ "version": "0.1.35",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {