@phnx-labs/agents-cli 1.20.22 → 1.20.23

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.
@@ -4,6 +4,7 @@ import ora from 'ora';
4
4
  import { resolveProvider, getAllProviders, getDefaultProviderId } from '../lib/cloud/registry.js';
5
5
  import { insertTask, updateTaskStatus, getTaskById, listTasks as listStoredTasks, listActiveTasks } from '../lib/cloud/store.js';
6
6
  import { renderStream } from '../lib/cloud/stream.js';
7
+ import { MissingTargetError } from '../lib/cloud/types.js';
7
8
  /** Print an error message to stderr and exit. */
8
9
  function die(msg, code = 1) {
9
10
  console.error(chalk.red(msg));
@@ -43,6 +44,47 @@ function statusColor(status) {
43
44
  function isJsonMode(opts) {
44
45
  return Boolean(opts.json) || !process.stdout.isTTY;
45
46
  }
47
+ /**
48
+ * After a `MissingTargetError`, try to resolve the target interactively.
49
+ * Returns the chosen id, or undefined when no interactive resolution is
50
+ * possible (non-TTY/JSON, provider can't enumerate, or user cancels) — the
51
+ * caller then prints the error's guidance.
52
+ *
53
+ * Codex has no `listTargets` (no list-environments CLI), so it always returns
54
+ * undefined here and the user sees the `codex cloud` guidance. Factory lists
55
+ * Droid Computers; if listing fails (not signed in) or parses to nothing, we
56
+ * fall back to a free-text prompt so a dispatch is never hard-blocked.
57
+ */
58
+ async function pickMissingTarget(provider, err, json) {
59
+ if (json || !process.stdout.isTTY)
60
+ return undefined;
61
+ if (!provider.listTargets)
62
+ return undefined;
63
+ const { select, input } = await import('@inquirer/prompts');
64
+ const promptName = err.kind === 'env' ? 'environment' : 'computer';
65
+ let targets;
66
+ try {
67
+ targets = await provider.listTargets();
68
+ }
69
+ catch (listErr) {
70
+ process.stderr.write(chalk.dim(`Could not list ${promptName}s: ${listErr.message}\n`));
71
+ targets = [];
72
+ }
73
+ try {
74
+ if (targets.length > 0) {
75
+ return await select({
76
+ message: `Select a ${promptName}`,
77
+ choices: targets.map((t) => ({ value: t.id, name: t.label ? `${t.id} ${chalk.dim(t.label)}` : t.id })),
78
+ });
79
+ }
80
+ const typed = (await input({ message: `No ${promptName}s found. Enter a ${promptName} name (blank to cancel):` })).trim();
81
+ return typed || undefined;
82
+ }
83
+ catch {
84
+ // User hit Ctrl-C / Esc on the prompt.
85
+ return undefined;
86
+ }
87
+ }
46
88
  /** Register the `agents cloud` command tree (run, list, status, logs, cancel, message, providers). */
47
89
  export function registerCloudCommands(program) {
48
90
  const cloud = program
@@ -182,16 +224,41 @@ Examples:
182
224
  }
183
225
  if (options.uploadAccountTokens)
184
226
  dispatchOptions.providerOptions.uploadAccountTokens = true;
185
- // Dispatch
186
- const spinner = ora({ text: `Dispatching to ${provider.name}...`, stream: process.stderr }).start();
227
+ // Dispatch. On a missing pre-provisioned target (Codex env / Factory
228
+ // computer), offer an interactive picker instead of a raw error.
229
+ const dispatchOnce = async () => {
230
+ const spinner = ora({ text: `Dispatching to ${provider.name}...`, stream: process.stderr }).start();
231
+ try {
232
+ const t = await provider.dispatch(dispatchOptions);
233
+ spinner.succeed(`Task ${t.id} dispatched to ${provider.name}`);
234
+ return t;
235
+ }
236
+ catch (err) {
237
+ spinner.fail('Dispatch failed');
238
+ throw err;
239
+ }
240
+ };
187
241
  let task;
188
242
  try {
189
- task = await provider.dispatch(dispatchOptions);
190
- spinner.succeed(`Task ${task.id} dispatched to ${provider.name}`);
243
+ task = await dispatchOnce();
191
244
  }
192
245
  catch (err) {
193
- spinner.fail('Dispatch failed');
194
- die(err.message);
246
+ if (err instanceof MissingTargetError) {
247
+ const picked = await pickMissingTarget(provider, err, json);
248
+ if (!picked) {
249
+ die(err.guidance ? `${err.message}\n\n${err.guidance}` : err.message);
250
+ }
251
+ dispatchOptions.providerOptions[err.kind] = picked;
252
+ try {
253
+ task = await dispatchOnce();
254
+ }
255
+ catch (err2) {
256
+ die(err2.message);
257
+ }
258
+ }
259
+ else {
260
+ die(err.message);
261
+ }
195
262
  }
196
263
  // Persist locally
197
264
  insertTask(task);
@@ -422,4 +489,57 @@ Examples:
422
489
  console.log(` ${p.id.padEnd(12)} ${p.name.padEnd(20)} ${status}${defaultTag}`);
423
490
  }
424
491
  });
