@phnx-labs/agents-cli 1.20.21 → 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.
Files changed (40) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/commands/cloud.js +142 -13
  3. package/dist/commands/exec.js +13 -1
  4. package/dist/commands/menubar.d.ts +10 -0
  5. package/dist/commands/menubar.js +83 -0
  6. package/dist/commands/routines.js +34 -1
  7. package/dist/commands/secrets.d.ts +1 -1
  8. package/dist/commands/secrets.js +95 -38
  9. package/dist/index.js +292 -225
  10. package/dist/lib/agents.js +8 -0
  11. package/dist/lib/cloud/antigravity.d.ts +70 -0
  12. package/dist/lib/cloud/antigravity.js +196 -0
  13. package/dist/lib/cloud/codex.d.ts +1 -0
  14. package/dist/lib/cloud/codex.js +8 -2
  15. package/dist/lib/cloud/factory.d.ts +79 -18
  16. package/dist/lib/cloud/factory.js +324 -26
  17. package/dist/lib/cloud/registry.d.ts +18 -2
  18. package/dist/lib/cloud/registry.js +28 -4
  19. package/dist/lib/cloud/types.d.ts +73 -2
  20. package/dist/lib/cloud/types.js +17 -0
  21. package/dist/lib/exec.d.ts +2 -0
  22. package/dist/lib/exec.js +5 -0
  23. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +20 -0
  24. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  25. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +115 -0
  26. package/dist/lib/menubar/install-menubar.d.ts +57 -0
  27. package/dist/lib/menubar/install-menubar.js +291 -0
  28. package/dist/lib/secrets/agent.d.ts +9 -1
  29. package/dist/lib/secrets/agent.js +91 -10
  30. package/dist/lib/secrets/bundles.d.ts +19 -12
  31. package/dist/lib/secrets/bundles.js +22 -14
  32. package/dist/lib/self-update.d.ts +34 -0
  33. package/dist/lib/self-update.js +63 -2
  34. package/dist/lib/startup/command-registry.d.ts +99 -0
  35. package/dist/lib/startup/command-registry.js +136 -0
  36. package/dist/lib/types.d.ts +8 -0
  37. package/dist/lib/version.d.ts +11 -0
  38. package/dist/lib/version.js +20 -0
  39. package/package.json +5 -3
  40. package/scripts/postinstall.js +35 -0
