@phnx-labs/agents-cli 1.20.20 → 1.20.22
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 +16 -7
- 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 +28 -3
- 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/factory.d.ts +68 -18
- package/dist/lib/cloud/factory.js +269 -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 +32 -2
- package/dist/lib/daemon.js +3 -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/session/sync/config.d.ts +17 -5
- package/dist/lib/session/sync/config.js +51 -4
- 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 +3 -2
- package/scripts/postinstall.js +35 -0
|
@@ -1,30 +1,176 @@
|
|
|
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.
|
|
6
21
|
*/
|
|
22
|
+
import { spawn, execFileSync } from 'child_process';
|
|
23
|
+
import * as fs from 'fs';
|
|
24
|
+
import * as path from 'path';
|
|
25
|
+
import { getShimsDir } from '../state.js';
|
|
26
|
+
const SHIMS_DIR = getShimsDir();
|
|
27
|
+
const DEFAULT_AUTONOMY = 'high';
|
|
28
|
+
const VALID_AUTONOMY = new Set(['low', 'medium', 'high']);
|
|
29
|
+
/** Locate the droid binary, checking agents-cli shims first then PATH. */
|
|
30
|
+
export function findDroidBinary() {
|
|
31
|
+
const shim = path.join(SHIMS_DIR, 'droid');
|
|
32
|
+
if (fs.existsSync(shim))
|
|
33
|
+
return shim;
|
|
34
|
+
try {
|
|
35
|
+
return execFileSync('which', ['droid'], { stdio: 'pipe' }).toString().trim() || null;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Normalize an autonomy value, falling back to the safe cloud default (`high`). */
|
|
42
|
+
export function resolveAutonomy(value, fallback = DEFAULT_AUTONOMY) {
|
|
43
|
+
return typeof value === 'string' && VALID_AUTONOMY.has(value)
|
|
44
|
+
? value
|
|
45
|
+
: fallback;
|
|
46
|
+
}
|
|
7
47
|
/**
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
48
|
+
* Build the remote `droid exec` argv. Headless, stream-json output, given
|
|
49
|
+
* autonomy. `sessionId` (when resuming) maps to `-s`.
|
|
50
|
+
*/
|
|
51
|
+
export function buildExecArgs(prompt, opts) {
|
|
52
|
+
const args = ['exec', '--auto', opts.autonomy, '--output-format', 'stream-json'];
|
|
53
|
+
if (opts.sessionId)
|
|
54
|
+
args.push('-s', opts.sessionId);
|
|
55
|
+
args.push(prompt);
|
|
56
|
+
return args;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Build the `ssh` argv that runs a remote command on a Droid Computer through
|
|
60
|
+
* the Droid relay. The relay is used as an OpenSSH ProxyCommand
|
|
61
|
+
* (`droid computer ssh <name> --proxy`), so the connection rides Factory's
|
|
62
|
+
* brokered tunnel rather than a directly reachable host.
|
|
12
63
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
64
|
+
* `remoteArgv` is the already-built remote command (e.g. droid exec argv); it is
|
|
65
|
+
* shell-quoted into a single remote command string.
|
|
66
|
+
*/
|
|
67
|
+
export function buildSshArgs(computer, remoteBin, remoteArgv, opts) {
|
|
68
|
+
const remoteCmd = [remoteBin, ...remoteArgv].map(shellQuote).join(' ');
|
|
69
|
+
const proxy = `ProxyCommand=${opts.droidBin} computer ssh ${computer} --proxy --port %p`;
|
|
70
|
+
return [
|
|
71
|
+
'-o', proxy,
|
|
72
|
+
'-o', 'StrictHostKeyChecking=accept-new',
|
|
73
|
+
'-p', opts.port ?? '22',
|
|
74
|
+
`${opts.user}@${computer}`,
|
|
75
|
+
remoteCmd,
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
/** POSIX single-quote a shell argument. */
|
|
79
|
+
function shellQuote(arg) {
|
|
80
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
81
|
+
}
|
|
82
|
+
/** Map a droid stream-json `result.subtype` / `is_error` to a CloudTaskStatus. */
|
|
83
|
+
export function mapResultStatus(line) {
|
|
84
|
+
if (line.is_error)
|
|
85
|
+
return 'failed';
|
|
86
|
+
if (line.subtype && /cancel/i.test(line.subtype))
|
|
87
|
+
return 'cancelled';
|
|
88
|
+
return 'completed';
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Map one parsed droid stream-json event to a CloudEvent. The stream-json
|
|
92
|
+
* schema is only partially documented, so this is defensive: known shapes map
|
|
93
|
+
* to typed events, everything else surfaces as `unknown` rather than being
|
|
94
|
+
* dropped (mirrors the rest of the cloud event pipeline).
|
|
17
95
|
*/
|
|
96
|
+
export function mapDroidEvent(obj) {
|
|
97
|
+
const ts = new Date().toISOString();
|
|
98
|
+
const type = String(obj.type ?? '');
|
|
99
|
+
switch (type) {
|
|
100
|
+
case 'result': {
|
|
101
|
+
return {
|
|
102
|
+
type: 'done',
|
|
103
|
+
status: mapResultStatus(obj),
|
|
104
|
+
summary: typeof obj.result === 'string' ? obj.result : undefined,
|
|
105
|
+
timestamp: ts,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
case 'assistant':
|
|
109
|
+
case 'message':
|
|
110
|
+
case 'text': {
|
|
111
|
+
const content = extractText(obj);
|
|
112
|
+
if (content)
|
|
113
|
+
return { type: 'text', content, timestamp: ts };
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
case 'thinking':
|
|
117
|
+
case 'reasoning': {
|
|
118
|
+
const content = extractText(obj);
|
|
119
|
+
if (content)
|
|
120
|
+
return { type: 'thinking', content, timestamp: ts };
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
case 'tool_call':
|
|
124
|
+
case 'tool_use': {
|
|
125
|
+
return { type: 'tool_use', tool: String(obj.name ?? obj.tool ?? 'tool'), input: obj.input ?? obj.arguments ?? {}, timestamp: ts };
|
|
126
|
+
}
|
|
127
|
+
case 'tool_result': {
|
|
128
|
+
return { type: 'tool_result', tool: String(obj.name ?? obj.tool ?? 'tool'), output: obj.output ?? obj.result ?? '', timestamp: ts };
|
|
129
|
+
}
|
|
130
|
+
case 'error': {
|
|
131
|
+
return { type: 'error', message: String(obj.message ?? obj.error ?? 'unknown error'), timestamp: ts };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { type: 'unknown', name: type || 'unknown', data: JSON.stringify(obj), timestamp: ts };
|
|
135
|
+
}
|
|
136
|
+
/** Pull a text string out of the varied droid message shapes. */
|
|
137
|
+
function extractText(obj) {
|
|
138
|
+
if (typeof obj.text === 'string')
|
|
139
|
+
return obj.text;
|
|
140
|
+
if (typeof obj.content === 'string')
|
|
141
|
+
return obj.content;
|
|
142
|
+
if (Array.isArray(obj.content)) {
|
|
143
|
+
return obj.content
|
|
144
|
+
.map((c) => (c && typeof c === 'object' && typeof c.text === 'string' ? c.text : ''))
|
|
145
|
+
.join('');
|
|
146
|
+
}
|
|
147
|
+
const msg = obj.message;
|
|
148
|
+
if (msg && typeof msg === 'object')
|
|
149
|
+
return extractText(msg);
|
|
150
|
+
return '';
|
|
151
|
+
}
|
|
18
152
|
export class FactoryCloudProvider {
|
|
19
153
|
id = 'factory';
|
|
20
154
|
name = 'Factory (Droid)';
|
|
155
|
+
defaultComputer;
|
|
156
|
+
defaultAutonomy;
|
|
157
|
+
/** session_id → buffered run, populated by dispatch, drained by stream. */
|
|
158
|
+
runs = new Map();
|
|
159
|
+
constructor(config) {
|
|
160
|
+
this.defaultComputer = config?.computer;
|
|
161
|
+
this.defaultAutonomy = resolveAutonomy(config?.autonomy);
|
|
162
|
+
}
|
|
21
163
|
capabilities() {
|
|
164
|
+
const droid = findDroidBinary() !== null;
|
|
165
|
+
const computer = Boolean(this.defaultComputer);
|
|
22
166
|
return {
|
|
23
|
-
|
|
24
|
-
dispatch
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
167
|
+
// Reachable only when the droid binary exists AND a computer is set.
|
|
168
|
+
// (A per-dispatch --computer can still override the missing default.)
|
|
169
|
+
available: droid && computer,
|
|
170
|
+
dispatch: droid,
|
|
171
|
+
status: droid,
|
|
172
|
+
list: droid,
|
|
173
|
+
stream: droid,
|
|
28
174
|
cancel: false,
|
|
29
175
|
message: false,
|
|
30
176
|
multiRepo: false,
|
|
@@ -32,22 +178,119 @@ export class FactoryCloudProvider {
|
|
|
32
178
|
images: false,
|
|
33
179
|
};
|
|
34
180
|
}
|
|
35
|
-
async dispatch(
|
|
36
|
-
|
|
181
|
+
async dispatch(options) {
|
|
182
|
+
const droidBin = findDroidBinary();
|
|
183
|
+
if (!droidBin) {
|
|
184
|
+
throw new Error('droid CLI not found. Install it: curl -fsSL https://app.factory.ai/cli | sh');
|
|
185
|
+
}
|
|
186
|
+
const computer = options.providerOptions?.computer ?? this.defaultComputer;
|
|
187
|
+
if (!computer) {
|
|
188
|
+
throw new Error('Factory cloud requires a Droid Computer. Pass --computer <name>, or set ' +
|
|
189
|
+
'cloud.providers.factory.computer in ~/.agents/agents.yaml. Create one in ' +
|
|
190
|
+
'Factory (Settings → Droid Computers), or register a machine with `droid computer register`.');
|
|
191
|
+
}
|
|
192
|
+
const autonomy = resolveAutonomy(options.providerOptions?.autonomy ?? options.providerOptions?.mode, this.defaultAutonomy);
|
|
193
|
+
const user = options.providerOptions?.user ?? 'droid';
|
|
194
|
+
const execArgs = buildExecArgs(options.prompt, { autonomy });
|
|
195
|
+
const sshArgs = buildSshArgs(computer, 'droid', execArgs, { droidBin, user });
|
|
196
|
+
const { events, status, summary, sessionId } = await this.runRemote(sshArgs);
|
|
197
|
+
const now = new Date().toISOString();
|
|
198
|
+
const id = sessionId ?? `droid-${Date.now()}`;
|
|
199
|
+
const task = {
|
|
200
|
+
id,
|
|
201
|
+
provider: 'factory',
|
|
202
|
+
status,
|
|
203
|
+
agent: 'droid',
|
|
204
|
+
prompt: options.prompt,
|
|
205
|
+
summary,
|
|
206
|
+
createdAt: now,
|
|
207
|
+
updatedAt: now,
|
|
208
|
+
};
|
|
209
|
+
this.runs.set(id, { events, task });
|
|
210
|
+
return task;
|
|
211
|
+
}
|
|
212
|
+
/** Run the remote droid exec to completion, collecting events + final result. */
|
|
213
|
+
runRemote(sshArgs) {
|
|
214
|
+
return new Promise((resolve, reject) => {
|
|
215
|
+
const proc = spawn('ssh', sshArgs, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
216
|
+
const events = [];
|
|
217
|
+
let status = 'failed';
|
|
218
|
+
let summary;
|
|
219
|
+
let sessionId;
|
|
220
|
+
let stdoutBuf = '';
|
|
221
|
+
let stderr = '';
|
|
222
|
+
const handleLine = (line) => {
|
|
223
|
+
const trimmed = line.trim();
|
|
224
|
+
if (!trimmed)
|
|
225
|
+
return;
|
|
226
|
+
let obj;
|
|
227
|
+
try {
|
|
228
|
+
obj = JSON.parse(trimmed);
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// Non-JSON line (banner, ssh notice) — surface it, don't drop it.
|
|
232
|
+
events.push({ type: 'unknown', name: 'stdout', data: trimmed, timestamp: new Date().toISOString() });
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (typeof obj.session_id === 'string')
|
|
236
|
+
sessionId = obj.session_id;
|
|
237
|
+
const event = mapDroidEvent(obj);
|
|
238
|
+
if (event.type === 'done') {
|
|
239
|
+
status = event.status ?? 'completed';
|
|
240
|
+
summary = event.summary ?? summary;
|
|
241
|
+
}
|
|
242
|
+
events.push(event);
|
|
243
|
+
};
|
|
244
|
+
proc.stdout.on('data', (d) => {
|
|
245
|
+
stdoutBuf += d.toString();
|
|
246
|
+
const lines = stdoutBuf.split('\n');
|
|
247
|
+
stdoutBuf = lines.pop() ?? '';
|
|
248
|
+
for (const line of lines)
|
|
249
|
+
handleLine(line);
|
|
250
|
+
});
|
|
251
|
+
proc.stderr.on('data', (d) => { stderr += d.toString(); });
|
|
252
|
+
proc.on('error', (err) => reject(err));
|
|
253
|
+
proc.on('close', (code) => {
|
|
254
|
+
if (stdoutBuf)
|
|
255
|
+
handleLine(stdoutBuf);
|
|
256
|
+
if (code !== 0 && events.every((e) => e.type !== 'done')) {
|
|
257
|
+
// Surface the auth error verbatim — it's the common first-run failure.
|
|
258
|
+
const detail = stderr.trim() || `ssh exited ${code}`;
|
|
259
|
+
reject(new Error(`Factory dispatch failed: ${detail}`));
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
resolve({ events, status, summary, sessionId });
|
|
263
|
+
});
|
|
264
|
+
});
|
|
37
265
|
}
|
|
38
|
-
async status(
|
|
39
|
-
|
|
266
|
+
async status(taskId) {
|
|
267
|
+
const run = this.runs.get(taskId);
|
|
268
|
+
if (run)
|
|
269
|
+
return run.task;
|
|
270
|
+
// No remote task registry — the command layer falls back to the local store.
|
|
271
|
+
throw new Error(`No live status for Factory task ${taskId} (synchronous run; see local cache).`);
|
|
40
272
|
}
|
|
41
|
-
async list(
|
|
42
|
-
|
|
273
|
+
async list() {
|
|
274
|
+
// Factory has no remote task list; `agents cloud list` reads the local store.
|
|
275
|
+
return [...this.runs.values()].map((r) => r.task);
|
|
43
276
|
}
|
|
44
|
-
async *stream(
|
|
45
|
-
|
|
277
|
+
async *stream(taskId) {
|
|
278
|
+
const run = this.runs.get(taskId);
|
|
279
|
+
if (!run) {
|
|
280
|
+
yield {
|
|
281
|
+
type: 'error',
|
|
282
|
+
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.`,
|
|
283
|
+
timestamp: new Date().toISOString(),
|
|
284
|
+
};
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
for (const event of run.events)
|
|
288
|
+
yield event;
|
|
46
289
|
}
|
|
47
290
|
async cancel(_taskId) {
|
|
48
|
-
throw new Error('Factory
|
|
291
|
+
throw new Error('Cancel is not supported for Factory (Droid) — droid exec runs synchronously to completion.');
|
|
49
292
|
}
|
|
50
293
|
async message(_taskId, _content) {
|
|
51
|
-
throw new Error('
|
|
294
|
+
throw new Error('Follow-up messages are not yet supported for Factory (Droid) cloud tasks.');
|
|
52
295
|
}
|
|
53
296
|
}
|
|
@@ -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
|
*
|
|
@@ -188,14 +199,33 @@ export interface CloudProvider {
|
|
|
188
199
|
/** Send a follow-up message to a finished/idle/needs_review task. */
|
|
189
200
|
message(taskId: string, content: string): Promise<void>;
|
|
190
201
|
}
|
|
202
|
+
/** Autonomy level passed to `droid exec --auto` for Factory cloud dispatches. */
|
|
203
|
+
export type DroidAutonomy = 'low' | 'medium' | 'high';
|
|
191
204
|
/** Per-provider configuration stored in the `cloud.providers` section of agents.yaml. */
|
|
192
205
|
export interface CloudProviderConfig {
|
|
193
206
|
rush?: Record<string, string>;
|
|
194
207
|
codex?: {
|
|
195
208
|
env?: string;
|
|
196
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* Factory (Droid) cloud. `computer` is the pre-provisioned Droid Computer
|
|
212
|
+
* name (managed in Factory's UI, or BYOM via `droid computer register`) —
|
|
213
|
+
* the Factory analogue of Codex's pre-built `env`. `autonomy` is the default
|
|
214
|
+
* `droid exec --auto` level for cloud runs (defaults to `high`).
|
|
215
|
+
*/
|
|
197
216
|
factory?: {
|
|
198
217
|
computer?: string;
|
|
218
|
+
autonomy?: DroidAutonomy;
|
|
219
|
+
};
|
|
220
|
+
/**
|
|
221
|
+
* Antigravity (Gemini Managed Agents) cloud. The Gemini API key is read from
|
|
222
|
+
* an `agents secrets` bundle named here (never stored in agents.yaml); if
|
|
223
|
+
* unset, the provider falls back to GEMINI_API_KEY / GOOGLE_API_KEY in the
|
|
224
|
+
* environment. `model` overrides the default managed-agent id.
|
|
225
|
+
*/
|
|
226
|
+
antigravity?: {
|
|
227
|
+
secretsBundle?: string;
|
|
228
|
+
model?: string;
|
|
199
229
|
};
|
|
200
230
|
}
|
|
201
231
|
/** Top-level `cloud` section of agents.yaml. */
|
package/dist/lib/daemon.js
CHANGED
|
@@ -265,6 +265,9 @@ export async function runDaemon() {
|
|
|
265
265
|
scheduler.reloadAll();
|
|
266
266
|
const reloaded = scheduler.listScheduled();
|
|
267
267
|
log('INFO', `Reloaded ${reloaded.length} jobs`);
|
|
268
|
+
// Drop the memoized R2 config so rotated/added sync credentials are re-read
|
|
269
|
+
// on the next cycle instead of waiting for a restart.
|
|
270
|
+
void import('./session/sync/config.js').then(m => m.clearR2ConfigCache());
|
|
268
271
|
};
|
|
269
272
|
const handleShutdown = async () => {
|
|
270
273
|
log('INFO', 'Daemon shutting down');
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors `src/lib/secrets/install-helper.ts` (stable Application Support path,
|
|
5
|
+
* survives npm re-sign) and the secrets-agent launchd pattern in
|
|
6
|
+
* `src/lib/secrets/agent.ts` (RunAtLoad + KeepAlive user service).
|
|
7
|
+
*
|
|
8
|
+
* The helper is a no-Dock `.accessory` status-bar app. It reads live agent
|
|
9
|
+
* state directly from disk and shells `agents` only for actions, so the plist
|
|
10
|
+
* bakes in the node interpreter + entry point + bin path so the GUI process can
|
|
11
|
+
* find the CLI without a login PATH.
|
|
12
|
+
*
|
|
13
|
+
* Opt-out is sticky: `agents menubar disable` drops a sentinel that the upgrade
|
|
14
|
+
* migration (`installMenubarLaunchAgent` in migrate.ts) honors, so a disabled
|
|
15
|
+
* menu bar never silently comes back on the next release.
|
|
16
|
+
*/
|
|
17
|
+
/** True if the user explicitly disabled the menu bar (don't auto-enable on upgrade). */
|
|
18
|
+
export declare function menubarDisabledByUser(): boolean;
|
|
19
|
+
/** True if the launchd plist for the menu-bar service is installed. */
|
|
20
|
+
export declare function menubarServiceInstalled(): boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Copy the bundled `.app` to the stable user path (idempotent unless forced).
|
|
23
|
+
* Returns the installed executable path, or null if no source bundle ships
|
|
24
|
+
* with this install (e.g. Linux package, or a build without the helper).
|
|
25
|
+
*/
|
|
26
|
+
export declare function ensureMenubarAppInstalled(opts?: {
|
|
27
|
+
forceReinstall?: boolean;
|
|
28
|
+
}): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Install + start the menu-bar helper as a launchd user service (idempotent).
|
|
31
|
+
* Clears the sticky opt-out, installs the .app, writes the plist, and
|
|
32
|
+
* bootstraps it into the GUI domain. Returns false on non-darwin or when no
|
|
33
|
+
* helper bundle ships with this install.
|
|
34
|
+
*/
|
|
35
|
+
export declare function enableMenubarService(opts?: {
|
|
36
|
+
clearOptOut?: boolean;
|
|
37
|
+
}): boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Stop + remove the menu-bar service and write the sticky opt-out so the
|
|
40
|
+
* upgrade migration won't re-enable it.
|
|
41
|
+
*/
|
|
42
|
+
export declare function disableMenubarService(): void;
|
|
43
|
+
/**
|
|
44
|
+
* Upgrade-time auto-enable. Runs from runMigration() once per sentinel bump.
|
|
45
|
+
* No-ops if: not darwin, the user opted out, no helper bundle ships, or the
|
|
46
|
+
* service is already installed. Best-effort — never throws into migration.
|
|
47
|
+
*/
|
|
48
|
+
export declare function installMenubarLaunchAgentOnUpgrade(): void;
|
|
49
|
+
export interface MenubarStatus {
|
|
50
|
+
platform: string;
|
|
51
|
+
source: string | null;
|
|
52
|
+
installedApp: string | null;
|
|
53
|
+
serviceInstalled: boolean;
|
|
54
|
+
running: boolean;
|
|
55
|
+
disabledByUser: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare function getMenubarStatus(): MenubarStatus;
|