@a5c-ai/agent-mux-cli 0.4.10-staging.ff407b73 → 5.0.1-staging.00fa5317c
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.
- package/dist/commands/launch-bridge-hooks.d.ts +59 -0
- package/dist/commands/launch-bridge-hooks.d.ts.map +1 -0
- package/dist/commands/launch-bridge-hooks.js +228 -0
- package/dist/commands/launch-bridge-hooks.js.map +1 -0
- package/dist/commands/launch-completion-engine.d.ts +7 -0
- package/dist/commands/launch-completion-engine.d.ts.map +1 -0
- package/dist/commands/launch-completion-engine.js +8 -0
- package/dist/commands/launch-completion-engine.js.map +1 -0
- package/dist/commands/launch.d.ts +6 -0
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +802 -37
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +7 -12
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/tui.d.ts.map +1 -1
- package/dist/commands/tui.js +1 -0
- package/dist/commands/tui.js.map +1 -1
- package/package.json +6 -6
package/dist/commands/launch.js
CHANGED
|
@@ -11,9 +11,11 @@ import { startTransportMuxRuntime } from '@a5c-ai/transport-mux';
|
|
|
11
11
|
import { flagStr, flagNum, flagBool, flagArr } from '../parse-args.js';
|
|
12
12
|
import { ExitCode } from '../exit-codes.js';
|
|
13
13
|
import { printError, printJsonError } from '../output.js';
|
|
14
|
+
import { resolve as resolvePath } from 'node:path';
|
|
14
15
|
/** Launch-specific flag definitions (global flags like model/json/debug are excluded). */
|
|
15
16
|
export const LAUNCH_FLAGS = {
|
|
16
17
|
'api-key': { type: 'string' },
|
|
18
|
+
'profile': { type: 'string' },
|
|
17
19
|
'api-base': { type: 'string' },
|
|
18
20
|
'region': { type: 'string' },
|
|
19
21
|
'project': { type: 'string' },
|
|
@@ -29,6 +31,7 @@ export const LAUNCH_FLAGS = {
|
|
|
29
31
|
'resume': { short: 'r', type: 'string' },
|
|
30
32
|
'session-id': { short: 's', type: 'string' },
|
|
31
33
|
'prompt': { short: 'p', type: 'string' },
|
|
34
|
+
'interactive': { short: 'i', type: 'boolean' },
|
|
32
35
|
'max-turns': { type: 'number' },
|
|
33
36
|
'max-budget-usd': { type: 'number' },
|
|
34
37
|
'dry-run': { type: 'boolean' },
|
|
@@ -39,14 +42,16 @@ export const LAUNCH_FLAGS = {
|
|
|
39
42
|
'workspace-mode': { type: 'string' },
|
|
40
43
|
'workspace-repo': { type: 'string', repeatable: true },
|
|
41
44
|
'workspace-name': { type: 'string' },
|
|
45
|
+
'yolo': { type: 'boolean' },
|
|
46
|
+
'bridge-interactive': { type: 'boolean' },
|
|
47
|
+
'bridge-hooks': { type: 'boolean' },
|
|
42
48
|
};
|
|
43
49
|
// ---------------------------------------------------------------------------
|
|
44
50
|
// Plan resolution
|
|
45
51
|
// ---------------------------------------------------------------------------
|
|
46
52
|
export function resolveLaunchPlan(input) {
|
|
47
|
-
const providerId = (input.provider ?? 'anthropic');
|
|
48
53
|
const providerConfig = resolveProvider({
|
|
49
|
-
provider:
|
|
54
|
+
provider: input.provider,
|
|
50
55
|
model: input.model,
|
|
51
56
|
transport: input.transport,
|
|
52
57
|
apiKey: input.apiKey,
|
|
@@ -56,6 +61,7 @@ export function resolveLaunchPlan(input) {
|
|
|
56
61
|
resourceGroup: input.resourceGroup,
|
|
57
62
|
endpointName: input.endpointName,
|
|
58
63
|
authCommand: input.authCommand,
|
|
64
|
+
profile: input.profile,
|
|
59
65
|
});
|
|
60
66
|
// Merge extra provider args into params
|
|
61
67
|
if (input.providerArgs) {
|
|
@@ -71,29 +77,34 @@ export function resolveLaunchPlan(input) {
|
|
|
71
77
|
}
|
|
72
78
|
else {
|
|
73
79
|
proxyNeeded = false;
|
|
74
|
-
proxyReason = `${input.harness} supports ${
|
|
80
|
+
proxyReason = `${input.harness} supports ${providerConfig.provider} natively`;
|
|
75
81
|
}
|
|
76
82
|
}
|
|
77
83
|
else {
|
|
78
84
|
if (input.proxyMode === 'never') {
|
|
79
|
-
throw new Error(`${input.harness} does not support ${
|
|
85
|
+
throw new Error(`${input.harness} does not support ${providerConfig.provider} natively. ` +
|
|
80
86
|
`Use --with-proxy-if-needed to auto-launch the proxy.`);
|
|
81
87
|
}
|
|
82
88
|
proxyReason =
|
|
83
|
-
`${input.harness} does not support ${
|
|
84
|
-
`proxy bridges ${
|
|
89
|
+
`${input.harness} does not support ${providerConfig.provider} natively; ` +
|
|
90
|
+
`proxy bridges ${providerConfig.provider} → ${translation.proxyExposedTransport}`;
|
|
85
91
|
}
|
|
86
92
|
const proxy = proxyNeeded
|
|
87
93
|
? {
|
|
88
|
-
targetProvider:
|
|
94
|
+
targetProvider: providerConfig.provider,
|
|
89
95
|
targetModel: providerConfig.model,
|
|
90
96
|
exposedTransport: translation.proxyExposedTransport ?? 'openai-chat',
|
|
91
97
|
port: input.proxyPort ?? 0,
|
|
98
|
+
apiBase: providerConfig.params['apiBase'] ? String(providerConfig.params['apiBase']) : undefined,
|
|
99
|
+
apiKey: providerConfig.auth.apiKey,
|
|
100
|
+
project: providerConfig.params['project'] ? String(providerConfig.params['project']) : undefined,
|
|
101
|
+
location: providerConfig.params['region'] ? String(providerConfig.params['region']) : undefined,
|
|
102
|
+
useVertexAi: providerConfig.provider === 'vertex' || Boolean(providerConfig.params['useVertexAi']),
|
|
92
103
|
}
|
|
93
104
|
: undefined;
|
|
94
105
|
return {
|
|
95
106
|
harness: input.harness,
|
|
96
|
-
provider:
|
|
107
|
+
provider: providerConfig.provider,
|
|
97
108
|
transport: providerConfig.transport,
|
|
98
109
|
model: providerConfig.model,
|
|
99
110
|
proxyNeeded,
|
|
@@ -105,14 +116,22 @@ export function resolveLaunchPlan(input) {
|
|
|
105
116
|
};
|
|
106
117
|
}
|
|
107
118
|
function appendHarnessSessionArgs(plan, session) {
|
|
119
|
+
const interactive = session.interactive !== false;
|
|
108
120
|
switch (plan.harness) {
|
|
109
121
|
case 'claude':
|
|
122
|
+
if (session.bridgeInteractive) {
|
|
123
|
+
plan.args.push('--bare');
|
|
124
|
+
}
|
|
110
125
|
if (session.resumeId)
|
|
111
126
|
plan.args.push('--resume', session.resumeId);
|
|
112
127
|
if (session.sessionId)
|
|
113
128
|
plan.args.push('--session-id', session.sessionId);
|
|
114
|
-
if (session.prompt)
|
|
115
|
-
plan.args.push('
|
|
129
|
+
if (session.prompt && !interactive) {
|
|
130
|
+
plan.args.push('-p', session.prompt);
|
|
131
|
+
}
|
|
132
|
+
if (session.prompt && interactive && !session.bridgeInteractive) {
|
|
133
|
+
plan.args.push(session.prompt);
|
|
134
|
+
}
|
|
116
135
|
if (session.maxTurns)
|
|
117
136
|
plan.args.push('--max-turns', String(session.maxTurns));
|
|
118
137
|
break;
|
|
@@ -120,7 +139,7 @@ function appendHarnessSessionArgs(plan, session) {
|
|
|
120
139
|
if (session.resumeId) {
|
|
121
140
|
plan.args.unshift('resume', session.resumeId);
|
|
122
141
|
}
|
|
123
|
-
else if (session.prompt) {
|
|
142
|
+
else if (session.prompt && !interactive) {
|
|
124
143
|
plan.args.unshift('exec', session.prompt);
|
|
125
144
|
}
|
|
126
145
|
break;
|
|
@@ -128,16 +147,186 @@ function appendHarnessSessionArgs(plan, session) {
|
|
|
128
147
|
if (session.prompt)
|
|
129
148
|
plan.args.push('--prompt', session.prompt);
|
|
130
149
|
break;
|
|
150
|
+
case 'pi':
|
|
151
|
+
if (session.prompt && !interactive && !session.bridgeInteractive) {
|
|
152
|
+
plan.args.push('--print', session.prompt);
|
|
153
|
+
}
|
|
154
|
+
break;
|
|
131
155
|
case 'opencode':
|
|
132
156
|
if (session.resumeId)
|
|
133
157
|
plan.args.push('--session', session.resumeId);
|
|
134
|
-
// OpenCode has no non-interactive prompt flag; prompt delivered via stdin after launch
|
|
135
158
|
break;
|
|
136
159
|
}
|
|
137
160
|
}
|
|
138
161
|
// ---------------------------------------------------------------------------
|
|
139
162
|
// Provider auth validation helper
|
|
140
163
|
// ---------------------------------------------------------------------------
|
|
164
|
+
async function prepareHarnessAutomationState(harness, cwd, env) {
|
|
165
|
+
if (!isAutomationPreseedEnabled(env))
|
|
166
|
+
return;
|
|
167
|
+
if (harness === 'claude')
|
|
168
|
+
await prepareClaudeAutomationState(cwd, env);
|
|
169
|
+
if (harness === 'codex')
|
|
170
|
+
await prepareCodexAutomationState(cwd);
|
|
171
|
+
}
|
|
172
|
+
function isAutomationPreseedEnabled(env) {
|
|
173
|
+
return env['AMUX_PRESEED_HARNESS_ONBOARDING'] === '1' || env['CI'] === 'true' || env['GITHUB_ACTIONS'] === 'true' || process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true';
|
|
174
|
+
}
|
|
175
|
+
function automationHome() {
|
|
176
|
+
return process.env['HOME'] || process.env['USERPROFILE'];
|
|
177
|
+
}
|
|
178
|
+
async function readJsonObject(filePath) {
|
|
179
|
+
try {
|
|
180
|
+
const fs = await import('node:fs/promises');
|
|
181
|
+
const value = JSON.parse(await fs.readFile(filePath, 'utf8'));
|
|
182
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function writeJsonObject(filePath, value) {
|
|
189
|
+
const { dirname } = await import('node:path');
|
|
190
|
+
const fs = await import('node:fs/promises');
|
|
191
|
+
await fs.mkdir(dirname(filePath), { recursive: true });
|
|
192
|
+
await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`);
|
|
193
|
+
}
|
|
194
|
+
function recordObject(value) {
|
|
195
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
196
|
+
}
|
|
197
|
+
function numberAtLeast(value, minimum) {
|
|
198
|
+
const numeric = Number(value);
|
|
199
|
+
return Number.isFinite(numeric) ? Math.max(numeric, minimum) : minimum;
|
|
200
|
+
}
|
|
201
|
+
function approveClaudeCustomApiKey(config, env) {
|
|
202
|
+
const apiKey = env['ANTHROPIC_API_KEY'] || process.env['ANTHROPIC_API_KEY'];
|
|
203
|
+
if (!apiKey)
|
|
204
|
+
return;
|
|
205
|
+
const fingerprint = apiKey.slice(-20);
|
|
206
|
+
const responses = recordObject(config['customApiKeyResponses']);
|
|
207
|
+
const approved = Array.isArray(responses['approved']) ? responses['approved'].filter((value) => typeof value === 'string') : [];
|
|
208
|
+
const rejected = Array.isArray(responses['rejected']) ? responses['rejected'].filter((value) => typeof value === 'string' && value !== fingerprint) : [];
|
|
209
|
+
if (!approved.includes(fingerprint))
|
|
210
|
+
approved.push(fingerprint);
|
|
211
|
+
config['customApiKeyResponses'] = { ...responses, approved, rejected };
|
|
212
|
+
}
|
|
213
|
+
const AUTOMATION_CLAUDE_ONBOARDING_VERSION = '999.999.999';
|
|
214
|
+
async function prepareClaudeAutomationState(cwd, env) {
|
|
215
|
+
const home = automationHome();
|
|
216
|
+
if (!home)
|
|
217
|
+
return;
|
|
218
|
+
const { join, resolve } = await import('node:path');
|
|
219
|
+
const settingsPath = join(home, '.claude', 'settings.json');
|
|
220
|
+
const settings = await readJsonObject(settingsPath);
|
|
221
|
+
await writeJsonObject(settingsPath, {
|
|
222
|
+
...settings,
|
|
223
|
+
theme: typeof settings['theme'] === 'string' ? settings['theme'] : 'dark',
|
|
224
|
+
skipDangerousModePermissionPrompt: true,
|
|
225
|
+
permissions: {
|
|
226
|
+
allow: [
|
|
227
|
+
'Bash(*)', 'Read(*)', 'Write(*)', 'Edit(*)', 'Glob(*)', 'Grep(*)',
|
|
228
|
+
'WebFetch(*)', 'WebSearch(*)', 'Agent(*)', 'Skill(*)',
|
|
229
|
+
'TodoRead', 'TodoWrite',
|
|
230
|
+
],
|
|
231
|
+
deny: [],
|
|
232
|
+
...recordObject(settings['permissions']),
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
const configPath = join(home, '.claude.json');
|
|
236
|
+
const config = await readJsonObject(configPath);
|
|
237
|
+
approveClaudeCustomApiKey(config, env);
|
|
238
|
+
const projects = recordObject(config['projects']);
|
|
239
|
+
const projectPath = resolve(cwd).replace(/\\/g, '/');
|
|
240
|
+
const project = recordObject(projects[projectPath]);
|
|
241
|
+
projects[projectPath] = {
|
|
242
|
+
allowedTools: [],
|
|
243
|
+
mcpContextUris: [],
|
|
244
|
+
mcpServers: {},
|
|
245
|
+
enabledMcpjsonServers: [],
|
|
246
|
+
disabledMcpjsonServers: [],
|
|
247
|
+
hasClaudeMdExternalIncludesApproved: false,
|
|
248
|
+
hasClaudeMdExternalIncludesWarningShown: false,
|
|
249
|
+
...project,
|
|
250
|
+
projectOnboardingSeenCount: numberAtLeast(project['projectOnboardingSeenCount'], 1),
|
|
251
|
+
hasTrustDialogAccepted: true,
|
|
252
|
+
hasCompletedProjectOnboarding: true,
|
|
253
|
+
lastVersionBase: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
|
|
254
|
+
};
|
|
255
|
+
await writeJsonObject(configPath, {
|
|
256
|
+
...config,
|
|
257
|
+
numStartups: numberAtLeast(config['numStartups'], 1),
|
|
258
|
+
hasCompletedOnboarding: true,
|
|
259
|
+
lastOnboardingVersion: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
|
|
260
|
+
lastReleaseNotesSeen: AUTOMATION_CLAUDE_ONBOARDING_VERSION,
|
|
261
|
+
hasIdeOnboardingBeenShown: { vscode: true, ...recordObject(config['hasIdeOnboardingBeenShown']) },
|
|
262
|
+
officialMarketplaceAutoInstallAttempted: true,
|
|
263
|
+
officialMarketplaceAutoInstalled: true,
|
|
264
|
+
projects,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function extractPromptArtifactPaths(prompt, cwd) {
|
|
268
|
+
if (!prompt)
|
|
269
|
+
return [];
|
|
270
|
+
const matches = prompt.matchAll(/(?:^|[\s`"'])((?:\.\/)?\.a5c-live-test\/[^\s`"')]+)/g);
|
|
271
|
+
const paths = new Set();
|
|
272
|
+
for (const match of matches) {
|
|
273
|
+
const cleaned = match[1]?.replace(/[.,;:!?]+$/, '');
|
|
274
|
+
if (!cleaned)
|
|
275
|
+
continue;
|
|
276
|
+
paths.add(resolvePath(cwd, cleaned.replace(/^\.\//, '')));
|
|
277
|
+
}
|
|
278
|
+
return [...paths];
|
|
279
|
+
}
|
|
280
|
+
function startPromptArtifactCompletionMonitor(input) {
|
|
281
|
+
const expectedPaths = extractPromptArtifactPaths(input.prompt, input.cwd);
|
|
282
|
+
if (expectedPaths.length === 0)
|
|
283
|
+
return undefined;
|
|
284
|
+
const lastSizes = new Map();
|
|
285
|
+
return setInterval(() => {
|
|
286
|
+
void (async () => {
|
|
287
|
+
const fs = await import('node:fs/promises');
|
|
288
|
+
for (const expectedPath of expectedPaths) {
|
|
289
|
+
try {
|
|
290
|
+
const stat = await fs.stat(expectedPath);
|
|
291
|
+
if (!stat.isFile() || stat.size <= 0)
|
|
292
|
+
continue;
|
|
293
|
+
if (lastSizes.get(expectedPath) === stat.size) {
|
|
294
|
+
input.onComplete();
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
lastSizes.set(expectedPath, stat.size);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
// expected artifact not written yet
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
}, 1000);
|
|
305
|
+
}
|
|
306
|
+
async function prepareCodexAutomationState(cwd) {
|
|
307
|
+
const home = automationHome();
|
|
308
|
+
if (!home)
|
|
309
|
+
return;
|
|
310
|
+
const { join, resolve, dirname } = await import('node:path');
|
|
311
|
+
const fs = await import('node:fs/promises');
|
|
312
|
+
const configPath = join(home, '.codex', 'config.toml');
|
|
313
|
+
await fs.mkdir(dirname(configPath), { recursive: true });
|
|
314
|
+
let config = '';
|
|
315
|
+
try {
|
|
316
|
+
config = await fs.readFile(configPath, 'utf8');
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
config = '';
|
|
320
|
+
}
|
|
321
|
+
const projectPath = resolve(cwd);
|
|
322
|
+
const basicKey = JSON.stringify(projectPath);
|
|
323
|
+
const literalKey = `'${projectPath}'`;
|
|
324
|
+
if (config.includes(`[projects.${basicKey}]`) || config.includes(`[projects.${literalKey}]`))
|
|
325
|
+
return;
|
|
326
|
+
const prefix = config.trimEnd();
|
|
327
|
+
const addition = `[projects.${basicKey}]\ntrust_level = "trusted"\n`;
|
|
328
|
+
await fs.writeFile(configPath, `${prefix}${prefix ? '\n\n' : ''}${addition}`);
|
|
329
|
+
}
|
|
141
330
|
async function validateProviderAuth(plan) {
|
|
142
331
|
const { execSync } = await import('node:child_process');
|
|
143
332
|
try {
|
|
@@ -276,6 +465,7 @@ export async function launchCommand(client, args) {
|
|
|
276
465
|
resourceGroup: flagStr(args.flags, 'resource-group'),
|
|
277
466
|
endpointName: flagStr(args.flags, 'endpoint-name'),
|
|
278
467
|
authCommand: flagStr(args.flags, 'auth-command'),
|
|
468
|
+
profile: flagStr(args.flags, 'profile'),
|
|
279
469
|
proxyMode,
|
|
280
470
|
proxyPort: flagNum(args.flags, 'proxy-port'),
|
|
281
471
|
adapter: adapter,
|
|
@@ -296,6 +486,7 @@ export async function launchCommand(client, args) {
|
|
|
296
486
|
model: flagStr(args.flags, 'model'),
|
|
297
487
|
apiKey: flagStr(args.flags, 'api-key'),
|
|
298
488
|
authCommand: flagStr(args.flags, 'auth-command'),
|
|
489
|
+
profile: flagStr(args.flags, 'profile'),
|
|
299
490
|
});
|
|
300
491
|
if (resolvedConfig.auth.type === 'api_key' && !resolvedConfig.auth.apiKey) {
|
|
301
492
|
const defaults = (await import('@a5c-ai/agent-mux-core')).PROVIDER_DEFAULTS;
|
|
@@ -361,6 +552,45 @@ export async function launchCommand(client, args) {
|
|
|
361
552
|
}
|
|
362
553
|
launchCwd = resolveWorkspaceDefaultCwd(workspace);
|
|
363
554
|
}
|
|
555
|
+
// Resolve interactive mode (default: true)
|
|
556
|
+
const interactiveFlag = flagBool(args.flags, 'interactive');
|
|
557
|
+
const isInteractive = interactiveFlag !== false;
|
|
558
|
+
// Bridge flags: --bridge-interactive and --bridge-hooks
|
|
559
|
+
const bridgeInteractive = flagBool(args.flags, 'bridge-interactive') === true;
|
|
560
|
+
const bridgeHooks = flagBool(args.flags, 'bridge-hooks') === true;
|
|
561
|
+
if (bridgeInteractive && isInteractive) {
|
|
562
|
+
const msg = '--bridge-interactive requires --no-interactive';
|
|
563
|
+
if (jsonMode)
|
|
564
|
+
printJsonError('VALIDATION_ERROR', msg);
|
|
565
|
+
else
|
|
566
|
+
printError(msg);
|
|
567
|
+
return ExitCode.USAGE_ERROR;
|
|
568
|
+
}
|
|
569
|
+
if (bridgeHooks && isInteractive) {
|
|
570
|
+
const msg = '--bridge-hooks requires --no-interactive';
|
|
571
|
+
if (jsonMode)
|
|
572
|
+
printJsonError('VALIDATION_ERROR', msg);
|
|
573
|
+
else
|
|
574
|
+
printError(msg);
|
|
575
|
+
return ExitCode.USAGE_ERROR;
|
|
576
|
+
}
|
|
577
|
+
if (bridgeInteractive) {
|
|
578
|
+
try {
|
|
579
|
+
const { getBridgeCapabilities } = await import('@a5c-ai/agent-catalog');
|
|
580
|
+
const caps = getBridgeCapabilities(plan.harness);
|
|
581
|
+
if (!caps?.interactiveBridge) {
|
|
582
|
+
const msg = `${plan.harness} does not support interactive bridging`;
|
|
583
|
+
if (jsonMode)
|
|
584
|
+
printJsonError('CAPABILITY_ERROR', msg);
|
|
585
|
+
else
|
|
586
|
+
printError(msg);
|
|
587
|
+
return ExitCode.USAGE_ERROR;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
// agent-catalog not available — skip capability check
|
|
592
|
+
}
|
|
593
|
+
}
|
|
364
594
|
// Append session/prompt args
|
|
365
595
|
const prompt = flagStr(args.flags, 'prompt');
|
|
366
596
|
appendHarnessSessionArgs(plan, {
|
|
@@ -368,23 +598,107 @@ export async function launchCommand(client, args) {
|
|
|
368
598
|
sessionId: flagStr(args.flags, 'session-id'),
|
|
369
599
|
prompt,
|
|
370
600
|
maxTurns: flagNum(args.flags, 'max-turns'),
|
|
601
|
+
interactive: isInteractive || bridgeInteractive,
|
|
602
|
+
bridgeInteractive,
|
|
371
603
|
});
|
|
604
|
+
// Add --model for harnesses that accept it as a CLI arg
|
|
605
|
+
const modelFlag = flagStr(args.flags, 'model');
|
|
606
|
+
if (modelFlag && ['pi', 'gemini', 'opencode'].includes(plan.harness)) {
|
|
607
|
+
plan.args.push('--model', modelFlag);
|
|
608
|
+
}
|
|
609
|
+
// --yolo: add harness-specific auto-approve flags resolved through
|
|
610
|
+
// agent-catalog → atlas graph (LaunchConfig records with commArgs)
|
|
611
|
+
if (flagBool(args.flags, 'yolo')) {
|
|
612
|
+
try {
|
|
613
|
+
const { getYoloLaunchArgs } = await import('@a5c-ai/agent-catalog');
|
|
614
|
+
const yoloArgs = getYoloLaunchArgs(plan.harness);
|
|
615
|
+
if (yoloArgs.length > 0) {
|
|
616
|
+
plan.args.push(...yoloArgs);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch {
|
|
620
|
+
// agent-catalog not available
|
|
621
|
+
}
|
|
622
|
+
}
|
|
372
623
|
// Passthrough args after --
|
|
373
624
|
const dashDashIdx = process.argv.indexOf('--');
|
|
374
625
|
if (dashDashIdx >= 0) {
|
|
375
626
|
plan.args.push(...process.argv.slice(dashDashIdx + 1));
|
|
376
627
|
}
|
|
628
|
+
// Also check parsed positionals for -- separator (handles spawn() without shell)
|
|
629
|
+
const argsDashIdx = args.positionals.indexOf('--');
|
|
630
|
+
if (argsDashIdx >= 0) {
|
|
631
|
+
plan.args.push(...args.positionals.slice(argsDashIdx + 1));
|
|
632
|
+
}
|
|
377
633
|
// Launch runtime if needed
|
|
378
634
|
let proxyRuntime;
|
|
379
635
|
if (plan.proxyNeeded && plan.proxy) {
|
|
380
636
|
try {
|
|
637
|
+
// When exposed transport differs from target (e.g., anthropic→foundry),
|
|
638
|
+
// the proxy needs a completion engine to translate request/response formats.
|
|
639
|
+
let completionEngine;
|
|
640
|
+
if ((plan.proxy.targetProvider === 'google' || plan.proxy.targetProvider === 'vertex') && plan.proxy.apiKey) {
|
|
641
|
+
const { createGoogleCompletionEngine } = await import('./launch-completion-engine.js');
|
|
642
|
+
completionEngine = createGoogleCompletionEngine({
|
|
643
|
+
apiBase: plan.proxy.useVertexAi ? undefined : plan.proxy.apiBase,
|
|
644
|
+
apiKey: plan.proxy.apiKey,
|
|
645
|
+
targetModel: plan.proxy.targetModel,
|
|
646
|
+
provider: plan.proxy.targetProvider,
|
|
647
|
+
project: plan.proxy.project,
|
|
648
|
+
location: plan.proxy.location,
|
|
649
|
+
useVertexAi: plan.proxy.useVertexAi,
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
else if (plan.proxy.apiBase && plan.proxy.apiKey) {
|
|
653
|
+
const { createOpenAICompletionEngine } = await import('./launch-completion-engine.js');
|
|
654
|
+
completionEngine = createOpenAICompletionEngine({
|
|
655
|
+
apiBase: plan.proxy.apiBase,
|
|
656
|
+
apiKey: plan.proxy.apiKey,
|
|
657
|
+
targetModel: plan.proxy.targetModel,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
381
660
|
proxyRuntime = await startTransportMuxRuntime({
|
|
382
661
|
targetProvider: plan.proxy.targetProvider,
|
|
383
662
|
targetModel: `${plan.proxy.targetProvider}/${plan.proxy.targetModel}`,
|
|
384
663
|
exposedTransport: plan.proxy.exposedTransport,
|
|
385
664
|
port: plan.proxy.port,
|
|
665
|
+
apiBase: plan.proxy.apiBase,
|
|
666
|
+
completionEngine,
|
|
386
667
|
});
|
|
387
668
|
proxyRuntime.applyHarnessEnv(plan.env);
|
|
669
|
+
if (plan.env['ANTHROPIC_API_KEY']) {
|
|
670
|
+
plan.env['ANTHROPIC_AUTH_TOKEN'] = '';
|
|
671
|
+
}
|
|
672
|
+
// Pi ignores OPENAI_BASE_URL — write a models.json config that registers
|
|
673
|
+
// a custom provider pointing to the local proxy.
|
|
674
|
+
if (plan.harness === 'pi') {
|
|
675
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
676
|
+
const { join } = await import('node:path');
|
|
677
|
+
const piConfigDir = process.env['PI_CODING_AGENT_DIR']
|
|
678
|
+
?? join(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '/tmp', '.pi', 'agent');
|
|
679
|
+
mkdirSync(piConfigDir, { recursive: true });
|
|
680
|
+
const modelsConfig = {
|
|
681
|
+
providers: {
|
|
682
|
+
'amux-proxy': {
|
|
683
|
+
baseUrl: `${proxyRuntime.url}/v1`,
|
|
684
|
+
api: 'openai-completions',
|
|
685
|
+
apiKey: proxyRuntime.authToken ?? 'proxy-token',
|
|
686
|
+
models: [{
|
|
687
|
+
id: plan.model,
|
|
688
|
+
reasoning: false,
|
|
689
|
+
input: ['text'],
|
|
690
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
691
|
+
contextWindow: 128000,
|
|
692
|
+
maxTokens: 16384,
|
|
693
|
+
}],
|
|
694
|
+
},
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
const modelsPath = join(piConfigDir, 'models.json');
|
|
698
|
+
writeFileSync(modelsPath, JSON.stringify(modelsConfig, null, 2));
|
|
699
|
+
plan.args.push('--provider', 'amux-proxy');
|
|
700
|
+
console.error(`[amux launch] Pi proxy config written to ${modelsPath}, proxy at ${proxyRuntime.url}`);
|
|
701
|
+
}
|
|
388
702
|
}
|
|
389
703
|
catch (err) {
|
|
390
704
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -395,14 +709,40 @@ export async function launchCommand(client, args) {
|
|
|
395
709
|
return ExitCode.GENERAL_ERROR;
|
|
396
710
|
}
|
|
397
711
|
}
|
|
712
|
+
// Bridge hooks: emulate lifecycle hooks when --bridge-hooks is set
|
|
713
|
+
let bridgeHookEmulator;
|
|
714
|
+
if (bridgeHooks) {
|
|
715
|
+
const { BridgeHookEmulator } = await import('./launch-bridge-hooks.js');
|
|
716
|
+
bridgeHookEmulator = new BridgeHookEmulator({
|
|
717
|
+
harness: plan.harness,
|
|
718
|
+
cwd: launchCwd,
|
|
719
|
+
env: plan.env,
|
|
720
|
+
sessionId: flagStr(args.flags, 'session-id'),
|
|
721
|
+
runsDir: plan.env['BABYSITTER_RUNS_DIR'] || undefined,
|
|
722
|
+
verbose: flagBool(args.flags, 'debug') === true,
|
|
723
|
+
});
|
|
724
|
+
await bridgeHookEmulator.emulateSessionStart();
|
|
725
|
+
}
|
|
726
|
+
await prepareHarnessAutomationState(plan.harness, launchCwd, plan.env);
|
|
398
727
|
// Spawn harness
|
|
399
|
-
|
|
400
|
-
let child;
|
|
728
|
+
let child = null;
|
|
401
729
|
let ptyProcess = null;
|
|
730
|
+
let ptyTerminationExpected = false;
|
|
731
|
+
const ptyCleanup = [];
|
|
732
|
+
const completePtyPrompt = () => {
|
|
733
|
+
if (!ptyProcess || ptyTerminationExpected)
|
|
734
|
+
return;
|
|
735
|
+
ptyTerminationExpected = true;
|
|
736
|
+
try {
|
|
737
|
+
ptyProcess.kill('SIGTERM');
|
|
738
|
+
}
|
|
739
|
+
catch { /* */ }
|
|
740
|
+
};
|
|
402
741
|
if (isInteractive) {
|
|
403
|
-
//
|
|
742
|
+
// Interactive mode: full TTY passthrough. If a prompt is provided, it's
|
|
743
|
+
// injected as initial stdin after the harness starts (like typing it in).
|
|
404
744
|
try {
|
|
405
|
-
const nodePty =
|
|
745
|
+
const nodePty = await import('node-pty');
|
|
406
746
|
ptyProcess = nodePty.spawn(plan.command, plan.args, {
|
|
407
747
|
name: 'xterm-256color',
|
|
408
748
|
cols: process.stdout.columns || 80,
|
|
@@ -410,8 +750,70 @@ export async function launchCommand(client, args) {
|
|
|
410
750
|
cwd: launchCwd,
|
|
411
751
|
env: { ...process.env, ...plan.env },
|
|
412
752
|
});
|
|
413
|
-
//
|
|
414
|
-
|
|
753
|
+
// End-of-turn detection: parse PTY output through adapter's event system
|
|
754
|
+
let turnDetected = false;
|
|
755
|
+
let lineBuf = '';
|
|
756
|
+
let assembler = null;
|
|
757
|
+
let adapter = null;
|
|
758
|
+
try {
|
|
759
|
+
const core = await import('@a5c-ai/agent-mux-core');
|
|
760
|
+
assembler = new core.StreamAssembler();
|
|
761
|
+
// Resolve the adapter for this harness to use its parseEvent
|
|
762
|
+
const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
|
|
763
|
+
const factory = adaptersModule.getAdapterFactory?.(plan.harness);
|
|
764
|
+
adapter = factory ? factory() : null;
|
|
765
|
+
}
|
|
766
|
+
catch { /* core/adapters not available */ }
|
|
767
|
+
// Pipe PTY to stdout + feed through event parser for turn detection
|
|
768
|
+
let interactiveOutputBuf = '';
|
|
769
|
+
let interactiveApiKeyHandled = false;
|
|
770
|
+
let interactiveBypassHandled = false;
|
|
771
|
+
ptyProcess.onData((data) => {
|
|
772
|
+
process.stdout.write(data);
|
|
773
|
+
interactiveOutputBuf += data;
|
|
774
|
+
// Auto-respond to Claude Code onboarding prompts
|
|
775
|
+
const stripped = interactiveOutputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
776
|
+
if (!interactiveApiKeyHandled && stripped.includes('usethisAPIkey')) {
|
|
777
|
+
interactiveApiKeyHandled = true;
|
|
778
|
+
setTimeout(() => ptyProcess.write('\x1b[A\r'), 200);
|
|
779
|
+
}
|
|
780
|
+
if (!interactiveBypassHandled && stripped.includes('BypassPermissionsmode')) {
|
|
781
|
+
interactiveBypassHandled = true;
|
|
782
|
+
setTimeout(() => ptyProcess.write('\x1b[B\r'), 200);
|
|
783
|
+
}
|
|
784
|
+
if (!assembler || !adapter || turnDetected)
|
|
785
|
+
return;
|
|
786
|
+
// Strip ANSI escapes, then feed lines to the event parser
|
|
787
|
+
const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
|
|
788
|
+
lineBuf += clean;
|
|
789
|
+
let idx;
|
|
790
|
+
while ((idx = lineBuf.indexOf('\n')) !== -1) {
|
|
791
|
+
const line = lineBuf.slice(0, idx).replace(/\r$/, '');
|
|
792
|
+
lineBuf = lineBuf.slice(idx + 1);
|
|
793
|
+
if (line.length === 0)
|
|
794
|
+
continue;
|
|
795
|
+
const assembled = assembler.feed(line);
|
|
796
|
+
if (assembled === null)
|
|
797
|
+
continue;
|
|
798
|
+
try {
|
|
799
|
+
const ctx = { runId: 'launch', agent: plan.harness, sessionId: undefined, turnIndex: 0, debug: false, outputFormat: 'text', source: 'stdout', assembler, eventCount: 0, lastEventType: null, adapterState: {} };
|
|
800
|
+
const result = adapter.parseEvent(assembled, ctx);
|
|
801
|
+
if (result === null)
|
|
802
|
+
continue;
|
|
803
|
+
const events = Array.isArray(result) ? result : [result];
|
|
804
|
+
for (const ev of events) {
|
|
805
|
+
// Detect turn completion events
|
|
806
|
+
if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
|
|
807
|
+
turnDetected = true;
|
|
808
|
+
// Give the harness a moment to flush output, then end the PTY.
|
|
809
|
+
setTimeout(completePtyPrompt, 1000);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
catch { /* parse error — ignore */ }
|
|
815
|
+
}
|
|
816
|
+
});
|
|
415
817
|
if (process.stdin.isTTY) {
|
|
416
818
|
process.stdin.setRawMode(true);
|
|
417
819
|
}
|
|
@@ -421,27 +823,324 @@ export async function launchCommand(client, args) {
|
|
|
421
823
|
process.stdout.on('resize', () => {
|
|
422
824
|
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
423
825
|
});
|
|
826
|
+
if (prompt && plan.args.some(a => a === prompt)) {
|
|
827
|
+
let artifactMonitor;
|
|
828
|
+
artifactMonitor = startPromptArtifactCompletionMonitor({
|
|
829
|
+
prompt,
|
|
830
|
+
cwd: launchCwd,
|
|
831
|
+
onComplete: () => {
|
|
832
|
+
if (artifactMonitor)
|
|
833
|
+
clearInterval(artifactMonitor);
|
|
834
|
+
completePtyPrompt();
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
ptyCleanup.push(() => { if (artifactMonitor)
|
|
838
|
+
clearInterval(artifactMonitor); });
|
|
839
|
+
}
|
|
840
|
+
// Inject prompt after observed onboarding prompts are dismissed.
|
|
841
|
+
if (prompt && !plan.args.some(a => a === prompt)) {
|
|
842
|
+
const startedAt = Date.now();
|
|
843
|
+
let promptInjected = false;
|
|
844
|
+
let artifactMonitor;
|
|
845
|
+
const injectPrompt = () => {
|
|
846
|
+
if (promptInjected)
|
|
847
|
+
return;
|
|
848
|
+
promptInjected = true;
|
|
849
|
+
ptyProcess.write(prompt);
|
|
850
|
+
setTimeout(() => ptyProcess.write('\r'), 500);
|
|
851
|
+
artifactMonitor = startPromptArtifactCompletionMonitor({
|
|
852
|
+
prompt,
|
|
853
|
+
cwd: launchCwd,
|
|
854
|
+
onComplete: () => {
|
|
855
|
+
if (artifactMonitor)
|
|
856
|
+
clearInterval(artifactMonitor);
|
|
857
|
+
completePtyPrompt();
|
|
858
|
+
},
|
|
859
|
+
});
|
|
860
|
+
ptyCleanup.push(() => { if (artifactMonitor)
|
|
861
|
+
clearInterval(artifactMonitor); });
|
|
862
|
+
};
|
|
863
|
+
const checkAndInject = () => {
|
|
864
|
+
if (promptInjected)
|
|
865
|
+
return;
|
|
866
|
+
if (interactiveOutputBuf.length === 0) {
|
|
867
|
+
if (Date.now() - startedAt >= 1000)
|
|
868
|
+
injectPrompt();
|
|
869
|
+
else
|
|
870
|
+
setTimeout(checkAndInject, 100);
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const s = interactiveOutputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
874
|
+
if (interactiveApiKeyHandled || interactiveBypassHandled) {
|
|
875
|
+
setTimeout(injectPrompt, 2000);
|
|
876
|
+
}
|
|
877
|
+
else if (s.includes('APIkey') || s.includes('Bypass')) {
|
|
878
|
+
setTimeout(checkAndInject, 500);
|
|
879
|
+
}
|
|
880
|
+
else {
|
|
881
|
+
setTimeout(injectPrompt, 3000);
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
checkAndInject();
|
|
885
|
+
}
|
|
424
886
|
// Create a fake ChildProcess-like for signal handling
|
|
425
887
|
child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
|
|
426
888
|
}
|
|
427
889
|
catch {
|
|
428
|
-
// node-pty not available, fall back to stdio inherit
|
|
890
|
+
// node-pty not available, fall back to stdio inherit with stdin pipe for prompt injection
|
|
429
891
|
const { spawn } = await import('node:child_process');
|
|
430
892
|
child = spawn(plan.command, plan.args, {
|
|
431
|
-
stdio: 'inherit',
|
|
893
|
+
stdio: prompt ? ['pipe', 'inherit', 'inherit'] : 'inherit',
|
|
432
894
|
env: { ...process.env, ...plan.env },
|
|
433
895
|
cwd: launchCwd,
|
|
434
|
-
shell:
|
|
896
|
+
shell: process.platform === 'win32',
|
|
435
897
|
});
|
|
436
898
|
}
|
|
437
899
|
}
|
|
900
|
+
else if (bridgeInteractive) {
|
|
901
|
+
// Bridge-interactive: spawn via PTY like interactive mode, but:
|
|
902
|
+
// - No human stdin forwarding
|
|
903
|
+
// - Parse PTY output via adapter for structured events
|
|
904
|
+
// - Emit events as NDJSON to stdout
|
|
905
|
+
// - Auto-kill on turn completion
|
|
906
|
+
// - Buffer PTY output to avoid pipe deadlock (stdout is piped)
|
|
907
|
+
// Pre-create full Claude Code automation state to skip all onboarding prompts
|
|
908
|
+
if (plan.harness === 'claude') {
|
|
909
|
+
await prepareClaudeAutomationState(launchCwd, plan.env);
|
|
910
|
+
}
|
|
911
|
+
let nodePty;
|
|
912
|
+
try {
|
|
913
|
+
nodePty = await import('node-pty');
|
|
914
|
+
}
|
|
915
|
+
catch {
|
|
916
|
+
const msg = '--bridge-interactive requires node-pty but it is not available. Install it with: npm install node-pty';
|
|
917
|
+
if (jsonMode)
|
|
918
|
+
printJsonError('SPAWN_ERROR', msg);
|
|
919
|
+
else
|
|
920
|
+
printError(msg);
|
|
921
|
+
return ExitCode.GENERAL_ERROR;
|
|
922
|
+
}
|
|
923
|
+
ptyProcess = nodePty.spawn(plan.command, plan.args, {
|
|
924
|
+
name: 'xterm-256color',
|
|
925
|
+
cols: 120,
|
|
926
|
+
rows: 40,
|
|
927
|
+
cwd: launchCwd,
|
|
928
|
+
env: { ...process.env, ...plan.env },
|
|
929
|
+
});
|
|
930
|
+
// Set up adapter + assembler for parsing PTY output into structured events
|
|
931
|
+
let assembler = null;
|
|
932
|
+
let adapter = null;
|
|
933
|
+
try {
|
|
934
|
+
const core = await import('@a5c-ai/agent-mux-core');
|
|
935
|
+
assembler = new core.StreamAssembler();
|
|
936
|
+
const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
|
|
937
|
+
const factory = adaptersModule.getAdapterFactory?.(plan.harness);
|
|
938
|
+
adapter = factory ? factory() : null;
|
|
939
|
+
}
|
|
940
|
+
catch { /* core/adapters not available — raw output only */ }
|
|
941
|
+
/** Emit a bridge event as NDJSON, deferred to avoid blocking PTY callback. */
|
|
942
|
+
function emitBridgeEvent(event) {
|
|
943
|
+
const line = JSON.stringify(event) + '\n';
|
|
944
|
+
setImmediate(() => {
|
|
945
|
+
try {
|
|
946
|
+
process.stdout.write(line);
|
|
947
|
+
}
|
|
948
|
+
catch { /* stdout closed */ }
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
let turnComplete = false;
|
|
952
|
+
let lineBuf = '';
|
|
953
|
+
let outputBuf = '';
|
|
954
|
+
let eventCount = 0;
|
|
955
|
+
let apiKeyPromptHandled = false;
|
|
956
|
+
let bypassPromptHandled = false;
|
|
957
|
+
let idleTimer = null;
|
|
958
|
+
const IDLE_TIMEOUT_MS = 30_000;
|
|
959
|
+
const harnessesWithEndEvents = new Set(['claude', 'codex', 'gemini', 'opencode']);
|
|
960
|
+
const useIdleTimeout = !harnessesWithEndEvents.has(plan.harness);
|
|
961
|
+
const parseCtx = {
|
|
962
|
+
runId: 'bridge',
|
|
963
|
+
agent: plan.harness,
|
|
964
|
+
sessionId: undefined,
|
|
965
|
+
turnIndex: 0,
|
|
966
|
+
debug: false,
|
|
967
|
+
outputFormat: 'text',
|
|
968
|
+
source: 'stdout',
|
|
969
|
+
assembler: assembler,
|
|
970
|
+
eventCount: 0,
|
|
971
|
+
lastEventType: null,
|
|
972
|
+
adapterState: {},
|
|
973
|
+
};
|
|
974
|
+
ptyProcess.onData((data) => {
|
|
975
|
+
// Buffer all PTY output — never write synchronously to stdout (pipe deadlock)
|
|
976
|
+
outputBuf += data;
|
|
977
|
+
// Auto-respond to Claude Code interactive prompts that block automation.
|
|
978
|
+
// ANSI cursor-move codes replace spaces, so stripped text is concatenated.
|
|
979
|
+
const stripped = outputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
980
|
+
if (!apiKeyPromptHandled && stripped.includes('usethisAPIkey')) {
|
|
981
|
+
apiKeyPromptHandled = true;
|
|
982
|
+
// Default is "No (recommended)". Send Up arrow + Enter to select "Yes".
|
|
983
|
+
setTimeout(() => ptyProcess.write('\x1b[A\r'), 200);
|
|
984
|
+
}
|
|
985
|
+
if (!bypassPromptHandled && stripped.includes('BypassPermissionsmode')) {
|
|
986
|
+
bypassPromptHandled = true;
|
|
987
|
+
// Default is "No, exit". Send Down arrow + Enter to select "Yes, I accept".
|
|
988
|
+
setTimeout(() => ptyProcess.write('\x1b[B\r'), 200);
|
|
989
|
+
}
|
|
990
|
+
if (!assembler || !adapter || turnComplete)
|
|
991
|
+
return;
|
|
992
|
+
// Strip ANSI escapes, then feed lines to the event parser
|
|
993
|
+
const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
|
|
994
|
+
lineBuf += clean;
|
|
995
|
+
let idx;
|
|
996
|
+
while ((idx = lineBuf.indexOf('\n')) !== -1) {
|
|
997
|
+
const line = lineBuf.slice(0, idx).replace(/\r$/, '');
|
|
998
|
+
lineBuf = lineBuf.slice(idx + 1);
|
|
999
|
+
if (line.length === 0)
|
|
1000
|
+
continue;
|
|
1001
|
+
const assembled = assembler.feed(line);
|
|
1002
|
+
if (assembled === null)
|
|
1003
|
+
continue;
|
|
1004
|
+
try {
|
|
1005
|
+
parseCtx.eventCount = eventCount;
|
|
1006
|
+
const result = adapter.parseEvent(assembled, parseCtx);
|
|
1007
|
+
if (result === null)
|
|
1008
|
+
continue;
|
|
1009
|
+
const events = Array.isArray(result) ? result : [result];
|
|
1010
|
+
for (const ev of events) {
|
|
1011
|
+
eventCount++;
|
|
1012
|
+
parseCtx.lastEventType = ev.type;
|
|
1013
|
+
// Emit as NDJSON bridge event
|
|
1014
|
+
emitBridgeEvent({
|
|
1015
|
+
type: ev.type,
|
|
1016
|
+
timestamp: new Date().toISOString(),
|
|
1017
|
+
data: ev,
|
|
1018
|
+
});
|
|
1019
|
+
// Detect turn completion events — schedule PTY termination
|
|
1020
|
+
if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
|
|
1021
|
+
turnComplete = true;
|
|
1022
|
+
if (idleTimer)
|
|
1023
|
+
clearTimeout(idleTimer);
|
|
1024
|
+
setTimeout(completePtyPrompt, 1000);
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
// Idle timeout fallback for harnesses without structured end events (Pi).
|
|
1028
|
+
if (useIdleTimeout) {
|
|
1029
|
+
if (idleTimer)
|
|
1030
|
+
clearTimeout(idleTimer);
|
|
1031
|
+
idleTimer = setTimeout(() => {
|
|
1032
|
+
if (!turnComplete && eventCount > 0) {
|
|
1033
|
+
turnComplete = true;
|
|
1034
|
+
completePtyPrompt();
|
|
1035
|
+
}
|
|
1036
|
+
}, IDLE_TIMEOUT_MS);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
catch { /* parse error — ignore */ }
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
// Inject prompt after observed onboarding prompts are dismissed.
|
|
1044
|
+
// If the PTY stays silent, inject after a short startup grace period because
|
|
1045
|
+
// some harnesses wait for input without rendering an initial prompt.
|
|
1046
|
+
if (prompt) {
|
|
1047
|
+
const startedAt = Date.now();
|
|
1048
|
+
let promptInjected = false;
|
|
1049
|
+
let artifactMonitor;
|
|
1050
|
+
const injectPrompt = () => {
|
|
1051
|
+
if (promptInjected)
|
|
1052
|
+
return;
|
|
1053
|
+
promptInjected = true;
|
|
1054
|
+
ptyProcess.write(prompt);
|
|
1055
|
+
setTimeout(() => ptyProcess.write('\r'), 500);
|
|
1056
|
+
artifactMonitor = startPromptArtifactCompletionMonitor({
|
|
1057
|
+
prompt,
|
|
1058
|
+
cwd: launchCwd,
|
|
1059
|
+
onComplete: () => {
|
|
1060
|
+
if (artifactMonitor)
|
|
1061
|
+
clearInterval(artifactMonitor);
|
|
1062
|
+
completePtyPrompt();
|
|
1063
|
+
},
|
|
1064
|
+
});
|
|
1065
|
+
ptyCleanup.push(() => { if (artifactMonitor)
|
|
1066
|
+
clearInterval(artifactMonitor); });
|
|
1067
|
+
};
|
|
1068
|
+
const checkAndInject = () => {
|
|
1069
|
+
if (promptInjected)
|
|
1070
|
+
return;
|
|
1071
|
+
if (outputBuf.length === 0) {
|
|
1072
|
+
if (Date.now() - startedAt >= 1000)
|
|
1073
|
+
injectPrompt();
|
|
1074
|
+
else
|
|
1075
|
+
setTimeout(checkAndInject, 100);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
const stripped = outputBuf.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
|
|
1079
|
+
if (apiKeyPromptHandled || bypassPromptHandled) {
|
|
1080
|
+
setTimeout(injectPrompt, 2000);
|
|
1081
|
+
}
|
|
1082
|
+
else if (stripped.includes('APIkey') || stripped.includes('Bypass')) {
|
|
1083
|
+
setTimeout(checkAndInject, 500);
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
setTimeout(injectPrompt, 3000);
|
|
1087
|
+
}
|
|
1088
|
+
};
|
|
1089
|
+
checkAndInject();
|
|
1090
|
+
}
|
|
1091
|
+
// Create a fake ChildProcess-like for signal handling
|
|
1092
|
+
child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
|
|
1093
|
+
// On PTY exit, flush remaining buffered text as a final output event
|
|
1094
|
+
const origOnExit = ptyProcess.onExit.bind(ptyProcess);
|
|
1095
|
+
const exitPromise = new Promise((resolve) => {
|
|
1096
|
+
origOnExit(({ exitCode: code }) => {
|
|
1097
|
+
// Flush any remaining output as a final bridge event
|
|
1098
|
+
if (outputBuf.length > 0) {
|
|
1099
|
+
emitBridgeEvent({
|
|
1100
|
+
type: 'output',
|
|
1101
|
+
timestamp: new Date().toISOString(),
|
|
1102
|
+
data: { text: outputBuf },
|
|
1103
|
+
});
|
|
1104
|
+
outputBuf = '';
|
|
1105
|
+
}
|
|
1106
|
+
resolve(code);
|
|
1107
|
+
});
|
|
1108
|
+
});
|
|
1109
|
+
// Store the exit promise so main exit handler can use it
|
|
1110
|
+
child.__bridgeExitPromise = exitPromise;
|
|
1111
|
+
}
|
|
438
1112
|
else {
|
|
1113
|
+
// Non-interactive: plain spawn. Each harness handles non-interactive mode
|
|
1114
|
+
// internally (claude -p, codex exec, gemini --prompt, pi stdin).
|
|
439
1115
|
const { spawn } = await import('node:child_process');
|
|
440
1116
|
child = spawn(plan.command, plan.args, {
|
|
441
|
-
stdio: ['pipe', '
|
|
1117
|
+
stdio: ['pipe', 'pipe', 'inherit'],
|
|
442
1118
|
env: { ...process.env, ...plan.env },
|
|
443
1119
|
cwd: launchCwd,
|
|
444
|
-
shell:
|
|
1120
|
+
shell: process.platform === 'win32',
|
|
1121
|
+
});
|
|
1122
|
+
// Pipe stdout through + idle-timeout kill for harnesses that don't exit
|
|
1123
|
+
// after completing a non-interactive task (e.g., Pi doesn't exit on its own).
|
|
1124
|
+
// Harnesses with proper exit behavior (claude -p, codex exec) don't need this.
|
|
1125
|
+
const niUseIdleKill = !new Set(['claude', 'codex', 'gemini', 'opencode']).has(plan.harness);
|
|
1126
|
+
let niIdleTimer = null;
|
|
1127
|
+
let niHasOutput = false;
|
|
1128
|
+
const NI_IDLE_TIMEOUT_MS = 30_000;
|
|
1129
|
+
child.stdout?.on('data', (chunk) => {
|
|
1130
|
+
process.stdout.write(chunk);
|
|
1131
|
+
niHasOutput = true;
|
|
1132
|
+
if (niUseIdleKill) {
|
|
1133
|
+
if (niIdleTimer)
|
|
1134
|
+
clearTimeout(niIdleTimer);
|
|
1135
|
+
niIdleTimer = setTimeout(() => {
|
|
1136
|
+
if (niHasOutput) {
|
|
1137
|
+
try {
|
|
1138
|
+
child.kill('SIGTERM');
|
|
1139
|
+
}
|
|
1140
|
+
catch { /* */ }
|
|
1141
|
+
}
|
|
1142
|
+
}, NI_IDLE_TIMEOUT_MS);
|
|
1143
|
+
}
|
|
445
1144
|
});
|
|
446
1145
|
}
|
|
447
1146
|
if (flagBool(args.flags, 'observe')) {
|
|
@@ -472,32 +1171,98 @@ export async function launchCommand(client, args) {
|
|
|
472
1171
|
}
|
|
473
1172
|
catch { /* process may already be dead */ }
|
|
474
1173
|
}
|
|
1174
|
+
else if (ptyProcess) {
|
|
1175
|
+
// PTY child runs in its own session — kill the process group to avoid orphans
|
|
1176
|
+
try {
|
|
1177
|
+
process.kill(-ptyProcess.pid, sig);
|
|
1178
|
+
}
|
|
1179
|
+
catch { /* */ }
|
|
1180
|
+
try {
|
|
1181
|
+
ptyProcess.kill(sig);
|
|
1182
|
+
}
|
|
1183
|
+
catch { /* */ }
|
|
1184
|
+
}
|
|
475
1185
|
else {
|
|
476
1186
|
child.kill(sig);
|
|
477
1187
|
}
|
|
478
1188
|
};
|
|
479
1189
|
process.on('SIGINT', forwardSignal);
|
|
480
1190
|
process.on('SIGTERM', forwardSignal);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
1191
|
+
// Ensure PTY cleanup on exit
|
|
1192
|
+
if (ptyProcess) {
|
|
1193
|
+
process.on('exit', () => { try {
|
|
1194
|
+
ptyProcess.kill('SIGKILL');
|
|
1195
|
+
}
|
|
1196
|
+
catch { /* */ } });
|
|
484
1197
|
}
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
resolve(code);
|
|
491
|
-
});
|
|
1198
|
+
const promptPassedAsFlag = (plan.harness === 'pi' && !isInteractive && plan.args.includes('--print'));
|
|
1199
|
+
if (prompt && child.stdin && !ptyProcess && !promptPassedAsFlag) {
|
|
1200
|
+
child.stdin.write(prompt + '\n');
|
|
1201
|
+
if (!isInteractive) {
|
|
1202
|
+
child.stdin.end();
|
|
492
1203
|
}
|
|
493
1204
|
else {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
1205
|
+
// Interactive with stdin pipe (no PTY): reconnect terminal stdin after prompt injection
|
|
1206
|
+
process.stdin.resume();
|
|
1207
|
+
process.stdin.pipe(child.stdin);
|
|
497
1208
|
}
|
|
498
|
-
}
|
|
1209
|
+
}
|
|
1210
|
+
// Close stdin for harnesses where prompt was passed as a CLI flag (not via stdin)
|
|
1211
|
+
// to prevent the process from hanging waiting for interactive input.
|
|
1212
|
+
if (promptPassedAsFlag && child.stdin && !ptyProcess) {
|
|
1213
|
+
child.stdin.end();
|
|
1214
|
+
}
|
|
1215
|
+
let exitCode = await (child.__bridgeExitPromise
|
|
1216
|
+
? child.__bridgeExitPromise
|
|
1217
|
+
: new Promise((resolve) => {
|
|
1218
|
+
if (ptyProcess) {
|
|
1219
|
+
ptyProcess.onExit(({ exitCode: code }) => {
|
|
1220
|
+
if (process.stdin.isTTY)
|
|
1221
|
+
process.stdin.setRawMode(false);
|
|
1222
|
+
resolve(code);
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
else {
|
|
1226
|
+
child.on('exit', (code, signal) => {
|
|
1227
|
+
resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
}));
|
|
1231
|
+
for (const cleanup of ptyCleanup.splice(0))
|
|
1232
|
+
cleanup();
|
|
1233
|
+
if (ptyTerminationExpected && exitCode !== 0)
|
|
1234
|
+
exitCode = 0;
|
|
499
1235
|
process.off('SIGINT', forwardSignal);
|
|
500
1236
|
process.off('SIGTERM', forwardSignal);
|
|
1237
|
+
// Bridge hooks: emulate stop hook and re-spawn if shouldContinue
|
|
1238
|
+
if (bridgeHookEmulator) {
|
|
1239
|
+
let stopResult = await bridgeHookEmulator.emulateStop();
|
|
1240
|
+
while (stopResult.shouldContinue && stopResult.resumeId) {
|
|
1241
|
+
// Re-spawn with --resume to continue the session
|
|
1242
|
+
const resumePlan = { ...plan, args: [...plan.args] };
|
|
1243
|
+
appendHarnessSessionArgs(resumePlan, {
|
|
1244
|
+
resumeId: stopResult.resumeId,
|
|
1245
|
+
interactive: false,
|
|
1246
|
+
});
|
|
1247
|
+
const { spawn: resumeSpawn } = await import('node:child_process');
|
|
1248
|
+
const resumeChild = resumeSpawn(resumePlan.command, resumePlan.args, {
|
|
1249
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
1250
|
+
env: { ...process.env, ...resumePlan.env },
|
|
1251
|
+
cwd: launchCwd,
|
|
1252
|
+
shell: process.platform === 'win32',
|
|
1253
|
+
});
|
|
1254
|
+
if (resumeChild.stdin) {
|
|
1255
|
+
resumeChild.stdin.end();
|
|
1256
|
+
}
|
|
1257
|
+
await new Promise((resolve) => {
|
|
1258
|
+
resumeChild.on('exit', (code, signal) => {
|
|
1259
|
+
resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
|
|
1260
|
+
});
|
|
1261
|
+
});
|
|
1262
|
+
stopResult = await bridgeHookEmulator.emulateStop();
|
|
1263
|
+
}
|
|
1264
|
+
await bridgeHookEmulator.emulateSessionEnd();
|
|
1265
|
+
}
|
|
501
1266
|
if (proxyRuntime) {
|
|
502
1267
|
await proxyRuntime.stop();
|
|
503
1268
|
}
|