@phnx-labs/agents-cli 1.14.7 → 1.16.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.
Files changed (105) hide show
  1. package/CHANGELOG.md +78 -39
  2. package/README.md +74 -7
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/beta.js +6 -1
  5. package/dist/commands/browser-picker.d.ts +21 -0
  6. package/dist/commands/browser-picker.js +114 -0
  7. package/dist/commands/browser.js +546 -75
  8. package/dist/commands/commands.js +72 -22
  9. package/dist/commands/daemon.js +2 -2
  10. package/dist/commands/exec.js +9 -2
  11. package/dist/commands/fork.js +2 -2
  12. package/dist/commands/hooks.js +71 -26
  13. package/dist/commands/mcp.js +85 -43
  14. package/dist/commands/plugins.js +48 -15
  15. package/dist/commands/prune.d.ts +0 -20
  16. package/dist/commands/prune.js +291 -16
  17. package/dist/commands/pull.js +3 -3
  18. package/dist/commands/repo.js +1 -1
  19. package/dist/commands/routines.js +2 -2
  20. package/dist/commands/secrets.js +37 -1
  21. package/dist/commands/sessions.js +62 -19
  22. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  23. package/dist/commands/{init.js → setup.js} +32 -21
  24. package/dist/commands/skills.js +60 -19
  25. package/dist/commands/subagents.js +41 -13
  26. package/dist/commands/teams.js +2 -3
  27. package/dist/commands/usage.js +6 -0
  28. package/dist/commands/utils.d.ts +16 -0
  29. package/dist/commands/utils.js +32 -0
  30. package/dist/commands/versions.js +8 -6
  31. package/dist/commands/view.js +61 -16
  32. package/dist/index.d.ts +1 -1
  33. package/dist/index.js +17 -20
  34. package/dist/lib/agents.js +2 -2
  35. package/dist/lib/auto-pull-worker.js +2 -3
  36. package/dist/lib/auto-pull.js +2 -2
  37. package/dist/lib/browser/cdp.d.ts +7 -1
  38. package/dist/lib/browser/cdp.js +29 -1
  39. package/dist/lib/browser/chrome.js +6 -3
  40. package/dist/lib/browser/devices.d.ts +4 -0
  41. package/dist/lib/browser/devices.js +27 -0
  42. package/dist/lib/browser/drivers/local.js +9 -4
  43. package/dist/lib/browser/drivers/ssh.d.ts +1 -0
  44. package/dist/lib/browser/drivers/ssh.js +32 -4
  45. package/dist/lib/browser/ipc.js +145 -23
  46. package/dist/lib/browser/profiles.d.ts +5 -2
  47. package/dist/lib/browser/profiles.js +77 -37
  48. package/dist/lib/browser/service.d.ts +84 -13
  49. package/dist/lib/browser/service.js +806 -122
  50. package/dist/lib/browser/types.d.ts +81 -3
  51. package/dist/lib/browser/types.js +16 -0
  52. package/dist/lib/cloud/rush.js +2 -2
  53. package/dist/lib/cloud/store.js +2 -2
  54. package/dist/lib/commands.d.ts +1 -0
  55. package/dist/lib/commands.js +6 -2
  56. package/dist/lib/daemon.js +6 -7
  57. package/dist/lib/doctor-diff.js +4 -4
  58. package/dist/lib/events.d.ts +94 -1
  59. package/dist/lib/events.js +264 -6
  60. package/dist/lib/exec.js +16 -10
  61. package/dist/lib/hooks.d.ts +11 -7
  62. package/dist/lib/hooks.js +125 -49
  63. package/dist/lib/migrate.d.ts +1 -1
  64. package/dist/lib/migrate.js +1178 -21
  65. package/dist/lib/models.js +2 -2
  66. package/dist/lib/permissions.d.ts +14 -11
  67. package/dist/lib/permissions.js +46 -42
  68. package/dist/lib/plugins.d.ts +30 -1
  69. package/dist/lib/plugins.js +75 -3
  70. package/dist/lib/pty-server.js +9 -10
  71. package/dist/lib/resources/hooks.d.ts +5 -1
  72. package/dist/lib/resources/hooks.js +21 -4
  73. package/dist/lib/rotate.js +3 -4
  74. package/dist/lib/routines.d.ts +15 -0
  75. package/dist/lib/routines.js +68 -0
  76. package/dist/lib/runner.js +9 -5
  77. package/dist/lib/secrets/index.d.ts +14 -11
  78. package/dist/lib/secrets/index.js +49 -21
  79. package/dist/lib/secrets/linux.d.ts +27 -0
  80. package/dist/lib/secrets/linux.js +161 -0
  81. package/dist/lib/session/active.d.ts +3 -0
  82. package/dist/lib/session/active.js +92 -6
  83. package/dist/lib/session/cloud.js +2 -2
  84. package/dist/lib/session/db.d.ts +4 -0
  85. package/dist/lib/session/db.js +34 -3
  86. package/dist/lib/session/discover.js +30 -15
  87. package/dist/lib/session/team-filter.js +2 -2
  88. package/dist/lib/shims.d.ts +2 -2
  89. package/dist/lib/shims.js +6 -6
  90. package/dist/lib/skills.js +6 -2
  91. package/dist/lib/state.d.ts +86 -14
  92. package/dist/lib/state.js +150 -23
  93. package/dist/lib/subagents.d.ts +28 -0
  94. package/dist/lib/subagents.js +98 -1
  95. package/dist/lib/sync-manifest.d.ts +1 -1
  96. package/dist/lib/sync-manifest.js +3 -3
  97. package/dist/lib/teams/persistence.js +15 -5
  98. package/dist/lib/teams/registry.js +2 -2
  99. package/dist/lib/types.d.ts +32 -3
  100. package/dist/lib/types.js +3 -3
  101. package/dist/lib/usage.d.ts +1 -1
  102. package/dist/lib/usage.js +15 -48
  103. package/dist/lib/versions.js +31 -21
  104. package/package.json +1 -1
  105. package/scripts/postinstall.js +37 -9
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * CLI entry point for agents-cli.
4
4
  *
