@phnx-labs/agents-cli 1.15.0 → 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.
Files changed (87) hide show
  1. package/CHANGELOG.md +78 -39
  2. package/README.md +6 -6
  3. package/dist/commands/alias.js +2 -2
  4. package/dist/commands/browser-picker.d.ts +21 -0
  5. package/dist/commands/browser-picker.js +114 -0
  6. package/dist/commands/browser.js +546 -75
  7. package/dist/commands/commands.js +72 -22
  8. package/dist/commands/daemon.js +2 -2
  9. package/dist/commands/fork.js +2 -2
  10. package/dist/commands/hooks.js +71 -26
  11. package/dist/commands/mcp.js +81 -39
  12. package/dist/commands/plugins.js +48 -15
  13. package/dist/commands/prune.js +23 -1
  14. package/dist/commands/pull.js +3 -3
  15. package/dist/commands/repo.js +1 -1
  16. package/dist/commands/routines.js +2 -2
  17. package/dist/commands/secrets.js +37 -1
  18. package/dist/commands/sessions.js +62 -19
  19. package/dist/commands/{init.d.ts → setup.d.ts} +7 -6
  20. package/dist/commands/{init.js → setup.js} +22 -21
  21. package/dist/commands/skills.js +60 -19
  22. package/dist/commands/subagents.js +41 -13
  23. package/dist/commands/utils.d.ts +16 -0
  24. package/dist/commands/utils.js +32 -0
  25. package/dist/commands/view.js +61 -16
  26. package/dist/index.d.ts +1 -1
  27. package/dist/index.js +17 -20
  28. package/dist/lib/agents.js +2 -2
  29. package/dist/lib/auto-pull-worker.js +2 -3
  30. package/dist/lib/auto-pull.js +2 -2
  31. package/dist/lib/browser/cdp.d.ts +7 -1
  32. package/dist/lib/browser/cdp.js +29 -1
  33. package/dist/lib/browser/chrome.js +5 -2
  34. package/dist/lib/browser/devices.d.ts +4 -0
  35. package/dist/lib/browser/devices.js +27 -0
  36. package/dist/lib/browser/drivers/local.js +9 -4
  37. package/dist/lib/browser/drivers/ssh.js +9 -2
  38. package/dist/lib/browser/ipc.js +144 -23
  39. package/dist/lib/browser/profiles.d.ts +5 -2
  40. package/dist/lib/browser/profiles.js +77 -37
  41. package/dist/lib/browser/service.d.ts +81 -13
  42. package/dist/lib/browser/service.js +738 -131
  43. package/dist/lib/browser/types.d.ts +81 -3
  44. package/dist/lib/browser/types.js +16 -0
  45. package/dist/lib/cloud/rush.js +2 -2
  46. package/dist/lib/cloud/store.js +2 -2
  47. package/dist/lib/commands.d.ts +1 -0
  48. package/dist/lib/commands.js +6 -2
  49. package/dist/lib/daemon.js +2 -3
  50. package/dist/lib/doctor-diff.js +4 -4
  51. package/dist/lib/events.js +2 -2
  52. package/dist/lib/hooks.d.ts +11 -7
  53. package/dist/lib/hooks.js +125 -49
  54. package/dist/lib/migrate.d.ts +1 -1
  55. package/dist/lib/migrate.js +1178 -21
  56. package/dist/lib/models.js +2 -2
  57. package/dist/lib/permissions.d.ts +8 -8
  58. package/dist/lib/permissions.js +8 -8
  59. package/dist/lib/plugins.d.ts +30 -1
  60. package/dist/lib/plugins.js +75 -3
  61. package/dist/lib/pty-server.js +9 -10
  62. package/dist/lib/resources/hooks.d.ts +5 -1
  63. package/dist/lib/resources/hooks.js +21 -4
  64. package/dist/lib/rotate.js +3 -4
  65. package/dist/lib/session/active.d.ts +3 -0
  66. package/dist/lib/session/active.js +92 -6
  67. package/dist/lib/session/cloud.js +2 -2
  68. package/dist/lib/session/db.js +8 -3
  69. package/dist/lib/session/discover.js +30 -15
  70. package/dist/lib/session/team-filter.js +2 -2
  71. package/dist/lib/shims.d.ts +2 -2
  72. package/dist/lib/shims.js +6 -6
  73. package/dist/lib/skills.js +6 -2
  74. package/dist/lib/state.d.ts +86 -14
  75. package/dist/lib/state.js +150 -23
  76. package/dist/lib/subagents.d.ts +28 -0
  77. package/dist/lib/subagents.js +98 -1
  78. package/dist/lib/sync-manifest.d.ts +1 -1
  79. package/dist/lib/sync-manifest.js +3 -3
  80. package/dist/lib/teams/persistence.js +15 -5
  81. package/dist/lib/teams/registry.js +2 -2
  82. package/dist/lib/types.d.ts +32 -3
  83. package/dist/lib/types.js +3 -3
  84. package/dist/lib/usage.js +2 -2
  85. package/dist/lib/versions.js +20 -21
  86. package/package.json +1 -1
  87. package/scripts/postinstall.js +1 -1
