@phnx-labs/agents-cli 1.16.0 → 1.17.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 +71 -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/import.d.ts +24 -0
- package/dist/commands/import.js +203 -0
- 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 +34 -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/import.d.ts +91 -0
- package/dist/lib/import.js +179 -0
- 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
package/dist/index.js
CHANGED
|
@@ -36,6 +36,7 @@ import { registerRulesCommands } from './commands/rules.js';
|
|
|
36
36
|
import { registerPermissionsCommands } from './commands/permissions.js';
|
|
37
37
|
import { registerMcpCommands } from './commands/mcp.js';
|
|
38
38
|
import { registerVersionsCommands } from './commands/versions.js';
|
|
39
|
+
import { registerImportCommand } from './commands/import.js';
|
|
39
40
|
import { registerPackagesCommands } from './commands/packages.js';
|
|
40
41
|
import { registerDaemonCommands } from './commands/daemon.js';
|
|
41
42
|
import { registerRoutinesCommands } from './commands/routines.js';
|
|
@@ -46,6 +47,7 @@ import { registerTrashCommands } from './commands/trash.js';
|
|
|
46
47
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
47
48
|
import { registerSubagentsCommands } from './commands/subagents.js';
|
|
48
49
|
import { registerPluginsCommands } from './commands/plugins.js';
|
|
50
|
+
import { registerWorkflowsCommands } from './commands/workflows.js';
|
|
49
51
|
import { registerSyncCommand } from './commands/sync.js';
|
|
50
52
|
import { registerRefreshRulesCommand } from './commands/refresh-rules.js';
|
|
51
53
|
import { registerDriveCommands } from './commands/drive.js';
|
|
@@ -60,8 +62,8 @@ import { registerBetaCommands } from './commands/beta.js';
|
|
|
60
62
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
61
63
|
import { isPromptCancelled } from './commands/utils.js';
|
|
62
64
|
import { AGENTS } from './lib/agents.js';
|
|
63
|
-
import { getGlobalDefault } from './lib/versions.js';
|
|
64
|
-
import { addShimsToPath, ensureShimCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
|
|
65
|
+
import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
|
|
66
|
+
import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
|
|
65
67
|
const program = new Command();
|
|
66
68
|
program
|
|
67
69
|
.name('agents')
|
|
@@ -86,9 +88,11 @@ Quick start:
|
|
|
86
88
|
|
|
87
89
|
Agent versions:
|
|
88
90
|
add <agent>[@version] Install an agent CLI (e.g. agents add codex)
|
|
91
|
+
import <agent> Adopt an existing global install (npm/homebrew) into agents-cli
|
|
89
92
|
remove <agent>[@version] Uninstall a version
|
|
90
93
|
use <agent>@<version> Set the default version
|
|
91
|
-
prune [target] Remove orphan resources
|
|
94
|
+
prune [target] Remove orphan resources and older duplicate version installs (targets: commands, sessions, runs, trash)
|
|
95
|
+
trash Inspect and restore soft-deleted version directories
|
|
92
96
|
view [agent[@version]] List versions, or inspect one in detail
|
|
93
97
|
|
|
94
98
|
Agent configuration (synced across versions):
|
|
@@ -97,7 +101,7 @@ Agent configuration (synced across versions):
|
|
|
97
101
|
skills Knowledge packs (SKILL.md + supporting files)
|
|
98
102
|
mcp MCP servers (stdio or HTTP)
|
|
99
103
|
permissions Allow/deny rules for tool calls
|
|
100
|
-
hooks Shell scripts that run on agent events
|
|
104
|
+
hooks Shell scripts that run on agent events (hooks.yaml in agents.yaml)
|
|
101
105
|
subagents Named sub-agent definitions
|
|
102
106
|
plugins Bundles of skills, hooks, and scripts
|
|
103
107
|
|
|
@@ -105,19 +109,30 @@ Packages:
|
|
|
105
109
|
search <query> Find MCP servers and skills in registries
|
|
106
110
|
install <pkg> Install from registry (mcp:name, skill:user/repo)
|
|
107
111
|
|
|
108
|
-
Run
|
|
112
|
+
Run and dispatch:
|
|
109
113
|
run <agent|profile> [prompt] Run an agent. Omit prompt for interactive mode.
|
|
110
114
|
teams Coordinate multiple agents on shared work
|
|
111
115
|
routines Run agents on a cron schedule (scheduler auto-starts)
|
|
112
|
-
sessions Browse and replay past runs
|
|
116
|
+
sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
|
|
117
|
+
browser Automate a browser — navigate, click, screenshot, console, network
|
|
118
|
+
pty Drive interactive terminal programs (REPLs, TUIs) via a persistent PTY session
|
|
113
119
|
|
|
114
|
-
Credentials:
|
|
120
|
+
Credentials and profiles:
|
|
115
121
|
profiles Bundles of (host CLI, endpoint, model, auth)
|
|
116
|
-
secrets Keychain-backed env bundles
|
|
122
|
+
secrets Keychain-backed env bundles; use 'secrets exec <bundle> -- <cmd>' to inject into a subprocess
|
|
117
123
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
124
|
+
Diagnostics:
|
|
125
|
+
doctor [agent[@version]] Diagnose CLI availability, sync status, and resource divergence
|
|
126
|
+
usage [agent] Show rate-limit and quota usage per agent
|
|
127
|
+
|
|
128
|
+
Config sync:
|
|
129
|
+
drive Sync session history across machines via rsync
|
|
130
|
+
pull Clone or pull the system repo at ~/.agents-system/
|
|
131
|
+
repo init --path <dir> Scaffold your own editable repo from a template
|
|
132
|
+
repo add <path|gh:user/repo> Merge an extra repo after the system repo
|
|
133
|
+
|
|
134
|
+
Beta features:
|
|
135
|
+
beta Enable preview features (factory, drive, and more)
|
|
121
136
|
|
|
122
137
|
Automation tips:
|
|
123
138
|
Pass explicit names/IDs Avoid pickers: agents sessions <id> --markdown
|
|
@@ -126,11 +141,6 @@ Automation tips:
|
|
|
126
141
|
Use agent@version targets e.g. --agents claude@2.1.79,codex@default
|
|
127
142
|
Non-TTY shells apply defaults Omitted required selections fail with a plain hint
|
|
128
143
|
|
|
129
|
-
Config sync (portable setup via git):
|
|
130
|
-
pull Clone or pull the system repo at ~/.agents-system/
|
|
131
|
-
repo init --path <dir> Scaffold your own editable repo from a template
|
|
132
|
-
repo add <path|gh:user/repo> Merge an extra repo after the system repo
|
|
133
|
-
|
|
134
144
|
Options:
|
|
135
145
|
-V, --version Show version number
|
|
136
146
|
-h, --help Show help
|
|
@@ -335,6 +345,12 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
|
|
|
335
345
|
if (status !== 'current') {
|
|
336
346
|
createdOrUpdated.push(`${status === 'created' ? 'Created' : 'Updated'} ${AGENTS[agent].cliCommand} shim`);
|
|
337
347
|
}
|
|
348
|
+
for (const version of listInstalledVersions(agent)) {
|
|
349
|
+
const aliasStatus = ensureVersionedAliasCurrent(agent, version);
|
|
350
|
+
if (aliasStatus !== 'current') {
|
|
351
|
+
createdOrUpdated.push(`${aliasStatus === 'created' ? 'Created' : 'Updated'} ${AGENTS[agent].cliCommand}@${version} alias`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
338
354
|
}
|
|
339
355
|
for (const notice of createdOrUpdated) {
|
|
340
356
|
console.log(chalk.green(notice));
|
|
@@ -444,7 +460,9 @@ program
|
|
|
444
460
|
registerMcpCommands(program);
|
|
445
461
|
registerSubagentsCommands(program);
|
|
446
462
|
registerPluginsCommands(program);
|
|
463
|
+
registerWorkflowsCommands(program);
|
|
447
464
|
registerVersionsCommands(program);
|
|
465
|
+
registerImportCommand(program);
|
|
448
466
|
registerPackagesCommands(program);
|
|
449
467
|
registerDaemonCommands(program);
|
|
450
468
|
registerRoutinesCommands(program);
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -101,10 +101,13 @@ export function verifyBrowserIdentity(reported, expected, port, host = 'localhos
|
|
|
101
101
|
return;
|
|
102
102
|
const matches = {
|
|
103
103
|
chrome: ['chrome', 'google-chrome', 'headlesschrome'],
|
|
104
|
-
chromium: ['chromium', 'headlesschrome'],
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
chromium: ['chromium', 'headlesschrome', 'chrome'],
|
|
105
|
+
// Comet reports itself as plain "Chrome/<version>" in /json/version — it
|
|
106
|
+
// doesn't override the Chromium branding. Accept chrome here so attaching
|
|
107
|
+
// to a Comet instance doesn't trip a false "identity mismatch".
|
|
108
|
+
comet: ['comet', 'chrome'],
|
|
109
|
+
brave: ['brave', 'brave-browser', 'chrome'],
|
|
110
|
+
edge: ['edge', 'microsoft-edge', 'msedge', 'chrome'],
|
|
108
111
|
};
|
|
109
112
|
const accepted = matches[expected] || [expected];
|
|
110
113
|
if (accepted.includes(reported))
|
|
@@ -14,3 +14,13 @@ export declare function getRunningChromeInfo(profileName: string): {
|
|
|
14
14
|
port: number;
|
|
15
15
|
} | null;
|
|
16
16
|
export declare function allocatePort(): number;
|
|
17
|
+
export interface PortOccupant {
|
|
18
|
+
pid: number;
|
|
19
|
+
command: string;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Identify the process listening on a TCP port via lsof. Returns null when nothing is bound.
|
|
23
|
+
* Used for clearer error messages when a profile's configured port is taken by a non-debug
|
|
24
|
+
* process (e.g. Comet running without --remote-debugging-port).
|
|
25
|
+
*/
|
|
26
|
+
export declare function getPortOccupant(port: number): PortOccupant | null;
|
|
@@ -69,6 +69,7 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
69
69
|
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
70
70
|
const userDataDir = path.join(runtimeDir, 'chrome-data');
|
|
71
71
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
72
|
+
const viewport = options.viewport ?? { width: 1512, height: 982 };
|
|
72
73
|
const args = [
|
|
73
74
|
`--remote-debugging-port=${port}`,
|
|
74
75
|
`--user-data-dir=${userDataDir}`,
|
|
@@ -77,7 +78,10 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
77
78
|
'--disable-backgrounding-occluded-windows',
|
|
78
79
|
'--disable-renderer-backgrounding',
|
|
79
80
|
...(options.headless ? ['--headless=new'] : []),
|
|
80
|
-
|
|
81
|
+
`--window-size=${viewport.width},${viewport.height}`,
|
|
82
|
+
...(viewport.x !== undefined && viewport.y !== undefined
|
|
83
|
+
? [`--window-position=${viewport.x},${viewport.y}`]
|
|
84
|
+
: []),
|
|
81
85
|
...(options.args || []),
|
|
82
86
|
];
|
|
83
87
|
let env = { ...process.env };
|
|
@@ -150,7 +154,11 @@ function isProcessRunning(pid) {
|
|
|
150
154
|
process.kill(pid, 0);
|
|
151
155
|
return true;
|
|
152
156
|
}
|
|
153
|
-
catch {
|
|
157
|
+
catch (err) {
|
|
158
|
+
// EPERM means the process exists but we lack permission to signal it —
|
|
159
|
+
// treat as alive. ESRCH means the process does not exist.
|
|
160
|
+
if (err && err.code === 'EPERM')
|
|
161
|
+
return true;
|
|
154
162
|
return false;
|
|
155
163
|
}
|
|
156
164
|
}
|
|
@@ -170,3 +178,30 @@ export function allocatePort() {
|
|
|
170
178
|
}
|
|
171
179
|
throw new Error('No available ports in range 9200-9300');
|
|
172
180
|
}
|
|
181
|
+
/**
|
|
182
|
+
* Identify the process listening on a TCP port via lsof. Returns null when nothing is bound.
|
|
183
|
+
* Used for clearer error messages when a profile's configured port is taken by a non-debug
|
|
184
|
+
* process (e.g. Comet running without --remote-debugging-port).
|
|
185
|
+
*/
|
|
186
|
+
export function getPortOccupant(port) {
|
|
187
|
+
try {
|
|
188
|
+
const out = execSync(`lsof -nP -iTCP:${port} -sTCP:LISTEN -Fpcn`, {
|
|
189
|
+
encoding: 'utf8',
|
|
190
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
191
|
+
});
|
|
192
|
+
let pid = 0;
|
|
193
|
+
let command = '';
|
|
194
|
+
for (const line of out.split('\n')) {
|
|
195
|
+
if (line.startsWith('p'))
|
|
196
|
+
pid = parseInt(line.slice(1), 10) || 0;
|
|
197
|
+
else if (line.startsWith('c') && !command)
|
|
198
|
+
command = line.slice(1);
|
|
199
|
+
}
|
|
200
|
+
if (!pid)
|
|
201
|
+
return null;
|
|
202
|
+
return { pid, command: command || 'unknown' };
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -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;
|