@agentbean/daemon 0.1.9 → 0.1.11

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.
@@ -32,6 +32,7 @@ export class ClaudeCodeAdapter {
32
32
  const child = spawn(command, args, {
33
33
  cwd,
34
34
  stdio: ['pipe', 'pipe', 'pipe'],
35
+ env: { ...process.env, ...(input.env ?? {}) },
35
36
  });
36
37
  const stdoutChunks = [];
37
38
  const stderrChunks = [];
@@ -57,7 +57,7 @@ export class CodexAdapter {
57
57
  cols: 80,
58
58
  rows: 30,
59
59
  cwd,
60
- env: process.env,
60
+ env: { ...process.env, ...(input.env ?? {}) },
61
61
  });
62
62
  const chunks = [];
63
63
  let finished = false;
@@ -72,6 +72,7 @@ export class HermesAdapter {
72
72
  const child = spawn(this.opts.command, buildArgs(runtimeArgs(this.opts.args), prompt), {
73
73
  cwd,
74
74
  stdio: ['ignore', 'pipe', 'pipe'],
75
+ env: { ...process.env, ...(input.env ?? {}) },
75
76
  });
76
77
  const stdoutChunks = [];
77
78
  const stderrChunks = [];
@@ -32,6 +32,7 @@ export class OpenClawAdapter {
32
32
  const child = spawn(this.opts.command, buildArgs(this.opts.args ?? [], prompt), {
33
33
  cwd,
34
34
  stdio: ['ignore', 'pipe', 'pipe'],
35
+ env: { ...process.env, ...(input.env ?? {}) },
35
36
  });
36
37
  const stdoutChunks = [];
37
38
  const stderrChunks = [];
@@ -2,6 +2,7 @@ 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
+ import { archiveOutputFiles, beginAgentWorkspaceRun, finishAgentWorkspaceRun, workspaceEnv, } from './workspace-manager.js';
5
6
  function errorMessage(err) {
6
7
  if (err instanceof Error && err.message)
7
8
  return err.message;
@@ -41,37 +42,65 @@ export class AgentInstance {
41
42
  };
42
43
  }
43
44
  async handleDispatch(opts) {
44
- const { socket, req, serverUrl, token, networkId } = opts;
45
+ const { socket, req, serverUrl, token, networkId, deviceId } = opts;
45
46
  const ctl = new AbortController();
46
47
  const dispatchStart = Date.now();
48
+ const teamId = req.teamId ?? req.networkId ?? networkId;
49
+ const projectWorkspace = req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace;
50
+ const run = beginAgentWorkspaceRun({
51
+ teamId,
52
+ teamName: req.teamName,
53
+ agentId: this.id,
54
+ agentName: this.name,
55
+ runId: req.requestId,
56
+ prompt: req.prompt,
57
+ projectDir: projectWorkspace,
58
+ });
59
+ let archivedFiles = [];
47
60
  try {
48
61
  const rawBody = await this.adapter.ask({
49
62
  prompt: req.prompt,
50
63
  history: req.history ?? [],
51
64
  systemPrompt: this.config.adapter.systemPrompt,
52
- workspace: req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace,
65
+ workspace: projectWorkspace,
53
66
  sandboxProfilePath: req.sandboxed && isSandboxAvailable()
54
67
  ? generateSandboxProfile(this.id, this.config.adapter.command)
55
68
  : undefined,
69
+ env: workspaceEnv(run),
56
70
  }, ctl.signal);
57
- const processed = await postProcess(rawBody, req.sandboxed ? getWorkspaceDir(this.id) : this.config.adapter.workspace, this.adapter.kind, dispatchStart);
71
+ const processed = await postProcess(rawBody, projectWorkspace, this.adapter.kind, dispatchStart, {
72
+ outputDirs: [run.outputDir, run.intermediateDir],
73
+ });
74
+ archivedFiles = archiveOutputFiles(run, processed.outputFiles);
75
+ finishAgentWorkspaceRun(run, { replyText: processed.replyText, files: archivedFiles, status: 'completed' });
58
76
  const artifactIds = [];
59
- if (processed.outputFiles.length > 0) {
60
- for (const filePath of processed.outputFiles) {
77
+ if (archivedFiles.length > 0) {
78
+ for (const file of archivedFiles) {
61
79
  try {
62
80
  const result = await uploadArtifact({
63
81
  serverUrl,
64
82
  token,
65
- networkId,
66
- filePath,
83
+ networkId: teamId,
84
+ filePath: file.archivedPath,
67
85
  channelId: req.channelId,
68
86
  uploaderId: this.id,
87
+ metaJson: JSON.stringify({
88
+ kind: 'agent-workspace-file',
89
+ teamId,
90
+ agentId: this.id,
91
+ runId: req.requestId,
92
+ deviceId: deviceId ?? null,
93
+ pathKind: file.pathKind,
94
+ relativePath: file.relativePath,
95
+ originalPath: file.originalPath,
96
+ sha256: file.sha256,
97
+ }),
69
98
  });
70
99
  if (result)
71
100
  artifactIds.push(result.id);
72
101
  }
73
102
  catch (err) {
74
- logger.warn({ err: err.message, filePath }, 'artifact upload failed');
103
+ logger.warn({ err: err.message, filePath: file.archivedPath }, 'artifact upload failed');
75
104
  }
76
105
  }
77
106
  }
@@ -85,6 +114,7 @@ export class AgentInstance {
85
114
  }
86
115
  catch (err) {
87
116
  const message = errorMessage(err);
117
+ finishAgentWorkspaceRun(run, { files: archivedFiles, status: 'failed', error: message });
88
118
  logger.error({ err: message, requestId: req.requestId, agentId: this.id }, 'dispatch failed');
89
119
  socket.emit('error_event', {
90
120
  agentId: this.id,
@@ -2,6 +2,7 @@ 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
+ import { archiveOutputFiles, beginAgentWorkspaceRun, finishAgentWorkspaceRun, workspaceEnv, } from './workspace-manager.js';
5
6
  function errorMessage(err) {
6
7
  if (err instanceof Error && err.message)
7
8
  return err.message;
@@ -57,32 +58,57 @@ export function createConnection(cfg, adapter) {
57
58
  queue = queue.then(async () => {
58
59
  const ctl = new AbortController();
59
60
  const dispatchStart = Date.now();
61
+ const teamId = 'default';
62
+ const run = beginAgentWorkspaceRun({
63
+ teamId,
64
+ agentId: cfg.id,
65
+ agentName: cfg.name,
66
+ runId: req.requestId,
67
+ prompt: req.prompt,
68
+ projectDir: cfg.adapter.workspace,
69
+ });
70
+ let archivedFiles = [];
60
71
  try {
61
72
  const rawBody = await adapter.ask({
62
73
  prompt: req.prompt,
63
74
  history: req.history ?? [],
64
75
  systemPrompt: cfg.adapter.systemPrompt,
65
76
  workspace: cfg.adapter.workspace,
77
+ env: workspaceEnv(run),
66
78
  }, ctl.signal);
67
- const processed = await postProcess(rawBody, cfg.adapter.workspace, cfg.adapter.kind, dispatchStart);
79
+ const processed = await postProcess(rawBody, cfg.adapter.workspace, cfg.adapter.kind, dispatchStart, {
80
+ outputDirs: [run.outputDir, run.intermediateDir],
81
+ });
82
+ archivedFiles = archiveOutputFiles(run, processed.outputFiles);
83
+ finishAgentWorkspaceRun(run, { replyText: processed.replyText, files: archivedFiles, status: 'completed' });
68
84
  const artifactIds = [];
69
- if (processed.outputFiles.length > 0) {
85
+ if (archivedFiles.length > 0) {
70
86
  const httpBase = cfg.server.url.replace(/\/agent$/, '');
71
- for (const filePath of processed.outputFiles) {
87
+ for (const file of archivedFiles) {
72
88
  try {
73
89
  const result = await uploadArtifact({
74
90
  serverUrl: httpBase,
75
91
  token: cfg.server.token,
76
- networkId: 'default',
77
- filePath,
92
+ networkId: teamId,
93
+ filePath: file.archivedPath,
78
94
  channelId: req.channelId,
79
95
  uploaderId: cfg.id,
96
+ metaJson: JSON.stringify({
97
+ kind: 'agent-workspace-file',
98
+ teamId,
99
+ agentId: cfg.id,
100
+ runId: req.requestId,
101
+ pathKind: file.pathKind,
102
+ relativePath: file.relativePath,
103
+ originalPath: file.originalPath,
104
+ sha256: file.sha256,
105
+ }),
80
106
  });
81
107
  if (result)
82
108
  artifactIds.push(result.id);
83
109
  }
84
110
  catch (err) {
85
- logger.warn({ err: err.message, filePath }, 'artifact upload failed');
111
+ logger.warn({ err: err.message, filePath: file.archivedPath }, 'artifact upload failed');
86
112
  }
87
113
  }
88
114
  }
@@ -95,6 +121,7 @@ export function createConnection(cfg, adapter) {
95
121
  }
96
122
  catch (err) {
97
123
  const message = errorMessage(err);
124
+ finishAgentWorkspaceRun(run, { files: archivedFiles, status: 'failed', error: message });
98
125
  logger.error({ err: message, requestId: req.requestId }, 'dispatch failed');
99
126
  currentSocket.emit('error_event', {
100
127
  at: Date.now(),
@@ -6,6 +6,7 @@ import { logger } from './log.js';
6
6
  import { AgentInstance } from './agent-instance.js';
7
7
  import { pickAdapter } from './adapters/factory.js';
8
8
  import { scanRuntimes, scanAgentOSAgents, scanLocalAgents, collectSystemInfo } from './scanner.js';
9
+ import { syncWorkspaceArtifacts } from './workspace-sync.js';
9
10
  function errorMessage(err) {
10
11
  if (err instanceof Error && err.message)
11
12
  return err.message;
@@ -103,7 +104,9 @@ export function createDeviceDaemon(cfg, agents) {
103
104
  let socket = null;
104
105
  let heartbeatTimer = null;
105
106
  let rescanTimer = null;
107
+ let workspaceSyncTimer = null;
106
108
  const RESCAN_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
109
+ const WORKSPACE_SYNC_INTERVAL_MS = 2 * 60 * 1000;
107
110
  const queues = new Map();
108
111
  const httpBase = cfg.server.url.replace(/\/agent$/, '');
109
112
  let firstConnect = true;
@@ -232,6 +235,14 @@ export function createDeviceDaemon(cfg, agents) {
232
235
  return;
233
236
  scanAndRegister(socket, false);
234
237
  }, RESCAN_INTERVAL_MS);
238
+ syncWorkspaceArtifacts({ serverUrl: httpBase, token: cfg.server.token, networkId: cfg.networkId });
239
+ if (workspaceSyncTimer)
240
+ clearInterval(workspaceSyncTimer);
241
+ workspaceSyncTimer = setInterval(() => {
242
+ if (!socket?.connected)
243
+ return;
244
+ syncWorkspaceArtifacts({ serverUrl: httpBase, token: cfg.server.token, networkId: cfg.networkId });
245
+ }, WORKSPACE_SYNC_INTERVAL_MS);
235
246
  });
236
247
  socket.on('connect_error', (err) => {
237
248
  logger.error({ err: err.message }, 'connect_error');
@@ -288,7 +299,8 @@ export function createDeviceDaemon(cfg, agents) {
288
299
  req,
289
300
  serverUrl: httpBase,
290
301
  token: cfg.server.token,
291
- networkId: cfg.networkId,
302
+ networkId: req.teamId ?? req.networkId ?? cfg.networkId,
303
+ deviceId: cfg.deviceId,
292
304
  });
293
305
  }).catch((err) => {
294
306
  const message = errorMessage(err);
@@ -316,6 +328,10 @@ export function createDeviceDaemon(cfg, agents) {
316
328
  clearInterval(rescanTimer);
317
329
  rescanTimer = null;
318
330
  }
331
+ if (workspaceSyncTimer) {
332
+ clearInterval(workspaceSyncTimer);
333
+ workspaceSyncTimer = null;
334
+ }
319
335
  });
320
336
  },
321
337
  async stop() {
@@ -327,6 +343,10 @@ export function createDeviceDaemon(cfg, agents) {
327
343
  clearInterval(rescanTimer);
328
344
  rescanTimer = null;
329
345
  }
346
+ if (workspaceSyncTimer) {
347
+ clearInterval(workspaceSyncTimer);
348
+ workspaceSyncTimer = null;
349
+ }
330
350
  socket?.close();
331
351
  socket = null;
332
352
  },
@@ -94,9 +94,9 @@ function canonicalPath(path) {
94
94
  return path;
95
95
  }
96
96
  }
97
- function resolveOutputRoots(workspace, outputDirs = []) {
97
+ function resolveOutputRoots(workspace, outputDirs = [], includeWorkspace = false) {
98
98
  const roots = new Set();
99
- if (workspace)
99
+ if (workspace && includeWorkspace)
100
100
  roots.add(resolve(workspace));
101
101
  for (const raw of [...outputDirs, ...outputDirsFromEnv()]) {
102
102
  const expanded = raw.replace(/^~(?=$|\/)/, homedir());
@@ -153,7 +153,7 @@ export async function postProcess(reply, workspace, kind, dispatchStart, options
153
153
  for (const filePath of extractMentionedFiles(reply, workspace, dispatchStart)) {
154
154
  outputFiles.add(canonicalPath(filePath));
155
155
  }
156
- for (const filePath of collectRecentOutputFiles(resolveOutputRoots(workspace, options.outputDirs), dispatchStart)) {
156
+ for (const filePath of collectRecentOutputFiles(resolveOutputRoots(workspace, options.outputDirs, options.scanWorkspace), dispatchStart)) {
157
157
  outputFiles.add(filePath);
158
158
  }
159
159
  // Extract code blocks for logging but do NOT auto-execute (security)
@@ -0,0 +1,134 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
3
+ import { basename, dirname, extname, isAbsolute, join, relative, resolve } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ function rootDir() {
6
+ return resolve(process.env.AGENTBEAN_HOME ?? join(homedir(), '.agentbean'));
7
+ }
8
+ function safeSegment(value) {
9
+ return value.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
10
+ }
11
+ function ensureDir(dir) {
12
+ if (!existsSync(dir))
13
+ mkdirSync(dir, { recursive: true });
14
+ return dir;
15
+ }
16
+ export function getAgentWorkspaceDir(teamId, agentId) {
17
+ return ensureDir(join(rootDir(), 'teams', safeSegment(teamId), 'agents', safeSegment(agentId)));
18
+ }
19
+ function writeJson(path, value) {
20
+ ensureDir(dirname(path));
21
+ writeFileSync(path, JSON.stringify(value, null, 2));
22
+ }
23
+ function fileHash(path) {
24
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
25
+ }
26
+ function uniqueDestination(dir, filename) {
27
+ const ext = extname(filename);
28
+ const stem = ext ? filename.slice(0, -ext.length) : filename;
29
+ let candidate = join(dir, filename);
30
+ let index = 1;
31
+ while (existsSync(candidate)) {
32
+ candidate = join(dir, `${stem}-${index}${ext}`);
33
+ index += 1;
34
+ }
35
+ return candidate;
36
+ }
37
+ export function beginAgentWorkspaceRun(input) {
38
+ const teamId = safeSegment(input.teamId);
39
+ const agentId = safeSegment(input.agentId);
40
+ const runId = safeSegment(input.runId);
41
+ const teamDir = ensureDir(join(rootDir(), 'teams', teamId));
42
+ const agentDir = ensureDir(join(teamDir, 'agents', agentId));
43
+ const runDir = ensureDir(join(agentDir, 'runs', runId));
44
+ const outputDir = ensureDir(join(runDir, 'outputs'));
45
+ const intermediateDir = ensureDir(join(runDir, 'intermediates'));
46
+ const logDir = ensureDir(join(runDir, 'logs'));
47
+ writeJson(join(teamDir, 'team.json'), {
48
+ id: input.teamId,
49
+ name: input.teamName ?? input.teamId,
50
+ updatedAt: new Date().toISOString(),
51
+ });
52
+ writeJson(join(agentDir, 'agent.json'), {
53
+ id: input.agentId,
54
+ name: input.agentName ?? input.agentId,
55
+ projectDir: input.projectDir ?? null,
56
+ updatedAt: new Date().toISOString(),
57
+ });
58
+ writeFileSync(join(runDir, 'prompt.md'), input.prompt);
59
+ writeJson(join(runDir, 'manifest.json'), {
60
+ teamId: input.teamId,
61
+ agentId: input.agentId,
62
+ runId: input.runId,
63
+ status: 'running',
64
+ createdAt: new Date().toISOString(),
65
+ files: [],
66
+ });
67
+ return { teamId: input.teamId, agentId: input.agentId, runId: input.runId, agentDir, runDir, outputDir, intermediateDir, logDir };
68
+ }
69
+ export function workspaceEnv(run) {
70
+ return {
71
+ AGENTBEAN_TEAM_ID: run.teamId,
72
+ AGENTBEAN_AGENT_ID: run.agentId,
73
+ AGENTBEAN_RUN_ID: run.runId,
74
+ AGENTBEAN_WORKSPACE: run.agentDir,
75
+ AGENTBEAN_OUTPUT_DIR: run.outputDir,
76
+ AGENTBEAN_INTERMEDIATE_DIR: run.intermediateDir,
77
+ AGENT_BEAN_OUTPUT_DIRS: [run.outputDir, run.intermediateDir].join(','),
78
+ };
79
+ }
80
+ export function archiveOutputFiles(run, files) {
81
+ const archived = [];
82
+ const seen = new Set();
83
+ for (const file of files) {
84
+ const abs = isAbsolute(file) ? file : resolve(file);
85
+ if (seen.has(abs))
86
+ continue;
87
+ seen.add(abs);
88
+ let st;
89
+ try {
90
+ st = statSync(abs);
91
+ if (!st.isFile())
92
+ continue;
93
+ }
94
+ catch {
95
+ continue;
96
+ }
97
+ const alreadyInRun = relative(run.runDir, abs);
98
+ const archivedPath = alreadyInRun && !alreadyInRun.startsWith('..') && !isAbsolute(alreadyInRun)
99
+ ? abs
100
+ : uniqueDestination(run.outputDir, basename(abs));
101
+ if (archivedPath !== abs)
102
+ copyFileSync(abs, archivedPath);
103
+ const sizeBytes = statSync(archivedPath).size;
104
+ archived.push({
105
+ originalPath: abs,
106
+ archivedPath,
107
+ relativePath: relative(run.agentDir, archivedPath),
108
+ pathKind: 'output',
109
+ sha256: fileHash(archivedPath),
110
+ sizeBytes,
111
+ });
112
+ }
113
+ return archived;
114
+ }
115
+ export function finishAgentWorkspaceRun(run, input) {
116
+ if (input.replyText !== undefined) {
117
+ writeFileSync(join(run.runDir, 'response.md'), input.replyText);
118
+ }
119
+ writeJson(join(run.runDir, 'manifest.json'), {
120
+ teamId: run.teamId,
121
+ agentId: run.agentId,
122
+ runId: run.runId,
123
+ status: input.status,
124
+ updatedAt: new Date().toISOString(),
125
+ error: input.error,
126
+ files: input.files.map((file) => ({
127
+ path: file.relativePath,
128
+ sha256: file.sha256,
129
+ sizeBytes: file.sizeBytes,
130
+ kind: file.pathKind,
131
+ originalPath: file.originalPath,
132
+ })),
133
+ });
134
+ }
@@ -0,0 +1,69 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, isAbsolute, join, normalize } from 'node:path';
4
+ import { logger } from './log.js';
5
+ import { getAgentWorkspaceDir } from './workspace-manager.js';
6
+ function hashFile(path) {
7
+ try {
8
+ return createHash('sha256').update(readFileSync(path)).digest('hex');
9
+ }
10
+ catch {
11
+ return null;
12
+ }
13
+ }
14
+ function safeRelativePath(value) {
15
+ const normalized = normalize(value).replace(/^(\.\.(\/|\\|$))+/, '');
16
+ if (!normalized || isAbsolute(normalized) || normalized.startsWith('..'))
17
+ return null;
18
+ return normalized;
19
+ }
20
+ export async function syncWorkspaceArtifacts(input) {
21
+ const base = input.serverUrl.replace(/\/agent$/, '');
22
+ let payload;
23
+ try {
24
+ const resp = await fetch(`${base}/api/networks/${encodeURIComponent(input.networkId)}/workspace`, {
25
+ headers: { Authorization: `Bearer ${input.token}` },
26
+ });
27
+ if (!resp.ok) {
28
+ logger.warn({ status: resp.status, body: await resp.text() }, 'workspace sync manifest rejected');
29
+ return;
30
+ }
31
+ payload = await resp.json();
32
+ }
33
+ catch (err) {
34
+ logger.warn({ err: err?.message }, 'workspace sync manifest failed');
35
+ return;
36
+ }
37
+ if (!payload.ok || !payload.agents?.length)
38
+ return;
39
+ let downloaded = 0;
40
+ for (const agent of payload.agents) {
41
+ const agentDir = getAgentWorkspaceDir(input.networkId, agent.id);
42
+ for (const run of agent.runs) {
43
+ for (const file of run.files) {
44
+ const rel = safeRelativePath(file.relativePath);
45
+ if (!rel)
46
+ continue;
47
+ const dest = join(agentDir, rel);
48
+ if (file.sha256 && existsSync(dest) && hashFile(dest) === file.sha256)
49
+ continue;
50
+ try {
51
+ const resp = await fetch(`${base}${file.downloadUrl}`, {
52
+ headers: { Authorization: `Bearer ${input.token}` },
53
+ });
54
+ if (!resp.ok)
55
+ continue;
56
+ const bytes = Buffer.from(await resp.arrayBuffer());
57
+ mkdirSync(dirname(dest), { recursive: true });
58
+ writeFileSync(dest, bytes);
59
+ downloaded += 1;
60
+ }
61
+ catch (err) {
62
+ logger.warn({ err: err?.message, fileId: file.id }, 'workspace artifact download failed');
63
+ }
64
+ }
65
+ }
66
+ }
67
+ if (downloaded > 0)
68
+ logger.info({ downloaded }, 'workspace artifacts synced');
69
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentbean/daemon",
3
3
  "private": false,
4
- "version": "0.1.9",
4
+ "version": "0.1.11",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {