@phnx-labs/agents-cli 1.15.0 → 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -39
- package/README.md +6 -6
- package/dist/commands/alias.js +2 -2
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +793 -83
- package/dist/commands/cloud.js +8 -0
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +70 -1
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +81 -39
- package/dist/commands/plugins.js +224 -17
- package/dist/commands/prune.js +29 -1
- 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 +154 -20
- 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} +22 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/view.js +78 -20
- package/dist/commands/workflows.d.ts +10 -0
- package/dist/commands/workflows.js +457 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +48 -36
- 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 +32 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +41 -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 +22 -6
- package/dist/lib/browser/drivers/ssh.js +9 -2
- package/dist/lib/browser/input.d.ts +1 -0
- package/dist/lib/browser/input.js +3 -0
- package/dist/lib/browser/ipc.js +158 -23
- package/dist/lib/browser/profiles.d.ts +10 -2
- package/dist/lib/browser/profiles.js +122 -37
- package/dist/lib/browser/service.d.ts +91 -13
- package/dist/lib/browser/service.js +767 -132
- package/dist/lib/browser/types.d.ts +91 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.d.ts +28 -1
- package/dist/lib/cloud/rush.js +69 -14
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -15
- package/dist/lib/commands.js +11 -7
- package/dist/lib/daemon.js +2 -3
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.js +2 -2
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +138 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1237 -22
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +8 -66
- package/dist/lib/permissions.js +18 -18
- package/dist/lib/plugins.d.ts +94 -24
- package/dist/lib/plugins.js +702 -123
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resource-patterns.d.ts +41 -0
- package/dist/lib/resource-patterns.js +82 -0
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/resources/index.d.ts +17 -0
- package/dist/lib/resources/index.js +7 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources/workflows.d.ts +24 -0
- package/dist/lib/resources/workflows.js +110 -0
- package/dist/lib/resources.d.ts +6 -1
- package/dist/lib/resources.js +12 -2
- package/dist/lib/rotate.js +3 -4
- 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 +18 -0
- package/dist/lib/session/db.js +109 -5
- package/dist/lib/session/discover.d.ts +6 -0
- package/dist/lib/session/discover.js +55 -29
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +4 -52
- package/dist/lib/shims.js +23 -15
- package/dist/lib/skills.js +6 -2
- package/dist/lib/sqlite.js +10 -4
- package/dist/lib/state.d.ts +101 -16
- package/dist/lib/state.js +179 -31
- 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 +75 -17
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.js +2 -2
- package/dist/lib/versions.d.ts +3 -0
- package/dist/lib/versions.js +158 -47
- package/dist/lib/workflows.d.ts +79 -0
- package/dist/lib/workflows.js +233 -0
- package/package.json +1 -5
- package/scripts/postinstall.js +60 -59
- package/dist/commands/fork.d.ts +0 -10
- package/dist/commands/fork.js +0 -146
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';
|
|
@@ -46,6 +46,7 @@ import { registerTrashCommands } from './commands/trash.js';
|
|
|
46
46
|
import { registerDoctorCommand } from './commands/doctor.js';
|
|
47
47
|
import { registerSubagentsCommands } from './commands/subagents.js';
|
|
48
48
|
import { registerPluginsCommands } from './commands/plugins.js';
|
|
49
|
+
import { registerWorkflowsCommands } from './commands/workflows.js';
|
|
49
50
|
import { registerSyncCommand } from './commands/sync.js';
|
|
50
51
|
import { registerRefreshRulesCommand } from './commands/refresh-rules.js';
|
|
51
52
|
import { registerDriveCommands } from './commands/drive.js';
|
|
@@ -59,10 +60,9 @@ import { registerAliasCommand } from './commands/alias.js';
|
|
|
59
60
|
import { registerBetaCommands } from './commands/beta.js';
|
|
60
61
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
61
62
|
import { isPromptCancelled } from './commands/utils.js';
|
|
62
|
-
import { getAgentsDir } from './lib/state.js';
|
|
63
63
|
import { AGENTS } from './lib/agents.js';
|
|
64
|
-
import { getGlobalDefault } from './lib/versions.js';
|
|
65
|
-
import { addShimsToPath, ensureShimCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
|
|
64
|
+
import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
|
|
65
|
+
import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
|
|
66
66
|
const program = new Command();
|
|
67
67
|
program
|
|
68
68
|
.name('agents')
|
|
@@ -80,7 +80,7 @@ Install, configure, run, and dispatch AI coding agents from one place.
|
|
|
80
80
|
Works with Claude, Codex, Gemini, Cursor, OpenCode, OpenClaw, and Droid.
|
|
81
81
|
|
|
82
82
|
Quick start:
|
|
83
|
-
agents
|
|
83
|
+
agents setup First-time setup (interactive)
|
|
84
84
|
agents view See what's installed
|
|
85
85
|
agents run <agent> ["prompt"] Run an agent (interactive without prompt, headless with)
|
|
86
86
|
agents sessions Browse past sessions across all agents
|
|
@@ -89,7 +89,8 @@ Agent versions:
|
|
|
89
89
|
add <agent>[@version] Install an agent CLI (e.g. agents add codex)
|
|
90
90
|
remove <agent>[@version] Uninstall a version
|
|
91
91
|
use <agent>@<version> Set the default version
|
|
92
|
-
prune [target] Remove orphan resources
|
|
92
|
+
prune [target] Remove orphan resources and older duplicate version installs (targets: commands, sessions, runs, trash)
|
|
93
|
+
trash Inspect and restore soft-deleted version directories
|
|
93
94
|
view [agent[@version]] List versions, or inspect one in detail
|
|
94
95
|
|
|
95
96
|
Agent configuration (synced across versions):
|
|
@@ -98,7 +99,7 @@ Agent configuration (synced across versions):
|
|
|
98
99
|
skills Knowledge packs (SKILL.md + supporting files)
|
|
99
100
|
mcp MCP servers (stdio or HTTP)
|
|
100
101
|
permissions Allow/deny rules for tool calls
|
|
101
|
-
hooks Shell scripts that run on agent events
|
|
102
|
+
hooks Shell scripts that run on agent events (hooks.yaml in agents.yaml)
|
|
102
103
|
subagents Named sub-agent definitions
|
|
103
104
|
plugins Bundles of skills, hooks, and scripts
|
|
104
105
|
|
|
@@ -106,19 +107,30 @@ Packages:
|
|
|
106
107
|
search <query> Find MCP servers and skills in registries
|
|
107
108
|
install <pkg> Install from registry (mcp:name, skill:user/repo)
|
|
108
109
|
|
|
109
|
-
Run
|
|
110
|
+
Run and dispatch:
|
|
110
111
|
run <agent|profile> [prompt] Run an agent. Omit prompt for interactive mode.
|
|
111
112
|
teams Coordinate multiple agents on shared work
|
|
112
113
|
routines Run agents on a cron schedule (scheduler auto-starts)
|
|
113
|
-
sessions Browse and replay past runs
|
|
114
|
+
sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
|
|
115
|
+
browser Automate a browser — navigate, click, screenshot, console, network
|
|
116
|
+
pty Drive interactive terminal programs (REPLs, TUIs) via a persistent PTY session
|
|
114
117
|
|
|
115
|
-
Credentials:
|
|
118
|
+
Credentials and profiles:
|
|
116
119
|
profiles Bundles of (host CLI, endpoint, model, auth)
|
|
117
|
-
secrets Keychain-backed env bundles
|
|
120
|
+
secrets Keychain-backed env bundles; use 'secrets exec <bundle> -- <cmd>' to inject into a subprocess
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
Diagnostics:
|
|
123
|
+
doctor [agent[@version]] Diagnose CLI availability, sync status, and resource divergence
|
|
124
|
+
usage [agent] Show rate-limit and quota usage per agent
|
|
125
|
+
|
|
126
|
+
Config sync:
|
|
127
|
+
drive Sync session history across machines via rsync
|
|
128
|
+
pull Clone or pull the system repo at ~/.agents-system/
|
|
129
|
+
repo init --path <dir> Scaffold your own editable repo from a template
|
|
130
|
+
repo add <path|gh:user/repo> Merge an extra repo after the system repo
|
|
131
|
+
|
|
132
|
+
Beta features:
|
|
133
|
+
beta Enable preview features (factory, drive, and more)
|
|
122
134
|
|
|
123
135
|
Automation tips:
|
|
124
136
|
Pass explicit names/IDs Avoid pickers: agents sessions <id> --markdown
|
|
@@ -127,11 +139,6 @@ Automation tips:
|
|
|
127
139
|
Use agent@version targets e.g. --agents claude@2.1.79,codex@default
|
|
128
140
|
Non-TTY shells apply defaults Omitted required selections fail with a plain hint
|
|
129
141
|
|
|
130
|
-
Config sync (portable setup via git):
|
|
131
|
-
pull Clone or pull the system repo at ~/.agents-system/
|
|
132
|
-
repo init --path <dir> Scaffold your own editable repo from a template
|
|
133
|
-
repo add <path|gh:user/repo> Merge an extra repo after the system repo
|
|
134
|
-
|
|
135
142
|
Options:
|
|
136
143
|
-V, --version Show version number
|
|
137
144
|
-h, --help Show help
|
|
@@ -199,7 +206,8 @@ async function showWhatsNew(fromVersion, toVersion) {
|
|
|
199
206
|
}
|
|
200
207
|
}
|
|
201
208
|
const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
202
|
-
|
|
209
|
+
import { getUpdateCheckPath, getMigratedSentinelPath, getUserAgentsDir } from './lib/state.js';
|
|
210
|
+
const UPDATE_CHECK_FILE = getUpdateCheckPath();
|
|
203
211
|
/** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
|
|
204
212
|
function readUpdateCache() {
|
|
205
213
|
try {
|
|
@@ -335,6 +343,12 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
|
|
|
335
343
|
if (status !== 'current') {
|
|
336
344
|
createdOrUpdated.push(`${status === 'created' ? 'Created' : 'Updated'} ${AGENTS[agent].cliCommand} shim`);
|
|
337
345
|
}
|
|
346
|
+
for (const version of listInstalledVersions(agent)) {
|
|
347
|
+
const aliasStatus = ensureVersionedAliasCurrent(agent, version);
|
|
348
|
+
if (aliasStatus !== 'current') {
|
|
349
|
+
createdOrUpdated.push(`${aliasStatus === 'created' ? 'Created' : 'Updated'} ${AGENTS[agent].cliCommand}@${version} alias`);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
338
352
|
}
|
|
339
353
|
for (const notice of createdOrUpdated) {
|
|
340
354
|
console.log(chalk.green(notice));
|
|
@@ -444,6 +458,7 @@ program
|
|
|
444
458
|
registerMcpCommands(program);
|
|
445
459
|
registerSubagentsCommands(program);
|
|
446
460
|
registerPluginsCommands(program);
|
|
461
|
+
registerWorkflowsCommands(program);
|
|
447
462
|
registerVersionsCommands(program);
|
|
448
463
|
registerPackagesCommands(program);
|
|
449
464
|
registerDaemonCommands(program);
|
|
@@ -526,7 +541,7 @@ program
|
|
|
526
541
|
});
|
|
527
542
|
registerPullCommand(program);
|
|
528
543
|
registerRepoCommands(program);
|
|
529
|
-
|
|
544
|
+
registerSetupCommand(program);
|
|
530
545
|
applyGlobalHelpConventions(program);
|
|
531
546
|
/** Calculate the Levenshtein edit distance between two strings. */
|
|
532
547
|
function levenshtein(a, b) {
|
|
@@ -582,7 +597,7 @@ await checkForUpdates();
|
|
|
582
597
|
// status marker that we'll print on the *next* invocation.
|
|
583
598
|
const { spawnDetachedSync } = await import('./lib/auto-pull.js');
|
|
584
599
|
spawnDetachedSync();
|
|
585
|
-
// First-run experience: no args + no config yet + TTY -> launch interactive
|
|
600
|
+
// First-run experience: no args + no config yet + TTY -> launch interactive setup.
|
|
586
601
|
// Skipped when stdin/stdout isn't a terminal (CI, pipes) or when user passes any args.
|
|
587
602
|
const passedArgs = process.argv.slice(2);
|
|
588
603
|
const requestedCommand = passedArgs.find((arg) => !arg.startsWith('-'));
|
|
@@ -613,14 +628,14 @@ async function registerLazyCommands() {
|
|
|
613
628
|
}
|
|
614
629
|
}
|
|
615
630
|
await registerLazyCommands();
|
|
616
|
-
const metaFilePath = path.join(
|
|
631
|
+
const metaFilePath = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
617
632
|
const firstRun = passedArgs.length === 0 &&
|
|
618
633
|
!fs.existsSync(metaFilePath) &&
|
|
619
634
|
process.stdin.isTTY &&
|
|
620
635
|
process.stdout.isTTY;
|
|
621
636
|
if (firstRun) {
|
|
622
637
|
try {
|
|
623
|
-
await
|
|
638
|
+
await runSetup(program);
|
|
624
639
|
}
|
|
625
640
|
catch (err) {
|
|
626
641
|
if (!(err instanceof Error && err.name === 'ExitPromptError')) {
|
|
@@ -629,14 +644,11 @@ if (firstRun) {
|
|
|
629
644
|
}
|
|
630
645
|
process.exit(0);
|
|
631
646
|
}
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
]);
|
|
638
|
-
if (!firstRun && requestedCommand && SYSTEM_REPO_COMMANDS.has(requestedCommand)) {
|
|
639
|
-
const { ensureInitialized } = await import('./commands/init.js');
|
|
647
|
+
// Every command requires the system repo to be cloned first. `setup` is the
|
|
648
|
+
// only exemption — it's the command that does the cloning.
|
|
649
|
+
const SETUP_EXEMPT_COMMANDS = new Set(['setup', 'help']);
|
|
650
|
+
if (!firstRun && requestedCommand && !SETUP_EXEMPT_COMMANDS.has(requestedCommand)) {
|
|
651
|
+
const { ensureInitialized } = await import('./commands/setup.js');
|
|
640
652
|
await ensureInitialized(program);
|
|
641
653
|
}
|
|
642
654
|
// One-shot idempotent migrations (split-layout, legacy file moves).
|
|
@@ -648,8 +660,8 @@ if (!firstRun && requestedCommand && SYSTEM_REPO_COMMANDS.has(requestedCommand))
|
|
|
648
660
|
if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
649
661
|
try {
|
|
650
662
|
const { runMigration } = await import('./lib/migrate.js');
|
|
651
|
-
const sentinel =
|
|
652
|
-
const sentinelValue = `${VERSION}-
|
|
663
|
+
const sentinel = getMigratedSentinelPath();
|
|
664
|
+
const sentinelValue = `${VERSION}-v8`;
|
|
653
665
|
let needRun = true;
|
|
654
666
|
try {
|
|
655
667
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
|
@@ -658,7 +670,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
658
670
|
}
|
|
659
671
|
catch { /* best-effort — fall through to run */ }
|
|
660
672
|
if (needRun) {
|
|
661
|
-
runMigration();
|
|
673
|
+
await runMigration();
|
|
662
674
|
try {
|
|
663
675
|
fs.mkdirSync(path.dirname(sentinel), { recursive: true });
|
|
664
676
|
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,38 @@ 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', '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'],
|
|
111
|
+
};
|
|
112
|
+
const accepted = matches[expected] || [expected];
|
|
113
|
+
if (accepted.includes(reported))
|
|
114
|
+
return;
|
|
115
|
+
const target = host === 'localhost' || host === '127.0.0.1' ? `port ${port}` : `${host}:${port}`;
|
|
116
|
+
throw new Error(`Browser identity mismatch: profile expects "${expected}" but ${target} is serving "${reported}". ` +
|
|
117
|
+
`Stop the running browser (e.g. \`pkill -f ${reported}\`) or update the profile to browser=${reported}, then retry.`);
|
|
87
118
|
}
|
|
88
119
|
export async function listTargets(port, host = 'localhost') {
|
|
89
120
|
const response = await fetch(`http://${host}:${port}/json`);
|
|
@@ -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,6 +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'] : []),
|
|
81
|
+
`--window-size=${viewport.width},${viewport.height}`,
|
|
82
|
+
...(viewport.x !== undefined && viewport.y !== undefined
|
|
83
|
+
? [`--window-position=${viewport.x},${viewport.y}`]
|
|
84
|
+
: []),
|
|
80
85
|
...(options.args || []),
|
|
81
86
|
];
|
|
82
87
|
let env = { ...process.env };
|
|
@@ -103,7 +108,8 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
103
108
|
for (let i = 0; i < 30; i++) {
|
|
104
109
|
await sleep(200);
|
|
105
110
|
try {
|
|
106
|
-
|
|
111
|
+
const result = await discoverBrowserWsUrl(port);
|
|
112
|
+
wsUrl = result.wsUrl;
|
|
107
113
|
break;
|
|
108
114
|
}
|
|
109
115
|
catch {
|
|
@@ -116,7 +122,8 @@ export async function launchBrowser(profileName, browserType, port, options = {}
|
|
|
116
122
|
return { pid, port, wsUrl };
|
|
117
123
|
}
|
|
118
124
|
export async function attachToChrome(port) {
|
|
119
|
-
|
|
125
|
+
const { wsUrl } = await discoverBrowserWsUrl(port);
|
|
126
|
+
return wsUrl;
|
|
120
127
|
}
|
|
121
128
|
export function killChrome(pid) {
|
|
122
129
|
try {
|
|
@@ -147,7 +154,11 @@ function isProcessRunning(pid) {
|
|
|
147
154
|
process.kill(pid, 0);
|
|
148
155
|
return true;
|
|
149
156
|
}
|
|
150
|
-
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;
|
|
151
162
|
return false;
|
|
152
163
|
}
|
|
153
164
|
}
|
|
@@ -167,3 +178,30 @@ export function allocatePort() {
|
|
|
167
178
|
}
|
|
168
179
|
throw new Error('No available ports in range 9200-9300');
|
|
169
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
|
+
}
|
|
@@ -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,5 +1,5 @@
|
|
|
1
|
-
import { CDPClient, discoverBrowserWsUrl } from '../cdp.js';
|
|
2
|
-
import { launchBrowser,
|
|
1
|
+
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.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:') {
|
|
@@ -7,14 +7,30 @@ 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
|
-
|
|
17
|
-
|
|
16
|
+
catch (err) {
|
|
17
|
+
if (err instanceof Error && err.message.startsWith('Browser identity mismatch')) {
|
|
18
|
+
throw err;
|
|
19
|
+
}
|
|
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;
|
|
32
|
+
const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
|
|
33
|
+
const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary);
|
|
18
34
|
const cdp = new CDPClient();
|
|
19
35
|
await cdp.connect(wsUrl);
|
|
20
36
|
return { cdp, port: newPort, pid };
|
|
@@ -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);
|
|
@@ -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 {
|
|
@@ -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);
|