@phnx-labs/agents-cli 1.18.2 → 1.18.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.
@@ -149,7 +149,19 @@ Examples:
149
149
  if (plugin.skills.length > 0) {
150
150
  console.log(chalk.bold('\n Skills'));
151
151
  for (const skill of plugin.skills) {
152
- console.log(` ${chalk.cyan(`${plugin.name}:${skill}`)}`);
152
+ console.log(` ${chalk.cyan(`/${plugin.name}:${skill}`)}`);
153
+ }
154
+ }
155
+ if (plugin.commands.length > 0) {
156
+ console.log(chalk.bold('\n Commands'));
157
+ for (const cmd of plugin.commands) {
158
+ console.log(` ${chalk.cyan(`/${plugin.name}:${cmd}`)}`);
159
+ }
160
+ }
161
+ if (plugin.agentDefs.length > 0) {
162
+ console.log(chalk.bold('\n Subagents'));
163
+ for (const a of plugin.agentDefs) {
164
+ console.log(` ${chalk.magenta(a)}`);
153
165
  }
154
166
  }
155
167
  if (plugin.hooks.length > 0) {
@@ -158,12 +170,40 @@ Examples:
158
170
  console.log(` ${chalk.yellow(hook)}`);
159
171
  }
160
172
  }
173
+ if (plugin.mcpServers.length > 0) {
174
+ console.log(chalk.bold('\n MCP Servers'));
175
+ for (const s of plugin.mcpServers) {
176
+ console.log(` ${chalk.green(s)}`);
177
+ }
178
+ }
179
+ if (plugin.lspServers.length > 0) {
180
+ console.log(chalk.bold('\n LSP Servers'));
181
+ for (const s of plugin.lspServers) {
182
+ console.log(` ${chalk.green(s)}`);
183
+ }
184
+ }
185
+ if (plugin.monitors.length > 0) {
186
+ console.log(chalk.bold('\n Monitors'));
187
+ for (const m of plugin.monitors) {
188
+ console.log(` ${chalk.blue(m)}`);
189
+ }
190
+ }
191
+ if (plugin.bin.length > 0) {
192
+ console.log(chalk.bold('\n Bin'));
193
+ for (const b of plugin.bin) {
194
+ console.log(` ${chalk.white(b)}`);
195
+ }
196
+ }
161
197
  if (plugin.scripts.length > 0) {
162
198
  console.log(chalk.bold('\n Scripts'));
163
199
  for (const script of plugin.scripts) {
164
200
  console.log(` ${chalk.gray(script)}`);
165
201
  }
166
202
  }
203
+ if (plugin.hasSettings) {
204
+ console.log(chalk.bold('\n Settings'));
205
+ console.log(` ${chalk.gray('settings.json')}`);
206
+ }
167
207
  // Show installation status per agent version
168
208
  console.log(chalk.bold('\n Installation Status'));
169
209
  let anyInstalled = false;
@@ -572,20 +612,24 @@ function formatPluginDetail(plugin, targets) {
572
612
  lines.push(' ' + chalk.gray('Supports: ') + supported.join(chalk.gray(' · ')));
573
613
  }
574
614
  lines.push(' ' + chalk.gray(formatPath(plugin.root)));
