@phnx-labs/agents-cli 1.19.1 → 1.20.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 +67 -0
- package/README.md +70 -10
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +3 -3
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/mcp.js +29 -0
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +56 -3
- package/dist/commands/routines.js +106 -13
- package/dist/commands/secrets.js +6 -8
- package/dist/commands/sessions.d.ts +36 -7
- package/dist/commands/sessions.js +130 -53
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +3 -3
- package/dist/commands/teams.js +13 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +147 -124
- package/dist/commands/view.js +12 -12
- package/dist/index.js +34 -6
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.js +162 -9
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +42 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +41 -1
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +21 -5
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/cli-resources.d.ts +109 -0
- package/dist/lib/cli-resources.js +255 -0
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/exec.d.ts +3 -2
- package/dist/lib/exec.js +62 -6
- package/dist/lib/hooks.js +182 -0
- package/dist/lib/mcp.js +6 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.js +5 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/profiles-presets.js +37 -0
- package/dist/lib/registry.d.ts +18 -0
- package/dist/lib/registry.js +44 -0
- package/dist/lib/resources/mcp.js +43 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +35 -0
- package/dist/lib/routines-format.js +173 -0
- package/dist/lib/routines.d.ts +7 -1
- package/dist/lib/routines.js +32 -12
- package/dist/lib/runner.js +19 -5
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
- package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +33 -2
- package/dist/lib/secrets/bundles.js +249 -26
- package/dist/lib/secrets/index.d.ts +10 -1
- package/dist/lib/secrets/index.js +143 -48
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +10 -4
- package/dist/lib/session/db.js +16 -16
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +6 -2
- package/dist/lib/shims.js +88 -10
- package/dist/lib/state.d.ts +0 -1
- package/dist/lib/state.js +2 -15
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +7 -4
- package/dist/lib/types.js +6 -3
- package/dist/lib/versions.d.ts +10 -2
- package/dist/lib/versions.js +227 -35
- package/package.json +9 -9
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/npm-shrinkwrap.json +0 -3162
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile +0 -0
|
@@ -1,6 +1,27 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
1
2
|
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
|
|
2
3
|
import { launchBrowser, getPortOccupant } from '../chrome.js';
|
|
3
4
|
import { parseEndpointUrl } from '../profiles.js';
|
|
5
|
+
/**
|
|
6
|
+
* Cheap TCP-level "is something bound here?" probe. Used as a fallback when
|
|
7
|
+
* `getPortOccupant()` (lsof-based) misses the process — Comet and other
|
|
8
|
+
* Electron apps sometimes hold a TCP socket that lsof's `-sTCP:LISTEN` filter
|
|
9
|
+
* doesn't report. If anything ACKs the connection, we treat the port as taken
|
|
10
|
+
* and surface a friendly error instead of silently auto-launching a fresh
|
|
11
|
+
* browser that would then conflict.
|
|
12
|
+
*/
|
|
13
|
+
async function probeTcpBound(port, host = '127.0.0.1', timeoutMs = 500) {
|
|
14
|
+
return new Promise((resolve) => {
|
|
15
|
+
const sock = net.createConnection({ port, host });
|
|
16
|
+
const cleanup = () => {
|
|
17
|
+
sock.removeAllListeners();
|
|
18
|
+
sock.destroy();
|
|
19
|
+
};
|
|
20
|
+
const timer = setTimeout(() => { cleanup(); resolve(false); }, timeoutMs);
|
|
21
|
+
sock.once('connect', () => { clearTimeout(timer); cleanup(); resolve(true); });
|
|
22
|
+
sock.once('error', () => { clearTimeout(timer); cleanup(); resolve(false); });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
4
25
|
/**
|
|
5
26
|
* Local-port listeners we refuse to attach through. These forward CDP traffic
|
|
6
27
|
* to a remote host — silently using them would let a `cdp://127.0.0.1:N`
|
|
@@ -34,7 +55,7 @@ export async function connectLocal(endpoint, profile) {
|
|
|
34
55
|
`ssh:// endpoint to drive the remote browser explicitly.`);
|
|
35
56
|
}
|
|
36
57
|
try {
|
|
37
|
-
const { wsUrl, browser } = await discoverBrowserWsUrl(port);
|
|
58
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(port, 'localhost', profile.name);
|
|
38
59
|
verifyBrowserIdentity(browser, profile.browser, port);
|
|
39
60
|
const cdp = new CDPClient();
|
|
40
61
|
await cdp.connect(wsUrl);
|
|
@@ -55,11 +76,31 @@ export async function connectLocal(endpoint, profile) {
|
|
|
55
76
|
`(\`kill ${occupant.pid}\`) or restart it with \`--remote-debugging-port=${port}\` ` +
|
|
56
77
|
`so profile "${profile.name}" can attach.`);
|
|
57
78
|
}
|
|
79
|
+
// lsof-based detection misses some Electron-family processes (Comet, custom
|
|
80
|
+
// chrome wrappers). Cheap TCP probe as a safety net: if something ACKs a
|
|
81
|
+
// connect, the port is bound — bail loudly with the profile name + endpoint
|
|
82
|
+
// rather than silently launching a duplicate browser.
|
|
83
|
+
if (await probeTcpBound(port)) {
|
|
84
|
+
throw new Error(`Profile "${profile.name}" is configured for cdp://127.0.0.1:${port}, ` +
|
|
85
|
+
`but something is already bound to that port without serving the Chrome ` +
|
|
86
|
+
`DevTools Protocol. If that's your browser running without remote debugging, ` +
|
|
87
|
+
`quit it and relaunch with \`--remote-debugging-port=${port}\`. Otherwise, ` +
|
|
88
|
+
`update the profile to a free port (\`agents browser profiles list\`).`);
|
|
89
|
+
}
|
|
58
90
|
const newPort = port;
|
|
59
91
|
const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
|
|
60
|
-
|
|
92
|
+
let launched;
|
|
93
|
+
try {
|
|
94
|
+
launched = await launchBrowser(profile.name, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary, profile.electron === true);
|
|
95
|
+
}
|
|
96
|
+
catch (launchErr) {
|
|
97
|
+
const reason = launchErr instanceof Error ? launchErr.message : String(launchErr);
|
|
98
|
+
throw new Error(`Could not start ${profile.browser} for profile "${profile.name}" on port ${newPort}: ${reason}. ` +
|
|
99
|
+
`Check that the browser binary is installed (\`agents browser profiles list\`) and ` +
|
|
100
|
+
`that no other process is holding the port.`);
|
|
101
|
+
}
|
|
61
102
|
const cdp = new CDPClient();
|
|
62
|
-
await cdp.connect(wsUrl);
|
|
63
|
-
return { cdp, port: newPort, pid };
|
|
103
|
+
await cdp.connect(launched.wsUrl);
|
|
104
|
+
return { cdp, port: newPort, pid: launched.pid };
|
|
64
105
|
}
|
|
65
106
|
}
|
|
@@ -63,7 +63,7 @@ export async function connectSSH(endpoint, profile) {
|
|
|
63
63
|
tunnel.kill();
|
|
64
64
|
throw new Error(`SSH tunnel failed to establish to ${host}`);
|
|
65
65
|
}
|
|
66
|
-
const { wsUrl, browser } = await discoverBrowserWsUrl(localPort);
|
|
66
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(localPort, 'localhost', profile.name);
|
|
67
67
|
try {
|
|
68
68
|
verifyBrowserIdentity(browser, profile.browser, remotePort, host);
|
|
69
69
|
}
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { BrowserService } from './service.js';
|
|
2
2
|
import type { IPCRequest, IPCResponse } from './types.js';
|
|
3
|
+
export interface IPCRequestOptions {
|
|
4
|
+
autoStartDaemon?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare class BrowserDaemonNotRunningError extends Error {
|
|
7
|
+
constructor();
|
|
8
|
+
}
|
|
9
|
+
export declare function formatBrowserDaemonNotRunningError(): string;
|
|
3
10
|
export declare function getSocketPath(): string;
|
|
4
11
|
export declare class BrowserIPCServer {
|
|
5
12
|
private server;
|
|
@@ -9,4 +16,4 @@ export declare class BrowserIPCServer {
|
|
|
9
16
|
stop(): Promise<void>;
|
|
10
17
|
private handleRequest;
|
|
11
18
|
}
|
|
12
|
-
export declare function sendIPCRequest(request: IPCRequest): Promise<IPCResponse>;
|
|
19
|
+
export declare function sendIPCRequest(request: IPCRequest, opts?: IPCRequestOptions): Promise<IPCResponse>;
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -5,9 +5,32 @@ import { getHelpersDir } from '../state.js';
|
|
|
5
5
|
import { startDaemon } from '../daemon.js';
|
|
6
6
|
import { getCliVersion } from '../version.js';
|
|
7
7
|
const SOCKET_NAME = 'browser.sock';
|
|
8
|
+
export class BrowserDaemonNotRunningError extends Error {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(formatBrowserDaemonNotRunningError());
|
|
11
|
+
this.name = 'BrowserDaemonNotRunningError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function formatBrowserDaemonNotRunningError() {
|
|
15
|
+
return [
|
|
16
|
+
'Browser daemon not running.',
|
|
17
|
+
'Start it with: agents browser start (auto-picks an installed browser)',
|
|
18
|
+
'Or pin a profile: agents browser start --profile <name>',
|
|
19
|
+
'List profiles: agents browser profiles list',
|
|
20
|
+
].join('\n');
|
|
21
|
+
}
|
|
8
22
|
export function getSocketPath() {
|
|
9
23
|
return path.join(getHelpersDir(), 'browser', SOCKET_NAME);
|
|
10
24
|
}
|
|
25
|
+
async function waitForSocket(socketPath, timeoutMs) {
|
|
26
|
+
const deadline = Date.now() + timeoutMs;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
if (fs.existsSync(socketPath))
|
|
29
|
+
return;
|
|
30
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
31
|
+
}
|
|
32
|
+
throw new Error('Timeout waiting for browser daemon socket');
|
|
33
|
+
}
|
|
11
34
|
export class BrowserIPCServer {
|
|
12
35
|
server = null;
|
|
13
36
|
service;
|
|
@@ -107,12 +130,14 @@ export class BrowserIPCServer {
|
|
|
107
130
|
taskName: request.taskName,
|
|
108
131
|
url: request.url,
|
|
109
132
|
endpointName: request.endpoint,
|
|
133
|
+
skipDomainSkill: request.skipDomainSkill,
|
|
110
134
|
});
|
|
111
135
|
return {
|
|
112
136
|
ok: true,
|
|
113
137
|
task: result.name,
|
|
114
138
|
tabId: result.tabId,
|
|
115
139
|
windowTargetId: result.windowId,
|
|
140
|
+
skill: result.skill,
|
|
116
141
|
};
|
|
117
142
|
}
|
|
118
143
|
case 'done': {
|
|
@@ -411,8 +436,8 @@ async function maybeWarnVersionMismatch() {
|
|
|
411
436
|
// itself a hint, but a noisy one. Stay silent on this path.
|
|
412
437
|
}
|
|
413
438
|
}
|
|
414
|
-
export async function sendIPCRequest(request) {
|
|
415
|
-
const result = await sendRawIPCRequest(request);
|
|
439
|
+
export async function sendIPCRequest(request, opts = {}) {
|
|
440
|
+
const result = await sendRawIPCRequest(request, opts);
|
|
416
441
|
// Run the version check after the user's request returns — keeps the
|
|
417
442
|
// critical path zero-overhead and ensures `start` doesn't get blocked
|
|
418
443
|
// on a daemon-restart warning that the user hasn't read yet.
|
|
@@ -421,38 +446,18 @@ export async function sendIPCRequest(request) {
|
|
|
421
446
|
}
|
|
422
447
|
return result;
|
|
423
448
|
}
|
|
424
|
-
async function sendRawIPCRequest(request) {
|
|
449
|
+
async function sendRawIPCRequest(request, opts = {}) {
|
|
425
450
|
const socketPath = getSocketPath();
|
|
451
|
+
const autoStartDaemon = opts.autoStartDaemon ?? true;
|
|
426
452
|
if (!fs.existsSync(socketPath)) {
|
|
453
|
+
if (!autoStartDaemon) {
|
|
454
|
+
throw new BrowserDaemonNotRunningError();
|
|
455
|
+
}
|
|
427
456
|
await fs.promises.mkdir(path.dirname(socketPath), { recursive: true, mode: 0o700 });
|
|
428
457
|
await fs.promises.chmod(path.dirname(socketPath), 0o700);
|
|
429
458
|
startDaemon();
|
|
430
459
|
if (!fs.existsSync(socketPath)) {
|
|
431
|
-
await
|
|
432
|
-
const socketDir = path.dirname(socketPath);
|
|
433
|
-
const socketName = path.basename(socketPath);
|
|
434
|
-
const watcher = fs.watch(socketDir, (_event, file) => {
|
|
435
|
-
if (file === socketName) {
|
|
436
|
-
clearTimeout(timeout);
|
|
437
|
-
watcher.close();
|
|
438
|
-
resolve();
|
|
439
|
-
}
|
|
440
|
-
});
|
|
441
|
-
watcher.on('error', (error) => {
|
|
442
|
-
clearTimeout(timeout);
|
|
443
|
-
watcher.close();
|
|
444
|
-
reject(error);
|
|
445
|
-
});
|
|
446
|
-
const timeout = setTimeout(() => {
|
|
447
|
-
watcher.close();
|
|
448
|
-
reject(new Error('Timeout waiting for browser daemon socket'));
|
|
449
|
-
}, 6000);
|
|
450
|
-
if (fs.existsSync(socketPath)) {
|
|
451
|
-
clearTimeout(timeout);
|
|
452
|
-
watcher.close();
|
|
453
|
-
resolve();
|
|
454
|
-
}
|
|
455
|
-
});
|
|
460
|
+
await waitForSocket(socketPath, 6000);
|
|
456
461
|
}
|
|
457
462
|
if (!fs.existsSync(socketPath)) {
|
|
458
463
|
throw new Error('Failed to start browser daemon');
|
|
@@ -475,6 +480,10 @@ async function sendRawIPCRequest(request) {
|
|
|
475
480
|
}
|
|
476
481
|
});
|
|
477
482
|
socket.on('error', (err) => {
|
|
483
|
+
if (!autoStartDaemon && (err.code === 'ENOENT' || err.code === 'ECONNREFUSED')) {
|
|
484
|
+
reject(new BrowserDaemonNotRunningError());
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
478
487
|
reject(new Error(`IPC error: ${err.message}`));
|
|
479
488
|
});
|
|
480
489
|
socket.on('close', () => {
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import type { BrowserProfile } from './types.js';
|
|
2
2
|
export type { BrowserProfile } from './types.js';
|
|
3
|
+
export declare const DEFAULT_BROWSER_PROFILE_NAME = "default";
|
|
3
4
|
export declare function getBrowserRuntimeDir(): string;
|
|
4
5
|
export declare function getProfileRuntimeDir(name: string): string;
|
|
5
6
|
export declare function listProfiles(): Promise<BrowserProfile[]>;
|
|
6
7
|
export declare function getProfile(name: string): Promise<BrowserProfile | null>;
|
|
8
|
+
/**
|
|
9
|
+
* Ensure a `default` profile exists, auto-picking the first installed
|
|
10
|
+
* Chromium-family browser per the platform priority list in chrome.ts.
|
|
11
|
+
*
|
|
12
|
+
* Re-uses an existing `default` profile as-is (we don't second-guess the user
|
|
13
|
+
* if they've already customized it). On first run we walk the priority list
|
|
14
|
+
* (macOS: chrome > brave > edge > chromium > comet; Linux: chrome > chromium >
|
|
15
|
+
* brave > edge; Windows: edge > chrome > brave) and pin the profile to the
|
|
16
|
+
* first match. Throws an actionable error if none of those binaries are
|
|
17
|
+
* installed so the user knows exactly which browsers we'd accept.
|
|
18
|
+
*/
|
|
19
|
+
export declare function ensureDefaultBrowserProfile(): Promise<BrowserProfile>;
|
|
7
20
|
/**
|
|
8
21
|
* Compute the LOCAL port a profile will occupy at runtime:
|
|
9
22
|
* - `cdp://127.0.0.1:N` → N (we listen on N directly)
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import * as path from 'path';
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, readMeta, writeMeta, } from '../state.js';
|
|
4
|
-
import { findBrowserPath } from './chrome.js';
|
|
4
|
+
import { findBrowserPath, findFirstInstalledBrowser } from './chrome.js';
|
|
5
|
+
import { DEFAULT_VIEWPORT } from './devices.js';
|
|
6
|
+
export const DEFAULT_BROWSER_PROFILE_NAME = 'default';
|
|
5
7
|
export function getBrowserRuntimeDir() {
|
|
6
8
|
return getBrowserRuntimeDirRoot();
|
|
7
9
|
}
|
|
@@ -67,6 +69,44 @@ export async function getProfile(name) {
|
|
|
67
69
|
return null;
|
|
68
70
|
return configToProfile(name, config);
|
|
69
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Ensure a `default` profile exists, auto-picking the first installed
|
|
74
|
+
* Chromium-family browser per the platform priority list in chrome.ts.
|
|
75
|
+
*
|
|
76
|
+
* Re-uses an existing `default` profile as-is (we don't second-guess the user
|
|
77
|
+
* if they've already customized it). On first run we walk the priority list
|
|
78
|
+
* (macOS: chrome > brave > edge > chromium > comet; Linux: chrome > chromium >
|
|
79
|
+
* brave > edge; Windows: edge > chrome > brave) and pin the profile to the
|
|
80
|
+
* first match. Throws an actionable error if none of those binaries are
|
|
81
|
+
* installed so the user knows exactly which browsers we'd accept.
|
|
82
|
+
*/
|
|
83
|
+
export async function ensureDefaultBrowserProfile() {
|
|
84
|
+
const existing = await getProfile(DEFAULT_BROWSER_PROFILE_NAME);
|
|
85
|
+
if (existing)
|
|
86
|
+
return existing;
|
|
87
|
+
const detected = findFirstInstalledBrowser();
|
|
88
|
+
if (!detected) {
|
|
89
|
+
throw new Error('No supported browser found. Install one of: Chrome, Brave, Edge, Chromium, or Comet, ' +
|
|
90
|
+
'then re-run `agents browser start`. Or create a profile explicitly with ' +
|
|
91
|
+
'`agents browser profiles create <name> --browser <chrome|comet|chromium|brave|edge|custom>`. ' +
|
|
92
|
+
'Note: Safari and Firefox are not supported — agents browser drives over the ' +
|
|
93
|
+
'Chrome DevTools Protocol, which they don\'t implement.');
|
|
94
|
+
}
|
|
95
|
+
const freePort = await findFreeProfilePort();
|
|
96
|
+
const profile = {
|
|
97
|
+
name: DEFAULT_BROWSER_PROFILE_NAME,
|
|
98
|
+
description: `Auto-detected ${detected.browserType} profile`,
|
|
99
|
+
browser: detected.browserType,
|
|
100
|
+
binary: detected.binary,
|
|
101
|
+
endpoints: [`cdp://127.0.0.1:${freePort}`],
|
|
102
|
+
viewport: {
|
|
103
|
+
width: DEFAULT_VIEWPORT.width,
|
|
104
|
+
height: DEFAULT_VIEWPORT.height,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
await createProfile(profile);
|
|
108
|
+
return profile;
|
|
109
|
+
}
|
|
70
110
|
/**
|
|
71
111
|
* Compute the LOCAL port a profile will occupy at runtime:
|
|
72
112
|
* - `cdp://127.0.0.1:N` → N (we listen on N directly)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ResolvedDomainSkill } from './domain-skills.js';
|
|
1
2
|
import { type TabInfo, type ProfileStatus, type HistoricalTask } from './types.js';
|
|
2
3
|
import { type RefOpts, type RefNode } from './refs.js';
|
|
3
4
|
import type { TargetFilter } from './types.js';
|
|
@@ -55,12 +56,14 @@ export declare class BrowserService {
|
|
|
55
56
|
taskName?: string;
|
|
56
57
|
url?: string;
|
|
57
58
|
endpointName?: string;
|
|
59
|
+
skipDomainSkill?: boolean;
|
|
58
60
|
}): Promise<{
|
|
59
61
|
task: string;
|
|
60
62
|
name: string;
|
|
61
63
|
tabId?: string;
|
|
62
64
|
windowId?: string;
|
|
63
65
|
profile: string;
|
|
66
|
+
skill?: ResolvedDomainSkill;
|
|
64
67
|
}>;
|
|
65
68
|
stop(taskName: string): Promise<{
|
|
66
69
|
ok: boolean;
|
|
@@ -3,12 +3,13 @@ import * as os from 'os';
|
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
import { execFile } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
|
-
import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from './cdp.js';
|
|
6
|
+
import { BrowserCdpConnectionError, CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity, } from './cdp.js';
|
|
7
7
|
import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, } from './profiles.js';
|
|
8
8
|
import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
|
|
9
9
|
import { connectLocal } from './drivers/local.js';
|
|
10
10
|
import { connectSSH, shellQuote } from './drivers/ssh.js';
|
|
11
11
|
import { clearProfileRuntime } from './runtime-state.js';
|
|
12
|
+
import { resolveDomainSkill } from './domain-skills.js';
|
|
12
13
|
import { generateTaskId, generateShortId, generateTaskName, } from './types.js';
|
|
13
14
|
import { getRefs, resolveRefToCoords } from './refs.js';
|
|
14
15
|
import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
@@ -347,7 +348,16 @@ export class BrowserService {
|
|
|
347
348
|
const result = await this.navigate(taskName, opts.url, effectiveProfileName);
|
|
348
349
|
tabId = result.tabId;
|
|
349
350
|
}
|
|
350
|
-
|
|
351
|
+
// Domain-skill discovery: when a URL is supplied, look up site-specific
|
|
352
|
+
// operating instructions and pass them back so the calling agent can pick
|
|
353
|
+
// them up alongside the task id. Failures swallowed by resolveDomainSkill.
|
|
354
|
+
let skill;
|
|
355
|
+
if (opts.url && !opts.skipDomainSkill) {
|
|
356
|
+
const resolved = resolveDomainSkill(opts.url);
|
|
357
|
+
if (resolved)
|
|
358
|
+
skill = resolved;
|
|
359
|
+
}
|
|
360
|
+
return { task: taskId, name: taskName, tabId, profile: effectiveProfileName, skill };
|
|
351
361
|
}
|
|
352
362
|
async stop(taskName) {
|
|
353
363
|
for (const [profileName, conn] of this.connections) {
|
|
@@ -1425,7 +1435,7 @@ export class BrowserService {
|
|
|
1425
1435
|
const existingInfo = getRunningChromeInfo(effectiveProfile.name);
|
|
1426
1436
|
if (existingInfo) {
|
|
1427
1437
|
try {
|
|
1428
|
-
const { wsUrl, browser } = await discoverBrowserWsUrl(existingInfo.port);
|
|
1438
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(existingInfo.port, 'localhost', effectiveProfile.name);
|
|
1429
1439
|
verifyBrowserIdentity(browser, effectiveProfile.browser, existingInfo.port);
|
|
1430
1440
|
const cdp = new CDPClient();
|
|
1431
1441
|
await cdp.connect(wsUrl);
|
|
@@ -1487,8 +1497,14 @@ export class BrowserService {
|
|
|
1487
1497
|
};
|
|
1488
1498
|
}
|
|
1489
1499
|
if (url.protocol === 'wss:' || url.protocol === 'ws:') {
|
|
1500
|
+
const port = parseInt(url.port || (url.protocol === 'wss:' ? '443' : '80'), 10);
|
|
1490
1501
|
const cdp = new CDPClient();
|
|
1491
|
-
|
|
1502
|
+
try {
|
|
1503
|
+
await cdp.connect(endpoint);
|
|
1504
|
+
}
|
|
1505
|
+
catch {
|
|
1506
|
+
throw new BrowserCdpConnectionError(port, profile.name, url.hostname || 'localhost');
|
|
1507
|
+
}
|
|
1492
1508
|
await this.enableDomains(cdp);
|
|
1493
1509
|
return {
|
|
1494
1510
|
cdp,
|
|
@@ -1502,7 +1518,7 @@ export class BrowserService {
|
|
|
1502
1518
|
}
|
|
1503
1519
|
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
1504
1520
|
const port = parseInt(url.port || (url.protocol === 'https:' ? '443' : '80'), 10);
|
|
1505
|
-
const { wsUrl, browser } = await discoverBrowserWsUrl(port, url.hostname);
|
|
1521
|
+
const { wsUrl, browser } = await discoverBrowserWsUrl(port, url.hostname, profile.name);
|
|
1506
1522
|
verifyBrowserIdentity(browser, profile.browser, port, url.hostname);
|
|
1507
1523
|
const cdp = new CDPClient();
|
|
1508
1524
|
await cdp.connect(wsUrl);
|
|
@@ -156,6 +156,7 @@ export interface IPCRequest {
|
|
|
156
156
|
since?: string;
|
|
157
157
|
until?: string;
|
|
158
158
|
appLevel?: string;
|
|
159
|
+
skipDomainSkill?: boolean;
|
|
159
160
|
}
|
|
160
161
|
/** Subset of IPCResponse describing a recording start result. */
|
|
161
162
|
export interface RecordStartFields {
|
|
@@ -200,6 +201,12 @@ export interface IPCResponse {
|
|
|
200
201
|
uploadMode?: 'input' | 'drop' | 'chooser';
|
|
201
202
|
appLogs?: any[];
|
|
202
203
|
version?: string;
|
|
204
|
+
skill?: {
|
|
205
|
+
name: string;
|
|
206
|
+
path: string;
|
|
207
|
+
content: string;
|
|
208
|
+
hostname: string;
|
|
209
|
+
};
|
|
203
210
|
}
|
|
204
211
|
export interface ConsoleEntry {
|
|
205
212
|
level: 'log' | 'info' | 'warn' | 'error';
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/** A single install method. Exactly one of the keys (npm/brew/script/binary) is set. */
|
|
2
|
+
export type InstallMethod = {
|
|
3
|
+
npm: string;
|
|
4
|
+
} | {
|
|
5
|
+
brew: string;
|
|
6
|
+
} | {
|
|
7
|
+
script: string;
|
|
8
|
+
} | {
|
|
9
|
+
binary: BinarySpec;
|
|
10
|
+
};
|
|
11
|
+
/** Per-platform binary download spec. Keys are `<os>-<arch>` (e.g. darwin-arm64). */
|
|
12
|
+
export interface BinarySpec {
|
|
13
|
+
[platform: string]: {
|
|
14
|
+
url: string;
|
|
15
|
+
/** Path inside the archive (relative). Required when url is a .tar.gz/.zip. */
|
|
16
|
+
extract?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** Parsed CLI manifest. */
|
|
20
|
+
export interface CliManifest {
|
|
21
|
+
/** Name as it appears on the command line (e.g. "higgsfield"). */
|
|
22
|
+
name: string;
|
|
23
|
+
/** One-line summary shown in `agents cli list`. */
|
|
24
|
+
description?: string;
|
|
25
|
+
/** Project homepage; used in detail view + post-install messaging. */
|
|
26
|
+
homepage?: string;
|
|
27
|
+
/** Command run to verify the binary is installed (default: "<name> --version"). */
|
|
28
|
+
check: string;
|
|
29
|
+
/** Install methods tried in order; first one whose tool is available is used. */
|
|
30
|
+
install: InstallMethod[];
|
|
31
|
+
/** Message printed after successful install — typically auth instructions. */
|
|
32
|
+
postInstall?: string;
|
|
33
|
+
/** Origin layer this manifest was resolved from. */
|
|
34
|
+
source: string;
|
|
35
|
+
/** Absolute path to the yaml file. */
|
|
36
|
+
path: string;
|
|
37
|
+
}
|
|
38
|
+
/** A validation problem in a CLI manifest. */
|
|
39
|
+
export interface CliManifestError {
|
|
40
|
+
/** Filename that failed to parse. */
|
|
41
|
+
file: string;
|
|
42
|
+
/** Human-readable reason. */
|
|
43
|
+
reason: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Parse a single CLI manifest from its YAML contents.
|
|
47
|
+
* Returns a manifest on success; throws on schema violations so callers can
|
|
48
|
+
* decide whether to surface or swallow the error per file.
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseCliManifest(contents: string, opts: {
|
|
51
|
+
name: string;
|
|
52
|
+
source: string;
|
|
53
|
+
path: string;
|
|
54
|
+
}): CliManifest;
|
|
55
|
+
/**
|
|
56
|
+
* Discover all CLI manifests resolvable from the current cwd. Returns valid
|
|
57
|
+
* manifests and any parse errors separately so the CLI can show both.
|
|
58
|
+
*/
|
|
59
|
+
export declare function listCliManifests(cwd?: string): {
|
|
60
|
+
manifests: CliManifest[];
|
|
61
|
+
errors: CliManifestError[];
|
|
62
|
+
};
|
|
63
|
+
/** Resolve a single CLI manifest by name. Returns null when not declared. */
|
|
64
|
+
export declare function resolveCliManifest(name: string, cwd?: string): CliManifest | null;
|
|
65
|
+
export declare function hasCommand(cmd: string): boolean;
|
|
66
|
+
/** Run the manifest's `check` command. Returns true when it exits 0. */
|
|
67
|
+
export declare function isCliInstalled(manifest: CliManifest): boolean;
|
|
68
|
+
/**
|
|
69
|
+
* Pick the first install method whose required host tool is available.
|
|
70
|
+
* Returns null when none of the declared methods can run on this host.
|
|
71
|
+
*/
|
|
72
|
+
export declare function selectInstallMethod(manifest: CliManifest): InstallMethod | null;
|
|
73
|
+
/** Short description of a method for display. */
|
|
74
|
+
export declare function describeMethod(method: InstallMethod): string;
|
|
75
|
+
export interface InstallResult {
|
|
76
|
+
manifest: CliManifest;
|
|
77
|
+
/** Method that was attempted (null if no compatible method existed). */
|
|
78
|
+
method: InstallMethod | null;
|
|
79
|
+
/** True when the post-install `check` passed. */
|
|
80
|
+
installed: boolean;
|
|
81
|
+
/** stdout/stderr captured from the install command, for surfacing on failure. */
|
|
82
|
+
output?: string;
|
|
83
|
+
/** Set when the install runner threw or exited non-zero. */
|
|
84
|
+
error?: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Install a single CLI by running its first compatible method. Streams the
|
|
88
|
+
* underlying command's output to the parent terminal so users see brew/npm
|
|
89
|
+
* progress live. Verifies success by re-running `check`.
|
|
90
|
+
*/
|
|
91
|
+
export declare function installCli(manifest: CliManifest, opts?: {
|
|
92
|
+
dryRun?: boolean;
|
|
93
|
+
}): InstallResult;
|
|
94
|
+
/**
|
|
95
|
+
* Map a declarative method to a shell command. Centralized so tests and dry-run
|
|
96
|
+
* surface the exact string that would execute.
|
|
97
|
+
*/
|
|
98
|
+
export declare function buildInstallCommand(method: InstallMethod): string;
|
|
99
|
+
export interface CliStatus {
|
|
100
|
+
manifest: CliManifest;
|
|
101
|
+
installed: boolean;
|
|
102
|
+
}
|
|
103
|
+
/** Convenience: list all manifests + their installed-on-host status. */
|
|
104
|
+
export declare function listCliStatus(cwd?: string): {
|
|
105
|
+
statuses: CliStatus[];
|
|
106
|
+
errors: CliManifestError[];
|
|
107
|
+
};
|
|
108
|
+
/** Names of CLIs that are declared but not currently installed on the host. */
|
|
109
|
+
export declare function getMissingClis(cwd?: string): CliManifest[];
|