@phnx-labs/agents-cli 1.15.0 → 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 (87) hide show
  1. package/CHANGELOG.md +78 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +546 -75
  7. package/dist/commands/commands.js +72 -22
  8. package/dist/commands/daemon.js +2 -2
  9. package/dist/commands/fork.js +2 -2
  10. package/dist/commands/hooks.js +71 -26
  11. package/dist/commands/mcp.js +81 -39
  12. package/dist/commands/plugins.js +48 -15
  13. package/dist/commands/prune.js +23 -1
  14. package/dist/commands/pull.js +3 -3
  15. package/dist/commands/repo.js +1 -1
  16. package/dist/commands/routines.js +2 -2
  17. package/dist/commands/secrets.js +37 -1
  18. package/dist/commands/sessions.js +62 -19
  19. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  20. package/dist/commands/{init.js → setup.js} +22 -21
  21. package/dist/commands/skills.js +60 -19
  22. package/dist/commands/subagents.js +41 -13
  23. package/dist/commands/utils.d.ts +16 -0
  24. package/dist/commands/utils.js +32 -0
  25. package/dist/commands/view.js +61 -16
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +17 -20
  28. package/dist/lib/agents.js +2 -2
  29. package/dist/lib/auto-pull-worker.js +2 -3
  30. package/dist/lib/auto-pull.js +2 -2
  31. package/dist/lib/browser/cdp.d.ts +7 -1
  32. package/dist/lib/browser/cdp.js +29 -1
  33. package/dist/lib/browser/chrome.js +5 -2
  34. package/dist/lib/browser/devices.d.ts +4 -0
  35. package/dist/lib/browser/devices.js +27 -0
  36. package/dist/lib/browser/drivers/local.js +9 -4
  37. package/dist/lib/browser/drivers/ssh.js +9 -2
  38. package/dist/lib/browser/ipc.js +144 -23
  39. package/dist/lib/browser/profiles.d.ts +5 -2
  40. package/dist/lib/browser/profiles.js +77 -37
  41. package/dist/lib/browser/service.d.ts +81 -13
  42. package/dist/lib/browser/service.js +738 -131
  43. package/dist/lib/browser/types.d.ts +81 -3
  44. package/dist/lib/browser/types.js +16 -0
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/commands.d.ts +1 -0
  48. package/dist/lib/commands.js +6 -2
  49. package/dist/lib/daemon.js +2 -3
  50. package/dist/lib/doctor-diff.js +4 -4
  51. package/dist/lib/events.js +2 -2
  52. package/dist/lib/hooks.d.ts +11 -7
  53. package/dist/lib/hooks.js +125 -49
  54. package/dist/lib/migrate.d.ts +1 -1
  55. package/dist/lib/migrate.js +1178 -21
  56. package/dist/lib/models.js +2 -2
  57. package/dist/lib/permissions.d.ts +8 -8
  58. package/dist/lib/permissions.js +8 -8
  59. package/dist/lib/plugins.d.ts +30 -1
  60. package/dist/lib/plugins.js +75 -3
  61. package/dist/lib/pty-server.js +9 -10
  62. package/dist/lib/resources/hooks.d.ts +5 -1
  63. package/dist/lib/resources/hooks.js +21 -4
  64. package/dist/lib/rotate.js +3 -4
  65. package/dist/lib/session/active.d.ts +3 -0
  66. package/dist/lib/session/active.js +92 -6
  67. package/dist/lib/session/cloud.js +2 -2
  68. package/dist/lib/session/db.js +8 -3
  69. package/dist/lib/session/discover.js +30 -15
  70. package/dist/lib/session/team-filter.js +2 -2
  71. package/dist/lib/shims.d.ts +2 -2
  72. package/dist/lib/shims.js +6 -6
  73. package/dist/lib/skills.js +6 -2
  74. package/dist/lib/state.d.ts +86 -14
  75. package/dist/lib/state.js +150 -23
  76. package/dist/lib/subagents.d.ts +28 -0
  77. package/dist/lib/subagents.js +98 -1
  78. package/dist/lib/sync-manifest.d.ts +1 -1
  79. package/dist/lib/sync-manifest.js +3 -3
  80. package/dist/lib/teams/persistence.js +15 -5
  81. package/dist/lib/teams/registry.js +2 -2
  82. package/dist/lib/types.d.ts +32 -3
  83. package/dist/lib/types.js +3 -3
  84. package/dist/lib/usage.js +2 -2
  85. package/dist/lib/versions.js +20 -21
  86. package/package.json +1 -1
  87. package/scripts/postinstall.js +1 -1
@@ -2,16 +2,16 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import * as fs from 'fs';
4
4
  import * as path from 'path';
5
- import * as yaml from 'yaml';
6
5
  import { AGENTS, ALL_AGENT_IDS, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
7
6
  import { formatUsageSection, formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
8
7
  import { readManifest } from '../lib/manifest.js';
9
8
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, resolveVersionAlias, getAvailableResources, getActuallySyncedResources, getNewResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, } from '../lib/versions.js';
10
9
  import { getShimsDir, isShimsInPath, ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
11
10
  import { getAgentResources } from '../lib/resources.js';
12
- import { getAgentsDir, getUserAgentsDir, getPromptcutsPath } from '../lib/state.js';
11
+ import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts } from '../lib/state.js';
13
12
  import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
14
13
  import { getCentralRulesFileName } from '../lib/rules/rules.js';
14
+ import { composeRulesFromState } from '../lib/rules/compose.js';
15
15
  import { getConfiguredRunStrategy } from '../lib/rotate.js';
16
16
  import { confirm } from '@inquirer/prompts';
17
17
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
@@ -512,25 +512,19 @@ async function showAgentResources(agentId, requestedVersion) {
512
512
  console.log(` ${display.padEnd(38)} ${pathStr}${syncStr}`);
513
513
  }
514
514
  }
