@phnx-labs/agents-cli 1.22.92 → 1.22.94

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 (55) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +5 -0
  3. package/dist/commands/browser.d.ts +1 -0
  4. package/dist/commands/browser.js +145 -11
  5. package/dist/lib/browser/arc-discovery.d.ts +60 -0
  6. package/dist/lib/browser/arc-discovery.js +187 -0
  7. package/dist/lib/browser/arc-dom.d.ts +14 -0
  8. package/dist/lib/browser/arc-dom.js +121 -0
  9. package/dist/lib/browser/drivers/arc.d.ts +57 -0
  10. package/dist/lib/browser/drivers/arc.js +281 -0
  11. package/dist/lib/browser/profiles.d.ts +5 -1
  12. package/dist/lib/browser/profiles.js +94 -9
  13. package/dist/lib/browser/refs.d.ts +2 -0
  14. package/dist/lib/browser/refs.js +2 -1
  15. package/dist/lib/browser/resolve-target.d.ts +2 -0
  16. package/dist/lib/browser/resolve-target.js +8 -0
  17. package/dist/lib/browser/runtime-state.js +9 -1
  18. package/dist/lib/browser/service.d.ts +37 -3
  19. package/dist/lib/browser/service.js +571 -41
  20. package/dist/lib/browser/types.d.ts +55 -1
  21. package/dist/lib/claude-account-token.d.ts +13 -0
  22. package/dist/lib/claude-account-token.js +29 -0
  23. package/dist/lib/daemon/attention-notify-service.d.ts +5 -2
  24. package/dist/lib/daemon/attention-notify-service.js +9 -2
  25. package/dist/lib/exec.js +11 -0
  26. package/dist/lib/feed/answer.js +9 -0
  27. package/dist/lib/feed/attention.d.ts +18 -2
  28. package/dist/lib/feed/attention.js +0 -0
  29. package/dist/lib/feed/feed.d.ts +6 -2
  30. package/dist/lib/feed/feed.js +10 -4
  31. package/dist/lib/harness/adapter.d.ts +23 -0
  32. package/dist/lib/harness/adapter.js +29 -0
  33. package/dist/lib/harness/adapters/claude.js +5 -3
  34. package/dist/lib/harness/adapters/copilot.js +4 -2
  35. package/dist/lib/harness/adapters/grok.js +4 -2
  36. package/dist/lib/harness/adapters/kimi.js +4 -2
  37. package/dist/lib/harness/adapters/muse.js +4 -3
  38. package/dist/lib/harness/adapters/opencode.js +4 -2
  39. package/dist/lib/helper-download.js +1 -1
  40. package/dist/lib/helper-versions.js +1 -1
  41. package/dist/lib/installations/shims.d.ts +2 -2
  42. package/dist/lib/installations/shims.js +16 -8
  43. package/dist/lib/menubar/download-menubar.js +4 -1
  44. package/dist/lib/menubar/install-menubar.d.ts +10 -9
  45. package/dist/lib/menubar/install-menubar.js +17 -15
  46. package/dist/lib/secrets-policy.d.ts +5 -2
  47. package/dist/lib/secrets-policy.js +18 -4
  48. package/dist/lib/session/active.d.ts +18 -11
  49. package/dist/lib/session/active.js +52 -44
  50. package/dist/lib/session/session-cache.d.ts +1 -1
  51. package/dist/lib/session/session-cache.js +1 -0
  52. package/dist/lib/session/state.d.ts +26 -0
  53. package/dist/lib/session/state.js +43 -27
  54. package/dist/lib/types.d.ts +7 -0
  55. package/package.json +1 -1
