@phnx-labs/agents-cli 1.14.2 → 1.14.4

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 (121) hide show
  1. package/README.md +17 -7
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.js +7 -0
  4. package/dist/commands/browser.d.ts +3 -0
  5. package/dist/commands/browser.js +392 -0
  6. package/dist/commands/daemon.js +1 -1
  7. package/dist/commands/doctor.d.ts +16 -9
  8. package/dist/commands/doctor.js +248 -12
  9. package/dist/commands/prune.js +9 -3
  10. package/dist/commands/refresh-rules.d.ts +15 -0
  11. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  12. package/dist/commands/routines.js +1 -1
  13. package/dist/commands/rules.js +100 -4
  14. package/dist/commands/secrets.js +198 -11
  15. package/dist/commands/sync.js +19 -0
  16. package/dist/commands/teams.js +184 -22
  17. package/dist/commands/trash.d.ts +10 -0
  18. package/dist/commands/trash.js +187 -0
  19. package/dist/commands/view.js +47 -14
  20. package/dist/index.js +62 -4
  21. package/dist/lib/agents.js +2 -2
  22. package/dist/lib/browser/cdp.d.ts +24 -0
  23. package/dist/lib/browser/cdp.js +94 -0
  24. package/dist/lib/browser/chrome.d.ts +16 -0
  25. package/dist/lib/browser/chrome.js +157 -0
  26. package/dist/lib/browser/drivers/local.d.ts +8 -0
  27. package/dist/lib/browser/drivers/local.js +22 -0
  28. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  29. package/dist/lib/browser/drivers/ssh.js +129 -0
  30. package/dist/lib/browser/index.d.ts +5 -0
  31. package/dist/lib/browser/index.js +5 -0
  32. package/dist/lib/browser/input.d.ts +6 -0
  33. package/dist/lib/browser/input.js +52 -0
  34. package/dist/lib/browser/ipc.d.ts +12 -0
  35. package/dist/lib/browser/ipc.js +223 -0
  36. package/dist/lib/browser/profiles.d.ts +11 -0
  37. package/dist/lib/browser/profiles.js +61 -0
  38. package/dist/lib/browser/refs.d.ts +21 -0
  39. package/dist/lib/browser/refs.js +88 -0
  40. package/dist/lib/browser/service.d.ts +45 -0
  41. package/dist/lib/browser/service.js +404 -0
  42. package/dist/lib/browser/types.d.ts +73 -0
  43. package/dist/lib/browser/types.js +7 -0
  44. package/dist/lib/cloud/codex.js +1 -1
  45. package/dist/lib/cloud/registry.js +2 -2
  46. package/dist/lib/cloud/rush.js +2 -2
  47. package/dist/lib/cloud/store.js +2 -2
  48. package/dist/lib/daemon.d.ts +1 -1
  49. package/dist/lib/daemon.js +47 -11
  50. package/dist/lib/diff-text.d.ts +25 -0
  51. package/dist/lib/diff-text.js +47 -0
  52. package/dist/lib/doctor-diff.d.ts +64 -0
  53. package/dist/lib/doctor-diff.js +497 -0
  54. package/dist/lib/git.js +3 -3
  55. package/dist/lib/hooks.d.ts +6 -0
  56. package/dist/lib/hooks.js +6 -1
  57. package/dist/lib/migrate.js +123 -0
  58. package/dist/lib/pty-client.js +3 -3
  59. package/dist/lib/pty-server.js +36 -7
  60. package/dist/lib/resources/commands.d.ts +46 -0
  61. package/dist/lib/resources/commands.js +208 -0
  62. package/dist/lib/resources/hooks.d.ts +12 -0
  63. package/dist/lib/resources/hooks.js +136 -0
  64. package/dist/lib/resources/index.d.ts +36 -0
  65. package/dist/lib/resources/index.js +69 -0
  66. package/dist/lib/resources/mcp.d.ts +34 -0
  67. package/dist/lib/resources/mcp.js +483 -0
  68. package/dist/lib/resources/permissions.d.ts +13 -0
  69. package/dist/lib/resources/permissions.js +184 -0
  70. package/dist/lib/resources/rules.d.ts +43 -0
  71. package/dist/lib/resources/rules.js +146 -0
  72. package/dist/lib/resources/skills.d.ts +37 -0
  73. package/dist/lib/resources/skills.js +238 -0
  74. package/dist/lib/resources/subagents.d.ts +46 -0
  75. package/dist/lib/resources/subagents.js +198 -0
  76. package/dist/lib/resources/types.d.ts +82 -0
  77. package/dist/lib/resources/types.js +8 -0
  78. package/dist/lib/resources.js +1 -1
  79. package/dist/lib/rotate.d.ts +8 -1
  80. package/dist/lib/rotate.js +17 -4
  81. package/dist/lib/rules/compile.d.ts +104 -0
  82. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  83. package/dist/lib/rules/compose.d.ts +78 -0
  84. package/dist/lib/rules/compose.js +170 -0
  85. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  86. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  87. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  88. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  89. package/dist/lib/secrets/bundles.d.ts +61 -4
  90. package/dist/lib/secrets/bundles.js +222 -54
  91. package/dist/lib/secrets/index.d.ts +24 -5
  92. package/dist/lib/secrets/index.js +70 -41
  93. package/dist/lib/session/active.js +5 -5
  94. package/dist/lib/session/db.js +4 -4
  95. package/dist/lib/session/discover.js +2 -2
  96. package/dist/lib/session/render.js +21 -7
  97. package/dist/lib/shims.d.ts +28 -4
  98. package/dist/lib/shims.js +72 -14
  99. package/dist/lib/state.d.ts +22 -28
  100. package/dist/lib/state.js +83 -78
  101. package/dist/lib/sync-manifest.d.ts +2 -2
  102. package/dist/lib/sync-manifest.js +5 -5
  103. package/dist/lib/teams/agents.d.ts +4 -2
  104. package/dist/lib/teams/agents.js +11 -4
  105. package/dist/lib/teams/api.d.ts +1 -1
  106. package/dist/lib/teams/api.js +2 -2
  107. package/dist/lib/teams/index.d.ts +1 -0
  108. package/dist/lib/teams/index.js +1 -0
  109. package/dist/lib/teams/persistence.js +3 -3
  110. package/dist/lib/teams/registry.d.ts +12 -1
  111. package/dist/lib/teams/registry.js +12 -2
  112. package/dist/lib/teams/worktree.d.ts +30 -0
  113. package/dist/lib/teams/worktree.js +96 -0
  114. package/dist/lib/types.d.ts +12 -6
  115. package/dist/lib/types.js +3 -3
  116. package/dist/lib/versions.d.ts +32 -3
  117. package/dist/lib/versions.js +147 -119
  118. package/package.json +3 -2
  119. package/scripts/postinstall.js +29 -0
  120. package/dist/commands/refresh-memory.d.ts +0 -15
  121. package/dist/lib/memory-compile.d.ts +0 -66
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ import { Command } from 'commander';
9
9
  import chalk from 'chalk';
