@a5c-ai/agent-mux-cli 5.0.1-staging.04a3db697 → 5.0.1-staging.04ca6ab00d21
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/bootstrap.d.ts +1 -1
- package/dist/commands/adapters.d.ts +2 -6
- package/dist/commands/adapters.d.ts.map +1 -1
- package/dist/commands/adapters.js +2 -129
- package/dist/commands/adapters.js.map +1 -1
- package/dist/commands/agent.d.ts +1 -1
- package/dist/commands/auth.d.ts +2 -6
- package/dist/commands/auth.d.ts.map +1 -1
- package/dist/commands/auth.js +2 -134
- package/dist/commands/auth.js.map +1 -1
- package/dist/commands/config.d.ts +2 -6
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +2 -218
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/detect-host.d.ts +2 -5
- package/dist/commands/detect-host.d.ts.map +1 -1
- package/dist/commands/detect-host.js +2 -30
- package/dist/commands/detect-host.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/gateway/index.d.ts +1 -1
- package/dist/commands/hooks.d.ts +1 -1
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/install-helpers.d.ts +3 -22
- package/dist/commands/install-helpers.d.ts.map +1 -1
- package/dist/commands/install-helpers.js +2 -55
- package/dist/commands/install-helpers.js.map +1 -1
- package/dist/commands/install.d.ts +2 -22
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +2 -445
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/launch-bridge-hooks.d.ts +5 -56
- package/dist/commands/launch-bridge-hooks.d.ts.map +1 -1
- package/dist/commands/launch-bridge-hooks.js +4 -224
- package/dist/commands/launch-bridge-hooks.js.map +1 -1
- package/dist/commands/launch-completion-engine.d.ts +4 -4
- package/dist/commands/launch-completion-engine.d.ts.map +1 -1
- package/dist/commands/launch-completion-engine.js +4 -4
- package/dist/commands/launch-completion-engine.js.map +1 -1
- package/dist/commands/launch.d.ts +5 -54
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +4 -910
- package/dist/commands/launch.js.map +1 -1
- package/dist/commands/mcp.d.ts +1 -1
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +1 -1
- package/dist/commands/plugin.d.ts +1 -1
- package/dist/commands/profiles.d.ts +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/remote.d.ts +1 -1
- package/dist/commands/remote.js +1 -1
- package/dist/commands/run.d.ts +1 -1
- package/dist/commands/run.js +1 -1
- package/dist/commands/sessions.d.ts +1 -1
- package/dist/commands/sessions.js +1 -1
- package/dist/commands/skill.d.ts +1 -1
- package/dist/commands/tui.d.ts +1 -1
- package/dist/commands/workspaces.d.ts +1 -1
- package/dist/commands/workspaces.js +1 -1
- package/dist/exit-codes.d.ts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -2
- package/package.json +16 -6
package/dist/commands/launch.js
CHANGED
|
@@ -1,914 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `amux launch` command
|
|
2
|
+
* `amux launch` command — thin wrapper.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* stdin/stdout passthrough and proper signal forwarding.
|
|
4
|
+
* The launch orchestration logic now lives in @a5c-ai/agent-launch-mux.
|
|
5
|
+
* This module re-exports everything for backward compatibility.
|
|
7
6
|
*/
|
|
8
|
-
|
|
9
|
-
import { translateForHarness } from '@a5c-ai/agent-mux-adapters';
|
|
10
|
-
import { startTransportMuxRuntime } from '@a5c-ai/transport-mux';
|
|
11
|
-
import { flagStr, flagNum, flagBool, flagArr } from '../parse-args.js';
|
|
12
|
-
import { ExitCode } from '../exit-codes.js';
|
|
13
|
-
import { printError, printJsonError } from '../output.js';
|
|
14
|
-
/** Launch-specific flag definitions (global flags like model/json/debug are excluded). */
|
|
15
|
-
export const LAUNCH_FLAGS = {
|
|
16
|
-
'api-key': { type: 'string' },
|
|
17
|
-
'profile': { type: 'string' },
|
|
18
|
-
'api-base': { type: 'string' },
|
|
19
|
-
'region': { type: 'string' },
|
|
20
|
-
'project': { type: 'string' },
|
|
21
|
-
'resource-group': { type: 'string' },
|
|
22
|
-
'endpoint-name': { type: 'string' },
|
|
23
|
-
'transport': { short: 't', type: 'string' },
|
|
24
|
-
'auth-command': { type: 'string' },
|
|
25
|
-
'with-proxy-if-needed': { type: 'boolean' },
|
|
26
|
-
'with-proxy': { type: 'boolean' },
|
|
27
|
-
'no-proxy': { type: 'boolean' },
|
|
28
|
-
'proxy-port': { type: 'number' },
|
|
29
|
-
'proxy-log-level': { type: 'string' },
|
|
30
|
-
'resume': { short: 'r', type: 'string' },
|
|
31
|
-
'session-id': { short: 's', type: 'string' },
|
|
32
|
-
'prompt': { short: 'p', type: 'string' },
|
|
33
|
-
'interactive': { short: 'i', type: 'boolean' },
|
|
34
|
-
'max-turns': { type: 'number' },
|
|
35
|
-
'max-budget-usd': { type: 'number' },
|
|
36
|
-
'dry-run': { type: 'boolean' },
|
|
37
|
-
'provider-arg': { type: 'string', repeatable: true },
|
|
38
|
-
'observe': { type: 'boolean' },
|
|
39
|
-
'workspace': { type: 'string' },
|
|
40
|
-
'workspace-create': { type: 'boolean' },
|
|
41
|
-
'workspace-mode': { type: 'string' },
|
|
42
|
-
'workspace-repo': { type: 'string', repeatable: true },
|
|
43
|
-
'workspace-name': { type: 'string' },
|
|
44
|
-
'yolo': { type: 'boolean' },
|
|
45
|
-
'bridge-interactive': { type: 'boolean' },
|
|
46
|
-
'bridge-hooks': { type: 'boolean' },
|
|
47
|
-
};
|
|
48
|
-
// ---------------------------------------------------------------------------
|
|
49
|
-
// Plan resolution
|
|
50
|
-
// ---------------------------------------------------------------------------
|
|
51
|
-
export function resolveLaunchPlan(input) {
|
|
52
|
-
const providerConfig = resolveProvider({
|
|
53
|
-
provider: input.provider,
|
|
54
|
-
model: input.model,
|
|
55
|
-
transport: input.transport,
|
|
56
|
-
apiKey: input.apiKey,
|
|
57
|
-
apiBase: input.apiBase,
|
|
58
|
-
region: input.region,
|
|
59
|
-
project: input.project,
|
|
60
|
-
resourceGroup: input.resourceGroup,
|
|
61
|
-
endpointName: input.endpointName,
|
|
62
|
-
authCommand: input.authCommand,
|
|
63
|
-
profile: input.profile,
|
|
64
|
-
});
|
|
65
|
-
// Merge extra provider args into params
|
|
66
|
-
if (input.providerArgs) {
|
|
67
|
-
Object.assign(providerConfig.params, input.providerArgs);
|
|
68
|
-
}
|
|
69
|
-
const translation = translateForHarness(input.harness, providerConfig, input.adapter);
|
|
70
|
-
let proxyNeeded = translation.proxyRequired;
|
|
71
|
-
let proxyReason;
|
|
72
|
-
if (!translation.proxyRequired) {
|
|
73
|
-
if (input.proxyMode === 'always') {
|
|
74
|
-
proxyNeeded = true;
|
|
75
|
-
proxyReason = 'Proxy forced via --with-proxy';
|
|
76
|
-
}
|
|
77
|
-
else {
|
|
78
|
-
proxyNeeded = false;
|
|
79
|
-
proxyReason = `${input.harness} supports ${providerConfig.provider} natively`;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
if (input.proxyMode === 'never') {
|
|
84
|
-
throw new Error(`${input.harness} does not support ${providerConfig.provider} natively. ` +
|
|
85
|
-
`Use --with-proxy-if-needed to auto-launch the proxy.`);
|
|
86
|
-
}
|
|
87
|
-
proxyReason =
|
|
88
|
-
`${input.harness} does not support ${providerConfig.provider} natively; ` +
|
|
89
|
-
`proxy bridges ${providerConfig.provider} → ${translation.proxyExposedTransport}`;
|
|
90
|
-
}
|
|
91
|
-
const proxy = proxyNeeded
|
|
92
|
-
? {
|
|
93
|
-
targetProvider: providerConfig.provider,
|
|
94
|
-
targetModel: providerConfig.model,
|
|
95
|
-
exposedTransport: translation.proxyExposedTransport ?? 'openai-chat',
|
|
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']),
|
|
102
|
-
}
|
|
103
|
-
: undefined;
|
|
104
|
-
return {
|
|
105
|
-
harness: input.harness,
|
|
106
|
-
provider: providerConfig.provider,
|
|
107
|
-
transport: providerConfig.transport,
|
|
108
|
-
model: providerConfig.model,
|
|
109
|
-
proxyNeeded,
|
|
110
|
-
proxyReason,
|
|
111
|
-
proxy,
|
|
112
|
-
command: input.harness,
|
|
113
|
-
args: [...translation.args],
|
|
114
|
-
env: { ...translation.env },
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
function appendHarnessSessionArgs(plan, session) {
|
|
118
|
-
const interactive = session.interactive !== false;
|
|
119
|
-
switch (plan.harness) {
|
|
120
|
-
case 'claude':
|
|
121
|
-
if (session.bridgeInteractive) {
|
|
122
|
-
plan.args.push('--bare');
|
|
123
|
-
}
|
|
124
|
-
if (session.resumeId)
|
|
125
|
-
plan.args.push('--resume', session.resumeId);
|
|
126
|
-
if (session.sessionId)
|
|
127
|
-
plan.args.push('--session-id', session.sessionId);
|
|
128
|
-
if (session.prompt && !interactive) {
|
|
129
|
-
plan.args.push('-p', session.prompt);
|
|
130
|
-
}
|
|
131
|
-
if (session.maxTurns)
|
|
132
|
-
plan.args.push('--max-turns', String(session.maxTurns));
|
|
133
|
-
break;
|
|
134
|
-
case 'codex':
|
|
135
|
-
if (session.resumeId) {
|
|
136
|
-
plan.args.unshift('resume', session.resumeId);
|
|
137
|
-
}
|
|
138
|
-
else if (session.prompt && !interactive) {
|
|
139
|
-
plan.args.unshift('exec', session.prompt);
|
|
140
|
-
}
|
|
141
|
-
break;
|
|
142
|
-
case 'gemini':
|
|
143
|
-
if (session.prompt)
|
|
144
|
-
plan.args.push('--prompt', session.prompt);
|
|
145
|
-
break;
|
|
146
|
-
case 'pi':
|
|
147
|
-
if (session.prompt && !interactive) {
|
|
148
|
-
plan.args.push('--prompt', session.prompt);
|
|
149
|
-
}
|
|
150
|
-
break;
|
|
151
|
-
case 'opencode':
|
|
152
|
-
if (session.resumeId)
|
|
153
|
-
plan.args.push('--session', session.resumeId);
|
|
154
|
-
break;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// Provider auth validation helper
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
async function validateProviderAuth(plan) {
|
|
161
|
-
const { execSync } = await import('node:child_process');
|
|
162
|
-
try {
|
|
163
|
-
switch (plan.provider) {
|
|
164
|
-
case 'bedrock':
|
|
165
|
-
execSync('aws sts get-caller-identity', { stdio: 'ignore', timeout: 10_000 });
|
|
166
|
-
break;
|
|
167
|
-
case 'vertex':
|
|
168
|
-
execSync('gcloud auth application-default print-access-token', { stdio: 'ignore', timeout: 10_000 });
|
|
169
|
-
break;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
const guidance = {
|
|
174
|
-
bedrock: 'AWS credentials not configured. Run: aws configure',
|
|
175
|
-
vertex: 'GCP credentials not configured. Run: gcloud auth application-default login',
|
|
176
|
-
};
|
|
177
|
-
return guidance[plan.provider] ?? null;
|
|
178
|
-
}
|
|
179
|
-
return null;
|
|
180
|
-
}
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
// Ollama lifecycle helper
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
async function ensureOllamaReady(model) {
|
|
185
|
-
const { execSync, spawnSync } = await import('node:child_process');
|
|
186
|
-
// Check if Ollama is running
|
|
187
|
-
try {
|
|
188
|
-
execSync('ollama list', { stdio: 'ignore', timeout: 5000 });
|
|
189
|
-
}
|
|
190
|
-
catch {
|
|
191
|
-
return { ok: false, message: 'Ollama is not running. Start it with: ollama serve' };
|
|
192
|
-
}
|
|
193
|
-
// Check if model is available
|
|
194
|
-
try {
|
|
195
|
-
const list = execSync('ollama list', { encoding: 'utf-8', timeout: 5000 });
|
|
196
|
-
const lines = list.split('\n').map(l => l.trim()).filter(Boolean);
|
|
197
|
-
const modelNames = lines.slice(1).map(l => l.split(/\s+/)[0]);
|
|
198
|
-
const modelBase = model.split(':')[0];
|
|
199
|
-
if (!modelNames.some(n => n.startsWith(modelBase))) {
|
|
200
|
-
console.error(`[amux launch] Model '${model}' not found locally. Pulling...`);
|
|
201
|
-
const pull = spawnSync('ollama', ['pull', model], { stdio: 'inherit', timeout: 600_000 });
|
|
202
|
-
if (pull.status !== 0) {
|
|
203
|
-
return { ok: false, message: `Failed to pull model '${model}'` };
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (e) {
|
|
208
|
-
console.error(`[amux launch] Warning: could not verify model availability`);
|
|
209
|
-
}
|
|
210
|
-
return { ok: true };
|
|
211
|
-
}
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
// Main command handler
|
|
214
|
-
// ---------------------------------------------------------------------------
|
|
215
|
-
export async function launchCommand(client, args) {
|
|
216
|
-
const jsonMode = flagBool(args.flags, 'json') === true;
|
|
217
|
-
const harness = args.positionals[0];
|
|
218
|
-
const provider = args.positionals[1];
|
|
219
|
-
if (!harness) {
|
|
220
|
-
const msg = 'Usage: amux launch <harness> [provider] [flags...]\nRun "amux launch --help" for details.';
|
|
221
|
-
if (jsonMode)
|
|
222
|
-
printJsonError('VALIDATION_ERROR', msg);
|
|
223
|
-
else
|
|
224
|
-
printError(msg);
|
|
225
|
-
return ExitCode.USAGE_ERROR;
|
|
226
|
-
}
|
|
227
|
-
// Validate harness exists
|
|
228
|
-
const adapter = client.adapters.get(harness);
|
|
229
|
-
if (!adapter) {
|
|
230
|
-
const available = client.adapters.list().map((a) => a.agent).join(', ');
|
|
231
|
-
const msg = `Unknown harness '${harness}'. Available: ${available}`;
|
|
232
|
-
if (jsonMode)
|
|
233
|
-
printJsonError('AGENT_NOT_FOUND', msg);
|
|
234
|
-
else
|
|
235
|
-
printError(msg);
|
|
236
|
-
return ExitCode.USAGE_ERROR;
|
|
237
|
-
}
|
|
238
|
-
// Check harness is installed
|
|
239
|
-
if (adapter.detectInstallation) {
|
|
240
|
-
const installResult = await adapter.detectInstallation();
|
|
241
|
-
if (!installResult.installed) {
|
|
242
|
-
const installCmd = adapter.capabilities?.installMethods?.[0]?.command ?? `npm install -g ${harness}`;
|
|
243
|
-
const msg = `${harness} is not installed. Install with: ${installCmd}`;
|
|
244
|
-
if (jsonMode)
|
|
245
|
-
printJsonError('AGENT_NOT_FOUND', msg);
|
|
246
|
-
else
|
|
247
|
-
printError(msg);
|
|
248
|
-
return ExitCode.USAGE_ERROR;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
if (flagStr(args.flags, 'resume') && adapter.capabilities && !adapter.capabilities.canResume) {
|
|
252
|
-
const msg = `${harness} does not support session resumption`;
|
|
253
|
-
if (jsonMode)
|
|
254
|
-
printJsonError('CAPABILITY_ERROR', msg);
|
|
255
|
-
else
|
|
256
|
-
printError(msg);
|
|
257
|
-
return ExitCode.USAGE_ERROR;
|
|
258
|
-
}
|
|
259
|
-
// Validate proxy flag mutual exclusion
|
|
260
|
-
const withProxy = flagBool(args.flags, 'with-proxy') === true;
|
|
261
|
-
const withProxyIfNeeded = flagBool(args.flags, 'with-proxy-if-needed') === true;
|
|
262
|
-
const noProxy = flagBool(args.flags, 'no-proxy') === true;
|
|
263
|
-
if ((withProxy || withProxyIfNeeded) && noProxy) {
|
|
264
|
-
const msg = 'Cannot use --with-proxy/--with-proxy-if-needed with --no-proxy';
|
|
265
|
-
if (jsonMode)
|
|
266
|
-
printJsonError('VALIDATION_ERROR', msg);
|
|
267
|
-
else
|
|
268
|
-
printError(msg);
|
|
269
|
-
return ExitCode.USAGE_ERROR;
|
|
270
|
-
}
|
|
271
|
-
const dryRun = flagBool(args.flags, 'dry-run') === true;
|
|
272
|
-
const proxyMode = noProxy ? 'never'
|
|
273
|
-
: withProxy ? 'always'
|
|
274
|
-
: withProxyIfNeeded ? 'if-needed'
|
|
275
|
-
: 'never';
|
|
276
|
-
const providerArgs = flagArr(args.flags, 'provider-arg') ?? [];
|
|
277
|
-
const extraParams = {};
|
|
278
|
-
for (const arg of providerArgs) {
|
|
279
|
-
const eqIdx = arg.indexOf('=');
|
|
280
|
-
if (eqIdx > 0) {
|
|
281
|
-
extraParams[arg.slice(0, eqIdx)] = arg.slice(eqIdx + 1);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
let plan;
|
|
285
|
-
try {
|
|
286
|
-
plan = resolveLaunchPlan({
|
|
287
|
-
harness,
|
|
288
|
-
provider: provider,
|
|
289
|
-
model: flagStr(args.flags, 'model'),
|
|
290
|
-
transport: flagStr(args.flags, 'transport'),
|
|
291
|
-
apiKey: flagStr(args.flags, 'api-key'),
|
|
292
|
-
apiBase: flagStr(args.flags, 'api-base'),
|
|
293
|
-
region: flagStr(args.flags, 'region'),
|
|
294
|
-
project: flagStr(args.flags, 'project'),
|
|
295
|
-
resourceGroup: flagStr(args.flags, 'resource-group'),
|
|
296
|
-
endpointName: flagStr(args.flags, 'endpoint-name'),
|
|
297
|
-
authCommand: flagStr(args.flags, 'auth-command'),
|
|
298
|
-
profile: flagStr(args.flags, 'profile'),
|
|
299
|
-
proxyMode,
|
|
300
|
-
proxyPort: flagNum(args.flags, 'proxy-port'),
|
|
301
|
-
adapter: adapter,
|
|
302
|
-
providerArgs: extraParams,
|
|
303
|
-
});
|
|
304
|
-
}
|
|
305
|
-
catch (err) {
|
|
306
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
307
|
-
if (jsonMode)
|
|
308
|
-
printJsonError('VALIDATION_ERROR', msg);
|
|
309
|
-
else
|
|
310
|
-
printError(msg);
|
|
311
|
-
return ExitCode.USAGE_ERROR;
|
|
312
|
-
}
|
|
313
|
-
// Warn if auth appears missing for the resolved provider
|
|
314
|
-
const resolvedConfig = resolveProvider({
|
|
315
|
-
provider: provider,
|
|
316
|
-
model: flagStr(args.flags, 'model'),
|
|
317
|
-
apiKey: flagStr(args.flags, 'api-key'),
|
|
318
|
-
authCommand: flagStr(args.flags, 'auth-command'),
|
|
319
|
-
profile: flagStr(args.flags, 'profile'),
|
|
320
|
-
});
|
|
321
|
-
if (resolvedConfig.auth.type === 'api_key' && !resolvedConfig.auth.apiKey) {
|
|
322
|
-
const defaults = (await import('@a5c-ai/agent-mux-core')).PROVIDER_DEFAULTS;
|
|
323
|
-
const provId = resolvedConfig.provider;
|
|
324
|
-
const envKey = defaults[provId]?.envKey;
|
|
325
|
-
if (envKey) {
|
|
326
|
-
console.error(`Warning: No API key found for ${provId}. Set ${envKey} or use --api-key.`);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
// Provider-specific auth validation (Bedrock STS, Vertex ADC, etc.)
|
|
330
|
-
if (!dryRun) {
|
|
331
|
-
const authWarning = await validateProviderAuth(plan);
|
|
332
|
-
if (authWarning) {
|
|
333
|
-
console.error(`Warning: ${authWarning}`);
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
// Ollama lifecycle: verify server is running and model is available (pull if needed)
|
|
337
|
-
if (plan.provider === 'ollama' && plan.model && !dryRun) {
|
|
338
|
-
const ollamaCheck = await ensureOllamaReady(plan.model);
|
|
339
|
-
if (!ollamaCheck.ok) {
|
|
340
|
-
if (jsonMode)
|
|
341
|
-
printJsonError('SPAWN_ERROR', ollamaCheck.message);
|
|
342
|
-
else
|
|
343
|
-
printError(ollamaCheck.message);
|
|
344
|
-
return ExitCode.GENERAL_ERROR;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
// Dry-run: print plan and exit without spawning anything
|
|
348
|
-
if (dryRun) {
|
|
349
|
-
const output = JSON.parse(JSON.stringify(plan));
|
|
350
|
-
for (const [k, v] of Object.entries(output.env)) {
|
|
351
|
-
if (k.toLowerCase().includes('key') || k.toLowerCase().includes('token')) {
|
|
352
|
-
output.env[k] = String(v).slice(0, 8) + '***';
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
console.log(JSON.stringify(output, null, 2));
|
|
356
|
-
return ExitCode.SUCCESS;
|
|
357
|
-
}
|
|
358
|
-
const workspaceService = new WorkspaceService();
|
|
359
|
-
let launchCwd = process.cwd();
|
|
360
|
-
const workspaceIdentifier = flagStr(args.flags, 'workspace');
|
|
361
|
-
const workspaceCreate = flagBool(args.flags, 'workspace-create') === true;
|
|
362
|
-
const workspaceRepos = flagArr(args.flags, 'workspace-repo');
|
|
363
|
-
const workspaceName = flagStr(args.flags, 'workspace-name') ?? `${harness}-workspace`;
|
|
364
|
-
if (workspaceCreate) {
|
|
365
|
-
const repos = workspaceRepos.length > 0 ? workspaceRepos : [process.cwd()];
|
|
366
|
-
const workspace = await workspaceService.createWorkspace({
|
|
367
|
-
name: workspaceName,
|
|
368
|
-
repos: repos.map((repo) => ({ path: repo })),
|
|
369
|
-
mode: flagStr(args.flags, 'workspace-mode') === 'symlink' ? 'symlink' : 'worktree',
|
|
370
|
-
});
|
|
371
|
-
launchCwd = resolveWorkspaceDefaultCwd(workspace);
|
|
372
|
-
}
|
|
373
|
-
else if (workspaceIdentifier) {
|
|
374
|
-
const workspace = await workspaceService.resolveWorkspace(workspaceIdentifier);
|
|
375
|
-
if (!workspace) {
|
|
376
|
-
const msg = `Unknown workspace '${workspaceIdentifier}'`;
|
|
377
|
-
if (jsonMode)
|
|
378
|
-
printJsonError('VALIDATION_ERROR', msg);
|
|
379
|
-
else
|
|
380
|
-
printError(msg);
|
|
381
|
-
return ExitCode.USAGE_ERROR;
|
|
382
|
-
}
|
|
383
|
-
launchCwd = resolveWorkspaceDefaultCwd(workspace);
|
|
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
|
-
}
|
|
424
|
-
// Append session/prompt args
|
|
425
|
-
const prompt = flagStr(args.flags, 'prompt');
|
|
426
|
-
appendHarnessSessionArgs(plan, {
|
|
427
|
-
resumeId: flagStr(args.flags, 'resume'),
|
|
428
|
-
sessionId: flagStr(args.flags, 'session-id'),
|
|
429
|
-
prompt,
|
|
430
|
-
maxTurns: flagNum(args.flags, 'max-turns'),
|
|
431
|
-
interactive: isInteractive || bridgeInteractive,
|
|
432
|
-
bridgeInteractive,
|
|
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
|
-
}
|
|
453
|
-
// Passthrough args after --
|
|
454
|
-
const dashDashIdx = process.argv.indexOf('--');
|
|
455
|
-
if (dashDashIdx >= 0) {
|
|
456
|
-
plan.args.push(...process.argv.slice(dashDashIdx + 1));
|
|
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
|
-
}
|
|
463
|
-
// Launch runtime if needed
|
|
464
|
-
let proxyRuntime;
|
|
465
|
-
if (plan.proxyNeeded && plan.proxy) {
|
|
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
|
-
}
|
|
490
|
-
proxyRuntime = await startTransportMuxRuntime({
|
|
491
|
-
targetProvider: plan.proxy.targetProvider,
|
|
492
|
-
targetModel: `${plan.proxy.targetProvider}/${plan.proxy.targetModel}`,
|
|
493
|
-
exposedTransport: plan.proxy.exposedTransport,
|
|
494
|
-
port: plan.proxy.port,
|
|
495
|
-
apiBase: plan.proxy.apiBase,
|
|
496
|
-
completionEngine,
|
|
497
|
-
});
|
|
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
|
-
}
|
|
529
|
-
}
|
|
530
|
-
catch (err) {
|
|
531
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
532
|
-
if (jsonMode)
|
|
533
|
-
printJsonError('SPAWN_ERROR', `Failed to launch transport runtime: ${msg}`);
|
|
534
|
-
else
|
|
535
|
-
printError(`Failed to launch transport runtime: ${msg}`);
|
|
536
|
-
return ExitCode.GENERAL_ERROR;
|
|
537
|
-
}
|
|
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
|
-
}
|
|
553
|
-
// Spawn harness
|
|
554
|
-
let child = null;
|
|
555
|
-
let ptyProcess = null;
|
|
556
|
-
if (isInteractive) {
|
|
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).
|
|
559
|
-
try {
|
|
560
|
-
const nodePty = await import('node-pty');
|
|
561
|
-
ptyProcess = nodePty.spawn(plan.command, plan.args, {
|
|
562
|
-
name: 'xterm-256color',
|
|
563
|
-
cols: process.stdout.columns || 80,
|
|
564
|
-
rows: process.stdout.rows || 24,
|
|
565
|
-
cwd: launchCwd,
|
|
566
|
-
env: { ...process.env, ...plan.env },
|
|
567
|
-
});
|
|
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
|
-
});
|
|
623
|
-
if (process.stdin.isTTY) {
|
|
624
|
-
process.stdin.setRawMode(true);
|
|
625
|
-
}
|
|
626
|
-
process.stdin.resume();
|
|
627
|
-
process.stdin.on('data', (data) => ptyProcess.write(data.toString()));
|
|
628
|
-
// Handle terminal resize
|
|
629
|
-
process.stdout.on('resize', () => {
|
|
630
|
-
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
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
|
-
}
|
|
636
|
-
// Create a fake ChildProcess-like for signal handling
|
|
637
|
-
child = { pid: ptyProcess.pid, kill: (sig) => ptyProcess.kill(sig) };
|
|
638
|
-
}
|
|
639
|
-
catch {
|
|
640
|
-
// node-pty not available, fall back to stdio inherit with stdin pipe for prompt injection
|
|
641
|
-
const { spawn } = await import('node:child_process');
|
|
642
|
-
child = spawn(plan.command, plan.args, {
|
|
643
|
-
stdio: prompt ? ['pipe', 'inherit', 'inherit'] : 'inherit',
|
|
644
|
-
env: { ...process.env, ...plan.env },
|
|
645
|
-
cwd: launchCwd,
|
|
646
|
-
shell: process.platform === 'win32',
|
|
647
|
-
});
|
|
648
|
-
}
|
|
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
|
-
}
|
|
787
|
-
else {
|
|
788
|
-
// Non-interactive: plain spawn. Each harness handles non-interactive mode
|
|
789
|
-
// internally (claude -p, codex exec, gemini --prompt, pi stdin).
|
|
790
|
-
const { spawn } = await import('node:child_process');
|
|
791
|
-
child = spawn(plan.command, plan.args, {
|
|
792
|
-
stdio: ['pipe', 'inherit', 'inherit'],
|
|
793
|
-
env: { ...process.env, ...plan.env },
|
|
794
|
-
cwd: launchCwd,
|
|
795
|
-
shell: process.platform === 'win32',
|
|
796
|
-
});
|
|
797
|
-
}
|
|
798
|
-
if (flagBool(args.flags, 'observe')) {
|
|
799
|
-
if (isInteractive) {
|
|
800
|
-
console.error('[amux launch] Warning: --observe does not work with interactive PTY mode');
|
|
801
|
-
}
|
|
802
|
-
else {
|
|
803
|
-
// Tee stdout to both console and a log file
|
|
804
|
-
const logPath = `.amux-launch-${Date.now()}.log`;
|
|
805
|
-
const logStream = (await import('node:fs')).createWriteStream(logPath);
|
|
806
|
-
child.stdout?.on('data', (chunk) => {
|
|
807
|
-
process.stdout.write(chunk);
|
|
808
|
-
logStream.write(chunk);
|
|
809
|
-
});
|
|
810
|
-
child.stderr?.on('data', (chunk) => {
|
|
811
|
-
process.stderr.write(chunk);
|
|
812
|
-
logStream.write(chunk);
|
|
813
|
-
});
|
|
814
|
-
child.on('exit', () => logStream.end());
|
|
815
|
-
console.error(`[amux launch] Observing output to ${logPath}`);
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
const forwardSignal = (sig) => {
|
|
819
|
-
if (process.platform === 'win32') {
|
|
820
|
-
try {
|
|
821
|
-
const { execSync } = require('node:child_process');
|
|
822
|
-
execSync(`taskkill /PID ${child.pid} /T /F`, { stdio: 'ignore' });
|
|
823
|
-
}
|
|
824
|
-
catch { /* process may already be dead */ }
|
|
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
|
-
}
|
|
837
|
-
else {
|
|
838
|
-
child.kill(sig);
|
|
839
|
-
}
|
|
840
|
-
};
|
|
841
|
-
process.on('SIGINT', forwardSignal);
|
|
842
|
-
process.on('SIGTERM', forwardSignal);
|
|
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();
|
|
855
|
-
}
|
|
856
|
-
else {
|
|
857
|
-
// Interactive with stdin pipe (no PTY): reconnect terminal stdin after prompt injection
|
|
858
|
-
process.stdin.resume();
|
|
859
|
-
process.stdin.pipe(child.stdin);
|
|
860
|
-
}
|
|
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
|
-
}));
|
|
878
|
-
process.off('SIGINT', forwardSignal);
|
|
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
|
-
}
|
|
909
|
-
if (proxyRuntime) {
|
|
910
|
-
await proxyRuntime.stop();
|
|
911
|
-
}
|
|
912
|
-
return exitCode;
|
|
913
|
-
}
|
|
7
|
+
export { launchCommand, resolveLaunchPlan, LAUNCH_FLAGS, } from '@a5c-ai/agent-launch-mux';
|
|
914
8
|
//# sourceMappingURL=launch.js.map
|