alanbox 0.1.1

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.
Files changed (41) hide show
  1. package/0commondflowv1/AGENTS.md +51 -0
  2. package/0commondflowv1/res/three-lens-review.js +124 -0
  3. package/0commondflowv1/src/AGENTS.md +26 -0
  4. package/0commondflowv1/src/args.js +75 -0
  5. package/0commondflowv1/src/cli.js +121 -0
  6. package/0commondflowv1/src/commands/AGENTS.md +29 -0
  7. package/0commondflowv1/src/commands/doctor.js +17 -0
  8. package/0commondflowv1/src/commands/info.js +33 -0
  9. package/0commondflowv1/src/commands/install.js +247 -0
  10. package/0commondflowv1/src/commands/swarm/auto.js +270 -0
  11. package/0commondflowv1/src/commands/swarm/custom.js +60 -0
  12. package/0commondflowv1/src/commands/swarm/index.js +20 -0
  13. package/0commondflowv1/src/core/AGENTS.md +31 -0
  14. package/0commondflowv1/src/core/handoff.js +100 -0
  15. package/0commondflowv1/src/core/prompt-builder.js +93 -0
  16. package/0commondflowv1/src/core/prompt-templates.js +92 -0
  17. package/0commondflowv1/src/core/storage.js +98 -0
  18. package/0commondflowv1/src/core/swarm-executor.js +167 -0
  19. package/0commondflowv1/src/core/workers.js +172 -0
  20. package/0commondflowv1/src/core/workflow-planner.js +366 -0
  21. package/0commondflowv1/src/core/workflow-storage.js +123 -0
  22. package/0commondflowv1/src/prompt/AGENTS.md +16 -0
  23. package/0commondflowv1/src/prompt/default.md +30 -0
  24. package/0commondflowv1/src/prompt/reviewer.md +59 -0
  25. package/0commondflowv1/src/prompt/synthesizer.md +31 -0
  26. package/0commondflowv1/src/prompt/verifier.md +31 -0
  27. package/0commondflowv1/src/runner/AGENTS.md +24 -0
  28. package/0commondflowv1/src/runner/codex-runner.js +519 -0
  29. package/0commondflowv1/src/runner/config.json +16 -0
  30. package/README.md +31 -0
  31. package/bin/multirunagent.js +15 -0
  32. package/hooks/hooks.json +18 -0
  33. package/mcp/README.md +5 -0
  34. package/package.json +45 -0
  35. package/plugin/AGENTS.md +14 -0
  36. package/plugin/plugin.json +36 -0
  37. package/scripts/sub-codex-hook.ps1 +15 -0
  38. package/skills/AGENTS.md +17 -0
  39. package/skills/aibox-swam/SKILL.md +77 -0
  40. package/skills/sub-codex-doctor/SKILL.md +27 -0
  41. package/skills/sub-codex-swarm/SKILL.md +56 -0
