@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.
Files changed (62) hide show
  1. package/dist/bootstrap.d.ts +1 -1
  2. package/dist/commands/adapters.d.ts +2 -6
  3. package/dist/commands/adapters.d.ts.map +1 -1
  4. package/dist/commands/adapters.js +2 -129
  5. package/dist/commands/adapters.js.map +1 -1
  6. package/dist/commands/agent.d.ts +1 -1
  7. package/dist/commands/auth.d.ts +2 -6
  8. package/dist/commands/auth.d.ts.map +1 -1
  9. package/dist/commands/auth.js +2 -134
  10. package/dist/commands/auth.js.map +1 -1
  11. package/dist/commands/config.d.ts +2 -6
  12. package/dist/commands/config.d.ts.map +1 -1
  13. package/dist/commands/config.js +2 -218
  14. package/dist/commands/config.js.map +1 -1
  15. package/dist/commands/detect-host.d.ts +2 -5
  16. package/dist/commands/detect-host.d.ts.map +1 -1
  17. package/dist/commands/detect-host.js +2 -30
  18. package/dist/commands/detect-host.js.map +1 -1
  19. package/dist/commands/doctor.d.ts +1 -1
  20. package/dist/commands/gateway/index.d.ts +1 -1
  21. package/dist/commands/hooks.d.ts +1 -1
  22. package/dist/commands/hooks.js +1 -1
  23. package/dist/commands/install-helpers.d.ts +3 -22
  24. package/dist/commands/install-helpers.d.ts.map +1 -1
  25. package/dist/commands/install-helpers.js +2 -55
  26. package/dist/commands/install-helpers.js.map +1 -1
  27. package/dist/commands/install.d.ts +2 -22
  28. package/dist/commands/install.d.ts.map +1 -1
  29. package/dist/commands/install.js +2 -445
  30. package/dist/commands/install.js.map +1 -1
  31. package/dist/commands/launch-bridge-hooks.d.ts +5 -56
  32. package/dist/commands/launch-bridge-hooks.d.ts.map +1 -1
  33. package/dist/commands/launch-bridge-hooks.js +4 -224
  34. package/dist/commands/launch-bridge-hooks.js.map +1 -1
  35. package/dist/commands/launch-completion-engine.d.ts +4 -4
  36. package/dist/commands/launch-completion-engine.d.ts.map +1 -1
  37. package/dist/commands/launch-completion-engine.js +4 -4
  38. package/dist/commands/launch-completion-engine.js.map +1 -1
  39. package/dist/commands/launch.d.ts +5 -54
  40. package/dist/commands/launch.d.ts.map +1 -1
  41. package/dist/commands/launch.js +4 -910
  42. package/dist/commands/launch.js.map +1 -1
  43. package/dist/commands/mcp.d.ts +1 -1
  44. package/dist/commands/models.d.ts +1 -1
  45. package/dist/commands/models.js +1 -1
  46. package/dist/commands/plugin.d.ts +1 -1
  47. package/dist/commands/profiles.d.ts +1 -1
  48. package/dist/commands/profiles.js +1 -1
  49. package/dist/commands/remote.d.ts +1 -1
  50. package/dist/commands/remote.js +1 -1
  51. package/dist/commands/run.d.ts +1 -1
  52. package/dist/commands/run.js +1 -1
  53. package/dist/commands/sessions.d.ts +1 -1
  54. package/dist/commands/sessions.js +1 -1
  55. package/dist/commands/skill.d.ts +1 -1
  56. package/dist/commands/tui.d.ts +1 -1
  57. package/dist/commands/workspaces.d.ts +1 -1
  58. package/dist/commands/workspaces.js +1 -1
  59. package/dist/exit-codes.d.ts +1 -1
  60. package/dist/index.d.ts +1 -1
  61. package/dist/index.js +2 -2
  62. package/package.json +16 -6
@@ -1,914 +1,8 @@
1
1
  /**
2
- * `amux launch` command implementation.
2
+ * `amux launch` command — thin wrapper.
3
3
  *
4
- * Resolves a launch plan for a given harness+provider combination,
5
- * optionally starts the transport-mux runtime, then exec-forks the harness with
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
- import { resolveProvider, resolveWorkspaceDefaultCwd, WorkspaceService, } from '@a5c-ai/agent-mux-core';
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