@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.
- package/README.md +17 -7
- package/dist/browser.d.ts +2 -0
- package/dist/browser.js +7 -0
- package/dist/commands/browser.d.ts +3 -0
- package/dist/commands/browser.js +392 -0
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +16 -9
- package/dist/commands/doctor.js +248 -12
- package/dist/commands/prune.js +9 -3
- package/dist/commands/refresh-rules.d.ts +15 -0
- package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
- package/dist/commands/routines.js +1 -1
- package/dist/commands/rules.js +100 -4
- package/dist/commands/secrets.js +198 -11
- package/dist/commands/sync.js +19 -0
- package/dist/commands/teams.js +184 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +47 -14
- package/dist/index.js +62 -4
- package/dist/lib/agents.js +2 -2
- package/dist/lib/browser/cdp.d.ts +24 -0
- package/dist/lib/browser/cdp.js +94 -0
- package/dist/lib/browser/chrome.d.ts +16 -0
- package/dist/lib/browser/chrome.js +157 -0
- package/dist/lib/browser/drivers/local.d.ts +8 -0
- package/dist/lib/browser/drivers/local.js +22 -0
- package/dist/lib/browser/drivers/ssh.d.ts +9 -0
- package/dist/lib/browser/drivers/ssh.js +129 -0
- package/dist/lib/browser/index.d.ts +5 -0
- package/dist/lib/browser/index.js +5 -0
- package/dist/lib/browser/input.d.ts +6 -0
- package/dist/lib/browser/input.js +52 -0
- package/dist/lib/browser/ipc.d.ts +12 -0
- package/dist/lib/browser/ipc.js +223 -0
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +61 -0
- package/dist/lib/browser/refs.d.ts +21 -0
- package/dist/lib/browser/refs.js +88 -0
- package/dist/lib/browser/service.d.ts +45 -0
- package/dist/lib/browser/service.js +404 -0
- package/dist/lib/browser/types.d.ts +73 -0
- package/dist/lib/browser/types.js +7 -0
- package/dist/lib/cloud/codex.js +1 -1
- package/dist/lib/cloud/registry.js +2 -2
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/daemon.d.ts +1 -1
- package/dist/lib/daemon.js +47 -11
- package/dist/lib/diff-text.d.ts +25 -0
- package/dist/lib/diff-text.js +47 -0
- package/dist/lib/doctor-diff.d.ts +64 -0
- package/dist/lib/doctor-diff.js +497 -0
- package/dist/lib/git.js +3 -3
- package/dist/lib/hooks.d.ts +6 -0
- package/dist/lib/hooks.js +6 -1
- package/dist/lib/migrate.js +123 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- package/dist/lib/resources/commands.d.ts +46 -0
- package/dist/lib/resources/commands.js +208 -0
- package/dist/lib/resources/hooks.d.ts +12 -0
- package/dist/lib/resources/hooks.js +136 -0
- package/dist/lib/resources/index.d.ts +36 -0
- package/dist/lib/resources/index.js +69 -0
- package/dist/lib/resources/mcp.d.ts +34 -0
- package/dist/lib/resources/mcp.js +483 -0
- package/dist/lib/resources/permissions.d.ts +13 -0
- package/dist/lib/resources/permissions.js +184 -0
- package/dist/lib/resources/rules.d.ts +43 -0
- package/dist/lib/resources/rules.js +146 -0
- package/dist/lib/resources/skills.d.ts +37 -0
- package/dist/lib/resources/skills.js +238 -0
- package/dist/lib/resources/subagents.d.ts +46 -0
- package/dist/lib/resources/subagents.js +198 -0
- package/dist/lib/resources/types.d.ts +82 -0
- package/dist/lib/resources/types.js +8 -0
- package/dist/lib/resources.js +1 -1
- package/dist/lib/rotate.d.ts +8 -1
- package/dist/lib/rotate.js +17 -4
- package/dist/lib/rules/compile.d.ts +104 -0
- package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
- package/dist/lib/rules/compose.d.ts +78 -0
- package/dist/lib/rules/compose.js +170 -0
- package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
- package/dist/lib/{memory.js → rules/rules.js} +10 -10
- package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/dist/lib/secrets/bundles.d.ts +61 -4
- package/dist/lib/secrets/bundles.js +222 -54
- package/dist/lib/secrets/index.d.ts +24 -5
- package/dist/lib/secrets/index.js +70 -41
- package/dist/lib/session/active.js +5 -5
- package/dist/lib/session/db.js +4 -4
- package/dist/lib/session/discover.js +2 -2
- package/dist/lib/session/render.js +21 -7
- package/dist/lib/shims.d.ts +28 -4
- package/dist/lib/shims.js +72 -14
- package/dist/lib/state.d.ts +22 -28
- package/dist/lib/state.js +83 -78
- package/dist/lib/sync-manifest.d.ts +2 -2
- package/dist/lib/sync-manifest.js +5 -5
- package/dist/lib/teams/agents.d.ts +4 -2
- package/dist/lib/teams/agents.js +11 -4
- package/dist/lib/teams/api.d.ts +1 -1
- package/dist/lib/teams/api.js +2 -2
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/persistence.js +3 -3
- package/dist/lib/teams/registry.d.ts +12 -1
- package/dist/lib/teams/registry.js +12 -2
- package/dist/lib/teams/worktree.d.ts +30 -0
- package/dist/lib/teams/worktree.js +96 -0
- package/dist/lib/types.d.ts +12 -6
- package/dist/lib/types.js +3 -3
- package/dist/lib/versions.d.ts +32 -3
- package/dist/lib/versions.js +147 -119
- package/package.json +3 -2
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- 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 {
|
|
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-
|
|
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
|
-
|
|
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();
|
package/dist/lib/agents.js
CHANGED
|
@@ -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,
|
|
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,
|
|
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,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>;
|