@phnx-labs/agents-cli 1.14.2 → 1.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/README.md +17 -7
  2. package/dist/browser.d.ts +2 -0
  3. package/dist/browser.js +7 -0
  4. package/dist/commands/browser.d.ts +3 -0
  5. package/dist/commands/browser.js +392 -0
  6. package/dist/commands/daemon.js +1 -1
  7. package/dist/commands/doctor.d.ts +16 -9
  8. package/dist/commands/doctor.js +248 -12
  9. package/dist/commands/prune.js +9 -3
  10. package/dist/commands/refresh-rules.d.ts +15 -0
  11. package/dist/commands/{refresh-memory.js → refresh-rules.js} +14 -14
  12. package/dist/commands/routines.js +1 -1
  13. package/dist/commands/rules.js +100 -4
  14. package/dist/commands/secrets.js +198 -11
  15. package/dist/commands/sync.js +19 -0
  16. package/dist/commands/teams.js +184 -22
  17. package/dist/commands/trash.d.ts +10 -0
  18. package/dist/commands/trash.js +187 -0
  19. package/dist/commands/view.js +47 -14
  20. package/dist/index.js +62 -4
  21. package/dist/lib/agents.js +2 -2
  22. package/dist/lib/browser/cdp.d.ts +24 -0
  23. package/dist/lib/browser/cdp.js +94 -0
  24. package/dist/lib/browser/chrome.d.ts +16 -0
  25. package/dist/lib/browser/chrome.js +157 -0
  26. package/dist/lib/browser/drivers/local.d.ts +8 -0
  27. package/dist/lib/browser/drivers/local.js +22 -0
  28. package/dist/lib/browser/drivers/ssh.d.ts +9 -0
  29. package/dist/lib/browser/drivers/ssh.js +129 -0
  30. package/dist/lib/browser/index.d.ts +5 -0
  31. package/dist/lib/browser/index.js +5 -0
  32. package/dist/lib/browser/input.d.ts +6 -0
  33. package/dist/lib/browser/input.js +52 -0
  34. package/dist/lib/browser/ipc.d.ts +12 -0
  35. package/dist/lib/browser/ipc.js +223 -0
  36. package/dist/lib/browser/profiles.d.ts +11 -0
  37. package/dist/lib/browser/profiles.js +61 -0
  38. package/dist/lib/browser/refs.d.ts +21 -0
  39. package/dist/lib/browser/refs.js +88 -0
  40. package/dist/lib/browser/service.d.ts +45 -0
  41. package/dist/lib/browser/service.js +404 -0
  42. package/dist/lib/browser/types.d.ts +73 -0
  43. package/dist/lib/browser/types.js +7 -0
  44. package/dist/lib/cloud/codex.js +1 -1
  45. package/dist/lib/cloud/registry.js +2 -2
  46. package/dist/lib/cloud/rush.js +2 -2
  47. package/dist/lib/cloud/store.js +2 -2
  48. package/dist/lib/daemon.d.ts +1 -1
  49. package/dist/lib/daemon.js +47 -11
  50. package/dist/lib/diff-text.d.ts +25 -0
  51. package/dist/lib/diff-text.js +47 -0
  52. package/dist/lib/doctor-diff.d.ts +64 -0
  53. package/dist/lib/doctor-diff.js +497 -0
  54. package/dist/lib/git.js +3 -3
  55. package/dist/lib/hooks.d.ts +6 -0
  56. package/dist/lib/hooks.js +6 -1
  57. package/dist/lib/migrate.js +123 -0
  58. package/dist/lib/pty-client.js +3 -3
  59. package/dist/lib/pty-server.js +36 -7
  60. package/dist/lib/resources/commands.d.ts +46 -0
  61. package/dist/lib/resources/commands.js +208 -0
  62. package/dist/lib/resources/hooks.d.ts +12 -0
  63. package/dist/lib/resources/hooks.js +136 -0
  64. package/dist/lib/resources/index.d.ts +36 -0
  65. package/dist/lib/resources/index.js +69 -0
  66. package/dist/lib/resources/mcp.d.ts +34 -0
  67. package/dist/lib/resources/mcp.js +483 -0
  68. package/dist/lib/resources/permissions.d.ts +13 -0
  69. package/dist/lib/resources/permissions.js +184 -0
  70. package/dist/lib/resources/rules.d.ts +43 -0
  71. package/dist/lib/resources/rules.js +146 -0
  72. package/dist/lib/resources/skills.d.ts +37 -0
  73. package/dist/lib/resources/skills.js +238 -0
  74. package/dist/lib/resources/subagents.d.ts +46 -0
  75. package/dist/lib/resources/subagents.js +198 -0
  76. package/dist/lib/resources/types.d.ts +82 -0
  77. package/dist/lib/resources/types.js +8 -0
  78. package/dist/lib/resources.js +1 -1
  79. package/dist/lib/rotate.d.ts +8 -1
  80. package/dist/lib/rotate.js +17 -4
  81. package/dist/lib/rules/compile.d.ts +104 -0
  82. package/dist/lib/{memory-compile.js → rules/compile.js} +160 -21
  83. package/dist/lib/rules/compose.d.ts +78 -0
  84. package/dist/lib/rules/compose.js +170 -0
  85. package/dist/lib/{memory.d.ts → rules/rules.d.ts} +5 -5
  86. package/dist/lib/{memory.js → rules/rules.js} +10 -10
  87. package/dist/lib/secrets/AgentsKeychain.app/Contents/CodeResources +0 -0
  88. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  89. package/dist/lib/secrets/bundles.d.ts +61 -4
  90. package/dist/lib/secrets/bundles.js +222 -54
  91. package/dist/lib/secrets/index.d.ts +24 -5
  92. package/dist/lib/secrets/index.js +70 -41
  93. package/dist/lib/session/active.js +5 -5
  94. package/dist/lib/session/db.js +4 -4
  95. package/dist/lib/session/discover.js +2 -2
  96. package/dist/lib/session/render.js +21 -7
  97. package/dist/lib/shims.d.ts +28 -4
  98. package/dist/lib/shims.js +72 -14
  99. package/dist/lib/state.d.ts +22 -28
  100. package/dist/lib/state.js +83 -78
  101. package/dist/lib/sync-manifest.d.ts +2 -2
  102. package/dist/lib/sync-manifest.js +5 -5
  103. package/dist/lib/teams/agents.d.ts +4 -2
  104. package/dist/lib/teams/agents.js +11 -4
  105. package/dist/lib/teams/api.d.ts +1 -1
  106. package/dist/lib/teams/api.js +2 -2
  107. package/dist/lib/teams/index.d.ts +1 -0
  108. package/dist/lib/teams/index.js +1 -0
  109. package/dist/lib/teams/persistence.js +3 -3
  110. package/dist/lib/teams/registry.d.ts +12 -1
  111. package/dist/lib/teams/registry.js +12 -2
  112. package/dist/lib/teams/worktree.d.ts +30 -0
  113. package/dist/lib/teams/worktree.js +96 -0
  114. package/dist/lib/types.d.ts +12 -6
  115. package/dist/lib/types.js +3 -3
  116. package/dist/lib/versions.d.ts +32 -3
  117. package/dist/lib/versions.js +147 -119
  118. package/package.json +3 -2
  119. package/scripts/postinstall.js +29 -0
  120. package/dist/commands/refresh-memory.d.ts +0 -15
  121. package/dist/lib/memory-compile.d.ts +0 -66