492
+ // ── agents cloud envs ─────────────────────────────────────────────────
493
+ // Discover the pre-provisioned targets a provider runs inside — Codex
494
+ // environments, Factory Droid Computers — so users don't copy opaque IDs
495
+ // out of a web UI.
496
+ cloud
497
+ .command('envs')
498
+ .alias('targets')
499
+ .description('List the pre-provisioned targets (Codex environments / Droid Computers) you can dispatch into.')
500
+ .option('--provider <id>', 'Only this provider (codex, factory, ...)')
501
+ .option('--json', 'JSON output')
502
+ .action(async (options) => {
503
+ const json = isJsonMode(options);
504
+ const only = options.provider;
505
+ // Providers that run inside a pre-provisioned target declare targetKind.
506
+ const providers = getAllProviders().filter((p) => p.targetKind && (!only || p.id === only));
507
+ if (only && providers.length === 0) {
508
+ die(`Provider '${only}' has no pre-provisioned targets (or is unknown). Targets apply to: codex, factory.`);
509
+ }
510
+ const results = [];
511
+ for (const p of providers) {
512
+ const kind = p.targetKind;
513
+ if (!p.listTargets) {
514
+ // Not enumerable (Codex). Surface guidance instead of a list.
515
+ const guidance = kind === 'env'
516
+ ? 'Codex environments are not listable from the CLI. Browse/create them with `codex cloud` (interactive), then use --env <id>.'
517
+ : 'Not enumerable from the CLI.';
518
+ results.push({ provider: p.id, kind, targets: [], note: guidance });
519
+ continue;
520
+ }
521
+ try {
522
+ const targets = await p.listTargets();
523
+ results.push({ provider: p.id, kind, targets: targets.map((t) => ({ id: t.id, label: t.label })) });
524
+ }
525
+ catch (err) {
526
+ results.push({ provider: p.id, kind, targets: [], note: err.message });
527
+ }
528
+ }
529
+ if (json) {
530
+ process.stdout.write(JSON.stringify(results, null, 2) + '\n');
531
+ return;
532
+ }
533
+ for (const r of results) {
534
+ console.log(chalk.bold(`\n${r.provider}`) + chalk.dim(` (${r.kind})`));
535
+ if (r.targets.length > 0) {
536
+ for (const t of r.targets) {
537
+ console.log(` ${t.id}${t.label ? ' ' + chalk.dim(t.label) : ''}`);
538
+ }
539
+ }
540
+ else {
541
+ console.log(chalk.dim(` ${r.note ?? 'none'}`));
542
+ }
543
+ }
544
+ });
425
545
  }
@@ -150,6 +150,10 @@ export function registerRunCommand(program) {
150
150
 
151
151
  # Inject a keychain-backed secrets bundle
152
152
  agents run claude "deploy the worker" --secrets prod --mode edit
153
+
154
+ # Pass arbitrary native flags to the underlying CLI via -- separator
155
+ agents run kimi -- --plan --some-kimi-option value
156
+ agents run claude "fix the bug" -- --custom-flag
153
157
  `,
154
158
  notes: `
155
159
  Modes (not every agent supports every mode — check agents.yaml capabilities):
@@ -168,9 +172,16 @@ export function registerRunCommand(program) {
168
172
  Fallback: --fallback codex,gemini retries on rate-limit failure via /continue handoff. Each entry accepts @version.
169
173
 
170
174
  Resume: --session-id <id> continues a prior Claude conversation.
175
+
176
+ Passthrough: everything after -- is forwarded verbatim to the underlying agent CLI.
177
+ agents run kimi -- --plan --some-native-flag value
171
178
  `,
172
179
  });
173
- runCmd.action(async (agentSpec, prompt, options) => {
180
+ runCmd.action(async (agentSpec, prompt, options, command) => {
181
+ // Capture everything after -- as passthrough args forwarded verbatim to the underlying CLI.
182
+ // Use command.args (all positional strings) and strip the declared positional args from the front.
183
+ const declaredArgCount = prompt !== undefined ? 2 : 1;
184
+ const passthroughArgs = command.args.slice(declaredArgCount);
174
185
  // --resume-checkpoint short-circuits normal dispatch entirely: the
175
186
  // checkpoint already carries the agent, version, prompt, session id,
176
187
  // iteration, and loop config of the killed run. Reconstruct ExecOptions
@@ -642,6 +653,7 @@ export function registerRunCommand(program) {
642
653
  env,
643
654
  toolsRestrict: workflowToolsRestrict,
644
655
  mcpConfigPath: workflowMcpConfigPath,
656
+ passthroughArgs,
645
657
  };
646
658
  if (options.interactive && options.headless) {
647
659
  console.error(chalk.red('--interactive and --headless are mutually exclusive. Pass one, or neither (mode is inferred from prompt presence).'));