@a5c-ai/agent-mux-cli 0.4.10-staging.ff407b73 → 5.0.1-staging.04a3db697
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 +445 -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
|
@@ -14,6 +14,7 @@ import { printError, printJsonError } from '../output.js';
|
|
|
14
14
|
/** Launch-specific flag definitions (global flags like model/json/debug are excluded). */
|
|
15
15
|
export const LAUNCH_FLAGS = {
|
|
16
16
|
'api-key': { type: 'string' },
|
|
17
|
+
'profile': { type: 'string' },
|
|
17
18
|
'api-base': { type: 'string' },
|
|
18
19
|
'region': { type: 'string' },
|
|
19
20
|
'project': { type: 'string' },
|
|
@@ -29,6 +30,7 @@ export const LAUNCH_FLAGS = {
|
|
|
29
30
|
'resume': { short: 'r', type: 'string' },
|
|
30
31
|
'session-id': { short: 's', type: 'string' },
|
|
31
32
|
'prompt': { short: 'p', type: 'string' },
|
|
33
|
+
'interactive': { short: 'i', type: 'boolean' },
|
|
32
34
|
'max-turns': { type: 'number' },
|
|
33
35
|
'max-budget-usd': { type: 'number' },
|
|
34
36
|
'dry-run': { type: 'boolean' },
|
|
@@ -39,14 +41,16 @@ export const LAUNCH_FLAGS = {
|
|
|
39
41
|
'workspace-mode': { type: 'string' },
|
|
40
42
|
'workspace-repo': { type: 'string', repeatable: true },
|
|
41
43
|
'workspace-name': { type: 'string' },
|
|
44
|
+
'yolo': { type: 'boolean' },
|
|
45
|
+
'bridge-interactive': { type: 'boolean' },
|
|
46
|
+
'bridge-hooks': { type: 'boolean' },
|
|
42
47
|
};
|
|
43
48
|
// ---------------------------------------------------------------------------
|
|
44
49
|
// Plan resolution
|
|
45
50
|
// ---------------------------------------------------------------------------
|
|
46
51
|
export function resolveLaunchPlan(input) {
|
|
47
|
-
const providerId = (input.provider ?? 'anthropic');
|
|
48
52
|
const providerConfig = resolveProvider({
|
|
49
|
-
provider:
|
|
53
|
+
provider: input.provider,
|
|
50
54
|
model: input.model,
|
|
51
55
|
transport: input.transport,
|
|
52
56
|
apiKey: input.apiKey,
|
|
@@ -56,6 +60,7 @@ export function resolveLaunchPlan(input) {
|
|
|
56
60
|
resourceGroup: input.resourceGroup,
|
|
57
61
|
endpointName: input.endpointName,
|
|
58
62
|
authCommand: input.authCommand,
|
|
63
|
+
profile: input.profile,
|
|
59
64
|
});
|
|
60
65
|
// Merge extra provider args into params
|
|
61
66
|
if (input.providerArgs) {
|
|
@@ -71,29 +76,34 @@ export function resolveLaunchPlan(input) {
|
|
|
71
76
|
}
|
|
72
77
|
else {
|
|
73
78
|
proxyNeeded = false;
|
|
74
|
-
proxyReason = `${input.harness} supports ${
|
|
79
|
+
proxyReason = `${input.harness} supports ${providerConfig.provider} natively`;
|
|
75
80
|
}
|
|
76
81
|
}
|
|
77
82
|
else {
|
|
78
83
|
if (input.proxyMode === 'never') {
|
|
79
|
-
throw new Error(`${input.harness} does not support ${
|
|
84
|
+
throw new Error(`${input.harness} does not support ${providerConfig.provider} natively. ` +
|
|
80
85
|
`Use --with-proxy-if-needed to auto-launch the proxy.`);
|
|
81
86
|
}
|
|
82
87
|
proxyReason =
|
|
83
|
-
`${input.harness} does not support ${
|
|
84
|
-
`proxy bridges ${
|
|
88
|
+
`${input.harness} does not support ${providerConfig.provider} natively; ` +
|
|
89
|
+
`proxy bridges ${providerConfig.provider} → ${translation.proxyExposedTransport}`;
|
|
85
90
|
}
|
|
86
91
|
const proxy = proxyNeeded
|
|
87
92
|
? {
|
|
88
|
-
targetProvider:
|
|
93
|
+
targetProvider: providerConfig.provider,
|
|
89
94
|
targetModel: providerConfig.model,
|
|
90
95
|
exposedTransport: translation.proxyExposedTransport ?? 'openai-chat',
|
|
91
96
|
port: input.proxyPort ?? 0,
|
|
97
|
+
apiBase: providerConfig.params['apiBase'] ? String(providerConfig.params['apiBase']) : undefined,
|
|
98
|
+
apiKey: providerConfig.auth.apiKey,
|
|
99
|
+
project: providerConfig.params['project'] ? String(providerConfig.params['project']) : undefined,
|
|
100
|
+
location: providerConfig.params['region'] ? String(providerConfig.params['region']) : undefined,
|
|
101
|
+
useVertexAi: providerConfig.provider === 'vertex' || Boolean(providerConfig.params['useVertexAi']),
|
|
92
102
|
}
|
|
93
103
|
: undefined;
|
|
94
104
|
return {
|
|
95
105
|
harness: input.harness,
|
|
96
|
-
provider:
|
|
106
|
+
provider: providerConfig.provider,
|
|
97
107
|
transport: providerConfig.transport,
|
|
98
108
|
model: providerConfig.model,
|
|
99
109
|
proxyNeeded,
|
|
@@ -105,14 +115,19 @@ export function resolveLaunchPlan(input) {
|
|
|
105
115
|
};
|
|
106
116
|
}
|
|
107
117
|
function appendHarnessSessionArgs(plan, session) {
|
|
118
|
+
const interactive = session.interactive !== false;
|
|
108
119
|
switch (plan.harness) {
|
|
109
120
|
case 'claude':
|
|
121
|
+
if (session.bridgeInteractive) {
|
|
122
|
+
plan.args.push('--bare');
|
|
123
|
+
}
|
|
110
124
|
if (session.resumeId)
|
|
111
125
|
plan.args.push('--resume', session.resumeId);
|
|
112
126
|
if (session.sessionId)
|
|
113
127
|
plan.args.push('--session-id', session.sessionId);
|
|
114
|
-
if (session.prompt)
|
|
115
|
-
plan.args.push('
|
|
128
|
+
if (session.prompt && !interactive) {
|
|
129
|
+
plan.args.push('-p', session.prompt);
|
|
130
|
+
}
|
|
116
131
|
if (session.maxTurns)
|
|
117
132
|
plan.args.push('--max-turns', String(session.maxTurns));
|
|
118
133
|
break;
|
|
@@ -120,7 +135,7 @@ function appendHarnessSessionArgs(plan, session) {
|
|
|
120
135
|
if (session.resumeId) {
|
|
121
136
|
plan.args.unshift('resume', session.resumeId);
|
|
122
137
|
}
|
|
123
|
-
else if (session.prompt) {
|
|
138
|
+
else if (session.prompt && !interactive) {
|
|
124
139
|
plan.args.unshift('exec', session.prompt);
|
|
125
140
|
}
|
|
126
141
|
break;
|
|
@@ -128,10 +143,14 @@ function appendHarnessSessionArgs(plan, session) {
|
|
|
128
143
|
if (session.prompt)
|
|
129
144
|
plan.args.push('--prompt', session.prompt);
|
|
130
145
|
break;
|
|
146
|
+
case 'pi':
|
|
147
|
+
if (session.prompt && !interactive) {
|
|
148
|
+
plan.args.push('--prompt', session.prompt);
|
|
149
|
+
}
|
|
150
|
+
break;
|
|
131
151
|
case 'opencode':
|
|
132
152
|
if (session.resumeId)
|
|
133
153
|
plan.args.push('--session', session.resumeId);
|
|
134
|
-
// OpenCode has no non-interactive prompt flag; prompt delivered via stdin after launch
|
|
135
154
|
break;
|
|
136
155
|
}
|
|
137
156
|
}
|
|
@@ -276,6 +295,7 @@ export async function launchCommand(client, args) {
|
|
|
276
295
|
resourceGroup: flagStr(args.flags, 'resource-group'),
|
|
277
296
|
endpointName: flagStr(args.flags, 'endpoint-name'),
|
|
278
297
|
authCommand: flagStr(args.flags, 'auth-command'),
|
|
298
|
+
profile: flagStr(args.flags, 'profile'),
|
|
279
299
|
proxyMode,
|
|
280
300
|
proxyPort: flagNum(args.flags, 'proxy-port'),
|
|
281
301
|
adapter: adapter,
|
|
@@ -296,6 +316,7 @@ export async function launchCommand(client, args) {
|
|
|
296
316
|
model: flagStr(args.flags, 'model'),
|
|
297
317
|
apiKey: flagStr(args.flags, 'api-key'),
|
|
298
318
|
authCommand: flagStr(args.flags, 'auth-command'),
|
|
319
|
+
profile: flagStr(args.flags, 'profile'),
|
|
299
320
|
});
|
|
300
321
|
if (resolvedConfig.auth.type === 'api_key' && !resolvedConfig.auth.apiKey) {
|
|
301
322
|
const defaults = (await import('@a5c-ai/agent-mux-core')).PROVIDER_DEFAULTS;
|
|
@@ -361,6 +382,45 @@ export async function launchCommand(client, args) {
|
|
|
361
382
|
}
|
|
362
383
|
launchCwd = resolveWorkspaceDefaultCwd(workspace);
|
|
363
384
|
}
|
|
385
|
+
// Resolve interactive mode (default: true)
|
|
386
|
+
const interactiveFlag = flagBool(args.flags, 'interactive');
|
|
387
|
+
const isInteractive = interactiveFlag !== false;
|
|
388
|
+
// Bridge flags: --bridge-interactive and --bridge-hooks
|
|
389
|
+
const bridgeInteractive = flagBool(args.flags, 'bridge-interactive') === true;
|
|
390
|
+
const bridgeHooks = flagBool(args.flags, 'bridge-hooks') === true;
|
|
391
|
+
if (bridgeInteractive && isInteractive) {
|
|
392
|
+
const msg = '--bridge-interactive requires --no-interactive';
|
|
393
|
+
if (jsonMode)
|
|
394
|
+
printJsonError('VALIDATION_ERROR', msg);
|
|
395
|
+
else
|
|
396
|
+
printError(msg);
|
|
397
|
+
return ExitCode.USAGE_ERROR;
|
|
398
|
+
}
|
|
399
|
+
if (bridgeHooks && isInteractive) {
|
|
400
|
+
const msg = '--bridge-hooks requires --no-interactive';
|
|
401
|
+
if (jsonMode)
|
|
402
|
+
printJsonError('VALIDATION_ERROR', msg);
|
|
403
|
+
else
|
|
404
|
+
printError(msg);
|
|
405
|
+
return ExitCode.USAGE_ERROR;
|
|
406
|
+
}
|
|
407
|
+
if (bridgeInteractive) {
|
|
408
|
+
try {
|
|
409
|
+
const { getBridgeCapabilities } = await import('@a5c-ai/agent-catalog');
|
|
410
|
+
const caps = getBridgeCapabilities(plan.harness);
|
|
411
|
+
if (!caps?.interactiveBridge) {
|
|
412
|
+
const msg = `${plan.harness} does not support interactive bridging`;
|
|
413
|
+
if (jsonMode)
|
|
414
|
+
printJsonError('CAPABILITY_ERROR', msg);
|
|
415
|
+
else
|
|
416
|
+
printError(msg);
|
|
417
|
+
return ExitCode.USAGE_ERROR;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// agent-catalog not available — skip capability check
|
|
422
|
+
}
|
|
423
|
+
}
|
|
364
424
|
// Append session/prompt args
|
|
365
425
|
const prompt = flagStr(args.flags, 'prompt');
|
|
366
426
|
appendHarnessSessionArgs(plan, {
|
|
@@ -368,23 +428,104 @@ export async function launchCommand(client, args) {
|
|
|
368
428
|
sessionId: flagStr(args.flags, 'session-id'),
|
|
369
429
|
prompt,
|
|
370
430
|
maxTurns: flagNum(args.flags, 'max-turns'),
|
|
431
|
+
interactive: isInteractive || bridgeInteractive,
|
|
432
|
+
bridgeInteractive,
|
|
371
433
|
});
|
|
434
|
+
// Add --model for harnesses that accept it as a CLI arg
|
|
435
|
+
const modelFlag = flagStr(args.flags, 'model');
|
|
436
|
+
if (modelFlag && ['pi', 'gemini', 'opencode'].includes(plan.harness)) {
|
|
437
|
+
plan.args.push('--model', modelFlag);
|
|
438
|
+
}
|
|
439
|
+
// --yolo: add harness-specific auto-approve flags resolved through
|
|
440
|
+
// agent-catalog → atlas graph (LaunchConfig records with commArgs)
|
|
441
|
+
if (flagBool(args.flags, 'yolo')) {
|
|
442
|
+
try {
|
|
443
|
+
const { getYoloLaunchArgs } = await import('@a5c-ai/agent-catalog');
|
|
444
|
+
const yoloArgs = getYoloLaunchArgs(plan.harness);
|
|
445
|
+
if (yoloArgs.length > 0) {
|
|
446
|
+
plan.args.push(...yoloArgs);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch {
|
|
450
|
+
// agent-catalog not available
|
|
451
|
+
}
|
|
452
|
+
}
|
|
372
453
|
// Passthrough args after --
|
|
373
454
|
const dashDashIdx = process.argv.indexOf('--');
|
|
374
455
|
if (dashDashIdx >= 0) {
|
|
375
456
|
plan.args.push(...process.argv.slice(dashDashIdx + 1));
|
|
376
457
|
}
|
|
458
|
+
// Also check parsed positionals for -- separator (handles spawn() without shell)
|
|
459
|
+
const argsDashIdx = args.positionals.indexOf('--');
|
|
460
|
+
if (argsDashIdx >= 0) {
|
|
461
|
+
plan.args.push(...args.positionals.slice(argsDashIdx + 1));
|
|
462
|
+
}
|
|
377
463
|
// Launch runtime if needed
|
|
378
464
|
let proxyRuntime;
|
|
379
465
|
if (plan.proxyNeeded && plan.proxy) {
|
|
380
466
|
try {
|
|
467
|
+
// When exposed transport differs from target (e.g., anthropic→foundry),
|
|
468
|
+
// the proxy needs a completion engine to translate request/response formats.
|
|
469
|
+
let completionEngine;
|
|
470
|
+
if ((plan.proxy.targetProvider === 'google' || plan.proxy.targetProvider === 'vertex') && plan.proxy.apiKey) {
|
|
471
|
+
const { createGoogleCompletionEngine } = await import('./launch-completion-engine.js');
|
|
472
|
+
completionEngine = createGoogleCompletionEngine({
|
|
473
|
+
apiBase: plan.proxy.useVertexAi ? undefined : plan.proxy.apiBase,
|
|
474
|
+
apiKey: plan.proxy.apiKey,
|
|
475
|
+
targetModel: plan.proxy.targetModel,
|
|
476
|
+
provider: plan.proxy.targetProvider,
|
|
477
|
+
project: plan.proxy.project,
|
|
478
|
+
location: plan.proxy.location,
|
|
479
|
+
useVertexAi: plan.proxy.useVertexAi,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
else if (plan.proxy.apiBase && plan.proxy.apiKey) {
|
|
483
|
+
const { createOpenAICompletionEngine } = await import('./launch-completion-engine.js');
|
|
484
|
+
completionEngine = createOpenAICompletionEngine({
|
|
485
|
+
apiBase: plan.proxy.apiBase,
|
|
486
|
+
apiKey: plan.proxy.apiKey,
|
|
487
|
+
targetModel: plan.proxy.targetModel,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
381
490
|
proxyRuntime = await startTransportMuxRuntime({
|
|
382
491
|
targetProvider: plan.proxy.targetProvider,
|
|
383
492
|
targetModel: `${plan.proxy.targetProvider}/${plan.proxy.targetModel}`,
|
|
384
493
|
exposedTransport: plan.proxy.exposedTransport,
|
|
385
494
|
port: plan.proxy.port,
|
|
495
|
+
apiBase: plan.proxy.apiBase,
|
|
496
|
+
completionEngine,
|
|
386
497
|
});
|
|
387
498
|
proxyRuntime.applyHarnessEnv(plan.env);
|
|
499
|
+
// Pi ignores OPENAI_BASE_URL — write a models.json config that registers
|
|
500
|
+
// a custom provider pointing to the local proxy.
|
|
501
|
+
if (plan.harness === 'pi') {
|
|
502
|
+
const { writeFileSync, mkdirSync } = await import('node:fs');
|
|
503
|
+
const { join } = await import('node:path');
|
|
504
|
+
const piConfigDir = process.env['PI_CODING_AGENT_DIR']
|
|
505
|
+
?? join(process.env['HOME'] ?? process.env['USERPROFILE'] ?? '/tmp', '.pi', 'agent');
|
|
506
|
+
mkdirSync(piConfigDir, { recursive: true });
|
|
507
|
+
const modelsConfig = {
|
|
508
|
+
providers: {
|
|
509
|
+
'amux-proxy': {
|
|
510
|
+
baseUrl: `${proxyRuntime.url}/v1`,
|
|
511
|
+
api: 'openai-completions',
|
|
512
|
+
apiKey: proxyRuntime.authToken ?? 'proxy-token',
|
|
513
|
+
models: [{
|
|
514
|
+
id: plan.model,
|
|
515
|
+
reasoning: false,
|
|
516
|
+
input: ['text'],
|
|
517
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
518
|
+
contextWindow: 128000,
|
|
519
|
+
maxTokens: 16384,
|
|
520
|
+
}],
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
};
|
|
524
|
+
const modelsPath = join(piConfigDir, 'models.json');
|
|
525
|
+
writeFileSync(modelsPath, JSON.stringify(modelsConfig, null, 2));
|
|
526
|
+
plan.args.push('--provider', 'amux-proxy');
|
|
527
|
+
console.error(`[amux launch] Pi proxy config written to ${modelsPath}, proxy at ${proxyRuntime.url}`);
|
|
528
|
+
}
|
|
388
529
|
}
|
|
389
530
|
catch (err) {
|
|
390
531
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -395,14 +536,28 @@ export async function launchCommand(client, args) {
|
|
|
395
536
|
return ExitCode.GENERAL_ERROR;
|
|
396
537
|
}
|
|
397
538
|
}
|
|
539
|
+
// Bridge hooks: emulate lifecycle hooks when --bridge-hooks is set
|
|
540
|
+
let bridgeHookEmulator;
|
|
541
|
+
if (bridgeHooks) {
|
|
542
|
+
const { BridgeHookEmulator } = await import('./launch-bridge-hooks.js');
|
|
543
|
+
bridgeHookEmulator = new BridgeHookEmulator({
|
|
544
|
+
harness: plan.harness,
|
|
545
|
+
cwd: launchCwd,
|
|
546
|
+
env: plan.env,
|
|
547
|
+
sessionId: flagStr(args.flags, 'session-id'),
|
|
548
|
+
runsDir: plan.env['BABYSITTER_RUNS_DIR'] || undefined,
|
|
549
|
+
verbose: flagBool(args.flags, 'debug') === true,
|
|
550
|
+
});
|
|
551
|
+
await bridgeHookEmulator.emulateSessionStart();
|
|
552
|
+
}
|
|
398
553
|
// Spawn harness
|
|
399
|
-
|
|
400
|
-
let child;
|
|
554
|
+
let child = null;
|
|
401
555
|
let ptyProcess = null;
|
|
402
556
|
if (isInteractive) {
|
|
403
|
-
//
|
|
557
|
+
// Interactive mode: full TTY passthrough. If a prompt is provided, it's
|
|
558
|
+
// injected as initial stdin after the harness starts (like typing it in).
|
|
404
559
|
try {
|
|
405
|
-
const nodePty =
|
|
560
|
+
const nodePty = await import('node-pty');
|
|
406
561
|
ptyProcess = nodePty.spawn(plan.command, plan.args, {
|
|
407
562
|
name: 'xterm-256color',
|
|
408
563
|
cols: process.stdout.columns || 80,
|
|
@@ -410,8 +565,61 @@ export async function launchCommand(client, args) {
|
|
|
410
565
|
cwd: launchCwd,
|
|
411
566
|
env: { ...process.env, ...plan.env },
|
|
412
567
|
});
|
|
413
|
-
//
|
|
414
|
-
|
|
568
|
+
// End-of-turn detection: parse PTY output through adapter's event system
|
|
569
|
+
let turnDetected = false;
|
|
570
|
+
let lineBuf = '';
|
|
571
|
+
let assembler = null;
|
|
572
|
+
let adapter = null;
|
|
573
|
+
try {
|
|
574
|
+
const core = await import('@a5c-ai/agent-mux-core');
|
|
575
|
+
assembler = new core.StreamAssembler();
|
|
576
|
+
// Resolve the adapter for this harness to use its parseEvent
|
|
577
|
+
const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
|
|
578
|
+
const factory = adaptersModule.getAdapterFactory?.(plan.harness);
|
|
579
|
+
adapter = factory ? factory() : null;
|
|
580
|
+
}
|
|
581
|
+
catch { /* core/adapters not available */ }
|
|
582
|
+
// Pipe PTY to stdout + feed through event parser for turn detection
|
|
583
|
+
ptyProcess.onData((data) => {
|
|
584
|
+
process.stdout.write(data);
|
|
585
|
+
if (!assembler || !adapter || turnDetected)
|
|
586
|
+
return;
|
|
587
|
+
// Strip ANSI escapes, then feed lines to the event parser
|
|
588
|
+
const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
|
|
589
|
+
lineBuf += clean;
|
|
590
|
+
let idx;
|
|
591
|
+
while ((idx = lineBuf.indexOf('\n')) !== -1) {
|
|
592
|
+
const line = lineBuf.slice(0, idx).replace(/\r$/, '');
|
|
593
|
+
lineBuf = lineBuf.slice(idx + 1);
|
|
594
|
+
if (line.length === 0)
|
|
595
|
+
continue;
|
|
596
|
+
const assembled = assembler.feed(line);
|
|
597
|
+
if (assembled === null)
|
|
598
|
+
continue;
|
|
599
|
+
try {
|
|
600
|
+
const ctx = { runId: 'launch', agent: plan.harness, sessionId: undefined, turnIndex: 0, debug: false, outputFormat: 'text', source: 'stdout', assembler, eventCount: 0, lastEventType: null, adapterState: {} };
|
|
601
|
+
const result = adapter.parseEvent(assembled, ctx);
|
|
602
|
+
if (result === null)
|
|
603
|
+
continue;
|
|
604
|
+
const events = Array.isArray(result) ? result : [result];
|
|
605
|
+
for (const ev of events) {
|
|
606
|
+
// Detect turn completion events
|
|
607
|
+
if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
|
|
608
|
+
turnDetected = true;
|
|
609
|
+
// Give the harness a moment to flush output, then kill
|
|
610
|
+
setTimeout(() => {
|
|
611
|
+
try {
|
|
612
|
+
ptyProcess.kill('SIGTERM');
|
|
613
|
+
}
|
|
614
|
+
catch { /* */ }
|
|
615
|
+
}, 1000);
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
catch { /* parse error — ignore */ }
|
|
621
|
+
}
|
|
622
|
+
});
|
|
415
623
|
if (process.stdin.isTTY) {
|
|
416
624
|
process.stdin.setRawMode(true);
|
|
417
625
|
}
|
|
@@ -421,27 +629,170 @@ export async function launchCommand(client, args) {
|
|
|
421
629
|
process.stdout.on('resize', () => {
|
|
422
630
|
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
423
631
|
});
|
|
632
|
+
// Inject prompt as initial input after a short delay for the harness to start
|
|
633
|
+
if (prompt && !plan.args.some(a => a === prompt)) {
|
|
634
|
+
setTimeout(() => ptyProcess.write(prompt + '\n'), 500);
|
|
635
|
+
}
|
|
424
636
|
// Create a fake ChildProcess-like for signal handling
|
|
425
637
|
child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
|
|
426
638
|
}
|
|
427
639
|
catch {
|
|
428
|
-
// node-pty not available, fall back to stdio inherit
|
|
640
|
+
// node-pty not available, fall back to stdio inherit with stdin pipe for prompt injection
|
|
429
641
|
const { spawn } = await import('node:child_process');
|
|
430
642
|
child = spawn(plan.command, plan.args, {
|
|
431
|
-
stdio: 'inherit',
|
|
643
|
+
stdio: prompt ? ['pipe', 'inherit', 'inherit'] : 'inherit',
|
|
432
644
|
env: { ...process.env, ...plan.env },
|
|
433
645
|
cwd: launchCwd,
|
|
434
|
-
shell:
|
|
646
|
+
shell: process.platform === 'win32',
|
|
435
647
|
});
|
|
436
648
|
}
|
|
437
649
|
}
|
|
650
|
+
else if (bridgeInteractive) {
|
|
651
|
+
// Bridge-interactive: spawn via PTY like interactive mode, but:
|
|
652
|
+
// - No human stdin forwarding
|
|
653
|
+
// - Parse PTY output via adapter for structured events
|
|
654
|
+
// - Emit events as NDJSON to stdout
|
|
655
|
+
// - Auto-kill on turn completion
|
|
656
|
+
// - Buffer PTY output to avoid pipe deadlock (stdout is piped)
|
|
657
|
+
let nodePty;
|
|
658
|
+
try {
|
|
659
|
+
nodePty = await import('node-pty');
|
|
660
|
+
}
|
|
661
|
+
catch {
|
|
662
|
+
const msg = '--bridge-interactive requires node-pty but it is not available. Install it with: npm install node-pty';
|
|
663
|
+
if (jsonMode)
|
|
664
|
+
printJsonError('SPAWN_ERROR', msg);
|
|
665
|
+
else
|
|
666
|
+
printError(msg);
|
|
667
|
+
return ExitCode.GENERAL_ERROR;
|
|
668
|
+
}
|
|
669
|
+
ptyProcess = nodePty.spawn(plan.command, plan.args, {
|
|
670
|
+
name: 'xterm-256color',
|
|
671
|
+
cols: 120,
|
|
672
|
+
rows: 40,
|
|
673
|
+
cwd: launchCwd,
|
|
674
|
+
env: { ...process.env, ...plan.env },
|
|
675
|
+
});
|
|
676
|
+
// Set up adapter + assembler for parsing PTY output into structured events
|
|
677
|
+
let assembler = null;
|
|
678
|
+
let adapter = null;
|
|
679
|
+
try {
|
|
680
|
+
const core = await import('@a5c-ai/agent-mux-core');
|
|
681
|
+
assembler = new core.StreamAssembler();
|
|
682
|
+
const adaptersModule = await import('@a5c-ai/agent-mux-adapters');
|
|
683
|
+
const factory = adaptersModule.getAdapterFactory?.(plan.harness);
|
|
684
|
+
adapter = factory ? factory() : null;
|
|
685
|
+
}
|
|
686
|
+
catch { /* core/adapters not available — raw output only */ }
|
|
687
|
+
/** Emit a bridge event as NDJSON, deferred to avoid blocking PTY callback. */
|
|
688
|
+
function emitBridgeEvent(event) {
|
|
689
|
+
const line = JSON.stringify(event) + '\n';
|
|
690
|
+
setImmediate(() => {
|
|
691
|
+
try {
|
|
692
|
+
process.stdout.write(line);
|
|
693
|
+
}
|
|
694
|
+
catch { /* stdout closed */ }
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
let turnComplete = false;
|
|
698
|
+
let lineBuf = '';
|
|
699
|
+
let outputBuf = '';
|
|
700
|
+
let eventCount = 0;
|
|
701
|
+
const parseCtx = {
|
|
702
|
+
runId: 'bridge',
|
|
703
|
+
agent: plan.harness,
|
|
704
|
+
sessionId: undefined,
|
|
705
|
+
turnIndex: 0,
|
|
706
|
+
debug: false,
|
|
707
|
+
outputFormat: 'text',
|
|
708
|
+
source: 'stdout',
|
|
709
|
+
assembler: assembler,
|
|
710
|
+
eventCount: 0,
|
|
711
|
+
lastEventType: null,
|
|
712
|
+
adapterState: {},
|
|
713
|
+
};
|
|
714
|
+
ptyProcess.onData((data) => {
|
|
715
|
+
// Buffer all PTY output — never write synchronously to stdout (pipe deadlock)
|
|
716
|
+
outputBuf += data;
|
|
717
|
+
if (!assembler || !adapter || turnComplete)
|
|
718
|
+
return;
|
|
719
|
+
// Strip ANSI escapes, then feed lines to the event parser
|
|
720
|
+
const clean = data.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '').replace(/\x1b\][^\x07]*\x07/g, '');
|
|
721
|
+
lineBuf += clean;
|
|
722
|
+
let idx;
|
|
723
|
+
while ((idx = lineBuf.indexOf('\n')) !== -1) {
|
|
724
|
+
const line = lineBuf.slice(0, idx).replace(/\r$/, '');
|
|
725
|
+
lineBuf = lineBuf.slice(idx + 1);
|
|
726
|
+
if (line.length === 0)
|
|
727
|
+
continue;
|
|
728
|
+
const assembled = assembler.feed(line);
|
|
729
|
+
if (assembled === null)
|
|
730
|
+
continue;
|
|
731
|
+
try {
|
|
732
|
+
parseCtx.eventCount = eventCount;
|
|
733
|
+
const result = adapter.parseEvent(assembled, parseCtx);
|
|
734
|
+
if (result === null)
|
|
735
|
+
continue;
|
|
736
|
+
const events = Array.isArray(result) ? result : [result];
|
|
737
|
+
for (const ev of events) {
|
|
738
|
+
eventCount++;
|
|
739
|
+
parseCtx.lastEventType = ev.type;
|
|
740
|
+
// Emit as NDJSON bridge event
|
|
741
|
+
emitBridgeEvent({
|
|
742
|
+
type: ev.type,
|
|
743
|
+
timestamp: new Date().toISOString(),
|
|
744
|
+
data: ev,
|
|
745
|
+
});
|
|
746
|
+
// Detect turn completion events — schedule PTY termination
|
|
747
|
+
if (ev.type === 'message_stop' || ev.type === 'turn_end' || ev.type === 'session_end') {
|
|
748
|
+
turnComplete = true;
|
|
749
|
+
setTimeout(() => {
|
|
750
|
+
try {
|
|
751
|
+
ptyProcess.kill('SIGTERM');
|
|
752
|
+
}
|
|
753
|
+
catch { /* */ }
|
|
754
|
+
}, 1000);
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch { /* parse error — ignore */ }
|
|
760
|
+
}
|
|
761
|
+
});
|
|
762
|
+
// Inject prompt as initial input after harness starts (like interactive mode)
|
|
763
|
+
if (prompt) {
|
|
764
|
+
setTimeout(() => ptyProcess.write(prompt + '\n'), 500);
|
|
765
|
+
}
|
|
766
|
+
// Create a fake ChildProcess-like for signal handling
|
|
767
|
+
child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
|
|
768
|
+
// On PTY exit, flush remaining buffered text as a final output event
|
|
769
|
+
const origOnExit = ptyProcess.onExit.bind(ptyProcess);
|
|
770
|
+
const exitPromise = new Promise((resolve) => {
|
|
771
|
+
origOnExit(({ exitCode: code }) => {
|
|
772
|
+
// Flush any remaining output as a final bridge event
|
|
773
|
+
if (outputBuf.length > 0) {
|
|
774
|
+
emitBridgeEvent({
|
|
775
|
+
type: 'output',
|
|
776
|
+
timestamp: new Date().toISOString(),
|
|
777
|
+
data: { text: outputBuf },
|
|
778
|
+
});
|
|
779
|
+
outputBuf = '';
|
|
780
|
+
}
|
|
781
|
+
resolve(code);
|
|
782
|
+
});
|
|
783
|
+
});
|
|
784
|
+
// Store the exit promise so main exit handler can use it
|
|
785
|
+
child.__bridgeExitPromise = exitPromise;
|
|
786
|
+
}
|
|
438
787
|
else {
|
|
788
|
+
// Non-interactive: plain spawn. Each harness handles non-interactive mode
|
|
789
|
+
// internally (claude -p, codex exec, gemini --prompt, pi stdin).
|
|
439
790
|
const { spawn } = await import('node:child_process');
|
|
440
791
|
child = spawn(plan.command, plan.args, {
|
|
441
792
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
442
793
|
env: { ...process.env, ...plan.env },
|
|
443
794
|
cwd: launchCwd,
|
|
444
|
-
shell:
|
|
795
|
+
shell: process.platform === 'win32',
|
|
445
796
|
});
|
|
446
797
|
}
|
|
447
798
|
if (flagBool(args.flags, 'observe')) {
|
|
@@ -472,32 +823,89 @@ export async function launchCommand(client, args) {
|
|
|
472
823
|
}
|
|
473
824
|
catch { /* process may already be dead */ }
|
|
474
825
|
}
|
|
826
|
+
else if (ptyProcess) {
|
|
827
|
+
// PTY child runs in its own session — kill the process group to avoid orphans
|
|
828
|
+
try {
|
|
829
|
+
process.kill(-ptyProcess.pid, sig);
|
|
830
|
+
}
|
|
831
|
+
catch { /* */ }
|
|
832
|
+
try {
|
|
833
|
+
ptyProcess.kill(sig);
|
|
834
|
+
}
|
|
835
|
+
catch { /* */ }
|
|
836
|
+
}
|
|
475
837
|
else {
|
|
476
838
|
child.kill(sig);
|
|
477
839
|
}
|
|
478
840
|
};
|
|
479
841
|
process.on('SIGINT', forwardSignal);
|
|
480
842
|
process.on('SIGTERM', forwardSignal);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
843
|
+
// Ensure PTY cleanup on exit
|
|
844
|
+
if (ptyProcess) {
|
|
845
|
+
process.on('exit', () => { try {
|
|
846
|
+
ptyProcess.kill('SIGKILL');
|
|
847
|
+
}
|
|
848
|
+
catch { /* */ } });
|
|
849
|
+
}
|
|
850
|
+
const promptPassedAsPiFlag = plan.harness === 'pi' && !isInteractive && plan.args.includes('--prompt');
|
|
851
|
+
if (prompt && child.stdin && !ptyProcess && !promptPassedAsPiFlag) {
|
|
852
|
+
child.stdin.write(prompt + '\n');
|
|
853
|
+
if (!isInteractive) {
|
|
854
|
+
child.stdin.end();
|
|
492
855
|
}
|
|
493
856
|
else {
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
857
|
+
// Interactive with stdin pipe (no PTY): reconnect terminal stdin after prompt injection
|
|
858
|
+
process.stdin.resume();
|
|
859
|
+
process.stdin.pipe(child.stdin);
|
|
497
860
|
}
|
|
498
|
-
}
|
|
861
|
+
}
|
|
862
|
+
const exitCode = await (child.__bridgeExitPromise
|
|
863
|
+
? child.__bridgeExitPromise
|
|
864
|
+
: new Promise((resolve) => {
|
|
865
|
+
if (ptyProcess) {
|
|
866
|
+
ptyProcess.onExit(({ exitCode: code }) => {
|
|
867
|
+
if (process.stdin.isTTY)
|
|
868
|
+
process.stdin.setRawMode(false);
|
|
869
|
+
resolve(code);
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
child.on('exit', (code, signal) => {
|
|
874
|
+
resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
}));
|
|
499
878
|
process.off('SIGINT', forwardSignal);
|
|
500
879
|
process.off('SIGTERM', forwardSignal);
|
|
880
|
+
// Bridge hooks: emulate stop hook and re-spawn if shouldContinue
|
|
881
|
+
if (bridgeHookEmulator) {
|
|
882
|
+
let stopResult = await bridgeHookEmulator.emulateStop();
|
|
883
|
+
while (stopResult.shouldContinue && stopResult.resumeId) {
|
|
884
|
+
// Re-spawn with --resume to continue the session
|
|
885
|
+
const resumePlan = { ...plan, args: [...plan.args] };
|
|
886
|
+
appendHarnessSessionArgs(resumePlan, {
|
|
887
|
+
resumeId: stopResult.resumeId,
|
|
888
|
+
interactive: false,
|
|
889
|
+
});
|
|
890
|
+
const { spawn: resumeSpawn } = await import('node:child_process');
|
|
891
|
+
const resumeChild = resumeSpawn(resumePlan.command, resumePlan.args, {
|
|
892
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
893
|
+
env: { ...process.env, ...resumePlan.env },
|
|
894
|
+
cwd: launchCwd,
|
|
895
|
+
shell: process.platform === 'win32',
|
|
896
|
+
});
|
|
897
|
+
if (resumeChild.stdin) {
|
|
898
|
+
resumeChild.stdin.end();
|
|
899
|
+
}
|
|
900
|
+
await new Promise((resolve) => {
|
|
901
|
+
resumeChild.on('exit', (code, signal) => {
|
|
902
|
+
resolve(signal ? 128 + (signal === 'SIGINT' ? 2 : 15) : (code ?? 1));
|
|
903
|
+
});
|
|
904
|
+
});
|
|
905
|
+
stopResult = await bridgeHookEmulator.emulateStop();
|
|
906
|
+
}
|
|
907
|
+
await bridgeHookEmulator.emulateSessionEnd();
|
|
908
|
+
}
|
|
501
909
|
if (proxyRuntime) {
|
|
502
910
|
await proxyRuntime.stop();
|
|
503
911
|
}
|