@@ -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;
@@ -0,0 +1,7 @@
1
+ export const TASK_ID_REGEX = /^[a-z0-9][a-z0-9-]*$/;
2
+ export function isValidTaskId(id) {
3
+ return TASK_ID_REGEX.test(id) && id.length <= 64;
4
+ }
5
+ export function generateTaskId() {
6
+ return crypto.randomUUID().slice(0, 8);
7
+ }
@@ -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-system/agents.yaml under cloud.providers.codex.env.');
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 { getAgentsDir } from '../state.js';
14
- const META_FILE = path.join(getAgentsDir(), 'agents.yaml');
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() {
@@ -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 { getAgentsDir } from '../state.js';
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(getAgentsDir(), 'cloud', 'rush-consent.json');
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')
@@ -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 { getAgentsDir } from '../state.js';
12
- const CLOUD_DIR = path.join(getAgentsDir(), 'cloud');
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 (
@@ -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 timestamped log line to the daemon log file (owner-only permissions). */
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>;
@@ -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 = 'daemon.log';
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(getAgentsDir(), PID_FILE);
33
+ return path.join(getDaemonDir(), PID_FILE);
24
34
  }
25
35
  function getLockPath() {
26
- return path.join(getAgentsDir(), LOCK_FILE);
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(getAgentsDir(), LOG_FILE);
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
- /** Append a timestamped log line to the daemon log file (owner-only permissions). */
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
- fs.appendFileSync(logPath, line, 'utf-8');
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;