@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.
- package/CHANGELOG.md +14 -0
- package/dist/commands/cloud.js +142 -13
- package/dist/commands/exec.js +13 -1
- package/dist/commands/menubar.d.ts +10 -0
- package/dist/commands/menubar.js +83 -0
- package/dist/commands/routines.js +34 -1
- package/dist/commands/secrets.d.ts +1 -1
- package/dist/commands/secrets.js +95 -38
- package/dist/index.js +292 -225
- package/dist/lib/agents.js +8 -0
- package/dist/lib/cloud/antigravity.d.ts +70 -0
- package/dist/lib/cloud/antigravity.js +196 -0
- package/dist/lib/cloud/codex.d.ts +1 -0
- package/dist/lib/cloud/codex.js +8 -2
- package/dist/lib/cloud/factory.d.ts +79 -18
- package/dist/lib/cloud/factory.js +324 -26
- package/dist/lib/cloud/registry.d.ts +18 -2
- package/dist/lib/cloud/registry.js +28 -4
- package/dist/lib/cloud/types.d.ts +73 -2
- package/dist/lib/cloud/types.js +17 -0
- package/dist/lib/exec.d.ts +2 -0
- package/dist/lib/exec.js +5 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +20 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +115 -0
- package/dist/lib/menubar/install-menubar.d.ts +57 -0
- package/dist/lib/menubar/install-menubar.js +291 -0
- package/dist/lib/secrets/agent.d.ts +9 -1
- package/dist/lib/secrets/agent.js +91 -10
- package/dist/lib/secrets/bundles.d.ts +19 -12
- package/dist/lib/secrets/bundles.js +22 -14
- package/dist/lib/self-update.d.ts +34 -0
- package/dist/lib/self-update.js +63 -2
- package/dist/lib/startup/command-registry.d.ts +99 -0
- package/dist/lib/startup/command-registry.js +136 -0
- package/dist/lib/types.d.ts +8 -0
- package/dist/lib/version.d.ts +11 -0
- package/dist/lib/version.js +20 -0
- package/package.json +5 -3
- package/scripts/postinstall.js +35 -0
|
@@ -1,30 +1,217 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Factory
|
|
2
|
+
* Factory (Droid) cloud provider.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
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
|
-
|
|
24
|
-
dispatch
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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(
|
|
39
|
-
|
|
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(
|
|
42
|
-
|
|
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(
|
|
45
|
-
|
|
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
|
|
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('
|
|
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
|
-
/**
|
|
15
|
-
|
|
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
|
-
/**
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
/**
|
|
9
|
-
|
|
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. */
|
package/dist/lib/cloud/types.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -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>
|
|
Binary file
|