@phnx-labs/agents-cli 1.19.1 → 1.20.0

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 (109) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +70 -10
  3. package/dist/commands/browser.js +88 -16
  4. package/dist/commands/cli.d.ts +14 -0
  5. package/dist/commands/cli.js +244 -0
  6. package/dist/commands/commands.js +3 -3
  7. package/dist/commands/computer.js +18 -1
  8. package/dist/commands/doctor.d.ts +1 -1
  9. package/dist/commands/doctor.js +2 -2
  10. package/dist/commands/exec.js +3 -3
  11. package/dist/commands/factory.d.ts +3 -14
  12. package/dist/commands/factory.js +3 -3
  13. package/dist/commands/hooks.js +3 -3
  14. package/dist/commands/mcp.js +29 -0
  15. package/dist/commands/plugins.js +11 -4
  16. package/dist/commands/profiles.js +1 -1
  17. package/dist/commands/prune.js +39 -160
  18. package/dist/commands/pull.js +56 -3
  19. package/dist/commands/routines.js +106 -13
  20. package/dist/commands/secrets.js +6 -8
  21. package/dist/commands/sessions.d.ts +36 -7
  22. package/dist/commands/sessions.js +130 -53
  23. package/dist/commands/setup.d.ts +1 -0
  24. package/dist/commands/setup.js +37 -28
  25. package/dist/commands/skills.js +3 -3
  26. package/dist/commands/teams.js +13 -0
  27. package/dist/commands/versions.d.ts +4 -3
  28. package/dist/commands/versions.js +147 -124
  29. package/dist/commands/view.js +12 -12
  30. package/dist/index.js +34 -6
  31. package/dist/lib/acp/harnesses.js +8 -0
  32. package/dist/lib/agents.js +162 -9
  33. package/dist/lib/browser/cdp.d.ts +8 -1
  34. package/dist/lib/browser/cdp.js +40 -3
  35. package/dist/lib/browser/chrome.d.ts +13 -0
  36. package/dist/lib/browser/chrome.js +42 -3
  37. package/dist/lib/browser/domain-skills.d.ts +51 -0
  38. package/dist/lib/browser/domain-skills.js +157 -0
  39. package/dist/lib/browser/drivers/local.js +45 -4
  40. package/dist/lib/browser/drivers/ssh.js +1 -1
  41. package/dist/lib/browser/ipc.d.ts +8 -1
  42. package/dist/lib/browser/ipc.js +37 -28
  43. package/dist/lib/browser/profiles.d.ts +13 -0
  44. package/dist/lib/browser/profiles.js +41 -1
  45. package/dist/lib/browser/service.d.ts +3 -0
  46. package/dist/lib/browser/service.js +21 -5
  47. package/dist/lib/browser/types.d.ts +7 -0
  48. package/dist/lib/cli-resources.d.ts +109 -0
  49. package/dist/lib/cli-resources.js +255 -0
  50. package/dist/lib/cloud/rush.js +5 -5
  51. package/dist/lib/command-skills.js +0 -2
  52. package/dist/lib/computer-rpc.d.ts +3 -0
  53. package/dist/lib/computer-rpc.js +53 -0
  54. package/dist/lib/daemon.js +20 -0
  55. package/dist/lib/exec.d.ts +3 -2
  56. package/dist/lib/exec.js +62 -6
  57. package/dist/lib/hooks.js +182 -0
  58. package/dist/lib/mcp.js +6 -0
  59. package/dist/lib/migrate.js +1 -1
  60. package/dist/lib/overdue.d.ts +26 -0
  61. package/dist/lib/overdue.js +101 -0
  62. package/dist/lib/permissions.js +5 -1
  63. package/dist/lib/plugin-marketplace.js +1 -1
  64. package/dist/lib/profiles-presets.js +37 -0
  65. package/dist/lib/registry.d.ts +18 -0
  66. package/dist/lib/registry.js +44 -0
  67. package/dist/lib/resources/mcp.js +43 -1
  68. package/dist/lib/resources/types.d.ts +1 -1
  69. package/dist/lib/resources.d.ts +1 -1
  70. package/dist/lib/rotate.js +10 -4
  71. package/dist/lib/routines-format.d.ts +35 -0
  72. package/dist/lib/routines-format.js +173 -0
  73. package/dist/lib/routines.d.ts +7 -1
  74. package/dist/lib/routines.js +32 -12
  75. package/dist/lib/runner.js +19 -5
  76. package/dist/lib/scheduler.js +8 -1
  77. package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
  78. package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
  79. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  80. package/dist/lib/secrets/bundles.d.ts +33 -2
  81. package/dist/lib/secrets/bundles.js +249 -26
  82. package/dist/lib/secrets/index.d.ts +10 -1
  83. package/dist/lib/secrets/index.js +143 -48
  84. package/dist/lib/session/active.d.ts +8 -0
  85. package/dist/lib/session/active.js +3 -2
  86. package/dist/lib/session/db.d.ts +10 -4
  87. package/dist/lib/session/db.js +16 -16
  88. package/dist/lib/session/parse.d.ts +1 -0
  89. package/dist/lib/session/parse.js +44 -0
  90. package/dist/lib/session/types.d.ts +1 -1
  91. package/dist/lib/session/types.js +1 -1
  92. package/dist/lib/shims.d.ts +6 -2
  93. package/dist/lib/shims.js +88 -10
  94. package/dist/lib/state.d.ts +0 -1
  95. package/dist/lib/state.js +2 -15
  96. package/dist/lib/teams/agents.js +1 -1
  97. package/dist/lib/teams/parsers.d.ts +1 -1
  98. package/dist/lib/teams/parsers.js +153 -3
  99. package/dist/lib/teams/summarizer.js +18 -2
  100. package/dist/lib/teams/worktree.js +14 -3
  101. package/dist/lib/types.d.ts +7 -4
  102. package/dist/lib/types.js +6 -3
  103. package/dist/lib/versions.d.ts +10 -2
  104. package/dist/lib/versions.js +227 -35
  105. package/package.json +9 -9
  106. package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
  107. package/npm-shrinkwrap.json +0 -3162
  108. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
  109. /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/embedded.provisionprofile +0 -0