@@ -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 { isValidTaskId } from '../lib/browser/types.js';
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
- .action(async (name) => {
70
+ .option('--json', 'Output machine-readable JSON')
71
+ .action(async (name, opts) => {
70
72
  const profile = await getProfile(name);
71
73
  if (!profile) {
72
- console.error(`Profile "${name}" not found`);
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 [task]')
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
- .action(async (task, opts) => {
102
- if (task && !isValidTaskId(task)) {
103
- console.error('Task ID must be lowercase alphanumeric with hyphens');
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(response.task);
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('Open a URL in the task window')
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(`Opened tab ${response.tabId}: ${url}`);
175
+ console.log(`Navigated ${response.tabId} to ${url}`);
147
176
  });
148
- browser
149
- .command('tabs [task]')
150
- .description('List open tabs')
151
- .option('-p, --profile <name>', 'Filter by profile')
152
- .action(async (task, opts) => {
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: 'tabs',
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
- if (!response.tabs || response.tabs.length === 0) {
163
- console.log('No tabs open');
164
- return;
165
- }
166
- console.log('TASK'.padEnd(15) + 'TAB'.padEnd(12) + 'URL');
167
- console.log('-'.repeat(80));
168
- for (const tab of response.tabs) {
169
- const shortId = tab.id.slice(0, 8);
170
- console.log(tab.task.padEnd(15) +
171
- shortId.padEnd(12) +
172
- tab.url.slice(0, 55));
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
- browser
211
+ tab
176
212
  .command('close <task> [tabId]')
177
- .description('Close tabs for a task')
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 task ${task}`);
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> <tabId> <expression>')
209
- .description('Evaluate JavaScript in a tab')
210
- .action(async (task, tabId, expression) => {
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
- console.error(response.error);
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 (!response.profiles || response.profiles.length === 0) {
237
- console.log('No browser profiles running');
312
+ if (opts.json) {
313
+ console.log(JSON.stringify(response.profiles ?? [], null, 2));
238
314
  return;
239
315
  }
240
- for (const profile of response.profiles) {
241
- console.log(`\n${profile.name} (port ${profile.port}, pid ${profile.pid})`);
242
- if (profile.tasks.length === 0) {
243
- console.log(' No active tasks');
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(' TASK'.padEnd(17) + 'TABS'.padEnd(8) + 'CREATED');
247
- for (const task of profile.tasks) {
248
- const age = formatAge(task.createdAt);
249
- console.log(' ' +
250
- task.id.padEnd(15) +
251
- String(task.tabCount).padEnd(8) +
252
- age);
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
- console.error(response.error);
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
- id: task.id,
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
- console.log('No active tasks');
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(18) + 'TASK'.padEnd(15) + 'TABS'.padEnd(8) + 'CREATED');
286
- console.log('-'.repeat(55));
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
- console.log(t.profile.padEnd(18) +
289
- t.id.padEnd(15) +
290
- String(t.tabs).padEnd(8) +
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('refs <task> [tabId]')
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, tabId, opts) => {
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> <tabId> <ref>')
523
+ .command('click <task> <ref>')
315
524
  .description('Click an element by ref')
316
- .action(async (task, tabId, ref) => {
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> <tabId> <ref> <text>')
540
+ .command('type <task> <ref> <text>')
331
541
  .description('Type text into an element by ref')
332
- .action(async (task, tabId, ref, text) => {
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> <tabId> <key>')
558
+ .command('press <task> <key>')
348
559
  .description('Press a key (Enter, Tab, Escape, etc)')
349
- .action(async (task, tabId, key) => {
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> <tabId> <ref>')
575
+ .command('hover <task> <ref>')
364
576
  .description('Hover over an element by ref')
365
- .action(async (task, tabId, ref) => {
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
+ }