515
- // Render the single ~/.agents/promptcuts.yaml (cross-agent, not per-version).
516
- // Reads the file to surface the shortcut count cheap (<1KB typical).
515
+ // Render promptcuts (cross-agent, not per-version). Shortcuts are layered
516
+ // across system + user files with user precedence; the displayed file path
517
+ // is whichever is "live" — user if it exists, else system.
517
518
  function renderPromptcuts() {
518
519
  console.log(chalk.bold(`\nPromptcuts\n`));
519
- const promptcutsPath = getPromptcutsPath();
520
- if (!fs.existsSync(promptcutsPath)) {
520
+ const merged = readMergedPromptcuts();
521
+ const count = Object.keys(merged).length;
522
+ if (count === 0) {
521
523
  console.log(` ${chalk.gray('none')}`);
522
524
  return;
523
525
  }
524
- let count = 0;
525
- try {
526
- const parsed = yaml.parse(fs.readFileSync(promptcutsPath, 'utf-8'));
527
- count = parsed?.shortcuts ? Object.keys(parsed.shortcuts).length : 0;
528
- }
529
- catch {
530
- count = 0;
531
- }
532
526
  const label = `${count} shortcut${count === 1 ? '' : 's'}`;
533
- console.log(` ${chalk.green(label).padEnd(24)} ${chalk.gray(formatPath(promptcutsPath, cwd))}`);
527
+ console.log(` ${chalk.green(label).padEnd(24)} ${chalk.gray(formatPath(getEffectivePromptcutsPath(), cwd))}`);
534
528
  }
535
529
  // 1. Agent CLI info
536
530
  console.log(chalk.bold('Agent CLIs\n'));
@@ -565,7 +559,58 @@ async function showAgentResources(agentId, requestedVersion) {
565
559
  }
566
560
  }
567
561
  renderSection('MCP Servers', agentData.mcp);
568
- renderSection('Rules', agentData.memory);
562
+ // Rules section with subrules breakdown
563
+ function renderRulesSection() {
564
+ console.log(chalk.bold('\nRules\n'));
565
+ const items = agentData.memory;
566
+ if (items.length === 0) {
567
+ console.log(` ${chalk.gray('none')}`);
568
+ return;
569
+ }
570
+ const versionStr = agentData.version ? ` (${agentData.version})` : '';
571
+ console.log(` ${chalk.bold(agentData.agentName)}${chalk.gray(versionStr)}:`);
572
+ // Get composed subrules for the user scope
573
+ let composedSubrules = [];
574
+ try {
575
+ const composed = composeRulesFromState({ cwd });
576
+ composedSubrules = composed.subrules;
577
+ }
578
+ catch {
579
+ // No preset configured or rules.yaml missing — show rules without subrule breakdown
580
+ }
581
+ for (const r of items) {
582
+ let nameColor = chalk.cyan;
583
+ if (r.syncState === 'synced')
584
+ nameColor = chalk.green;
585
+ else if (r.syncState === 'new')
586
+ nameColor = chalk.blue;
587
+ else if (r.syncState === 'modified')
588
+ nameColor = chalk.yellow;
589
+ else if (r.syncState === 'deleted')
590
+ nameColor = chalk.red;
591
+ let display = nameColor(r.name);
592
+ if (r.ruleCount !== undefined)
593
+ display += chalk.gray(` (${r.ruleCount} rules)`);
594
+ const sourceTag = r.scope === 'project' ? chalk.blue('[project]')
595
+ : r.scope === 'user' ? chalk.cyan('[user]')
596
+ : chalk.gray('[system]');
597
+ display += ` ${sourceTag}`;
598
+ const pathStr = r.path ? chalk.gray(formatPath(r.path, cwd)) : '';
599
+ const syncStr = r.syncState ? chalk.gray(` [${r.syncState}]`) : '';
600
+ console.log(` ${display.padEnd(38)} ${pathStr}${syncStr}`);
601
+ // Show subrules for user-scope rules (the compiled CLAUDE.md)
602
+ if (r.scope === 'user' && composedSubrules.length > 0) {
603
+ for (const sub of composedSubrules) {
604
+ const scopeLabel = sub.layerScope === 'project' ? chalk.blue('[project]')
605
+ : sub.layerScope === 'user' ? chalk.cyan('[user]')
606
+ : sub.layerScope === 'extra' ? chalk.magenta(`[${sub.layerAlias || 'extra'}]`)
607
+ : chalk.gray('[system]');
608
+ console.log(` ${chalk.gray('-')} ${sub.name} ${scopeLabel}`);
609
+ }
610
+ }
611
+ }
612
+ }
613
+ renderRulesSection();
569
614
  renderSection('Hooks', agentData.hooks);
570
615
  renderPromptcuts();
571
616
  // Show legend at the end if git repo exists
package/dist/index.d.ts CHANGED
@@ -3,6 +3,6 @@
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
  export {};
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,7 +118,8 @@ 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 {
@@ -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 };
@@ -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 {