@@ -1,30 +1,217 @@
1
1
  /**
2
- * Factory/Droid cloud provider -- stub for Phase 2.
2
+ * Factory (Droid) cloud provider.
3
3
  *
4
- * Will dispatch tasks to a `droid daemon` running on a remote machine.
5
- * All methods throw until the droid daemon API is documented and stable.
4
+ * Dispatches to a Factory **Droid Computer** a persistent cloud VM (managed
5
+ * by Factory, or bring-your-own via `droid computer register`). There is no
6
+ * `droid cloud run`; remote execution = reach the computer over the Droid relay
7
+ * (`droid computer ssh <name>`) and run a headless `droid exec` there.
8
+ *
9
+ * `droid exec` is synchronous (it runs to completion and exits, unlike Codex
10
+ * Cloud which is async server-side). So `dispatch()` runs the remote exec to
11
+ * completion with `--output-format stream-json`, buffers the NDJSON events, and
12
+ * `stream()` replays them. The task id is droid's own `session_id` (captured
13
+ * from the run output), so it lines up with `droid exec -s <id>` for future
14
+ * resume support.
15
+ *
16
+ * Transport note: the exact relay SSH composition (user + ProxyCommand) is
17
+ * built in `buildSshArgs()` and must be confirmed against a live provisioned
18
+ * Droid Computer (Factory auth required). `capabilities().available` gates on
19
+ * the droid binary + a configured computer so the provider fails with a clear
20
+ * message rather than misfiring when unconfigured.
21
+ */
22
+ import { spawn, execFileSync } from 'child_process';
23
+ import * as fs from 'fs';
24
+ import * as path from 'path';
25
+ import { MissingTargetError } from './types.js';
26
+ import { getShimsDir } from '../state.js';
27
+ const SHIMS_DIR = getShimsDir();
28
+ const DEFAULT_AUTONOMY = 'high';
29
+ const VALID_AUTONOMY = new Set(['low', 'medium', 'high']);
30
+ /** Locate the droid binary, checking agents-cli shims first then PATH. */
31
+ export function findDroidBinary() {
32
+ const shim = path.join(SHIMS_DIR, 'droid');
33
+ if (fs.existsSync(shim))
34
+ return shim;
35
+ try {
36
+ return execFileSync('which', ['droid'], { stdio: 'pipe' }).toString().trim() || null;
37
+ }
38
+ catch {
39
+ return null;
40
+ }
41
+ }
42
+ /** Normalize an autonomy value, falling back to the safe cloud default (`high`). */
43
+ export function resolveAutonomy(value, fallback = DEFAULT_AUTONOMY) {
44
+ return typeof value === 'string' && VALID_AUTONOMY.has(value)
45
+ ? value
46
+ : fallback;
47
+ }
48
+ /** Run the droid CLI and capture output (used for `computer list`). */
49
+ function runDroid(bin, args) {
50
+ return new Promise((resolve) => {
51
+ const proc = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
52
+ let stdout = '';
53
+ let stderr = '';
54
+ proc.stdout.on('data', (d) => { stdout += d.toString(); });
55
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
56
+ proc.on('error', (e) => resolve({ stdout, stderr: stderr + String(e), code: 127 }));
57
+ proc.on('close', (code) => resolve({ stdout, stderr, code: code ?? 1 }));
58
+ });
59
+ }
60
+ /**
61
+ * Parse `droid computer list` text into targets. Defensive: the exact column
62
+ * layout isn't documented, so we take the first whitespace token of each data
63
+ * row as the computer name and keep the remainder as a label, skipping headers,
64
+ * separators, and status messages. The interactive picker degrades to free-text
65
+ * entry if this yields nothing, so an unexpected layout never blocks a dispatch.
6
66
  */
67
+ export function parseComputerList(text) {
68
+ const out = [];
69
+ for (const raw of text.split('\n')) {
70
+ const line = raw.trim();
71
+ if (!line)
72
+ continue;
73
+ if (/^(name|computer|status|id)\b/i.test(line))
74
+ continue; // header row
75
+ if (/^[-=_\s|]+$/.test(line))
76
+ continue; // separator rule
77
+ if (/^(no |failed|error|warning)\b/i.test(line))
78
+ continue; // status message
79
+ const name = line.split(/\s+/)[0];
80
+ if (!name)
81
+ continue;
82
+ const label = line.slice(name.length).trim() || undefined;
83
+ out.push({ id: name, label, kind: 'computer' });
84
+ }
85
+ return out;
86
+ }
7
87
  /**
8
- * Factory/Droid cloud provider stub for Phase 2.
9
- *
10
- * Integration path: `droid daemon` running on a remote machine (workstation, cloud VM, k8s pod).
11
- * Dispatch via HTTP to the daemon, stream output, cancel via HTTP DELETE.
88
+ * Build the remote `droid exec` argv. Headless, stream-json output, given
89
+ * autonomy. `sessionId` (when resuming) maps to `-s`.
90
+ */
91
+ export function buildExecArgs(prompt, opts) {
92
+ const args = ['exec', '--auto', opts.autonomy, '--output-format', 'stream-json'];
93
+ if (opts.sessionId)
94
+ args.push('-s', opts.sessionId);
95
+ args.push(prompt);
96
+ return args;
97
+ }
98
+ /**
99
+ * Build the `ssh` argv that runs a remote command on a Droid Computer through
100
+ * the Droid relay. The relay is used as an OpenSSH ProxyCommand
101
+ * (`droid computer ssh <name> --proxy`), so the connection rides Factory's
102
+ * brokered tunnel rather than a directly reachable host.
12
103
  *
13
- * Not yet implemented because:
14
- * 1. Droid v0.104 has no cloud dispatch command (droid exec is local only)
15
- * 2. droid daemon API isn't documented yet
16
- * 3. droid computer register/ssh is the remote execution primitive but needs exploration
104
+ * `remoteArgv` is the already-built remote command (e.g. droid exec argv); it is
105
+ * shell-quoted into a single remote command string.
17
106
  */
