@phnx-labs/agents-cli 1.22.93 → 1.22.95

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 (33) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/commands/browser.d.ts +1 -0
  3. package/dist/commands/browser.js +156 -11
  4. package/dist/lib/browser/arc-discovery.d.ts +60 -0
  5. package/dist/lib/browser/arc-discovery.js +187 -0
  6. package/dist/lib/browser/arc-dom.d.ts +14 -0
  7. package/dist/lib/browser/arc-dom.js +121 -0
  8. package/dist/lib/browser/chrome.d.ts +17 -0
  9. package/dist/lib/browser/chrome.js +121 -16
  10. package/dist/lib/browser/chromium-discovery.d.ts +27 -0
  11. package/dist/lib/browser/chromium-discovery.js +96 -0
  12. package/dist/lib/browser/drivers/arc.d.ts +57 -0
  13. package/dist/lib/browser/drivers/arc.js +281 -0
  14. package/dist/lib/browser/drivers/firefox.d.ts +103 -0
  15. package/dist/lib/browser/drivers/firefox.js +377 -0
  16. package/dist/lib/browser/drivers/local.d.ts +8 -0
  17. package/dist/lib/browser/drivers/local.js +38 -3
  18. package/dist/lib/browser/firefox-discovery.d.ts +68 -0
  19. package/dist/lib/browser/firefox-discovery.js +162 -0
  20. package/dist/lib/browser/profiles.d.ts +39 -1
  21. package/dist/lib/browser/profiles.js +239 -9
  22. package/dist/lib/browser/refs.d.ts +2 -0
  23. package/dist/lib/browser/refs.js +2 -1
  24. package/dist/lib/browser/resolve-target.d.ts +2 -0
  25. package/dist/lib/browser/resolve-target.js +14 -0
  26. package/dist/lib/browser/runtime-state.js +9 -1
  27. package/dist/lib/browser/service.d.ts +86 -3
  28. package/dist/lib/browser/service.js +1045 -42
  29. package/dist/lib/browser/types.d.ts +85 -2
  30. package/dist/lib/daemon/usage-sync-service.js +12 -0
  31. package/dist/lib/open-url.js +6 -0
  32. package/dist/lib/types.d.ts +20 -1
  33. package/package.json +1 -1