@@ -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
  }
@@ -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)
@@ -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)
@@ -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);
@@ -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';
@@ -0,0 +1,109 @@
1
+ /** A single install method. Exactly one of the keys (npm/brew/script/binary) is set. */
2
+ export type InstallMethod = {
3
+ npm: string;
4
+ } | {
5
+ brew: string;
6
+ } | {
7
+ script: string;
8
+ } | {
9
+ binary: BinarySpec;
10
+ };
11
+ /** Per-platform binary download spec. Keys are `<os>-<arch>` (e.g. darwin-arm64). */
12
+ export interface BinarySpec {
13
+ [platform: string]: {
14
+ url: string;
15
+ /** Path inside the archive (relative). Required when url is a .tar.gz/.zip. */
16
+ extract?: string;
17
+ };
18
+ }
19
+ /** Parsed CLI manifest. */
20
+ export interface CliManifest {
21
+ /** Name as it appears on the command line (e.g. "higgsfield"). */
22
+ name: string;
23
+ /** One-line summary shown in `agents cli list`. */
24
+ description?: string;
25
+ /** Project homepage; used in detail view + post-install messaging. */
26
+ homepage?: string;
27
+ /** Command run to verify the binary is installed (default: "<name> --version"). */
28
+ check: string;
29
+ /** Install methods tried in order; first one whose tool is available is used. */
30
+ install: InstallMethod[];
31
+ /** Message printed after successful install — typically auth instructions. */
32
+ postInstall?: string;
33
+ /** Origin layer this manifest was resolved from. */
34
+ source: string;
35
+ /** Absolute path to the yaml file. */
36
+ path: string;
37
+ }
38
+ /** A validation problem in a CLI manifest. */
39
+ export interface CliManifestError {
40
+ /** Filename that failed to parse. */
41
+ file: string;
42
+ /** Human-readable reason. */
43
+ reason: string;
44
+ }
45
+ /**
46
+ * Parse a single CLI manifest from its YAML contents.
47
+ * Returns a manifest on success; throws on schema violations so callers can
48
+ * decide whether to surface or swallow the error per file.
49
+ */
50
+ export declare function parseCliManifest(contents: string, opts: {
51
+ name: string;
52
+ source: string;
53
+ path: string;
54
+ }): CliManifest;
55
+ /**
56
+ * Discover all CLI manifests resolvable from the current cwd. Returns valid
57
+ * manifests and any parse errors separately so the CLI can show both.
58
+ */
59
+ export declare function listCliManifests(cwd?: string): {
60
+ manifests: CliManifest[];
61
+ errors: CliManifestError[];
62
+ };
63
+ /** Resolve a single CLI manifest by name. Returns null when not declared. */
64
+ export declare function resolveCliManifest(name: string, cwd?: string): CliManifest | null;
65
+ export declare function hasCommand(cmd: string): boolean;
66
+ /** Run the manifest's `check` command. Returns true when it exits 0. */
67
+ export declare function isCliInstalled(manifest: CliManifest): boolean;
68
+ /**
69
+ * Pick the first install method whose required host tool is available.
70
+ * Returns null when none of the declared methods can run on this host.
71
+ */
72
+ export declare function selectInstallMethod(manifest: CliManifest): InstallMethod | null;
73
+ /** Short description of a method for display. */
74
+ export declare function describeMethod(method: InstallMethod): string;
75
+ export interface InstallResult {
76
+ manifest: CliManifest;
77
+ /** Method that was attempted (null if no compatible method existed). */
78
+ method: InstallMethod | null;
79
+ /** True when the post-install `check` passed. */
80
+ installed: boolean;
81
+ /** stdout/stderr captured from the install command, for surfacing on failure. */
82
+ output?: string;
83
+ /** Set when the install runner threw or exited non-zero. */
84
+ error?: string;
85
+ }
86
+ /**
87
+ * Install a single CLI by running its first compatible method. Streams the
88
+ * underlying command's output to the parent terminal so users see brew/npm
89
+ * progress live. Verifies success by re-running `check`.
90
+ */
91
+ export declare function installCli(manifest: CliManifest, opts?: {
92
+ dryRun?: boolean;
93
+ }): InstallResult;
94
+ /**
95
+ * Map a declarative method to a shell command. Centralized so tests and dry-run
96
+ * surface the exact string that would execute.
97
+ */
98
+ export declare function buildInstallCommand(method: InstallMethod): string;
99
+ export interface CliStatus {
100
+ manifest: CliManifest;
101
+ installed: boolean;
102
+ }
103
+ /** Convenience: list all manifests + their installed-on-host status. */
104
+ export declare function listCliStatus(cwd?: string): {
105
+ statuses: CliStatus[];
106
+ errors: CliManifestError[];
107
+ };
108
+ /** Names of CLIs that are declared but not currently installed on the host. */
109
+ export declare function getMissingClis(cwd?: string): CliManifest[];