@phnx-labs/agents-cli 1.19.2 → 1.20.0
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 +67 -0
- package/README.md +69 -9
- package/dist/browser.js +0 -0
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +3 -3
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +56 -3
- package/dist/commands/routines.js +106 -13
- package/dist/commands/secrets.js +5 -7
- package/dist/commands/sessions.d.ts +28 -0
- package/dist/commands/sessions.js +98 -33
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +3 -3
- package/dist/commands/teams.js +13 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +131 -127
- package/dist/commands/view.js +12 -12
- package/dist/computer.js +0 -0
- package/dist/index.js +34 -6
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.js +110 -23
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +42 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +41 -1
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +21 -5
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/cli-resources.d.ts +109 -0
- package/dist/lib/cli-resources.js +255 -0
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/exec.d.ts +3 -2
- package/dist/lib/exec.js +44 -9
- package/dist/lib/hooks.js +182 -0
- package/dist/lib/mcp.js +6 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.js +5 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/profiles-presets.js +37 -0
- package/dist/lib/resources/mcp.js +37 -0
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +35 -0
- package/dist/lib/routines-format.js +173 -0
- package/dist/lib/routines.d.ts +7 -1
- package/dist/lib/routines.js +32 -12
- package/dist/lib/runner.js +19 -5
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +22 -1
- package/dist/lib/secrets/bundles.js +234 -36
- package/dist/lib/secrets/index.d.ts +6 -11
- package/dist/lib/secrets/index.js +107 -87
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +0 -4
- package/dist/lib/session/db.js +0 -26
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +1 -1
- package/dist/lib/shims.js +66 -4
- package/dist/lib/state.d.ts +0 -1
- package/dist/lib/state.js +2 -15
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +6 -3
- package/dist/lib/types.js +6 -3
- package/dist/lib/versions.d.ts +10 -2
- package/dist/lib/versions.js +227 -35
- package/package.json +7 -7
- package/npm-shrinkwrap.json +0 -3162
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI tool resources — declarative manifests for command-line binaries the user
|
|
3
|
+
* wants installed on the host (e.g. higgsfield, gh, glab).
|
|
4
|
+
*
|
|
5
|
+
* A CLI resource is a YAML file under <repo>/cli/<name>.yaml. Resolution follows
|
|
6
|
+
* the same project > user > system > extra-repo precedence as other resources,
|
|
7
|
+
* but unlike skills/commands/hooks, CLI resources are NOT copied into per-agent
|
|
8
|
+
* version homes — they install binaries onto the host PATH. The relationship is
|
|
9
|
+
* "Brewfile-style": declare once in ~/.agents/cli/, install on any new machine.
|
|
10
|
+
*/
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import { execSync, spawnSync } from 'child_process';
|
|
13
|
+
import * as yaml from 'yaml';
|
|
14
|
+
import { listResources, resolveResource } from './resources.js';
|
|
15
|
+
// ─── Parsing ─────────────────────────────────────────────────────────────────
|
|
16
|
+
/**
|
|
17
|
+
* Parse a single CLI manifest from its YAML contents.
|
|
18
|
+
* Returns a manifest on success; throws on schema violations so callers can
|
|
19
|
+
* decide whether to surface or swallow the error per file.
|
|
20
|
+
*/
|
|
21
|
+
export function parseCliManifest(contents, opts) {
|
|
22
|
+
const raw = yaml.parse(contents);
|
|
23
|
+
if (!raw || typeof raw !== 'object') {
|
|
24
|
+
throw new Error('manifest must be a YAML object');
|
|
25
|
+
}
|
|
26
|
+
const name = typeof raw.name === 'string' && raw.name.trim() ? raw.name.trim() : opts.name;
|
|
27
|
+
const description = typeof raw.description === 'string' ? raw.description : undefined;
|
|
28
|
+
const homepage = typeof raw.homepage === 'string' ? raw.homepage : undefined;
|
|
29
|
+
const check = typeof raw.check === 'string' && raw.check.trim()
|
|
30
|
+
? raw.check.trim()
|
|
31
|
+
: `${name} --version`;
|
|
32
|
+
const postInstall = typeof raw.post_install === 'string' ? raw.post_install : undefined;
|
|
33
|
+
if (!Array.isArray(raw.install) || raw.install.length === 0) {
|
|
34
|
+
throw new Error('install must be a non-empty list of methods');
|
|
35
|
+
}
|
|
36
|
+
const install = raw.install.map((entry, i) => {
|
|
37
|
+
if (!entry || typeof entry !== 'object') {
|
|
38
|
+
throw new Error(`install[${i}] must be an object with one of: npm, brew, script, binary`);
|
|
39
|
+
}
|
|
40
|
+
const e = entry;
|
|
41
|
+
const keys = Object.keys(e).filter((k) => e[k] !== undefined && e[k] !== null);
|
|
42
|
+
if (keys.length !== 1) {
|
|
43
|
+
throw new Error(`install[${i}] must declare exactly one method (got: ${keys.join(', ') || 'none'})`);
|
|
44
|
+
}
|
|
45
|
+
const key = keys[0];
|
|
46
|
+
const value = e[key];
|
|
47
|
+
if (key === 'npm' || key === 'brew' || key === 'script') {
|
|
48
|
+
if (typeof value !== 'string' || !value.trim()) {
|
|
49
|
+
throw new Error(`install[${i}].${key} must be a non-empty string`);
|
|
50
|
+
}
|
|
51
|
+
return { [key]: value.trim() };
|
|
52
|
+
}
|
|
53
|
+
if (key === 'binary') {
|
|
54
|
+
if (!value || typeof value !== 'object') {
|
|
55
|
+
throw new Error(`install[${i}].binary must be a platform map`);
|
|
56
|
+
}
|
|
57
|
+
const binary = {};
|
|
58
|
+
for (const [platform, spec] of Object.entries(value)) {
|
|
59
|
+
if (!spec || typeof spec !== 'object') {
|
|
60
|
+
throw new Error(`install[${i}].binary.${platform} must be an object with a url`);
|
|
61
|
+
}
|
|
62
|
+
const s = spec;
|
|
63
|
+
if (typeof s.url !== 'string' || !s.url.trim()) {
|
|
64
|
+
throw new Error(`install[${i}].binary.${platform}.url must be a non-empty string`);
|
|
65
|
+
}
|
|
66
|
+
binary[platform] = {
|
|
67
|
+
url: s.url.trim(),
|
|
68
|
+
extract: typeof s.extract === 'string' ? s.extract : undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
return { binary };
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`install[${i}] has unknown method "${key}" (expected: npm, brew, script, binary)`);
|
|
74
|
+
});
|
|
75
|
+
return {
|
|
76
|
+
name,
|
|
77
|
+
description,
|
|
78
|
+
homepage,
|
|
79
|
+
check,
|
|
80
|
+
install,
|
|
81
|
+
postInstall,
|
|
82
|
+
source: opts.source,
|
|
83
|
+
path: opts.path,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Discover all CLI manifests resolvable from the current cwd. Returns valid
|
|
88
|
+
* manifests and any parse errors separately so the CLI can show both.
|
|
89
|
+
*/
|
|
90
|
+
export function listCliManifests(cwd) {
|
|
91
|
+
const resolved = listResources('cli', cwd);
|
|
92
|
+
const manifests = [];
|
|
93
|
+
const errors = [];
|
|
94
|
+
for (const entry of resolved) {
|
|
95
|
+
if (!entry.path.endsWith('.yaml') && !entry.path.endsWith('.yml'))
|
|
96
|
+
continue;
|
|
97
|
+
try {
|
|
98
|
+
const contents = fs.readFileSync(entry.path, 'utf-8');
|
|
99
|
+
const manifest = parseCliManifest(contents, {
|
|
100
|
+
name: entry.name,
|
|
101
|
+
source: entry.source,
|
|
102
|
+
path: entry.path,
|
|
103
|
+
});
|
|
104
|
+
manifests.push(manifest);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
errors.push({ file: entry.path, reason: err.message });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return { manifests, errors };
|
|
111
|
+
}
|
|
112
|
+
/** Resolve a single CLI manifest by name. Returns null when not declared. */
|
|
113
|
+
export function resolveCliManifest(name, cwd) {
|
|
114
|
+
const resolved = resolveResource('cli', name, cwd);
|
|
115
|
+
if (!resolved)
|
|
116
|
+
return null;
|
|
117
|
+
if (!resolved.path.endsWith('.yaml') && !resolved.path.endsWith('.yml'))
|
|
118
|
+
return null;
|
|
119
|
+
const contents = fs.readFileSync(resolved.path, 'utf-8');
|
|
120
|
+
return parseCliManifest(contents, {
|
|
121
|
+
name: resolved.name,
|
|
122
|
+
source: resolved.source,
|
|
123
|
+
path: resolved.path,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// ─── Host detection ──────────────────────────────────────────────────────────
|
|
127
|
+
/**
|
|
128
|
+
* Return true if a command resolves on the current PATH. Uses `which` on
|
|
129
|
+
* POSIX hosts; results are cached for the lifetime of the process.
|
|
130
|
+
*/
|
|
131
|
+
const cmdExistsCache = new Map();
|
|
132
|
+
export function hasCommand(cmd) {
|
|
133
|
+
if (cmdExistsCache.has(cmd))
|
|
134
|
+
return cmdExistsCache.get(cmd);
|
|
135
|
+
const result = spawnSync('command', ['-v', cmd], { shell: true, stdio: 'ignore' });
|
|
136
|
+
const ok = result.status === 0;
|
|
137
|
+
cmdExistsCache.set(cmd, ok);
|
|
138
|
+
return ok;
|
|
139
|
+
}
|
|
140
|
+
/** Run the manifest's `check` command. Returns true when it exits 0. */
|
|
141
|
+
export function isCliInstalled(manifest) {
|
|
142
|
+
const result = spawnSync(manifest.check, {
|
|
143
|
+
shell: true,
|
|
144
|
+
stdio: 'ignore',
|
|
145
|
+
timeout: 10_000,
|
|
146
|
+
});
|
|
147
|
+
return result.status === 0;
|
|
148
|
+
}
|
|
149
|
+
// ─── Method selection ────────────────────────────────────────────────────────
|
|
150
|
+
/**
|
|
151
|
+
* Pick the first install method whose required host tool is available.
|
|
152
|
+
* Returns null when none of the declared methods can run on this host.
|
|
153
|
+
*/
|
|
154
|
+
export function selectInstallMethod(manifest) {
|
|
155
|
+
for (const method of manifest.install) {
|
|
156
|
+
if ('npm' in method && hasCommand('npm'))
|
|
157
|
+
return method;
|
|
158
|
+
if ('brew' in method && hasCommand('brew'))
|
|
159
|
+
return method;
|
|
160
|
+
if ('script' in method && (hasCommand('curl') || hasCommand('wget')))
|
|
161
|
+
return method;
|
|
162
|
+
if ('binary' in method) {
|
|
163
|
+
const key = `${process.platform}-${process.arch}`;
|
|
164
|
+
if (method.binary[key])
|
|
165
|
+
return method;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
/** Short description of a method for display. */
|
|
171
|
+
export function describeMethod(method) {
|
|
172
|
+
if ('npm' in method)
|
|
173
|
+
return `npm install -g ${method.npm}`;
|
|
174
|
+
if ('brew' in method)
|
|
175
|
+
return `brew install ${method.brew}`;
|
|
176
|
+
if ('script' in method)
|
|
177
|
+
return `curl ${method.script} | sh`;
|
|
178
|
+
const key = `${process.platform}-${process.arch}`;
|
|
179
|
+
const spec = method.binary[key];
|
|
180
|
+
return spec ? `download ${spec.url}` : 'binary download';
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Install a single CLI by running its first compatible method. Streams the
|
|
184
|
+
* underlying command's output to the parent terminal so users see brew/npm
|
|
185
|
+
* progress live. Verifies success by re-running `check`.
|
|
186
|
+
*/
|
|
187
|
+
export function installCli(manifest, opts = {}) {
|
|
188
|
+
const method = selectInstallMethod(manifest);
|
|
189
|
+
if (!method) {
|
|
190
|
+
return {
|
|
191
|
+
manifest,
|
|
192
|
+
method: null,
|
|
193
|
+
installed: false,
|
|
194
|
+
error: `No compatible install method for this host (${process.platform}-${process.arch}). Declared methods: ${manifest.install.map(describeMethod).join('; ')}`,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
if (opts.dryRun) {
|
|
198
|
+
return { manifest, method, installed: false, output: `[dry-run] would run: ${describeMethod(method)}` };
|
|
199
|
+
}
|
|
200
|
+
const cmd = buildInstallCommand(method);
|
|
201
|
+
try {
|
|
202
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
203
|
+
}
|
|
204
|
+
catch (err) {
|
|
205
|
+
return {
|
|
206
|
+
manifest,
|
|
207
|
+
method,
|
|
208
|
+
installed: false,
|
|
209
|
+
error: `install command failed: ${err.message}`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// Re-check; many installers exit 0 but leave the binary off PATH for the
|
|
213
|
+
// current shell (e.g. brew on a fresh install). Trust `check`, not the
|
|
214
|
+
// installer's exit code.
|
|
215
|
+
cmdExistsCache.delete(manifest.name);
|
|
216
|
+
const installed = isCliInstalled(manifest);
|
|
217
|
+
return { manifest, method, installed };
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Map a declarative method to a shell command. Centralized so tests and dry-run
|
|
221
|
+
* surface the exact string that would execute.
|
|
222
|
+
*/
|
|
223
|
+
export function buildInstallCommand(method) {
|
|
224
|
+
if ('npm' in method)
|
|
225
|
+
return `npm install -g ${method.npm}`;
|
|
226
|
+
if ('brew' in method)
|
|
227
|
+
return `brew install ${method.brew}`;
|
|
228
|
+
if ('script' in method) {
|
|
229
|
+
// Prefer curl when both are present; fall back to wget.
|
|
230
|
+
return hasCommand('curl')
|
|
231
|
+
? `curl -fsSL ${method.script} | sh`
|
|
232
|
+
: `wget -qO- ${method.script} | sh`;
|
|
233
|
+
}
|
|
234
|
+
const key = `${process.platform}-${process.arch}`;
|
|
235
|
+
const spec = method.binary[key];
|
|
236
|
+
// The downloader is intentionally minimal — binary install is mostly used
|
|
237
|
+
// for pre-built tarballs whose extract path varies per project. We expect
|
|
238
|
+
// the manifest author to document any post-download steps in post_install.
|
|
239
|
+
return spec.extract
|
|
240
|
+
? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
|
|
241
|
+
: `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
|
|
242
|
+
}
|
|
243
|
+
/** Convenience: list all manifests + their installed-on-host status. */
|
|
244
|
+
export function listCliStatus(cwd) {
|
|
245
|
+
const { manifests, errors } = listCliManifests(cwd);
|
|
246
|
+
const statuses = manifests.map((manifest) => ({
|
|
247
|
+
manifest,
|
|
248
|
+
installed: isCliInstalled(manifest),
|
|
249
|
+
}));
|
|
250
|
+
return { statuses, errors };
|
|
251
|
+
}
|
|
252
|
+
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
253
|
+
export function getMissingClis(cwd) {
|
|
254
|
+
return listCliStatus(cwd).statuses.filter((s) => !s.installed).map((s) => s.manifest);
|
|
255
|
+
}
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -16,7 +16,7 @@ import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
|
16
16
|
import { getAccountInfo } from '../agents.js';
|
|
17
17
|
import { loadClaudeOauth } from '../usage.js';
|
|
18
18
|
import { selectBalancedVersion } from '../rotate.js';
|
|
19
|
-
const PROXY_BASE = 'https://api.prix.dev';
|
|
19
|
+
const PROXY_BASE = process.env.RUSH_PROXY_BASE ?? 'https://api.prix.dev';
|
|
20
20
|
const PROXY_HOST = new URL(PROXY_BASE).host;
|
|
21
21
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
22
22
|
// Persistent consent record for uploading Claude OAuth blobs to Rush Cloud.
|
|
@@ -441,7 +441,7 @@ export class RushCloudProvider {
|
|
|
441
441
|
}
|
|
442
442
|
async status(taskId) {
|
|
443
443
|
const token = readToken();
|
|
444
|
-
const res = await api('GET', `/api/v1/cloud-runs/${taskId}`, token);
|
|
444
|
+
const res = await api('GET', `/api/v1/cloud-runs/${encodeURIComponent(taskId)}`, token);
|
|
445
445
|
if (!res.ok) {
|
|
446
446
|
throw new Error(`Failed to get task status (${res.status}).`);
|
|
447
447
|
}
|
|
@@ -487,7 +487,7 @@ export class RushCloudProvider {
|
|
|
487
487
|
}
|
|
488
488
|
async *stream(taskId) {
|
|
489
489
|
const token = readToken();
|
|
490
|
-
const res = await fetch(`${PROXY_BASE}/api/v1/cloud-runs/${taskId}/stream`, {
|
|
490
|
+
const res = await fetch(`${PROXY_BASE}/api/v1/cloud-runs/${encodeURIComponent(taskId)}/stream`, {
|
|
491
491
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
492
492
|
});
|
|
493
493
|
if (!res.ok) {
|
|
@@ -497,14 +497,14 @@ export class RushCloudProvider {
|
|
|
497
497
|
}
|
|
498
498
|
async cancel(taskId) {
|
|
499
499
|
const token = readToken();
|
|
500
|
-
const res = await api('DELETE', `/api/v1/cloud-runs/${taskId}`, token);
|
|
500
|
+
const res = await api('DELETE', `/api/v1/cloud-runs/${encodeURIComponent(taskId)}`, token);
|
|
501
501
|
if (!res.ok) {
|
|
502
502
|
throw new Error(`Failed to cancel task (${res.status}).`);
|
|
503
503
|
}
|
|
504
504
|
}
|
|
505
505
|
async message(taskId, content) {
|
|
506
506
|
const token = readToken();
|
|
507
|
-
const res = await api('POST', `/api/v1/cloud-runs/${taskId}/message`, token, { content });
|
|
507
|
+
const res = await api('POST', `/api/v1/cloud-runs/${encodeURIComponent(taskId)}/message`, token, { content });
|
|
508
508
|
if (!res.ok) {
|
|
509
509
|
throw new Error(`Failed to send message (${res.status}).`);
|
|
510
510
|
}
|
|
@@ -50,8 +50,6 @@ function readSkillCommandMarker(skillMdPath) {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
export function shouldInstallCommandAsSkill(agent, version) {
|
|
53
|
-
if (agent !== 'codex')
|
|
54
|
-
return false;
|
|
55
53
|
return !supports(agent, 'commands', version).ok && supports(agent, 'skills', version).ok;
|
|
56
54
|
}
|
|
57
55
|
export function commandSkillName(commandName) {
|
|
@@ -15,6 +15,9 @@ export declare function resolveLogPath(): string;
|
|
|
15
15
|
export declare function resolvePolicyPath(): string;
|
|
16
16
|
export declare function loadComputerAllowList(): string[];
|
|
17
17
|
export declare function writeComputerPolicy(allowedBundleIds: string[]): void;
|
|
18
|
+
export declare function resolvePeersPath(): string;
|
|
19
|
+
export declare function loadDefaultPeers(): string[];
|
|
20
|
+
export declare function writeComputerPeers(allowedExecPaths: string[]): void;
|
|
18
21
|
export declare function resolveHelperExec(): string | null;
|
|
19
22
|
export declare function resolveHelperApp(): string | null;
|
|
20
23
|
export declare function openComputerClient(): ComputerClient;
|
package/dist/lib/computer-rpc.js
CHANGED
|
@@ -110,6 +110,59 @@ export function writeComputerPolicy(allowedBundleIds) {
|
|
|
110
110
|
const policy = { allow: allowedBundleIds };
|
|
111
111
|
fs.writeFileSync(resolvePolicyPath(), JSON.stringify(policy, null, 2), { mode: 0o600 });
|
|
112
112
|
}
|
|
113
|
+
// Peer-auth (F5): the helper reads a list of executable paths it will
|
|
114
|
+
// accept connections from. Anything else — `nc`, `/usr/bin/python3`, a
|
|
115
|
+
// random electron app — gets the socket closed before its first RPC.
|
|
116
|
+
// File mirrors computer-policy.json: JSON, mode 0600, missing/unparseable
|
|
117
|
+
// means deny-everything.
|
|
118
|
+
export function resolvePeersPath() {
|
|
119
|
+
return path.join(getHelpersDir(), 'computer-peers.json');
|
|
120
|
+
}
|
|
121
|
+
// Default peer set: this exact `agents` CLI binary plus Rush.app if it's
|
|
122
|
+
// installed. realpath() the symlink chain so we record the on-disk path
|
|
123
|
+
// the helper will see via proc_pidpath, not the shim path.
|
|
124
|
+
//
|
|
125
|
+
// Why path-based instead of codesign-team-id? The agents CLI is unsigned
|
|
126
|
+
// today (npm distribution), and even if we sign Rush.app the team-id
|
|
127
|
+
// check would need a separate roundtrip. Path is concrete and fast; the
|
|
128
|
+
// daemon already runs as the user so anyone who can swap a binary at
|
|
129
|
+
// these paths can do worse via other means.
|
|
130
|
+
export function loadDefaultPeers() {
|
|
131
|
+
const out = new Set();
|
|
132
|
+
const add = (p) => {
|
|
133
|
+
try {
|
|
134
|
+
out.add(fs.realpathSync(p));
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
out.add(p);
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
// The Node executable currently running the CLI. This is what
|
|
141
|
+
// proc_pidpath() will report when the CLI calls into the daemon.
|
|
142
|
+
if (process.execPath)
|
|
143
|
+
add(process.execPath);
|
|
144
|
+
// Rush.app — the consumer Electron client. Both the helper-binary and
|
|
145
|
+
// the main app binary are possible callers depending on how Rush wires
|
|
146
|
+
// the RPC client.
|
|
147
|
+
const rushCandidates = [
|
|
148
|
+
'/Applications/Rush.app/Contents/MacOS/Rush',
|
|
149
|
+
'/Applications/Rush.app/Contents/MacOS/Electron',
|
|
150
|
+
];
|
|
151
|
+
for (const p of rushCandidates) {
|
|
152
|
+
if (fs.existsSync(p))
|
|
153
|
+
add(p);
|
|
154
|
+
}
|
|
155
|
+
return [...out].sort();
|
|
156
|
+
}
|
|
157
|
+
// Write the peer-auth allow list. Same mode 0600 + atomic-ish semantics
|
|
158
|
+
// as the policy file. The daemon picks it up at startup and on SIGHUP.
|
|
159
|
+
export function writeComputerPeers(allowedExecPaths) {
|
|
160
|
+
const dir = getHelpersDir();
|
|
161
|
+
if (!fs.existsSync(dir)) {
|
|
162
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
163
|
+
}
|
|
164
|
+
fs.writeFileSync(resolvePeersPath(), JSON.stringify({ allow: allowedExecPaths }, null, 2), { mode: 0o600 });
|
|
165
|
+
}
|
|
113
166
|
// Resolve the helper executable inside the dist .app bundle. Used by the
|
|
114
167
|
// stdio fallback and by install-helper to find the source bundle.
|
|
115
168
|
export function resolveHelperExec() {
|
package/dist/lib/daemon.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
|
14
14
|
import { listJobs as listAllJobs } from './routines.js';
|
|
15
15
|
import { JobScheduler } from './scheduler.js';
|
|
16
16
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
17
|
+
import { detectOverdueJobs, notifyOverdue } from './overdue.js';
|
|
17
18
|
import { BrowserService } from './browser/service.js';
|
|
18
19
|
import { BrowserIPCServer } from './browser/ipc.js';
|
|
19
20
|
const PID_FILE = 'daemon.pid';
|
|
@@ -178,6 +179,25 @@ export async function runDaemon() {
|
|
|
178
179
|
for (const job of scheduled) {
|
|
179
180
|
log('INFO', ` ${job.name} -> next: ${job.nextRun?.toISOString() || 'unknown'}`);
|
|
180
181
|
}
|
|
182
|
+
// Backlog detection: any enabled recurring job whose most-recent expected
|
|
183
|
+
// fire is older than its most-recent recorded run is overdue. Happens when
|
|
184
|
+
// the laptop was off or the daemon crashed through a scheduled fire.
|
|
185
|
+
// We log it and pop a native notification — the user can review with
|
|
186
|
+
// `agents routines list` and run them with `agents routines catchup`.
|
|
187
|
+
try {
|
|
188
|
+
const overdue = detectOverdueJobs();
|
|
189
|
+
if (overdue.length > 0) {
|
|
190
|
+
log('WARN', `${overdue.length} routine(s) overdue:`);
|
|
191
|
+
for (const job of overdue) {
|
|
192
|
+
const last = job.lastRanAt ? job.lastRanAt.toISOString() : 'never';
|
|
193
|
+
log('WARN', ` ${job.name} -- expected ${job.expectedAt.toISOString()}, last ran ${last}`);
|
|
194
|
+
}
|
|
195
|
+
notifyOverdue(overdue);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (err) {
|
|
199
|
+
log('ERROR', `Overdue detection failed: ${err.message}`);
|
|
200
|
+
}
|
|
181
201
|
// Before the BrowserService comes up, reap browser + tunnel processes
|
|
182
202
|
// spawned by previous daemons that are no longer alive. Without this,
|
|
183
203
|
// a daemon hard-crash (SIGKILL, OOM) would leak every browser and SSH
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -27,8 +27,9 @@ export interface ExecOptions {
|
|
|
27
27
|
export declare function parseExecEnv(entries: string[]): Record<string, string> | undefined;
|
|
28
28
|
/**
|
|
29
29
|
* Build the process environment for an agent invocation.
|
|
30
|
-
* Pins CLAUDE_CONFIG_DIR for Claude
|
|
31
|
-
* other
|
|
30
|
+
* Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
|
|
31
|
+
* for GitHub Copilot; strips the other agents' env vars so they don't leak
|
|
32
|
+
* into unrelated invocations.
|
|
32
33
|
*/
|
|
33
34
|
export declare function buildExecEnv(options: ExecOptions): NodeJS.ProcessEnv;
|
|
34
35
|
/** Describes how to translate ExecOptions into CLI arguments for a specific agent. */
|
package/dist/lib/exec.js
CHANGED
|
@@ -35,8 +35,9 @@ export function parseExecEnv(entries) {
|
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Build the process environment for an agent invocation.
|
|
38
|
-
* Pins CLAUDE_CONFIG_DIR for Claude
|
|
39
|
-
* other
|
|
38
|
+
* Pins CLAUDE_CONFIG_DIR for Claude, CODEX_HOME for Codex, and COPILOT_HOME
|
|
39
|
+
* for GitHub Copilot; strips the other agents' env vars so they don't leak
|
|
40
|
+
* into unrelated invocations.
|
|
40
41
|
*/
|
|
41
42
|
export function buildExecEnv(options) {
|
|
42
43
|
const result = { ...process.env };
|
|
@@ -56,6 +57,7 @@ export function buildExecEnv(options) {
|
|
|
56
57
|
result.CLAUDE_CONFIG_DIR = path.join(getVersionHomePath('claude', version), '.claude');
|
|
57
58
|
}
|
|
58
59
|
delete result.CODEX_HOME;
|
|
60
|
+
delete result.COPILOT_HOME;
|
|
59
61
|
}
|
|
60
62
|
else if (options.agent === 'codex') {
|
|
61
63
|
const cwd = options.cwd || process.cwd();
|
|
@@ -67,10 +69,27 @@ export function buildExecEnv(options) {
|
|
|
67
69
|
result.CODEX_HOME = path.join(getVersionHomePath('codex', version), '.codex');
|
|
68
70
|
}
|
|
69
71
|
delete result.CLAUDE_CONFIG_DIR;
|
|
72
|
+
delete result.COPILOT_HOME;
|
|
73
|
+
}
|
|
74
|
+
else if (options.agent === 'copilot') {
|
|
75
|
+
// Copilot honors COPILOT_HOME (relocates ~/.copilot, including settings,
|
|
76
|
+
// mcp-config.json, sessions, logs). Pin it at the per-version home so
|
|
77
|
+
// version switches isolate MCP servers, auth, and session history.
|
|
78
|
+
const cwd = options.cwd || process.cwd();
|
|
79
|
+
const resolvedVersion = options.version ?? resolveVersion('copilot', cwd);
|
|
80
|
+
const version = options.version
|
|
81
|
+
? resolvedVersion
|
|
82
|
+
: (resolvedVersion && isVersionInstalled('copilot', resolvedVersion) ? resolvedVersion : null);
|
|
83
|
+
if (version) {
|
|
84
|
+
result.COPILOT_HOME = path.join(getVersionHomePath('copilot', version), '.copilot');
|
|
85
|
+
}
|
|
86
|
+
delete result.CLAUDE_CONFIG_DIR;
|
|
87
|
+
delete result.CODEX_HOME;
|
|
70
88
|
}
|
|
71
89
|
else {
|
|
72
90
|
delete result.CLAUDE_CONFIG_DIR;
|
|
73
91
|
delete result.CODEX_HOME;
|
|
92
|
+
delete result.COPILOT_HOME;
|
|
74
93
|
}
|
|
75
94
|
return {
|
|
76
95
|
...result,
|
|
@@ -148,14 +167,25 @@ export const AGENT_COMMANDS = {
|
|
|
148
167
|
jsonFlags: ['--output-format', 'stream-json'],
|
|
149
168
|
modelFlag: '--model',
|
|
150
169
|
},
|
|
170
|
+
// GitHub Copilot CLI (`@github/copilot`, GA 2026-02-25). Flags verified
|
|
171
|
+
// against `copilot --help` from v0.0.413+:
|
|
172
|
+
// -p, --prompt <text> non-interactive one-shot
|
|
173
|
+
// --mode <interactive|plan|autopilot>
|
|
174
|
+
// --allow-all-tools required for non-interactive tool exec
|
|
175
|
+
// --allow-all (alias --yolo) tools + paths + URLs
|
|
176
|
+
// --output-format <text|json> json => JSONL, one object per line
|
|
177
|
+
// --model <model>
|
|
178
|
+
// Plan mode is read-only so it does not need an allow-tools grant; edit/full
|
|
179
|
+
// need at minimum --allow-all-tools so headless runs don't stall on prompts.
|
|
151
180
|
copilot: {
|
|
152
181
|
base: ['copilot'],
|
|
153
|
-
promptFlag: '
|
|
182
|
+
promptFlag: '-p',
|
|
154
183
|
modeFlags: {
|
|
155
|
-
plan: [],
|
|
156
|
-
edit: [],
|
|
157
|
-
full: [],
|
|
184
|
+
plan: ['--mode', 'plan'],
|
|
185
|
+
edit: ['--allow-all-tools'],
|
|
186
|
+
full: ['--allow-all'],
|
|
158
187
|
},
|
|
188
|
+
jsonFlags: ['--output-format', 'json'],
|
|
159
189
|
modelFlag: '--model',
|
|
160
190
|
},
|
|
161
191
|
amp: {
|
|
@@ -197,13 +227,18 @@ export const AGENT_COMMANDS = {
|
|
|
197
227
|
},
|
|
198
228
|
modelFlag: '--model',
|
|
199
229
|
},
|
|
230
|
+
// Antigravity full mode uses --dangerously-skip-permissions (YOLO).
|
|
231
|
+
// TODO: --output-format json is documented but currently broken upstream
|
|
232
|
+
// ("flags provided but not defined: -output-format"). Track resolution at
|
|
233
|
+
// https://github.com/google-antigravity/antigravity-cli/issues/7 before
|
|
234
|
+
// adding `jsonFlags` here.
|
|
200
235
|
antigravity: {
|
|
201
236
|
base: ['agy'],
|
|
202
237
|
promptFlag: 'positional',
|
|
203
238
|
modeFlags: {
|
|
204
239
|
plan: [],
|
|
205
240
|
edit: [],
|
|
206
|
-
full: [],
|
|
241
|
+
full: ['--dangerously-skip-permissions'],
|
|
207
242
|
},
|
|
208
243
|
modelFlag: '--model',
|
|
209
244
|
},
|
|
@@ -211,9 +246,9 @@ export const AGENT_COMMANDS = {
|
|
|
211
246
|
base: ['grok'],
|
|
212
247
|
promptFlag: '-p',
|
|
213
248
|
modeFlags: {
|
|
214
|
-
plan: [],
|
|
249
|
+
plan: ['--mode', 'plan'],
|
|
215
250
|
edit: [],
|
|
216
|
-
full: [],
|
|
251
|
+
full: ['--always-approve'],
|
|
217
252
|
},
|
|
218
253
|
jsonFlags: ['--output-format', 'streaming-json'],
|
|
219
254
|
modelFlag: '--model',
|