@@ -0,0 +1,281 @@
1
+ /** Native Apple Events transport. Never uses CDP or launches an Arc process. */
2
+ import { spawn } from 'node:child_process';
3
+ export const ARC_NATIVE_CAPABILITIES = Object.freeze({
4
+ createTab: true, navigate: true, evaluateSync: true, closeTab: true, enumerate: true,
5
+ screenshot: false, asyncEvaluate: false, networkCapture: false, consoleCapture: false,
6
+ upload: false, pdf: false, background: false,
7
+ });
8
+ export class ArcNativeCapabilityError extends Error {
9
+ capability;
10
+ constructor(capability, message) {
11
+ super(message ?? `Native Arc does not support ${capability}. Use a configured browser with that capability.`);
12
+ this.capability = capability;
13
+ this.name = 'ArcNativeCapabilityError';
14
+ }
15
+ }
16
+ const TIMEOUT_MS = 15_000;
17
+ const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
18
+ const APP_ID = 'company.thebrowser.Browser';
19
+ let operationQueue = Promise.resolve();
20
+ /** Serialize native operations in this service, including failures. */
21
+ function serialized(operation) {
22
+ const result = operationQueue.then(operation, operation);
23
+ operationQueue = result.catch(() => undefined);
24
+ return result;
25
+ }
26
+ /** AppleScript strings do not implement JSON's \uXXXX escape syntax. */
27
+ export function escapeAppleScriptString(value) {
28
+ const parts = value.split(/([\u0000-\u001f])/u).filter(Boolean);
29
+ if (parts.length === 0)
30
+ return '""';
31
+ return '(' + parts.map(part => part.length === 1 && part.charCodeAt(0) < 32
32
+ ? `(character id ${part.charCodeAt(0)})`
33
+ : `"${part.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`).join(' & ') + ')';
34
+ }
35
+ /** No shell, bounded output and lifetime, and no blocking of the shared daemon. */
36
+ export function execAppleScript(source, timeoutMs = TIMEOUT_MS) {
37
+ if (process.platform !== 'darwin')
38
+ return Promise.reject(new Error('Native Arc requires macOS.'));
39
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
40
+ return Promise.reject(new Error('Invalid Apple Events timeout.'));
41
+ return new Promise((resolve, reject) => {
42
+ const child = spawn('/usr/bin/osascript', [], { stdio: ['pipe', 'pipe', 'pipe'] });
43
+ const chunks = [];
44
+ const errors = [];
45
+ let bytes = 0;
46
+ let failure;
47
+ const abort = (error) => {
48
+ failure ??= error;
49
+ child.kill('SIGKILL');
50
+ };
51
+ const timer = setTimeout(() => abort(new Error(`Arc Apple Event timed out after ${timeoutMs}ms.`)), timeoutMs);
52
+ for (const [stream, target] of [[child.stdout, chunks], [child.stderr, errors]]) {
53
+ stream.on('data', (chunk) => {
54
+ bytes += chunk.length;
55
+ if (bytes > MAX_OUTPUT_BYTES)
56
+ abort(new Error('Arc Apple Event output exceeded the safety limit.'));
57
+ else
58
+ target.push(chunk);
59
+ });
60
+ }
61
+ child.stdin.on('error', error => abort(error));
62
+ child.once('error', error => { clearTimeout(timer); reject(error); });
63
+ child.once('close', (code, signal) => {
64
+ clearTimeout(timer);
65
+ if (failure) {
66
+ reject(failure);
67
+ return;
68
+ }
69
+ if (code !== 0) {
70
+ reject(new Error(`Arc Apple Event failed (${signal ?? code}): ${Buffer.concat(errors).toString('utf8').trim()}`));
71
+ return;
72
+ }
73
+ resolve(Buffer.concat(chunks).toString('utf8').trim());
74
+ });
75
+ child.stdin.end(source);
76
+ });
77
+ }
78
+ // Foundation serializes arbitrary titles/URLs, including tabs, newlines and Unicode.
79
+ // Delimiter splitting is not a safe wire format for browser-controlled strings.
80
+ const PRELUDE = `use framework "Foundation"
81
+ use scripting additions
82
+ on jsonString(rawValue)
83
+ if rawValue is missing value then set rawValue to ""
84
+ set wrapped to {current application's NSString's stringWithString:(rawValue as text)}
85
+ set jsonData to current application's NSJSONSerialization's dataWithJSONObject:wrapped options:0 |error|:(missing value)
86
+ set jsonText to (current application's NSString's alloc()'s initWithData:jsonData encoding:(current application's NSUTF8StringEncoding)) as text
87
+ return text 2 thru -2 of jsonText
88
+ end jsonString
89
+ `;
90
+ function script(body) {
91
+ return `${PRELUDE}\nif application id "${APP_ID}" is not running then error "Arc is not running. Open it before starting a native task."
92
+ tell application id "${APP_ID}"
93
+ ${body}
94
+ end tell`;
95
+ }
96
+ function identity(value, kind) {
97
+ if (typeof value !== 'string' || value.length === 0 || value.length > 256)
98
+ throw new Error(`Missing or invalid native ${kind} ID.`);
99
+ return escapeAppleScriptString(value);
100
+ }
101
+ /**
102
+ * Resolve all ordinals afresh INSIDE the operation; never persist an index.
103
+ *
104
+ * Every `X of every Y` below is ONE Apple Event returning a list. The naive
105
+ * form (`id of tab ti of targetSpace` inside a repeat) costs one round trip per
106
+ * tab — on a 30-tab Space that alone took the create path past the 15 s budget
107
+ * from inside the shared daemon, whose loop also serves everything else.
108
+ */
109
+ function lookup(ref, missing = 'error "Native Arc target is no longer in its original window and Space."') {
110
+ return `set targetWindowIndex to 0
111
+ set windowIds to id of every window
112
+ set windowVisible to visible of every window
113
+ repeat with wi from 1 to count of windowIds
114
+ if (item wi of windowIds as text) is ${identity(ref.windowId, 'window')} and (item wi of windowVisible) then set targetWindowIndex to wi as integer
115
+ end repeat
116
+ if targetWindowIndex is 0 then
117
+ ${missing}
118
+ end if
119
+ set targetSpaceIndex to 0
120
+ set spaceIds to id of every space of window targetWindowIndex
121
+ repeat with si from 1 to count of spaceIds
122
+ if (item si of spaceIds as text) is ${identity(ref.spaceId, 'Space')} then set targetSpaceIndex to si as integer
123
+ end repeat
124
+ if targetSpaceIndex is 0 then
125
+ ${missing}
126
+ end if
127
+ set targetSpace to a reference to space targetSpaceIndex of window targetWindowIndex
128
+ ${ref.tabId === undefined ? '' : `set targetTabIndex to 0
129
+ set tabIds to id of every tab of targetSpace
130
+ repeat with ti from 1 to count of tabIds
131
+ if (item ti of tabIds as text) is ${identity(ref.tabId, 'tab')} then set targetTabIndex to ti as integer
132
+ end repeat
133
+ if targetTabIndex is 0 then
134
+ ${missing}
135
+ end if
136
+ set targetTab to a reference to tab targetTabIndex of targetSpace`}`;
137
+ }
138
+ const TAB_JSON = `"{\\"windowId\\":" & my jsonString(id of window targetWindowIndex) & ",\\"spaceId\\":" & my jsonString(id of targetSpace) & ",\\"tabId\\":" & my jsonString(id of targetTab) & ",\\"url\\":" & my jsonString(URL of targetTab) & ",\\"title\\":" & my jsonString(title of targetTab) & "}"`;
139
+ function parse(raw) {
140
+ try {
141
+ return JSON.parse(raw);
142
+ }
143
+ catch {
144
+ throw new Error('Arc returned an invalid native response; no target was adopted.');
145
+ }
146
+ }
147
+ export function isArcRunning() {
148
+ if (process.platform !== 'darwin')
149
+ return Promise.resolve(false);
150
+ return serialized(async () => (await execAppleScript(`return application id "${APP_ID}" is running`)) === 'true');
151
+ }
152
+ export function enumerateArcSpaces() {
153
+ return serialized(async () => parse(await execAppleScript(script(`set resultText to "["
154
+ set separator to ""
155
+ set windowIds to id of every window
156
+ set windowVisible to visible of every window
157
+ repeat with wi from 1 to count of windowIds
158
+ if item wi of windowVisible then
159
+ set targetWindowIndex to wi as integer
160
+ set windowIdText to my jsonString(item wi of windowIds)
161
+ set activeId to ""
162
+ try
163
+ set activeId to id of active tab of window targetWindowIndex as text
164
+ end try
165
+ set spaceIds to id of every space of window targetWindowIndex
166
+ set spaceTitles to title of every space of window targetWindowIndex
167
+ repeat with si from 1 to count of spaceIds
168
+ set targetSpace to a reference to space si of window targetWindowIndex
169
+ set resultText to resultText & separator & "{\\"windowId\\":" & windowIdText & ",\\"spaceId\\":" & my jsonString(item si of spaceIds) & ",\\"spaceTitle\\":" & my jsonString(item si of spaceTitles) & ",\\"activeTabId\\":" & my jsonString(activeId) & ",\\"tabs\\":["
170
+ set tabIds to id of every tab of targetSpace
171
+ set tabUrls to URL of every tab of targetSpace
172
+ set tabTitles to title of every tab of targetSpace
173
+ set tabSeparator to ""
174
+ repeat with ti from 1 to count of tabIds
175
+ set resultText to resultText & tabSeparator & "{\\"windowId\\":" & windowIdText & ",\\"spaceId\\":" & my jsonString(item si of spaceIds) & ",\\"tabId\\":" & my jsonString(item ti of tabIds) & ",\\"url\\":" & my jsonString(item ti of tabUrls) & ",\\"title\\":" & my jsonString(item ti of tabTitles) & "}"
176
+ set tabSeparator to ","
177
+ end repeat
178
+ set resultText to resultText & "]}"
179
+ set separator to ","
180
+ end repeat
181
+ end if
182
+ end repeat
183
+ return resultText & "]"`))));
184
+ }
185
+ /** Caller durably records marker intent before calling; never use a real page URL as ownership. */
186
+ export function createArcTab(target, markerUrl) {
187
+ if (!markerUrl || markerUrl.length > 16_384)
188
+ return Promise.reject(new Error('A bounded unique tab creation marker URL is required.'));
189
+ return serialized(async () => parse(await execAppleScript(script(`${lookup(target)}
190
+ set existingUrls to URL of every tab of targetSpace
191
+ repeat with ti from 1 to count of existingUrls
192
+ if (item ti of existingUrls as text) is ${escapeAppleScriptString(markerUrl)} then error "Creation marker already exists; reconcile the saved intent instead of creating another tab."
193
+ end repeat
194
+ tell targetSpace
195
+ make new tab with properties {URL:${escapeAppleScriptString(markerUrl)}}
196
+ end tell
197
+ set matchCount to 0
198
+ set targetTabIndex to 0
199
+ -- Arc commits the new tab's URL asynchronously: read back immediately it is
200
+ -- still empty. Poll briefly (bounded) until the marker shows up.
201
+ repeat with attempt from 1 to 30
202
+ set matchCount to 0
203
+ set createdUrls to URL of every tab of targetSpace
204
+ repeat with ti from 1 to count of createdUrls
205
+ if (item ti of createdUrls as text) is ${escapeAppleScriptString(markerUrl)} then
206
+ set matchCount to matchCount + 1
207
+ set targetTabIndex to ti as integer
208
+ end if
209
+ end repeat
210
+ if matchCount is not 0 then exit repeat
211
+ delay 0.1
212
+ end repeat
213
+ if matchCount is not 1 then error "Creation marker did not resolve exactly one tab; preserve the creation intent for recovery."
214
+ set targetTab to a reference to tab targetTabIndex of targetSpace
215
+ return ${TAB_JSON}`))));
216
+ }
217
+ export function resolveArcTab(ref) {
218
+ return serialized(async () => parse(await execAppleScript(script(`${lookup(ref, 'return "null"')}\nreturn ${TAB_JSON}`))));
219
+ }
220
+ export function navigateArcTab(ref, url) {
221
+ return serialized(async () => {
222
+ await execAppleScript(script(`${lookup(ref)}\nset URL of targetTab to ${escapeAppleScriptString(url)}\nreturn "navigated"`));
223
+ });
224
+ }
225
+ /** Arc serializes a returned object once. JSON.stringify at the top level would double-encode. */
226
+ export function executeJavaScript(ref, expression) {
227
+ const wrapper = `(()=>{try{const value=(0,eval)(${JSON.stringify(expression)});if(value!=null&&typeof value.then==='function')return {ok:false,error:'Native Arc does not support asynchronous evaluation.'};return {ok:true,hasValue:value!==undefined,value:value===undefined?null:JSON.parse(JSON.stringify(value))}}catch(error){return {ok:false,error:String(error)}}})()`;
228
+ return serialized(async () => {
229
+ const result = parse(await execAppleScript(script(`${lookup(ref)}\nreturn execute targetTab javascript ${escapeAppleScriptString(wrapper)}`)));
230
+ if (!result || result.ok !== true)
231
+ throw new Error(result?.error ?? 'Native Arc JavaScript returned no result.');
232
+ return result.hasValue ? result.value : undefined;
233
+ });
234
+ }
235
+ export function closeArcTab(ref) {
236
+ return serialized(async () => {
237
+ const result = await execAppleScript(script(`${lookup(ref, 'return "missing"')}\nclose targetTab\nreturn "closed"`));
238
+ if (result !== 'closed' && result !== 'missing')
239
+ throw new Error('Arc did not confirm tab cleanup.');
240
+ return result;
241
+ });
242
+ }
243
+ /** Explicit user-facing focus only. Evaluation and cleanup never invoke this. */
244
+ export function selectArcTab(ref) {
245
+ return serialized(async () => { await execAppleScript(script(`${lookup(ref)}\nselect targetTab\nreturn "selected"`)); });
246
+ }
247
+ /** AppleScript that selects `tabId` in whatever Space of window `targetWindowIndex` holds it; returns "true"/"false". */
248
+ function selectTabInWindowScript(tabId) {
249
+ return `set spaceCount to count of spaces of window targetWindowIndex
250
+ repeat with si from 1 to spaceCount
251
+ set candidateIds to id of every tab of space si of window targetWindowIndex
252
+ repeat with ti from 1 to count of candidateIds
253
+ if (item ti of candidateIds as text) is ${identity(tabId, 'tab')} then
254
+ select tab ti of space si of window targetWindowIndex
255
+ return "true"
256
+ end if
257
+ end repeat
258
+ end repeat
259
+ return "false"`;
260
+ }
261
+ /**
262
+ * Select `tabId` wherever it lives in window `windowId` (any Space). Used to put
263
+ * the owner back on their tab after a creation that failed mid-way, when there is
264
+ * no owned tab to check against. Returns false when the tab is gone.
265
+ */
266
+ export function selectWindowTab(windowId, tabId) {
267
+ return serialized(async () => (await execAppleScript(script(`set targetWindowIndex to 0
268
+ set windowIds to id of every window
269
+ set windowVisible to visible of every window
270
+ repeat with wi from 1 to count of windowIds
271
+ if (item wi of windowIds as text) is ${identity(windowId, 'window')} and (item wi of windowVisible) then set targetWindowIndex to wi as integer
272
+ end repeat
273
+ if targetWindowIndex is 0 then return "false"
274
+ ${selectTabInWindowScript(tabId)}`))) === 'true');
275
+ }
276
+ /** Restore only while the task's new tab is still selected; preserve a later human choice. */
277
+ export function restoreArcSelection(owned, previousTabId) {
278
+ return serialized(async () => (await execAppleScript(script(`${lookup(owned, 'return "false"')}
279
+ if (id of active tab of window targetWindowIndex as text) is not ${identity(owned.tabId, 'tab')} then return "false"
280
+ ${selectTabInWindowScript(previousTabId)}`))) === 'true');
281
+ }
@@ -0,0 +1,103 @@
1
+ import type { BrowserProfile, ConnectionKey } from '../types.js';
2
+ /** How agents obtain a live Firefox — matches the CDP driver's launch/attach split. */
3
+ export interface FirefoxConnection {
4
+ bidi: FirefoxBiDiClient;
5
+ port: number;
6
+ /** The Firefox process pid we launched, or 0 when we attached to a running one. */
7
+ pid: number;
8
+ /** The BiDi session id from `session.new`. */
9
+ sessionId: string;
10
+ }
11
+ /**
12
+ * A verb Firefox-over-BiDi cannot serve (network capture, upload, pdf, trusted
13
+ * key input, …). Mirrors `ArcNativeCapabilityError`: the service throws it in
14
+ * the one place the capability is missing, and the message steers the caller to
15
+ * a Chromium-family profile that has it.
16
+ */
17
+ export declare class FirefoxCapabilityError extends Error {
18
+ readonly capability: string;
19
+ constructor(capability: string, message?: string);
20
+ }
21
+ /** A BiDi error frame carrying the protocol error + human message. */
22
+ export declare class FirefoxBiDiError extends Error {
23
+ readonly bidiError: string;
24
+ constructor(bidiError: string, message: string);
25
+ }
26
+ /**
27
+ * Minimal WebDriver BiDi client: request/response keyed by the `id` field, with
28
+ * events drained and ignored. Deliberately shaped like `CDPClient` so the
29
+ * service treats a Firefox connection the same way it treats a CDP one. The
30
+ * large `maxPayload` matches CDPClient's, so a base64 screenshot of a
31
+ * content-rich page never trips the socket's decompressed-size cap.
32
+ */
33
+ export declare class FirefoxBiDiClient {
34
+ private ws;
35
+ private nextId;
36
+ private pending;
37
+ private closed;
38
+ get isOpen(): boolean;
39
+ connect(url: string): Promise<void>;
40
+ private handleMessage;
41
+ private handleClose;
42
+ send<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>): Promise<T>;
43
+ close(): void;
44
+ }
45
+ /**
46
+ * The loud error raised when a Firefox already holds this profile but exposes no
47
+ * BiDi port, so agents cannot attach and must not launch a rival (Firefox is
48
+ * single-instance per profile dir). Mirrors `attachOnlyRequiredError` in
49
+ * `drivers/local.ts`: name the exact relaunch that makes the running instance
50
+ * attachable. Exported so the contract is unit-testable without a real Firefox.
51
+ */
52
+ export declare function firefoxAttachRequiredError(profile: Pick<BrowserProfile, 'name'> & {
53
+ firefox?: {
54
+ profileName: string;
55
+ };
56
+ }, port: number, profileDir: string): Error;
57
+ /**
58
+ * Connect a Firefox profile over BiDi: attach if the port is already served,
59
+ * otherwise launch Firefox and wait for it. Fails loud when a Firefox holds the
60
+ * profile without a port.
61
+ */
62
+ export declare function connectFirefox(profile: BrowserProfile, key: ConnectionKey, port: number, opts?: {
63
+ profileDir: string;
64
+ headless?: boolean;
65
+ }): Promise<FirefoxConnection>;
66
+ export declare function deserializeBidi(remote: unknown): unknown;
67
+ /** One top-level browsing context (a tab), flattened from `browsingContext.getTree`. */
68
+ export interface BiDiContext {
69
+ context: string;
70
+ url: string;
71
+ }
72
+ /** Every top-level tab currently open in this Firefox. */
73
+ export declare function bidiTopLevelContexts(bidi: FirefoxBiDiClient): Promise<BiDiContext[]>;
74
+ /** Open a fresh tab and return its context id. */
75
+ export declare function bidiCreateTab(bidi: FirefoxBiDiClient): Promise<string>;
76
+ /** Navigate a context and wait for the document to finish loading. */
77
+ export declare function bidiNavigate(bidi: FirefoxBiDiClient, context: string, url: string): Promise<void>;
78
+ /** Reload a context in place, waiting for load. */
79
+ export declare function bidiReload(bidi: FirefoxBiDiClient, context: string): Promise<void>;
80
+ /** Close a tab. */
81
+ export declare function bidiCloseTab(bidi: FirefoxBiDiClient, context: string): Promise<void>;
82
+ /** Bring a tab to the foreground (explicit focus only). */
83
+ export declare function bidiActivate(bidi: FirefoxBiDiClient, context: string): Promise<void>;
84
+ /**
85
+ * Evaluate an expression in a context and return the deserialized value.
86
+ * `awaitPromise` mirrors the CDP evaluate contract; a thrown/rejected value
87
+ * surfaces as an Error rather than a silent undefined.
88
+ */
89
+ export declare function bidiEvaluate(bidi: FirefoxBiDiClient, context: string, expression: string): Promise<unknown>;
90
+ /** Capture a screenshot of the viewport as raw bytes. `quality` is 0–1 for JPEG. */
91
+ export declare function bidiScreenshot(bidi: FirefoxBiDiClient, context: string, format: {
92
+ type: 'image/png';
93
+ } | {
94
+ type: 'image/jpeg';
95
+ quality: number;
96
+ }): Promise<Buffer>;
97
+ /**
98
+ * A real, trusted left click at viewport coordinates via `input.performActions`
99
+ * — the pointer path CDP `Input.dispatchMouseEvent` gives Chromium, which Arc
100
+ * (Apple Events) never had. The service resolves a ref to (x, y) first, exactly
101
+ * as the CDP click path does.
102
+ */
103
+ export declare function bidiClickAt(bidi: FirefoxBiDiClient, context: string, x: number, y: number): Promise<void>;