@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
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { CDPClient, discoverBrowserWsUrl } from './cdp.js';
|
|
4
|
+
import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir } from './profiles.js';
|
|
5
|
+
import { killChrome, getRunningChromeInfo } from './chrome.js';
|
|
6
|
+
import { connectLocal } from './drivers/local.js';
|
|
7
|
+
import { connectSSH } from './drivers/ssh.js';
|
|
8
|
+
import { generateTaskId, isValidTaskId, } from './types.js';
|
|
9
|
+
import { getRefs, resolveRefToCoords } from './refs.js';
|
|
10
|
+
import { clickAtCoords, hoverAtCoords, typeText, pressKey, focusNode } from './input.js';
|
|
11
|
+
export class BrowserService {
|
|
12
|
+
connections = new Map();
|
|
13
|
+
async start(profileName, taskId) {
|
|
14
|
+
const profile = await getProfile(profileName);
|
|
15
|
+
if (!profile) {
|
|
16
|
+
throw new Error(`Profile "${profileName}" not found`);
|
|
17
|
+
}
|
|
18
|
+
const finalTaskId = taskId || generateTaskId();
|
|
19
|
+
if (!isValidTaskId(finalTaskId)) {
|
|
20
|
+
throw new Error(`Invalid task ID "${finalTaskId}". Must be lowercase alphanumeric with hyphens.`);
|
|
21
|
+
}
|
|
22
|
+
let conn = this.connections.get(profileName);
|
|
23
|
+
if (!conn) {
|
|
24
|
+
conn = await this.connectProfile(profile);
|
|
25
|
+
this.connections.set(profileName, conn);
|
|
26
|
+
}
|
|
27
|
+
if (conn.tasks.has(finalTaskId)) {
|
|
28
|
+
const task = conn.tasks.get(finalTaskId);
|
|
29
|
+
return { task: finalTaskId, windowTargetId: task.windowTargetId };
|
|
30
|
+
}
|
|
31
|
+
const { windowTargetId } = await this.createTaskWindow(conn.cdp, finalTaskId);
|
|
32
|
+
const task = {
|
|
33
|
+
id: finalTaskId,
|
|
34
|
+
profile: profileName,
|
|
35
|
+
windowTargetId,
|
|
36
|
+
tabIds: [],
|
|
37
|
+
createdAt: Date.now(),
|
|
38
|
+
pid: conn.pid,
|
|
39
|
+
};
|
|
40
|
+
conn.tasks.set(finalTaskId, task);
|
|
41
|
+
await this.saveTaskState(profileName, conn.tasks);
|
|
42
|
+
return { task: finalTaskId, windowTargetId };
|
|
43
|
+
}
|
|
44
|
+
async stop(taskId) {
|
|
45
|
+
for (const [profileName, conn] of this.connections) {
|
|
46
|
+
const task = conn.tasks.get(taskId);
|
|
47
|
+
if (task) {
|
|
48
|
+
await Promise.all(task.tabIds.map((tabId) => conn.cdp.send('Target.closeTarget', { targetId: tabId }).catch(() => {
|
|
49
|
+
// Tab already closed
|
|
50
|
+
})));
|
|
51
|
+
for (const tabId of task.tabIds) {
|
|
52
|
+
conn.sessionCache.delete(tabId);
|
|
53
|
+
}
|
|
54
|
+
this.invalidateTargetCache(conn);
|
|
55
|
+
if (task.windowTargetId) {
|
|
56
|
+
try {
|
|
57
|
+
await conn.cdp.send('Target.closeTarget', { targetId: task.windowTargetId });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// Window already closed
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
conn.tasks.delete(taskId);
|
|
64
|
+
await this.saveTaskState(profileName, conn.tasks);
|
|
65
|
+
return { ok: true, profile: profileName };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return { ok: false };
|
|
69
|
+
}
|
|
70
|
+
async stopProfile(profileName) {
|
|
71
|
+
const conn = this.connections.get(profileName);
|
|
72
|
+
if (conn) {
|
|
73
|
+
conn.cdp.close();
|
|
74
|
+
killChrome(conn.pid);
|
|
75
|
+
this.connections.delete(profileName);
|
|
76
|
+
}
|
|
77
|
+
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
78
|
+
const pidFile = path.join(runtimeDir, 'pid');
|
|
79
|
+
const portFile = path.join(runtimeDir, 'port');
|
|
80
|
+
if (fs.existsSync(pidFile)) {
|
|
81
|
+
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
|
|
82
|
+
killChrome(pid);
|
|
83
|
+
fs.unlinkSync(pidFile);
|
|
84
|
+
}
|
|
85
|
+
if (fs.existsSync(portFile)) {
|
|
86
|
+
fs.unlinkSync(portFile);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async navigate(taskId, url, profileName) {
|
|
90
|
+
const { conn, task } = await this.findTask(taskId, profileName);
|
|
91
|
+
const result = (await conn.cdp.send('Target.createTarget', {
|
|
92
|
+
url,
|
|
93
|
+
}));
|
|
94
|
+
const tabId = result.targetId;
|
|
95
|
+
task.tabIds.push(tabId);
|
|
96
|
+
this.invalidateTargetCache(conn);
|
|
97
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
98
|
+
return { tabId, url };
|
|
99
|
+
}
|
|
100
|
+
async tabs(taskId, profileName) {
|
|
101
|
+
if (taskId) {
|
|
102
|
+
const { conn, task } = await this.findTask(taskId, profileName);
|
|
103
|
+
return this.getTabsForTask(conn.cdp, task);
|
|
104
|
+
}
|
|
105
|
+
const allTabs = [];
|
|
106
|
+
for (const [, conn] of this.connections) {
|
|
107
|
+
for (const [, task] of conn.tasks) {
|
|
108
|
+
const tabs = await this.getTabsForTask(conn.cdp, task);
|
|
109
|
+
allTabs.push(...tabs);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return allTabs;
|
|
113
|
+
}
|
|
114
|
+
async close(taskId, tabId) {
|
|
115
|
+
const { conn, task } = await this.findTask(taskId);
|
|
116
|
+
if (tabId !== undefined) {
|
|
117
|
+
await conn.cdp.send('Target.closeTarget', { targetId: tabId });
|
|
118
|
+
task.tabIds = task.tabIds.filter((id) => id !== tabId);
|
|
119
|
+
conn.sessionCache.delete(tabId);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
await Promise.all(task.tabIds.map((id) => conn.cdp.send('Target.closeTarget', { targetId: id }).catch(() => { })));
|
|
123
|
+
for (const id of task.tabIds) {
|
|
124
|
+
conn.sessionCache.delete(id);
|
|
125
|
+
}
|
|
126
|
+
task.tabIds = [];
|
|
127
|
+
}
|
|
128
|
+
this.invalidateTargetCache(conn);
|
|
129
|
+
await this.saveTaskState(task.profile, conn.tasks);
|
|
130
|
+
}
|
|
131
|
+
async evaluate(taskId, tabId, expression) {
|
|
132
|
+
const { conn } = await this.findTask(taskId);
|
|
133
|
+
const target = await this.getTarget(conn, tabId);
|
|
134
|
+
if (!target) {
|
|
135
|
+
throw new Error(`Tab ${tabId} not found`);
|
|
136
|
+
}
|
|
137
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
138
|
+
const result = (await conn.cdp.send('Runtime.evaluate', { expression, returnByValue: true }, sessionId));
|
|
139
|
+
return result.result.value;
|
|
140
|
+
}
|
|
141
|
+
async screenshot(taskId, tabId, outputPath) {
|
|
142
|
+
const { conn, task } = await this.findTask(taskId);
|
|
143
|
+
const targetTabId = tabId ?? task.tabIds[task.tabIds.length - 1];
|
|
144
|
+
if (targetTabId === undefined) {
|
|
145
|
+
throw new Error('No tabs open for this task');
|
|
146
|
+
}
|
|
147
|
+
const target = await this.getTarget(conn, targetTabId);
|
|
148
|
+
if (!target) {
|
|
149
|
+
throw new Error(`Tab ${targetTabId} not found`);
|
|
150
|
+
}
|
|
151
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
152
|
+
const { data } = (await conn.cdp.send('Page.captureScreenshot', { format: 'jpeg', quality: 70 }, sessionId));
|
|
153
|
+
let buffer = Buffer.from(data, 'base64');
|
|
154
|
+
const MAX_SIZE = 100 * 1024;
|
|
155
|
+
if (buffer.length > MAX_SIZE) {
|
|
156
|
+
let quality = 50;
|
|
157
|
+
while (buffer.length > MAX_SIZE && quality > 10) {
|
|
158
|
+
const { data: resized } = (await conn.cdp.send('Page.captureScreenshot', { format: 'jpeg', quality }, sessionId));
|
|
159
|
+
buffer = Buffer.from(resized, 'base64');
|
|
160
|
+
quality -= 10;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const sessionsDir = path.join(getBrowserRuntimeDir(), 'sessions', taskId);
|
|
164
|
+
const finalPath = outputPath || path.join(sessionsDir, `${Date.now()}.jpg`);
|
|
165
|
+
await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
|
|
166
|
+
await fs.promises.writeFile(finalPath, buffer);
|
|
167
|
+
return finalPath;
|
|
168
|
+
}
|
|
169
|
+
refsCache = new Map();
|
|
170
|
+
async refs(taskId, tabId, opts = {}) {
|
|
171
|
+
const { conn, task } = await this.findTask(taskId);
|
|
172
|
+
const targetTabId = tabId || task.tabIds[task.tabIds.length - 1];
|
|
173
|
+
if (!targetTabId)
|
|
174
|
+
throw new Error('No tabs open for this task');
|
|
175
|
+
const target = await this.getTarget(conn, targetTabId);
|
|
176
|
+
if (!target)
|
|
177
|
+
throw new Error(`Tab ${targetTabId} not found`);
|
|
178
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
179
|
+
return getRefs(conn.cdp, sessionId, opts);
|
|
180
|
+
}
|
|
181
|
+
async click(taskId, tabId, ref) {
|
|
182
|
+
const { conn } = await this.findTask(taskId);
|
|
183
|
+
const target = await this.getTarget(conn, tabId);
|
|
184
|
+
if (!target)
|
|
185
|
+
throw new Error(`Tab ${tabId} not found`);
|
|
186
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
187
|
+
const { nodeMap } = await getRefs(conn.cdp, sessionId, { interactive: false, limit: 1000 });
|
|
188
|
+
const { x, y } = await resolveRefToCoords(conn.cdp, sessionId, nodeMap, ref);
|
|
189
|
+
await clickAtCoords(conn.cdp, sessionId, x, y);
|
|
190
|
+
}
|
|
191
|
+
async type(taskId, tabId, ref, text) {
|
|
192
|
+
const { conn } = await this.findTask(taskId);
|
|
193
|
+
const target = await this.getTarget(conn, tabId);
|
|
194
|
+
if (!target)
|
|
195
|
+
throw new Error(`Tab ${tabId} not found`);
|
|
196
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
197
|
+
const { nodeMap } = await getRefs(conn.cdp, sessionId, { interactive: false, limit: 1000 });
|
|
198
|
+
const node = nodeMap.get(ref);
|
|
199
|
+
if (!node)
|
|
200
|
+
throw new Error(`Ref ${ref} not found`);
|
|
201
|
+
if (node.backendNodeId) {
|
|
202
|
+
await focusNode(conn.cdp, sessionId, node.backendNodeId);
|
|
203
|
+
}
|
|
204
|
+
await typeText(conn.cdp, sessionId, text);
|
|
205
|
+
}
|
|
206
|
+
async press(taskId, tabId, key) {
|
|
207
|
+
const { conn } = await this.findTask(taskId);
|
|
208
|
+
const target = await this.getTarget(conn, tabId);
|
|
209
|
+
if (!target)
|
|
210
|
+
throw new Error(`Tab ${tabId} not found`);
|
|
211
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
212
|
+
await pressKey(conn.cdp, sessionId, key);
|
|
213
|
+
}
|
|
214
|
+
async hover(taskId, tabId, ref) {
|
|
215
|
+
const { conn } = await this.findTask(taskId);
|
|
216
|
+
const target = await this.getTarget(conn, tabId);
|
|
217
|
+
if (!target)
|
|
218
|
+
throw new Error(`Tab ${tabId} not found`);
|
|
219
|
+
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
220
|
+
const { nodeMap } = await getRefs(conn.cdp, sessionId, { interactive: false, limit: 1000 });
|
|
221
|
+
const { x, y } = await resolveRefToCoords(conn.cdp, sessionId, nodeMap, ref);
|
|
222
|
+
await hoverAtCoords(conn.cdp, sessionId, x, y);
|
|
223
|
+
}
|
|
224
|
+
async status(profileName) {
|
|
225
|
+
const statuses = [];
|
|
226
|
+
if (profileName) {
|
|
227
|
+
const status = await this.getProfileStatus(profileName);
|
|
228
|
+
if (status)
|
|
229
|
+
statuses.push(status);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
for (const name of this.connections.keys()) {
|
|
233
|
+
const status = await this.getProfileStatus(name);
|
|
234
|
+
if (status)
|
|
235
|
+
statuses.push(status);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return statuses;
|
|
239
|
+
}
|
|
240
|
+
async shutdown() {
|
|
241
|
+
for (const [, conn] of this.connections) {
|
|
242
|
+
conn.cdp.close();
|
|
243
|
+
}
|
|
244
|
+
this.connections.clear();
|
|
245
|
+
}
|
|
246
|
+
async connectProfile(profile) {
|
|
247
|
+
const existingInfo = getRunningChromeInfo(profile.name);
|
|
248
|
+
if (existingInfo) {
|
|
249
|
+
const wsUrl = await discoverBrowserWsUrl(existingInfo.port);
|
|
250
|
+
const cdp = new CDPClient();
|
|
251
|
+
await cdp.connect(wsUrl);
|
|
252
|
+
await this.enableDomains(cdp);
|
|
253
|
+
const tasks = this.loadTaskState(profile.name);
|
|
254
|
+
return {
|
|
255
|
+
cdp,
|
|
256
|
+
port: existingInfo.port,
|
|
257
|
+
pid: existingInfo.pid,
|
|
258
|
+
tasks,
|
|
259
|
+
sessionCache: new Map(),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
for (const endpoint of profile.endpoints) {
|
|
263
|
+
try {
|
|
264
|
+
const conn = await this.connectEndpoint(profile, endpoint);
|
|
265
|
+
if (conn)
|
|
266
|
+
return conn;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// Try next endpoint
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
throw new Error(`Could not connect to any endpoint for profile "${profile.name}"`);
|
|
273
|
+
}
|
|
274
|
+
async connectEndpoint(profile, endpoint) {
|
|
275
|
+
const url = new URL(endpoint);
|
|
276
|
+
if (url.protocol === 'cdp:') {
|
|
277
|
+
const conn = await connectLocal(endpoint, profile);
|
|
278
|
+
await this.enableDomains(conn.cdp);
|
|
279
|
+
return {
|
|
280
|
+
cdp: conn.cdp,
|
|
281
|
+
port: conn.port,
|
|
282
|
+
pid: conn.pid,
|
|
283
|
+
tasks: conn.pid === 0 ? this.loadTaskState(profile.name) : new Map(),
|
|
284
|
+
sessionCache: new Map(),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (url.protocol === 'ssh:') {
|
|
288
|
+
const conn = await connectSSH(endpoint, profile);
|
|
289
|
+
await this.enableDomains(conn.cdp);
|
|
290
|
+
return {
|
|
291
|
+
cdp: conn.cdp,
|
|
292
|
+
port: conn.port,
|
|
293
|
+
pid: conn.pid,
|
|
294
|
+
tasks: new Map(),
|
|
295
|
+
sessionCache: new Map(),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
async enableDomains(cdp) {
|
|
301
|
+
await cdp.send('Target.setDiscoverTargets', { discover: true });
|
|
302
|
+
}
|
|
303
|
+
async createTaskWindow(cdp, _taskId) {
|
|
304
|
+
const result = (await cdp.send('Target.createTarget', {
|
|
305
|
+
url: 'about:blank',
|
|
306
|
+
newWindow: true,
|
|
307
|
+
}));
|
|
308
|
+
return { windowTargetId: result.targetId };
|
|
309
|
+
}
|
|
310
|
+
async findTask(taskId, profileName) {
|
|
311
|
+
if (profileName) {
|
|
312
|
+
const conn = this.connections.get(profileName);
|
|
313
|
+
if (!conn) {
|
|
314
|
+
throw new Error(`Profile "${profileName}" not connected`);
|
|
315
|
+
}
|
|
316
|
+
const task = conn.tasks.get(taskId);
|
|
317
|
+
if (!task) {
|
|
318
|
+
throw new Error(`Task "${taskId}" not found on profile "${profileName}"`);
|
|
319
|
+
}
|
|
320
|
+
return { conn, task };
|
|
321
|
+
}
|
|
322
|
+
for (const [, conn] of this.connections) {
|
|
323
|
+
const task = conn.tasks.get(taskId);
|
|
324
|
+
if (task) {
|
|
325
|
+
return { conn, task };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
throw new Error(`Task "${taskId}" not found`);
|
|
329
|
+
}
|
|
330
|
+
async getTabsForTask(cdp, task) {
|
|
331
|
+
const targets = (await cdp.send('Target.getTargets'));
|
|
332
|
+
return task.tabIds
|
|
333
|
+
.map((id) => {
|
|
334
|
+
const target = targets.targetInfos.find((t) => t.targetId === id);
|
|
335
|
+
if (!target)
|
|
336
|
+
return null;
|
|
337
|
+
return {
|
|
338
|
+
id,
|
|
339
|
+
url: target.url,
|
|
340
|
+
title: target.title,
|
|
341
|
+
task: task.id,
|
|
342
|
+
};
|
|
343
|
+
})
|
|
344
|
+
.filter((t) => t !== null);
|
|
345
|
+
}
|
|
346
|
+
async getProfileStatus(profileName) {
|
|
347
|
+
const conn = this.connections.get(profileName);
|
|
348
|
+
if (!conn)
|
|
349
|
+
return null;
|
|
350
|
+
const tasks = [];
|
|
351
|
+
for (const [, task] of conn.tasks) {
|
|
352
|
+
tasks.push({
|
|
353
|
+
id: task.id,
|
|
354
|
+
tabCount: task.tabIds.length,
|
|
355
|
+
createdAt: task.createdAt,
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return {
|
|
359
|
+
name: profileName,
|
|
360
|
+
running: true,
|
|
361
|
+
port: conn.port,
|
|
362
|
+
pid: conn.pid,
|
|
363
|
+
tasks,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async getTarget(conn, tabId) {
|
|
367
|
+
const now = Date.now();
|
|
368
|
+
if (!conn.targetCache || now - conn.targetCache.ts > 1000) {
|
|
369
|
+
const { targetInfos } = (await conn.cdp.send('Target.getTargets'));
|
|
370
|
+
conn.targetCache = { targets: targetInfos, ts: now };
|
|
371
|
+
}
|
|
372
|
+
return conn.targetCache.targets.find((target) => target.targetId === tabId);
|
|
373
|
+
}
|
|
374
|
+
async getSessionId(conn, tabId) {
|
|
375
|
+
const cachedSessionId = conn.sessionCache.get(tabId);
|
|
376
|
+
if (cachedSessionId) {
|
|
377
|
+
return cachedSessionId;
|
|
378
|
+
}
|
|
379
|
+
const { sessionId } = (await conn.cdp.send('Target.attachToTarget', {
|
|
380
|
+
targetId: tabId,
|
|
381
|
+
flatten: true,
|
|
382
|
+
}));
|
|
383
|
+
conn.sessionCache.set(tabId, sessionId);
|
|
384
|
+
return sessionId;
|
|
385
|
+
}
|
|
386
|
+
invalidateTargetCache(conn) {
|
|
387
|
+
conn.targetCache = undefined;
|
|
388
|
+
}
|
|
389
|
+
async saveTaskState(profileName, tasks) {
|
|
390
|
+
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
391
|
+
await fs.promises.mkdir(runtimeDir, { recursive: true });
|
|
392
|
+
const state = Object.fromEntries(tasks);
|
|
393
|
+
await fs.promises.writeFile(path.join(runtimeDir, 'tasks.json'), JSON.stringify(state, null, 2));
|
|
394
|
+
}
|
|
395
|
+
loadTaskState(profileName) {
|
|
396
|
+
const runtimeDir = getProfileRuntimeDir(profileName);
|
|
397
|
+
const tasksFile = path.join(runtimeDir, 'tasks.json');
|
|
398
|
+
if (!fs.existsSync(tasksFile)) {
|
|
399
|
+
return new Map();
|
|
400
|
+
}
|
|
401
|
+
const state = JSON.parse(fs.readFileSync(tasksFile, 'utf-8'));
|
|
402
|
+
return new Map(Object.entries(state));
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge';
|
|
2
|
+
export interface BrowserProfile {
|
|
3
|
+
name: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
browser: BrowserType;
|
|
6
|
+
endpoints: string[];
|
|
7
|
+
chrome?: ChromeOptions;
|
|
8
|
+
secrets?: string;
|
|
9
|
+
viewport?: {
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface ChromeOptions {
|
|
15
|
+
headless?: boolean;
|
|
16
|
+
args?: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface Task {
|
|
19
|
+
id: string;
|
|
20
|
+
profile: string;
|
|
21
|
+
windowTargetId?: string;
|
|
22
|
+
tabIds: string[];
|
|
23
|
+
createdAt: number;
|
|
24
|
+
pid: number;
|
|
25
|
+
}
|
|
26
|
+
export interface TabInfo {
|
|
27
|
+
id: string;
|
|
28
|
+
url: string;
|
|
29
|
+
title: string;
|
|
30
|
+
task: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ProfileStatus {
|
|
33
|
+
name: string;
|
|
34
|
+
running: boolean;
|
|
35
|
+
port?: number;
|
|
36
|
+
pid?: number;
|
|
37
|
+
tasks: TaskStatus[];
|
|
38
|
+
}
|
|
39
|
+
export interface TaskStatus {
|
|
40
|
+
id: string;
|
|
41
|
+
tabCount: number;
|
|
42
|
+
createdAt: number;
|
|
43
|
+
}
|
|
44
|
+
export type IPCAction = 'start' | 'stop' | 'status' | 'navigate' | 'tabs' | 'close' | 'evaluate' | 'screenshot' | 'refs' | 'click' | 'type' | 'press' | 'hover';
|
|
45
|
+
export interface IPCRequest {
|
|
46
|
+
action: IPCAction;
|
|
47
|
+
task?: string;
|
|
48
|
+
profile?: string;
|
|
49
|
+
url?: string;
|
|
50
|
+
tabId?: string;
|
|
51
|
+
expr?: string;
|
|
52
|
+
path?: string;
|
|
53
|
+
ref?: number;
|
|
54
|
+
text?: string;
|
|
55
|
+
key?: string;
|
|
56
|
+
interactive?: boolean;
|
|
57
|
+
limit?: number;
|
|
58
|
+
}
|
|
59
|
+
export interface IPCResponse {
|
|
60
|
+
ok: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
task?: string;
|
|
63
|
+
tabId?: string;
|
|
64
|
+
windowTargetId?: string;
|
|
65
|
+
tabs?: TabInfo[];
|
|
66
|
+
profiles?: ProfileStatus[];
|
|
67
|
+
result?: unknown;
|
|
68
|
+
path?: string;
|
|
69
|
+
refs?: string;
|
|
70
|
+
}
|
|
71
|
+
export declare const TASK_ID_REGEX: RegExp;
|
|
72
|
+
export declare function isValidTaskId(id: string): boolean;
|
|
73
|
+
export declare function generateTaskId(): string;
|
package/dist/lib/cloud/codex.js
CHANGED
|
@@ -107,7 +107,7 @@ export class CodexCloudProvider {
|
|
|
107
107
|
async dispatch(options) {
|
|
108
108
|
const env = options.providerOptions?.env ?? this.defaultEnv;
|
|
109
109
|
if (!env) {
|
|
110
|
-
throw new Error('Codex Cloud requires --env <id>. Set a default in ~/.agents
|
|
110
|
+
throw new Error('Codex Cloud requires --env <id>. Set a default in ~/.agents/agents.yaml under cloud.providers.codex.env.');
|
|
111
111
|
}
|
|
112
112
|
// Codex envs bundle their own repo list — the repos a task can touch are
|
|
113
113
|
// fixed at env-creation time, not per-dispatch. Passing 2+ repos here is
|
|
@@ -10,8 +10,8 @@ import * as yaml from 'yaml';
|
|
|
10
10
|
import { RushCloudProvider } from './rush.js';
|
|
11
11
|
import { CodexCloudProvider } from './codex.js';
|
|
12
12
|
import { FactoryCloudProvider } from './factory.js';
|
|
13
|
-
import {
|
|
14
|
-
const META_FILE = path.join(
|
|
13
|
+
import { getUserAgentsDir } from '../state.js';
|
|
14
|
+
const META_FILE = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
15
15
|
let _config = null;
|
|
16
16
|
/** Parse the `cloud` section from agents.yaml, caching the result for the process lifetime. */
|
|
17
17
|
function loadCloudConfig() {
|
package/dist/lib/cloud/rush.js
CHANGED
|
@@ -9,7 +9,7 @@ import * as path from 'path';
|
|
|
9
9
|
import * as os from 'os';
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
|
-
import {
|
|
12
|
+
import { getUserAgentsDir } from '../state.js';
|
|
13
13
|
import { resolveDispatchRepos } from './types.js';
|
|
14
14
|
import { parseSSE } from './stream.js';
|
|
15
15
|
import { listInstalledVersions, getVersionHomePath } from '../versions.js';
|
|
@@ -20,7 +20,7 @@ const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
|
20
20
|
// Persistent consent record for uploading Claude OAuth blobs to Rush Cloud.
|
|
21
21
|
// Created on first explicit consent (env var or flag); subsequent dispatches
|
|
22
22
|
// see it and proceed without re-prompting.
|
|
23
|
-
const RUSH_CONSENT_PATH = path.join(
|
|
23
|
+
const RUSH_CONSENT_PATH = path.join(getUserAgentsDir(), 'cloud', 'rush-consent.json');
|
|
24
24
|
const RUSH_CONSENT_ENV = 'AGENTS_RUSH_UPLOAD_TOKENS';
|
|
25
25
|
function hasRushUploadConsent(opts) {
|
|
26
26
|
if (process.env[RUSH_CONSENT_ENV] === '1')
|
package/dist/lib/cloud/store.js
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import Database from '../sqlite.js';
|
|
11
|
-
import {
|
|
12
|
-
const CLOUD_DIR = path.join(
|
|
11
|
+
import { getUserAgentsDir } from '../state.js';
|
|
12
|
+
const CLOUD_DIR = path.join(getUserAgentsDir(), 'cloud');
|
|
13
13
|
const DB_PATH = path.join(CLOUD_DIR, 'tasks.db');
|
|
14
14
|
const SCHEMA = `
|
|
15
15
|
CREATE TABLE IF NOT EXISTS tasks (
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare function writeDaemonPid(pid: number): void;
|
|
|
14
14
|
export declare function removeDaemonPid(): void;
|
|
15
15
|
/** Check if the daemon process is alive by sending signal 0 to the stored PID. */
|
|
16
16
|
export declare function isDaemonRunning(): boolean;
|
|
17
|
-
/** Append a
|
|
17
|
+
/** Append a JSONL log entry to the daemon log file (owner-only permissions). */
|
|
18
18
|
export declare function log(level: string, message: string): void;
|
|
19
19
|
/** Main daemon loop: load jobs, schedule crons, monitor runs, and handle signals. */
|
|
20
20
|
export declare function runDaemon(): Promise<void>;
|
package/dist/lib/daemon.js
CHANGED
|
@@ -14,16 +14,26 @@ import { getAgentsDir } from './state.js';
|
|
|
14
14
|
import { listJobs as listAllJobs } from './routines.js';
|
|
15
15
|
import { JobScheduler } from './scheduler.js';
|
|
16
16
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
17
|
+
import { BrowserService } from './browser/service.js';
|
|
18
|
+
import { BrowserIPCServer } from './browser/ipc.js';
|
|
19
|
+
const DAEMON_DIR = 'helpers/daemon';
|
|
17
20
|
const PID_FILE = 'daemon.pid';
|
|
18
21
|
const LOCK_FILE = 'daemon.lock';
|
|
19
|
-
const LOG_FILE = '
|
|
22
|
+
const LOG_FILE = 'logs.jsonl';
|
|
23
|
+
const LOG_MAX_SIZE = 5 * 1024 * 1024; // 5 MB
|
|
24
|
+
const LOG_ROTATE_COUNT = 3;
|
|
20
25
|
const PLIST_NAME = 'com.phnx-labs.agents-daemon';
|
|
21
26
|
const SYSTEMD_UNIT = 'agents-daemon.service';
|
|
27
|
+
function getDaemonDir() {
|
|
28
|
+
const dir = path.join(getAgentsDir(), DAEMON_DIR);
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
return dir;
|
|
31
|
+
}
|
|
22
32
|
function getPidPath() {
|
|
23
|
-
return path.join(
|
|
33
|
+
return path.join(getDaemonDir(), PID_FILE);
|
|
24
34
|
}
|
|
25
35
|
function getLockPath() {
|
|
26
|
-
return path.join(
|
|
36
|
+
return path.join(getDaemonDir(), LOCK_FILE);
|
|
27
37
|
}
|
|
28
38
|
/**
|
|
29
39
|
* Acquire an exclusive start lock. Returns a release function on success,
|
|
@@ -67,7 +77,7 @@ function acquireStartLock() {
|
|
|
67
77
|
}
|
|
68
78
|
}
|
|
69
79
|
function getLogPath() {
|
|
70
|
-
return path.join(
|
|
80
|
+
return path.join(getDaemonDir(), LOG_FILE);
|
|
71
81
|
}
|
|
72
82
|
function getLaunchdPlistPath() {
|
|
73
83
|
return path.join(os.homedir(), 'Library', 'LaunchAgents', `${PLIST_NAME}.plist`);
|
|
@@ -122,12 +132,28 @@ function redactSecrets(message) {
|
|
|
122
132
|
safe = safe.replace(/(ANTHROPIC_API_KEY|OPENAI_API_KEY|API_KEY|SECRET|TOKEN|PASSWORD)=\S+/gi, '$1=[REDACTED]');
|
|
123
133
|
return safe;
|
|
124
134
|
}
|
|
125
|
-
|
|
135
|
+
function rotateLogsIfNeeded(logPath) {
|
|
136
|
+
try {
|
|
137
|
+
const stat = fs.statSync(logPath);
|
|
138
|
+
if (stat.size < LOG_MAX_SIZE)
|
|
139
|
+
return;
|
|
140
|
+
for (let i = LOG_ROTATE_COUNT - 1; i >= 1; i--) {
|
|
141
|
+
const older = `${logPath}.${i}`;
|
|
142
|
+
const newer = i === 1 ? logPath : `${logPath}.${i - 1}`;
|
|
143
|
+
if (fs.existsSync(newer))
|
|
144
|
+
fs.renameSync(newer, older);
|
|
145
|
+
}
|
|
146
|
+
if (fs.existsSync(logPath))
|
|
147
|
+
fs.renameSync(logPath, `${logPath}.1`);
|
|
148
|
+
}
|
|
149
|
+
catch { }
|
|
150
|
+
}
|
|
151
|
+
/** Append a JSONL log entry to the daemon log file (owner-only permissions). */
|
|
126
152
|
export function log(level, message) {
|
|
127
|
-
const timestamp = new Date().toISOString();
|
|
128
|
-
const line = `[${timestamp}] [${level.toUpperCase()}] ${redactSecrets(message)}\n`;
|
|
129
153
|
const logPath = getLogPath();
|
|
130
|
-
|
|
154
|
+
rotateLogsIfNeeded(logPath);
|
|
155
|
+
const entry = { ts: new Date().toISOString(), level: level.toUpperCase(), message: redactSecrets(message) };
|
|
156
|
+
fs.appendFileSync(logPath, JSON.stringify(entry) + '\n', 'utf-8');
|
|
131
157
|
try {
|
|
132
158
|
fs.chmodSync(logPath, 0o600);
|
|
133
159
|
}
|
|
@@ -153,6 +179,15 @@ export async function runDaemon() {
|
|
|
153
179
|
for (const job of scheduled) {
|
|
154
180
|
log('INFO', ` ${job.name} -> next: ${job.nextRun?.toISOString() || 'unknown'}`);
|
|
155
181
|
}
|
|
182
|
+
const browserService = new BrowserService();
|
|
183
|
+
const browserIPC = new BrowserIPCServer(browserService);
|
|
184
|
+
try {
|
|
185
|
+
await browserIPC.start();
|
|
186
|
+
log('INFO', 'Browser IPC server started');
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
log('ERROR', `Browser IPC failed to start: ${err.message}`);
|
|
190
|
+
}
|
|
156
191
|
const monitorInterval = setInterval(() => {
|
|
157
192
|
monitorRunningJobs();
|
|
158
193
|
}, 60_000);
|
|
@@ -162,16 +197,17 @@ export async function runDaemon() {
|
|
|
162
197
|
const reloaded = scheduler.listScheduled();
|
|
163
198
|
log('INFO', `Reloaded ${reloaded.length} jobs`);
|
|
164
199
|
};
|
|
165
|
-
const handleShutdown = () => {
|
|
200
|
+
const handleShutdown = async () => {
|
|
166
201
|
log('INFO', 'Daemon shutting down');
|
|
167
202
|
scheduler.stopAll();
|
|
203
|
+
await browserIPC.stop();
|
|
168
204
|
clearInterval(monitorInterval);
|
|
169
205
|
removeDaemonPid();
|
|
170
206
|
process.exit(0);
|
|
171
207
|
};
|
|
172
208
|
process.on('SIGHUP', handleReload);
|
|
173
|
-
process.on('SIGTERM', handleShutdown);
|
|
174
|
-
process.on('SIGINT', handleShutdown);
|
|
209
|
+
process.on('SIGTERM', () => handleShutdown());
|
|
210
|
+
process.on('SIGINT', () => handleShutdown());
|
|
175
211
|
await new Promise(() => { });
|
|
176
212
|
}
|
|
177
213
|
/** Generate a macOS launchd plist for auto-starting the daemon. */
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny unified-diff helpers for human-readable doctor output.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the `diff` package's createPatch into one call that returns a
|
|
5
|
+
* pre-coloured unified diff (red = removed, green = added, dim = context).
|
|
6
|
+
* Used by `agents doctor --diff`.
|
|
7
|
+
*/
|
|
8
|
+
export interface UnifiedDiffOptions {
|
|
9
|
+
/** Number of context lines around each change (default: 3). */
|
|
10
|
+
context?: number;
|
|
11
|
+
/** Filename label shown in the patch header for the "expected" side. */
|
|
12
|
+
fromLabel?: string;
|
|
13
|
+
/** Filename label shown in the patch header for the "actual" side. */
|
|
14
|
+
toLabel?: string;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Build a unified-diff text comparing two strings. Returns an empty string
|
|
18
|
+
* when contents are identical.
|
|
19
|
+
*/
|
|
20
|
+
export declare function unifiedDiff(expected: string, actual: string, options?: UnifiedDiffOptions): string;
|
|
21
|
+
/**
|
|
22
|
+
* Colour a unified-diff string for terminal output. Indents each line with
|
|
23
|
+
* a constant prefix so it nests cleanly under a header.
|
|
24
|
+
*/
|
|
25
|
+
export declare function colorizeUnifiedDiff(patch: string, indent?: string): string;
|