107
+ export function buildSshArgs(computer, remoteBin, remoteArgv, opts) {
108
+ const remoteCmd = [remoteBin, ...remoteArgv].map(shellQuote).join(' ');
109
+ const proxy = `ProxyCommand=${opts.droidBin} computer ssh ${computer} --proxy --port %p`;
110
+ return [
111
+ '-o', proxy,
112
+ '-o', 'StrictHostKeyChecking=accept-new',
113
+ '-p', opts.port ?? '22',
114
+ `${opts.user}@${computer}`,
115
+ remoteCmd,
116
+ ];
117
+ }
118
+ /** POSIX single-quote a shell argument. */
119
+ function shellQuote(arg) {
120
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
121
+ }
122
+ /** Map a droid stream-json `result.subtype` / `is_error` to a CloudTaskStatus. */
123
+ export function mapResultStatus(line) {
124
+ if (line.is_error)
125
+ return 'failed';
126
+ if (line.subtype && /cancel/i.test(line.subtype))
127
+ return 'cancelled';
128
+ return 'completed';
129
+ }
130
+ /**
131
+ * Map one parsed droid stream-json event to a CloudEvent. The stream-json
132
+ * schema is only partially documented, so this is defensive: known shapes map
133
+ * to typed events, everything else surfaces as `unknown` rather than being
134
+ * dropped (mirrors the rest of the cloud event pipeline).
135
+ */
136
+ export function mapDroidEvent(obj) {
137
+ const ts = new Date().toISOString();
138
+ const type = String(obj.type ?? '');
139
+ switch (type) {
140
+ case 'result': {
141
+ return {
142
+ type: 'done',
143
+ status: mapResultStatus(obj),
144
+ summary: typeof obj.result === 'string' ? obj.result : undefined,
145
+ timestamp: ts,
146
+ };
147
+ }
148
+ case 'assistant':
149
+ case 'message':
150
+ case 'text': {
151
+ const content = extractText(obj);
152
+ if (content)
153
+ return { type: 'text', content, timestamp: ts };
154
+ break;
155
+ }
156
+ case 'thinking':
157
+ case 'reasoning': {
158
+ const content = extractText(obj);
159
+ if (content)
160
+ return { type: 'thinking', content, timestamp: ts };
161
+ break;
162
+ }
163
+ case 'tool_call':
164
+ case 'tool_use': {
165
+ return { type: 'tool_use', tool: String(obj.name ?? obj.tool ?? 'tool'), input: obj.input ?? obj.arguments ?? {}, timestamp: ts };
166
+ }
167
+ case 'tool_result': {
168
+ return { type: 'tool_result', tool: String(obj.name ?? obj.tool ?? 'tool'), output: obj.output ?? obj.result ?? '', timestamp: ts };
169
+ }
170
+ case 'error': {
171
+ return { type: 'error', message: String(obj.message ?? obj.error ?? 'unknown error'), timestamp: ts };
172
+ }
173
+ }
174
+ return { type: 'unknown', name: type || 'unknown', data: JSON.stringify(obj), timestamp: ts };
175
+ }
176
+ /** Pull a text string out of the varied droid message shapes. */
177
+ function extractText(obj) {
178
+ if (typeof obj.text === 'string')
179
+ return obj.text;
180
+ if (typeof obj.content === 'string')
181
+ return obj.content;
182
+ if (Array.isArray(obj.content)) {
183
+ return obj.content
184
+ .map((c) => (c && typeof c === 'object' && typeof c.text === 'string' ? c.text : ''))
185
+ .join('');
186
+ }
187
+ const msg = obj.message;
188
+ if (msg && typeof msg === 'object')
189
+ return extractText(msg);
190
+ return '';
191
+ }
18
192
  export class FactoryCloudProvider {
19
193
  id = 'factory';
20
194
  name = 'Factory (Droid)';
195
+ targetKind = 'computer';
196
+ defaultComputer;
197
+ defaultAutonomy;
198
+ /** session_id → buffered run, populated by dispatch, drained by stream. */
199
+ runs = new Map();
200
+ constructor(config) {
201
+ this.defaultComputer = config?.computer;
202
+ this.defaultAutonomy = resolveAutonomy(config?.autonomy);
203
+ }
21
204
  capabilities() {
205
+ const droid = findDroidBinary() !== null;
206
+ const computer = Boolean(this.defaultComputer);
22
207
  return {
23
- available: false,
24
- dispatch: false,
25
- status: false,
26
- list: false,
27
- stream: false,
208
+ // Reachable only when the droid binary exists AND a computer is set.
209
+ // (A per-dispatch --computer can still override the missing default.)
210
+ available: droid && computer,
211
+ dispatch: droid,
212
+ status: droid,
213
+ list: droid,
214
+ stream: droid,
28
215
  cancel: false,
29
216
  message: false,
30
217
  multiRepo: false,
@@ -32,22 +219,133 @@ export class FactoryCloudProvider {
32
219
  images: false,
33
220
  };
34
221
  }
35
- async dispatch(_options) {
36
- throw new Error('Factory cloud provider is not yet available. Coming in a future release.');
222
+ /** Enumerate Droid Computers via `droid computer list`. Throws if not signed in. */
223
+ async listTargets() {
224
+ const droidBin = findDroidBinary();
225
+ if (!droidBin) {
226
+ throw new Error('droid CLI not found. Install it: curl -fsSL https://app.factory.ai/cli | sh');
227
+ }
228
+ const { stdout, stderr, code } = await runDroid(droidBin, ['computer', 'list']);
229
+ if (code !== 0) {
230
+ // Surface droid's own message verbatim — e.g. "No authenticated user with
231
+ // organization available" when the user hasn't logged in.
232
+ throw new Error((stderr.trim() || stdout.trim() || `droid computer list exited ${code}`));
233
+ }
234
+ return parseComputerList(stdout);
235
+ }
236
+ async dispatch(options) {
237
+ const droidBin = findDroidBinary();
238
+ if (!droidBin) {
239
+ throw new Error('droid CLI not found. Install it: curl -fsSL https://app.factory.ai/cli | sh');
240
+ }
241
+ const computer = options.providerOptions?.computer ?? this.defaultComputer;
242
+ if (!computer) {
243
+ throw new MissingTargetError('computer', 'Factory cloud requires a Droid Computer.', 'Pass --computer <name>, or set cloud.providers.factory.computer in ~/.agents/agents.yaml. ' +
244
+ 'Create one in Factory (Settings → Droid Computers), or register a machine with `droid computer register`. ' +
245
+ 'List yours with `agents cloud envs --provider factory`.');
246
+ }
247
+ const autonomy = resolveAutonomy(options.providerOptions?.autonomy ?? options.providerOptions?.mode, this.defaultAutonomy);
248
+ const user = options.providerOptions?.user ?? 'droid';
249
+ const execArgs = buildExecArgs(options.prompt, { autonomy });
250
+ const sshArgs = buildSshArgs(computer, 'droid', execArgs, { droidBin, user });
251
+ const { events, status, summary, sessionId } = await this.runRemote(sshArgs);
252
+ const now = new Date().toISOString();
253
+ const id = sessionId ?? `droid-${Date.now()}`;
254
+ const task = {
255
+ id,
256
+ provider: 'factory',
257
+ status,
258
+ agent: 'droid',
259
+ prompt: options.prompt,
260
+ summary,
261
+ createdAt: now,
262
+ updatedAt: now,
263
+ };
264
+ this.runs.set(id, { events, task });
265
+ return task;
266
+ }
267
+ /** Run the remote droid exec to completion, collecting events + final result. */
268
+ runRemote(sshArgs) {
269
+ return new Promise((resolve, reject) => {
270
+ const proc = spawn('ssh', sshArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
271
+ const events = [];
272
+ let status = 'failed';
273
+ let summary;
274
+ let sessionId;
275
+ let stdoutBuf = '';
276
+ let stderr = '';
277
+ const handleLine = (line) => {
278
+ const trimmed = line.trim();
279
+ if (!trimmed)
280
+ return;
281
+ let obj;
282
+ try {
283
+ obj = JSON.parse(trimmed);
284
+ }
285
+ catch {
286
+ // Non-JSON line (banner, ssh notice) — surface it, don't drop it.
287
+ events.push({ type: 'unknown', name: 'stdout', data: trimmed, timestamp: new Date().toISOString() });
288
+ return;
289
+ }
290
+ if (typeof obj.session_id === 'string')
291
+ sessionId = obj.session_id;
292
+ const event = mapDroidEvent(obj);
293
+ if (event.type === 'done') {
294
+ status = event.status ?? 'completed';
295
+ summary = event.summary ?? summary;
296
+ }
297
+ events.push(event);
298
+ };
299
+ proc.stdout.on('data', (d) => {
300
+ stdoutBuf += d.toString();
301
+ const lines = stdoutBuf.split('\n');
302
+ stdoutBuf = lines.pop() ?? '';
303
+ for (const line of lines)
304
+ handleLine(line);
305
+ });
306
+ proc.stderr.on('data', (d) => { stderr += d.toString(); });
307
+ proc.on('error', (err) => reject(err));
308
+ proc.on('close', (code) => {
309
+ if (stdoutBuf)
310
+ handleLine(stdoutBuf);
311
+ if (code !== 0 && events.every((e) => e.type !== 'done')) {
312
+ // Surface the auth error verbatim — it's the common first-run failure.
313
+ const detail = stderr.trim() || `ssh exited ${code}`;
314
+ reject(new Error(`Factory dispatch failed: ${detail}`));
315
+ return;
316
+ }
317
+ resolve({ events, status, summary, sessionId });
318
+ });
319
+ });
37
320
  }
38
- async status(_taskId) {
39
- throw new Error('Factory cloud provider is not yet available.');
321
+ async status(taskId) {
322
+ const run = this.runs.get(taskId);
323
+ if (run)
324
+ return run.task;
325
+ // No remote task registry — the command layer falls back to the local store.
326
+ throw new Error(`No live status for Factory task ${taskId} (synchronous run; see local cache).`);
40
327
  }
41
- async list(_filter) {
42
- return [];
328
+ async list() {
329
+ // Factory has no remote task list; `agents cloud list` reads the local store.
330
+ return [...this.runs.values()].map((r) => r.task);
43
331
  }
44
- async *stream(_taskId) {
45
- throw new Error('Factory cloud provider is not yet available.');
332
+ async *stream(taskId) {
333
+ const run = this.runs.get(taskId);
334
+ if (!run) {
335
+ yield {
336
+ type: 'error',
337
+ message: `Factory run ${taskId} is not buffered in this process. droid exec is synchronous — its output is not retained after the run completes. See 'agents cloud status ${taskId}' for the summary.`,
338
+ timestamp: new Date().toISOString(),
339
+ };
340
+ return;
341
+ }
342
+ for (const event of run.events)
343
+ yield event;
46
344
  }
47
345
  async cancel(_taskId) {
48
- throw new Error('Factory cloud provider is not yet available.');
346
+ throw new Error('Cancel is not supported for Factory (Droid) droid exec runs synchronously to completion.');
49
347
  }
50
348
  async message(_taskId, _content) {
51
- throw new Error('Factory cloud provider is not yet available.');
349
+ throw new Error('Follow-up messages are not yet supported for Factory (Droid) cloud tasks.');
52
350
  }
53
351
  }
@@ -5,11 +5,27 @@
5
5
  * implementations, and exposes lookup helpers used by the `agents cloud` commands.
6
6
  */
7
7
  import type { CloudProvider, CloudProviderId } from './types.js';
8
+ /**
9
+ * The cloud provider an agent dispatches to by default. Reads the canonical
10
+ * `cloudProvider` field on the agent registry entry — one source of truth, no
11
+ * side map. Returns undefined for agents with no native cloud.
12
+ */
13
+ export declare function nativeProviderForAgent(agentId: string): CloudProviderId | undefined;
8
14
  /** Look up a provider by ID, throwing if the ID is unknown. */
9
15
  export declare function getProvider(id: CloudProviderId): CloudProvider;
10
16
  /** Return the user's configured default provider, falling back to 'rush'. */
11
17
  export declare function getDefaultProviderId(): CloudProviderId;
12
18
  /** Return every registered provider (used by `agents cloud providers`). */
13
19
  export declare function getAllProviders(): CloudProvider[];
14
- /** Resolve the active provider from an explicit flag or the configured default. */
15
- export declare function resolveProvider(explicit?: string): CloudProvider;
20
+ /**
21
+ * Resolve the active provider for a dispatch.
22
+ *
23
+ * Precedence: explicit `--provider` > the agent's native cloud
24
+ * (`cloudProvider`) > configured `cloud.default_provider` > `rush`. This is
25
+ * what makes `agents cloud run --agent droid` land on Factory and
26
+ * `--agent codex` land on Codex Cloud without the user naming a provider.
27
+ *
28
+ * Callers that already hold a concrete provider id (e.g. resolving a stored
29
+ * task's provider) pass it as `explicit` and the agent arg is ignored.
30
+ */
31
+ export declare function resolveProvider(explicit?: string, agentId?: string): CloudProvider;
@@ -10,7 +10,9 @@ import * as yaml from 'yaml';
10
10
  import { RushCloudProvider } from './rush.js';
11
11
  import { CodexCloudProvider } from './codex.js';
12
12
  import { FactoryCloudProvider } from './factory.js';
13
+ import { AntigravityCloudProvider } from './antigravity.js';
13
14
  import { getUserAgentsDir } from '../state.js';
15
+ import { AGENTS } from '../agents.js';
14
16
  const META_FILE = path.join(getUserAgentsDir(), 'agents.yaml');
15
17
  let _config = null;
16
18
  /** Parse the `cloud` section from agents.yaml, caching the result for the process lifetime. */
@@ -39,7 +41,17 @@ function initProviders() {
39
41
  const config = loadCloudConfig();
40
42
  providers.set('rush', new RushCloudProvider());
41
43
  providers.set('codex', new CodexCloudProvider(config.providers?.codex));
42
- providers.set('factory', new FactoryCloudProvider());
44
+ providers.set('factory', new FactoryCloudProvider(config.providers?.factory));
45
+ providers.set('antigravity', new AntigravityCloudProvider(config.providers?.antigravity));
46
+ }
47
+ /**
48
+ * The cloud provider an agent dispatches to by default. Reads the canonical
49
+ * `cloudProvider` field on the agent registry entry — one source of truth, no
50
+ * side map. Returns undefined for agents with no native cloud.
51
+ */
52
+ export function nativeProviderForAgent(agentId) {
53
+ const agent = AGENTS[agentId];
54
+ return agent?.cloudProvider;
43
55
  }
44
56
  /** Look up a provider by ID, throwing if the ID is unknown. */
45
57
  export function getProvider(id) {
@@ -60,8 +72,20 @@ export function getAllProviders() {
60
72
  initProviders();
61
73
  return [...providers.values()];
62
74
  }
63
- /** Resolve the active provider from an explicit flag or the configured default. */
64
- export function resolveProvider(explicit) {
65
- const id = (explicit ?? getDefaultProviderId());
75
+ /**
76
+ * Resolve the active provider for a dispatch.
77
+ *
78
+ * Precedence: explicit `--provider` > the agent's native cloud
79
+ * (`cloudProvider`) > configured `cloud.default_provider` > `rush`. This is
80
+ * what makes `agents cloud run --agent droid` land on Factory and
81
+ * `--agent codex` land on Codex Cloud without the user naming a provider.
82
+ *
83
+ * Callers that already hold a concrete provider id (e.g. resolving a stored
84
+ * task's provider) pass it as `explicit` and the agent arg is ignored.
85
+ */
86
+ export function resolveProvider(explicit, agentId) {
87
+ const id = (explicit
88
+ ?? (agentId ? nativeProviderForAgent(agentId) : undefined)
89
+ ?? getDefaultProviderId());
66
90
  return getProvider(id);
67
91
  }
@@ -5,8 +5,19 @@
5
5
  * Factory) implement, plus the shared task and event types that flow through
6
6
  * the dispatch pipeline.
7
7
  */
8
- /** Identifier for a supported cloud dispatch backend. */
9
- export type CloudProviderId = 'rush' | 'codex' | 'factory';
8
+ /**
9
+ * Identifier for a supported cloud dispatch backend.
10
+ *
11
+ * Each id is one agent's *own* cloud:
12
+ * - `rush` — Rush Cloud (runs Claude against a GitHub repo → PR)
13
+ * - `codex` — OpenAI Codex Cloud (`codex cloud exec`)
14
+ * - `factory` — Factory Droid Computers (`droid computer ssh` + remote `droid exec`)
15
+ * - `antigravity` — Google Gemini Managed Agents (Interactions API)
16
+ *
17
+ * Agents route to their native cloud automatically (see `cloudProvider` in the
18
+ * agent registry); `--provider` overrides.
19
+ */
20
+ export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity';
10
21
  /**
11
22
  * Lifecycle state of a cloud-dispatched task.
12
23
  *
@@ -164,6 +175,33 @@ export interface ProviderCapabilities {
164
175
  skills: boolean;
165
176
  images: boolean;
166
177
  }
178
+ /**
179
+ * A pre-provisioned dispatch target a provider runs *inside* — a Codex
180
+ * environment (`env_…`) or a Factory Droid Computer (a name). Surfaced by
181
+ * `agents cloud envs` and the missing-target picker so users don't have to
182
+ * copy opaque IDs out of a web UI.
183
+ */
184
+ export interface CloudTarget {
185
+ /** The value passed to dispatch (env id / computer name). */
186
+ id: string;
187
+ /** Human label — repo, description, or status. */
188
+ label?: string;
189
+ kind: TargetKind;
190
+ }
191
+ /** Which dispatch option a provider's pre-provisioned target maps to. */
192
+ export type TargetKind = 'env' | 'computer';
193
+ /**
194
+ * Thrown by a provider's `dispatch()` when it needs a pre-provisioned target
195
+ * (Codex env / Factory computer) and none was supplied. The CLI catches this
196
+ * to offer a picker (`listTargets`) or actionable guidance, instead of a raw
197
+ * error. `kind` names the missing flag; `guidance` is shown when the target
198
+ * can't be enumerated.
199
+ */
200
+ export declare class MissingTargetError extends Error {
201
+ kind: TargetKind;
202
+ guidance?: string | undefined;
203
+ constructor(kind: TargetKind, message: string, guidance?: string | undefined);
204
+ }
167
205
  /**
168
206
  * Contract that every cloud backend must implement.
169
207
  *
@@ -187,15 +225,48 @@ export interface CloudProvider {
187
225
  cancel(taskId: string): Promise<void>;
188
226
  /** Send a follow-up message to a finished/idle/needs_review task. */
189
227
  message(taskId: string, content: string): Promise<void>;
228
+ /**
229
+ * The pre-provisioned target this provider runs inside, if any. Set for
230
+ * Codex (`env`) and Factory (`computer`); undefined for Rush (per-repo) and
231
+ * Antigravity (on-demand sandbox).
232
+ */
233
+ targetKind?: TargetKind;
234
+ /**
235
+ * Enumerate selectable targets for `agents cloud envs` and the picker.
236
+ * Present only when the backend can list non-interactively (Factory via
237
+ * `droid computer list`). Codex has no such CLI — it omits this, and callers
238
+ * fall back to `MissingTargetError.guidance`. May throw (e.g. not signed in);
239
+ * callers surface that verbatim.
240
+ */
241
+ listTargets?(): Promise<CloudTarget[]>;
190
242
  }
243
+ /** Autonomy level passed to `droid exec --auto` for Factory cloud dispatches. */
244
+ export type DroidAutonomy = 'low' | 'medium' | 'high';
191
245
  /** Per-provider configuration stored in the `cloud.providers` section of agents.yaml. */
192
246
  export interface CloudProviderConfig {
193
247
  rush?: Record<string, string>;
194
248
  codex?: {
195
249
  env?: string;
196
250
  };
251
+ /**
252
+ * Factory (Droid) cloud. `computer` is the pre-provisioned Droid Computer
253
+ * name (managed in Factory's UI, or BYOM via `droid computer register`) —
254
+ * the Factory analogue of Codex's pre-built `env`. `autonomy` is the default
255
+ * `droid exec --auto` level for cloud runs (defaults to `high`).
256
+ */
197
257
  factory?: {
198
258
  computer?: string;
259
+ autonomy?: DroidAutonomy;
260
+ };
261
+ /**
262
+ * Antigravity (Gemini Managed Agents) cloud. The Gemini API key is read from
263
+ * an `agents secrets` bundle named here (never stored in agents.yaml); if
264
+ * unset, the provider falls back to GEMINI_API_KEY / GOOGLE_API_KEY in the
265
+ * environment. `model` overrides the default managed-agent id.
266
+ */
267
+ antigravity?: {
268
+ secretsBundle?: string;
269
+ model?: string;
199
270
  };
200
271
  }
201
272
  /** Top-level `cloud` section of agents.yaml. */
@@ -32,3 +32,20 @@ export function resolveDispatchRepos(options) {
32
32
  }
33
33
  return out;
34
34
  }
35
+ /**
36
+ * Thrown by a provider's `dispatch()` when it needs a pre-provisioned target
37
+ * (Codex env / Factory computer) and none was supplied. The CLI catches this
38
+ * to offer a picker (`listTargets`) or actionable guidance, instead of a raw
39
+ * error. `kind` names the missing flag; `guidance` is shown when the target
40
+ * can't be enumerated.
41
+ */
42
+ export class MissingTargetError extends Error {
43
+ kind;
44
+ guidance;
45
+ constructor(kind, message, guidance) {
46
+ super(message);
47
+ this.kind = kind;
48
+ this.guidance = guidance;
49
+ this.name = 'MissingTargetError';
50
+ }
51
+ }
@@ -99,6 +99,8 @@ export interface ExecOptions {
99
99
  * flag alone merely ADDS to the existing server set).
100
100
  */
101
101
  mcpConfigPath?: string;
102
+ /** Raw args captured after `--` on the command line, forwarded verbatim to the underlying agent CLI. */
103
+ passthroughArgs?: string[];
102
104
  }
103
105
  /**
104
106
  * Resolve interactive vs headless. Explicit flags are definitive and win over
package/dist/lib/exec.js CHANGED
@@ -596,6 +596,11 @@ export function buildExecCommand(options) {
596
596
  cmd.push('--strict-mcp-config');
597
597
  }
598
598
  }
599
+ // Forward arbitrary native flags supplied after `--` verbatim. Appended last
600
+ // so they cannot be misinterpreted as values for earlier flags or as the prompt.
601
+ if (options.passthroughArgs && options.passthroughArgs.length > 0) {
602
+ cmd.push(...options.passthroughArgs);
603
+ }
599
604
  return cmd;
600
605
  }
601
606
  /** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
@@ -0,0 +1,20 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleExecutable</key>
6
+ <string>MenubarHelper</string>
7
+ <key>CFBundleIdentifier</key>
8
+ <string>com.phnx-labs.agents-menubar</string>
9
+ <key>CFBundleName</key>
10
+ <string>Agents Menu Bar</string>
11
+ <key>CFBundlePackageType</key>
12
+ <string>APPL</string>
13
+ <key>CFBundleShortVersionString</key>
14
+ <string>0.1.0</string>
15
+ <key>CFBundleVersion</key>
16
+ <string>1</string>
17
+ <key>LSUIElement</key>
18
+ <true/>
19
+ </dict>
20
+ </plist>