5
5
  * Registers all commands, handles update checks, auto-corrects typos,
6
- * and launches the first-run interactive init when appropriate.
6
+ * and launches the first-run interactive setup when appropriate.
7
7
  */
8
8
  import { Command } from 'commander';
9
9
  import chalk from 'chalk';
@@ -26,7 +26,7 @@ const VERSION = packageJson.version;
26
26
  // Import command registrations
27
27
  import { registerPullCommand } from './commands/pull.js';
28
28
  import { registerRepoCommands } from './commands/repo.js';
29
- import { registerInitCommand, runInit } from './commands/init.js';
29
+ import { registerSetupCommand, runSetup } from './commands/setup.js';
30
30
  import { registerStatusCommand } from './commands/status.js';
31
31
  import { registerViewCommand } from './commands/view.js';
32
32
  import { registerCommandsCommands } from './commands/commands.js';
@@ -59,7 +59,6 @@ import { registerAliasCommand } from './commands/alias.js';
59
59
  import { registerBetaCommands } from './commands/beta.js';
60
60
  import { applyGlobalHelpConventions } from './lib/help.js';
61
61
  import { isPromptCancelled } from './commands/utils.js';
62
- import { getAgentsDir } from './lib/state.js';
63
62
  import { AGENTS } from './lib/agents.js';
64
63
  import { getGlobalDefault } from './lib/versions.js';
65
64
  import { addShimsToPath, ensureShimCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
@@ -80,7 +79,7 @@ Install, configure, run, and dispatch AI coding agents from one place.
80
79
  Works with Claude, Codex, Gemini, Cursor, OpenCode, OpenClaw, and Droid.
81
80
 
82
81
  Quick start:
83
- agents init First-time setup (interactive)
82
+ agents setup First-time setup (interactive)
84
83
  agents view See what's installed
85
84
  agents run <agent> ["prompt"] Run an agent (interactive without prompt, headless with)
86
85
  agents sessions Browse past sessions across all agents
@@ -199,7 +198,8 @@ async function showWhatsNew(fromVersion, toVersion) {
199
198
  }
200
199
  }
201
200
  const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
202
- const UPDATE_CHECK_FILE = path.join(getAgentsDir(), '.update-check');
201
+ import { getUpdateCheckPath, getMigratedSentinelPath, getUserAgentsDir } from './lib/state.js';
202
+ const UPDATE_CHECK_FILE = getUpdateCheckPath();
203
203
  /** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
204
204
  function readUpdateCache() {
205
205
  try {
@@ -526,7 +526,7 @@ program
526
526
  });
527
527
  registerPullCommand(program);
528
528
  registerRepoCommands(program);
529
- registerInitCommand(program);
529
+ registerSetupCommand(program);
530
530
  applyGlobalHelpConventions(program);
531
531
  /** Calculate the Levenshtein edit distance between two strings. */