@@ -0,0 +1,121 @@
1
+ import { truncate } from '../format.js';
2
+ const INTERACTIVE_SELECTOR = [
3
+ 'a[href]', 'button', 'input', 'select', 'textarea', '[contenteditable="true"]',
4
+ '[role="button"]', '[role="link"]', '[role="textbox"]', '[role="checkbox"]',
5
+ '[role="radio"]', '[role="combobox"]', '[role="listbox"]', '[role="option"]',
6
+ '[role="menuitem"]', '[role="tab"]', '[role="slider"]', '[role="spinbutton"]',
7
+ '[role="searchbox"]', '[role="switch"]', '[role="treeitem"]',
8
+ ].join(',');
9
+ /** Build one synchronous expression that reads DOM-backed refs from native Arc. */
10
+ export function arcRefsExpression(opts = {}) {
11
+ const interactive = opts.interactive ?? true;
12
+ const limit = opts.limit ?? 500;
13
+ return `(() => {
14
+ const selectorFor = (el) => {
15
+ if (el.id) return '#' + CSS.escape(el.id);
16
+ const parts = [];
17
+ for (let node = el; node && node.nodeType === 1 && node !== document.documentElement; node = node.parentElement) {
18
+ let part = node.localName;
19
+ if (!part) return '';
20
+ const siblings = node.parentElement ? Array.from(node.parentElement.children).filter((s) => s.localName === node.localName) : [];
21
+ if (siblings.length > 1) part += ':nth-of-type(' + (siblings.indexOf(node) + 1) + ')';
22
+ parts.unshift(part);
23
+ }
24
+ return parts.join(' > ');
25
+ };
26
+ const roleFor = (el) => {
27
+ const explicit = el.getAttribute('role');
28
+ if (explicit) return explicit.toLowerCase();
29
+ if (el.matches('button')) return 'button';
30
+ if (el.matches('a[href]')) return 'link';
31
+ if (el.matches('textarea,[contenteditable="true"]')) return 'textbox';
32
+ if (el.matches('select')) return 'combobox';
33
+ if (el.matches('input')) {
34
+ const type = (el.getAttribute('type') || 'text').toLowerCase();
35
+ if (type === 'checkbox') return 'checkbox';
36
+ if (type === 'radio') return 'radio';
37
+ if (type === 'range') return 'slider';
38
+ if (type === 'number') return 'spinbutton';
39
+ return 'textbox';
40
+ }
41
+ return el.localName || 'generic';
42
+ };
43
+ const nameFor = (el) => el.getAttribute('aria-label') || el.getAttribute('title') ||
44
+ (el.labels && el.labels[0] && el.labels[0].innerText) || el.innerText || el.value || el.getAttribute('placeholder') || '';
45
+ const attrsFor = (el) => ['disabled','checked','selected','expanded','required','readonly','invalid']
46
+ .filter((name) => el[name] === true || el.getAttribute('aria-' + name) === 'true');
47
+ const editorFor = (el) => {
48
+ for (let node = el, i = 0; node && i < 6; node = node.parentElement, i++) {
49
+ if (node.hasAttribute('data-lexical-editor')) return 'lexical';
50
+ if (node.classList.contains('ProseMirror')) return 'prosemirror';
51
+ if (node.hasAttribute('data-slate-editor')) return 'slate';
52
+ if (Array.from(node.classList).some((c) => /^DraftEditor-/.test(c))) return 'draft';
53
+ if (node.classList.contains('ql-editor')) return 'quill';
54
+ if (node.classList.contains('ck-editor__editable')) return 'ckeditor5';
55
+ if (node.tagName === 'TRIX-EDITOR') return 'trix';
56
+ }
57
+ return undefined;
58
+ };
59
+ const source = ${interactive ? `document.querySelectorAll(${JSON.stringify(INTERACTIVE_SELECTOR)})` : `document.querySelectorAll('body *')`};
60
+ const rows = [];
61
+ for (const el of source) {
62
+ if (rows.length >= ${JSON.stringify(limit)}) break;
63
+ const selector = selectorFor(el);
64
+ if (!selector) continue;
65
+ const row = { role: roleFor(el), name: String(nameFor(el)).trim(), attrs: attrsFor(el), selector };
66
+ const editor = editorFor(el);
67
+ if (editor) row.editor = editor;
68
+ rows.push(row);
69
+ }
70
+ return JSON.stringify(rows);
71
+ })()`;
72
+ }
73
+ export function parseArcRefsResult(value, opts = {}) {
74
+ const parsed = typeof value === 'string' ? JSON.parse(value) : value;
75
+ if (!Array.isArray(parsed))
76
+ throw new Error('Arc returned an invalid DOM ref listing.');
77
+ const nodeMap = new Map();
78
+ const lines = [];
79
+ const compact = opts.compact ?? false;
80
+ parsed.forEach((raw, index) => {
81
+ const row = raw;
82
+ if (typeof row.role !== 'string' || typeof row.name !== 'string' ||
83
+ typeof row.selector !== 'string' || !Array.isArray(row.attrs) ||
84
+ !row.attrs.every((attr) => typeof attr === 'string')) {
85
+ throw new Error('Arc returned an invalid DOM ref entry.');
86
+ }
87
+ const ref = index + 1;
88
+ const node = { ref, role: row.role, name: row.name, attrs: row.attrs, selector: row.selector };
89
+ if (typeof row.editor === 'string')
90
+ node.editor = row.editor;
91
+ nodeMap.set(ref, node);
92
+ const name = row.name ? ` "${truncate(row.name, 50)}"` : '';
93
+ const attrs = row.attrs.length ? ` [${row.attrs.join('] [')}]` : '';
94
+ const editor = row.editor ? ` [editor=${row.editor}]` : '';
95
+ lines.push(`${compact ? '' : '- '}${row.role}${name} [ref=${ref}]${attrs}${editor}`);
96
+ });
97
+ return { refs: lines.join('\n'), nodeMap, opts: { interactive: opts.interactive ?? true, limit: opts.limit ?? 500 } };
98
+ }
99
+ export function arcClickExpression(selector) {
100
+ return `(() => { const el = document.querySelector(${JSON.stringify(selector)}); if (!el) throw new Error('DOM ref is missing'); el.click(); return true; })()`;
101
+ }
102
+ export function arcFillExpression(selector, text, clear = true) {
103
+ return `(() => {
104
+ const el = document.querySelector(${JSON.stringify(selector)});
105
+ if (!el) throw new Error('DOM ref is missing');
106
+ const next = ${JSON.stringify(text)};
107
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
108
+ const proto = el instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
109
+ const setter = Object.getOwnPropertyDescriptor(proto, 'value').set;
110
+ setter.call(el, ${clear ? 'next' : 'el.value + next'});
111
+ } else if (el.isContentEditable) {
112
+ el.textContent = ${clear ? 'next' : '(el.textContent || "") + next'};
113
+ } else throw new Error('DOM ref is not editable');
114
+ el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: next }));
115
+ el.dispatchEvent(new Event('change', { bubbles: true }));
116
+ return true;
117
+ })()`;
118
+ }
119
+ export function arcScrollExpression(deltaX, deltaY) {
120
+ return `(() => { window.scrollBy(${JSON.stringify(deltaX)}, ${JSON.stringify(deltaY)}); return true; })()`;
121
+ }
@@ -0,0 +1,57 @@
1
+ export interface ArcNativeTabRef {
2
+ windowId: string;
3
+ spaceId: string;
4
+ tabId: string;
5
+ }
6
+ export interface ArcNativeTab extends ArcNativeTabRef {
7
+ url: string;
8
+ title: string;
9
+ }
10
+ export interface ArcEnumeratedSpace {
11
+ windowId: string;
12
+ spaceId: string;
13
+ spaceTitle: string;
14
+ activeTabId?: string;
15
+ tabs: ArcNativeTab[];
16
+ }
17
+ export declare const ARC_NATIVE_CAPABILITIES: Readonly<{
18
+ readonly createTab: true;
19
+ readonly navigate: true;
20
+ readonly evaluateSync: true;
21
+ readonly closeTab: true;
22
+ readonly enumerate: true;
23
+ readonly screenshot: false;
24
+ readonly asyncEvaluate: false;
25
+ readonly networkCapture: false;
26
+ readonly consoleCapture: false;
27
+ readonly upload: false;
28
+ readonly pdf: false;
29
+ readonly background: false;
30
+ }>;
31
+ export declare class ArcNativeCapabilityError extends Error {
32
+ readonly capability: string;
33
+ constructor(capability: string, message?: string);
34
+ }
35
+ /** AppleScript strings do not implement JSON's \uXXXX escape syntax. */
36
+ export declare function escapeAppleScriptString(value: string): string;
37
+ /** No shell, bounded output and lifetime, and no blocking of the shared daemon. */
38
+ export declare function execAppleScript(source: string, timeoutMs?: number): Promise<string>;
39
+ export declare function isArcRunning(): Promise<boolean>;
40
+ export declare function enumerateArcSpaces(): Promise<ArcEnumeratedSpace[]>;
41
+ /** Caller durably records marker intent before calling; never use a real page URL as ownership. */
42
+ export declare function createArcTab(target: Pick<ArcNativeTabRef, 'windowId' | 'spaceId'>, markerUrl: string): Promise<ArcNativeTab>;
43
+ export declare function resolveArcTab(ref: ArcNativeTabRef): Promise<ArcNativeTab | null>;
44
+ export declare function navigateArcTab(ref: ArcNativeTabRef, url: string): Promise<void>;
45
+ /** Arc serializes a returned object once. JSON.stringify at the top level would double-encode. */
46
+ export declare function executeJavaScript(ref: ArcNativeTabRef, expression: string): Promise<unknown>;
47
+ export declare function closeArcTab(ref: ArcNativeTabRef): Promise<'closed' | 'missing'>;
48
+ /** Explicit user-facing focus only. Evaluation and cleanup never invoke this. */
49
+ export declare function selectArcTab(ref: ArcNativeTabRef): Promise<void>;
50
+ /**
51
+ * Select `tabId` wherever it lives in window `windowId` (any Space). Used to put
52
+ * the owner back on their tab after a creation that failed mid-way, when there is
53
+ * no owned tab to check against. Returns false when the tab is gone.
54
+ */
55
+ export declare function selectWindowTab(windowId: string, tabId: string): Promise<boolean>;
56
+ /** Restore only while the task's new tab is still selected; preserve a later human choice. */
57
+ export declare function restoreArcSelection(owned: ArcNativeTabRef, previousTabId: string): Promise<boolean>;
@@ -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
+ }
@@ -85,8 +85,12 @@ export declare function getProfileDownloadsDir(name: string): string;
85
85
  * one-shot migration in migrate.ts folds old captures into here.
