@phnx-labs/agents-cli 1.19.2 → 1.20.3

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 (156) hide show
  1. package/CHANGELOG.md +140 -0
  2. package/README.md +72 -12
  3. package/dist/browser.js +0 -0
  4. package/dist/commands/browser.js +88 -16
  5. package/dist/commands/cli.d.ts +14 -0
  6. package/dist/commands/cli.js +244 -0
  7. package/dist/commands/cloud.js +1 -1
  8. package/dist/commands/commands.js +27 -10
  9. package/dist/commands/computer.js +18 -1
  10. package/dist/commands/doctor.d.ts +1 -1
  11. package/dist/commands/doctor.js +2 -2
  12. package/dist/commands/exec.js +38 -18
  13. package/dist/commands/factory.d.ts +3 -14
  14. package/dist/commands/factory.js +3 -3
  15. package/dist/commands/feedback.d.ts +7 -0
  16. package/dist/commands/feedback.js +89 -0
  17. package/dist/commands/helper.d.ts +12 -0
  18. package/dist/commands/helper.js +87 -0
  19. package/dist/commands/hooks.js +89 -10
  20. package/dist/commands/mcp.js +166 -10
  21. package/dist/commands/packages.js +196 -27
  22. package/dist/commands/permissions.js +21 -6
  23. package/dist/commands/plugins.js +11 -4
  24. package/dist/commands/profiles.d.ts +8 -0
  25. package/dist/commands/profiles.js +118 -5
  26. package/dist/commands/prune.js +39 -160
  27. package/dist/commands/pull.js +58 -5
  28. package/dist/commands/routines.js +107 -14
  29. package/dist/commands/rules.js +8 -4
  30. package/dist/commands/secrets-migrate.d.ts +24 -0
  31. package/dist/commands/secrets-migrate.js +198 -0
  32. package/dist/commands/secrets-sync.d.ts +11 -0
  33. package/dist/commands/secrets-sync.js +155 -0
  34. package/dist/commands/secrets.js +79 -46
  35. package/dist/commands/sessions.d.ts +28 -0
  36. package/dist/commands/sessions.js +98 -33
  37. package/dist/commands/setup.d.ts +1 -0
  38. package/dist/commands/setup.js +37 -28
  39. package/dist/commands/skills.js +25 -8
  40. package/dist/commands/subagents.js +69 -49
  41. package/dist/commands/teams.js +61 -10
  42. package/dist/commands/utils.d.ts +33 -0
  43. package/dist/commands/utils.js +139 -0
  44. package/dist/commands/versions.d.ts +4 -3
  45. package/dist/commands/versions.js +134 -130
  46. package/dist/commands/view.d.ts +6 -0
  47. package/dist/commands/view.js +175 -19
  48. package/dist/commands/workflows.js +29 -6
  49. package/dist/computer.js +0 -0
  50. package/dist/index.js +38 -6
  51. package/dist/lib/acp/client.js +6 -1
  52. package/dist/lib/acp/harnesses.js +8 -0
  53. package/dist/lib/agents.d.ts +4 -0
  54. package/dist/lib/agents.js +125 -34
  55. package/dist/lib/auto-pull-worker.js +18 -1
  56. package/dist/lib/browser/cdp.d.ts +8 -1
  57. package/dist/lib/browser/cdp.js +40 -3
  58. package/dist/lib/browser/chrome.d.ts +13 -0
  59. package/dist/lib/browser/chrome.js +46 -3
  60. package/dist/lib/browser/domain-skills.d.ts +51 -0
  61. package/dist/lib/browser/domain-skills.js +157 -0
  62. package/dist/lib/browser/drivers/local.js +45 -4
  63. package/dist/lib/browser/drivers/ssh.js +2 -2
  64. package/dist/lib/browser/ipc.d.ts +8 -1
  65. package/dist/lib/browser/ipc.js +37 -28
  66. package/dist/lib/browser/profiles.d.ts +16 -3
  67. package/dist/lib/browser/profiles.js +44 -4
  68. package/dist/lib/browser/service.d.ts +3 -0
  69. package/dist/lib/browser/service.js +40 -5
  70. package/dist/lib/browser/types.d.ts +11 -4
  71. package/dist/lib/cli-resources.d.ts +137 -0
  72. package/dist/lib/cli-resources.js +477 -0
  73. package/dist/lib/cloud/factory.d.ts +1 -1
  74. package/dist/lib/cloud/factory.js +1 -1
  75. package/dist/lib/cloud/rush.js +5 -5
  76. package/dist/lib/command-skills.js +0 -2
  77. package/dist/lib/computer-rpc.d.ts +3 -0
  78. package/dist/lib/computer-rpc.js +53 -0
  79. package/dist/lib/daemon.js +20 -0
  80. package/dist/lib/events.d.ts +16 -2
  81. package/dist/lib/events.js +33 -2
  82. package/dist/lib/exec.d.ts +42 -13
  83. package/dist/lib/exec.js +127 -33
  84. package/dist/lib/help.js +11 -5
  85. package/dist/lib/hooks/cache.d.ts +38 -0
  86. package/dist/lib/hooks/cache.js +242 -0
  87. package/dist/lib/hooks/profile.d.ts +33 -0
  88. package/dist/lib/hooks/profile.js +129 -0
  89. package/dist/lib/hooks.d.ts +0 -10
  90. package/dist/lib/hooks.js +246 -11
  91. package/dist/lib/mcp.d.ts +15 -0
  92. package/dist/lib/mcp.js +46 -0
  93. package/dist/lib/migrate.js +1 -1
  94. package/dist/lib/overdue.d.ts +26 -0
  95. package/dist/lib/overdue.js +101 -0
  96. package/dist/lib/permissions.d.ts +13 -0
  97. package/dist/lib/permissions.js +55 -1
  98. package/dist/lib/plugin-marketplace.js +1 -1
  99. package/dist/lib/plugins.js +15 -1
  100. package/dist/lib/profiles-presets.d.ts +26 -0
  101. package/dist/lib/profiles-presets.js +216 -0
  102. package/dist/lib/profiles.d.ts +34 -0
  103. package/dist/lib/profiles.js +112 -1
  104. package/dist/lib/resources/mcp.js +37 -0
  105. package/dist/lib/resources.d.ts +1 -1
  106. package/dist/lib/rotate.js +10 -4
  107. package/dist/lib/routines-format.d.ts +47 -0
  108. package/dist/lib/routines-format.js +194 -0
  109. package/dist/lib/routines.d.ts +8 -2
  110. package/dist/lib/routines.js +34 -14
  111. package/dist/lib/runner.js +83 -15
  112. package/dist/lib/scheduler.js +8 -1
  113. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  114. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  115. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -9
  116. package/dist/lib/secrets/bundles.d.ts +34 -17
  117. package/dist/lib/secrets/bundles.js +210 -36
  118. package/dist/lib/secrets/index.d.ts +49 -30
  119. package/dist/lib/secrets/index.js +126 -115
  120. package/dist/lib/secrets/install-helper.d.ts +45 -0
  121. package/dist/lib/secrets/install-helper.js +165 -0
  122. package/dist/lib/secrets/linux.js +4 -4
  123. package/dist/lib/secrets/sync.d.ts +56 -0
  124. package/dist/lib/secrets/sync.js +180 -0
  125. package/dist/lib/session/active.d.ts +8 -0
  126. package/dist/lib/session/active.js +3 -2
  127. package/dist/lib/session/db.d.ts +0 -4
  128. package/dist/lib/session/db.js +0 -26
  129. package/dist/lib/session/parse.d.ts +1 -0
  130. package/dist/lib/session/parse.js +44 -0
  131. package/dist/lib/session/render.js +4 -4
  132. package/dist/lib/session/types.d.ts +2 -2
  133. package/dist/lib/session/types.js +1 -1
  134. package/dist/lib/shims.d.ts +5 -2
  135. package/dist/lib/shims.js +70 -38
  136. package/dist/lib/state.d.ts +14 -2
  137. package/dist/lib/state.js +51 -20
  138. package/dist/lib/teams/agents.d.ts +5 -4
  139. package/dist/lib/teams/agents.js +48 -22
  140. package/dist/lib/teams/api.d.ts +2 -1
  141. package/dist/lib/teams/api.js +4 -3
  142. package/dist/lib/teams/parsers.d.ts +1 -1
  143. package/dist/lib/teams/parsers.js +153 -3
  144. package/dist/lib/teams/summarizer.js +18 -2
  145. package/dist/lib/teams/worktree.js +14 -3
  146. package/dist/lib/types.d.ts +63 -4
  147. package/dist/lib/types.js +8 -3
  148. package/dist/lib/usage.d.ts +27 -2
  149. package/dist/lib/usage.js +100 -17
  150. package/dist/lib/versions.d.ts +45 -3
  151. package/dist/lib/versions.js +455 -60
  152. package/package.json +15 -14
  153. package/scripts/install-helper.js +97 -0
  154. package/scripts/postinstall.js +16 -0
  155. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  156. package/npm-shrinkwrap.json +0 -3162
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Domain-skill discovery for `agents browser start`.
3
+ *
4
+ * When a browser task opens a URL, look up a site-specific SKILL.md from
5
+ * `~/.agents/skills/browser/domain-skills/<dir>/SKILL.md` and surface its
6
+ * contents so the calling agent gets per-site operating instructions
7
+ * (selectors, gotchas, sign-in quirks) before it starts driving the page.
8
+ *
9
+ * Matching is intentionally simple: derive the hostname's second-level
10
+ * label (e.g. `perplexity` from `perplexity.ai`, `slack` from `app.slack.com`)
11
+ * and look for a directory of the same name. If the user wants a different
12
+ * mapping (e.g. `mail.google.com` -> `gmail/`), they can pin it via a
13
+ * `domains: [...]` array in the SKILL.md frontmatter; that override beats
14
+ * the directory-name match.
15
+ */
16
+ import * as fs from 'fs';
17
+ import * as os from 'os';
18
+ import * as path from 'path';
19
+ /** Where domain-skills live. Override via $AGENTS_BROWSER_DOMAIN_SKILLS_DIR for tests. */
20
+ export function domainSkillsRoot() {
21
+ const override = process.env.AGENTS_BROWSER_DOMAIN_SKILLS_DIR;
22
+ if (override)
23
+ return override;
24
+ return path.join(os.homedir(), '.agents', 'skills', 'browser', 'domain-skills');
25
+ }
26
+ /**
27
+ * Derive match candidates from a hostname. Order matters — earlier candidates
28
+ * are tried first.
29
+ *
30
+ * Examples:
31
+ * perplexity.ai -> ['perplexity.ai', 'perplexity']
32
+ * app.slack.com -> ['app.slack.com', 'slack.com', 'slack']
33
+ * mail.google.com -> ['mail.google.com', 'google.com', 'google', 'mail']
34
+ * higgsfield.ai -> ['higgsfield.ai', 'higgsfield']
35
+ */
36
+ export function hostnameMatchCandidates(hostname) {
37
+ const cleaned = hostname.toLowerCase().replace(/^www\./, '');
38
+ if (!cleaned)
39
+ return [];
40
+ const parts = cleaned.split('.').filter(Boolean);
41
+ const out = new Set();
42
+ out.add(cleaned);
43
+ // Progressive label-stripping from the left: app.slack.com -> slack.com.
44
+ for (let i = 1; i < parts.length; i++) {
45
+ out.add(parts.slice(i).join('.'));
46
+ }
47
+ // Second-level label without TLD: app.slack.com -> slack, perplexity.ai -> perplexity.
48
+ if (parts.length >= 2) {
49
+ out.add(parts[parts.length - 2]);
50
+ }
51
+ // First label too, so mail.google.com can resolve a `mail` dir if that's how
52
+ // the user organized their skills. Last so explicit second-level wins.
53
+ if (parts.length >= 2) {
54
+ out.add(parts[0]);
55
+ }
56
+ return Array.from(out);
57
+ }
58
+ /** Parse a SKILL.md's frontmatter `domains:` list, if any. Best-effort, no schema. */
59
+ function parseDomainsFrontmatter(content) {
60
+ // Frontmatter must be at file start: ---\n...\n---\n
61
+ if (!content.startsWith('---'))
62
+ return [];
63
+ const end = content.indexOf('\n---', 3);
64
+ if (end < 0)
65
+ return [];
66
+ const fm = content.slice(3, end);
67
+ // Inline array form: domains: [a, b, c]
68
+ const inline = fm.match(/^domains:\s*\[([^\]]*)\]/m);
69
+ if (inline) {
70
+ return inline[1]
71
+ .split(',')
72
+ .map((s) => s.trim().replace(/^["']|["']$/g, '').toLowerCase())
73
+ .filter(Boolean);
74
+ }
75
+ // Block list form:
76
+ // domains:
77
+ // - a
78
+ // - b
79
+ const block = fm.match(/^domains:\s*\n((?:\s+-\s+\S+\n?)+)/m);
80
+ if (block) {
81
+ return block[1]
82
+ .split('\n')
83
+ .map((line) => line.replace(/^\s*-\s*/, '').trim().replace(/^["']|["']$/g, '').toLowerCase())
84
+ .filter(Boolean);
85
+ }
86
+ return [];
87
+ }
88
+ /**
89
+ * Resolve a URL to its matching domain-skill, or null if none.
90
+ *
91
+ * Two-pass strategy:
92
+ * 1. Index every SKILL.md in the root and read its `domains:` frontmatter.
93
+ * If any pinned domain matches a candidate, return that skill.
94
+ * 2. Fall back to directory-name match against the candidate list.
95
+ *
96
+ * Errors (missing root, unreadable file, invalid URL) are swallowed and
97
+ * yield null — domain-skill discovery must never break browser start.
98
+ */
99
+ export function resolveDomainSkill(url) {
100
+ let hostname;
101
+ try {
102
+ hostname = new URL(url).hostname;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ if (!hostname)
108
+ return null;
109
+ const root = domainSkillsRoot();
110
+ let entries;
111
+ try {
112
+ entries = fs.readdirSync(root, { withFileTypes: true });
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ const candidates = hostnameMatchCandidates(hostname);
118
+ if (candidates.length === 0)
119
+ return null;
120
+ const candidateSet = new Set(candidates);
121
+ const indexed = [];
122
+ for (const e of entries) {
123
+ if (!e.isDirectory())
124
+ continue;
125
+ const skillPath = path.join(root, e.name, 'SKILL.md');
126
+ let content;
127
+ try {
128
+ content = fs.readFileSync(skillPath, 'utf-8');
129
+ }
130
+ catch {
131
+ continue;
132
+ }
133
+ indexed.push({
134
+ name: e.name,
135
+ skillPath,
136
+ content,
137
+ pinned: parseDomainsFrontmatter(content),
138
+ });
139
+ }
140
+ // Pass 1: explicit `domains:` overrides.
141
+ for (const s of indexed) {
142
+ for (const d of s.pinned) {
143
+ if (candidateSet.has(d)) {
144
+ return { name: s.name, path: s.skillPath, content: s.content, hostname };
145
+ }
146
+ }
147
+ }
148
+ // Pass 2: directory-name match, walking candidates in priority order.
149
+ const byName = new Map(indexed.map((s) => [s.name.toLowerCase(), s]));
150
+ for (const c of candidates) {
151
+ const hit = byName.get(c);
152
+ if (hit) {
153
+ return { name: hit.name, path: hit.skillPath, content: hit.content, hostname };
154
+ }
155
+ }
156
+ return null;
157
+ }
@@ -1,6 +1,27 @@
1
+ import * as net from 'net';
1
2
  import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from '../cdp.js';
2
3
  import { launchBrowser, getPortOccupant } from '../chrome.js';
3
4
  import { parseEndpointUrl } from '../profiles.js';
5
+ /**
6
+ * Cheap TCP-level "is something bound here?" probe. Used as a fallback when
7
+ * `getPortOccupant()` (lsof-based) misses the process — Comet and other
8
+ * Electron apps sometimes hold a TCP socket that lsof's `-sTCP:LISTEN` filter
9
+ * doesn't report. If anything ACKs the connection, we treat the port as taken
10
+ * and surface a friendly error instead of silently auto-launching a fresh
11
+ * browser that would then conflict.
12
+ */
13
+ async function probeTcpBound(port, host = '127.0.0.1', timeoutMs = 500) {
14
+ return new Promise((resolve) => {
15
+ const sock = net.createConnection({ port, host });
16
+ const cleanup = () => {
17
+ sock.removeAllListeners();
18
+ sock.destroy();
19
+ };
20
+ const timer = setTimeout(() => { cleanup(); resolve(false); }, timeoutMs);
21
+ sock.once('connect', () => { clearTimeout(timer); cleanup(); resolve(true); });
22
+ sock.once('error', () => { clearTimeout(timer); cleanup(); resolve(false); });
23
+ });
24
+ }
4
25
  /**
5
26
  * Local-port listeners we refuse to attach through. These forward CDP traffic
6
27
  * to a remote host — silently using them would let a `cdp://127.0.0.1:N`
@@ -34,7 +55,7 @@ export async function connectLocal(endpoint, profile) {
34
55
  `ssh:// endpoint to drive the remote browser explicitly.`);
35
56
  }
36
57
  try {
37
- const { wsUrl, browser } = await discoverBrowserWsUrl(port);
58
+ const { wsUrl, browser } = await discoverBrowserWsUrl(port, 'localhost', profile.name);
38
59
  verifyBrowserIdentity(browser, profile.browser, port);
39
60
  const cdp = new CDPClient();
40
61
  await cdp.connect(wsUrl);
@@ -55,11 +76,31 @@ export async function connectLocal(endpoint, profile) {
55
76
  `(\`kill ${occupant.pid}\`) or restart it with \`--remote-debugging-port=${port}\` ` +
56
77
  `so profile "${profile.name}" can attach.`);
57
78
  }
79
+ // lsof-based detection misses some Electron-family processes (Comet, custom
80
+ // chrome wrappers). Cheap TCP probe as a safety net: if something ACKs a
81
+ // connect, the port is bound — bail loudly with the profile name + endpoint
82
+ // rather than silently launching a duplicate browser.
83
+ if (await probeTcpBound(port)) {
84
+ throw new Error(`Profile "${profile.name}" is configured for cdp://127.0.0.1:${port}, ` +
85
+ `but something is already bound to that port without serving the Chrome ` +
86
+ `DevTools Protocol. If that's your browser running without remote debugging, ` +
87
+ `quit it and relaunch with \`--remote-debugging-port=${port}\`. Otherwise, ` +
88
+ `update the profile to a free port (\`agents browser profiles list\`).`);
89
+ }
58
90
  const newPort = port;
59
91
  const chromeOpts = { ...profile.chrome, viewport: profile.viewport };
60
- const { pid, wsUrl } = await launchBrowser(profile.name, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary, profile.electron === true);
92
+ let launched;
93
+ try {
94
+ launched = await launchBrowser(profile.name, profile.browser, newPort, chromeOpts, profile.secrets, profile.binary, profile.electron === true);
95
+ }
96
+ catch (launchErr) {
97
+ const reason = launchErr instanceof Error ? launchErr.message : String(launchErr);
98
+ throw new Error(`Could not start ${profile.browser} for profile "${profile.name}" on port ${newPort}: ${reason}. ` +
99
+ `Check that the browser binary is installed (\`agents browser profiles list\`) and ` +
100
+ `that no other process is holding the port.`);
101
+ }
61
102
  const cdp = new CDPClient();
62
- await cdp.connect(wsUrl);
63
- return { cdp, port: newPort, pid };
103
+ await cdp.connect(launched.wsUrl);
104
+ return { cdp, port: newPort, pid: launched.pid };
64
105
  }
65
106
  }
@@ -63,7 +63,7 @@ export async function connectSSH(endpoint, profile) {
63
63
  tunnel.kill();
64
64
  throw new Error(`SSH tunnel failed to establish to ${host}`);
65
65
  }
66
- const { wsUrl, browser } = await discoverBrowserWsUrl(localPort);
66
+ const { wsUrl, browser } = await discoverBrowserWsUrl(localPort, 'localhost', profile.name);
67
67
  try {
68
68
  verifyBrowserIdentity(browser, profile.browser, remotePort, host);
69
69
  }
@@ -180,7 +180,7 @@ async function ensureRemoteBrowser(user, host, browserType, port, customBinary)
180
180
  const remoteCmd = [
181
181
  shellQuote(browserPath),
182
182
  `--remote-debugging-port=${port}`,
183
- shellQuote('--remote-allow-origins=*'),
183
+ shellQuote(`--remote-allow-origins=http://127.0.0.1:${port}`),
184
184
  '--disable-background-timer-throttling',
185
185
  `--user-data-dir=/tmp/agents-browser-${port}`,
186
186
  '</dev/null >/dev/null 2>&1 &',
@@ -1,5 +1,12 @@
1
1
  import { BrowserService } from './service.js';
2
2
  import type { IPCRequest, IPCResponse } from './types.js';
3
+ export interface IPCRequestOptions {
4
+ autoStartDaemon?: boolean;
5
+ }
6
+ export declare class BrowserDaemonNotRunningError extends Error {
7
+ constructor();
8
+ }
9
+ export declare function formatBrowserDaemonNotRunningError(): string;
3
10
  export declare function getSocketPath(): string;
4
11
  export declare class BrowserIPCServer {
5
12
  private server;
@@ -9,4 +16,4 @@ export declare class BrowserIPCServer {
9
16
  stop(): Promise<void>;
10
17
  private handleRequest;
11
18
  }
12
- export declare function sendIPCRequest(request: IPCRequest): Promise<IPCResponse>;
19
+ export declare function sendIPCRequest(request: IPCRequest, opts?: IPCRequestOptions): Promise<IPCResponse>;
@@ -5,9 +5,32 @@ import { getHelpersDir } from '../state.js';
5
5
  import { startDaemon } from '../daemon.js';
6
6
  import { getCliVersion } from '../version.js';
7
7
  const SOCKET_NAME = 'browser.sock';
8
+ export class BrowserDaemonNotRunningError extends Error {
9
+ constructor() {
10
+ super(formatBrowserDaemonNotRunningError());
11
+ this.name = 'BrowserDaemonNotRunningError';
12
+ }
13
+ }
14
+ export function formatBrowserDaemonNotRunningError() {
15
+ return [
16
+ 'Browser daemon not running.',
17
+ 'Start it with: agents browser start (auto-picks an installed browser)',
18
+ 'Or pin a profile: agents browser start --profile <name>',
19
+ 'List profiles: agents browser profiles list',
20
+ ].join('\n');
21
+ }
8
22
  export function getSocketPath() {
9
23
  return path.join(getHelpersDir(), 'browser', SOCKET_NAME);
10
24
  }
25
+ async function waitForSocket(socketPath, timeoutMs) {
26
+ const deadline = Date.now() + timeoutMs;
27
+ while (Date.now() < deadline) {
28
+ if (fs.existsSync(socketPath))
29
+ return;
30
+ await new Promise((resolve) => setTimeout(resolve, 100));
31
+ }
32
+ throw new Error('Timeout waiting for browser daemon socket');
33
+ }
11
34
  export class BrowserIPCServer {
12
35
  server = null;
13
36
  service;
@@ -107,12 +130,14 @@ export class BrowserIPCServer {
107
130
  taskName: request.taskName,
108
131
  url: request.url,
109
132
  endpointName: request.endpoint,
133
+ skipDomainSkill: request.skipDomainSkill,
110
134
  });
111
135
  return {
112
136
  ok: true,
113
137
  task: result.name,
114
138
  tabId: result.tabId,
115
139
  windowTargetId: result.windowId,
140
+ skill: result.skill,
116
141
  };
117
142
  }
118
143
  case 'done': {
@@ -411,8 +436,8 @@ async function maybeWarnVersionMismatch() {
411
436
  // itself a hint, but a noisy one. Stay silent on this path.
412
437
  }
413
438
  }
414
- export async function sendIPCRequest(request) {
415
- const result = await sendRawIPCRequest(request);
439
+ export async function sendIPCRequest(request, opts = {}) {
440
+ const result = await sendRawIPCRequest(request, opts);
416
441
  // Run the version check after the user's request returns — keeps the
417
442
  // critical path zero-overhead and ensures `start` doesn't get blocked
418
443
  // on a daemon-restart warning that the user hasn't read yet.
@@ -421,38 +446,18 @@ export async function sendIPCRequest(request) {
421
446
  }
422
447
  return result;
423
448
  }
424
- async function sendRawIPCRequest(request) {
449
+ async function sendRawIPCRequest(request, opts = {}) {
425
450
  const socketPath = getSocketPath();
451
+ const autoStartDaemon = opts.autoStartDaemon ?? true;
426
452
  if (!fs.existsSync(socketPath)) {
453
+ if (!autoStartDaemon) {
454
+ throw new BrowserDaemonNotRunningError();
455
+ }
427
456
  await fs.promises.mkdir(path.dirname(socketPath), { recursive: true, mode: 0o700 });
428
457
  await fs.promises.chmod(path.dirname(socketPath), 0o700);
429
458
  startDaemon();
430
459
  if (!fs.existsSync(socketPath)) {
431
- await new Promise((resolve, reject) => {
432
- const socketDir = path.dirname(socketPath);
433
- const socketName = path.basename(socketPath);
434
- const watcher = fs.watch(socketDir, (_event, file) => {
435
- if (file === socketName) {
436
- clearTimeout(timeout);
437
- watcher.close();
438
- resolve();
439
- }
440
- });
441
- watcher.on('error', (error) => {
442
- clearTimeout(timeout);
443
- watcher.close();
444
- reject(error);
445
- });
446
- const timeout = setTimeout(() => {
447
- watcher.close();
448
- reject(new Error('Timeout waiting for browser daemon socket'));
449
- }, 6000);
450
- if (fs.existsSync(socketPath)) {
451
- clearTimeout(timeout);
452
- watcher.close();
453
- resolve();
454
- }
455
- });
460
+ await waitForSocket(socketPath, 6000);
456
461
  }
457
462
  if (!fs.existsSync(socketPath)) {
458
463
  throw new Error('Failed to start browser daemon');
@@ -475,6 +480,10 @@ async function sendRawIPCRequest(request) {
475
480
  }
476
481
  });
477
482
  socket.on('error', (err) => {
483
+ if (!autoStartDaemon && (err.code === 'ENOENT' || err.code === 'ECONNREFUSED')) {
484
+ reject(new BrowserDaemonNotRunningError());
485
+ return;
486
+ }
478
487
  reject(new Error(`IPC error: ${err.message}`));
479
488
  });
480
489
  socket.on('close', () => {
@@ -1,9 +1,22 @@
1
1
  import type { BrowserProfile } from './types.js';
2
2
  export type { BrowserProfile } from './types.js';
3
+ export declare const DEFAULT_BROWSER_PROFILE_NAME = "default";
3
4
  export declare function getBrowserRuntimeDir(): string;
4
5
  export declare function getProfileRuntimeDir(name: string): string;
5
6
  export declare function listProfiles(): Promise<BrowserProfile[]>;
6
7
  export declare function getProfile(name: string): Promise<BrowserProfile | null>;
8
+ /**
9
+ * Ensure a `default` profile exists, auto-picking the first installed
10
+ * Chromium-family browser per the platform priority list in chrome.ts.
11
+ *
12
+ * Re-uses an existing `default` profile as-is (we don't second-guess the user
13
+ * if they've already customized it). On first run we walk the priority list
14
+ * (macOS: chrome > brave > edge > chromium > comet; Linux: chrome > chromium >
15
+ * brave > edge; Windows: edge > chrome > brave) and pin the profile to the
16
+ * first match. Throws an actionable error if none of those binaries are
17
+ * installed so the user knows exactly which browsers we'd accept.
18
+ */
19
+ export declare function ensureDefaultBrowserProfile(): Promise<BrowserProfile>;
7
20
  /**
8
21
  * Compute the LOCAL port a profile will occupy at runtime:
9
22
  * - `cdp://127.0.0.1:N` → N (we listen on N directly)
@@ -52,12 +65,12 @@ export declare function resolveEndpoint(profile: BrowserProfile, endpointName?:
52
65
  * Returns undefined for endpoint shapes that don't carry a port (e.g. ws:// without one).
53
66
  *
54
67
  * Ports are scoped by host: a `cdp://127.0.0.1:9222` profile (local Chrome on
55
- * this machine) and an `ssh://mac-mini:9222` profile (Comet on mac-mini)
56
- * point at different physical ports — the host disambiguates them.
68
+ * this machine) and an `ssh://remote-host:9222` profile (Comet on a remote
69
+ * host) point at different physical ports — the host disambiguates them.
57
70
  *
58
71
  * Accepts both `scheme://host:port` and `scheme://host?port=N` shapes (the
59
72
  * latter is the documented form in `types.ts` for `ssh://`). Without this,
60
- * `ssh://mac-mini?port=18805` would silently fall back to 9222 and every
73
+ * `ssh://remote-host?port=18805` would silently fall back to 9222 and every
61
74
  * `?port=`-style SSH profile would collide on creation.
62
75
  */
63
76
  export declare function extractConfiguredEndpoint(profile: BrowserProfile): {
@@ -1,7 +1,9 @@
1
1
  import * as path from 'path';
2
2
  import { execFileSync } from 'child_process';
3
3
  import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, readMeta, writeMeta, } from '../state.js';
4
- import { findBrowserPath } from './chrome.js';
4
+ import { findBrowserPath, findFirstInstalledBrowser } from './chrome.js';
5
+ import { DEFAULT_VIEWPORT } from './devices.js';
6
+ export const DEFAULT_BROWSER_PROFILE_NAME = 'default';
5
7
  export function getBrowserRuntimeDir() {
6
8
  return getBrowserRuntimeDirRoot();
7
9
  }
@@ -67,6 +69,44 @@ export async function getProfile(name) {
67
69
  return null;
68
70
  return configToProfile(name, config);
69
71
  }
72
+ /**
73
+ * Ensure a `default` profile exists, auto-picking the first installed
74
+ * Chromium-family browser per the platform priority list in chrome.ts.
75
+ *
76
+ * Re-uses an existing `default` profile as-is (we don't second-guess the user
77
+ * if they've already customized it). On first run we walk the priority list
78
+ * (macOS: chrome > brave > edge > chromium > comet; Linux: chrome > chromium >
79
+ * brave > edge; Windows: edge > chrome > brave) and pin the profile to the
80
+ * first match. Throws an actionable error if none of those binaries are
81
+ * installed so the user knows exactly which browsers we'd accept.
82
+ */
83
+ export async function ensureDefaultBrowserProfile() {
84
+ const existing = await getProfile(DEFAULT_BROWSER_PROFILE_NAME);
85
+ if (existing)
86
+ return existing;
87
+ const detected = findFirstInstalledBrowser();
88
+ if (!detected) {
89
+ throw new Error('No supported browser found. Install one of: Chrome, Brave, Edge, Chromium, or Comet, ' +
90
+ 'then re-run `agents browser start`. Or create a profile explicitly with ' +
91
+ '`agents browser profiles create <name> --browser <chrome|comet|chromium|brave|edge|custom>`. ' +
92
+ 'Note: Safari and Firefox are not supported — agents browser drives over the ' +
93
+ 'Chrome DevTools Protocol, which they don\'t implement.');
94
+ }
95
+ const freePort = await findFreeProfilePort();
96
+ const profile = {
97
+ name: DEFAULT_BROWSER_PROFILE_NAME,
98
+ description: `Auto-detected ${detected.browserType} profile`,
99
+ browser: detected.browserType,
100
+ binary: detected.binary,
101
+ endpoints: [`cdp://127.0.0.1:${freePort}`],
102
+ viewport: {
103
+ width: DEFAULT_VIEWPORT.width,
104
+ height: DEFAULT_VIEWPORT.height,
105
+ },
106
+ };
107
+ await createProfile(profile);
108
+ return profile;
109
+ }
70
110
  /**
71
111
  * Compute the LOCAL port a profile will occupy at runtime:
72
112
  * - `cdp://127.0.0.1:N` → N (we listen on N directly)
@@ -259,12 +299,12 @@ export function resolveEndpoint(profile, endpointName) {
259
299
  * Returns undefined for endpoint shapes that don't carry a port (e.g. ws:// without one).
260
300
  *
261
301
  * Ports are scoped by host: a `cdp://127.0.0.1:9222` profile (local Chrome on
262
- * this machine) and an `ssh://mac-mini:9222` profile (Comet on mac-mini)
263
- * point at different physical ports — the host disambiguates them.
302
+ * this machine) and an `ssh://remote-host:9222` profile (Comet on a remote
303
+ * host) point at different physical ports — the host disambiguates them.
264
304
  *
265
305
  * Accepts both `scheme://host:port` and `scheme://host?port=N` shapes (the
266
306
  * latter is the documented form in `types.ts` for `ssh://`). Without this,
267
- * `ssh://mac-mini?port=18805` would silently fall back to 9222 and every
307
+ * `ssh://remote-host?port=18805` would silently fall back to 9222 and every
268
308
  * `?port=`-style SSH profile would collide on creation.
269
309
  */
270
310
  export function extractConfiguredEndpoint(profile) {
@@ -1,3 +1,4 @@
1
+ import { type ResolvedDomainSkill } from './domain-skills.js';
1
2
  import { type TabInfo, type ProfileStatus, type HistoricalTask } from './types.js';
2
3
  import { type RefOpts, type RefNode } from './refs.js';
3
4
  import type { TargetFilter } from './types.js';
@@ -55,12 +56,14 @@ export declare class BrowserService {
55
56
  taskName?: string;
56
57
  url?: string;
57
58
  endpointName?: string;
59
+ skipDomainSkill?: boolean;
58
60
  }): Promise<{
59
61
  task: string;
60
62
  name: string;
61
63
  tabId?: string;
62
64
  windowId?: string;
63
65
  profile: string;
66
+ skill?: ResolvedDomainSkill;
64
67
  }>;
65
68
  stop(taskName: string): Promise<{
66
69
  ok: boolean;
@@ -3,12 +3,13 @@ import * as os from 'os';
3
3
  import * as path from 'path';
4
4
  import { execFile } from 'child_process';
5
5
  import { promisify } from 'util';
6
- import { CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity } from './cdp.js';
6
+ import { BrowserCdpConnectionError, CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity, } from './cdp.js';
7
7
  import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, } from './profiles.js';
8
8
  import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
9
9
  import { connectLocal } from './drivers/local.js';
10
10
  import { connectSSH, shellQuote } from './drivers/ssh.js';
11
11
  import { clearProfileRuntime } from './runtime-state.js';
12
+ import { resolveDomainSkill } from './domain-skills.js';
12
13
  import { generateTaskId, generateShortId, generateTaskName, } from './types.js';
13
14
  import { getRefs, resolveRefToCoords } from './refs.js';
14
15
  import { clickAtCoords, hoverAtCoords, scrollAtCoords, typeText, pressKey, focusNode } from './input.js';
@@ -347,7 +348,16 @@ export class BrowserService {
347
348
  const result = await this.navigate(taskName, opts.url, effectiveProfileName);
348
349
  tabId = result.tabId;
349
350
  }
350
- return { task: taskId, name: taskName, tabId, profile: effectiveProfileName };
351
+ // Domain-skill discovery: when a URL is supplied, look up site-specific
352
+ // operating instructions and pass them back so the calling agent can pick
353
+ // them up alongside the task id. Failures swallowed by resolveDomainSkill.
354
+ let skill;
355
+ if (opts.url && !opts.skipDomainSkill) {
356
+ const resolved = resolveDomainSkill(opts.url);
357
+ if (resolved)
358
+ skill = resolved;
359
+ }
360
+ return { task: taskId, name: taskName, tabId, profile: effectiveProfileName, skill };
351
361
  }
352
362
  async stop(taskName) {
353
363
  for (const [profileName, conn] of this.connections) {
@@ -1425,7 +1435,7 @@ export class BrowserService {
1425
1435
  const existingInfo = getRunningChromeInfo(effectiveProfile.name);
1426
1436
  if (existingInfo) {
1427
1437
  try {
1428
- const { wsUrl, browser } = await discoverBrowserWsUrl(existingInfo.port);
1438
+ const { wsUrl, browser } = await discoverBrowserWsUrl(existingInfo.port, 'localhost', effectiveProfile.name);
1429
1439
  verifyBrowserIdentity(browser, effectiveProfile.browser, existingInfo.port);
1430
1440
  const cdp = new CDPClient();
1431
1441
  await cdp.connect(wsUrl);
@@ -1487,8 +1497,14 @@ export class BrowserService {
1487
1497
  };
1488
1498
  }
1489
1499
  if (url.protocol === 'wss:' || url.protocol === 'ws:') {
1500
+ const port = parseInt(url.port || (url.protocol === 'wss:' ? '443' : '80'), 10);
1490
1501
  const cdp = new CDPClient();
1491
- await cdp.connect(endpoint);
1502
+ try {
1503
+ await cdp.connect(endpoint);
1504
+ }
1505
+ catch {
1506
+ throw new BrowserCdpConnectionError(port, profile.name, url.hostname || 'localhost');
1507
+ }
1492
1508
  await this.enableDomains(cdp);
1493
1509
  return {
1494
1510
  cdp,
@@ -1502,7 +1518,7 @@ export class BrowserService {
1502
1518
  }
1503
1519
  if (url.protocol === 'http:' || url.protocol === 'https:') {
1504
1520
  const port = parseInt(url.port || (url.protocol === 'https:' ? '443' : '80'), 10);
1505
- const { wsUrl, browser } = await discoverBrowserWsUrl(port, url.hostname);
1521
+ const { wsUrl, browser } = await discoverBrowserWsUrl(port, url.hostname, profile.name);
1506
1522
  verifyBrowserIdentity(browser, profile.browser, port, url.hostname);
1507
1523
  const cdp = new CDPClient();
1508
1524
  await cdp.connect(wsUrl);
@@ -1685,6 +1701,25 @@ export class BrowserService {
1685
1701
  targetId: tabId,
1686
1702
  flatten: true,
1687
1703
  }));
1704
+ // Inject a one-shot stealth shim before any page script runs. Chromium
1705
+ // unconditionally exposes navigator.webdriver = true when a remote-debug
1706
+ // transport is attached; Cloudflare Turnstile, hCaptcha, and similar bot
1707
+ // checks read that property first. For browsers agents-cli spawns the
1708
+ // --disable-blink-features=AutomationControlled launch flag already
1709
+ // covers this, but for attach-to-running profiles (the Comet / Arc /
1710
+ // Brave case where the user launched the browser themselves) the flag
1711
+ // is unavailable — Page.addScriptToEvaluateOnNewDocument is the only
1712
+ // lever. Non-page targets (workers, service workers) will reject these
1713
+ // calls; we swallow the error and keep going.
1714
+ try {
1715
+ await conn.cdp.send('Page.enable', {}, sessionId);
1716
+ await conn.cdp.send('Page.addScriptToEvaluateOnNewDocument', {
1717
+ source: "Object.defineProperty(navigator,'webdriver',{get:()=>undefined});",
1718
+ }, sessionId);
1719
+ }
1720
+ catch {
1721
+ // Target doesn't support Page domain — nothing to inject.
1722
+ }
1688
1723
  conn.sessionCache.set(tabId, sessionId);
1689
1724
  return sessionId;
1690
1725
  }
@@ -1,15 +1,15 @@
1
1
  export type BrowserType = 'chrome' | 'comet' | 'chromium' | 'brave' | 'edge' | 'custom';
2
2
  /**
3
3
  * A single named endpoint preset within a profile. Lets one profile cover
4
- * the local + remote variants of the same app (e.g. Rush on this Mac vs.
5
- * Rush on mac-mini) instead of forcing two parallel profiles.
4
+ * the local + remote variants of the same app (e.g. an Electron app on this
5
+ * Mac vs. on a remote host) instead of forcing two parallel profiles.
6
6
  *
7
7
  * Per-endpoint overrides take precedence over profile-level fields.
8
8
  */
9
9
  export interface EndpointPreset {
10
10
  /** CDP URL — `cdp://host:port` or `ssh://host?port=N` */
11
11
  target: string;
12
- /** Override the profile-level binary (e.g. mac-mini has no local binary). */
12
+ /** Override the profile-level binary (e.g. a remote host has no local binary). */
13
13
  binary?: string;
14
14
  /** Override the profile-level targetFilter (Electron app builds may diverge). */
15
15
  targetFilter?: string;
@@ -43,7 +43,7 @@ export interface BrowserProfile {
43
43
  };
44
44
  /** Directory holding source-side JSONL logs (e.g. ~/.rush/logs). */
45
45
  logDir?: string;
46
- /** Optional SSH host where logDir lives, e.g. "user@mac-mini". */
46
+ /** Optional SSH host where logDir lives, e.g. "user@remote-host". */
47
47
  logHost?: string;
48
48
  }
49
49
  /** Parsed form of `BrowserProfile.targetFilter`. */
@@ -156,6 +156,7 @@ export interface IPCRequest {
156
156
  since?: string;
157
157
  until?: string;
158
158
  appLevel?: string;
159
+ skipDomainSkill?: boolean;
159
160
  }
160
161
  /** Subset of IPCResponse describing a recording start result. */
161
162
  export interface RecordStartFields {
@@ -200,6 +201,12 @@ export interface IPCResponse {
200
201
  uploadMode?: 'input' | 'drop' | 'chooser';
201
202
  appLogs?: any[];
202
203
  version?: string;
204
+ skill?: {
205
+ name: string;
206
+ path: string;
207
+ content: string;
208
+ hostname: string;
209
+ };
203
210
  }
204
211
  export interface ConsoleEntry {
205
212
  level: 'log' | 'info' | 'warn' | 'error';