10
10
  import ora from 'ora';
11
11
  import * as fs from 'fs';
12
+ import * as os from 'os';
12
13
  import * as path from 'path';
13
14
  import { fileURLToPath } from 'url';
14
15
  import { confirm, select } from '@inquirer/prompts';
@@ -41,13 +42,15 @@ import { registerRoutinesCommands } from './commands/routines.js';
41
42
  import { registerRunCommand } from './commands/exec.js';
42
43
  import { registerModelsCommand } from './commands/models.js';
43
44
  import { registerPruneCommand } from './commands/prune.js';
45
+ import { registerTrashCommands } from './commands/trash.js';
44
46
  import { registerDoctorCommand } from './commands/doctor.js';
45
47
  import { registerSubagentsCommands } from './commands/subagents.js';
46
48
  import { registerPluginsCommands } from './commands/plugins.js';
47
49
  import { registerSyncCommand } from './commands/sync.js';
48
- import { registerRefreshMemoryCommand } from './commands/refresh-memory.js';
50
+ import { registerRefreshRulesCommand } from './commands/refresh-rules.js';
49
51
  import { registerDriveCommands } from './commands/drive.js';
50
52
  import { registerPtyCommands } from './commands/pty.js';
53
+ import { registerBrowserCommand } from './commands/browser.js';
51
54
  import { registerProfilesCommands } from './commands/profiles.js';
52
55
  import { registerSecretsCommands } from './commands/secrets.js';
53
56
  import { registerFactoryCommands } from './commands/factory.js';
@@ -59,7 +62,7 @@ import { isPromptCancelled } from './commands/utils.js';
59
62
  import { getAgentsDir } from './lib/state.js';
60
63
  import { AGENTS } from './lib/agents.js';
