@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.
Files changed (64) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/dist/commands/browser.js +248 -9
  3. package/dist/commands/cloud.js +8 -0
  4. package/dist/commands/exec.js +70 -1
  5. package/dist/commands/import.d.ts +24 -0
  6. package/dist/commands/import.js +203 -0
  7. package/dist/commands/plugins.js +179 -5
  8. package/dist/commands/prune.js +6 -0
  9. package/dist/commands/secrets.js +117 -19
  10. package/dist/commands/view.js +21 -8
  11. package/dist/commands/workflows.d.ts +10 -0
  12. package/dist/commands/workflows.js +457 -0
  13. package/dist/index.js +34 -16
  14. package/dist/lib/browser/cdp.js +7 -4
  15. package/dist/lib/browser/chrome.d.ts +10 -0
  16. package/dist/lib/browser/chrome.js +37 -2
  17. package/dist/lib/browser/drivers/local.js +13 -2
  18. package/dist/lib/browser/input.d.ts +1 -0
  19. package/dist/lib/browser/input.js +3 -0
  20. package/dist/lib/browser/ipc.js +14 -0
  21. package/dist/lib/browser/profiles.d.ts +5 -0
  22. package/dist/lib/browser/profiles.js +45 -0
  23. package/dist/lib/browser/service.d.ts +10 -0
  24. package/dist/lib/browser/service.js +29 -1
  25. package/dist/lib/browser/types.d.ts +11 -1
  26. package/dist/lib/cloud/rush.d.ts +28 -1
  27. package/dist/lib/cloud/rush.js +68 -13
  28. package/dist/lib/commands.d.ts +0 -15
  29. package/dist/lib/commands.js +5 -5
  30. package/dist/lib/hooks.js +24 -11
  31. package/dist/lib/import.d.ts +91 -0
  32. package/dist/lib/import.js +179 -0
  33. package/dist/lib/migrate.js +59 -1
  34. package/dist/lib/permissions.d.ts +0 -58
  35. package/dist/lib/permissions.js +10 -10
  36. package/dist/lib/plugins.d.ts +75 -34
  37. package/dist/lib/plugins.js +640 -133
  38. package/dist/lib/resource-patterns.d.ts +41 -0
  39. package/dist/lib/resource-patterns.js +82 -0
  40. package/dist/lib/resources/index.d.ts +17 -0
  41. package/dist/lib/resources/index.js +7 -0
  42. package/dist/lib/resources/types.d.ts +1 -1
  43. package/dist/lib/resources/workflows.d.ts +24 -0
  44. package/dist/lib/resources/workflows.js +110 -0
  45. package/dist/lib/resources.d.ts +6 -1
  46. package/dist/lib/resources.js +12 -2
  47. package/dist/lib/session/db.d.ts +18 -0
  48. package/dist/lib/session/db.js +106 -7
  49. package/dist/lib/session/discover.d.ts +6 -0
  50. package/dist/lib/session/discover.js +28 -17
  51. package/dist/lib/shims.d.ts +3 -51
  52. package/dist/lib/shims.js +18 -10
  53. package/dist/lib/sqlite.js +10 -4
  54. package/dist/lib/state.d.ts +15 -2
  55. package/dist/lib/state.js +29 -8
  56. package/dist/lib/types.d.ts +43 -14
  57. package/dist/lib/versions.d.ts +3 -0
  58. package/dist/lib/versions.js +139 -27
  59. package/dist/lib/workflows.d.ts +79 -0
  60. package/dist/lib/workflows.js +233 -0
  61. package/package.json +1 -5
  62. package/scripts/postinstall.js +59 -58
  63. package/dist/commands/fork.d.ts +0 -10
  64. 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 (commands/skills/hooks) and/or older duplicate version installs
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 agents:
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 injected at spawn
122
+ secrets Keychain-backed env bundles; use 'secrets exec <bundle> -- <cmd>' to inject into a subprocess
117
123
 
118
- Helpers:
119
- beta Enable preview features like drive and factory
120
- pty Drive interactive terminal programs (REPLs, TUIs)
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);
@@ -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
- comet: ['comet'],
106
- brave: ['brave', 'brave-browser'],
107
- edge: ['edge', 'microsoft-edge', 'msedge'],
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
- ...(options.viewport ? [`--window-size=${options.viewport.width},${options.viewport.height}`] : []),
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, allocatePort } from '../chrome.js';
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
- const newPort = allocatePort();
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);
@@ -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[];
@@ -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;