@agentbean/daemon 0.1.31 → 0.1.32

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.
@@ -12,8 +12,7 @@ function buildPrompt(input, systemPrompt) {
12
12
  return parts.join('\n\n---\n\n');
13
13
  }
14
14
  function normalizeClaudeArgs(args) {
15
- const filtered = (args ?? []).filter((arg) => arg !== '--bare');
16
- return ['-p', ...filtered];
15
+ return ['-p', ...(args ?? [])];
17
16
  }
18
17
  export class ClaudeCodeAdapter {
19
18
  opts;
@@ -1,6 +1,8 @@
1
- import { spawn } from 'node-pty';
2
- import { homedir } from 'node:os';
3
- import { join } from 'node:path';
1
+ import { spawn as spawnChild } from 'node:child_process';
2
+ import { spawn as spawnPty } from 'node-pty';
3
+ import { accessSync, constants, existsSync, mkdtempSync, readFileSync } from 'node:fs';
4
+ import { homedir, tmpdir } from 'node:os';
5
+ import { delimiter, join } from 'node:path';
4
6
  function renderPayload(input, systemPrompt) {
5
7
  const parts = [];
6
8
  if (systemPrompt)
@@ -42,13 +44,46 @@ export function extractCodexReply(output, payload) {
42
44
  }
43
45
  return clean.trim();
44
46
  }
45
- function normalizeExecArgs(args) {
47
+ function hasFlag(args, ...flags) {
48
+ return args.some((arg) => flags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)));
49
+ }
50
+ function flagValue(args, ...flags) {
51
+ for (let i = 0; i < args.length; i += 1) {
52
+ const arg = args[i];
53
+ for (const flag of flags) {
54
+ if (arg === flag)
55
+ return args[i + 1];
56
+ if (arg.startsWith(`${flag}=`))
57
+ return arg.slice(flag.length + 1);
58
+ }
59
+ }
60
+ return undefined;
61
+ }
62
+ function createOutputLastMessagePath() {
63
+ return join(mkdtempSync(join(tmpdir(), 'agentbean-codex-')), 'last-message.txt');
64
+ }
65
+ function normalizeExecArgs(args, outputLastMessagePath) {
46
66
  const baseArgs = args && args.length > 0 ? args : ['exec'];
47
67
  const subcommand = baseArgs[0];
48
- if ((subcommand === 'exec' || subcommand === 'e') && !baseArgs.includes('--skip-git-repo-check')) {
49
- return [subcommand, '--skip-git-repo-check', ...baseArgs.slice(1)];
68
+ if (subcommand === 'exec' || subcommand === 'e') {
69
+ const rest = baseArgs.slice(1);
70
+ const normalized = [subcommand];
71
+ if (!hasFlag(rest, '--skip-git-repo-check')) {
72
+ normalized.push('--skip-git-repo-check');
73
+ }
74
+ const configuredOutputPath = flagValue(rest, '--output-last-message', '-o');
75
+ if (!configuredOutputPath) {
76
+ normalized.push('--output-last-message', outputLastMessagePath);
77
+ }
78
+ if (!hasFlag(rest, '--json', '--experimental-json')) {
79
+ normalized.push('--json');
80
+ }
81
+ return {
82
+ args: [...normalized, ...rest],
83
+ outputLastMessagePath: configuredOutputPath ?? outputLastMessagePath,
84
+ };
50
85
  }
51
- return baseArgs;
86
+ return { args: baseArgs };
52
87
  }
53
88
  function adapterTimeoutMs() {
54
89
  const fromEnv = Number.parseInt(process.env.AGENTBEAN_CODEX_TIMEOUT_MS ?? '', 10);
@@ -67,6 +102,89 @@ function buildRuntimeEnv(extra) {
67
102
  ].filter(Boolean).join(':');
68
103
  return { ...process.env, PATH: pathEntries, ...(extra ?? {}) };
69
104
  }
105
+ function assertExecutable(command, env, label) {
106
+ const trimmed = command.trim();
107
+ if (!trimmed) {
108
+ throw new Error(`${label} command is empty`);
109
+ }
110
+ if (trimmed.includes('/')) {
111
+ try {
112
+ accessSync(trimmed, constants.X_OK);
113
+ return;
114
+ }
115
+ catch {
116
+ throw new Error(`${label} command is not executable: ${trimmed}`);
117
+ }
118
+ }
119
+ const pathEntries = (env.PATH ?? '').split(delimiter).filter(Boolean);
120
+ for (const dir of pathEntries) {
121
+ try {
122
+ accessSync(join(dir, trimmed), constants.X_OK);
123
+ return;
124
+ }
125
+ catch { }
126
+ }
127
+ throw new Error(`${label} command was not found on PATH: ${trimmed}. PATH=${env.PATH ?? ''}`);
128
+ }
129
+ function readOutputLastMessage(path) {
130
+ if (!path || !existsSync(path))
131
+ return null;
132
+ const text = readFileSync(path, 'utf8').trim();
133
+ return text || null;
134
+ }
135
+ function spawnRuntimeProcess(command, args, opts) {
136
+ try {
137
+ const pty = spawnPty(command, args, {
138
+ name: 'xterm-color',
139
+ cols: 80,
140
+ rows: 30,
141
+ cwd: opts.cwd,
142
+ env: opts.env,
143
+ });
144
+ return {
145
+ onData: (cb) => pty.onData(cb),
146
+ onExit: (cb) => pty.onExit(({ exitCode }) => cb({ exitCode })),
147
+ kill: (signal) => pty.kill(signal),
148
+ };
149
+ }
150
+ catch (err) {
151
+ const child = spawnChild(command, args, {
152
+ cwd: opts.cwd,
153
+ env: opts.env,
154
+ stdio: ['ignore', 'pipe', 'pipe'],
155
+ });
156
+ const dataHandlers = [];
157
+ const exitHandlers = [];
158
+ let exited = false;
159
+ const emitExit = (exitCode) => {
160
+ if (exited)
161
+ return;
162
+ exited = true;
163
+ for (const handler of exitHandlers)
164
+ handler({ exitCode });
165
+ };
166
+ child.stdout?.on('data', (data) => {
167
+ for (const handler of dataHandlers)
168
+ handler(String(data));
169
+ });
170
+ child.stderr?.on('data', (data) => {
171
+ for (const handler of dataHandlers)
172
+ handler(String(data));
173
+ });
174
+ child.on('error', (childErr) => {
175
+ const message = childErr instanceof Error ? childErr.message : String(childErr);
176
+ for (const handler of dataHandlers)
177
+ handler(message);
178
+ emitExit(1);
179
+ });
180
+ child.on('exit', (code) => emitExit(code ?? 1));
181
+ return {
182
+ onData: (cb) => { dataHandlers.push(cb); },
183
+ onExit: (cb) => { exitHandlers.push(cb); },
184
+ kill: (signal) => { child.kill(signal); },
185
+ };
186
+ }
187
+ }
70
188
  export class CodexAdapter {
71
189
  opts;
72
190
  kind = 'codex';
@@ -78,19 +196,26 @@ export class CodexAdapter {
78
196
  const payload = renderPayload(input, this.opts.systemPrompt ?? input.systemPrompt);
79
197
  const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
80
198
  const baseCommand = this.opts.command || 'codex';
81
- const configuredArgs = normalizeExecArgs(this.opts.args);
199
+ const defaultOutputLastMessagePath = createOutputLastMessagePath();
200
+ const normalizedExec = normalizeExecArgs(this.opts.args, defaultOutputLastMessagePath);
201
+ const configuredArgs = normalizedExec.args;
82
202
  const baseArgs = [...configuredArgs, payload];
83
203
  const command = input.sandboxProfilePath ? 'sandbox-exec' : baseCommand;
84
204
  const args = input.sandboxProfilePath
85
205
  ? ['-f', input.sandboxProfilePath, '--', baseCommand, ...baseArgs]
86
206
  : baseArgs;
87
- const pty = spawn(command, args, {
88
- name: 'xterm-color',
89
- cols: 80,
90
- rows: 30,
91
- cwd,
92
- env: buildRuntimeEnv(input.env),
93
- });
207
+ const env = buildRuntimeEnv(input.env);
208
+ try {
209
+ assertExecutable(command, env, input.sandboxProfilePath ? 'Sandbox launcher' : 'Codex runtime');
210
+ if (input.sandboxProfilePath) {
211
+ assertExecutable(baseCommand, env, 'Codex runtime');
212
+ }
213
+ }
214
+ catch (err) {
215
+ reject(err);
216
+ return;
217
+ }
218
+ const runtime = spawnRuntimeProcess(command, args, { cwd, env });
94
219
  const chunks = [];
95
220
  let finished = false;
96
221
  const MAX_EXEC_MS = adapterTimeoutMs();
@@ -100,10 +225,10 @@ export class CodexAdapter {
100
225
  finished = true;
101
226
  clearTimeout(maxTimer);
102
227
  signal.removeEventListener('abort', onAbort);
103
- pty.kill('SIGTERM');
228
+ runtime.kill('SIGTERM');
104
229
  setTimeout(() => {
105
230
  try {
106
- pty.kill('SIGKILL');
231
+ runtime.kill('SIGKILL');
107
232
  }
108
233
  catch { }
109
234
  }, 2_000).unref();
@@ -114,12 +239,12 @@ export class CodexAdapter {
114
239
  if (finished)
115
240
  return;
116
241
  finished = true;
117
- pty.kill('SIGKILL');
242
+ runtime.kill('SIGKILL');
118
243
  signal.removeEventListener('abort', onAbort);
119
244
  reject(new Error('codex adapter timeout'));
120
245
  }, MAX_EXEC_MS).unref();
121
- pty.onData((data) => chunks.push(data));
122
- pty.onExit(({ exitCode }) => {
246
+ runtime.onData((data) => chunks.push(data));
247
+ runtime.onExit(({ exitCode }) => {
123
248
  clearTimeout(maxTimer);
124
249
  signal.removeEventListener('abort', onAbort);
125
250
  if (finished)
@@ -132,7 +257,7 @@ export class CodexAdapter {
132
257
  const detail = stripAnsi(raw).trim();
133
258
  return reject(new Error(detail ? `codex exit ${exitCode}: ${detail}` : `codex exit ${exitCode}`));
134
259
  }
135
- const reply = extractCodexReply(raw, payload);
260
+ const reply = readOutputLastMessage(normalizedExec.outputLastMessagePath) ?? extractCodexReply(raw, payload);
136
261
  resolve(reply || '(Codex 已完成处理)');
137
262
  });
138
263
  });
@@ -140,7 +265,7 @@ export class CodexAdapter {
140
265
  async health() {
141
266
  return new Promise((resolve) => {
142
267
  try {
143
- const pty = spawn('bash', ['-c', 'codex --version'], {
268
+ const pty = spawnPty('bash', ['-c', 'codex --version'], {
144
269
  name: 'xterm-color', cols: 80, rows: 30,
145
270
  cwd: this.opts.cwd ?? process.cwd(),
146
271
  env: buildRuntimeEnv(),
@@ -108,7 +108,7 @@ export class AgentInstance {
108
108
  sandboxProfilePath: req.sandboxed && isSandboxAvailable()
109
109
  ? generateSandboxProfile(this.id, this.config.adapter.command)
110
110
  : undefined,
111
- env: workspaceEnv(run),
111
+ env: { ...(this.config.adapter.env ?? {}), ...workspaceEnv(run) },
112
112
  }, ctl.signal);
113
113
  const processed = await postProcess(rawBody, projectWorkspace, this.adapter.kind, dispatchStart, {
114
114
  outputDirs: [run.outputDir, run.intermediateDir],
package/dist/config.js CHANGED
@@ -23,6 +23,16 @@ function deepInterpolate(node) {
23
23
  }
24
24
  return node;
25
25
  }
26
+ function parseEnvMap(value) {
27
+ if (!value || typeof value !== 'object' || Array.isArray(value))
28
+ return undefined;
29
+ const out = {};
30
+ for (const [key, raw] of Object.entries(value)) {
31
+ if (key.trim())
32
+ out[key.trim()] = String(raw ?? '');
33
+ }
34
+ return Object.keys(out).length > 0 ? out : undefined;
35
+ }
26
36
  export function loadConfig(path) {
27
37
  const raw = parseYaml(readFileSync(path, 'utf8'));
28
38
  if (!raw || typeof raw !== 'object')
@@ -62,6 +72,7 @@ export function loadConfig(path) {
62
72
  cwd: typeof a.cwd === 'string' ? a.cwd : undefined,
63
73
  workspace: typeof a.workspace === 'string' ? a.workspace : undefined,
64
74
  systemPrompt: typeof a.systemPrompt === 'string' ? a.systemPrompt : undefined,
75
+ env: parseEnvMap(a.env),
65
76
  },
66
77
  server: { url: s.url, token: s.token },
67
78
  heartbeatIntervalMs: isValidHeartbeat(interp.heartbeatIntervalMs) ? interp.heartbeatIntervalMs : 10_000,
@@ -121,6 +132,7 @@ export function loadDeviceConfig(path) {
121
132
  cwd: typeof ad.cwd === 'string' ? ad.cwd : undefined,
122
133
  workspace: typeof ad.workspace === 'string' ? ad.workspace : undefined,
123
134
  systemPrompt: typeof ad.systemPrompt === 'string' ? ad.systemPrompt : undefined,
135
+ env: parseEnvMap(ad.env),
124
136
  },
125
137
  visibility: a.visibility === 'public' ? 'public' : 'private',
126
138
  sandboxed: a.sandboxed === true,
@@ -391,6 +391,7 @@ export function createDeviceDaemon(cfg, agents) {
391
391
  args: custom.args ?? [],
392
392
  cwd: custom.cwd ?? undefined,
393
393
  workspace: custom.cwd ?? undefined,
394
+ env: custom.env ?? undefined,
394
395
  systemPrompt: custom.description ?? undefined,
395
396
  },
396
397
  visibility: 'public',
package/dist/sandbox.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
1
+ import { accessSync, constants, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { homedir } from 'node:os';
3
3
  import { dirname, join } from 'node:path';
4
4
  function escapeSchemeString(value) {
@@ -11,7 +11,15 @@ export function getWorkspaceDir(agentId) {
11
11
  return dir;
12
12
  }
13
13
  export function isSandboxAvailable() {
14
- return process.platform === 'darwin';
14
+ if (process.platform !== 'darwin')
15
+ return false;
16
+ try {
17
+ accessSync('/usr/bin/sandbox-exec', constants.X_OK);
18
+ return true;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
15
23
  }
16
24
  export function generateSandboxProfile(agentId, runtimePath) {
17
25
  const workspaceDir = getWorkspaceDir(agentId);
package/dist/scanner.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { readdirSync, readFileSync, statSync, existsSync, writeFileSync, mkdirSync, } from "node:fs";
3
- import { join } from "node:path";
3
+ 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";
@@ -274,6 +274,7 @@ async function checkHermesGateway() {
274
274
  adapterKind: "hermes",
275
275
  command: path,
276
276
  args: [],
277
+ cwd: dirname(path),
277
278
  source: "gateway",
278
279
  };
279
280
  }
@@ -293,6 +294,7 @@ async function checkOpenClawGateway() {
293
294
  adapterKind: "openclaw",
294
295
  command: path,
295
296
  args: ["agent", "--agent", agentId ?? "main"],
297
+ cwd: dirname(path),
296
298
  source: "gateway",
297
299
  };
298
300
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@agentbean/daemon",
3
3
  "private": false,
4
- "version": "0.1.31",
4
+ "version": "0.1.32",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {