@phnx-labs/agents-cli 1.18.6 → 1.19.1
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 +13 -2
- package/README.md +22 -20
- package/dist/commands/browser.js +25 -2
- package/dist/commands/cloud.js +3 -3
- package/dist/commands/computer.d.ts +6 -0
- package/dist/commands/computer.js +477 -0
- package/dist/commands/doctor.js +19 -17
- package/dist/commands/exec.js +37 -59
- package/dist/commands/factory.js +12 -5
- package/dist/commands/import.js +6 -1
- package/dist/commands/mcp.js +9 -4
- package/dist/commands/packages.d.ts +3 -0
- package/dist/commands/packages.js +20 -12
- package/dist/commands/permissions.d.ts +2 -0
- package/dist/commands/permissions.js +20 -1
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +23 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pty.js +126 -112
- package/dist/commands/pull.js +29 -25
- package/dist/commands/repo.js +24 -26
- package/dist/commands/routines.js +29 -26
- package/dist/commands/secrets.js +66 -73
- package/dist/commands/sessions-tail.js +21 -22
- package/dist/commands/sessions.js +36 -68
- package/dist/commands/setup.js +20 -24
- package/dist/commands/teams.js +30 -39
- package/dist/commands/versions.js +60 -68
- package/dist/commands/worktree.d.ts +20 -0
- package/dist/commands/worktree.js +242 -0
- package/dist/computer.d.ts +2 -0
- package/dist/computer.js +7 -0
- package/dist/index.js +70 -26
- package/dist/lib/agents.d.ts +4 -1
- package/dist/lib/agents.js +23 -5
- package/dist/lib/browser/cdp.d.ts +15 -1
- package/dist/lib/browser/cdp.js +77 -8
- package/dist/lib/browser/chrome.js +17 -24
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +20 -8
- package/dist/lib/browser/ipc.js +38 -5
- package/dist/lib/browser/profiles.js +34 -2
- package/dist/lib/browser/runtime-state.d.ts +1 -2
- package/dist/lib/browser/runtime-state.js +11 -3
- package/dist/lib/browser/service.d.ts +5 -0
- package/dist/lib/browser/service.js +32 -4
- package/dist/lib/browser/types.d.ts +1 -1
- package/dist/lib/browser/upload.d.ts +2 -0
- package/dist/lib/browser/upload.js +34 -0
- package/dist/lib/cloud/rush.d.ts +2 -1
- package/dist/lib/cloud/rush.js +28 -9
- package/dist/lib/computer-rpc.d.ts +24 -0
- package/dist/lib/computer-rpc.js +263 -0
- package/dist/lib/daemon.js +7 -7
- package/dist/lib/exec.d.ts +2 -1
- package/dist/lib/exec.js +3 -2
- package/dist/lib/fs-atomic.d.ts +18 -0
- package/dist/lib/fs-atomic.js +76 -0
- package/dist/lib/git.js +2 -4
- package/dist/lib/help.d.ts +15 -0
- package/dist/lib/help.js +41 -0
- package/dist/lib/hooks/match.d.ts +1 -0
- package/dist/lib/hooks/match.js +57 -12
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +27 -10
- package/dist/lib/import.d.ts +1 -0
- package/dist/lib/import.js +7 -0
- package/dist/lib/manifest.js +27 -1
- package/dist/lib/mcp.d.ts +14 -0
- package/dist/lib/mcp.js +79 -14
- package/dist/lib/migrate.js +3 -3
- package/dist/lib/models.js +3 -1
- package/dist/lib/permissions.d.ts +5 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/plugin-marketplace.d.ts +3 -1
- package/dist/lib/plugin-marketplace.js +36 -1
- package/dist/lib/plugins.d.ts +19 -1
- package/dist/lib/plugins.js +99 -8
- package/dist/lib/redact.d.ts +4 -0
- package/dist/lib/redact.js +18 -0
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +15 -0
- package/dist/lib/sandbox.js +15 -5
- package/dist/lib/secrets/bundles.d.ts +7 -12
- package/dist/lib/secrets/bundles.js +45 -29
- package/dist/lib/secrets/index.js +4 -4
- package/dist/lib/session/cloud.d.ts +2 -0
- package/dist/lib/session/cloud.js +34 -6
- package/dist/lib/session/parse.js +7 -2
- package/dist/lib/session/render.d.ts +4 -1
- package/dist/lib/session/render.js +81 -35
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +29 -7
- package/dist/lib/state.d.ts +5 -5
- package/dist/lib/state.js +43 -13
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/types.js +0 -2
- package/dist/lib/versions.js +65 -40
- package/dist/lib/workflows.d.ts +7 -0
- package/dist/lib/workflows.js +42 -1
- package/npm-shrinkwrap.json +3162 -0
- package/package.json +32 -26
- package/scripts/postinstall.js +8 -2
|
@@ -7,14 +7,34 @@ import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from './cdp.js
|
|
|
7
7
|
import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, } from './profiles.js';
|
|
8
8
|
import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
|
|
9
9
|
import { connectLocal } from './drivers/local.js';
|
|
10
|
-
import { connectSSH } from './drivers/ssh.js';
|
|
10
|
+
import { connectSSH, shellQuote } from './drivers/ssh.js';
|
|
11
11
|
import { clearProfileRuntime } from './runtime-state.js';
|
|
12
12
|
import { generateTaskId, generateShortId, generateTaskName, } from './types.js';
|
|
13
13
|
import { getRefs, resolveRefToCoords } from './refs.js';
|
|
14
14
|
import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
15
15
|
import { typeEditorText } from './editor.js';
|
|
16
|
-
import { detectUploadPattern, uploadToDropTarget, uploadToFileInput, uploadViaFileChooser, } from './upload.js';
|
|
16
|
+
import { detectUploadPattern, stageUploadFile, uploadToDropTarget, uploadToFileInput, uploadViaFileChooser, } from './upload.js';
|
|
17
17
|
import { emit } from '../events.js';
|
|
18
|
+
function isPathInside(candidate, dir) {
|
|
19
|
+
const rel = path.relative(dir, candidate);
|
|
20
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
21
|
+
}
|
|
22
|
+
export function resolveScreenshotOutputPath(outputPath, automaticPath) {
|
|
23
|
+
if (!outputPath)
|
|
24
|
+
return automaticPath;
|
|
25
|
+
const runtimeDir = getBrowserRuntimeDir();
|
|
26
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
27
|
+
const runtimeReal = fs.realpathSync(runtimeDir);
|
|
28
|
+
const requested = path.resolve(outputPath);
|
|
29
|
+
const parent = path.dirname(requested);
|
|
30
|
+
fs.mkdirSync(parent, { recursive: true });
|
|
31
|
+
const parentReal = fs.realpathSync(parent);
|
|
32
|
+
const resolved = path.join(parentReal, path.basename(requested));
|
|
33
|
+
if (!isPathInside(resolved, runtimeReal)) {
|
|
34
|
+
return automaticPath;
|
|
35
|
+
}
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
18
38
|
/**
|
|
19
39
|
* Read width/height from a JPEG buffer by walking SOF markers. Returns null
|
|
20
40
|
* if the buffer doesn't start with the JPEG SOI marker or no SOF segment is
|
|
@@ -151,6 +171,10 @@ async function execSSH(host, cmd) {
|
|
|
151
171
|
});
|
|
152
172
|
return stdout;
|
|
153
173
|
}
|
|
174
|
+
export function readNewestMatchingRemoteFileCommand(dir, prefix, tailLines) {
|
|
175
|
+
const glob = `${shellQuote(dir)}/${prefix}*.jsonl`;
|
|
176
|
+
return `latest=$(ls -1t ${glob} 2>/dev/null | head -1); if [ -n "$latest" ]; then tail -n ${tailLines} "$latest"; fi`;
|
|
177
|
+
}
|
|
154
178
|
export function readNewestMatchingFile(dir, prefix, tailLines) {
|
|
155
179
|
let entries;
|
|
156
180
|
try {
|
|
@@ -619,7 +643,8 @@ export class BrowserService {
|
|
|
619
643
|
extension = 'jpg';
|
|
620
644
|
}
|
|
621
645
|
const sessionsDir = path.join(getBrowserRuntimeDir(), 'sessions', task.name);
|
|
622
|
-
const
|
|
646
|
+
const automaticPath = path.join(sessionsDir, `${Date.now()}.${extension}`);
|
|
647
|
+
const finalPath = resolveScreenshotOutputPath(outputPath, automaticPath);
|
|
623
648
|
await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
|
|
624
649
|
await fs.promises.writeFile(finalPath, buffer);
|
|
625
650
|
const dims = (extension === 'png' ? readPngDimensions(buffer) : readJpegDimensions(buffer)) ??
|
|
@@ -920,6 +945,9 @@ export class BrowserService {
|
|
|
920
945
|
}
|
|
921
946
|
return { mode: resolved };
|
|
922
947
|
}
|
|
948
|
+
stageUpload(source) {
|
|
949
|
+
return { path: stageUploadFile(source) };
|
|
950
|
+
}
|
|
923
951
|
async status(profileName) {
|
|
924
952
|
const seen = new Set();
|
|
925
953
|
const statuses = [];
|
|
@@ -1194,7 +1222,7 @@ export class BrowserService {
|
|
|
1194
1222
|
if (!prefix)
|
|
1195
1223
|
return '';
|
|
1196
1224
|
if (profile.logHost) {
|
|
1197
|
-
return execSSH(profile.logHost,
|
|
1225
|
+
return execSSH(profile.logHost, readNewestMatchingRemoteFileCommand(logDir, prefix, tailN));
|
|
1198
1226
|
}
|
|
1199
1227
|
return readNewestMatchingFile(logDir, prefix, tailN);
|
|
1200
1228
|
}));
|
|
@@ -43,7 +43,7 @@ export interface BrowserProfile {
|
|
|
43
43
|
};
|
|
44
44
|
/** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
|
|
45
45
|
logDir?: string;
|
|
46
|
-
/** Optional SSH host where logDir lives, e.g. "
|
|
46
|
+
/** Optional SSH host where logDir lives, e.g. "user@mac-mini". */
|
|
47
47
|
logHost?: string;
|
|
48
48
|
}
|
|
49
49
|
/** Parsed form of `BrowserProfile.targetFilter`. */
|
|
@@ -27,6 +27,8 @@ import { type RefNode } from './refs.js';
|
|
|
27
27
|
export interface UploadOptions {
|
|
28
28
|
files: string[];
|
|
29
29
|
}
|
|
30
|
+
export declare function getUploadStagingDir(): string;
|
|
31
|
+
export declare function stageUploadFile(source: string): string;
|
|
30
32
|
/** Pattern A — direct file input via `DOM.setFileInputFiles`. */
|
|
31
33
|
export declare function uploadToFileInput(cdp: CDPClient, sessionId: string, backendNodeId: number, files: string[]): Promise<void>;
|
|
32
34
|
/** Pattern B — synthetic drag-drop onto a target node. */
|
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import { randomBytes } from 'crypto';
|
|
3
4
|
import { clickAtCoords } from './input.js';
|
|
5
|
+
import { getBrowserRuntimeDir } from './profiles.js';
|
|
4
6
|
import { resolveRefToCoords } from './refs.js';
|
|
7
|
+
export function getUploadStagingDir() {
|
|
8
|
+
return path.join(getBrowserRuntimeDir(), 'uploads');
|
|
9
|
+
}
|
|
10
|
+
export function stageUploadFile(source) {
|
|
11
|
+
if (!path.isAbsolute(source)) {
|
|
12
|
+
throw new Error(`upload-stage: source path must be absolute: ${source}`);
|
|
13
|
+
}
|
|
14
|
+
const resolvedSource = fs.realpathSync(source);
|
|
15
|
+
const stat = fs.statSync(resolvedSource);
|
|
16
|
+
if (!stat.isFile()) {
|
|
17
|
+
throw new Error(`upload-stage: source path is not a file: ${source}`);
|
|
18
|
+
}
|
|
19
|
+
const stagingDir = getUploadStagingDir();
|
|
20
|
+
fs.mkdirSync(stagingDir, { recursive: true, mode: 0o700 });
|
|
21
|
+
fs.chmodSync(stagingDir, 0o700);
|
|
22
|
+
const stagedPath = path.join(stagingDir, `${randomBytes(8).toString('hex')}${path.extname(source)}`);
|
|
23
|
+
fs.copyFileSync(resolvedSource, stagedPath);
|
|
24
|
+
return stagedPath;
|
|
25
|
+
}
|
|
5
26
|
const RESOLVE_FILE_INPUT_FN = `(function() {
|
|
6
27
|
const start = this;
|
|
7
28
|
// Walk multiple paths to find an <input type=file>:
|
|
@@ -191,6 +212,10 @@ function validateFiles(files) {
|
|
|
191
212
|
if (!files || files.length === 0) {
|
|
192
213
|
throw new Error('At least one file path is required');
|
|
193
214
|
}
|
|
215
|
+
const stagingDir = getUploadStagingDir();
|
|
216
|
+
fs.mkdirSync(stagingDir, { recursive: true, mode: 0o700 });
|
|
217
|
+
fs.chmodSync(stagingDir, 0o700);
|
|
218
|
+
const resolvedStagingDir = fs.realpathSync(stagingDir);
|
|
194
219
|
for (const f of files) {
|
|
195
220
|
if (!path.isAbsolute(f)) {
|
|
196
221
|
throw new Error(`Upload path must be absolute: ${f}`);
|
|
@@ -198,8 +223,17 @@ function validateFiles(files) {
|
|
|
198
223
|
if (!fs.existsSync(f)) {
|
|
199
224
|
throw new Error(`File not found: ${f}`);
|
|
200
225
|
}
|
|
226
|
+
const resolvedFile = fs.realpathSync(f);
|
|
227
|
+
if (!isPathInside(resolvedFile, resolvedStagingDir)) {
|
|
228
|
+
throw new Error(`upload: path ${f} is outside the upload staging directory ${stagingDir}. ` +
|
|
229
|
+
`Stage files via 'agents browser upload-stage <path>' first.`);
|
|
230
|
+
}
|
|
201
231
|
}
|
|
202
232
|
}
|
|
233
|
+
function isPathInside(candidate, dir) {
|
|
234
|
+
const rel = path.relative(dir, candidate);
|
|
235
|
+
return rel === '' || (!!rel && !rel.startsWith('..') && !path.isAbsolute(rel));
|
|
236
|
+
}
|
|
203
237
|
const MIME_BY_EXT = {
|
|
204
238
|
'.png': 'image/png',
|
|
205
239
|
'.jpg': 'image/jpeg',
|
package/dist/lib/cloud/rush.d.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOptions, ProviderCapabilities } from './types.js';
|
|
8
8
|
export declare const RUSH_CONSENT_PATH: string;
|
|
9
|
-
export declare function hasRushUploadConsent(opts?: DispatchOptions): boolean;
|
|
9
|
+
export declare function hasRushUploadConsent(accountFingerprint: string, opts?: DispatchOptions, consentPath?: string): boolean;
|
|
10
10
|
/** One version's entry in the account manifest sent on every dispatch. */
|
|
11
11
|
export interface AccountManifestEntry {
|
|
12
12
|
version: string;
|
|
@@ -44,6 +44,7 @@ export declare function buildAccountManifest(strategy?: string): Promise<Account
|
|
|
44
44
|
* never upload tokens for versions the server hasn't asked about.
|
|
45
45
|
*/
|
|
46
46
|
export declare function buildAccountTokensPayload(versions: string[]): Promise<AccountTokenEntry[]>;
|
|
47
|
+
export declare function accountTokensFingerprint(tokens: AccountTokenEntry[]): string;
|
|
47
48
|
/**
|
|
48
49
|
* Build the POST body for /api/v1/cloud-runs. Exported so tests can verify
|
|
49
50
|
* the back-compat shape (singular fields + repos[]) without needing real
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -17,24 +17,38 @@ import { getAccountInfo } from '../agents.js';
|
|
|
17
17
|
import { loadClaudeOauth } from '../usage.js';
|
|
18
18
|
import { selectBalancedVersion } from '../rotate.js';
|
|
19
19
|
const PROXY_BASE = 'https://api.prix.dev';
|
|
20
|
+
const PROXY_HOST = new URL(PROXY_BASE).host;
|
|
20
21
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
21
22
|
// Persistent consent record for uploading Claude OAuth blobs to Rush Cloud.
|
|
22
23
|
// Created on first explicit consent (env var or flag); subsequent dispatches
|
|
23
24
|
// see it and proceed without re-prompting.
|
|
24
25
|
export const RUSH_CONSENT_PATH = path.join(getCloudDir(), 'rush-consent.json');
|
|
25
26
|
const RUSH_CONSENT_ENV = 'AGENTS_RUSH_UPLOAD_TOKENS';
|
|
26
|
-
export function hasRushUploadConsent(opts) {
|
|
27
|
+
export function hasRushUploadConsent(accountFingerprint, opts, consentPath = RUSH_CONSENT_PATH) {
|
|
27
28
|
if (process.env[RUSH_CONSENT_ENV] === '1')
|
|
28
29
|
return true;
|
|
29
30
|
const po = opts?.providerOptions;
|
|
30
31
|
if (po?.uploadAccountTokens === true)
|
|
31
32
|
return true;
|
|
32
|
-
|
|
33
|
+
try {
|
|
34
|
+
if (!fs.existsSync(consentPath))
|
|
35
|
+
return false;
|
|
36
|
+
const body = JSON.parse(fs.readFileSync(consentPath, 'utf-8'));
|
|
37
|
+
return body.host === PROXY_HOST && body.account_fingerprint === accountFingerprint;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
33
42
|
}
|
|
34
|
-
function recordRushUploadConsent(grantedBy) {
|
|
43
|
+
function recordRushUploadConsent(grantedBy, accountFingerprint) {
|
|
35
44
|
try {
|
|
36
45
|
fs.mkdirSync(path.dirname(RUSH_CONSENT_PATH), { recursive: true });
|
|
37
|
-
const body = {
|
|
46
|
+
const body = {
|
|
47
|
+
granted_at: new Date().toISOString(),
|
|
48
|
+
granted_by: grantedBy,
|
|
49
|
+
host: PROXY_HOST,
|
|
50
|
+
account_fingerprint: accountFingerprint,
|
|
51
|
+
};
|
|
38
52
|
fs.writeFileSync(RUSH_CONSENT_PATH, JSON.stringify(body, null, 2), { mode: 0o600 });
|
|
39
53
|
}
|
|
40
54
|
catch {
|
|
@@ -107,7 +121,7 @@ async function findInstallation(token, owner, repo) {
|
|
|
107
121
|
}
|
|
108
122
|
}
|
|
109
123
|
}
|
|
110
|
-
throw new Error(`No GitHub App installation found for ${owner}/${repo}. Install the Rush GitHub App at https://github.com/apps/
|
|
124
|
+
throw new Error(`No GitHub App installation found for ${owner}/${repo}. Install the Rush GitHub App at https://github.com/apps/cloud-bot.`);
|
|
111
125
|
}
|
|
112
126
|
/** sha256 → hex. */
|
|
113
127
|
function sha256(input) {
|
|
@@ -239,6 +253,10 @@ export async function buildAccountTokensPayload(versions) {
|
|
|
239
253
|
}
|
|
240
254
|
return out;
|
|
241
255
|
}
|
|
256
|
+
export function accountTokensFingerprint(tokens) {
|
|
257
|
+
const canonical = [...tokens].sort((a, b) => a.version.localeCompare(b.version));
|
|
258
|
+
return sha256(JSON.stringify(canonical));
|
|
259
|
+
}
|
|
242
260
|
/**
|
|
243
261
|
* Build the POST body for /api/v1/cloud-runs. Exported so tests can verify
|
|
244
262
|
* the back-compat shape (singular fields + repos[]) without needing real
|
|
@@ -360,11 +378,13 @@ export class RushCloudProvider {
|
|
|
360
378
|
const errBody = await res.clone().text();
|
|
361
379
|
const promptCode = parsePromptCode(errBody);
|
|
362
380
|
if (promptCode === 'NEW_ACCOUNT' || promptCode === 'TOKEN_ROTATED') {
|
|
381
|
+
const accountTokens = await buildAccountTokensPayload(accountManifest.versions.map((v) => v.version));
|
|
382
|
+
const accountFingerprint = accountTokensFingerprint(accountTokens);
|
|
363
383
|
// Refuse to silently exfiltrate Claude OAuth credentials. The retry
|
|
364
384
|
// path below reads accessToken+refreshToken from every installed
|
|
365
385
|
// Claude version and POSTs them to api.prix.dev. That's an explicit
|
|
366
386
|
// data-flow decision the user has to opt into.
|
|
367
|
-
if (!hasRushUploadConsent(options)) {
|
|
387
|
+
if (!hasRushUploadConsent(accountFingerprint, options)) {
|
|
368
388
|
throw new Error([
|
|
369
389
|
`Rush Cloud asked to sync your Claude credentials (reason: ${promptCode.toLowerCase()}).`,
|
|
370
390
|
`This would upload accessToken + refreshToken from every installed Claude version`,
|
|
@@ -383,8 +403,7 @@ export class RushCloudProvider {
|
|
|
383
403
|
const grantedBy = process.env[RUSH_CONSENT_ENV] === '1' ? 'env'
|
|
384
404
|
: options.providerOptions?.uploadAccountTokens === true ? 'flag'
|
|
385
405
|
: 'manual';
|
|
386
|
-
process.stderr.write(`[rush] uploading
|
|
387
|
-
const accountTokens = await buildAccountTokensPayload(accountManifest.versions.map((v) => v.version));
|
|
406
|
+
process.stderr.write(`[rush] uploading ${accountTokens.length} account token(s) to ${PROXY_HOST}\n`);
|
|
388
407
|
const retryBody = buildDispatchBody({
|
|
389
408
|
agent: options.agent,
|
|
390
409
|
prompt: options.prompt,
|
|
@@ -397,7 +416,7 @@ export class RushCloudProvider {
|
|
|
397
416
|
// Persist consent on first successful upload so we don't re-prompt
|
|
398
417
|
// every time tokens rotate.
|
|
399
418
|
if (res.ok && grantedBy !== 'manual') {
|
|
400
|
-
recordRushUploadConsent(grantedBy);
|
|
419
|
+
recordRushUploadConsent(grantedBy, accountFingerprint);
|
|
401
420
|
}
|
|
402
421
|
}
|
|
403
422
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface RPCResponse {
|
|
2
|
+
id: number | null;
|
|
3
|
+
result?: Record<string, unknown>;
|
|
4
|
+
error?: {
|
|
5
|
+
code: string;
|
|
6
|
+
message: string;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
export interface ComputerClient {
|
|
10
|
+
call(method: string, params?: Record<string, unknown>): Promise<RPCResponse>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveSocketPath(): string;
|
|
14
|
+
export declare function resolveLogPath(): string;
|
|
15
|
+
export declare function resolvePolicyPath(): string;
|
|
16
|
+
export declare function loadComputerAllowList(): string[];
|
|
17
|
+
export declare function writeComputerPolicy(allowedBundleIds: string[]): void;
|
|
18
|
+
export declare function resolveHelperExec(): string | null;
|
|
19
|
+
export declare function resolveHelperApp(): string | null;
|
|
20
|
+
export declare function openComputerClient(): ComputerClient;
|
|
21
|
+
export declare function describeTransport(): {
|
|
22
|
+
kind: 'socket' | 'stdio' | 'none';
|
|
23
|
+
path: string | null;
|
|
24
|
+
};
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
// JSON-RPC client for the computer-helper.
|
|
2
|
+
//
|
|
3
|
+
// Two transports, picked at runtime:
|
|
4
|
+
// - socket: ~/.agents/.cache/helpers/computer.sock (or COMPUTER_HELPER_SOCKET
|
|
5
|
+
// env) when the launchd daemon is installed and listening. Sub-50ms per
|
|
6
|
+
// call. Path is internal scratch — sibling of browser.sock.
|
|
7
|
+
// - stdio: spawn the helper binary as a child process per call. Used in
|
|
8
|
+
// dev (no install-helper) and as a fallback. The legacy probe.py and
|
|
9
|
+
// drive-capcut.py scripts in rush/agents still use this shape.
|
|
10
|
+
//
|
|
11
|
+
// Both transports share the same line-delimited JSON-RPC wire format:
|
|
12
|
+
// in: {"id":N,"method":"...","params":{...}}
|
|
13
|
+
// out: {"id":N,"result":{...}} or {"id":N,"error":{"code":"...","message":"..."}}
|
|
14
|
+
import { spawn } from 'child_process';
|
|
15
|
+
import { createConnection } from 'net';
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { fileURLToPath } from 'url';
|
|
19
|
+
import { getHelpersDir, getLogsDir, getUserPermissionsDir, getPermissionsDir } from './state.js';
|
|
20
|
+
// Resolve the socket path used by the launchd-managed daemon. Internal
|
|
21
|
+
// scratch — lives under getHelpersDir() (~/.agents/.cache/helpers/),
|
|
22
|
+
// matching browser.sock.
|
|
23
|
+
export function resolveSocketPath() {
|
|
24
|
+
const envPath = process.env.COMPUTER_HELPER_SOCKET;
|
|
25
|
+
if (envPath && envPath.length > 0)
|
|
26
|
+
return envPath;
|
|
27
|
+
return path.join(getHelpersDir(), 'computer.sock');
|
|
28
|
+
}
|
|
29
|
+
// Default log path for the launchd-managed daemon. Lives in the cache/logs/
|
|
30
|
+
// bucket (matches the scheduler daemon's logs.jsonl convention).
|
|
31
|
+
export function resolveLogPath() {
|
|
32
|
+
return path.join(getLogsDir(), 'computer-helper.log');
|
|
33
|
+
}
|
|
34
|
+
// Policy file the helper reads at startup and on SIGHUP. Sibling of
|
|
35
|
+
// computer.sock under ~/.agents/.cache/helpers/. Allow-list of bare bundle
|
|
36
|
+
// ids (e.g. "com.apple.mail"), derived from Computer(...) patterns in
|
|
37
|
+
// ~/.agents/permissions/groups/.
|
|
38
|
+
export function resolvePolicyPath() {
|
|
39
|
+
return path.join(getHelpersDir(), 'computer-policy.json');
|
|
40
|
+
}
|
|
41
|
+
// Walk all permission group YAMLs (user dir wins on name collision) and
|
|
42
|
+
// collect Computer(<bundle-id>) patterns from each group's `allow:` list.
|
|
43
|
+
// Returns distinct bundle ids. Line-by-line regex extraction matches
|
|
44
|
+
// buildPermissionsFromGroups: YAML parsers stumble on the nested quotes in
|
|
45
|
+
// some rule values, but the strict pattern below catches our shape cleanly.
|
|
46
|
+
export function loadComputerAllowList() {
|
|
47
|
+
const seenFiles = new Set();
|
|
48
|
+
const allowed = new Set();
|
|
49
|
+
for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
50
|
+
const groupsDir = path.join(baseDir, 'groups');
|
|
51
|
+
if (!fs.existsSync(groupsDir))
|
|
52
|
+
continue;
|
|
53
|
+
let entries;
|
|
54
|
+
try {
|
|
55
|
+
entries = fs.readdirSync(groupsDir, { withFileTypes: true });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const entry of entries) {
|
|
61
|
+
if (!entry.isFile())
|
|
62
|
+
continue;
|
|
63
|
+
if (!entry.name.endsWith('.yml') && !entry.name.endsWith('.yaml'))
|
|
64
|
+
continue;
|
|
65
|
+
// User dir wins on filename collision.
|
|
66
|
+
const stem = entry.name.replace(/\.(yaml|yml)$/, '');
|
|
67
|
+
if (seenFiles.has(stem))
|
|
68
|
+
continue;
|
|
69
|
+
seenFiles.add(stem);
|
|
70
|
+
const filePath = path.join(groupsDir, entry.name);
|
|
71
|
+
let content;
|
|
72
|
+
try {
|
|
73
|
+
content = fs.readFileSync(filePath, 'utf-8');
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// Strict regex: optional whitespace, dash, quoted Computer(<id>).
|
|
79
|
+
// Only honors `allow:` lines — `deny:` Computer patterns would be a
|
|
80
|
+
// contradiction (everything is deny-by-default already).
|
|
81
|
+
let inAllow = false;
|
|
82
|
+
for (const rawLine of content.split('\n')) {
|
|
83
|
+
const line = rawLine.replace(/\r$/, '');
|
|
84
|
+
const sectionMatch = line.match(/^(\w+)\s*:\s*$/);
|
|
85
|
+
if (sectionMatch) {
|
|
86
|
+
inAllow = sectionMatch[1] === 'allow';
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (!inAllow)
|
|
90
|
+
continue;
|
|
91
|
+
const ruleMatch = line.match(/^\s*-\s*"Computer\(([^)]+)\)"\s*$/);
|
|
92
|
+
if (ruleMatch) {
|
|
93
|
+
const bundleId = ruleMatch[1].trim();
|
|
94
|
+
if (bundleId.length > 0)
|
|
95
|
+
allowed.add(bundleId);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...allowed].sort();
|
|
101
|
+
}
|
|
102
|
+
// Write the policy file the helper reads at startup and on SIGHUP.
|
|
103
|
+
// Mode 0600 — same lockdown as the socket (lives in the user-owned cache
|
|
104
|
+
// dir, but be explicit).
|
|
105
|
+
export function writeComputerPolicy(allowedBundleIds) {
|
|
106
|
+
const dir = getHelpersDir();
|
|
107
|
+
if (!fs.existsSync(dir)) {
|
|
108
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
109
|
+
}
|
|
110
|
+
const policy = { allow: allowedBundleIds };
|
|
111
|
+
fs.writeFileSync(resolvePolicyPath(), JSON.stringify(policy, null, 2), { mode: 0o600 });
|
|
112
|
+
}
|
|
113
|
+
// Resolve the helper executable inside the dist .app bundle. Used by the
|
|
114
|
+
// stdio fallback and by install-helper to find the source bundle.
|
|
115
|
+
export function resolveHelperExec() {
|
|
116
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
117
|
+
const candidates = [
|
|
118
|
+
// Local build (running from the agents-cli checkout).
|
|
119
|
+
path.resolve(here, '..', '..', 'packages', 'computer-helper', 'dist', 'ComputerHelper.app', 'Contents', 'MacOS', 'ComputerHelper'),
|
|
120
|
+
// Bundled with the npm package (later: CDN download lands here).
|
|
121
|
+
path.resolve(here, '..', 'computer-helper', 'ComputerHelper.app', 'Contents', 'MacOS', 'ComputerHelper'),
|
|
122
|
+
];
|
|
123
|
+
for (const c of candidates) {
|
|
124
|
+
if (fs.existsSync(c))
|
|
125
|
+
return c;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
// Resolve the dist .app bundle directory (not the inner exec).
|
|
130
|
+
export function resolveHelperApp() {
|
|
131
|
+
const exec = resolveHelperExec();
|
|
132
|
+
if (!exec)
|
|
133
|
+
return null;
|
|
134
|
+
// exec = <bundle>/Contents/MacOS/ComputerHelper
|
|
135
|
+
return path.resolve(exec, '..', '..', '..');
|
|
136
|
+
}
|
|
137
|
+
// Pick the best transport. If the socket exists, use it. Otherwise fall
|
|
138
|
+
// back to spawning the helper as a subprocess (legacy path).
|
|
139
|
+
export function openComputerClient() {
|
|
140
|
+
const sockPath = resolveSocketPath();
|
|
141
|
+
if (fs.existsSync(sockPath)) {
|
|
142
|
+
return new SocketClient(sockPath);
|
|
143
|
+
}
|
|
144
|
+
const helperExec = resolveHelperExec();
|
|
145
|
+
if (!helperExec) {
|
|
146
|
+
throw new Error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
|
|
147
|
+
}
|
|
148
|
+
return new StdioClient(helperExec);
|
|
149
|
+
}
|
|
150
|
+
// Shared waiter map + line parser. Both transports plug their reader into
|
|
151
|
+
// `handleChunk` and their writer into `send`.
|
|
152
|
+
class BaseClient {
|
|
153
|
+
buf = '';
|
|
154
|
+
waiters = new Map();
|
|
155
|
+
nextId = 1;
|
|
156
|
+
closed = false;
|
|
157
|
+
handleChunk(chunk) {
|
|
158
|
+
this.buf += chunk;
|
|
159
|
+
let nl;
|
|
160
|
+
while ((nl = this.buf.indexOf('\n')) !== -1) {
|
|
161
|
+
const line = this.buf.slice(0, nl);
|
|
162
|
+
this.buf = this.buf.slice(nl + 1);
|
|
163
|
+
if (!line)
|
|
164
|
+
continue;
|
|
165
|
+
try {
|
|
166
|
+
const obj = JSON.parse(line);
|
|
167
|
+
const id = typeof obj.id === 'number' ? obj.id : null;
|
|
168
|
+
if (id !== null && this.waiters.has(id)) {
|
|
169
|
+
const resolve = this.waiters.get(id);
|
|
170
|
+
this.waiters.delete(id);
|
|
171
|
+
resolve(obj);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
// Drop garbage; helper diagnostics go to stderr / log file.
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
failPending(code, message) {
|
|
180
|
+
for (const [id, resolve] of this.waiters) {
|
|
181
|
+
resolve({ id, error: { code, message } });
|
|
182
|
+
}
|
|
183
|
+
this.waiters.clear();
|
|
184
|
+
}
|
|
185
|
+
async call(method, params) {
|
|
186
|
+
if (this.closed) {
|
|
187
|
+
return { id: null, error: { code: 'helper_exited', message: 'helper not running' } };
|
|
188
|
+
}
|
|
189
|
+
const id = this.nextId++;
|
|
190
|
+
const payload = JSON.stringify({ id, method, params: params ?? {} }) + '\n';
|
|
191
|
+
return new Promise((resolve) => {
|
|
192
|
+
this.waiters.set(id, resolve);
|
|
193
|
+
this.send(payload);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
class SocketClient extends BaseClient {
|
|
198
|
+
sock;
|
|
199
|
+
constructor(socketPath) {
|
|
200
|
+
super();
|
|
201
|
+
this.sock = createConnection({ path: socketPath });
|
|
202
|
+
this.sock.setEncoding('utf8');
|
|
203
|
+
this.sock.on('data', (chunk) => this.handleChunk(chunk));
|
|
204
|
+
this.sock.on('error', (err) => {
|
|
205
|
+
this.closed = true;
|
|
206
|
+
this.failPending('socket_error', err.message);
|
|
207
|
+
});
|
|
208
|
+
this.sock.on('close', () => {
|
|
209
|
+
this.closed = true;
|
|
210
|
+
this.failPending('helper_exited', 'socket closed before reply');
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
send(payload) {
|
|
214
|
+
this.sock.write(payload);
|
|
215
|
+
}
|
|
216
|
+
async close() {
|
|
217
|
+
if (this.closed)
|
|
218
|
+
return;
|
|
219
|
+
this.sock.end();
|
|
220
|
+
await new Promise((resolve) => {
|
|
221
|
+
if (this.closed)
|
|
222
|
+
return resolve();
|
|
223
|
+
this.sock.on('close', () => resolve());
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
class StdioClient extends BaseClient {
|
|
228
|
+
proc;
|
|
229
|
+
constructor(helperPath) {
|
|
230
|
+
super();
|
|
231
|
+
this.proc = spawn(helperPath, [], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
232
|
+
this.proc.stdout.setEncoding('utf8');
|
|
233
|
+
this.proc.stdout.on('data', (chunk) => this.handleChunk(chunk));
|
|
234
|
+
this.proc.on('exit', () => {
|
|
235
|
+
this.closed = true;
|
|
236
|
+
this.failPending('helper_exited', 'helper exited before reply');
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
send(payload) {
|
|
240
|
+
this.proc.stdin.write(payload);
|
|
241
|
+
}
|
|
242
|
+
async close() {
|
|
243
|
+
if (this.closed)
|
|
244
|
+
return;
|
|
245
|
+
this.proc.stdin.end();
|
|
246
|
+
await new Promise((resolve) => {
|
|
247
|
+
if (this.closed)
|
|
248
|
+
return resolve();
|
|
249
|
+
this.proc.on('exit', () => resolve());
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Describe which transport is currently in use. Useful for diagnostics
|
|
254
|
+
// like `agents computer status`.
|
|
255
|
+
export function describeTransport() {
|
|
256
|
+
const sockPath = resolveSocketPath();
|
|
257
|
+
if (fs.existsSync(sockPath))
|
|
258
|
+
return { kind: 'socket', path: sockPath };
|
|
259
|
+
const exec = resolveHelperExec();
|
|
260
|
+
if (exec)
|
|
261
|
+
return { kind: 'stdio', path: exec };
|
|
262
|
+
return { kind: 'none', path: null };
|
|
263
|
+
}
|
package/dist/lib/daemon.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* (macOS), systemd (Linux), or as a plain detached process. PID tracking,
|
|
7
7
|
* log output, reload (SIGHUP), and graceful shutdown are handled here.
|
|
8
8
|
*/
|
|
9
|
-
import { spawn,
|
|
9
|
+
import { spawn, execFileSync } from 'child_process';
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import * as os from 'os';
|
|
@@ -286,7 +286,7 @@ function getAgentsBinPath() {
|
|
|
286
286
|
if (argv1 && fs.existsSync(argv1))
|
|
287
287
|
return argv1;
|
|
288
288
|
try {
|
|
289
|
-
return
|
|
289
|
+
return execFileSync('which', ['agents'], { encoding: 'utf-8' }).trim();
|
|
290
290
|
}
|
|
291
291
|
catch {
|
|
292
292
|
return 'agents';
|
|
@@ -348,9 +348,9 @@ function startDaemonLocked() {
|
|
|
348
348
|
fs.mkdirSync(unitDir, { recursive: true });
|
|
349
349
|
}
|
|
350
350
|
fs.writeFileSync(unitPath, generateSystemdUnit(), 'utf-8');
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
351
|
+
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
352
|
+
execFileSync('systemctl', ['--user', 'enable', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
353
|
+
execFileSync('systemctl', ['--user', 'start', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
354
354
|
const pid = waitForPid(3000);
|
|
355
355
|
return { pid, method: 'systemd' };
|
|
356
356
|
}
|
|
@@ -402,8 +402,8 @@ export function stopDaemon() {
|
|
|
402
402
|
}
|
|
403
403
|
if (platform === 'linux') {
|
|
404
404
|
try {
|
|
405
|
-
|
|
406
|
-
|
|
405
|
+
execFileSync('systemctl', ['--user', 'stop', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
406
|
+
execFileSync('systemctl', ['--user', 'disable', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
407
407
|
}
|
|
408
408
|
catch (err) {
|
|
409
409
|
if (process.env.AGENTS_DEBUG) {
|
package/dist/lib/exec.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentId } from './types.js';
|
|
2
2
|
/** Agent execution modes controlling tool access and autonomy level. */
|
|
3
|
-
export type ExecMode = 'plan' | 'edit' | 'full';
|
|
3
|
+
export type ExecMode = 'plan' | 'edit' | 'full' | 'auto';
|
|
4
4
|
/** Reasoning effort levels passed to agents that support them. 'auto' defers to the agent's default. */
|
|
5
5
|
export type ExecEffort = 'low' | 'medium' | 'high' | 'xhigh' | 'max' | 'auto';
|
|
6
6
|
/** Options for spawning an agent process. Omitting `prompt` launches the CLI interactively. */
|
|
@@ -39,6 +39,7 @@ export interface AgentCommandTemplate {
|
|
|
39
39
|
plan: string[];
|
|
40
40
|
edit: string[];
|
|
41
41
|
full: string[];
|
|
42
|
+
auto?: string[];
|
|
42
43
|
};
|
|
43
44
|
jsonFlags?: string[];
|
|
44
45
|
modelFlag?: string;
|
package/dist/lib/exec.js
CHANGED
|
@@ -86,6 +86,7 @@ export const AGENT_COMMANDS = {
|
|
|
86
86
|
plan: ['--permission-mode', 'plan'],
|
|
87
87
|
edit: ['--permission-mode', 'acceptEdits'],
|
|
88
88
|
full: ['--dangerously-skip-permissions'],
|
|
89
|
+
auto: ['--permission-mode', 'auto'],
|
|
89
90
|
},
|
|
90
91
|
jsonFlags: ['--output-format', 'stream-json', '--verbose'],
|
|
91
92
|
modelFlag: '--model',
|
|
@@ -226,8 +227,8 @@ export function buildExecCommand(options) {
|
|
|
226
227
|
}
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
|
-
// Add mode flags
|
|
230
|
-
const modeFlags = template.modeFlags[options.mode];
|
|
230
|
+
// Add mode flags. 'auto' is only defined for claude; other agents fall back to edit flags.
|
|
231
|
+
const modeFlags = template.modeFlags[options.mode] ?? template.modeFlags.edit;
|
|
231
232
|
cmd.push(...modeFlags);
|
|
232
233
|
// Add print/headless flags only when a prompt is provided. Without a prompt
|
|
233
234
|
// the caller wants an interactive REPL -- passing --print would immediately
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export declare function sleepSync(ms: number): void;
|
|
2
|
+
/**
|
|
3
|
+
* Ensures the target file (and its parent directory) exist so proper-lockfile
|
|
4
|
+
* can create a sibling .lock directory. Created with flag 'wx' so concurrent
|
|
5
|
+
* creation races are safe (EEXIST is swallowed).
|
|
6
|
+
*/
|
|
7
|
+
export declare function ensureLockTarget(filePath: string, initialContent?: string, dirMode?: number): void;
|
|
8
|
+
/**
|
|
9
|
+
* Writes content to filePath via a temp file + rename so readers never see a
|
|
10
|
+
* partial write. On POSIX, rename(2) is atomic.
|
|
11
|
+
*/
|
|
12
|
+
export declare function atomicWriteFileSync(filePath: string, content: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Acquires an exclusive proper-lockfile lock on filePath, runs fn, then
|
|
15
|
+
* releases the lock. Retries up to LOCK_RETRIES times with linear back-off.
|
|
16
|
+
* Breaks stale locks older than LOCK_STALE_MS.
|
|
17
|
+
*/
|
|
18
|
+
export declare function withFileLock<T>(filePath: string, fn: () => T): T;
|