@@ -0,0 +1,59 @@
1
+ # Review Prompt
2
+
3
+ 你是一个严格、务实的代码审查 agent。你的目标是发现真实会影响正确性、可维护性、安全性或交付质量的问题,并给出可执行的修复建议。
4
+
5
+ ## 审查目标
6
+
7
+ - 优先发现会导致运行错误、行为不符合预期、数据丢失、安全风险或用户体验明显退化的问题。
8
+ - 检查改动是否过度设计、重复实现、破坏现有约定或引入不必要复杂度。
9
+ - 关注边界条件、错误处理、跨平台兼容、路径处理、配置来源和向后兼容。
10
+ - 只报告有证据的问题;不把个人风格偏好当成缺陷。
11
+
12
+ ## 工作方式
13
+
14
+ 1. 先理解任务背景、改动范围和相关 AGENTS.md / README / 配置约束。
15
+ 2. 阅读与改动直接相关的代码路径,不要只看单个片段就下结论。
16
+ 3. 对每个发现给出:问题、影响、证据位置、建议修复方式。
17
+ 4. 如果没有发现高置信问题,明确说明“未发现需要阻塞的 review 问题”,并列出已检查范围。
18
+ 5. 不要直接修改代码,除非调用方明确要求你修复。
19
+
20
+ ## 输出格式
21
+
22
+ 请用中文输出,结构如下:
23
+
24
+ ```markdown
25
+ ## Review 结果
26
+
27
+ ### 阻塞问题
28
+
29
+ - 无 / 或列出问题
30
+
31
+ ### 非阻塞建议
32
+
33
+ - 无 / 或列出建议
34
+
35
+ ### 已检查范围
36
+
37
+ - 文件或模块列表
38
+
39
+ ### 结论
40
+
41
+ - 通过 / 需要修改 / 信息不足
42
+ ```
43
+
44
+ ## 问题条目格式
45
+
46
+ ```markdown
47
+ - **级别**:阻塞 / 重要 / 建议
48
+ - **位置**:path/to/file.js:行号
49
+ - **问题**:一句话说明缺陷
50
+ - **影响**:说明为什么这会造成实际问题
51
+ - **建议**:给出最小可行修复方向
52
+ ```
53
+
54
+ ## 约束
55
+
56
+ - 不要臆造未读取文件中的实现。
57
+ - 不要要求大重构,除非问题无法用局部修改解决。
58
+ - 不要输出冗长背景介绍;优先给出可操作结论。
59
+ - 如果需求或上下文不足,先列出缺失信息,并说明需要补充什么。
@@ -0,0 +1,31 @@
1
+ # Synthesizer Prompt
2
+
3
+ 你是 multirunagent auto workflow 的汇总 agent。
4
+
5
+ ## 总目标
6
+
7
+ {{goal}}
8
+
9
+ ## 当前任务
10
+
11
+ {{task}}
12
+
13
+ ## 工作目录
14
+
15
+ {{cwd}}
16
+
17
+ ## 补充约束
18
+
19
+ {{constraints}}
20
+
21
+ ## 预期状态变化
22
+
23
+ {{expectedState}}
24
+
25
+ ## 汇总规则
26
+
27
+ 1. 优先读取上游 HANDOFF_JSON 摘要,不要重复展开所有 stdout。
28
+ 2. 合并重复发现;冲突结论要标明来源和不确定性。
29
+ 3. 输出最终可执行结论:阻塞问题、非阻塞建议、已检查范围、建议下一步。
30
+ 4. 不做未授权的真实修改。
31
+ 5. 最后输出 `HANDOFF_JSON`,status 根据汇总质量选择 completed / blocked / failed。
@@ -0,0 +1,31 @@
1
+ # Verifier Prompt
2
+
3
+ 你是 multirunagent auto workflow 的验证 agent。
4
+
5
+ ## 总目标
6
+
7
+ {{goal}}
8
+
9
+ ## 当前任务
10
+
11
+ {{task}}
12
+
13
+ ## 工作目录
14
+
15
+ {{cwd}}
16
+
17
+ ## 补充约束
18
+
19
+ {{constraints}}
20
+
21
+ ## 预期状态变化
22
+
23
+ {{expectedState}}
24
+
25
+ ## 验证规则
26
+
27
+ 1. 检查上游结果是否真正回答了目标,不要只看是否有输出。
28
+ 2. 如果有可运行的轻量命令或 dry-run 证据,优先使用;否则给出人工可读证据。
29
+ 3. 若目标未达成、证据不足或上游失败,`HANDOFF_JSON.status` 返回 failed 或 blocked,并在 findings / nextSteps / verification 中说明原因。
30
+ 4. 若目标达成,`HANDOFF_JSON.status` 返回 completed,并列出关键证据。
31
+ 5. 不扩大任务范围,不做破坏性操作。
@@ -0,0 +1,24 @@
1
+ ## runner
2
+
3
+ `0commondflowv1/src/runner` 负责读取 provider 配置、解析 CLI 命令参数,并通过子进程启动 Codex 或 Claude。这里决定是否使用原生 CLI home,或在显式配置时启用隔离 home。
4
+
5
+ **Important:** 默认必须使用原生 Codex / Claude CLI 登录态,不设置 `CODEX_HOME` 或 `CLAUDE_CONFIG_DIR`。只有显式配置 `accountsDir`、worker account 或 home 时才启用隔离。
6
+
7
+ ### Important files
8
+
9
+ - `codex-runner.js` — provider 解析、命令参数构造、环境变量处理和子进程执行层。
10
+ - `config.json` — 内置最小配置;默认 provider 是 `codex`,可选 provider 是 `claude`,保留 `timeoutMs`。
11
+ - `accounts/` — 旧隔离 home 运行产物目录;不是源码。除非用户明确要求,不要删除或把其中内容写进配置默认值。
12
+
13
+ ### Implementation notes
14
+
15
+ - Codex provider 使用 `codex exec`;完整 prompt 应通过 stdin 传入,避免 Windows argv 多行截断。Claude provider 非交互模式继续使用 `claude -p <prompt>`,未验证前不要改成 stdin。
16
+ - 不要把个人 Antigravity cockpit 路径、全局 npm 安装路径或本机私有 home 写成通用默认配置。
17
+ - `info` 输出可以展示 `<native>`、`<not inherited>`、command、commandArgs 这类排查信息,但不要打印 secret。
18
+ - Windows `.cmd`、`.bat`、`.ps1`、PowerShell 路径和 Node `spawn` 参数需要实际考虑;provider 裸命令应通过 PATH/PATHEXT 解析后再启动,不要把用户本机 npm shim 绝对路径写进默认配置。
19
+
20
+ ### Verification
21
+
22
+ - 从包根目录运行:`node bin/multirunagent.js doctor`
23
+ - 检查默认 provider:`node bin/multirunagent.js info`
24
+ - 注意:`--dry-run` 已移除,检查子 worker 命令构造请用 `info`(已无 swarm dry-run 预览)。
@@ -0,0 +1,519 @@
1
+
2
+
3
+
4
+ const { spawn } = require('child_process');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ const CONFIG_PATH = path.join(__dirname, 'config.json');
9
+
10
+ function loadConfig(configPath = CONFIG_PATH) {
11
+ const raw = fs.readFileSync(configPath, 'utf8');
12
+ const config = JSON.parse(raw);
13
+ const providers = normalizeProviders(config);
14
+ return {
15
+ providers,
16
+ defaultProvider: config.defaultProvider || 'codex',
17
+ codexCommand: config.codexCommand || 'codex',
18
+ codexCommandPath: config.codexCommandPath || '',
19
+ codexNodeCommand: config.codexNodeCommand || '',
20
+ codexNodeArgs: Array.isArray(config.codexNodeArgs) ? config.codexNodeArgs : [],
21
+ defaultAccount: String(config.defaultAccount || ''),
22
+ defaultWorker: config.defaultWorker || 'worker-1',
23
+ primaryWorker: config.primaryWorker || config.defaultWorker || '',
24
+ defaultNamespace: config.defaultNamespace || 'sub-codex',
25
+ accounts: config.accounts || {},
26
+ workers: config.workers || {},
27
+ accountsDir: config.accountsDir || '',
28
+ defaultProjectPath: config.defaultProjectPath || process.cwd(),
29
+ defaultModel: config.defaultModel || '',
30
+ defaultProfile: config.defaultProfile || '',
31
+ sandbox: config.sandbox || 'workspace-write',
32
+ skipGitRepoCheck: config.skipGitRepoCheck !== false,
33
+ color: config.color || 'never',
34
+ timeoutMs: Number(config.timeoutMs || 30 * 60 * 1000),
35
+ inheritOpenAIKey: config.inheritOpenAIKey === true,
36
+ ephemeral: config.ephemeral === true,
37
+ ignoreUserConfig: config.ignoreUserConfig === true,
38
+ outputLastMessage: config.outputLastMessage || '',
39
+ };
40
+ }
41
+
42
+ function normalizeProviders(config) {
43
+ const providers = config.providers && typeof config.providers === 'object' ? config.providers : {};
44
+ return {
45
+ codex: {
46
+ command: config.codexCommandPath || config.codexNodeCommand || config.codexCommand || 'codex',
47
+ commandArgs: Array.isArray(config.codexNodeArgs) ? config.codexNodeArgs : [],
48
+ homeEnv: 'CODEX_HOME',
49
+ ...providers.codex,
50
+ },
51
+ claude: {
52
+ command: 'claude',
53
+ commandArgs: [],
54
+ homeEnv: 'CLAUDE_CONFIG_DIR',
55
+ ...providers.claude,
56
+ },
57
+ ...Object.fromEntries(Object.entries(providers).filter(([name]) => name !== 'codex' && name !== 'claude')),
58
+ };
59
+ }
60
+
61
+ function resolveAccountHome(config, account) {
62
+ const resolvedAccount = resolveAccountName(config, account);
63
+ if (!resolvedAccount && !config.accountsDir) {
64
+ return '';
65
+ }
66
+ const accountName = sanitizeAccountName(resolvedAccount);
67
+ const accountConfig = config.accounts?.[accountName];
68
+
69
+ if (accountConfig?.codexHome) {
70
+ return path.isAbsolute(accountConfig.codexHome)
71
+ ? accountConfig.codexHome
72
+ : path.resolve(__dirname, accountConfig.codexHome);
73
+ }
74
+
75
+ if (!config.accountsDir) {
76
+ return '';
77
+ }
78
+
79
+ const accountsDir = path.isAbsolute(config.accountsDir)
80
+ ? config.accountsDir
81
+ : path.resolve(__dirname, config.accountsDir);
82
+
83
+ // 每个账号单独映射到一个 CODEX_HOME,避免子 Codex 复用父 Codex 的登录态。
84
+ return path.join(accountsDir, accountName);
85
+ }
86
+
87
+ function resolveAccountName(config, account) {
88
+ const requested = account || config.defaultAccount;
89
+ const workerConfig = config.workers?.[requested];
90
+ return workerConfig?.account || requested;
91
+ }
92
+
93
+ function accountNameForWorker(worker, providerName = 'codex') {
94
+ const text = String(worker || '').trim();
95
+ if (/^worker-[A-Za-z0-9_.-]+$/.test(text)) {
96
+ return text.replace(/^worker-/, `${providerName}-`);
97
+ }
98
+ return '';
99
+ }
100
+
101
+ function resolveWorkerConfig(config, worker) {
102
+ if (!worker) return null;
103
+ return config.workers?.[worker] || null;
104
+ }
105
+
106
+ function resolveProvider(config, workerConfig) {
107
+ const providerName = workerConfig?.provider || config.defaultProvider || 'codex';
108
+ const provider = config.providers?.[providerName];
109
+ if (!provider) {
110
+ throw new Error(`unknown provider "${providerName}"`);
111
+ }
112
+ return { name: providerName, ...provider };
113
+ }
114
+
115
+ function sanitizeAccountName(value) {
116
+ const text = String(value || '').trim();
117
+ const safe = text.replace(/[^a-zA-Z0-9_.-]/g, '-').replace(/^-+|-+$/g, '');
118
+ if (!safe) {
119
+ throw new Error('account name is empty after sanitization');
120
+ }
121
+ return safe;
122
+ }
123
+
124
+ function buildChildEnv(config, accountHome, extraEnv = {}, provider = resolveProvider(config)) {
125
+ const env = { ...process.env, ...extraEnv };
126
+
127
+ // 默认不设置 home env,让 provider CLI 使用自己的原生登录态;显式配置 account/home 时才隔离。
128
+ if (accountHome) {
129
+ fs.mkdirSync(accountHome, { recursive: true });
130
+ env[provider.homeEnv || 'CODEX_HOME'] = accountHome;
131
+ }
132
+ env.FORCE_COLOR = '0';
133
+ env.NO_COLOR = '1';
134
+
135
+ // 只有启用隔离 home 时,默认才删除父进程 OPENAI_API_KEY,避免绕过隔离。
136
+ if (accountHome && !config.inheritOpenAIKey && !Object.prototype.hasOwnProperty.call(extraEnv, 'OPENAI_API_KEY')) {
137
+ delete env.OPENAI_API_KEY;
138
+ }
139
+
140
+ return env;
141
+ }
142
+
143
+ function buildExecInvocation(config, options) {
144
+ if (options.provider?.name === 'claude') {
145
+ return {
146
+ args: buildClaudePrintArgs(config, options),
147
+ stdinText: '',
148
+ promptTransport: 'argv',
149
+ };
150
+ }
151
+
152
+ return {
153
+ args: buildCodexExecArgs(config, options),
154
+ stdinText: options.prompt,
155
+ promptTransport: 'stdin',
156
+ };
157
+ }
158
+
159
+ function buildExecArgs(config, options) {
160
+ return buildExecInvocation(config, options).args;
161
+ }
162
+
163
+ function buildCodexExecArgs(config, options) {
164
+ // Codex provider 使用 codex exec 做非交互式执行,完整 prompt 通过 stdin 传入,避免 Windows argv 多行截断。
165
+ const args = ['exec', '--sandbox', options.sandbox || config.sandbox];
166
+
167
+ if (config.skipGitRepoCheck) {
168
+ args.push('--skip-git-repo-check');
169
+ }
170
+ const model = options.model || config.defaultModel;
171
+ if (model) {
172
+ args.push('-m', model);
173
+ }
174
+ const profile = options.profile || config.defaultProfile;
175
+ if (profile) {
176
+ args.push('-p', profile);
177
+ }
178
+ if (options.json) {
179
+ args.push('--json');
180
+ }
181
+ if (options.ephemeral || config.ephemeral) {
182
+ args.push('--ephemeral');
183
+ }
184
+ if (options.ignoreUserConfig || config.ignoreUserConfig) {
185
+ args.push('--ignore-user-config');
186
+ }
187
+ if (config.color) {
188
+ args.push('--color', config.color);
189
+ }
190
+ const outputLastMessage = options.outputLastMessage || config.outputLastMessage;
191
+ if (outputLastMessage) {
192
+ args.push('-o', path.resolve(outputLastMessage));
193
+ }
194
+
195
+ return args;
196
+ }
197
+
198
+ function summarizePromptTransport(invocation) {
199
+ const stdinText = invocation.stdinText || '';
200
+ if (invocation.promptTransport !== 'stdin') {
201
+ return { promptTransport: invocation.promptTransport };
202
+ }
203
+ const lines = stdinText ? stdinText.split(/\r?\n/).length : 0;
204
+ return {
205
+ promptTransport: 'stdin',
206
+ stdin: {
207
+ chars: stdinText.length,
208
+ lines,
209
+ preview: stdinText.slice(0, 300),
210
+ },
211
+ };
212
+ }
213
+
214
+ function buildClaudePrintArgs(config, options) {
215
+ const args = ['-p'];
216
+ const model = options.model || config.defaultModel;
217
+ if (model) {
218
+ args.push('--model', model);
219
+ }
220
+ if (options.json) {
221
+ args.push('--output-format', 'json');
222
+ }
223
+ if (options.ephemeral || config.ephemeral) {
224
+ args.push('--no-session-persistence');
225
+ }
226
+ if (options.ignoreUserConfig || config.ignoreUserConfig) {
227
+ args.push('--setting-sources', '');
228
+ }
229
+ const permissionMode = options.permissionMode || config.claudePermissionMode || '';
230
+ if (permissionMode) {
231
+ args.push('--permission-mode', permissionMode);
232
+ }
233
+ args.push(options.prompt);
234
+ return args;
235
+ }
236
+
237
+ function runCodex(args, options = {}) {
238
+ const config = options.config || loadConfig();
239
+ const workerConfig = resolveWorkerConfig(config, options.worker);
240
+ const providerOverridden = Boolean(options.provider && options.provider !== workerConfig?.provider);
241
+ const provider = options.provider
242
+ ? resolveProvider(config, { ...(workerConfig || {}), provider: options.provider })
243
+ : resolveProvider(config, workerConfig);
244
+ const account = options.account
245
+ || (!providerOverridden && provider.name === workerConfig?.provider ? workerConfig?.account : '')
246
+ || (config.accountsDir ? accountNameForWorker(options.worker, provider.name) : '');
247
+ const accountHome = !providerOverridden && workerConfig?.codexHome
248
+ ? path.resolve(workerConfig.codexHome)
249
+ : resolveAccountHome(config, account);
250
+ const cwd = path.resolve(options.cwd || workerConfig?.cwd || config.defaultProjectPath);
251
+ const timeoutMs = Number(options.timeoutMs || config.timeoutMs);
252
+ const env = buildChildEnv(config, accountHome, options.env, provider);
253
+ const command = options.command || workerConfig?.command || provider.command || config.codexNodeCommand || config.codexCommandPath || config.codexCommand;
254
+ const baseArgs = resolveCommandArgs(config, workerConfig, provider);
255
+ const finalArgs = [...baseArgs, ...args];
256
+
257
+ return new Promise((resolve, reject) => {
258
+ let stdout = '';
259
+ let stderr = '';
260
+ let timedOut = false;
261
+
262
+ const stdinText = typeof options.stdinText === 'string' ? options.stdinText : '';
263
+ const launch = buildLaunch(command, finalArgs, env);
264
+ const child = spawn(launch.command, launch.args, {
265
+ cwd,
266
+ env,
267
+ stdio: options.stdio || [stdinText ? 'pipe' : 'ignore', 'pipe', 'pipe'],
268
+ shell: false,
269
+ windowsVerbatimArguments: launch.windowsVerbatimArguments === true,
270
+ });
271
+
272
+ if (stdinText && child.stdin) {
273
+ child.stdin.on('error', () => {});
274
+ child.stdin.end(stdinText, 'utf8');
275
+ }
276
+
277
+ const timer = timeoutMs > 0
278
+ ? setTimeout(() => {
279
+ timedOut = true;
280
+ child.kill('SIGTERM');
281
+ }, timeoutMs)
282
+ : null;
283
+
284
+ child.stdout?.on('data', (chunk) => {
285
+ const text = chunk.toString();
286
+ stdout += text;
287
+ if (options.stream) writeStreamChunk(process.stdout, text, options.streamPrefix);
288
+ });
289
+
290
+ child.stderr?.on('data', (chunk) => {
291
+ const text = chunk.toString();
292
+ stderr += text;
293
+ if (options.stream) writeStreamChunk(process.stderr, text, options.streamPrefix);
294
+ });
295
+
296
+ child.on('error', (error) => {
297
+ if (timer) clearTimeout(timer);
298
+ error.accountHome = accountHome;
299
+ reject(error);
300
+ });
301
+
302
+ child.on('close', (code, signal) => {
303
+ if (timer) clearTimeout(timer);
304
+ resolve({
305
+ code: timedOut ? 124 : code,
306
+ signal,
307
+ stdout,
308
+ stderr: timedOut && !stderr ? `Timed out after ${timeoutMs}ms` : stderr,
309
+ accountHome,
310
+ cwd,
311
+ args: finalArgs,
312
+ });
313
+ });
314
+ });
315
+ }
316
+
317
+ function writeStreamChunk(stream, text, prefix = '') {
318
+ if (!prefix) {
319
+ stream.write(text);
320
+ return;
321
+ }
322
+
323
+ const lines = text.split(/(\r?\n)/);
324
+ for (let i = 0; i < lines.length; i += 1) {
325
+ const part = lines[i];
326
+ if (!part) continue;
327
+ if (/^\r?\n$/.test(part)) {
328
+ stream.write(part);
329
+ } else {
330
+ stream.write(`[${prefix}] ${part}`);
331
+ }
332
+ }
333
+ }
334
+
335
+ async function execPrompt(prompt, options = {}) {
336
+ if (!prompt || !prompt.trim()) {
337
+ throw new Error('prompt is required');
338
+ }
339
+
340
+ const config = options.config || loadConfig();
341
+ const workerConfig = resolveWorkerConfig(config, options.worker);
342
+ const provider = options.provider
343
+ ? resolveProvider(config, { ...(workerConfig || {}), provider: options.provider })
344
+ : resolveProvider(config, workerConfig);
345
+ const invocation = buildExecInvocation(config, { ...options, provider, prompt });
346
+ return runCodex(invocation.args, { ...options, config, stdinText: invocation.stdinText });
347
+ }
348
+
349
+ async function login(options = {}) {
350
+ const config = options.config || loadConfig();
351
+ const workerConfig = resolveWorkerConfig(config, options.worker);
352
+ const providerOverridden = Boolean(options.provider && options.provider !== workerConfig?.provider);
353
+ const provider = options.provider
354
+ ? resolveProvider(config, { ...(workerConfig || {}), provider: options.provider })
355
+ : resolveProvider(config, workerConfig);
356
+ const account = options.account
357
+ || (!providerOverridden && provider.name === workerConfig?.provider ? workerConfig?.account : '')
358
+ || (config.accountsDir ? accountNameForWorker(options.worker, provider.name) : '');
359
+ const accountHome = !providerOverridden && workerConfig?.codexHome
360
+ ? path.resolve(workerConfig.codexHome)
361
+ : resolveAccountHome(config, account);
362
+ const cwd = path.resolve(options.cwd || workerConfig?.cwd || config.defaultProjectPath);
363
+ const env = buildChildEnv(config, accountHome, options.env, provider);
364
+ const command = options.command || workerConfig?.command || provider.command || config.codexNodeCommand || config.codexCommandPath || config.codexCommand;
365
+ const baseArgs = resolveCommandArgs(config, workerConfig, provider);
366
+ const loginArgs = provider.name === 'claude' ? ['auth'] : ['login'];
367
+
368
+ return new Promise((resolve, reject) => {
369
+ // login 必须继承 stdio,方便浏览器/设备码等交互式登录流程正常显示。
370
+ const launch = buildLaunch(command, [...baseArgs, ...loginArgs], env);
371
+ const child = spawn(launch.command, launch.args, {
372
+ cwd,
373
+ env,
374
+ stdio: 'inherit',
375
+ shell: false,
376
+ windowsVerbatimArguments: launch.windowsVerbatimArguments === true,
377
+ });
378
+
379
+ child.on('error', reject);
380
+ child.on('close', (code, signal) => resolve({ code, signal, accountHome, cwd }));
381
+ });
382
+ }
383
+
384
+ function resolveCommandArgs(config, workerConfig, provider = resolveProvider(config, workerConfig)) {
385
+ if (Array.isArray(workerConfig?.commandArgs)) {
386
+ return workerConfig.commandArgs;
387
+ }
388
+ if (Array.isArray(provider.commandArgs)) {
389
+ return provider.commandArgs;
390
+ }
391
+ if (Array.isArray(config.codexNodeArgs) && config.codexNodeArgs.length > 0) {
392
+ return config.codexNodeArgs;
393
+ }
394
+ return [];
395
+ }
396
+
397
+ async function doctor(options = {}) {
398
+ const config = options.config || loadConfig();
399
+ const workerConfig = resolveWorkerConfig(config, options.worker);
400
+ const provider = options.provider
401
+ ? resolveProvider(config, { ...(workerConfig || {}), provider: options.provider })
402
+ : resolveProvider(config, workerConfig);
403
+ return runCodex([provider.name === 'claude' ? 'doctor' : 'doctor'], {
404
+ ...options,
405
+ config,
406
+ stdio: ['ignore', 'pipe', 'pipe'],
407
+ stream: options.stream !== false,
408
+ });
409
+ }
410
+
411
+ function needsShell(command) {
412
+ return process.platform === 'win32' && /\.(cmd|bat)$/i.test(String(command || ''));
413
+ }
414
+
415
+ function buildLaunch(command, args, env = process.env) {
416
+ const resolvedCommand = process.platform === 'win32'
417
+ ? resolveWindowsCommand(command, env)
418
+ : command;
419
+
420
+ if (process.platform === 'win32' && /\.ps1$/i.test(String(resolvedCommand || ''))) {
421
+ return {
422
+ command: 'powershell.exe',
423
+ args: ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', resolvedCommand, ...args],
424
+ };
425
+ }
426
+
427
+ if (!needsShell(resolvedCommand)) {
428
+ return { command: resolvedCommand, args };
429
+ }
430
+
431
+ const comspec = env.ComSpec || process.env.ComSpec || 'cmd.exe';
432
+ return {
433
+ command: comspec,
434
+ args: ['/d', '/c', 'call', resolvedCommand, ...args],
435
+ };
436
+ }
437
+
438
+ function resolveWindowsCommand(command, env = process.env) {
439
+ const text = String(command || '').trim();
440
+ if (!text || process.platform !== 'win32') return command;
441
+
442
+ const candidates = buildWindowsCommandCandidates(text, env);
443
+ for (const candidate of candidates) {
444
+ if (isExistingFile(candidate)) return candidate;
445
+ }
446
+ return command;
447
+ }
448
+
449
+ function buildWindowsCommandCandidates(command, env) {
450
+ const hasDirectory = /[\\/]/.test(command);
451
+ const hasExtension = Boolean(path.extname(command));
452
+ const names = hasExtension
453
+ ? [command]
454
+ : [...windowsExecutableExtensions(env).map((extension) => `${command}${extension}`), command];
455
+
456
+ if (hasDirectory) {
457
+ return names;
458
+ }
459
+
460
+ const pathValue = env.Path || env.PATH || '';
461
+ const directories = pathValue.split(path.delimiter).filter(Boolean);
462
+ return directories.flatMap((directory) => names.map((name) => path.join(directory, name)));
463
+ }
464
+
465
+ function windowsExecutableExtensions(env) {
466
+ const preferred = ['.EXE', '.COM', '.CMD', '.BAT', '.PS1'];
467
+ const values = String(env.PATHEXT || '')
468
+ .split(';')
469
+ .map((item) => item.trim())
470
+ .filter(Boolean)
471
+ .map((item) => (item.startsWith('.') ? item : `.${item}`));
472
+ const seen = new Set();
473
+ return [...preferred, ...values]
474
+ .map((item) => item.toUpperCase())
475
+ .filter((item) => {
476
+ if (seen.has(item)) return false;
477
+ seen.add(item);
478
+ return true;
479
+ });
480
+ }
481
+
482
+ function isExistingFile(filePath) {
483
+ try {
484
+ return fs.statSync(filePath).isFile();
485
+ } catch {
486
+ return false;
487
+ }
488
+ }
489
+
490
+ function quoteCmdArg(value) {
491
+ const text = String(value);
492
+ if (text.length === 0) return '""';
493
+ return `"${text.replace(/"/g, '\\"')}"`;
494
+ }
495
+
496
+ module.exports = {
497
+ CONFIG_PATH,
498
+ loadConfig,
499
+ resolveAccountHome,
500
+ resolveAccountName,
501
+ accountNameForWorker,
502
+ resolveWorkerConfig,
503
+ resolveProvider,
504
+ buildChildEnv,
505
+ buildExecInvocation,
506
+ buildExecArgs,
507
+ summarizePromptTransport,
508
+ runCodex,
509
+ execPrompt,
510
+ login,
511
+ doctor,
512
+ needsShell,
513
+ buildLaunch,
514
+ resolveCommandArgs,
515
+ writeStreamChunk,
516
+ };
517
+
518
+
519
+
@@ -0,0 +1,16 @@
1
+ {
2
+ "defaultProvider": "codex",
3
+ "providers": {
4
+ "codex": {
5
+ "command": "codex",
6
+ "commandArgs": [],
7
+ "homeEnv": "CODEX_HOME"
8
+ },
9
+ "claude": {
10
+ "command": "claude",
11
+ "commandArgs": [],
12
+ "homeEnv": "CLAUDE_CONFIG_DIR"
13
+ }
14
+ },
15
+ "timeoutMs": 1800000
16
+ }