61
64
  import { getGlobalDefault } from './lib/versions.js';
62
- import { addShimsToPath, ensureShimCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, } from './lib/shims.js';
65
+ import { addShimsToPath, ensureShimCurrent, getPathShadowingExecutable, getPathSetupInstructions, hasAliasShadowingShim, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
63
66
  const program = new Command();
64
67
  program
65
68
  .name('agents')
@@ -319,7 +322,7 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
319
322
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
320
323
  return;
321
324
  }
322
- if (requestedCommand === 'sync' || requestedCommand === 'refresh-memory') {
325
+ if (requestedCommand === 'sync' || requestedCommand === 'refresh-rules') {
323
326
  return;
324
327
  }
325
328
  const installedAgents = listAgentsWithInstalledVersions();
@@ -336,6 +339,13 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
336
339
  for (const notice of createdOrUpdated) {
337
340
  console.log(chalk.green(notice));
338
341
  }
342
+ // Best-effort: remove leftover ~/.agents/shims/<cli> files from the pre-split
343
+ // layout BEFORE running detection. These cause false-positive "shadowing"
344
+ // results that make the repair prompt loop forever (the prompt user said
345
+ // "yes" to never deletes the file; next invocation finds it again).
346
+ for (const agent of installedAgents) {
347
+ removeLegacyUserShim(agent);
348
+ }
339
349
  const defaultAgents = installedAgents.filter((agent) => getGlobalDefault(agent));
340
350
  const shadowed = defaultAgents
341
351
  .map((agent) => ({ agent, shadowedBy: getPathShadowingExecutable(agent) }))
@@ -348,6 +358,15 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
348
358
  if (shadowed.length === 0 && isShimsInPath()) {
349
359
  return;
350
360
  }
361
+ // Suppress repeated prompts within the same shell. A successful rc-file
362
+ // edit doesn't reload the parent shell, so the next invocation sees the
363
+ // same PATH and re-fires detection. The sentinel survives only as long as
364
+ // the parent shell process — once the user opens a new terminal, the
365
+ // PPID changes and the prompt is allowed again.
366
+ const sentinelPath = path.join(os.tmpdir(), `agents-shim-prompted-${process.ppid}`);
367
+ if (fs.existsSync(sentinelPath)) {
368
+ return;
369
+ }
351
370
  const affected = [];
352
371
  for (const { agent, shadowedBy } of shadowed) {
353
372
  affected.push(`${AGENTS[agent].cliCommand} -> ${shadowedBy}`);
@@ -368,6 +387,10 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
368
387
  if (!shouldRepair) {
369
388
  console.log(chalk.yellow('Shim integration still needs attention.'));
370
389
  console.log(chalk.gray(getPathSetupInstructions()));
390
+ try {
391
+ fs.writeFileSync(sentinelPath, '1');
392
+ }
393
+ catch { /* best-effort */ }
371
394
  return;
372
395
  }
373
396
  const pathResult = addShimsToPath();
@@ -383,6 +406,10 @@ async function maybeBootstrapShimIntegration(requestedCommand) {
383
406
  console.log(chalk.green(`Repaired shim PATH setup in ~/${pathResult.rcFile}`));
384
407
  }
385
408
  console.log(chalk.gray(getPathSetupInstructions()));
409
+ try {
410
+ fs.writeFileSync(sentinelPath, '1');
411
+ }
412
+ catch { /* best-effort */ }
386
413
  }
387
414
  // Register all commands
388
415
  registerViewCommand(program);
@@ -424,6 +451,7 @@ registerRoutinesCommands(program);
424
451
  registerRunCommand(program);
425
452
  registerModelsCommand(program);
426
453
  registerPruneCommand(program);
454
+ registerTrashCommands(program);
427
455
  registerDoctorCommand(program);
428
456
  // Deprecated 'exec' alias for 'run'
429
457
  program
@@ -440,12 +468,13 @@ registerProfilesCommands(program);
440
468
  registerSecretsCommands(program);
441
469
  registerBetaCommands(program);
442
470
  registerSyncCommand(program);
443
- registerRefreshMemoryCommand(program);
471
+ registerRefreshRulesCommand(program);
444
472
  registerDriveCommands(program);
445
473
  registerFactoryCommands(program);
446
474
  registerUsageCommand(program);
447
475
  registerAliasCommand(program);
448
476
  registerPtyCommands(program);
477
+ registerBrowserCommand(program);
449
478
  // Deprecated 'jobs' and 'cron' aliases for 'routines'
450
479
  for (const alias of ['jobs', 'cron']) {
451
480
  program
@@ -610,6 +639,35 @@ if (!firstRun && requestedCommand && SYSTEM_REPO_COMMANDS.has(requestedCommand))
610
639
  const { ensureInitialized } = await import('./commands/init.js');
611
640
  await ensureInitialized(program);
612
641
  }
642
+ // One-shot idempotent migrations (split-layout, legacy file moves).
643
+ // Each step is internally guarded by existence checks so it's safe to run
644
+ // every invocation. A sentinel file in the system dir short-circuits the
645
+ // scan once a migration version has run, so the hot path stays cheap.
646
+ // AGENTS_SKIP_MIGRATION=1 disables the bootstrap-time run for tests and
647
+ // scripted invocations that prepare their own legacy fixtures.
648
+ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
649
+ try {
650
+ const { runMigration } = await import('./lib/migrate.js');
651
+ const sentinel = path.join(getAgentsDir(), '.migrated');
652
+ const sentinelValue = `${VERSION}-v2`;
653
+ let needRun = true;
654
+ try {
655
+ if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
656
+ needRun = false;
657
+ }
658
+ }
659
+ catch { /* best-effort — fall through to run */ }
660
+ if (needRun) {
661
+ runMigration();
662
+ try {
663
+ fs.mkdirSync(path.dirname(sentinel), { recursive: true });
664
+ fs.writeFileSync(sentinel, sentinelValue);
665
+ }
666
+ catch { /* best-effort */ }
667
+ }
668
+ }
669
+ catch { /* migration must never block CLI startup */ }
670
+ }
613
671
  try {
614
672
  await maybeBootstrapShimIntegration(requestedCommand);
615
673
  await program.parseAsync();
@@ -155,7 +155,7 @@ export const AGENTS = {
155
155
  format: 'markdown',
156
156
  variableSyntax: '$ARGUMENTS',
157
157
  supportsHooks: true,
158
- capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, memoryImports: true },
158
+ capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, rulesImports: true },
159
159
  },
