@agentbean/daemon 0.1.29 → 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.
@@ -11,6 +11,9 @@ function buildPrompt(input, systemPrompt) {
11
11
  parts.push(input.prompt);
12
12
  return parts.join('\n\n---\n\n');
13
13
  }
14
+ function normalizeClaudeArgs(args) {
15
+ return ['-p', ...(args ?? [])];
16
+ }
14
17
  export class ClaudeCodeAdapter {
15
18
  opts;
16
19
  kind = 'claude-code';
@@ -21,7 +24,7 @@ export class ClaudeCodeAdapter {
21
24
  return new Promise((resolve, reject) => {
22
25
  const prompt = buildPrompt(input, this.opts.systemPrompt ?? input.systemPrompt);
23
26
  const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
24
- const baseArgs = ['-p', '--bare', ...(this.opts.args ?? [])];
27
+ const baseArgs = normalizeClaudeArgs(this.opts.args);
25
28
  if (input.workspace)
26
29
  baseArgs.push('--add-dir', input.workspace);
27
30
  baseArgs.push('--add-dir', join(homedir(), '.codex', 'generated_images'));
@@ -1,4 +1,8 @@
1
- import { spawn } from 'node-pty';
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';
2
6
  function renderPayload(input, systemPrompt) {
3
7
  const parts = [];
4
8
  if (systemPrompt)
@@ -40,18 +44,147 @@ export function extractCodexReply(output, payload) {
40
44
  }
41
45
  return clean.trim();
42
46
  }
43
- 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) {
44
66
  const baseArgs = args && args.length > 0 ? args : ['exec'];
45
67
  const subcommand = baseArgs[0];
46
- if ((subcommand === 'exec' || subcommand === 'e') && !baseArgs.includes('--skip-git-repo-check')) {
47
- 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
+ };
48
85
  }
49
- return baseArgs;
86
+ return { args: baseArgs };
50
87
  }
51
88
  function adapterTimeoutMs() {
52
89
  const fromEnv = Number.parseInt(process.env.AGENTBEAN_CODEX_TIMEOUT_MS ?? '', 10);
53
90
  return Number.isFinite(fromEnv) && fromEnv > 0 ? fromEnv : 900_000;
54
91
  }
92
+ function buildRuntimeEnv(extra) {
93
+ const pathEntries = [
94
+ process.env.PATH,
95
+ '/opt/homebrew/bin',
96
+ '/usr/local/bin',
97
+ join(homedir(), '.local/bin'),
98
+ join(homedir(), '.bun/bin'),
99
+ join(homedir(), '.npm-global/bin'),
100
+ join(homedir(), '.asdf/shims'),
101
+ join(homedir(), '.local/share/mise/shims'),
102
+ ].filter(Boolean).join(':');
103
+ return { ...process.env, PATH: pathEntries, ...(extra ?? {}) };
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
+ }
55
188
  export class CodexAdapter {
56
189
  opts;
57
190
  kind = 'codex';
@@ -63,19 +196,26 @@ export class CodexAdapter {
63
196
  const payload = renderPayload(input, this.opts.systemPrompt ?? input.systemPrompt);
64
197
  const cwd = input.workspace ?? this.opts.cwd ?? process.cwd();
65
198
  const baseCommand = this.opts.command || 'codex';
66
- const configuredArgs = normalizeExecArgs(this.opts.args);
199
+ const defaultOutputLastMessagePath = createOutputLastMessagePath();
200
+ const normalizedExec = normalizeExecArgs(this.opts.args, defaultOutputLastMessagePath);
201
+ const configuredArgs = normalizedExec.args;
67
202
  const baseArgs = [...configuredArgs, payload];
68
203
  const command = input.sandboxProfilePath ? 'sandbox-exec' : baseCommand;
69
204
  const args = input.sandboxProfilePath
70
205
  ? ['-f', input.sandboxProfilePath, '--', baseCommand, ...baseArgs]
71
206
  : baseArgs;
72
- const pty = spawn(command, args, {
73
- name: 'xterm-color',
74
- cols: 80,
75
- rows: 30,
76
- cwd,
77
- env: { ...process.env, ...(input.env ?? {}) },
78
- });
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 });
79
219
  const chunks = [];
80
220
  let finished = false;
81
221
  const MAX_EXEC_MS = adapterTimeoutMs();
@@ -85,10 +225,10 @@ export class CodexAdapter {
85
225
  finished = true;
86
226
  clearTimeout(maxTimer);
87
227
  signal.removeEventListener('abort', onAbort);
88
- pty.kill('SIGTERM');
228
+ runtime.kill('SIGTERM');
89
229
  setTimeout(() => {
90
230
  try {
91
- pty.kill('SIGKILL');
231
+ runtime.kill('SIGKILL');
92
232
  }
93
233
  catch { }
94
234
  }, 2_000).unref();
@@ -99,12 +239,12 @@ export class CodexAdapter {
99
239
  if (finished)
100
240
  return;
101
241
  finished = true;
102
- pty.kill('SIGKILL');
242
+ runtime.kill('SIGKILL');
103
243
  signal.removeEventListener('abort', onAbort);
104
244
  reject(new Error('codex adapter timeout'));
105
245
  }, MAX_EXEC_MS).unref();
106
- pty.onData((data) => chunks.push(data));
107
- pty.onExit(({ exitCode }) => {
246
+ runtime.onData((data) => chunks.push(data));
247
+ runtime.onExit(({ exitCode }) => {
108
248
  clearTimeout(maxTimer);
109
249
  signal.removeEventListener('abort', onAbort);
110
250
  if (finished)
@@ -117,7 +257,7 @@ export class CodexAdapter {
117
257
  const detail = stripAnsi(raw).trim();
118
258
  return reject(new Error(detail ? `codex exit ${exitCode}: ${detail}` : `codex exit ${exitCode}`));
119
259
  }
120
- const reply = extractCodexReply(raw, payload);
260
+ const reply = readOutputLastMessage(normalizedExec.outputLastMessagePath) ?? extractCodexReply(raw, payload);
121
261
  resolve(reply || '(Codex 已完成处理)');
122
262
  });
123
263
  });
@@ -125,10 +265,10 @@ export class CodexAdapter {
125
265
  async health() {
126
266
  return new Promise((resolve) => {
127
267
  try {
128
- const pty = spawn('bash', ['-c', 'codex --version'], {
268
+ const pty = spawnPty('bash', ['-c', 'codex --version'], {
129
269
  name: 'xterm-color', cols: 80, rows: 30,
130
270
  cwd: this.opts.cwd ?? process.cwd(),
131
- env: process.env,
271
+ env: buildRuntimeEnv(),
132
272
  });
133
273
  pty.onExit(({ exitCode }) => resolve({ ok: exitCode === 0, detail: exitCode === 0 ? undefined : `exit ${exitCode}` }));
134
274
  }
@@ -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,
@@ -71,6 +71,14 @@ export function resolveCustomAgentRuntime(custom, runtimes) {
71
71
  if (bestRuntime?.command?.trim()) {
72
72
  return { command: bestRuntime.command.trim(), runtime: bestRuntime };
73
73
  }
74
+ if (configured && isAbsolute(configured) && !configuredAbsoluteExists) {
75
+ const fallback = basename(configured).trim();
76
+ if (fallback)
77
+ return { command: fallback };
78
+ }
79
+ if (!configured && normalizeAdapterKind(custom.adapterKind) === 'codex') {
80
+ return { command: 'codex' };
81
+ }
74
82
  return { command: configured };
75
83
  }
76
84
  export function nativeDirectoryPickerCommands(platform = process.platform) {
@@ -383,6 +391,7 @@ export function createDeviceDaemon(cfg, agents) {
383
391
  args: custom.args ?? [],
384
392
  cwd: custom.cwd ?? undefined,
385
393
  workspace: custom.cwd ?? undefined,
394
+ env: custom.env ?? undefined,
386
395
  systemPrompt: custom.description ?? undefined,
387
396
  },
388
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.29",
4
+ "version": "0.1.32",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "bin": {