@phnx-labs/agents-cli 1.16.0 → 1.17.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 +65 -0
- package/dist/commands/browser.js +248 -9
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/exec.js +70 -1
- package/dist/commands/plugins.js +179 -5
- package/dist/commands/prune.js +6 -0
- package/dist/commands/secrets.js +117 -19
- package/dist/commands/view.js +21 -8
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.js +31 -16
- package/dist/lib/browser/cdp.js +7 -4
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +37 -2
- package/dist/lib/browser/drivers/local.js +13 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +14 -0
- package/dist/lib/browser/profiles.d.ts +5 -0
- package/dist/lib/browser/profiles.js +45 -0
- package/dist/lib/browser/service.d.ts +10 -0
- package/dist/lib/browser/service.js +29 -1
- package/dist/lib/browser/types.d.ts +11 -1
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +68 -13
- package/dist/lib/commands.d.ts +0 -15
- package/dist/lib/commands.js +5 -5
- package/dist/lib/hooks.js +24 -11
- package/dist/lib/migrate.js +59 -1
- package/dist/lib/permissions.d.ts +0 -58
- package/dist/lib/permissions.js +10 -10
- package/dist/lib/plugins.d.ts +75 -34
- package/dist/lib/plugins.js +640 -133
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +106 -7
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +28 -17
- package/dist/lib/shims.d.ts +3 -51
- package/dist/lib/shims.js +18 -10
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +15 -2
- package/dist/lib/state.js +29 -8
- package/dist/lib/types.d.ts +43 -14
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +139 -27
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +59 -58
- package/dist/commands/fork.d.ts +0 -10
- package/dist/commands/fork.js +0 -146
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
|
|
2
|
-
import { launchBrowser,
|
|
2
|
+
import { launchBrowser, getPortOccupant } from '../chrome.js';
|
|
3
3
|
export async function connectLocal(endpoint, profile) {
|
|
4
4
|
const url = new URL(endpoint);
|
|
5
5
|
if (url.protocol !== 'cdp:') {
|
|
@@ -17,7 +17,18 @@ export async function connectLocal(endpoint, profile) {
|
|
|
17
17
|
if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
|
|
18
18
|
throw err;
|
|
19
19
|
}
|
|
20
|
-
|
|
20
|
+
// Distinguish "nothing listening on this port" (fine to launch fresh) from
|
|
21
|
+
// "something is listening but it's not a debuggable browser" (bail loudly —
|
|
22
|
+
// silently launching on a different port leads to confusing `pid 0` and
|
|
23
|
+
// `CDP connection not open` errors downstream).
|
|
24
|
+
const occupant = getPortOccupant(port);
|
|
25
|
+
if (occupant) {
|
|
26
|
+
throw new Error(`Port ${port} is occupied by ${occupant.command} (pid ${occupant.pid}) but is ` +
|
|
27
|
+
`not serving the Chrome DevTools Protocol. Either stop that process ` +
|
|
28
|
+
`(\`kill ${occupant.pid}\`) or restart it with \`--remote-debugging-port=${port}\` ` +
|
|
29
|
+
`so profile "${profile.name}" can attach.`);
|
|
30
|
+
}
|
|
31
|
+
const newPort = port;
|
|
21
32
|
const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
|
|
22
33
|
const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary);
|
|
23
34
|
const cdp = new CDPClient();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CDPClient } from './cdp.js';
|
|
2
2
|
export declare function clickAtCoords(cdp: CDPClient, sessionId: string, x: number, y: number): Promise<void>;
|
|
3
3
|
export declare function hoverAtCoords(cdp: CDPClient, sessionId: string, x: number, y: number): Promise<void>;
|
|
4
|
+
export declare function scrollAtCoords(cdp: CDPClient, sessionId: string, x: number, y: number, deltaX: number, deltaY: number): Promise<void>;
|
|
4
5
|
export declare function typeText(cdp: CDPClient, sessionId: string, text: string): Promise<void>;
|
|
5
6
|
export declare function pressKey(cdp: CDPClient, sessionId: string, keyName: string): Promise<void>;
|
|
6
7
|
export declare function focusNode(cdp: CDPClient, sessionId: string, backendNodeId: number): Promise<void>;
|
|
@@ -5,6 +5,9 @@ export async function clickAtCoords(cdp, sessionId, x, y) {
|
|
|
5
5
|
export async function hoverAtCoords(cdp, sessionId, x, y) {
|
|
6
6
|
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y }, sessionId);
|
|
7
7
|
}
|
|
8
|
+
export async function scrollAtCoords(cdp, sessionId, x, y, deltaX, deltaY) {
|
|
9
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseWheel', x, y, deltaX, deltaY }, sessionId);
|
|
10
|
+
}
|
|
8
11
|
export async function typeText(cdp, sessionId, text) {
|
|
9
12
|
for (const char of text) {
|
|
10
13
|
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', text: char }, sessionId);
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -78,6 +78,13 @@ export class BrowserIPCServer {
|
|
|
78
78
|
windowTargetId: result.windowId,
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
|
+
case 'launch-profile': {
|
|
82
|
+
if (!request.profile) {
|
|
83
|
+
return { ok: false, error: 'Profile required' };
|
|
84
|
+
}
|
|
85
|
+
const result = await this.service.launchProfile(request.profile);
|
|
86
|
+
return { ok: true, port: result.port, pid: result.pid };
|
|
87
|
+
}
|
|
81
88
|
case 'done': {
|
|
82
89
|
if (!request.task) {
|
|
83
90
|
return { ok: false, error: 'Task required' };
|
|
@@ -191,6 +198,13 @@ export class BrowserIPCServer {
|
|
|
191
198
|
await this.service.hover(request.task, request.ref, request.tabId);
|
|
192
199
|
return { ok: true };
|
|
193
200
|
}
|
|
201
|
+
case 'scroll': {
|
|
202
|
+
if (!request.task) {
|
|
203
|
+
return { ok: false, error: 'Task required' };
|
|
204
|
+
}
|
|
205
|
+
await this.service.scroll(request.task, request.scrollX ?? 0, request.scrollY ?? 0, request.scrollAtX, request.scrollAtY, request.tabId);
|
|
206
|
+
return { ok: true };
|
|
207
|
+
}
|
|
194
208
|
case 'set-viewport': {
|
|
195
209
|
if (!request.task || !request.width || !request.height) {
|
|
196
210
|
return { ok: false, error: 'Task, width, and height required' };
|
|
@@ -4,6 +4,11 @@ export declare function getBrowserRuntimeDir(): string;
|
|
|
4
4
|
export declare function getProfileRuntimeDir(name: string): string;
|
|
5
5
|
export declare function listProfiles(): Promise<BrowserProfile[]>;
|
|
6
6
|
export declare function getProfile(name: string): Promise<BrowserProfile | null>;
|
|
7
|
+
/**
|
|
8
|
+
* Find a port in 9222–9399 that is not already claimed by another profile
|
|
9
|
+
* and is not currently in use by any OS process.
|
|
10
|
+
*/
|
|
11
|
+
export declare function findFreeProfilePort(): Promise<number>;
|
|
7
12
|
export declare function createProfile(profile: BrowserProfile): Promise<void>;
|
|
8
13
|
export declare function updateProfile(profile: BrowserProfile): Promise<void>;
|
|
9
14
|
export declare function deleteProfile(name: string): Promise<void>;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
|
+
import { execSync } from 'child_process';
|
|
2
3
|
import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, readMeta, writeMeta, } from '../state.js';
|
|
4
|
+
import { findBrowserPath } from './chrome.js';
|
|
3
5
|
export function getBrowserRuntimeDir() {
|
|
4
6
|
return getBrowserRuntimeDirRoot();
|
|
5
7
|
}
|
|
@@ -51,11 +53,54 @@ export async function getProfile(name) {
|
|
|
51
53
|
return null;
|
|
52
54
|
return configToProfile(name, config);
|
|
53
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* Find a port in 9222–9399 that is not already claimed by another profile
|
|
58
|
+
* and is not currently in use by any OS process.
|
|
59
|
+
*/
|
|
60
|
+
export async function findFreeProfilePort() {
|
|
61
|
+
const profiles = await listProfiles();
|
|
62
|
+
const usedByProfile = new Set();
|
|
63
|
+
for (const p of profiles) {
|
|
64
|
+
const port = extractConfiguredPort(p);
|
|
65
|
+
if (port !== undefined)
|
|
66
|
+
usedByProfile.add(port);
|
|
67
|
+
}
|
|
68
|
+
for (let port = 9222; port <= 9399; port++) {
|
|
69
|
+
if (usedByProfile.has(port))
|
|
70
|
+
continue;
|
|
71
|
+
try {
|
|
72
|
+
execSync(`lsof -i :${port}`, { stdio: 'ignore' });
|
|
73
|
+
// lsof succeeded → something is listening → port is in use
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// lsof threw → nothing on this port → it's free
|
|
77
|
+
return port;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
throw new Error('No available ports in range 9222-9399');
|
|
81
|
+
}
|
|
54
82
|
export async function createProfile(profile) {
|
|
55
83
|
const meta = readMeta();
|
|
56
84
|
if (meta.browser?.[profile.name]) {
|
|
57
85
|
throw new Error(`Profile "${profile.name}" already exists`);
|
|
58
86
|
}
|
|
87
|
+
// Check for port collision with existing profiles
|
|
88
|
+
const newPort = extractConfiguredPort(profile);
|
|
89
|
+
if (newPort !== undefined && meta.browser) {
|
|
90
|
+
for (const [existingName, existingConfig] of Object.entries(meta.browser)) {
|
|
91
|
+
const existingProfile = configToProfile(existingName, existingConfig);
|
|
92
|
+
const existingPort = extractConfiguredPort(existingProfile);
|
|
93
|
+
if (existingPort === newPort) {
|
|
94
|
+
throw new Error(`Port ${newPort} is already used by profile "${existingName}". ` +
|
|
95
|
+
`Each profile must own a unique port. Use a different port or omit --endpoint to auto-assign.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Resolve the browser binary at create time. Fails fast with an actionable
|
|
100
|
+
// error ("Comet not installed at /Applications/Comet.app") rather than
|
|
101
|
+
// deferring the failure to the first task. `findBrowserPath` short-circuits
|
|
102
|
+
// for browser=custom without a binary by throwing — same outcome.
|
|
103
|
+
findBrowserPath(profile.browser, profile.binary);
|
|
59
104
|
meta.browser = meta.browser ?? {};
|
|
60
105
|
meta.browser[profile.name] = profileToConfig(profile);
|
|
61
106
|
writeMeta(meta);
|
|
@@ -17,6 +17,15 @@ export declare class BrowserService {
|
|
|
17
17
|
tabId?: string;
|
|
18
18
|
windowId?: string;
|
|
19
19
|
}>;
|
|
20
|
+
/**
|
|
21
|
+
* Launch (or attach to) the profile's browser without creating a task. Used by
|
|
22
|
+
* `agents browser profiles launch <name>` so users can warm up the browser —
|
|
23
|
+
* including the first-run onboarding flow — before any automation starts.
|
|
24
|
+
*/
|
|
25
|
+
launchProfile(profileName: string): Promise<{
|
|
26
|
+
port: number;
|
|
27
|
+
pid: number;
|
|
28
|
+
}>;
|
|
20
29
|
stop(taskName: string): Promise<{
|
|
21
30
|
ok: boolean;
|
|
22
31
|
profile?: string;
|
|
@@ -60,6 +69,7 @@ export declare class BrowserService {
|
|
|
60
69
|
type(taskId: string, ref: number, text: string, tabHint?: string): Promise<void>;
|
|
61
70
|
press(taskId: string, key: string, tabHint?: string): Promise<void>;
|
|
62
71
|
hover(taskId: string, ref: number, tabHint?: string): Promise<void>;
|
|
72
|
+
scroll(taskId: string, deltaX: number, deltaY: number, atX?: number, atY?: number, tabHint?: string): Promise<void>;
|
|
63
73
|
status(profileName?: string): Promise<ProfileStatus[]>;
|
|
64
74
|
private reconcileFromDisk;
|
|
65
75
|
setViewport(taskId: string, width: number, height: number, options?: {
|
|
@@ -7,7 +7,7 @@ import { connectLocal } from './drivers/local.js';
|
|
|
7
7
|
import { connectSSH } from './drivers/ssh.js';
|
|
8
8
|
import { generateTaskId, generateShortId, generateFunName, } from './types.js';
|
|
9
9
|
import { getRefs, resolveRefToCoords } from './refs.js';
|
|
10
|
-
import { clickAtCoords, hoverAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
10
|
+
import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
11
11
|
import { emit } from '../events.js';
|
|
12
12
|
export class BrowserService {
|
|
13
13
|
connections = new Map();
|
|
@@ -97,6 +97,24 @@ export class BrowserService {
|
|
|
97
97
|
}
|
|
98
98
|
return { task: taskId, name: taskName, tabId };
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Launch (or attach to) the profile's browser without creating a task. Used by
|
|
102
|
+
* `agents browser profiles launch <name>` so users can warm up the browser —
|
|
103
|
+
* including the first-run onboarding flow — before any automation starts.
|
|
104
|
+
*/
|
|
105
|
+
async launchProfile(profileName) {
|
|
106
|
+
const profile = await getProfile(profileName);
|
|
107
|
+
if (!profile) {
|
|
108
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
109
|
+
}
|
|
110
|
+
let conn = this.connections.get(profileName);
|
|
111
|
+
if (!conn) {
|
|
112
|
+
conn = await this.connectProfile(profile);
|
|
113
|
+
this.connections.set(profileName, conn);
|
|
114
|
+
}
|
|
115
|
+
emit('browser.launch', { profile: profileName, task: '', pid: conn.pid });
|
|
116
|
+
return { port: conn.port, pid: conn.pid };
|
|
117
|
+
}
|
|
100
118
|
async stop(taskName) {
|
|
101
119
|
for (const [profileName, conn] of this.connections) {
|
|
102
120
|
const task = conn.tasks.get(taskName);
|
|
@@ -424,6 +442,16 @@ export class BrowserService {
|
|
|
424
442
|
const { x, y } = await resolveRefToCoords(conn.cdp, sessionId, nodeMap, ref);
|
|
425
443
|
await hoverAtCoords(conn.cdp, sessionId, x, y);
|
|
426
444
|
}
|
|
445
|
+
async scroll(taskId, deltaX, deltaY, atX, atY, tabHint) {
|
|
446
|
+
const { conn, task } = await this.findTask(taskId);
|
|
447
|
+
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
448
|
+
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
449
|
+
const target = await this.getTarget(conn, cdpTargetId);
|
|
450
|
+
if (!target)
|
|
451
|
+
throw new Error(`Tab ${shortId} not found`);
|
|
452
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
453
|
+
await scrollAtCoords(conn.cdp, sessionId, atX ?? 0, atY ?? 0, deltaX, deltaY);
|
|
454
|
+
}
|
|
427
455
|
async status(profileName) {
|
|
428
456
|
const seen = new Set();
|
|
429
457
|
const statuses = [];
|
|
@@ -11,6 +11,8 @@ export interface BrowserProfile {
|
|
|
11
11
|
viewport?: {
|
|
12
12
|
width: number;
|
|
13
13
|
height: number;
|
|
14
|
+
x?: number;
|
|
15
|
+
y?: number;
|
|
14
16
|
};
|
|
15
17
|
}
|
|
16
18
|
export interface ChromeOptions {
|
|
@@ -19,6 +21,8 @@ export interface ChromeOptions {
|
|
|
19
21
|
viewport?: {
|
|
20
22
|
width: number;
|
|
21
23
|
height: number;
|
|
24
|
+
x?: number;
|
|
25
|
+
y?: number;
|
|
22
26
|
};
|
|
23
27
|
}
|
|
24
28
|
export interface Task {
|
|
@@ -69,7 +73,7 @@ export interface HistoricalTask {
|
|
|
69
73
|
domains: string[];
|
|
70
74
|
tabCount: number;
|
|
71
75
|
}
|
|
72
|
-
export type IPCAction = 'start' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download';
|
|
76
|
+
export type IPCAction = 'start' | 'launch-profile' | 'done' | 'stop' | 'status' | 'history' | 'navigate' | 'tab-add' | 'tab-focus' | 'tab-close' | 'tab-list' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover' | 'scroll' | 'set-viewport' | 'set-device' | 'console' | 'errors' | 'requests' | 'response-body' | 'wait' | 'set-download-path' | 'wait-download';
|
|
73
77
|
export interface IPCRequest {
|
|
74
78
|
action: IPCAction;
|
|
75
79
|
task?: string;
|
|
@@ -82,6 +86,10 @@ export interface IPCRequest {
|
|
|
82
86
|
ref?: number;
|
|
83
87
|
text?: string;
|
|
84
88
|
key?: string;
|
|
89
|
+
scrollX?: number;
|
|
90
|
+
scrollY?: number;
|
|
91
|
+
scrollAtX?: number;
|
|
92
|
+
scrollAtY?: number;
|
|
85
93
|
interactive?: boolean;
|
|
86
94
|
limit?: number;
|
|
87
95
|
width?: number;
|
|
@@ -111,6 +119,8 @@ export interface IPCResponse {
|
|
|
111
119
|
result?: unknown;
|
|
112
120
|
path?: string;
|
|
113
121
|
refs?: string;
|
|
122
|
+
port?: number;
|
|
123
|
+
pid?: number;
|
|
114
124
|
logs?: ConsoleEntry[];
|
|
115
125
|
errors?: ErrorEntry[];
|
|
116
126
|
requests?: NetworkRequest[];
|
package/dist/lib/cloud/rush.d.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* Requires the Rush GitHub App installed on the target repo.
|
|
6
6
|
*/
|
|
7
7
|
import type { CloudProvider, CloudTask, CloudTaskStatus, CloudEvent, DispatchOptions, ProviderCapabilities } from './types.js';
|
|
8
|
+
export declare const RUSH_CONSENT_PATH: string;
|
|
9
|
+
export declare function hasRushUploadConsent(opts?: DispatchOptions): boolean;
|
|
8
10
|
/** One version's entry in the account manifest sent on every dispatch. */
|
|
9
11
|
export interface AccountManifestEntry {
|
|
10
12
|
version: string;
|
|
@@ -35,7 +37,7 @@ export interface AccountTokenEntry {
|
|
|
35
37
|
* Returns null when no Claude versions are signed in (the dispatch falls back
|
|
36
38
|
* to the platform-wide key, current behavior).
|
|
37
39
|
*/
|
|
38
|
-
export declare function buildAccountManifest(): Promise<AccountManifest | null>;
|
|
40
|
+
export declare function buildAccountManifest(strategy?: string): Promise<AccountManifest | null>;
|
|
39
41
|
/**
|
|
40
42
|
* Re-load OAuth blobs for the given versions so they can be uploaded to the
|
|
41
43
|
* server on a retry. Only the versions named in the manifest are loaded — we
|
|
@@ -52,6 +54,7 @@ export declare function buildDispatchBody(input: {
|
|
|
52
54
|
agent?: string;
|
|
53
55
|
prompt: string;
|
|
54
56
|
mode?: string;
|
|
57
|
+
strategy?: string;
|
|
55
58
|
resolvedRepos: Array<{
|
|
56
59
|
installation_id: number;
|
|
57
60
|
repo_owner: string;
|
|
@@ -60,6 +63,30 @@ export declare function buildDispatchBody(input: {
|
|
|
60
63
|
accountManifest?: AccountManifest | null;
|
|
61
64
|
accountTokens?: AccountTokenEntry[] | null;
|
|
62
65
|
}): Record<string, unknown>;
|
|
66
|
+
/** A single account registered in Rush Cloud's multi-account rotation pool. */
|
|
67
|
+
export interface RemoteAccount {
|
|
68
|
+
id: string;
|
|
69
|
+
provider: string;
|
|
70
|
+
email: string | null;
|
|
71
|
+
subscription_type: string | null;
|
|
72
|
+
five_hour_pct: number | null;
|
|
73
|
+
seven_day_pct: number | null;
|
|
74
|
+
usage_fetched_at: string | null;
|
|
75
|
+
created_at: string;
|
|
76
|
+
}
|
|
77
|
+
/** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
|
|
78
|
+
export declare function listRemoteAccounts(): Promise<RemoteAccount[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
|
|
81
|
+
* The server validates the token against the Anthropic usage API and stores it
|
|
82
|
+
* encrypted in Vault. Returns the account metadata (no token).
|
|
83
|
+
*/
|
|
84
|
+
export declare function addRemoteAccount(provider: string, pastedToken: string): Promise<RemoteAccount & {
|
|
85
|
+
five_hour_pct: number | null;
|
|
86
|
+
seven_day_pct: number | null;
|
|
87
|
+
}>;
|
|
88
|
+
/** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
|
|
89
|
+
export declare function removeRemoteAccount(id: string): Promise<void>;
|
|
63
90
|
export declare class RushCloudProvider implements CloudProvider {
|
|
64
91
|
id: "rush";
|
|
65
92
|
name: string;
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -15,14 +15,15 @@ import { parseSSE } from './stream.js';
|
|
|
15
15
|
import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
16
16
|
import { getAccountInfo } from '../agents.js';
|
|
17
17
|
import { loadClaudeOauth } from '../usage.js';
|
|
18
|
+
import { selectBalancedVersion } from '../rotate.js';
|
|
18
19
|
const PROXY_BASE = 'https://api.prix.dev';
|
|
19
20
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
20
21
|
// Persistent consent record for uploading Claude OAuth blobs to Rush Cloud.
|
|
21
22
|
// Created on first explicit consent (env var or flag); subsequent dispatches
|
|
22
23
|
// see it and proceed without re-prompting.
|
|
23
|
-
const RUSH_CONSENT_PATH = path.join(getCloudDir(), 'rush-consent.json');
|
|
24
|
+
export const RUSH_CONSENT_PATH = path.join(getCloudDir(), 'rush-consent.json');
|
|
24
25
|
const RUSH_CONSENT_ENV = 'AGENTS_RUSH_UPLOAD_TOKENS';
|
|
25
|
-
function hasRushUploadConsent(opts) {
|
|
26
|
+
export function hasRushUploadConsent(opts) {
|
|
26
27
|
if (process.env[RUSH_CONSENT_ENV] === '1')
|
|
27
28
|
return true;
|
|
28
29
|
const po = opts?.providerOptions;
|
|
@@ -184,20 +185,37 @@ async function readClaudeCredentialsBlob(home) {
|
|
|
184
185
|
* Returns null when no Claude versions are signed in (the dispatch falls back
|
|
185
186
|
* to the platform-wide key, current behavior).
|
|
186
187
|
*/
|
|
187
|
-
export async function buildAccountManifest() {
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
188
|
+
export async function buildAccountManifest(strategy) {
|
|
189
|
+
let candidateVersions;
|
|
190
|
+
if (strategy === 'balanced') {
|
|
191
|
+
// Use the same health-checked, deduped-by-email set that `agents run --balanced` uses.
|
|
192
|
+
// `result.healthy` contains one candidate per unique email, ordered by remaining capacity.
|
|
193
|
+
const result = await selectBalancedVersion('claude');
|
|
194
|
+
if (!result || result.healthy.length === 0)
|
|
195
|
+
return null;
|
|
196
|
+
candidateVersions = result.healthy
|
|
197
|
+
.filter((c) => !!c.email)
|
|
198
|
+
.map((c) => ({ version: c.version, email: c.email }));
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
// Default: all installed versions that have a signed-in account.
|
|
202
|
+
const versions = listInstalledVersions('claude');
|
|
203
|
+
if (versions.length === 0)
|
|
204
|
+
return null;
|
|
205
|
+
const rows = await Promise.all(versions.map(async (version) => {
|
|
206
|
+
const home = getVersionHomePath('claude', version);
|
|
207
|
+
const info = await getAccountInfo('claude', home);
|
|
208
|
+
return info.email ? { version, email: info.email } : null;
|
|
209
|
+
}));
|
|
210
|
+
candidateVersions = rows.filter((r) => r !== null);
|
|
211
|
+
}
|
|
191
212
|
const entries = [];
|
|
192
|
-
for (const version of
|
|
213
|
+
for (const { version, email } of candidateVersions) {
|
|
193
214
|
const home = getVersionHomePath('claude', version);
|
|
194
|
-
const info = await getAccountInfo('claude', home);
|
|
195
|
-
if (!info.email)
|
|
196
|
-
continue;
|
|
197
215
|
const blob = await readClaudeCredentialsBlob(home);
|
|
198
216
|
if (!blob)
|
|
199
217
|
continue;
|
|
200
|
-
entries.push({ version, email
|
|
218
|
+
entries.push({ version, email, cred_fp: sha256(blob) });
|
|
201
219
|
}
|
|
202
220
|
if (entries.length === 0)
|
|
203
221
|
return null;
|
|
@@ -237,6 +255,7 @@ export function buildDispatchBody(input) {
|
|
|
237
255
|
prompt: input.prompt,
|
|
238
256
|
repos: input.resolvedRepos,
|
|
239
257
|
mode: input.mode,
|
|
258
|
+
...(input.strategy ? { strategy: input.strategy } : {}),
|
|
240
259
|
};
|
|
241
260
|
if (input.resolvedRepos.length === 1) {
|
|
242
261
|
body.installation_id = primary.installation_id;
|
|
@@ -251,6 +270,37 @@ export function buildDispatchBody(input) {
|
|
|
251
270
|
}
|
|
252
271
|
return body;
|
|
253
272
|
}
|
|
273
|
+
/** Fetch all Claude accounts in this user's Rush Cloud rotation pool (no tokens). */
|
|
274
|
+
export async function listRemoteAccounts() {
|
|
275
|
+
const token = readToken();
|
|
276
|
+
const res = await api('GET', '/api/v1/cloud-accounts', token);
|
|
277
|
+
if (!res.ok) {
|
|
278
|
+
throw new Error(`Failed to list accounts (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
279
|
+
}
|
|
280
|
+
const data = await res.json();
|
|
281
|
+
return data.accounts ?? [];
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Register a CLAUDE_CODE_OAUTH_TOKEN with Rush Cloud's rotation pool.
|
|
285
|
+
* The server validates the token against the Anthropic usage API and stores it
|
|
286
|
+
* encrypted in Vault. Returns the account metadata (no token).
|
|
287
|
+
*/
|
|
288
|
+
export async function addRemoteAccount(provider, pastedToken) {
|
|
289
|
+
const token = readToken();
|
|
290
|
+
const res = await api('POST', '/api/v1/cloud-accounts', token, { provider, token: pastedToken });
|
|
291
|
+
if (!res.ok) {
|
|
292
|
+
throw new Error(`Failed to add account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
293
|
+
}
|
|
294
|
+
return await res.json();
|
|
295
|
+
}
|
|
296
|
+
/** Remove a Claude account from Rush Cloud's rotation pool by its ID. */
|
|
297
|
+
export async function removeRemoteAccount(id) {
|
|
298
|
+
const token = readToken();
|
|
299
|
+
const res = await api('DELETE', `/api/v1/cloud-accounts/${encodeURIComponent(id)}`, token);
|
|
300
|
+
if (!res.ok) {
|
|
301
|
+
throw new Error(`Failed to remove account (${res.status}): ${sanitizeErrorBody(await res.text())}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
254
304
|
export class RushCloudProvider {
|
|
255
305
|
id = 'rush';
|
|
256
306
|
name = 'Rush Cloud';
|
|
@@ -289,13 +339,18 @@ export class RushCloudProvider {
|
|
|
289
339
|
repo_owner: r.owner,
|
|
290
340
|
repo_name: r.name,
|
|
291
341
|
})));
|
|
292
|
-
const
|
|
342
|
+
const strategy = options.providerOptions?.strategy;
|
|
343
|
+
// When balanced, the server owns the pool and rotates internally — no
|
|
344
|
+
// client-side manifest needed. We just forward the strategy so the server
|
|
345
|
+
// knows to load from Vault instead of waiting for a manifest.
|
|
346
|
+
const accountManifest = strategy === 'balanced' ? null : await buildAccountManifest();
|
|
293
347
|
const body = buildDispatchBody({
|
|
294
348
|
agent: options.agent,
|
|
295
349
|
prompt: options.prompt,
|
|
296
350
|
mode: options.providerOptions?.mode,
|
|
297
351
|
resolvedRepos,
|
|
298
352
|
accountManifest,
|
|
353
|
+
strategy,
|
|
299
354
|
});
|
|
300
355
|
let res = await api('POST', '/api/v1/cloud-runs', token, body);
|
|
301
356
|
// Server detects drift (new account or rotated token) by comparing the
|
|
@@ -317,7 +372,7 @@ export class RushCloudProvider {
|
|
|
317
372
|
``,
|
|
318
373
|
`To consent, re-run with one of:`,
|
|
319
374
|
` AGENTS_RUSH_UPLOAD_TOKENS=1 agents cloud run ...`,
|
|
320
|
-
` agents cloud run --upload-account-tokens
|
|
375
|
+
` agents cloud run --upload-account-tokens ...`,
|
|
321
376
|
``,
|
|
322
377
|
`Consent will be recorded at ${RUSH_CONSENT_PATH} so you won't be asked again.`,
|
|
323
378
|
`Remove that file to revoke.`,
|
package/dist/lib/commands.d.ts
CHANGED
|
@@ -35,10 +35,6 @@ export interface InstalledCommand {
|
|
|
35
35
|
path: string;
|
|
36
36
|
description?: string;
|
|
37
37
|
}
|
|
38
|
-
/** Parse command metadata (name, description) from YAML frontmatter or TOML headers. */
|
|
39
|
-
export declare function parseCommandMetadata(filePath: string): CommandMetadata | null;
|
|
40
|
-
/** Validate command metadata, returning errors and warnings. */
|
|
41
|
-
export declare function validateCommandMetadata(metadata: CommandMetadata | null, commandName: string): ValidationResult;
|
|
42
38
|
/** Discover all command markdown files in a repository's commands/ directory. */
|
|
43
39
|
export declare function discoverCommands(repoPath: string): DiscoveredCommand[];
|
|
44
40
|
/** Find the source path for a command in a repository. */
|
|
@@ -98,17 +94,6 @@ export declare function iterCommandsCapableVersions(filter?: {
|
|
|
98
94
|
}>;
|
|
99
95
|
/** Remove a command from an agent's config directory. */
|
|
100
96
|
export declare function uninstallCommand(agentId: AgentId, commandName: string): boolean;
|
|
101
|
-
/** List command names installed for an agent in the active version home. */
|
|
102
|
-
export declare function listInstalledCommands(agentId: AgentId): string[];
|
|
103
|
-
/**
|
|
104
|
-
* Check if a command exists for an agent.
|
|
105
|
-
*/
|
|
106
|
-
export declare function commandExists(agentId: AgentId, commandName: string): boolean;
|
|
107
|
-
/**
|
|
108
|
-
* Check if installed command content matches source content.
|
|
109
|
-
* Handles format conversion (markdown to TOML for Gemini).
|
|
110
|
-
*/
|
|
111
|
-
export declare function commandContentMatches(agentId: AgentId, commandName: string, sourcePath: string): boolean;
|
|
112
97
|
/**
|
|
113
98
|
* List installed commands with scope information.
|
|
114
99
|
* Pass options.home to read from a version-managed agent's home directory.
|
package/dist/lib/commands.js
CHANGED
|
@@ -15,7 +15,7 @@ import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAge
|
|
|
15
15
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
16
16
|
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
17
17
|
/** Parse command metadata (name, description) from YAML frontmatter or TOML headers. */
|
|
18
|
-
|
|
18
|
+
function parseCommandMetadata(filePath) {
|
|
19
19
|
if (!fs.existsSync(filePath)) {
|
|
20
20
|
return null;
|
|
21
21
|
}
|
|
@@ -51,7 +51,7 @@ export function parseCommandMetadata(filePath) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
/** Validate command metadata, returning errors and warnings. */
|
|
54
|
-
|
|
54
|
+
function validateCommandMetadata(metadata, commandName) {
|
|
55
55
|
const errors = [];
|
|
56
56
|
const warnings = [];
|
|
57
57
|
if (!metadata) {
|
|
@@ -336,7 +336,7 @@ export function uninstallCommand(agentId, commandName) {
|
|
|
336
336
|
return false;
|
|
337
337
|
}
|
|
338
338
|
/** List command names installed for an agent in the active version home. */
|
|
339
|
-
|
|
339
|
+
function listInstalledCommands(agentId) {
|
|
340
340
|
const agent = AGENTS[agentId];
|
|
341
341
|
const home = getEffectiveHome(agentId);
|
|
342
342
|
const commandsDir = path.join(home, `.${agentId}`, agent.commandsSubdir);
|
|
@@ -352,7 +352,7 @@ export function listInstalledCommands(agentId) {
|
|
|
352
352
|
/**
|
|
353
353
|
* Check if a command exists for an agent.
|
|
354
354
|
*/
|
|
355
|
-
|
|
355
|
+
function commandExists(agentId, commandName) {
|
|
356
356
|
const agent = AGENTS[agentId];
|
|
357
357
|
const home = getEffectiveHome(agentId);
|
|
358
358
|
const commandsDir = path.join(home, `.${agentId}`, agent.commandsSubdir);
|
|
@@ -370,7 +370,7 @@ function normalizeContent(content) {
|
|
|
370
370
|
* Check if installed command content matches source content.
|
|
371
371
|
* Handles format conversion (markdown to TOML for Gemini).
|
|
372
372
|
*/
|
|
373
|
-
|
|
373
|
+
function commandContentMatches(agentId, commandName, sourcePath) {
|
|
374
374
|
const agent = AGENTS[agentId];
|
|
375
375
|
const home = getEffectiveHome(agentId);
|
|
376
376
|
const commandsDir = path.join(home, `.${agentId}`, agent.commandsSubdir);
|
package/dist/lib/hooks.js
CHANGED
|
@@ -14,16 +14,15 @@ import * as TOML from 'smol-toml';
|
|
|
14
14
|
import { AGENTS, HOOKS_CAPABLE_AGENTS } from './agents.js';
|
|
15
15
|
import { supports, explainSkip } from './capabilities.js';
|
|
16
16
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
17
|
-
import { getHooksDir as getSystemHooksDir, getUserHooksDir, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getTrashHooksDir } from './state.js';
|
|
17
|
+
import { getHooksDir as getSystemHooksDir, getUserHooksDir, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getTrashHooksDir, getEnabledExtraRepos } from './state.js';
|
|
18
18
|
function getCentralHooksDir() { return getUserHooksDir(); }
|
|
19
19
|
/**
|
|
20
|
-
* Resolve a hook script's absolute path
|
|
21
|
-
*
|
|
22
|
-
* system dir (where npm-shipped defaults live). Returns null if neither
|
|
23
|
-
* exists. Mirrors the precedence used by `listCentralHooks`.
|
|
20
|
+
* Resolve a hook script's absolute path. Checks user dir first, then enabled
|
|
21
|
+
* extra repos in insertion order, then system dir. Returns null if not found.
|
|
24
22
|
*/
|
|
25
23
|
function resolveHookScriptPath(script) {
|
|
26
|
-
|
|
24
|
+
const extraDirs = getEnabledExtraRepos().map(e => e.dir);
|
|
25
|
+
for (const root of [getUserAgentsDir(), ...extraDirs, getSystemAgentsDir()]) {
|
|
27
26
|
const candidate = path.join(root, 'hooks', script);
|
|
28
27
|
if (fs.existsSync(candidate))
|
|
29
28
|
return candidate;
|
|
@@ -32,14 +31,15 @@ function resolveHookScriptPath(script) {
|
|
|
32
31
|
}
|
|
33
32
|
/**
|
|
34
33
|
* Prefixes used for stale-entry cleanup in agent settings files. A registered
|
|
35
|
-
* hook command is considered "managed
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* collected on rewrite.
|
|
34
|
+
* hook command is considered "managed" if it lives under any known hooks dir
|
|
35
|
+
* (user, extra repos, or system). Entries from removed extra repos are also
|
|
36
|
+
* garbage-collected because they won't appear in this list any more.
|
|
39
37
|
*/
|
|
40
38
|
function getManagedHookPrefixes() {
|
|
39
|
+
const extraDirs = getEnabledExtraRepos().map(e => e.dir);
|
|
41
40
|
return [
|
|
42
41
|
path.join(getUserAgentsDir(), 'hooks') + path.sep,
|
|
42
|
+
...extraDirs.map(d => path.join(d, 'hooks') + path.sep),
|
|
43
43
|
path.join(getSystemAgentsDir(), 'hooks') + path.sep,
|
|
44
44
|
];
|
|
45
45
|
}
|
|
@@ -616,16 +616,29 @@ export function registerHooksToSettings(agentId, versionHome, hookManifest, agen
|
|
|
616
616
|
return { registered: [], errors: [] };
|
|
617
617
|
}
|
|
618
618
|
const overrideRoots = agentsDirOverride ? [agentsDirOverride] : null;
|
|
619
|
+
// Scripts are copied into the version home during sync — prefer that stable
|
|
620
|
+
// local path so registered commands don't break when source dirs change.
|
|
621
|
+
const localHooksDir = !overrideRoots
|
|
622
|
+
? path.join(versionHome, `.${agentId}`, AGENTS[agentId].hooksDir)
|
|
623
|
+
: null;
|
|
619
624
|
const resolveScript = (script) => {
|
|
620
625
|
if (overrideRoots) {
|
|
621
626
|
const candidate = path.join(overrideRoots[0], 'hooks', script);
|
|
622
627
|
return fs.existsSync(candidate) ? candidate : null;
|
|
623
628
|
}
|
|
629
|
+
if (localHooksDir) {
|
|
630
|
+
const local = path.join(localHooksDir, script);
|
|
631
|
+
if (fs.existsSync(local))
|
|
632
|
+
return local;
|
|
633
|
+
}
|
|
624
634
|
return resolveHookScriptPath(script);
|
|
625
635
|
};
|
|
626
636
|
const managedPrefixes = overrideRoots
|
|
627
637
|
? [path.join(overrideRoots[0], 'hooks') + path.sep]
|
|
628
|
-
:
|
|
638
|
+
: [
|
|
639
|
+
...getManagedHookPrefixes(),
|
|
640
|
+
...(localHooksDir ? [localHooksDir + path.sep] : []),
|
|
641
|
+
];
|
|
629
642
|
if (agentId === 'claude') {
|
|
630
643
|
return registerHooksForClaude(versionHome, manifest, resolveScript, managedPrefixes);
|
|
631
644
|
}
|