@phnx-labs/agents-cli 1.14.7 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +78 -39
- package/README.md +74 -7
- package/dist/commands/alias.js +2 -2
- package/dist/commands/beta.js +6 -1
- package/dist/commands/browser-picker.d.ts +21 -0
- package/dist/commands/browser-picker.js +114 -0
- package/dist/commands/browser.js +546 -75
- package/dist/commands/commands.js +72 -22
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/exec.js +9 -2
- package/dist/commands/fork.js +2 -2
- package/dist/commands/hooks.js +71 -26
- package/dist/commands/mcp.js +85 -43
- package/dist/commands/plugins.js +48 -15
- package/dist/commands/prune.d.ts +0 -20
- package/dist/commands/prune.js +291 -16
- package/dist/commands/pull.js +3 -3
- package/dist/commands/repo.js +1 -1
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.js +37 -1
- package/dist/commands/sessions.js +62 -19
- package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
- package/dist/commands/{init.js → setup.js} +32 -21
- package/dist/commands/skills.js +60 -19
- package/dist/commands/subagents.js +41 -13
- package/dist/commands/teams.js +2 -3
- package/dist/commands/usage.js +6 -0
- package/dist/commands/utils.d.ts +16 -0
- package/dist/commands/utils.js +32 -0
- package/dist/commands/versions.js +8 -6
- package/dist/commands/view.js +61 -16
- package/dist/index.d.ts +1 -1
- package/dist/index.js +17 -20
- package/dist/lib/agents.js +2 -2
- package/dist/lib/auto-pull-worker.js +2 -3
- package/dist/lib/auto-pull.js +2 -2
- package/dist/lib/browser/cdp.d.ts +7 -1
- package/dist/lib/browser/cdp.js +29 -1
- package/dist/lib/browser/chrome.js +6 -3
- package/dist/lib/browser/devices.d.ts +4 -0
- package/dist/lib/browser/devices.js +27 -0
- package/dist/lib/browser/drivers/local.js +9 -4
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +32 -4
- package/dist/lib/browser/ipc.js +145 -23
- package/dist/lib/browser/profiles.d.ts +5 -2
- package/dist/lib/browser/profiles.js +77 -37
- package/dist/lib/browser/service.d.ts +84 -13
- package/dist/lib/browser/service.js +806 -122
- package/dist/lib/browser/types.d.ts +81 -3
- package/dist/lib/browser/types.js +16 -0
- package/dist/lib/cloud/rush.js +2 -2
- package/dist/lib/cloud/store.js +2 -2
- package/dist/lib/commands.d.ts +1 -0
- package/dist/lib/commands.js +6 -2
- package/dist/lib/daemon.js +6 -7
- package/dist/lib/doctor-diff.js +4 -4
- package/dist/lib/events.d.ts +94 -1
- package/dist/lib/events.js +264 -6
- package/dist/lib/exec.js +16 -10
- package/dist/lib/hooks.d.ts +11 -7
- package/dist/lib/hooks.js +125 -49
- package/dist/lib/migrate.d.ts +1 -1
- package/dist/lib/migrate.js +1178 -21
- package/dist/lib/models.js +2 -2
- package/dist/lib/permissions.d.ts +14 -11
- package/dist/lib/permissions.js +46 -42
- package/dist/lib/plugins.d.ts +30 -1
- package/dist/lib/plugins.js +75 -3
- package/dist/lib/pty-server.js +9 -10
- package/dist/lib/resources/hooks.d.ts +5 -1
- package/dist/lib/resources/hooks.js +21 -4
- package/dist/lib/rotate.js +3 -4
- package/dist/lib/routines.d.ts +15 -0
- package/dist/lib/routines.js +68 -0
- package/dist/lib/runner.js +9 -5
- package/dist/lib/secrets/index.d.ts +14 -11
- package/dist/lib/secrets/index.js +49 -21
- package/dist/lib/secrets/linux.d.ts +27 -0
- package/dist/lib/secrets/linux.js +161 -0
- package/dist/lib/session/active.d.ts +3 -0
- package/dist/lib/session/active.js +92 -6
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +34 -3
- package/dist/lib/session/discover.js +30 -15
- package/dist/lib/session/team-filter.js +2 -2
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +6 -6
- package/dist/lib/skills.js +6 -2
- package/dist/lib/state.d.ts +86 -14
- package/dist/lib/state.js +150 -23
- package/dist/lib/subagents.d.ts +28 -0
- package/dist/lib/subagents.js +98 -1
- package/dist/lib/sync-manifest.d.ts +1 -1
- package/dist/lib/sync-manifest.js +3 -3
- package/dist/lib/teams/persistence.js +15 -5
- package/dist/lib/teams/registry.js +2 -2
- package/dist/lib/types.d.ts +32 -3
- package/dist/lib/types.js +3 -3
- package/dist/lib/usage.d.ts +1 -1
- package/dist/lib/usage.js +15 -48
- package/dist/lib/versions.js +31 -21
- package/package.json +1 -1
- package/scripts/postinstall.js +37 -9
package/dist/commands/browser.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { listProfiles, getProfile, createProfile, deleteProfile, } from '../lib/browser/profiles.js';
|
|
2
2
|
import { sendIPCRequest } from '../lib/browser/ipc.js';
|
|
3
|
-
import {
|
|
3
|
+
import { browserTaskPicker } from './browser-picker.js';
|
|
4
|
+
import { isInteractiveTerminal } from './utils.js';
|
|
4
5
|
export function registerBrowserCommand(program) {
|
|
5
6
|
const browser = program
|
|
6
7
|
.command('browser')
|
|
@@ -66,12 +67,22 @@ function registerProfilesCommands(browser) {
|
|
|
66
67
|
profiles
|
|
67
68
|
.command('show <name>')
|
|
68
69
|
.description('Show profile details')
|
|
69
|
-
.
|
|
70
|
+
.option('--json', 'Output machine-readable JSON')
|
|
71
|
+
.action(async (name, opts) => {
|
|
70
72
|
const profile = await getProfile(name);
|
|
71
73
|
if (!profile) {
|
|
72
|
-
|
|
74
|
+
if (opts.json) {
|
|
75
|
+
console.log(JSON.stringify({ ok: false, error: `Profile "${name}" not found` }));
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
console.error(`Profile "${name}" not found`);
|
|
79
|
+
}
|
|
73
80
|
process.exit(1);
|
|
74
81
|
}
|
|
82
|
+
if (opts.json) {
|
|
83
|
+
console.log(JSON.stringify(profile, null, 2));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
75
86
|
console.log(`Name: ${profile.name}`);
|
|
76
87
|
console.log(`Browser: ${profile.browser}`);
|
|
77
88
|
if (profile.description)
|
|
@@ -95,24 +106,42 @@ function registerProfilesCommands(browser) {
|
|
|
95
106
|
}
|
|
96
107
|
function registerTaskCommands(browser) {
|
|
97
108
|
browser
|
|
98
|
-
.command('start
|
|
99
|
-
.description('Start a browser task')
|
|
109
|
+
.command('start')
|
|
110
|
+
.description('Start a browser task with a profile')
|
|
100
111
|
.requiredOption('-p, --profile <name>', 'Browser profile to use')
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
process.exit(1);
|
|
105
|
-
}
|
|
112
|
+
.option('-t, --task <name>', 'Task name (auto-generated if omitted)')
|
|
113
|
+
.option('-u, --url <url>', 'Open URL in first tab')
|
|
114
|
+
.action(async (opts) => {
|
|
106
115
|
const response = await sendIPCRequest({
|
|
107
116
|
action: 'start',
|
|
108
117
|
profile: opts.profile,
|
|
118
|
+
taskName: opts.task,
|
|
119
|
+
url: opts.url,
|
|
120
|
+
});
|
|
121
|
+
if (!response.ok) {
|
|
122
|
+
console.error(response.error);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
if (opts.url && response.tabId) {
|
|
126
|
+
console.log(`Started task "${response.task}" with tab ${response.tabId}`);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
console.log(`Started task "${response.task}"`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
browser
|
|
133
|
+
.command('done <task>')
|
|
134
|
+
.description('Complete a task and close its tabs')
|
|
135
|
+
.action(async (task) => {
|
|
136
|
+
const response = await sendIPCRequest({
|
|
137
|
+
action: 'done',
|
|
109
138
|
task,
|
|
110
139
|
});
|
|
111
140
|
if (!response.ok) {
|
|
112
141
|
console.error(response.error);
|
|
113
142
|
process.exit(1);
|
|
114
143
|
}
|
|
115
|
-
console.log(
|
|
144
|
+
console.log(`Completed task: ${task}`);
|
|
116
145
|
});
|
|
117
146
|
browser
|
|
118
147
|
.command('stop <task>')
|
|
@@ -130,7 +159,7 @@ function registerTaskCommands(browser) {
|
|
|
130
159
|
});
|
|
131
160
|
browser
|
|
132
161
|
.command('navigate <task> <url>')
|
|
133
|
-
.description('
|
|
162
|
+
.description('Navigate current tab to URL (creates tab if none exist)')
|
|
134
163
|
.option('-p, --profile <name>', 'Browser profile (optional if task is unique)')
|
|
135
164
|
.action(async (task, url, opts) => {
|
|
136
165
|
const response = await sendIPCRequest({
|
|
@@ -143,41 +172,48 @@ function registerTaskCommands(browser) {
|
|
|
143
172
|
console.error(response.error);
|
|
144
173
|
process.exit(1);
|
|
145
174
|
}
|
|
146
|
-
console.log(`
|
|
175
|
+
console.log(`Navigated ${response.tabId} to ${url}`);
|
|
147
176
|
});
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
.
|
|
152
|
-
.
|
|
177
|
+
// Tab subcommand group
|
|
178
|
+
const tab = browser.command('tab').description('Manage tabs');
|
|
179
|
+
tab
|
|
180
|
+
.command('add <task> <url>')
|
|
181
|
+
.description('Open URL in new tab (becomes current)')
|
|
182
|
+
.option('-p, --profile <name>', 'Browser profile')
|
|
183
|
+
.action(async (task, url, opts) => {
|
|
153
184
|
const response = await sendIPCRequest({
|
|
154
|
-
action: '
|
|
185
|
+
action: 'tab-add',
|
|
155
186
|
task,
|
|
187
|
+
url,
|
|
156
188
|
profile: opts.profile,
|
|
157
189
|
});
|
|
158
190
|
if (!response.ok) {
|
|
159
191
|
console.error(response.error);
|
|
160
192
|
process.exit(1);
|
|
161
193
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
194
|
+
console.log(`Opened tab ${response.tabId}: ${url}`);
|
|
195
|
+
});
|
|
196
|
+
tab
|
|
197
|
+
.command('focus <task> <tabId>')
|
|
198
|
+
.description('Switch to tab (by ID, prefix, or URL substring)')
|
|
199
|
+
.action(async (task, tabId) => {
|
|
200
|
+
const response = await sendIPCRequest({
|
|
201
|
+
action: 'tab-focus',
|
|
202
|
+
task,
|
|
203
|
+
tabId,
|
|
204
|
+
});
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
console.error(response.error);
|
|
207
|
+
process.exit(1);
|
|
173
208
|
}
|
|
209
|
+
console.log(`Focused tab ${response.tabId}`);
|
|
174
210
|
});
|
|
175
|
-
|
|
211
|
+
tab
|
|
176
212
|
.command('close <task> [tabId]')
|
|
177
|
-
.description('Close
|
|
213
|
+
.description('Close tab(s) — omit tabId to close all')
|
|
178
214
|
.action(async (task, tabId) => {
|
|
179
215
|
const response = await sendIPCRequest({
|
|
180
|
-
action: 'close',
|
|
216
|
+
action: 'tab-close',
|
|
181
217
|
task,
|
|
182
218
|
tabId,
|
|
183
219
|
});
|
|
@@ -185,7 +221,40 @@ function registerTaskCommands(browser) {
|
|
|
185
221
|
console.error(response.error);
|
|
186
222
|
process.exit(1);
|
|
187
223
|
}
|
|
188
|
-
console.log(tabId ? `Closed tab ${tabId}` : `Closed all tabs for
|
|
224
|
+
console.log(tabId ? `Closed tab ${tabId}` : `Closed all tabs for ${task}`);
|
|
225
|
+
});
|
|
226
|
+
tab
|
|
227
|
+
.command('list <task>')
|
|
228
|
+
.description('List tabs for a task')
|
|
229
|
+
.option('--json', 'Output machine-readable JSON')
|
|
230
|
+
.action(async (task, opts) => {
|
|
231
|
+
const response = await sendIPCRequest({
|
|
232
|
+
action: 'tab-list',
|
|
233
|
+
task,
|
|
234
|
+
});
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
if (opts.json) {
|
|
237
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
console.error(response.error);
|
|
241
|
+
}
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
if (opts.json) {
|
|
245
|
+
console.log(JSON.stringify(response.tabs ?? [], null, 2));
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!response.tabs || response.tabs.length === 0) {
|
|
249
|
+
console.log('No tabs open');
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
console.log('TAB'.padEnd(12) + 'URL');
|
|
253
|
+
console.log('-'.repeat(70));
|
|
254
|
+
for (const t of response.tabs) {
|
|
255
|
+
const current = t.current ? ' *' : '';
|
|
256
|
+
console.log(t.id.padEnd(12) + t.url.slice(0, 55) + current);
|
|
257
|
+
}
|
|
189
258
|
});
|
|
190
259
|
browser
|
|
191
260
|
.command('screenshot <task> [tabId]')
|
|
@@ -205,13 +274,14 @@ function registerTaskCommands(browser) {
|
|
|
205
274
|
console.log(response.path);
|
|
206
275
|
});
|
|
207
276
|
browser
|
|
208
|
-
.command('evaluate <task> <
|
|
209
|
-
.description('Evaluate JavaScript in
|
|
210
|
-
.
|
|
277
|
+
.command('evaluate <task> <expression>')
|
|
278
|
+
.description('Evaluate JavaScript in current tab')
|
|
279
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
280
|
+
.action(async (task, expression, opts) => {
|
|
211
281
|
const response = await sendIPCRequest({
|
|
212
282
|
action: 'evaluate',
|
|
213
283
|
task,
|
|
214
|
-
tabId,
|
|
284
|
+
tabId: opts.tab,
|
|
215
285
|
expr: expression,
|
|
216
286
|
});
|
|
217
287
|
if (!response.ok) {
|
|
@@ -224,32 +294,98 @@ function registerTaskCommands(browser) {
|
|
|
224
294
|
.command('status')
|
|
225
295
|
.description('Show running browser tasks')
|
|
226
296
|
.option('-p, --profile <name>', 'Filter by profile')
|
|
297
|
+
.option('--json', 'Output machine-readable JSON')
|
|
227
298
|
.action(async (opts) => {
|
|
228
299
|
const response = await sendIPCRequest({
|
|
229
300
|
action: 'status',
|
|
230
301
|
profile: opts.profile,
|
|
231
302
|
});
|
|
232
303
|
if (!response.ok) {
|
|
233
|
-
|
|
304
|
+
if (opts.json) {
|
|
305
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
console.error(response.error);
|
|
309
|
+
}
|
|
234
310
|
process.exit(1);
|
|
235
311
|
}
|
|
236
|
-
if (
|
|
237
|
-
console.log(
|
|
312
|
+
if (opts.json) {
|
|
313
|
+
console.log(JSON.stringify(response.profiles ?? [], null, 2));
|
|
238
314
|
return;
|
|
239
315
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
316
|
+
// Build flat list of tasks with profile context
|
|
317
|
+
const allTasks = [];
|
|
318
|
+
for (const profile of response.profiles || []) {
|
|
319
|
+
for (const task of profile.tasks) {
|
|
320
|
+
allTasks.push({ task, profile });
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (allTasks.length === 0) {
|
|
324
|
+
// Show recent history instead
|
|
325
|
+
const historyResponse = await sendIPCRequest({ action: 'history', limit: 5 });
|
|
326
|
+
if (historyResponse.ok && historyResponse.history && historyResponse.history.length > 0) {
|
|
327
|
+
console.log('No active tasks. Recent history:\n');
|
|
328
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'DOMAINS'.padEnd(22) + 'DURATION'.padEnd(10) + 'ENDED');
|
|
329
|
+
console.log('-'.repeat(75));
|
|
330
|
+
for (const h of historyResponse.history) {
|
|
331
|
+
const domains = h.domains?.slice(0, 2).join(', ') || '-';
|
|
332
|
+
const duration = formatDuration(h.endedAt - h.createdAt);
|
|
333
|
+
const ended = formatAge(h.endedAt);
|
|
334
|
+
console.log(h.profile.padEnd(15) +
|
|
335
|
+
h.name.padEnd(18) +
|
|
336
|
+
domains.slice(0, 20).padEnd(22) +
|
|
337
|
+
duration.padEnd(10) +
|
|
338
|
+
ended);
|
|
339
|
+
}
|
|
340
|
+
console.log('\nRun `browser history` for more.');
|
|
244
341
|
}
|
|
245
342
|
else {
|
|
246
|
-
console.log('
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
343
|
+
console.log('No browser tasks running');
|
|
344
|
+
}
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
// Interactive picker for TTY, plain output otherwise
|
|
348
|
+
if (isInteractiveTerminal()) {
|
|
349
|
+
const picked = await browserTaskPicker({
|
|
350
|
+
message: 'Browser tasks:',
|
|
351
|
+
tasks: allTasks,
|
|
352
|
+
});
|
|
353
|
+
if (picked) {
|
|
354
|
+
// Show tab list for the selected task
|
|
355
|
+
const tabResponse = await sendIPCRequest({
|
|
356
|
+
action: 'tab-list',
|
|
357
|
+
task: picked.task.task.name,
|
|
358
|
+
});
|
|
359
|
+
if (tabResponse.ok && tabResponse.tabs) {
|
|
360
|
+
console.log(`\nTabs for ${picked.task.task.name}:`);
|
|
361
|
+
for (const tab of tabResponse.tabs) {
|
|
362
|
+
console.log(` ${tab.id} ${tab.url}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
// Non-interactive: simple table output
|
|
369
|
+
for (const profile of response.profiles || []) {
|
|
370
|
+
const portLabel = profile.configuredPort && profile.configuredPort !== profile.port
|
|
371
|
+
? `port ${profile.port} (configured ${profile.configuredPort})`
|
|
372
|
+
: `port ${profile.port}`;
|
|
373
|
+
console.log(`\n${profile.name} (${portLabel}, pid ${profile.pid})`);
|
|
374
|
+
if (profile.tasks.length === 0) {
|
|
375
|
+
console.log(' No active tasks');
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
console.log(' TASK'.padEnd(20) + 'TABS'.padEnd(6) + 'DOMAINS'.padEnd(25) + 'CREATED');
|
|
379
|
+
for (const task of profile.tasks) {
|
|
380
|
+
const age = formatAge(task.createdAt);
|
|
381
|
+
const name = task.name || task.id;
|
|
382
|
+
const domains = task.domains?.slice(0, 2).join(', ') || '-';
|
|
383
|
+
console.log(' ' +
|
|
384
|
+
name.padEnd(18) +
|
|
385
|
+
String(task.tabCount).padEnd(6) +
|
|
386
|
+
domains.slice(0, 23).padEnd(25) +
|
|
387
|
+
age);
|
|
388
|
+
}
|
|
253
389
|
}
|
|
254
390
|
}
|
|
255
391
|
}
|
|
@@ -258,13 +394,19 @@ function registerTaskCommands(browser) {
|
|
|
258
394
|
.command('tasks')
|
|
259
395
|
.description('List all browser tasks')
|
|
260
396
|
.option('-p, --profile <name>', 'Filter by profile')
|
|
397
|
+
.option('--json', 'Output machine-readable JSON')
|
|
261
398
|
.action(async (opts) => {
|
|
262
399
|
const response = await sendIPCRequest({
|
|
263
400
|
action: 'status',
|
|
264
401
|
profile: opts.profile,
|
|
265
402
|
});
|
|
266
403
|
if (!response.ok) {
|
|
267
|
-
|
|
404
|
+
if (opts.json) {
|
|
405
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
console.error(response.error);
|
|
409
|
+
}
|
|
268
410
|
process.exit(1);
|
|
269
411
|
}
|
|
270
412
|
const allTasks = [];
|
|
@@ -272,35 +414,102 @@ function registerTaskCommands(browser) {
|
|
|
272
414
|
for (const task of profile.tasks) {
|
|
273
415
|
allTasks.push({
|
|
274
416
|
profile: profile.name,
|
|
275
|
-
|
|
417
|
+
name: task.name || task.id,
|
|
276
418
|
tabs: task.tabCount,
|
|
419
|
+
domains: task.domains || [],
|
|
277
420
|
created: task.createdAt,
|
|
278
421
|
});
|
|
279
422
|
}
|
|
280
423
|
}
|
|
424
|
+
if (opts.json) {
|
|
425
|
+
console.log(JSON.stringify(allTasks, null, 2));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
281
428
|
if (allTasks.length === 0) {
|
|
282
|
-
|
|
429
|
+
// Show recent history instead
|
|
430
|
+
const historyResponse = await sendIPCRequest({ action: 'history', limit: 5 });
|
|
431
|
+
if (historyResponse.ok && historyResponse.history && historyResponse.history.length > 0) {
|
|
432
|
+
console.log('No active tasks. Recent history:\n');
|
|
433
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'DOMAINS'.padEnd(22) + 'DURATION'.padEnd(10) + 'ENDED');
|
|
434
|
+
console.log('-'.repeat(75));
|
|
435
|
+
for (const h of historyResponse.history) {
|
|
436
|
+
const domains = h.domains?.slice(0, 2).join(', ') || '-';
|
|
437
|
+
const duration = formatDuration(h.endedAt - h.createdAt);
|
|
438
|
+
const ended = formatAge(h.endedAt);
|
|
439
|
+
console.log(h.profile.padEnd(15) +
|
|
440
|
+
h.name.padEnd(18) +
|
|
441
|
+
domains.slice(0, 20).padEnd(22) +
|
|
442
|
+
duration.padEnd(10) +
|
|
443
|
+
ended);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
console.log('No active tasks');
|
|
448
|
+
}
|
|
283
449
|
return;
|
|
284
450
|
}
|
|
285
|
-
console.log('PROFILE'.padEnd(
|
|
286
|
-
console.log('-'.repeat(
|
|
451
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'TABS'.padEnd(6) + 'DOMAINS'.padEnd(22) + 'CREATED');
|
|
452
|
+
console.log('-'.repeat(70));
|
|
287
453
|
for (const t of allTasks) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
454
|
+
const domains = t.domains.slice(0, 2).join(', ') || '-';
|
|
455
|
+
console.log(t.profile.padEnd(15) +
|
|
456
|
+
t.name.padEnd(18) +
|
|
457
|
+
String(t.tabs).padEnd(6) +
|
|
458
|
+
domains.slice(0, 20).padEnd(22) +
|
|
291
459
|
formatAge(t.created));
|
|
292
460
|
}
|
|
293
461
|
});
|
|
294
462
|
browser
|
|
295
|
-
.command('
|
|
463
|
+
.command('history')
|
|
464
|
+
.description('Show recent browser task history')
|
|
465
|
+
.option('-l, --limit <n>', 'Number of entries (default 10)', '10')
|
|
466
|
+
.option('--json', 'Output machine-readable JSON')
|
|
467
|
+
.action(async (opts) => {
|
|
468
|
+
const response = await sendIPCRequest({
|
|
469
|
+
action: 'history',
|
|
470
|
+
limit: parseInt(opts.limit, 10),
|
|
471
|
+
});
|
|
472
|
+
if (!response.ok) {
|
|
473
|
+
if (opts.json) {
|
|
474
|
+
console.log(JSON.stringify({ ok: false, error: response.error }));
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
console.error(response.error);
|
|
478
|
+
}
|
|
479
|
+
process.exit(1);
|
|
480
|
+
}
|
|
481
|
+
if (opts.json) {
|
|
482
|
+
console.log(JSON.stringify(response.history ?? [], null, 2));
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (!response.history || response.history.length === 0) {
|
|
486
|
+
console.log('No browser task history');
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
console.log('PROFILE'.padEnd(15) + 'TASK'.padEnd(18) + 'DOMAINS'.padEnd(22) + 'DURATION'.padEnd(10) + 'ENDED');
|
|
490
|
+
console.log('-'.repeat(75));
|
|
491
|
+
for (const h of response.history) {
|
|
492
|
+
const domains = h.domains?.slice(0, 2).join(', ') || '-';
|
|
493
|
+
const duration = formatDuration(h.endedAt - h.createdAt);
|
|
494
|
+
const ended = formatAge(h.endedAt);
|
|
495
|
+
console.log(h.profile.padEnd(15) +
|
|
496
|
+
h.name.padEnd(18) +
|
|
497
|
+
domains.slice(0, 20).padEnd(22) +
|
|
498
|
+
duration.padEnd(10) +
|
|
499
|
+
ended);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
browser
|
|
503
|
+
.command('refs <task>')
|
|
296
504
|
.description('Get DOM refs for interactive elements')
|
|
505
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
297
506
|
.option('--all', 'Include non-interactive elements')
|
|
298
507
|
.option('-l, --limit <n>', 'Max elements (default 500)', '500')
|
|
299
|
-
.action(async (task,
|
|
508
|
+
.action(async (task, opts) => {
|
|
300
509
|
const response = await sendIPCRequest({
|
|
301
510
|
action: 'refs',
|
|
302
511
|
task,
|
|
303
|
-
tabId,
|
|
512
|
+
tabId: opts.tab,
|
|
304
513
|
interactive: !opts.all,
|
|
305
514
|
limit: parseInt(opts.limit, 10),
|
|
306
515
|
});
|
|
@@ -311,13 +520,14 @@ function registerTaskCommands(browser) {
|
|
|
311
520
|
console.log(response.refs);
|
|
312
521
|
});
|
|
313
522
|
browser
|
|
314
|
-
.command('click <task> <
|
|
523
|
+
.command('click <task> <ref>')
|
|
315
524
|
.description('Click an element by ref')
|
|
316
|
-
.
|
|
525
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
526
|
+
.action(async (task, ref, opts) => {
|
|
317
527
|
const response = await sendIPCRequest({
|
|
318
528
|
action: 'click',
|
|
319
529
|
task,
|
|
320
|
-
tabId,
|
|
530
|
+
tabId: opts.tab,
|
|
321
531
|
ref: parseInt(ref, 10),
|
|
322
532
|
});
|
|
323
533
|
if (!response.ok) {
|
|
@@ -327,13 +537,14 @@ function registerTaskCommands(browser) {
|
|
|
327
537
|
console.log('Clicked');
|
|
328
538
|
});
|
|
329
539
|
browser
|
|
330
|
-
.command('type <task> <
|
|
540
|
+
.command('type <task> <ref> <text>')
|
|
331
541
|
.description('Type text into an element by ref')
|
|
332
|
-
.
|
|
542
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
543
|
+
.action(async (task, ref, text, opts) => {
|
|
333
544
|
const response = await sendIPCRequest({
|
|
334
545
|
action: 'type',
|
|
335
546
|
task,
|
|
336
|
-
tabId,
|
|
547
|
+
tabId: opts.tab,
|
|
337
548
|
ref: parseInt(ref, 10),
|
|
338
549
|
text,
|
|
339
550
|
});
|
|
@@ -344,13 +555,14 @@ function registerTaskCommands(browser) {
|
|
|
344
555
|
console.log('Typed');
|
|
345
556
|
});
|
|
346
557
|
browser
|
|
347
|
-
.command('press <task> <
|
|
558
|
+
.command('press <task> <key>')
|
|
348
559
|
.description('Press a key (Enter, Tab, Escape, etc)')
|
|
349
|
-
.
|
|
560
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
561
|
+
.action(async (task, key, opts) => {
|
|
350
562
|
const response = await sendIPCRequest({
|
|
351
563
|
action: 'press',
|
|
352
564
|
task,
|
|
353
|
-
tabId,
|
|
565
|
+
tabId: opts.tab,
|
|
354
566
|
key,
|
|
355
567
|
});
|
|
356
568
|
if (!response.ok) {
|
|
@@ -360,13 +572,14 @@ function registerTaskCommands(browser) {
|
|
|
360
572
|
console.log('Pressed');
|
|
361
573
|
});
|
|
362
574
|
browser
|
|
363
|
-
.command('hover <task> <
|
|
575
|
+
.command('hover <task> <ref>')
|
|
364
576
|
.description('Hover over an element by ref')
|
|
365
|
-
.
|
|
577
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
578
|
+
.action(async (task, ref, opts) => {
|
|
366
579
|
const response = await sendIPCRequest({
|
|
367
580
|
action: 'hover',
|
|
368
581
|
task,
|
|
369
|
-
tabId,
|
|
582
|
+
tabId: opts.tab,
|
|
370
583
|
ref: parseInt(ref, 10),
|
|
371
584
|
});
|
|
372
585
|
if (!response.ok) {
|
|
@@ -375,6 +588,253 @@ function registerTaskCommands(browser) {
|
|
|
375
588
|
}
|
|
376
589
|
console.log('Hovered');
|
|
377
590
|
});
|
|
591
|
+
// ─── Viewport & Device ───────────────────────────────────────────────────────
|
|
592
|
+
const setCmd = browser.command('set').description('Set browser emulation options');
|
|
593
|
+
setCmd
|
|
594
|
+
.command('viewport <task> <width> <height>')
|
|
595
|
+
.description('Set viewport size')
|
|
596
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
597
|
+
.option('-m, --mobile', 'Enable mobile emulation')
|
|
598
|
+
.option('-s, --scale <factor>', 'Device scale factor', parseFloat)
|
|
599
|
+
.action(async (task, width, height, opts) => {
|
|
600
|
+
const response = await sendIPCRequest({
|
|
601
|
+
action: 'set-viewport',
|
|
602
|
+
task,
|
|
603
|
+
tabId: opts.tab,
|
|
604
|
+
width: parseInt(width, 10),
|
|
605
|
+
height: parseInt(height, 10),
|
|
606
|
+
mobile: opts.mobile,
|
|
607
|
+
deviceScaleFactor: opts.scale,
|
|
608
|
+
});
|
|
609
|
+
if (!response.ok) {
|
|
610
|
+
console.error(response.error);
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
console.log(`Viewport set to ${width}x${height}${opts.mobile ? ' (mobile)' : ''}`);
|
|
614
|
+
});
|
|
615
|
+
setCmd
|
|
616
|
+
.command('device <task> <device-name>')
|
|
617
|
+
.description('Emulate a device (iPhone 14, iPad, MacBook Pro)')
|
|
618
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
619
|
+
.action(async (task, deviceName, opts) => {
|
|
620
|
+
const response = await sendIPCRequest({
|
|
621
|
+
action: 'set-device',
|
|
622
|
+
task,
|
|
623
|
+
tabId: opts.tab,
|
|
624
|
+
deviceName,
|
|
625
|
+
});
|
|
626
|
+
if (!response.ok) {
|
|
627
|
+
console.error(response.error);
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
console.log(`Device set to ${deviceName}`);
|
|
631
|
+
});
|
|
632
|
+
browser
|
|
633
|
+
.command('devices')
|
|
634
|
+
.description('List available device presets')
|
|
635
|
+
.action(async () => {
|
|
636
|
+
const { DEVICES } = await import('../lib/browser/devices.js');
|
|
637
|
+
console.log('Available devices:');
|
|
638
|
+
for (const [name, desc] of Object.entries(DEVICES)) {
|
|
639
|
+
console.log(` ${name.padEnd(16)} ${desc.width}x${desc.height} @${desc.deviceScaleFactor}x${desc.mobile ? ' (mobile)' : ''}`);
|
|
640
|
+
}
|
|
641
|
+
});
|
|
642
|
+
// ─── Console & Errors ────────────────────────────────────────────────────────
|
|
643
|
+
browser
|
|
644
|
+
.command('console <task>')
|
|
645
|
+
.description('Read console logs from a tab')
|
|
646
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
647
|
+
.option('-l, --level <level>', 'Filter by level (log, info, warn, error)')
|
|
648
|
+
.option('--clear', 'Clear logs after reading')
|
|
649
|
+
.action(async (task, opts) => {
|
|
650
|
+
const response = await sendIPCRequest({
|
|
651
|
+
action: 'console',
|
|
652
|
+
task,
|
|
653
|
+
tabId: opts.tab,
|
|
654
|
+
level: opts.level,
|
|
655
|
+
clear: opts.clear,
|
|
656
|
+
});
|
|
657
|
+
if (!response.ok) {
|
|
658
|
+
console.error(response.error);
|
|
659
|
+
process.exit(1);
|
|
660
|
+
}
|
|
661
|
+
if (!response.logs || response.logs.length === 0) {
|
|
662
|
+
console.log('No console logs');
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
for (const log of response.logs) {
|
|
666
|
+
const prefix = `[${log.level.toUpperCase()}]`.padEnd(8);
|
|
667
|
+
const loc = log.url ? ` (${log.url}${log.line ? `:${log.line}` : ''})` : '';
|
|
668
|
+
console.log(`${prefix} ${log.text}${loc}`);
|
|
669
|
+
}
|
|
670
|
+
});
|
|
671
|
+
browser
|
|
672
|
+
.command('errors <task>')
|
|
673
|
+
.description('Read page errors from a tab')
|
|
674
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
675
|
+
.option('--clear', 'Clear errors after reading')
|
|
676
|
+
.action(async (task, opts) => {
|
|
677
|
+
const response = await sendIPCRequest({
|
|
678
|
+
action: 'errors',
|
|
679
|
+
task,
|
|
680
|
+
tabId: opts.tab,
|
|
681
|
+
clear: opts.clear,
|
|
682
|
+
});
|
|
683
|
+
if (!response.ok) {
|
|
684
|
+
console.error(response.error);
|
|
685
|
+
process.exit(1);
|
|
686
|
+
}
|
|
687
|
+
if (!response.errors || response.errors.length === 0) {
|
|
688
|
+
console.log('No errors');
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
for (const err of response.errors) {
|
|
692
|
+
console.log(`[ERROR] ${err.message}`);
|
|
693
|
+
if (err.stack)
|
|
694
|
+
console.log(err.stack);
|
|
695
|
+
if (err.url)
|
|
696
|
+
console.log(` at ${err.url}${err.line ? `:${err.line}` : ''}`);
|
|
697
|
+
console.log();
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
// ─── Network ─────────────────────────────────────────────────────────────────
|
|
701
|
+
browser
|
|
702
|
+
.command('requests <task>')
|
|
703
|
+
.description('Read captured network requests')
|
|
704
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
705
|
+
.option('-f, --filter <text>', 'Filter URLs containing text')
|
|
706
|
+
.option('--clear', 'Clear requests after reading')
|
|
707
|
+
.action(async (task, opts) => {
|
|
708
|
+
const response = await sendIPCRequest({
|
|
709
|
+
action: 'requests',
|
|
710
|
+
task,
|
|
711
|
+
tabId: opts.tab,
|
|
712
|
+
filter: opts.filter,
|
|
713
|
+
clear: opts.clear,
|
|
714
|
+
});
|
|
715
|
+
if (!response.ok) {
|
|
716
|
+
console.error(response.error);
|
|
717
|
+
process.exit(1);
|
|
718
|
+
}
|
|
719
|
+
if (!response.requests || response.requests.length === 0) {
|
|
720
|
+
console.log('No requests captured');
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
console.log('METHOD'.padEnd(8) + 'STATUS'.padEnd(8) + 'URL');
|
|
724
|
+
console.log('-'.repeat(72));
|
|
725
|
+
for (const req of response.requests) {
|
|
726
|
+
const status = req.status ? String(req.status) : '...';
|
|
727
|
+
console.log(`${req.method.padEnd(8)}${status.padEnd(8)}${req.url.slice(0, 100)}`);
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
browser
|
|
731
|
+
.command('responsebody <task> <url-pattern>')
|
|
732
|
+
.description('Wait for and read a response body by URL pattern')
|
|
733
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
734
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
735
|
+
.option('--max-chars <n>', 'Max characters to return', parseInt)
|
|
736
|
+
.action(async (task, urlPattern, opts) => {
|
|
737
|
+
const response = await sendIPCRequest({
|
|
738
|
+
action: 'response-body',
|
|
739
|
+
task,
|
|
740
|
+
tabId: opts.tab,
|
|
741
|
+
urlPattern,
|
|
742
|
+
timeout: opts.timeout,
|
|
743
|
+
maxChars: opts.maxChars,
|
|
744
|
+
});
|
|
745
|
+
if (!response.ok) {
|
|
746
|
+
console.error(response.error);
|
|
747
|
+
process.exit(1);
|
|
748
|
+
}
|
|
749
|
+
console.log(response.body);
|
|
750
|
+
});
|
|
751
|
+
// ─── Wait ────────────────────────────────────────────────────────────────────
|
|
752
|
+
browser
|
|
753
|
+
.command('wait <task>')
|
|
754
|
+
.description('Wait for a condition')
|
|
755
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
756
|
+
.option('--time <ms>', 'Wait for milliseconds')
|
|
757
|
+
.option('--selector <css>', 'Wait for CSS selector to appear')
|
|
758
|
+
.option('--url <pattern>', 'Wait for URL to match pattern')
|
|
759
|
+
.option('--fn <js>', 'Wait for JS expression to return truthy')
|
|
760
|
+
.option('--state <state>', 'Wait for load state (domcontentloaded, load, networkidle)')
|
|
761
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
762
|
+
.action(async (task, opts) => {
|
|
763
|
+
let waitType;
|
|
764
|
+
let waitValue;
|
|
765
|
+
if (opts.time) {
|
|
766
|
+
waitType = 'time';
|
|
767
|
+
waitValue = parseInt(opts.time, 10);
|
|
768
|
+
}
|
|
769
|
+
else if (opts.selector) {
|
|
770
|
+
waitType = 'selector';
|
|
771
|
+
waitValue = opts.selector;
|
|
772
|
+
}
|
|
773
|
+
else if (opts.url) {
|
|
774
|
+
waitType = 'url';
|
|
775
|
+
waitValue = opts.url;
|
|
776
|
+
}
|
|
777
|
+
else if (opts.fn) {
|
|
778
|
+
waitType = 'function';
|
|
779
|
+
waitValue = opts.fn;
|
|
780
|
+
}
|
|
781
|
+
else if (opts.state) {
|
|
782
|
+
waitType = 'load';
|
|
783
|
+
waitValue = opts.state;
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
console.error('One of --time, --selector, --url, --fn, or --state required');
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
const response = await sendIPCRequest({
|
|
790
|
+
action: 'wait',
|
|
791
|
+
task,
|
|
792
|
+
tabId: opts.tab,
|
|
793
|
+
waitType,
|
|
794
|
+
waitValue,
|
|
795
|
+
timeout: opts.timeout,
|
|
796
|
+
});
|
|
797
|
+
if (!response.ok) {
|
|
798
|
+
console.error(response.error);
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
console.log('Wait condition met');
|
|
802
|
+
});
|
|
803
|
+
// ─── Downloads ───────────────────────────────────────────────────────────────
|
|
804
|
+
browser
|
|
805
|
+
.command('download <task>')
|
|
806
|
+
.description('Set download directory for a task')
|
|
807
|
+
.option('-t, --tab <tabId>', 'Tab ID (defaults to current)')
|
|
808
|
+
.requiredOption('-p, --path <dir>', 'Download directory path')
|
|
809
|
+
.action(async (task, opts) => {
|
|
810
|
+
const response = await sendIPCRequest({
|
|
811
|
+
action: 'set-download-path',
|
|
812
|
+
task,
|
|
813
|
+
tabId: opts.tab,
|
|
814
|
+
downloadPath: opts.path,
|
|
815
|
+
});
|
|
816
|
+
if (!response.ok) {
|
|
817
|
+
console.error(response.error);
|
|
818
|
+
process.exit(1);
|
|
819
|
+
}
|
|
820
|
+
console.log(`Download path set to ${opts.path}`);
|
|
821
|
+
});
|
|
822
|
+
browser
|
|
823
|
+
.command('waitdownload <task>')
|
|
824
|
+
.description('Wait for a download to complete')
|
|
825
|
+
.option('--timeout <ms>', 'Timeout in milliseconds', parseInt)
|
|
826
|
+
.action(async (task, opts) => {
|
|
827
|
+
const response = await sendIPCRequest({
|
|
828
|
+
action: 'wait-download',
|
|
829
|
+
task,
|
|
830
|
+
timeout: opts.timeout,
|
|
831
|
+
});
|
|
832
|
+
if (!response.ok) {
|
|
833
|
+
console.error(response.error);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
console.log(`Downloaded: ${response.downloadPath}`);
|
|
837
|
+
});
|
|
378
838
|
}
|
|
379
839
|
function collect(val, memo) {
|
|
380
840
|
memo.push(val);
|
|
@@ -390,3 +850,14 @@ function formatAge(timestamp) {
|
|
|
390
850
|
const hours = Math.floor(minutes / 60);
|
|
391
851
|
return `${hours}h ago`;
|
|
392
852
|
}
|
|
853
|
+
function formatDuration(ms) {
|
|
854
|
+
const seconds = Math.floor(ms / 1000);
|
|
855
|
+
if (seconds < 60)
|
|
856
|
+
return `${seconds}s`;
|
|
857
|
+
const minutes = Math.floor(seconds / 60);
|
|
858
|
+
if (minutes < 60)
|
|
859
|
+
return `${minutes}m`;
|
|
860
|
+
const hours = Math.floor(minutes / 60);
|
|
861
|
+
const mm = minutes % 60;
|
|
862
|
+
return mm ? `${hours}h ${mm}m` : `${hours}h`;
|
|
863
|
+
}
|