@phnx-labs/agents-cli 1.14.2 → 1.14.3
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/commands/browser.d.ts +2 -0
- package/dist/commands/browser.js +388 -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 +162 -22
- package/dist/commands/trash.d.ts +10 -0
- package/dist/commands/trash.js +187 -0
- package/dist/commands/view.js +46 -13
- 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 +77 -0
- package/dist/lib/pty-client.js +3 -3
- package/dist/lib/pty-server.js +36 -7
- 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 -76
- 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 +8 -1
- package/dist/lib/teams/registry.js +8 -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 +30 -2
- package/dist/lib/versions.js +127 -105
- package/package.json +1 -1
- package/scripts/postinstall.js +29 -0
- package/dist/commands/refresh-memory.d.ts +0 -15
- package/dist/lib/memory-compile.d.ts +0 -66
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export async function clickAtCoords(cdp, sessionId, x, y) {
|
|
2
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mousePressed', x, y, button: 'left', clickCount: 1 }, sessionId);
|
|
3
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseReleased', x, y, button: 'left', clickCount: 1 }, sessionId);
|
|
4
|
+
}
|
|
5
|
+
export async function hoverAtCoords(cdp, sessionId, x, y) {
|
|
6
|
+
await cdp.send('Input.dispatchMouseEvent', { type: 'mouseMoved', x, y }, sessionId);
|
|
7
|
+
}
|
|
8
|
+
export async function typeText(cdp, sessionId, text) {
|
|
9
|
+
for (const char of text) {
|
|
10
|
+
await cdp.send('Input.dispatchKeyEvent', { type: 'keyDown', text: char }, sessionId);
|
|
11
|
+
await cdp.send('Input.dispatchKeyEvent', { type: 'keyUp', text: char }, sessionId);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const KEY_CODES = {
|
|
15
|
+
Enter: { key: 'Enter', code: 'Enter', keyCode: 13 },
|
|
16
|
+
Tab: { key: 'Tab', code: 'Tab', keyCode: 9 },
|
|
17
|
+
Escape: { key: 'Escape', code: 'Escape', keyCode: 27 },
|
|
18
|
+
Backspace: { key: 'Backspace', code: 'Backspace', keyCode: 8 },
|
|
19
|
+
Delete: { key: 'Delete', code: 'Delete', keyCode: 46 },
|
|
20
|
+
ArrowUp: { key: 'ArrowUp', code: 'ArrowUp', keyCode: 38 },
|
|
21
|
+
ArrowDown: { key: 'ArrowDown', code: 'ArrowDown', keyCode: 40 },
|
|
22
|
+
ArrowLeft: { key: 'ArrowLeft', code: 'ArrowLeft', keyCode: 37 },
|
|
23
|
+
ArrowRight: { key: 'ArrowRight', code: 'ArrowRight', keyCode: 39 },
|
|
24
|
+
Home: { key: 'Home', code: 'Home', keyCode: 36 },
|
|
25
|
+
End: { key: 'End', code: 'End', keyCode: 35 },
|
|
26
|
+
PageUp: { key: 'PageUp', code: 'PageUp', keyCode: 33 },
|
|
27
|
+
PageDown: { key: 'PageDown', code: 'PageDown', keyCode: 34 },
|
|
28
|
+
Space: { key: ' ', code: 'Space', keyCode: 32 },
|
|
29
|
+
};
|
|
30
|
+
export async function pressKey(cdp, sessionId, keyName) {
|
|
31
|
+
const keyInfo = KEY_CODES[keyName];
|
|
32
|
+
if (!keyInfo) {
|
|
33
|
+
throw new Error(`Unknown key: ${keyName}. Valid: ${Object.keys(KEY_CODES).join(', ')}`);
|
|
34
|
+
}
|
|
35
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
36
|
+
type: 'keyDown',
|
|
37
|
+
key: keyInfo.key,
|
|
38
|
+
code: keyInfo.code,
|
|
39
|
+
windowsVirtualKeyCode: keyInfo.keyCode,
|
|
40
|
+
nativeVirtualKeyCode: keyInfo.keyCode,
|
|
41
|
+
}, sessionId);
|
|
42
|
+
await cdp.send('Input.dispatchKeyEvent', {
|
|
43
|
+
type: 'keyUp',
|
|
44
|
+
key: keyInfo.key,
|
|
45
|
+
code: keyInfo.code,
|
|
46
|
+
windowsVirtualKeyCode: keyInfo.keyCode,
|
|
47
|
+
nativeVirtualKeyCode: keyInfo.keyCode,
|
|
48
|
+
}, sessionId);
|
|
49
|
+
}
|
|
50
|
+
export async function focusNode(cdp, sessionId, backendNodeId) {
|
|
51
|
+
await cdp.send('DOM.focus', { backendNodeId }, sessionId);
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { BrowserService } from './service.js';
|
|
2
|
+
import type { IPCRequest, IPCResponse } from './types.js';
|
|
3
|
+
export declare function getSocketPath(): string;
|
|
4
|
+
export declare class BrowserIPCServer {
|
|
5
|
+
private server;
|
|
6
|
+
private service;
|
|
7
|
+
constructor(service: BrowserService);
|
|
8
|
+
start(): Promise<void>;
|
|
9
|
+
stop(): Promise<void>;
|
|
10
|
+
private handleRequest;
|
|
11
|
+
}
|
|
12
|
+
export declare function sendIPCRequest(request: IPCRequest): Promise<IPCResponse>;
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { getAgentsDir } from '../state.js';
|
|
5
|
+
import { startDaemon } from '../daemon.js';
|
|
6
|
+
const SOCKET_NAME = 'browser.sock';
|
|
7
|
+
export function getSocketPath() {
|
|
8
|
+
return path.join(getAgentsDir(), SOCKET_NAME);
|
|
9
|
+
}
|
|
10
|
+
export class BrowserIPCServer {
|
|
11
|
+
server = null;
|
|
12
|
+
service;
|
|
13
|
+
constructor(service) {
|
|
14
|
+
this.service = service;
|
|
15
|
+
}
|
|
16
|
+
async start() {
|
|
17
|
+
const socketPath = getSocketPath();
|
|
18
|
+
if (fs.existsSync(socketPath)) {
|
|
19
|
+
fs.unlinkSync(socketPath);
|
|
20
|
+
}
|
|
21
|
+
this.server = net.createServer((socket) => {
|
|
22
|
+
let buffer = '';
|
|
23
|
+
socket.on('data', async (data) => {
|
|
24
|
+
buffer += data.toString();
|
|
25
|
+
const lines = buffer.split('\n');
|
|
26
|
+
buffer = lines.pop() || '';
|
|
27
|
+
for (const line of lines) {
|
|
28
|
+
if (!line.trim())
|
|
29
|
+
continue;
|
|
30
|
+
try {
|
|
31
|
+
const request = JSON.parse(line);
|
|
32
|
+
const response = await this.handleRequest(request);
|
|
33
|
+
socket.write(JSON.stringify(response) + '\n');
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
const error = err instanceof Error ? err.message : 'Unknown error';
|
|
37
|
+
socket.write(JSON.stringify({ ok: false, error }) + '\n');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
socket.on('error', () => {
|
|
42
|
+
// Client disconnected
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
this.server.listen(socketPath, () => {
|
|
47
|
+
fs.chmodSync(socketPath, 0o600);
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
this.server.on('error', reject);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async stop() {
|
|
54
|
+
if (this.server) {
|
|
55
|
+
this.server.close();
|
|
56
|
+
this.server = null;
|
|
57
|
+
}
|
|
58
|
+
const socketPath = getSocketPath();
|
|
59
|
+
if (fs.existsSync(socketPath)) {
|
|
60
|
+
fs.unlinkSync(socketPath);
|
|
61
|
+
}
|
|
62
|
+
await this.service.shutdown();
|
|
63
|
+
}
|
|
64
|
+
async handleRequest(request) {
|
|
65
|
+
switch (request.action) {
|
|
66
|
+
case 'start': {
|
|
67
|
+
if (!request.profile) {
|
|
68
|
+
return { ok: false, error: 'Profile required' };
|
|
69
|
+
}
|
|
70
|
+
const result = await this.service.start(request.profile, request.task);
|
|
71
|
+
return { ok: true, task: result.task, windowTargetId: result.windowTargetId };
|
|
72
|
+
}
|
|
73
|
+
case 'stop': {
|
|
74
|
+
if (request.task) {
|
|
75
|
+
const result = await this.service.stop(request.task);
|
|
76
|
+
return { ok: result.ok, error: result.ok ? undefined : 'Task not found' };
|
|
77
|
+
}
|
|
78
|
+
if (request.profile) {
|
|
79
|
+
await this.service.stopProfile(request.profile);
|
|
80
|
+
return { ok: true };
|
|
81
|
+
}
|
|
82
|
+
return { ok: false, error: 'Task or profile required' };
|
|
83
|
+
}
|
|
84
|
+
case 'status': {
|
|
85
|
+
const profiles = await this.service.status(request.profile);
|
|
86
|
+
return { ok: true, profiles };
|
|
87
|
+
}
|
|
88
|
+
case 'navigate': {
|
|
89
|
+
if (!request.task || !request.url) {
|
|
90
|
+
return { ok: false, error: 'Task and URL required' };
|
|
91
|
+
}
|
|
92
|
+
const result = await this.service.navigate(request.task, request.url, request.profile);
|
|
93
|
+
return { ok: true, tabId: result.tabId };
|
|
94
|
+
}
|
|
95
|
+
case 'tabs': {
|
|
96
|
+
const tabs = await this.service.tabs(request.task, request.profile);
|
|
97
|
+
return { ok: true, tabs };
|
|
98
|
+
}
|
|
99
|
+
case 'close': {
|
|
100
|
+
if (!request.task) {
|
|
101
|
+
return { ok: false, error: 'Task required' };
|
|
102
|
+
}
|
|
103
|
+
await this.service.close(request.task, request.tabId);
|
|
104
|
+
return { ok: true };
|
|
105
|
+
}
|
|
106
|
+
case 'evaluate': {
|
|
107
|
+
if (!request.task || request.tabId === undefined || !request.expr) {
|
|
108
|
+
return { ok: false, error: 'Task, tabId, and expression required' };
|
|
109
|
+
}
|
|
110
|
+
const result = await this.service.evaluate(request.task, request.tabId, request.expr);
|
|
111
|
+
return { ok: true, result };
|
|
112
|
+
}
|
|
113
|
+
case 'screenshot': {
|
|
114
|
+
if (!request.task) {
|
|
115
|
+
return { ok: false, error: 'Task required' };
|
|
116
|
+
}
|
|
117
|
+
const resultPath = await this.service.screenshot(request.task, request.tabId, request.path);
|
|
118
|
+
return { ok: true, path: resultPath };
|
|
119
|
+
}
|
|
120
|
+
case 'refs': {
|
|
121
|
+
if (!request.task) {
|
|
122
|
+
return { ok: false, error: 'Task required' };
|
|
123
|
+
}
|
|
124
|
+
const { refs } = await this.service.refs(request.task, request.tabId, {
|
|
125
|
+
interactive: request.interactive ?? true,
|
|
126
|
+
limit: request.limit ?? 500,
|
|
127
|
+
});
|
|
128
|
+
return { ok: true, refs };
|
|
129
|
+
}
|
|
130
|
+
case 'click': {
|
|
131
|
+
if (!request.task || !request.tabId || request.ref === undefined) {
|
|
132
|
+
return { ok: false, error: 'Task, tabId, and ref required' };
|
|
133
|
+
}
|
|
134
|
+
await this.service.click(request.task, request.tabId, request.ref);
|
|
135
|
+
return { ok: true };
|
|
136
|
+
}
|
|
137
|
+
case 'type': {
|
|
138
|
+
if (!request.task || !request.tabId || request.ref === undefined || !request.text) {
|
|
139
|
+
return { ok: false, error: 'Task, tabId, ref, and text required' };
|
|
140
|
+
}
|
|
141
|
+
await this.service.type(request.task, request.tabId, request.ref, request.text);
|
|
142
|
+
return { ok: true };
|
|
143
|
+
}
|
|
144
|
+
case 'press': {
|
|
145
|
+
if (!request.task || !request.tabId || !request.key) {
|
|
146
|
+
return { ok: false, error: 'Task, tabId, and key required' };
|
|
147
|
+
}
|
|
148
|
+
await this.service.press(request.task, request.tabId, request.key);
|
|
149
|
+
return { ok: true };
|
|
150
|
+
}
|
|
151
|
+
case 'hover': {
|
|
152
|
+
if (!request.task || !request.tabId || request.ref === undefined) {
|
|
153
|
+
return { ok: false, error: 'Task, tabId, and ref required' };
|
|
154
|
+
}
|
|
155
|
+
await this.service.hover(request.task, request.tabId, request.ref);
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
return { ok: false, error: `Unknown action: ${request.action}` };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function sendIPCRequest(request) {
|
|
164
|
+
const socketPath = getSocketPath();
|
|
165
|
+
if (!fs.existsSync(socketPath)) {
|
|
166
|
+
await fs.promises.mkdir(path.dirname(socketPath), { recursive: true });
|
|
167
|
+
startDaemon();
|
|
168
|
+
if (!fs.existsSync(socketPath)) {
|
|
169
|
+
await new Promise((resolve, reject) => {
|
|
170
|
+
const socketDir = path.dirname(socketPath);
|
|
171
|
+
const socketName = path.basename(socketPath);
|
|
172
|
+
const watcher = fs.watch(socketDir, (_event, file) => {
|
|
173
|
+
if (file === socketName) {
|
|
174
|
+
clearTimeout(timeout);
|
|
175
|
+
watcher.close();
|
|
176
|
+
resolve();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
watcher.on('error', (error) => {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
watcher.close();
|
|
182
|
+
reject(error);
|
|
183
|
+
});
|
|
184
|
+
const timeout = setTimeout(() => {
|
|
185
|
+
watcher.close();
|
|
186
|
+
reject(new Error('Timeout waiting for browser daemon socket'));
|
|
187
|
+
}, 6000);
|
|
188
|
+
if (fs.existsSync(socketPath)) {
|
|
189
|
+
clearTimeout(timeout);
|
|
190
|
+
watcher.close();
|
|
191
|
+
resolve();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
if (!fs.existsSync(socketPath)) {
|
|
196
|
+
throw new Error('Failed to start browser daemon');
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const socket = net.createConnection(socketPath);
|
|
201
|
+
let buffer = '';
|
|
202
|
+
socket.on('connect', () => {
|
|
203
|
+
socket.write(JSON.stringify(request) + '\n');
|
|
204
|
+
});
|
|
205
|
+
socket.on('data', (data) => {
|
|
206
|
+
buffer += data.toString();
|
|
207
|
+
const idx = buffer.indexOf('\n');
|
|
208
|
+
if (idx !== -1) {
|
|
209
|
+
const response = JSON.parse(buffer.slice(0, idx));
|
|
210
|
+
socket.end();
|
|
211
|
+
resolve(response);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
socket.on('error', (err) => {
|
|
215
|
+
reject(new Error(`IPC error: ${err.message}`));
|
|
216
|
+
});
|
|
217
|
+
socket.on('close', () => {
|
|
218
|
+
if (!buffer.includes('\n')) {
|
|
219
|
+
reject(new Error('Connection closed before response'));
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { BrowserProfile } from './types.js';
|
|
2
|
+
export type { BrowserProfile } from './types.js';
|
|
3
|
+
export declare function getBrowserProfilesDir(): string;
|
|
4
|
+
export declare function getBrowserRuntimeDir(): string;
|
|
5
|
+
export declare function getProfilePath(name: string): string;
|
|
6
|
+
export declare function getProfileRuntimeDir(name: string): string;
|
|
7
|
+
export declare function listProfiles(): Promise<BrowserProfile[]>;
|
|
8
|
+
export declare function getProfile(name: string): Promise<BrowserProfile | null>;
|
|
9
|
+
export declare function createProfile(profile: BrowserProfile): Promise<void>;
|
|
10
|
+
export declare function updateProfile(profile: BrowserProfile): Promise<void>;
|
|
11
|
+
export declare function deleteProfile(name: string): Promise<void>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import * as yaml from 'yaml';
|
|
5
|
+
import { getUserAgentsDir } from '../state.js';
|
|
6
|
+
export function getBrowserProfilesDir() {
|
|
7
|
+
return path.join(getUserAgentsDir(), 'browser', 'profiles');
|
|
8
|
+
}
|
|
9
|
+
export function getBrowserRuntimeDir() {
|
|
10
|
+
const agentsDir = path.join(os.homedir(), '.agents');
|
|
11
|
+
return path.join(agentsDir, 'browser');
|
|
12
|
+
}
|
|
13
|
+
export function getProfilePath(name) {
|
|
14
|
+
return path.join(getBrowserProfilesDir(), `${name}.yaml`);
|
|
15
|
+
}
|
|
16
|
+
export function getProfileRuntimeDir(name) {
|
|
17
|
+
return path.join(getBrowserRuntimeDir(), name);
|
|
18
|
+
}
|
|
19
|
+
export async function listProfiles() {
|
|
20
|
+
const dir = getBrowserProfilesDir();
|
|
21
|
+
if (!fs.existsSync(dir))
|
|
22
|
+
return [];
|
|
23
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith('.yaml'));
|
|
24
|
+
const profiles = [];
|
|
25
|
+
for (const file of files) {
|
|
26
|
+
const content = fs.readFileSync(path.join(dir, file), 'utf-8');
|
|
27
|
+
const profile = yaml.parse(content);
|
|
28
|
+
profiles.push(profile);
|
|
29
|
+
}
|
|
30
|
+
return profiles;
|
|
31
|
+
}
|
|
32
|
+
export async function getProfile(name) {
|
|
33
|
+
const filePath = getProfilePath(name);
|
|
34
|
+
if (!fs.existsSync(filePath))
|
|
35
|
+
return null;
|
|
36
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
37
|
+
return yaml.parse(content);
|
|
38
|
+
}
|
|
39
|
+
export async function createProfile(profile) {
|
|
40
|
+
const dir = getBrowserProfilesDir();
|
|
41
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
42
|
+
const filePath = getProfilePath(profile.name);
|
|
43
|
+
if (fs.existsSync(filePath)) {
|
|
44
|
+
throw new Error(`Profile "${profile.name}" already exists`);
|
|
45
|
+
}
|
|
46
|
+
fs.writeFileSync(filePath, yaml.stringify(profile), 'utf-8');
|
|
47
|
+
}
|
|
48
|
+
export async function updateProfile(profile) {
|
|
49
|
+
const filePath = getProfilePath(profile.name);
|
|
50
|
+
if (!fs.existsSync(filePath)) {
|
|
51
|
+
throw new Error(`Profile "${profile.name}" does not exist`);
|
|
52
|
+
}
|
|
53
|
+
fs.writeFileSync(filePath, yaml.stringify(profile), 'utf-8');
|
|
54
|
+
}
|
|
55
|
+
export async function deleteProfile(name) {
|
|
56
|
+
const filePath = getProfilePath(name);
|
|
57
|
+
if (!fs.existsSync(filePath)) {
|
|
58
|
+
throw new Error(`Profile "${name}" does not exist`);
|
|
59
|
+
}
|
|
60
|
+
fs.unlinkSync(filePath);
|
|
61
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { CDPClient } from './cdp.js';
|
|
2
|
+
export interface RefOpts {
|
|
3
|
+
interactive?: boolean;
|
|
4
|
+
limit?: number;
|
|
5
|
+
compact?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface RefNode {
|
|
8
|
+
ref: number;
|
|
9
|
+
role: string;
|
|
10
|
+
name: string;
|
|
11
|
+
attrs: string[];
|
|
12
|
+
backendNodeId?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function getRefs(cdp: CDPClient, sessionId: string, opts?: RefOpts): Promise<{
|
|
15
|
+
refs: string;
|
|
16
|
+
nodeMap: Map<number, RefNode>;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function resolveRefToCoords(cdp: CDPClient, sessionId: string, nodeMap: Map<number, RefNode>, ref: number): Promise<{
|
|
19
|
+
x: number;
|
|
20
|
+
y: number;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
const INTERACTIVE_ROLES = new Set([
|
|
2
|
+
'button',
|
|
3
|
+
'link',
|
|
4
|
+
'textbox',
|
|
5
|
+
'checkbox',
|
|
6
|
+
'radio',
|
|
7
|
+
'combobox',
|
|
8
|
+
'listbox',
|
|
9
|
+
'option',
|
|
10
|
+
'menuitem',
|
|
11
|
+
'tab',
|
|
12
|
+
'slider',
|
|
13
|
+
'spinbutton',
|
|
14
|
+
'searchbox',
|
|
15
|
+
'switch',
|
|
16
|
+
'menuitemcheckbox',
|
|
17
|
+
'menuitemradio',
|
|
18
|
+
'treeitem',
|
|
19
|
+
]);
|
|
20
|
+
export async function getRefs(cdp, sessionId, opts = {}) {
|
|
21
|
+
const { interactive = true, limit = 500, compact = false } = opts;
|
|
22
|
+
const { nodes } = (await cdp.send('Accessibility.getFullAXTree', {}, sessionId));
|
|
23
|
+
const nodeMap = new Map();
|
|
24
|
+
const lines = [];
|
|
25
|
+
let refCounter = 1;
|
|
26
|
+
for (const node of nodes) {
|
|
27
|
+
if (refCounter > limit)
|
|
28
|
+
break;
|
|
29
|
+
const role = node.role?.value?.toLowerCase() || '';
|
|
30
|
+
if (!role || role === 'none' || role === 'generic')
|
|
31
|
+
continue;
|
|
32
|
+
if (interactive && !INTERACTIVE_ROLES.has(role))
|
|
33
|
+
continue;
|
|
34
|
+
const name = node.name?.value || '';
|
|
35
|
+
const attrs = [];
|
|
36
|
+
for (const prop of node.properties || []) {
|
|
37
|
+
const propName = prop.name;
|
|
38
|
+
const propValue = prop.value?.value;
|
|
39
|
+
if (propName === 'disabled' && propValue === true)
|
|
40
|
+
attrs.push('disabled');
|
|
41
|
+
if (propName === 'checked' && propValue === true)
|
|
42
|
+
attrs.push('checked');
|
|
43
|
+
if (propName === 'selected' && propValue === true)
|
|
44
|
+
attrs.push('selected');
|
|
45
|
+
if (propName === 'expanded' && propValue === true)
|
|
46
|
+
attrs.push('expanded');
|
|
47
|
+
if (propName === 'required' && propValue === true)
|
|
48
|
+
attrs.push('required');
|
|
49
|
+
if (propName === 'readonly' && propValue === true)
|
|
50
|
+
attrs.push('readonly');
|
|
51
|
+
if (propName === 'invalid' && propValue === true)
|
|
52
|
+
attrs.push('invalid');
|
|
53
|
+
}
|
|
54
|
+
const ref = refCounter++;
|
|
55
|
+
const refNode = {
|
|
56
|
+
ref,
|
|
57
|
+
role,
|
|
58
|
+
name,
|
|
59
|
+
attrs,
|
|
60
|
+
backendNodeId: node.backendDOMNodeId,
|
|
61
|
+
};
|
|
62
|
+
nodeMap.set(ref, refNode);
|
|
63
|
+
const attrStr = attrs.length > 0 ? ` [${attrs.join('] [')}]` : '';
|
|
64
|
+
const nameStr = name ? ` "${truncate(name, 50)}"` : '';
|
|
65
|
+
const line = compact
|
|
66
|
+
? `${role}${nameStr} [ref=${ref}]${attrStr}`
|
|
67
|
+
: `- ${role}${nameStr} [ref=${ref}]${attrStr}`;
|
|
68
|
+
lines.push(line);
|
|
69
|
+
}
|
|
70
|
+
return { refs: lines.join('\n'), nodeMap };
|
|
71
|
+
}
|
|
72
|
+
function truncate(s, max) {
|
|
73
|
+
if (s.length <= max)
|
|
74
|
+
return s;
|
|
75
|
+
return s.slice(0, max - 1) + '…';
|
|
76
|
+
}
|
|
77
|
+
export async function resolveRefToCoords(cdp, sessionId, nodeMap, ref) {
|
|
78
|
+
const node = nodeMap.get(ref);
|
|
79
|
+
if (!node)
|
|
80
|
+
throw new Error(`Ref ${ref} not found`);
|
|
81
|
+
if (!node.backendNodeId)
|
|
82
|
+
throw new Error(`Ref ${ref} has no DOM node`);
|
|
83
|
+
const { model } = (await cdp.send('DOM.getBoxModel', { backendNodeId: node.backendNodeId }, sessionId));
|
|
84
|
+
const [x1, y1, x2, y2, x3, y3, x4, y4] = model.content;
|
|
85
|
+
const centerX = (x1 + x2 + x3 + x4) / 4;
|
|
86
|
+
const centerY = (y1 + y2 + y3 + y4) / 4;
|
|
87
|
+
return { x: centerX, y: centerY };
|
|
88
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type TabInfo, type ProfileStatus } from './types.js';
|
|
2
|
+
import { type RefOpts, type RefNode } from './refs.js';
|
|
3
|
+
export declare class BrowserService {
|
|
4
|
+
private connections;
|
|
5
|
+
start(profileName: string, taskId?: string): Promise<{
|
|
6
|
+
task: string;
|
|
7
|
+
windowTargetId?: string;
|
|
8
|
+
}>;
|
|
9
|
+
stop(taskId: string): Promise<{
|
|
10
|
+
ok: boolean;
|
|
11
|
+
profile?: string;
|
|
12
|
+
}>;
|
|
13
|
+
stopProfile(profileName: string): Promise<void>;
|
|
14
|
+
navigate(taskId: string, url: string, profileName?: string): Promise<{
|
|
15
|
+
tabId: string;
|
|
16
|
+
url: string;
|
|
17
|
+
}>;
|
|
18
|
+
tabs(taskId?: string, profileName?: string): Promise<TabInfo[]>;
|
|
19
|
+
close(taskId: string, tabId?: string): Promise<void>;
|
|
20
|
+
evaluate(taskId: string, tabId: string, expression: string): Promise<unknown>;
|
|
21
|
+
screenshot(taskId: string, tabId?: string, outputPath?: string): Promise<string>;
|
|
22
|
+
private refsCache;
|
|
23
|
+
refs(taskId: string, tabId?: string, opts?: RefOpts): Promise<{
|
|
24
|
+
refs: string;
|
|
25
|
+
nodeMap: Map<number, RefNode>;
|
|
26
|
+
}>;
|
|
27
|
+
click(taskId: string, tabId: string, ref: number): Promise<void>;
|
|
28
|
+
type(taskId: string, tabId: string, ref: number, text: string): Promise<void>;
|
|
29
|
+
press(taskId: string, tabId: string, key: string): Promise<void>;
|
|
30
|
+
hover(taskId: string, tabId: string, ref: number): Promise<void>;
|
|
31
|
+
status(profileName?: string): Promise<ProfileStatus[]>;
|
|
32
|
+
shutdown(): Promise<void>;
|
|
33
|
+
private connectProfile;
|
|
34
|
+
private connectEndpoint;
|
|
35
|
+
private enableDomains;
|
|
36
|
+
private createTaskWindow;
|
|
37
|
+
private findTask;
|
|
38
|
+
private getTabsForTask;
|
|
39
|
+
private getProfileStatus;
|
|
40
|
+
private getTarget;
|
|
41
|
+
private getSessionId;
|
|
42
|
+
private invalidateTargetCache;
|
|
43
|
+
private saveTaskState;
|
|
44
|
+
private loadTaskState;
|
|
45
|
+
}
|