86
86
  */
87
87
  export declare function getProfileSessionsDir(name: string, task: string): string;
88
+ /** Whether this name has persisted agents-cli metadata on the current device. */
89
+ export declare function isProfileDeclaredHere(name: string): boolean;
88
90
  export declare function listProfiles(): Promise<BrowserProfileWithDeclarations[]>;
89
91
  export declare function getProfile(name: string): Promise<BrowserProfileWithDeclarations | null>;
92
+ /** Persist only the agents-cli alias for an auto-discovered Arc profile. */
93
+ export declare function persistDiscoveredArcProfile(name: string): Promise<void>;
90
94
  /**
91
95
  * True when `profile` can actually launch or attach on THIS machine.
92
96
  *
@@ -245,7 +249,7 @@ export declare function updateProfile(profile: BrowserProfile): Promise<void>;
245
249
  * ({@link getProfileRuntimeDir}) and any live `<name>@<endpoint>` connection, so
246
250
  * changing either orphans the cached browser data. Delete and recreate instead.
247
251
  */
248
- export type EditableProfileFields = Partial<Pick<BrowserProfile, 'description' | 'binary' | 'electron' | 'targetFilter' | 'endpoints' | 'launchPolicy' | 'userDataDir' | 'chrome' | 'secrets' | 'viewport'>>;
252
+ export type EditableProfileFields = Partial<Pick<BrowserProfile, 'description' | 'binary' | 'electron' | 'targetFilter' | 'endpoints' | 'launchPolicy' | 'userDataDir' | 'chrome' | 'secrets' | 'viewport' | 'arc'>>;
249
253
  export interface EditProfileResult {
250
254
  profile: BrowserProfile;
251
255
  devices: string[];
@@ -5,6 +5,7 @@ import { getConfigValue } from '../device-config.js';
5
5
  import { machineId } from '../machine-id.js';
6
6
  import { declaringDevices, profileRegistry, } from './registry.js';
7
7
  import { findBrowserPath, isPortInUse } from './chrome.js';
8
+ import { arcSpaceProfiles, discoverArcProfiles } from './arc-discovery.js';
8
9
  export { declaringDevices, profileKind, profileRegistry, } from './registry.js';
9
10
  /**
10
11
  * Name of the profile the setup wizards pin as this machine's default browser
@@ -129,6 +130,7 @@ function configToProfile(name, config, devices = []) {
129
130
  viewport: config.viewport,
130
131
  logDir: config.logDir,
131
132
  logHost: config.logHost,
133
+ arc: config.arc,
132
134
  devices,
133
135
  };
134
136
  }
@@ -162,26 +164,100 @@ function profileToConfig(profile) {
162
164
  config.logDir = profile.logDir;
163
165
  if (profile.logHost)
164
166
  config.logHost = profile.logHost;
167
+ if (profile.arc)
168
+ config.arc = profile.arc;
165
169
  return config;
166
170
  }
171
+ /**
172
+ * Every Arc Space on this Mac as an agents-cli profile (PHNX-2399). A Space
173
+ * already carries its Arc profile's cookies and logins, so it IS the browser
174
+ * profile agents pick with `--profile` — no second concept.
175
+ *
176
+ * Discovery reads files Arc rewrites on every Space or tab change, so a torn
177
+ * read is an ordinary event, not corruption. It is reported as `error` rather
178
+ * than thrown: the profile store serves every browser on this machine, and a
179
+ * momentary Arc read must never take the Comet or Chrome rows down with it.
180
+ * The error surfaces where an Arc profile is actually asked for by name.
181
+ */
182
+ function discoverArcSpaceProfiles() {
183
+ const discovered = discoverArcProfiles();
184
+ if (!discovered.ok) {
185
+ if (discovered.kind === 'unsupported' || discovered.kind === 'not-installed')
186
+ return { profiles: [] };
187
+ return { profiles: [], error: `Cannot discover Arc Spaces: ${discovered.reason}` };
188
+ }
189
+ return {
190
+ profiles: arcSpaceProfiles(discovered).map((space) => ({
191
+ name: space.name,
192
+ description: `Arc Space "${space.spaceTitle}" (${space.profileName})`,
193
+ browser: 'arc',
194
+ endpoints: { native: { target: 'arc-native://local' } },
195
+ defaultEndpoint: 'native',
196
+ launchPolicy: 'attach-only',
197
+ arc: {
198
+ profileId: space.profileId,
199
+ profileName: space.profileName,
200
+ spaceId: space.spaceId,
201
+ spaceTitle: space.spaceTitle,
202
+ },
203
+ devices: [machineId()],
204
+ })),
205
+ };
206
+ }
207
+ /**
208
+ * Bring a persisted Arc alias up to date with the live Space (title renames).
209
+ * A Space that is no longer discoverable leaves the alias as stored: listing
210
+ * still works, and using the profile fails loud in `resolveArcSpace` with the
211
+ * Space that is missing from Arc's visible windows.
212
+ */
213
+ function refreshLocalArcProfile(profile, live) {
214
+ if (!profile.arc || !profile.devices.includes(machineId()))
215
+ return profile;
216
+ const current = live.find((candidate) => candidate.arc?.spaceId === profile.arc?.spaceId);
217
+ if (!current)
218
+ return profile;
219
+ return { ...profile, arc: current.arc, description: profile.description ?? current.description };
220
+ }
167
221
  function selectedDeclaration(declarations) {
168
222
  return declarations.find((declaration) => declaration.device === machineId()) ?? declarations[0];
169
223
  }
170
224
  function localDeclaration(name) {
171
225
  return profileRegistry().get(name)?.find((declaration) => declaration.device === machineId());
172
226
  }
227
+ /** Whether this name has persisted agents-cli metadata on the current device. */
228
+ export function isProfileDeclaredHere(name) {
229
+ return localDeclaration(name) !== undefined;
230
+ }
173
231
  export async function listProfiles() {
174
- return [...profileRegistry()].map(([name, declarations]) => {
232
+ const native = discoverArcSpaceProfiles();
233
+ const configured = [...profileRegistry()].map(([name, declarations]) => {
175
234
  const selected = selectedDeclaration(declarations);
176
- return configToProfile(name, selected.config, declarations.map((declaration) => declaration.device));
235
+ return refreshLocalArcProfile(configToProfile(name, selected.config, declarations.map((declaration) => declaration.device)), native.profiles);
177
236
  });
237
+ const names = new Set(configured.map((profile) => profile.name));
238
+ return [...configured, ...native.profiles.filter((profile) => !names.has(profile.name))];
178
239
  }
179
240
  export async function getProfile(name) {
241
+ const native = discoverArcSpaceProfiles();
180
242
  const declarations = profileRegistry().get(name);
181
- if (!declarations?.length)
182
- return null;
183
- const selected = selectedDeclaration(declarations);
184
- return configToProfile(name, selected.config, declarations.map((declaration) => declaration.device));
243
+ if (declarations?.length) {
244
+ const selected = selectedDeclaration(declarations);
245
+ return refreshLocalArcProfile(configToProfile(name, selected.config, declarations.map((declaration) => declaration.device)), native.profiles);
246
+ }
247
+ const discovered = native.profiles.find((profile) => profile.name === name) ?? null;
248
+ // Only an Arc profile asked for by name pays for a failed Arc read.
249
+ if (!discovered && native.error && name.startsWith('arc-'))
250
+ throw new Error(native.error);
251
+ return discovered;
252
+ }
253
+ /** Persist only the agents-cli alias for an auto-discovered Arc profile. */
254
+ export async function persistDiscoveredArcProfile(name) {
255
+ if (localDeclaration(name))
256
+ return;
257
+ const profile = discoverArcSpaceProfiles().profiles.find((candidate) => candidate.name === name);
258
+ if (!profile)
259
+ return;
260
+ await createProfile(profile);
185
261
  }
186
262
  /**
187
263
  * True when `profile` can actually launch or attach on THIS machine.
@@ -194,6 +270,10 @@ export async function getProfile(name) {
194
270
  * for the current machine.
195
271
  */
196
272
  export function isProfileLaunchableHere(profile) {
273
+ const nativeArc = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('arc-native:'));
274
+ if (nativeArc) {
275
+ return !!profile.arc && discoverArcSpaceProfiles().profiles.some((candidate) => candidate.arc?.spaceId === profile.arc?.spaceId);
276
+ }
197
277
  const remote = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('ssh://'));
198
278
  if (remote)
199
279
  return true;
@@ -334,7 +414,7 @@ export async function ensureDefaultBrowserProfile() {
334
414
  const configured = getConfiguredDefaultProfileName();
335
415
  if (configured && configured !== DEFAULT_PROFILE_ALIAS) {
336
416
  const chosen = await getProfile(configured);
337
- if (chosen && isProfileLaunchableHere(chosen))
417
+ if (chosen && (!chosen.devices.includes(machineId()) || isProfileLaunchableHere(chosen)))
338
418
  return chosen;
339
419
  if (!chosen) {
340
420
  const central = readMeta().browser ?? {};
@@ -520,7 +600,11 @@ export async function createProfile(profile) {
520
600
  // Skip for SSH profiles: the browser binary lives on the remote host, so a
521
601
  // local lookup would validate the wrong machine. The remote launcher resolves
522
602
  // it at connect time.
523
- if (!hasSshEndpoint(profile.endpoints)) {
603
+ const nativeArc = Object.values(getEndpointPresets(profile)).some((preset) => preset.target.startsWith('arc-native:'));
604
+ if (nativeArc && (!profile.arc || profile.browser !== 'arc')) {
605
+ throw new Error('An arc-native profile requires stable Arc Space metadata. Pick a discovered one from `agents browser profiles list`.');
606
+ }
607
+ if (!hasSshEndpoint(profile.endpoints) && !nativeArc) {
524
608
  findBrowserPath(profile.browser, profile.binary);
525
609
  }
526
610
  const config = profileToConfig(profile);
@@ -575,7 +659,8 @@ export async function editProfile(name, patch) {
575
659
  assertLocalPortFree(merged, { ignore: name });
576
660
  // Same rule as create: the binary lives on the remote for an SSH profile, so a
577
661
  // local lookup would validate the wrong machine.
578
- if (!hasSshEndpoint(merged.endpoints)) {
662
+ const nativeArc = Object.values(getEndpointPresets(merged)).some((preset) => preset.target.startsWith('arc-native:'));
663
+ if (!hasSshEndpoint(merged.endpoints) && !nativeArc) {
579
664
  findBrowserPath(merged.browser, merged.binary);
580
665
  }
581
666
  await updateProfile(merged);
@@ -10,6 +10,8 @@ export interface RefNode {
10
10
  name: string;
11
11
  attrs: string[];
12
12
  backendNodeId?: number;
13
+ /** Stable selector used by non-CDP DOM backends such as native Arc. */
14
+ selector?: string;
13
15
  editor?: string;
14
16
  }
15
17
  /**