@phnx-labs/agents-cli 1.18.3 → 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.
@@ -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;