575
- if (plugin.skills.length > 0) {
576
- lines.push('');
577
- lines.push(chalk.bold(' Skills'));
578
- lines.push(' ' + plugin.skills.map((s) => chalk.cyan(s)).join(chalk.gray(', ')));
579
- }
580
- if (plugin.hooks.length > 0) {
581
- lines.push('');
582
- lines.push(chalk.bold(' Hooks'));
583
- lines.push(' ' + plugin.hooks.map((h) => chalk.yellow(h)).join(chalk.gray(', ')));
584
- }
585
- if (plugin.scripts.length > 0) {
615
+ const section = (label, items, colorFn) => {
616
+ if (items.length === 0)
617
+ return;
586
618
  lines.push('');
587
- lines.push(chalk.bold(' Scripts'));
588
- lines.push(' ' + plugin.scripts.map((s) => chalk.white(s)).join(chalk.gray(', ')));
619
+ lines.push(chalk.bold(` ${label}`));
620
+ lines.push(' ' + items.map(colorFn).join(chalk.gray(', ')));
621
+ };
622
+ section('Skills', plugin.skills.map((s) => `/${plugin.name}:${s}`), chalk.cyan);
623
+ section('Commands', plugin.commands.map((c) => `/${plugin.name}:${c}`), chalk.cyan);
624
+ section('Subagents', plugin.agentDefs, chalk.magenta);
625
+ section('Hooks', plugin.hooks, chalk.yellow);
626
+ section('MCP Servers', plugin.mcpServers, chalk.green);
627
+ section('LSP Servers', plugin.lspServers, chalk.green);
628
+ section('Monitors', plugin.monitors, chalk.blue);
629
+ section('Bin', plugin.bin, chalk.white);
630
+ section('Scripts', plugin.scripts, chalk.white);
631
+ if (plugin.hasSettings) {
632
+ section('Settings', ['settings.json'], chalk.gray);
589
633
  }
590
634
  if (targets.length > 0) {
591
635
  lines.push('');
@@ -604,19 +604,28 @@ async function showAgentResources(agentId, requestedVersion) {
604
604
  const versionStr = agentData.version ? ` (${agentData.version})` : '';
605
605
  const agentHeader = home ? termLink(agentData.agentName, home) : agentData.agentName;
606
606
  console.log(` ${chalk.bold(agentHeader)}${chalk.gray(versionStr)}:`);
607
+ const pluralize = (n, singular) => `${n} ${singular}${n === 1 ? '' : 's'}`;
607
608
  for (const p of plugins) {
608
609
  const linkedName = termLink(p.name, linkTarget(p.root));
609
610
  const parts = [];
610
611
  if (p.skills.length > 0)
611
- parts.push(`${p.skills.length} skill${p.skills.length === 1 ? '' : 's'}`);
612
+ parts.push(pluralize(p.skills.length, 'skill'));
612
613
  if (p.commands.length > 0)
613
- parts.push(`${p.commands.length} command${p.commands.length === 1 ? '' : 's'}`);
614
- if (p.hooks.length > 0)
615
- parts.push(`${p.hooks.length} hook${p.hooks.length === 1 ? '' : 's'}`);
614
+ parts.push(pluralize(p.commands.length, 'command'));
616
615
  if (p.agentDefs.length > 0)
617
- parts.push(`${p.agentDefs.length} subagent${p.agentDefs.length === 1 ? '' : 's'}`);
618
- if (p.hasMcp)
619
- parts.push('mcp');
616
+ parts.push(pluralize(p.agentDefs.length, 'subagent'));
617
+ if (p.hooks.length > 0)
618
+ parts.push(pluralize(p.hooks.length, 'hook'));
619
+ if (p.mcpServers.length > 0)
620
+ parts.push(`${p.mcpServers.length} MCP`);
621
+ if (p.lspServers.length > 0)
622
+ parts.push(`${p.lspServers.length} LSP`);
623
+ if (p.monitors.length > 0)
624
+ parts.push(pluralize(p.monitors.length, 'monitor'));
625
+ if (p.bin.length > 0)
626
+ parts.push(pluralize(p.bin.length, 'bin'));
627
+ if (p.hasSettings)
628
+ parts.push('settings');
620
629
  const contents = parts.length > 0 ? chalk.gray(` (${parts.join(', ')})`) : '';
621
630
  console.log(` ${chalk.cyan(linkedName)}${contents} ${chalk.cyan('[user]')}`);
622
631
  }
@@ -1,4 +1,15 @@
1
1
  import type { DeviceDescriptor } from './types.js';
2
+ /**
3
+ * Default viewport for newly-created profiles. Matches Safari's logical
4
+ * resolution on a 14-inch MacBook Pro (M1/M2/M3 Pro/Max) — the most common
5
+ * shape this CLI sees in practice. Shared with the `MacBook Pro` device
6
+ * preset below so both surfaces agree.
7
+ */
8
+ export declare const DEFAULT_VIEWPORT: {
9
+ readonly width: 1512;
10
+ readonly height: 982;
11
+ readonly deviceScaleFactor: 2;
12
+ };
2
13
  export declare const DEVICES: Record<string, DeviceDescriptor>;
3
14
  export declare function getDevice(name: string): DeviceDescriptor | undefined;
4
15
  export declare function listDevices(): string[];
@@ -1,3 +1,14 @@
1
+ /**
2
+ * Default viewport for newly-created profiles. Matches Safari's logical
3
+ * resolution on a 14-inch MacBook Pro (M1/M2/M3 Pro/Max) — the most common
4
+ * shape this CLI sees in practice. Shared with the `MacBook Pro` device
5
+ * preset below so both surfaces agree.
6
+ */
7
+ export const DEFAULT_VIEWPORT = {
8
+ width: 1512,
9
+ height: 982,
10
+ deviceScaleFactor: 2,
11
+ };
1
12
  export const DEVICES = {
2
13
  'iPhone 14': {
3
14
  width: 390,
@@ -12,9 +23,9 @@ export const DEVICES = {
12
23
  mobile: true,
13
24
  },
14
25
  'MacBook Pro': {
15
- width: 1440,
16
- height: 900,
17
- deviceScaleFactor: 2,
26
+ width: DEFAULT_VIEWPORT.width,
27
+ height: DEFAULT_VIEWPORT.height,
28
+ deviceScaleFactor: DEFAULT_VIEWPORT.deviceScaleFactor,
18
29
  mobile: false,
19
30
  },
20
31
  };
@@ -70,6 +70,7 @@ export class BrowserIPCServer {
70
70
  const result = await this.service.start(request.profile, {
71
71
  taskName: request.taskName,
72
72
  url: request.url,
73
+ endpointName: request.endpoint,
73
74
  });
74
75
  return {
75
76
  ok: true,
@@ -78,13 +79,6 @@ export class BrowserIPCServer {
78
79
  windowTargetId: result.windowId,
79
80
  };
80
81
  }
81
- case 'launch-profile': {
82
- if (!request.profile) {
83
- return { ok: false, error: 'Profile required' };
84
- }
85
- const result = await this.service.launchProfile(request.profile);
86
- return { ok: true, port: result.port, pid: result.pid };
87
- }
88
82
  case 'done': {
89
83
  if (!request.task) {
90
84
  return { ok: false, error: 'Task required' };
@@ -153,12 +147,38 @@ export class BrowserIPCServer {
153
147
  const result = await this.service.evaluate(request.task, request.tabId, request.expr);
154
148
  return { ok: true, result };
155
149
  }
150
+ case 'record-start': {
151
+ if (!request.task)
152
+ return { ok: false, error: 'Task required' };
153
+ try {
154
+ const r = await this.service.recordStart(request.task, request.tabId, {
155
+ fps: request.fps,
156
+ duration: request.duration,
157
+ maxMb: request.maxMb,
158
+ });
159
+ return { ok: true, path: r.path, fps: r.fps, durationCapSec: r.durationCapSec, maxMb: r.maxMb };
160
+ }
161
+ catch (err) {
162
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
163
+ }
164
+ }
165
+ case 'record-stop': {
166
+ if (!request.task)
167
+ return { ok: false, error: 'Task required' };
168
+ try {
169
+ const r = await this.service.recordStop(request.task);
170
+ return { ok: true, path: r.path, bytes: r.bytes, durationMs: r.durationMs, stopReason: r.reason };
171
+ }
172
+ catch (err) {
173
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
174
+ }
175
+ }
156
176
  case 'screenshot': {
157
177
  if (!request.task) {
158
178
  return { ok: false, error: 'Task required' };
159
179
  }
160
- const resultPath = await this.service.screenshot(request.task, request.tabId, request.path);
161
- return { ok: true, path: resultPath };
180
+ const shot = await this.service.screenshot(request.task, request.tabId, request.path, request.quality);
181
+ return { ok: true, path: shot.path, bytes: shot.bytes, width: shot.width, height: shot.height };
162
182
  }
163
183
  case 'refs': {
164
184
  if (!request.task) {
@@ -13,7 +13,29 @@ export declare function createProfile(profile: BrowserProfile): Promise<void>;
13
13
  export declare function updateProfile(profile: BrowserProfile): Promise<void>;
14
14
  export declare function deleteProfile(name: string): Promise<void>;
15
15
  /**
16
- * Extract the port intended by the profile's first endpoint.
16
+ * Resolve a profile's endpoint presets into a normalized map regardless of
17
+ * whether the YAML uses the legacy `string[]` shape or the new map shape.
18
+ * The legacy entries get auto-named `endpoint-0`, `endpoint-1`, ... .
19
+ */
20
+ export declare function getEndpointPresets(profile: BrowserProfile): Record<string, import('./types.js').EndpointPreset>;
21
+ /**
22
+ * Pick the endpoint preset to use. Order:
23
+ * 1. Explicit name passed in (errors if unknown)
24
+ * 2. `profile.defaultEndpoint` if set
25
+ * 3. First entry (preserves legacy string[] behavior)
26
+ *
27
+ * Returns the resolved name + the preset (with per-endpoint overrides
28
+ * already applied to binary / targetFilter), so callers don't have to
29
+ * remember the precedence rules.
30
+ */
31
+ export declare function resolveEndpoint(profile: BrowserProfile, endpointName?: string): {
32
+ name: string;
33
+ target: string;
34
+ binary?: string;
35
+ targetFilter?: string;
36
+ };
37
+ /**
38
+ * Extract the port intended by the profile's default endpoint.
17
39
  * Returns undefined for endpoint shapes that don't carry a port (e.g. ws:// without one).
18
40
  */
19
41
  export declare function extractConfiguredPort(profile: BrowserProfile): number | undefined;
@@ -17,6 +17,7 @@ function configToProfile(name, config) {
17
17
  electron: config.electron,
18
18
  targetFilter: config.targetFilter,
19
19
  endpoints: config.endpoints,
20
+ defaultEndpoint: config.defaultEndpoint,
20
21
  chrome: config.chrome,
21
22
  secrets: config.secrets,
22
23
  viewport: config.viewport,
@@ -35,6 +36,8 @@ function profileToConfig(profile) {
35
36
  config.electron = profile.electron;
36
37
  if (profile.targetFilter)
37
38
  config.targetFilter = profile.targetFilter;
39
+ if (profile.defaultEndpoint)
40
+ config.defaultEndpoint = profile.defaultEndpoint;
38
41
  if (profile.chrome)
39
42
  config.chrome = profile.chrome;
40
43
  if (profile.secrets)
@@ -125,13 +128,70 @@ export async function deleteProfile(name) {
125
128
  writeMeta(meta);
126
129
  }
127
130
  /**
128
- * Extract the port intended by the profile's first endpoint.
131
+ * Resolve a profile's endpoint presets into a normalized map regardless of
132
+ * whether the YAML uses the legacy `string[]` shape or the new map shape.
133
+ * The legacy entries get auto-named `endpoint-0`, `endpoint-1`, ... .
134
+ */
135
+ export function getEndpointPresets(profile) {
136
+ if (Array.isArray(profile.endpoints)) {
137
+ const out = {};
138
+ profile.endpoints.forEach((target, i) => {
139
+ out[`endpoint-${i}`] = { target };
140
+ });
141
+ return out;
142
+ }
143
+ return profile.endpoints;
144
+ }
145
+ /**
146
+ * Pick the endpoint preset to use. Order:
147
+ * 1. Explicit name passed in (errors if unknown)
148
+ * 2. `profile.defaultEndpoint` if set
149
+ * 3. First entry (preserves legacy string[] behavior)
150
+ *
151
+ * Returns the resolved name + the preset (with per-endpoint overrides
152
+ * already applied to binary / targetFilter), so callers don't have to
153
+ * remember the precedence rules.
154
+ */
155
+ export function resolveEndpoint(profile, endpointName) {
156
+ const presets = getEndpointPresets(profile);
157
+ const names = Object.keys(presets);
158
+ if (names.length === 0) {
159
+ throw new Error(`Profile "${profile.name}" has no endpoints configured`);
160
+ }
161
+ let chosenName;
162
+ if (endpointName) {
163
+ if (!presets[endpointName]) {
164
+ throw new Error(`Endpoint "${endpointName}" not found on profile "${profile.name}". ` +
165
+ `Available: ${names.join(', ')}`);
166
+ }
167
+ chosenName = endpointName;
168
+ }
169
+ else if (profile.defaultEndpoint && presets[profile.defaultEndpoint]) {
170
+ chosenName = profile.defaultEndpoint;
171
+ }
172
+ else {
173
+ chosenName = names[0];
174
+ }
175
+ const preset = presets[chosenName];
176
+ return {
177
+ name: chosenName,
178
+ target: preset.target,
179
+ binary: preset.binary ?? profile.binary,
180
+ targetFilter: preset.targetFilter ?? profile.targetFilter,
181
+ };
182
+ }
183
+ /**
184
+ * Extract the port intended by the profile's default endpoint.
129
185
  * Returns undefined for endpoint shapes that don't carry a port (e.g. ws:// without one).
130
186
  */
131
187
  export function extractConfiguredPort(profile) {
132
- const endpoint = profile.endpoints[0];
133
- if (!endpoint)
188
+ const presets = getEndpointPresets(profile);
189
+ const firstName = profile.defaultEndpoint && presets[profile.defaultEndpoint]
190
+ ? profile.defaultEndpoint
191
+ : Object.keys(presets)[0];
192
+ if (!firstName)
134
193
  return undefined;
194
+ const endpoint = presets[firstName].target;
135
195
  let url;
136
196
  try {
137
197
  url = new URL(endpoint);
@@ -45,20 +45,13 @@ export declare class BrowserService {
45
45
  start(profileName: string, opts?: {
46
46
  taskName?: string;
47
47
  url?: string;
48
+ endpointName?: string;
48
49
  }): Promise<{
49
50
  task: string;
50
51
  name: string;
51
52
  tabId?: string;
52
53
  windowId?: string;
53
- }>;
54
- /**
55
- * Launch (or attach to) the profile's browser without creating a task. Used by
56
- * `agents browser profiles launch <name>` so users can warm up the browser —
57
- * including the first-run onboarding flow — before any automation starts.
58
- */
59
- launchProfile(profileName: string): Promise<{
60
- port: number;
61
- pid: number;
54
+ profile: string;
62
55
  }>;
63
56
  stop(taskName: string): Promise<{
64
57
  ok: boolean;
@@ -93,7 +86,34 @@ export declare class BrowserService {
93
86
  tabs(taskId?: string, profileName?: string): Promise<TabInfo[]>;
94
87
  tabClose(taskId: string, tabHint?: string): Promise<void>;
95
88
  evaluate(taskId: string, tabHint: string | undefined, expression: string): Promise<unknown>;
96
- screenshot(taskId: string, tabHint?: string, outputPath?: string): Promise<string>;
89
+ screenshot(taskId: string, tabHint?: string, outputPath?: string, quality?: 'compressed' | 'raw'): Promise<{
90
+ path: string;
91
+ bytes: number;
92
+ width: number;
93
+ height: number;
94
+ }>;
95
+ private recordings;
96
+ recordStart(taskId: string, tabHint?: string, opts?: {
97
+ fps?: number;
98
+ duration?: number;
99
+ maxMb?: number;
100
+ }): Promise<{
101
+ path: string;
102
+ fps: number;
103
+ durationCapSec: number;
104
+ maxMb: number;
105
+ }>;
106
+ recordStop(taskId: string, reason?: 'manual' | 'duration-cap' | 'size-cap'): Promise<{
107
+ path: string;
108
+ bytes: number;
109
+ durationMs: number;
110
+ reason: string;
111
+ }>;
112
+ recordStatus(taskId: string): Promise<{
113
+ recording: boolean;
114
+ path?: string;
115
+ elapsedMs?: number;
116
+ }>;
97
117
  private refsCache;
98
118
  refs(taskId: string, tabHint?: string, opts?: RefOpts): Promise<{
99
119
  refs: string;
@@ -152,10 +172,21 @@ export declare class BrowserService {
152
172
  shutdown(): Promise<void>;
153
173
  private findAvailableFork;
154
174
  private forkElectronProfile;
175
+ /**
176
+ * Connect to a profile at a specific endpoint preset. The caller has
177
+ * already resolved the endpoint and built the `effectiveProfile` with
178
+ * the per-endpoint binary/targetFilter overrides applied; we just use it.
179
+ *
180
+ * `effectiveProfile.name` is the composite identifier (`<profile>@<endpoint>`)
181
+ * so per-endpoint pid/port files don't collide when the same app runs
182
+ * locally and remotely at the same time.
183
+ */
155
184
  private connectProfile;
156
185
  private connectEndpoint;
157
186
  private enableDomains;
158
187
  private getOrCreateWindow;
188
+ private hasTaskNamed;
189
+ private generateUniqueTaskName;
159
190
  private findTask;
160
191
  private getTabsForTask;
161
192
  private getProfileStatus;