532
532
  function levenshtein(a, b) {
@@ -582,7 +582,7 @@ await checkForUpdates();
582
582
  // status marker that we'll print on the *next* invocation.
583
583
  const { spawnDetachedSync } = await import('./lib/auto-pull.js');
584
584
  spawnDetachedSync();
585
- // First-run experience: no args + no config yet + TTY -> launch interactive init.
585
+ // First-run experience: no args + no config yet + TTY -> launch interactive setup.
586
586
  // Skipped when stdin/stdout isn't a terminal (CI, pipes) or when user passes any args.
587
587
  const passedArgs = process.argv.slice(2);
588
588
  const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
@@ -613,14 +613,14 @@ async function registerLazyCommands() {
613
613
  }
614
614
  }
615
615
  await registerLazyCommands();
616
- const metaFilePath = path.join(getAgentsDir(), 'agents.yaml');
616
+ const metaFilePath = path.join(getUserAgentsDir(), 'agents.yaml');
617
617
  const firstRun = passedArgs.length === 0 &&
618
618
  !fs.existsSync(metaFilePath) &&
619
619
  process.stdin.isTTY &&
620
620
  process.stdout.isTTY;
621
621
  if (firstRun) {
622
622
  try {
623
- await runInit(program);
623
+ await runSetup(program);
624
624
  }
625
625
  catch (err) {
626
626
  if (!(err instanceof Error && err.name === 'ExitPromptError')) {
@@ -629,14 +629,11 @@ if (firstRun) {
629
629
  }
630
630
  process.exit(0);
631
631
  }
632
- // Commands that require the system repo to be cloned first.
633
- const SYSTEM_REPO_COMMANDS = new Set([
634
- 'view', 'status', 'skills', 'rules', 'commands', 'hooks',
635
- 'mcp', 'permissions', 'versions', 'packages', 'sync',
636
- 'subagents', 'repo', 'plugins', 'doctor',
637
- ]);
638
- if (!firstRun && requestedCommand && SYSTEM_REPO_COMMANDS.has(requestedCommand)) {
639
- const { ensureInitialized } = await import('./commands/init.js');
632
+ // Every command requires the system repo to be cloned first. `setup` is the
633
+ // only exemption it's the command that does the cloning.
634
+ const SETUP_EXEMPT_COMMANDS = new Set(['setup', 'help']);
635
+ if (!firstRun && requestedCommand && !SETUP_EXEMPT_COMMANDS.has(requestedCommand)) {
636
+ const { ensureInitialized } = await import('./commands/setup.js');
640
637
  await ensureInitialized(program);
641
638
  }
642
639
  // One-shot idempotent migrations (split-layout, legacy file moves).
@@ -648,8 +645,8 @@ if (!firstRun && requestedCommand && SYSTEM_REPO_COMMANDS.has(requestedCommand))
648
645
  if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
649
646
  try {
650
647
  const { runMigration } = await import('./lib/migrate.js');
651
- const sentinel = path.join(getAgentsDir(), '.migrated');
652
- const sentinelValue = `${VERSION}-v2`;
648
+ const sentinel = getMigratedSentinelPath();
649
+ const sentinelValue = `${VERSION}-v8`;
653
650
  let needRun = true;
654
651
  try {
655
652
  if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
@@ -658,7 +655,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
658
655
  }
659
656
  catch { /* best-effort — fall through to run */ }
660
657
  if (needRun) {
661
- runMigration();
658
+ await runMigration();
662
659
  try {
663
660
  fs.mkdirSync(path.dirname(sentinel), { recursive: true });
664
661
  fs.writeFileSync(sentinel, sentinelValue);
@@ -16,7 +16,7 @@ import * as os from 'os';
16
16
  import * as TOML from 'smol-toml';
17
17
  import chalk from 'chalk';
18
18
  import { walkForFiles } from './fs-walk.js';
19
- import { getVersionsDir, getShimsDir, getAgentsDir } from './state.js';
19
+ import { getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
20
20
  import { resolveVersion, getVersionHomePath, getBinaryPath } from './versions.js';
21
21
  import { loadClaudeOauth } from './usage.js';
22
22
  const execFileAsync = promisify(execFile);
@@ -29,7 +29,7 @@ const HOME = os.homedir();
29
29
  export const CODEX_HOOKS_MIN_VERSION = '0.116.0';
30
30
  /** Minimum Gemini CLI version that supports the hooks system (v0.26.0, Jan 2026). */
31
31
  export const GEMINI_HOOKS_MIN_VERSION = '0.26.0';
32
- const CLI_VERSION_CACHE_PATH = path.join(getAgentsDir(), '.cli-version-cache.json');
32
+ const CLI_VERSION_CACHE_PATH = getCliVersionCachePath();
33
33
  let cliVersionCache = null;
34
34
  function loadCliVersionCache() {
35
35
  if (cliVersionCache)
@@ -9,14 +9,13 @@
9
9
  * Lock mtime under 5 min => skip (another invocation already in flight).
10
10
  */
11
11
  import * as fs from 'fs';
12
- import * as path from 'path';
13
12
  import { simpleGit } from 'simple-git';
14
13
  import { tryAutoPull, isGitRepo } from './git.js';
15
- import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos, } from './state.js';
14
+ import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos, getFetchCacheDir, } from './state.js';
16
15
  import { lockFilePath, statusFilePath } from './auto-pull.js';
17
16
  const LOCK_TTL_MS = 5 * 60 * 1000;
18
17
  function ensureFetchDir() {
19
- const dir = path.join(getSystemAgentsDir(), '.fetch');
18
+ const dir = getFetchCacheDir();
20
19
  if (!fs.existsSync(dir)) {
21
20
  try {
22
21
  fs.mkdirSync(dir, { recursive: true });
@@ -13,10 +13,10 @@ import * as fs from 'fs';
13
13
  import * as path from 'path';
14
14
  import { spawn } from 'child_process';
15
15
  import { fileURLToPath } from 'url';
16
- import { getSystemAgentsDir } from './state.js';
16
+ import { getFetchCacheDir } from './state.js';
17
17
  /** Where lock files and per-repo status markers live. */
18
18
  function fetchStateDir() {
19
- return path.join(getSystemAgentsDir(), '.fetch');
19
+ return getFetchCacheDir();
20
20
  }
21
21
  /** Per-repo lock file path. mtime acts as a recency check. */
22
22
  export function lockFilePath(alias) {
@@ -14,7 +14,13 @@ export declare class CDPClient {
14
14
  private handleMessage;
15
15
  private handleClose;
16
16
  }
17
- export declare function discoverBrowserWsUrl(port: number, host?: string): Promise<string>;
17
+ export interface BrowserDiscovery {
18
+ wsUrl: string;
19
+ browser: string;
20
+ }
21
+ export declare function discoverBrowserWsUrl(port: number, host?: string): Promise<BrowserDiscovery>;
22
+ export declare function normalizeBrowserName(s: string): string;
23
+ export declare function verifyBrowserIdentity(reported: string, expected: string, port: number, host?: string): void;
18
24
  export declare function listTargets(port: number, host?: string): Promise<Array<{
19
25
  id: string;
20
26
  type: string;
@@ -83,7 +83,35 @@ export async function discoverBrowserWsUrl(port, host = 'localhost') {
83
83
  throw new Error(`Failed to discover browser: ${response.status}`);
84
84
  }
85
85
  const data = (await response.json());
86
- return data.webSocketDebuggerUrl;
86
+ const browserField = data.Browser || data.Product || '';
87
+ return {
88
+ wsUrl: data.webSocketDebuggerUrl,
89
+ browser: normalizeBrowserName(browserField),
90
+ };
91
+ }
92
+ export function normalizeBrowserName(s) {
93
+ if (!s)
94
+ return 'unknown';
95
+ return s.split('/')[0].trim().toLowerCase().replace(/\s+/g, '-');
96
+ }
97
+ export function verifyBrowserIdentity(reported, expected, port, host = 'localhost') {
98
+ if (expected === 'custom')
99
+ return;
100
+ if (reported === 'unknown')
101
+ return;
102
+ const matches = {
103
+ chrome: ['chrome', 'google-chrome', 'headlesschrome'],
104
+ chromium: ['chromium', 'headlesschrome'],
105
+ comet: ['comet'],
106
+ brave: ['brave', 'brave-browser'],
107
+ edge: ['edge', 'microsoft-edge', 'msedge'],
108
+ };
109
+ const accepted = matches[expected] || [expected];
110
+ if (accepted.includes(reported))
111
+ return;
112
+ const target = host === 'localhost' || host === '127.0.0.1' ? `port ${port}` : `${host}:${port}`;
113
+ throw new Error(`Browser identity mismatch: profile expects "${expected}" but ${target} is serving "${reported}". ` +
114
+ `Stop the running browser (e.g. \`pkill -f ${reported}\`) or update the profile to browser=${reported}, then retry.`);
87
115
  }
88
116
  export async function listTargets(port, host = 'localhost') {
89
117
  const response = await fetch(`http://${host}:${port}/json`);
@@ -77,6 +77,7 @@ export async function launchBrowser(profileName, browserType, port, options = {}
77
77
  '--disable-backgrounding-occluded-windows',
78
78
  '--disable-renderer-backgrounding',
79
79
  ...(options.headless ? ['--headless=new'] : []),
80
+ ...(options.viewport ? [`--window-size=${options.viewport.width},${options.viewport.height}`] : []),
80
81
  ...(options.args || []),
81
82
  ];
82
83
  let env = { ...process.env };
@@ -103,7 +104,8 @@ export async function launchBrowser(profileName, browserType, port, options = {}
103
104
  for (let i = 0; i < 30; i++) {
104
105
  await sleep(200);
105
106
  try {
106
- wsUrl = await discoverBrowserWsUrl(port);
107
+ const result = await discoverBrowserWsUrl(port);
108
+ wsUrl = result.wsUrl;
107
109
  break;
108
110
  }
109
111
  catch {
@@ -116,11 +118,12 @@ export async function launchBrowser(profileName, browserType, port, options = {}
116
118
  return { pid, port, wsUrl };
117
119
  }
118
120
  export async function attachToChrome(port) {
119
- return discoverBrowserWsUrl(port);
121
+ const { wsUrl } = await discoverBrowserWsUrl(port);
122
+ return wsUrl;
120
123
  }
121
124
  export function killChrome(pid) {
122
125
  try {
123
- process.kill(pid, 'SIGTERM');
126
+ process.kill(pid, 'SIGINT');
124
127
  }
125
128
  catch {
126
129
  // Process already dead
@@ -0,0 +1,4 @@
1
+ import type { DeviceDescriptor } from './types.js';
2
+ export declare const DEVICES: Record<string, DeviceDescriptor>;
3
+ export declare function getDevice(name: string): DeviceDescriptor | undefined;
4
+ export declare function listDevices(): string[];
@@ -0,0 +1,27 @@
1
+ export const DEVICES = {
2
+ 'iPhone 14': {
3
+ width: 390,
4
+ height: 844,
5
+ deviceScaleFactor: 3,
6
+ mobile: true,
7
+ },
8
+ 'iPad': {
9
+ width: 768,
10
+ height: 1024,
11
+ deviceScaleFactor: 2,
12
+ mobile: true,
13
+ },
14
+ 'MacBook Pro': {
15
+ width: 1440,
16
+ height: 900,
17
+ deviceScaleFactor: 2,
18
+ mobile: false,
19
+ },
20
+ };
21
+ export function getDevice(name) {
22
+ const key = Object.keys(DEVICES).find((k) => k.toLowerCase() === name.toLowerCase());
23
+ return key ? DEVICES[key] : undefined;
24
+ }
25
+ export function listDevices() {
26
+ return Object.keys(DEVICES);
27
+ }
@@ -1,4 +1,4 @@
1
- import { CDPClient, discoverBrowserWsUrl } from '../cdp.js';
1
+ import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
2
2
  import { launchBrowser, allocatePort } from '../chrome.js';
3
3
  export async function connectLocal(endpoint, profile) {
4
4
  const url = new URL(endpoint);
@@ -7,14 +7,19 @@ export async function connectLocal(endpoint, profile) {
7
7
  }
8
8
  const port = parseInt(url.port, 10) || 9222;
9
9
  try {
10
- const wsUrl = await discoverBrowserWsUrl(port);
10
+ const { wsUrl, browser } = await discoverBrowserWsUrl(port);
11
+ verifyBrowserIdentity(browser, profile.browser, port);
11
12
  const cdp = new CDPClient();
12
13
  await cdp.connect(wsUrl);
13
14
  return { cdp, port, pid: 0 };
14
15
  }
15
- catch {
16
+ catch (err) {
17
+ if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
18
+ throw err;
19
+ }
16
20
  const newPort = allocatePort();
17
- const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, profile.chrome, profile.secrets, profile.binary);
21
+ const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
22
+ const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary);
18
23
  const cdp = new CDPClient();
19
24
  await cdp.connect(wsUrl);
20
25
  return { cdp, port: newPort, pid };
@@ -7,3 +7,4 @@ export interface SSHConnection {
7
7
  cleanup: () => void;
8
8
  }
9
9
  export declare function connectSSH(endpoint: string, profile: BrowserProfile): Promise<SSHConnection>;
10
+ export declare function restartRemoteBrowser(user: string, host: string, browserType: string, port: number, customBinary?: string): Promise<void>;
@@ -1,6 +1,6 @@
1
1
  import { spawn } from 'child_process';
2
2
  import * as net from 'net';
3
- import { CDPClient, discoverBrowserWsUrl } from '../cdp.js';
3
+ import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
4
4
  import { allocatePort } from '../chrome.js';
5
5
  export async function connectSSH(endpoint, profile) {
6
6
  const url = new URL(endpoint);
@@ -9,7 +9,7 @@ export async function connectSSH(endpoint, profile) {
9
9
  }
10
10
  const user = url.username || process.env.USER || 'root';
11
11
  const host = url.hostname;
12
- const remotePort = parseInt(url.searchParams.get('port') || '9222', 10);
12
+ const remotePort = url.port ? parseInt(url.port, 10) : 9222;
13
13
  const localPort = allocatePort();
14
14
  try {
15
15
  await ensureRemoteBrowser(user, host, profile.browser, remotePort, profile.binary);
@@ -17,7 +17,7 @@ export async function connectSSH(endpoint, profile) {
17
17
  catch {
18
18
  // Browser may already be running, continue
19
19
  }
20
- const tunnel = await startSSHTunnel(user, host, localPort, remotePort);
20
+ let tunnel = await startSSHTunnel(user, host, localPort, remotePort);
21
21
  try {
22
22
  await waitForPort(localPort, 8000);
23
23
  }
@@ -25,7 +25,14 @@ export async function connectSSH(endpoint, profile) {
25
25
  tunnel.kill();
26
26
  throw new Error(`SSH tunnel failed to establish to ${host}`);
27
27
  }
28
- const wsUrl = await discoverBrowserWsUrl(localPort);
28
+ const { wsUrl, browser } = await discoverBrowserWsUrl(localPort);
29
+ try {
30
+ verifyBrowserIdentity(browser, profile.browser, remotePort, host);
31
+ }
32
+ catch (err) {
33
+ tunnel.kill();
34
+ throw err;
35
+ }
29
36
  const cdp = new CDPClient();
30
37
  await cdp.connect(wsUrl);
31
38
  return {
@@ -133,6 +140,27 @@ async function ensureRemoteBrowser(user, host, browserType, port, customBinary)
133
140
  }, 2000);
134
141
  });
135
142
  }
143
+ export async function restartRemoteBrowser(user, host, browserType, port, customBinary) {
144
+ // Kill any process using the remote debugging port
145
+ const killCmd = `lsof -ti :${port} | xargs kill -9 2>/dev/null || true`;
146
+ await runSSHCommand(user, host, killCmd);
147
+ await sleep(500);
148
+ await ensureRemoteBrowser(user, host, browserType, port, customBinary);
149
+ await sleep(1500);
150
+ }
151
+ function runSSHCommand(user, host, cmd) {
152
+ return new Promise((resolve) => {
153
+ const child = spawn('ssh', [`${user}@${host}`, '-o', 'BatchMode=yes', cmd], {
154
+ stdio: 'ignore',
155
+ });
156
+ child.on('close', () => resolve());
157
+ child.on('error', () => resolve());
158
+ setTimeout(() => {
159
+ child.kill();
160
+ resolve();
161
+ }, 3000);
162
+ });
163
+ }
136
164
  function sleep(ms) {
137
165
  return new Promise((resolve) => setTimeout(resolve, ms));
138
166
  }
@@ -1,11 +1,11 @@
1
1
  import * as net from 'net';
2
2
  import * as fs from 'fs';
3
3
  import * as path from 'path';
4
- import { getAgentsDir } from '../state.js';
4
+ import { getHelpersDir } from '../state.js';
5
5
  import { startDaemon } from '../daemon.js';
6
6
  const SOCKET_NAME = 'browser.sock';
7
7
  export function getSocketPath() {
8
- return path.join(getAgentsDir(), SOCKET_NAME);
8
+ return path.join(getHelpersDir(), SOCKET_NAME);
9
9
  }
10
10
  export class BrowserIPCServer {
11
11
  server = null;
@@ -67,8 +67,23 @@ export class BrowserIPCServer {
67
67
  if (!request.profile) {
68
68
  return { ok: false, error: 'Profile required' };
69
69
  }
70
- const result = await this.service.start(request.profile, request.task);
71
- return { ok: true, task: result.task, windowTargetId: result.windowTargetId };
70
+ const result = await this.service.start(request.profile, {
71
+ taskName: request.taskName,
72
+ url: request.url,
73
+ });
74
+ return {
75
+ ok: true,
76
+ task: result.name,
77
+ tabId: result.tabId,
78
+ windowTargetId: result.windowId,
79
+ };
80
+ }
81
+ case 'done': {
82
+ if (!request.task) {
83
+ return { ok: false, error: 'Task required' };
84
+ }
85
+ const result = await this.service.done(request.task);
86
+ return { ok: result.ok, error: result.ok ? undefined : 'Task not found' };
72
87
  }
73
88
  case 'stop': {
74
89
  if (request.task) {
@@ -85,6 +100,10 @@ export class BrowserIPCServer {
85
100
  const profiles = await this.service.status(request.profile);
86
101
  return { ok: true, profiles };
87
102
  }
103
+ case 'history': {
104
+ const history = await this.service.getHistory(request.limit ?? 10);
105
+ return { ok: true, history };
106
+ }
88
107
  case 'navigate': {
89
108
  if (!request.task || !request.url) {
90
109
  return { ok: false, error: 'Task and URL required' };
@@ -92,20 +111,37 @@ export class BrowserIPCServer {
92
111
  const result = await this.service.navigate(request.task, request.url, request.profile);
93
112
  return { ok: true, tabId: result.tabId };
94
113
  }
95
- case 'tabs': {
96
- const tabs = await this.service.tabs(request.task, request.profile);
97
- return { ok: true, tabs };
114
+ case 'tab-add': {
115
+ if (!request.task || !request.url) {
116
+ return { ok: false, error: 'Task and URL required' };
117
+ }
118
+ const result = await this.service.tabAdd(request.task, request.url, request.profile);
119
+ return { ok: true, tabId: result.tabId };
120
+ }
121
+ case 'tab-focus': {
122
+ if (!request.task || !request.tabId) {
123
+ return { ok: false, error: 'Task and tabId required' };
124
+ }
125
+ const result = await this.service.tabFocus(request.task, request.tabId);
126
+ return { ok: true, tabId: result.tabId };
98
127
  }
99
- case 'close': {
128
+ case 'tab-close': {
100
129
  if (!request.task) {
101
130
  return { ok: false, error: 'Task required' };
102
131
  }
103
- await this.service.close(request.task, request.tabId);
132
+ await this.service.tabClose(request.task, request.tabId);
104
133
  return { ok: true };
105
134
  }
135
+ case 'tab-list': {
136
+ if (!request.task) {
137
+ return { ok: false, error: 'Task required' };
138
+ }
139
+ const tabs = await this.service.tabList(request.task);
140
+ return { ok: true, tabs: tabs.map(t => ({ id: t.id, url: t.url, title: t.title, task: request.task })) };
141
+ }
106
142
  case 'evaluate': {
107
- if (!request.task || request.tabId === undefined || !request.expr) {
108
- return { ok: false, error: 'Task, tabId, and expression required' };
143
+ if (!request.task || !request.expr) {
144
+ return { ok: false, error: 'Task and expression required' };
109
145
  }
110
146
  const result = await this.service.evaluate(request.task, request.tabId, request.expr);
111
147
  return { ok: true, result };
@@ -128,33 +164,118 @@ export class BrowserIPCServer {
128
164
  return { ok: true, refs };
129
165
  }
130
166
  case 'click': {
131
- if (!request.task || !request.tabId || request.ref === undefined) {
132
- return { ok: false, error: 'Task, tabId, and ref required' };
167
+ if (!request.task || request.ref === undefined) {
168
+ return { ok: false, error: 'Task and ref required' };
133
169
  }
134
- await this.service.click(request.task, request.tabId, request.ref);
170
+ await this.service.click(request.task, request.ref, request.tabId);
135
171
  return { ok: true };
136
172
  }
137
173
  case 'type': {
138
- if (!request.task || !request.tabId || request.ref === undefined || !request.text) {
139
- return { ok: false, error: 'Task, tabId, ref, and text required' };
174
+ if (!request.task || request.ref === undefined || !request.text) {
175
+ return { ok: false, error: 'Task, ref, and text required' };
140
176
  }
141
- await this.service.type(request.task, request.tabId, request.ref, request.text);
177
+ await this.service.type(request.task, request.ref, request.text, request.tabId);
142
178
  return { ok: true };
143
179
  }
144
180
  case 'press': {
145
- if (!request.task || !request.tabId || !request.key) {
146
- return { ok: false, error: 'Task, tabId, and key required' };
181
+ if (!request.task || !request.key) {
182
+ return { ok: false, error: 'Task and key required' };
147
183
  }
148
- await this.service.press(request.task, request.tabId, request.key);
184
+ await this.service.press(request.task, request.key, request.tabId);
149
185
  return { ok: true };
150
186
  }
151
187
  case 'hover': {
152
- if (!request.task || !request.tabId || request.ref === undefined) {
153
- return { ok: false, error: 'Task, tabId, and ref required' };
188
+ if (!request.task || request.ref === undefined) {
189
+ return { ok: false, error: 'Task and ref required' };
154
190
  }
155
- await this.service.hover(request.task, request.tabId, request.ref);
191
+ await this.service.hover(request.task, request.ref, request.tabId);
192
+ return { ok: true };
193
+ }
194
+ case 'set-viewport': {
195
+ if (!request.task || !request.width || !request.height) {
196
+ return { ok: false, error: 'Task, width, and height required' };
197
+ }
198
+ await this.service.setViewport(request.task, request.width, request.height, {
199
+ mobile: request.mobile,
200
+ deviceScaleFactor: request.deviceScaleFactor,
201
+ tabHint: request.tabId,
202
+ });
156
203
  return { ok: true };
157
204
  }
205
+ case 'set-device': {
206
+ if (!request.task || !request.deviceName) {
207
+ return { ok: false, error: 'Task and device name required' };
208
+ }
209
+ await this.service.setDevice(request.task, request.deviceName, request.tabId);
210
+ return { ok: true };
211
+ }
212
+ case 'console': {
213
+ if (!request.task) {
214
+ return { ok: false, error: 'Task required' };
215
+ }
216
+ const logs = await this.service.getConsoleLogs(request.task, {
217
+ level: request.level,
218
+ clear: request.clear,
219
+ tabHint: request.tabId,
220
+ });
221
+ return { ok: true, logs };
222
+ }
223
+ case 'errors': {
224
+ if (!request.task) {
225
+ return { ok: false, error: 'Task required' };
226
+ }
227
+ const errors = await this.service.getErrors(request.task, {
228
+ clear: request.clear,
229
+ tabHint: request.tabId,
230
+ });
231
+ return { ok: true, errors };
232
+ }
233
+ case 'requests': {
234
+ if (!request.task) {
235
+ return { ok: false, error: 'Task required' };
236
+ }
237
+ const requests = await this.service.getNetworkRequests(request.task, {
238
+ filter: request.filter,
239
+ clear: request.clear,
240
+ tabHint: request.tabId,
241
+ });
242
+ return { ok: true, requests };
243
+ }
244
+ case 'response-body': {
245
+ if (!request.task || !request.urlPattern) {
246
+ return { ok: false, error: 'Task and URL pattern required' };
247
+ }
248
+ const body = await this.service.getResponseBody(request.task, request.urlPattern, {
249
+ timeout: request.timeout,
250
+ maxChars: request.maxChars,
251
+ tabHint: request.tabId,
252
+ });
253
+ return { ok: true, body };
254
+ }
255
+ case 'wait': {
256
+ if (!request.task || !request.waitType || request.waitValue === undefined) {
257
+ return { ok: false, error: 'Task, wait type, and wait value required' };
258
+ }
259
+ await this.service.wait(request.task, request.waitType, request.waitValue, {
260
+ timeout: request.timeout,
261
+ tabHint: request.tabId,
262
+ });
263
+ return { ok: true };
264
+ }
265
+ case 'set-download-path': {
266
+ if (!request.task || !request.downloadPath) {
267
+ return { ok: false, error: 'Task and download path required' };
268
+ }
269
+ await this.service.setDownloadPath(request.task, request.downloadPath, request.tabId);
270
+ return { ok: true };
271
+ }
272
+ case 'wait-download': {
273
+ if (!request.task) {
274
+ return { ok: false, error: 'Task required' };
275
+ }
276
+ const downloadPath = await this.service.waitForDownload(request.task, request.timeout);
277
+ return { ok: true, downloadPath };
278
+ }
158
279
  default:
159
280
  return { ok: false, error: `Unknown action: ${request.action}` };
160
281
  }
@@ -195,6 +316,7 @@ export async function sendIPCRequest(request) {
195
316
  if (!fs.existsSync(socketPath)) {
196
317
  throw new Error('Failed to start browser daemon');
197
318
  }
319
+ await new Promise((r) => setTimeout(r, 300));
198
320
  }
199
321
  return new Promise((resolve, reject) => {
200
322
  const socket = net.createConnection(socketPath);
@@ -1,11 +1,14 @@
1
1
  import type { BrowserProfile } from './types.js';
2
2
  export type { BrowserProfile } from './types.js';
3
- export declare function getBrowserProfilesDir(): string;
4
3
  export declare function getBrowserRuntimeDir(): string;
5
- export declare function getProfilePath(name: string): string;
6
4
  export declare function getProfileRuntimeDir(name: string): string;
7
5
  export declare function listProfiles(): Promise<BrowserProfile[]>;
8
6
  export declare function getProfile(name: string): Promise<BrowserProfile | null>;
9
7
  export declare function createProfile(profile: BrowserProfile): Promise<void>;
10
8
  export declare function updateProfile(profile: BrowserProfile): Promise<void>;
11
9
  export declare function deleteProfile(name: string): Promise<void>;
10
+ /**
11
+ * Extract the port intended by the profile's first endpoint.
12
+ * Returns undefined for endpoint shapes that don't carry a port (e.g. ws:// without one).
13
+ */
14
+ export declare function extractConfiguredPort(profile: BrowserProfile): number | undefined;