160
160
  // codex hooks: gated to >= 0.116.0 (introduced [features] codex_hooks flag).
161
161
  codex: {
@@ -192,7 +192,7 @@ export const AGENTS = {
192
192
  supportsHooks: true,
193
193
  nativeAgentsSkillsDir: true,
194
194
  // gemini hooks: shipped in v0.26.0 (Jan 2026); older binaries silently ignore the `hooks` key.
195
- capabilities: { hooks: { since: '0.26.0' }, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, memoryImports: true },
195
+ capabilities: { hooks: { since: '0.26.0' }, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, rulesImports: true },
196
196
  },
197
197
  cursor: {
198
198
  id: 'cursor',
@@ -0,0 +1,24 @@
1
+ type EventHandler = (params: Record<string, unknown>) => void;
2
+ export declare class CDPClient {
3
+ private ws;
4
+ private messageId;
5
+ private pending;
6
+ private eventHandlers;
7
+ connect(wsUrl: string): Promise<void>;
8
+ send<T = unknown>(method: string, params?: Record<string, unknown>, sessionId?: string): Promise<T>;
9
+ on(event: string, handler: EventHandler): void;
10
+ off(event: string, handler: EventHandler): void;
11
+ close(): void;
12
+ get connected(): boolean;
13
+ get isOpen(): boolean;
14
+ private handleMessage;
15
+ private handleClose;
16
+ }
17
+ export declare function discoverBrowserWsUrl(port: number, host?: string): Promise<string>;
18
+ export declare function listTargets(port: number, host?: string): Promise<Array<{
19
+ id: string;
20
+ type: string;
21
+ title: string;
22
+ url: string;
23
+ }>>;
24
+ export {};
@@ -0,0 +1,94 @@
1
+ export class CDPClient {
2
+ ws = null;
3
+ messageId = 0;
4
+ pending = new Map();
5
+ eventHandlers = new Map();
6
+ async connect(wsUrl) {
7
+ return new Promise((resolve, reject) => {
8
+ this.ws = new WebSocket(wsUrl);
9
+ this.ws.onopen = () => resolve();
10
+ this.ws.onerror = (ev) => reject(new Error('WebSocket error'));
11
+ this.ws.onclose = () => this.handleClose();
12
+ this.ws.onmessage = (ev) => this.handleMessage(String(ev.data));
13
+ });
14
+ }
15
+ async send(method, params, sessionId) {
16
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
17
+ throw new Error('CDP connection not open');
18
+ }
19
+ const id = ++this.messageId;
20
+ const message = sessionId
21
+ ? JSON.stringify({ id, method, params, sessionId })
22
+ : JSON.stringify({ id, method, params });
23
+ return new Promise((resolve, reject) => {
24
+ this.pending.set(id, { resolve: resolve, reject });
25
+ this.ws.send(message);
26
+ });
27
+ }
28
+ on(event, handler) {
29
+ if (!this.eventHandlers.has(event)) {
30
+ this.eventHandlers.set(event, new Set());
31
+ }
32
+ this.eventHandlers.get(event).add(handler);
33
+ }
34
+ off(event, handler) {
35
+ this.eventHandlers.get(event)?.delete(handler);
36
+ }
37
+ close() {
38
+ if (this.ws) {
39
+ this.ws.close();
40
+ this.ws = null;
41
+ }
42
+ }
43
+ get connected() {
44
+ return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
45
+ }
46
+ get isOpen() {
47
+ return this.connected;
48
+ }
49
+ handleMessage(data) {
50
+ const msg = JSON.parse(data);
51
+ if ('id' in msg) {
52
+ const pending = this.pending.get(msg.id);
53
+ if (pending) {
54
+ this.pending.delete(msg.id);
55
+ if ('error' in msg) {
56
+ pending.reject(new Error(msg.error.message || 'CDP error'));
57
+ }
58
+ else {
59
+ pending.resolve(msg.result);
60
+ }
61
+ }
62
+ }
63
+ else if ('method' in msg) {
64
+ const handlers = this.eventHandlers.get(msg.method);
65
+ if (handlers) {
66
+ for (const handler of handlers) {
67
+ handler(msg.params || {});
68
+ }
69
+ }
70
+ }
71
+ }
72
+ handleClose() {
73
+ for (const pending of this.pending.values()) {
74
+ pending.reject(new Error('CDP connection closed'));
75
+ }
76
+ this.pending.clear();
77
+ this.ws = null;
78
+ }
79
+ }
80
+ export async function discoverBrowserWsUrl(port, host = 'localhost') {
81
+ const response = await fetch(`http://${host}:${port}/json/version`);
82
+ if (!response.ok) {
83
+ throw new Error(`Failed to discover browser: ${response.status}`);
84
+ }
85
+ const data = (await response.json());
86
+ return data.webSocketDebuggerUrl;
87
+ }
88
+ export async function listTargets(port, host = 'localhost') {
89
+ const response = await fetch(`http://${host}:${port}/json`);
90
+ if (!response.ok) {
91
+ throw new Error(`Failed to list targets: ${response.status}`);
92
+ }
93
+ return response.json();
94
+ }
@@ -0,0 +1,16 @@
1
+ import type { ChromeOptions } from './types.js';
2
+ import type { BrowserType } from './types.js';
3
+ export declare function findBrowserPath(browserType: BrowserType): string;
4
+ export interface LaunchResult {
5
+ pid: number;
6
+ port: number;
7
+ wsUrl: string;
8
+ }
9
+ export declare function launchBrowser(profileName: string, browserType: BrowserType, port: number, options?: ChromeOptions, secrets?: string): Promise<LaunchResult>;
10
+ export declare function attachToChrome(port: number): Promise<string>;
11
+ export declare function killChrome(pid: number): void;
12
+ export declare function getRunningChromeInfo(profileName: string): {
13
+ pid: number;
14
+ port: number;
15
+ } | null;
16
+ export declare function allocatePort(): number;
@@ -0,0 +1,157 @@
1
+ import { spawn, execSync } from 'child_process';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { getProfileRuntimeDir } from './profiles.js';
6
+ import { discoverBrowserWsUrl } from './cdp.js';
7
+ import { readBundle, resolveBundleEnv, bundleExists } from '../secrets/bundles.js';
8
+ const BROWSER_PATHS = {
9
+ darwin: {
10
+ chrome: [
11
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
12
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
13
+ ],
14
+ comet: ['/Applications/Comet.app/Contents/MacOS/Comet'],
15
+ chromium: ['/Applications/Chromium.app/Contents/MacOS/Chromium'],
16
+ brave: ['/Applications/Brave Browser.app/Contents/MacOS/Brave Browser'],
17
+ edge: ['/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],
18
+ },
19
+ linux: {
20
+ chrome: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable'],
21
+ comet: [],
22
+ chromium: ['/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium'],
23
+ brave: ['/usr/bin/brave-browser', '/usr/bin/brave'],
24
+ edge: ['/usr/bin/microsoft-edge'],
25
+ },
26
+ win32: {
27
+ chrome: [
28
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
29
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
30
+ ],
31
+ comet: [],
32
+ chromium: [],
33
+ brave: [
34
+ 'C:\\Program Files\\BraveSoftware\\Brave-Browser\\Application\\brave.exe',
35
+ ],
36
+ edge: [
37
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
38
+ ],
39
+ },
40
+ };
41
+ export function findBrowserPath(browserType) {
42
+ const platform = os.platform();
43
+ const platformPaths = BROWSER_PATHS[platform];
44
+ if (!platformPaths) {
45
+ throw new Error(`Unsupported platform: ${platform}`);
46
+ }
47
+ const candidates = platformPaths[browserType] || [];
48
+ for (const p of candidates) {
49
+ if (fs.existsSync(p)) {
50
+ return p;
51
+ }
52
+ }
53
+ throw new Error(`Browser "${browserType}" not found. Install it first.`);
54
+ }
55
+ export async function launchBrowser(profileName, browserType, port, options = {}, secrets) {
56
+ const browserPath = findBrowserPath(browserType);
57
+ const runtimeDir = getProfileRuntimeDir(profileName);
58
+ const userDataDir = path.join(runtimeDir, 'chrome-data');
59
+ fs.mkdirSync(userDataDir, { recursive: true });
60
+ const args = [
61
+ `--remote-debugging-port=${port}`,
62
+ `--user-data-dir=${userDataDir}`,
63
+ '--remote-allow-origins=*',
64
+ '--disable-background-timer-throttling',
65
+ '--disable-backgrounding-occluded-windows',
66
+ '--disable-renderer-backgrounding',
67
+ ...(options.headless ? ['--headless=new'] : []),
68
+ ...(options.args || []),
69
+ ];
70
+ let env = { ...process.env };
71
+ if (secrets && bundleExists(secrets)) {
72
+ try {
73
+ const bundle = readBundle(secrets);
74
+ const bundleEnv = resolveBundleEnv(bundle);
75
+ env = { ...env, ...bundleEnv };
76
+ }
77
+ catch {
78
+ // Bundle failed to resolve, continue without secrets
79
+ }
80
+ }
81
+ const child = spawn(browserPath, args, {
82
+ detached: true,
83
+ stdio: 'ignore',
84
+ env,
85
+ });
86
+ child.unref();
87
+ const pid = child.pid;
88
+ fs.writeFileSync(path.join(runtimeDir, 'pid'), String(pid));
89
+ fs.writeFileSync(path.join(runtimeDir, 'port'), String(port));
90
+ let wsUrl = null;
91
+ for (let i = 0; i < 30; i++) {
92
+ await sleep(200);
93
+ try {
94
+ wsUrl = await discoverBrowserWsUrl(port);
95
+ break;
96
+ }
97
+ catch {
98
+ // Chrome still starting
99
+ }
100
+ }
101
+ if (!wsUrl) {
102
+ throw new Error('Chrome failed to start within 6 seconds');
103
+ }
104
+ return { pid, port, wsUrl };
105
+ }
106
+ export async function attachToChrome(port) {
107
+ return discoverBrowserWsUrl(port);
108
+ }
109
+ export function killChrome(pid) {
110
+ try {
111
+ process.kill(pid, 'SIGTERM');
112
+ }
113
+ catch {
114
+ // Process already dead
115
+ }
116
+ }
117
+ export function getRunningChromeInfo(profileName) {
118
+ const runtimeDir = getProfileRuntimeDir(profileName);
119
+ const pidFile = path.join(runtimeDir, 'pid');
120
+ const portFile = path.join(runtimeDir, 'port');
121
+ if (!fs.existsSync(pidFile) || !fs.existsSync(portFile)) {
122
+ return null;
123
+ }
124
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
125
+ const port = parseInt(fs.readFileSync(portFile, 'utf-8').trim(), 10);
126
+ if (!isProcessRunning(pid)) {
127
+ fs.unlinkSync(pidFile);
128
+ fs.unlinkSync(portFile);
129
+ return null;
130
+ }
131
+ return { pid, port };
132
+ }
133
+ function isProcessRunning(pid) {
134
+ try {
135
+ process.kill(pid, 0);
136
+ return true;
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ }
142
+ function sleep(ms) {
143
+ return new Promise((resolve) => setTimeout(resolve, ms));
144
+ }
145
+ export function allocatePort() {
146
+ const base = 9200;
147
+ const max = 9300;
148
+ for (let port = base; port < max; port++) {
149
+ try {
150
+ execSync(`lsof -i :${port}`, { stdio: 'ignore' });
151
+ }
152
+ catch {
153
+ return port;
154
+ }
155
+ }
156
+ throw new Error('No available ports in range 9200-9300');
157
+ }
@@ -0,0 +1,8 @@
1
+ import { CDPClient } from '../cdp.js';
2
+ import type { BrowserProfile } from '../types.js';
3
+ export interface LocalConnection {
4
+ cdp: CDPClient;
5
+ port: number;
6
+ pid: number;
7
+ }
8
+ export declare function connectLocal(endpoint: string, profile: BrowserProfile): Promise<LocalConnection>;
@@ -0,0 +1,22 @@
1
+ import { CDPClient, discoverBrowserWsUrl } from '../cdp.js';
2
+ import { launchBrowser, allocatePort } from '../chrome.js';
3
+ export async function connectLocal(endpoint, profile) {
4
+ const url = new URL(endpoint);
5
+ if (url.protocol !== 'cdp:') {
6
+ throw new Error(`Invalid local endpoint: ${endpoint}`);
7
+ }
8
+ const port = parseInt(url.port, 10) || 9222;
9
+ try {
10
+ const wsUrl = await discoverBrowserWsUrl(port);
11
+ const cdp = new CDPClient();
12
+ await cdp.connect(wsUrl);
13
+ return { cdp, port, pid: 0 };
14
+ }
15
+ catch {
16
+ const newPort = allocatePort();
17
+ const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, profile.chrome, profile.secrets);
18
+ const cdp = new CDPClient();
19
+ await cdp.connect(wsUrl);
20
+ return { cdp, port: newPort, pid };
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ import { CDPClient } from '../cdp.js';
2
+ import type { BrowserProfile } from '../types.js';
3
+ export interface SSHConnection {
4
+ cdp: CDPClient;
5
+ port: number;
6
+ pid: number;
7
+ cleanup: () => void;
8
+ }
9
+ export declare function connectSSH(endpoint: string, profile: BrowserProfile): Promise<SSHConnection>;
@@ -0,0 +1,129 @@
1
+ import { spawn } from 'child_process';
2
+ import * as net from 'net';
3
+ import { CDPClient, discoverBrowserWsUrl } from '../cdp.js';
4
+ import { allocatePort } from '../chrome.js';
5
+ export async function connectSSH(endpoint, profile) {
6
+ const url = new URL(endpoint);
7
+ if (url.protocol !== 'ssh:') {
8
+ throw new Error(`Invalid SSH endpoint: ${endpoint}`);
9
+ }
10
+ const user = url.username || process.env.USER || 'root';
11
+ const host = url.hostname;
12
+ const remotePort = parseInt(url.searchParams.get('port') || '9222', 10);
13
+ const localPort = allocatePort();
14
+ try {
15
+ await ensureRemoteBrowser(user, host, profile.browser, remotePort);
16
+ }
17
+ catch {
18
+ // Browser may already be running, continue
19
+ }
20
+ const tunnel = await startSSHTunnel(user, host, localPort, remotePort);
21
+ try {
22
+ await waitForPort(localPort, 8000);
23
+ }
24
+ catch {
25
+ tunnel.kill();
26
+ throw new Error(`SSH tunnel failed to establish to ${host}`);
27
+ }
28
+ const wsUrl = await discoverBrowserWsUrl(localPort);
29
+ const cdp = new CDPClient();
30
+ await cdp.connect(wsUrl);
31
+ return {
32
+ cdp,
33
+ port: localPort,
34
+ pid: tunnel.pid || 0,
35
+ cleanup: () => {
36
+ cdp.close();
37
+ tunnel.kill();
38
+ },
39
+ };
40
+ }
41
+ function startSSHTunnel(user, host, localPort, remotePort) {
42
+ return new Promise((resolve, reject) => {
43
+ const args = [
44
+ '-L',
45
+ `${localPort}:127.0.0.1:${remotePort}`,
46
+ `${user}@${host}`,
47
+ '-N',
48
+ '-o',
49
+ 'StrictHostKeyChecking=accept-new',
50
+ '-o',
51
+ 'BatchMode=yes',
52
+ '-o',
53
+ 'ConnectTimeout=10',
54
+ ];
55
+ const tunnel = spawn('ssh', args, {
56
+ stdio: ['ignore', 'ignore', 'pipe'],
57
+ detached: false,
58
+ });
59
+ let stderr = '';
60
+ tunnel.stderr?.on('data', (data) => {
61
+ stderr += data.toString();
62
+ });
63
+ tunnel.on('error', (err) => {
64
+ reject(new Error(`SSH tunnel failed: ${err.message}`));
65
+ });
66
+ setTimeout(() => {
67
+ if (tunnel.killed) {
68
+ reject(new Error(`SSH tunnel died: ${stderr}`));
69
+ }
70
+ else {
71
+ resolve(tunnel);
72
+ }
73
+ }, 500);
74
+ });
75
+ }
76
+ async function waitForPort(port, timeoutMs) {
77
+ const start = Date.now();
78
+ while (Date.now() - start < timeoutMs) {
79
+ try {
80
+ await tryConnect(port);
81
+ return;
82
+ }
83
+ catch {
84
+ await sleep(200);
85
+ }
86
+ }
87
+ throw new Error(`Port ${port} not ready after ${timeoutMs}ms`);
88
+ }
89
+ function tryConnect(port) {
90
+ return new Promise((resolve, reject) => {
91
+ const socket = net.createConnection({ port, host: '127.0.0.1' });
92
+ socket.on('connect', () => {
93
+ socket.destroy();
94
+ resolve();
95
+ });
96
+ socket.on('error', reject);
97
+ });
98
+ }
99
+ async function ensureRemoteBrowser(user, host, browserType, port) {
100
+ const browserPaths = {
101
+ chrome: '/Applications/Google\\ Chrome.app/Contents/MacOS/Google\\ Chrome',
102
+ comet: '/Applications/Comet.app/Contents/MacOS/Comet',
103
+ chromium: '/Applications/Chromium.app/Contents/MacOS/Chromium',
104
+ brave: '/Applications/Brave\\ Browser.app/Contents/MacOS/Brave\\ Browser',
105
+ edge: '/Applications/Microsoft\\ Edge.app/Contents/MacOS/Microsoft\\ Edge',
106
+ };
107
+ const browserPath = browserPaths[browserType];
108
+ if (!browserPath) {
109
+ throw new Error(`Unknown browser type: ${browserType}`);
110
+ }
111
+ const remoteCmd = `${browserPath} --remote-debugging-port=${port} '--remote-allow-origins=*' --disable-background-timer-throttling --user-data-dir=/tmp/agents-browser-${port} </dev/null >/dev/null 2>&1 &`;
112
+ return new Promise((resolve, reject) => {
113
+ const child = spawn('ssh', [
114
+ `${user}@${host}`,
115
+ '-o',
116
+ 'BatchMode=yes',
117
+ remoteCmd,
118
+ ], { stdio: 'ignore' });
119
+ child.on('close', () => resolve());
120
+ child.on('error', reject);
121
+ setTimeout(() => {
122
+ child.kill();
123
+ resolve();
124
+ }, 2000);
125
+ });
126
+ }
127
+ function sleep(ms) {
128
+ return new Promise((resolve) => setTimeout(resolve, ms));
129
+ }
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './profiles.js';
3
+ export * from './cdp.js';
4
+ export * from './chrome.js';
5
+ export * from './service.js';
@@ -0,0 +1,5 @@
1
+ export * from './types.js';
2
+ export * from './profiles.js';
3
+ export * from './cdp.js';
4
+ export * from './chrome.js';
5
+ export * from './service.js';
@@ -0,0 +1,6 @@
1
+ import type { CDPClient } from './cdp.js';
2
+ export declare function clickAtCoords(cdp: CDPClient, sessionId: string, x: number, y: number): Promise<void>;
3
+ export declare function hoverAtCoords(cdp: CDPClient, sessionId: string, x: number, y: number): Promise<void>;
4
+ export declare function typeText(cdp: CDPClient, sessionId: string, text: string): Promise<void>;
5
+ export declare function pressKey(cdp: CDPClient, sessionId: string, keyName: string): Promise<void>;
6
+ export declare function focusNode(cdp: CDPClient, sessionId: string, backendNodeId: number): Promise<void>;