@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.
- package/CHANGELOG.md +78 -39
- package/README.md +74 -7
- package/dist/commands/alias.js +2 -2
- package/dist/commands/beta.js +6 -1
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +546 -75
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +9 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +85 -43
- package/dist/commands/plugins.js +48 -15
- package/dist/commands/prune.d.ts +0 -20
- package/dist/commands/prune.js +291 -16
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +37 -1
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +32 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/teams.js +2 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/versions.js +8 -6
- package/dist/commands/view.js +61 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -20
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +29 -1
- package/dist/lib/browser/chrome.js +6 -3
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +9 -4
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +32 -4
- package/dist/lib/browser/ipc.js +145 -23
- package/dist/lib/browser/profiles.d.ts +5 -2
- package/dist/lib/browser/profiles.js +77 -37
- package/dist/lib/browser/service.d.ts +84 -13
- package/dist/lib/browser/service.js +806 -122
- package/dist/lib/browser/types.d.ts +81 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -0
- package/dist/lib/commands.js +6 -2
- package/dist/lib/daemon.js +6 -7
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.d.ts +94 -1
- package/dist/lib/events.js +264 -6
- package/dist/lib/exec.js +16 -10
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +125 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1178 -21
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +14 -11
- package/dist/lib/permissions.js +46 -42
- package/dist/lib/plugins.d.ts +30 -1
- package/dist/lib/plugins.js +75 -3
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/routines.d.ts +15 -0
- package/dist/lib/routines.js +68 -0
- package/dist/lib/runner.js +9 -5
- package/dist/lib/secrets/index.d.ts +14 -11
- package/dist/lib/secrets/index.js +49 -21
- package/dist/lib/secrets/linux.d.ts +27 -0
- package/dist/lib/secrets/linux.js +161 -0
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +34 -3
- package/dist/lib/session/discover.js +30 -15
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +6 -6
- package/dist/lib/skills.js +6 -2
- package/dist/lib/state.d.ts +86 -14
- package/dist/lib/state.js +150 -23
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +32 -3
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.d.ts +1 -1
- package/dist/lib/usage.js +15 -48
- package/dist/lib/versions.js +31 -21
- package/package.json +1 -1
- 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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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 =
|
|
652
|
-
const sentinelValue = `${VERSION}-
|
|
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);
|
package/dist/lib/agents.js
CHANGED
|
@@ -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,
|
|
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 =
|
|
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 =
|
|
18
|
+
const dir = getFetchCacheDir();
|
|
20
19
|
if (!fs.existsSync(dir)) {
|
|
21
20
|
try {
|
|
22
21
|
fs.mkdirSync(dir, { recursive: true });
|
package/dist/lib/auto-pull.js
CHANGED
|
@@ -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 {
|
|
16
|
+
import { getFetchCacheDir } from './state.js';
|
|
17
17
|
/** Where lock files and per-repo status markers live. */
|
|
18
18
|
function fetchStateDir() {
|
|
19
|
-
return
|
|
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
|
|
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;
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
+
const { wsUrl } = await discoverBrowserWsUrl(port);
|
|
122
|
+
return wsUrl;
|
|
120
123
|
}
|
|
121
124
|
export function killChrome(pid) {
|
|
122
125
|
try {
|
|
123
|
-
process.kill(pid, '
|
|
126
|
+
process.kill(pid, 'SIGINT');
|
|
124
127
|
}
|
|
125
128
|
catch {
|
|
126
129
|
// Process already dead
|
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
}
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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,
|
|
71
|
-
|
|
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 '
|
|
96
|
-
|
|
97
|
-
|
|
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.
|
|
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 ||
|
|
108
|
-
return { ok: false, error: 'Task
|
|
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 ||
|
|
132
|
-
return { ok: false, error: 'Task
|
|
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.
|
|
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 ||
|
|
139
|
-
return { ok: false, error: 'Task,
|
|
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.
|
|
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.
|
|
146
|
-
return { ok: false, error: 'Task
|
|
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.
|
|
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 ||
|
|
153
|
-
return { ok: false, error: 'Task
|
|
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.
|
|
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;
|