@phnx-labs/agents-cli 1.19.2 → 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 (103) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +69 -9
  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/commands.js +3 -3
  8. package/dist/commands/computer.js +18 -1
  9. package/dist/commands/doctor.d.ts +1 -1
  10. package/dist/commands/doctor.js +2 -2
  11. package/dist/commands/exec.js +3 -3
  12. package/dist/commands/factory.d.ts +3 -14
  13. package/dist/commands/factory.js +3 -3
  14. package/dist/commands/hooks.js +3 -3
  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 +5 -7
  21. package/dist/commands/sessions.d.ts +28 -0
  22. package/dist/commands/sessions.js +98 -33
  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 +131 -127
  29. package/dist/commands/view.js +12 -12
  30. package/dist/computer.js +0 -0
  31. package/dist/index.js +34 -6
  32. package/dist/lib/acp/harnesses.js +8 -0
  33. package/dist/lib/agents.js +110 -23
  34. package/dist/lib/browser/cdp.d.ts +8 -1
  35. package/dist/lib/browser/cdp.js +40 -3
  36. package/dist/lib/browser/chrome.d.ts +13 -0
  37. package/dist/lib/browser/chrome.js +42 -3
  38. package/dist/lib/browser/domain-skills.d.ts +51 -0
  39. package/dist/lib/browser/domain-skills.js +157 -0
  40. package/dist/lib/browser/drivers/local.js +45 -4
  41. package/dist/lib/browser/drivers/ssh.js +1 -1
  42. package/dist/lib/browser/ipc.d.ts +8 -1
  43. package/dist/lib/browser/ipc.js +37 -28
  44. package/dist/lib/browser/profiles.d.ts +13 -0
  45. package/dist/lib/browser/profiles.js +41 -1
  46. package/dist/lib/browser/service.d.ts +3 -0
  47. package/dist/lib/browser/service.js +21 -5
  48. package/dist/lib/browser/types.d.ts +7 -0
  49. package/dist/lib/cli-resources.d.ts +109 -0
  50. package/dist/lib/cli-resources.js +255 -0
  51. package/dist/lib/cloud/rush.js +5 -5
  52. package/dist/lib/command-skills.js +0 -2
  53. package/dist/lib/computer-rpc.d.ts +3 -0
  54. package/dist/lib/computer-rpc.js +53 -0
  55. package/dist/lib/daemon.js +20 -0
  56. package/dist/lib/exec.d.ts +3 -2
  57. package/dist/lib/exec.js +44 -9
  58. package/dist/lib/hooks.js +182 -0
  59. package/dist/lib/mcp.js +6 -0
  60. package/dist/lib/migrate.js +1 -1
  61. package/dist/lib/overdue.d.ts +26 -0
  62. package/dist/lib/overdue.js +101 -0
  63. package/dist/lib/permissions.js +5 -1
  64. package/dist/lib/plugin-marketplace.js +1 -1
  65. package/dist/lib/profiles-presets.js +37 -0
  66. package/dist/lib/resources/mcp.js +37 -0
  67. package/dist/lib/resources.d.ts +1 -1
  68. package/dist/lib/rotate.js +10 -4
  69. package/dist/lib/routines-format.d.ts +35 -0
  70. package/dist/lib/routines-format.js +173 -0
  71. package/dist/lib/routines.d.ts +7 -1
  72. package/dist/lib/routines.js +32 -12
  73. package/dist/lib/runner.js +19 -5
  74. package/dist/lib/scheduler.js +8 -1
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/bundles.d.ts +22 -1
  78. package/dist/lib/secrets/bundles.js +234 -36
  79. package/dist/lib/secrets/index.d.ts +6 -11
  80. package/dist/lib/secrets/index.js +107 -87
  81. package/dist/lib/session/active.d.ts +8 -0
  82. package/dist/lib/session/active.js +3 -2
  83. package/dist/lib/session/db.d.ts +0 -4
  84. package/dist/lib/session/db.js +0 -26
  85. package/dist/lib/session/parse.d.ts +1 -0
  86. package/dist/lib/session/parse.js +44 -0
  87. package/dist/lib/session/types.d.ts +1 -1
  88. package/dist/lib/session/types.js +1 -1
  89. package/dist/lib/shims.d.ts +1 -1
  90. package/dist/lib/shims.js +66 -4
  91. package/dist/lib/state.d.ts +0 -1
  92. package/dist/lib/state.js +2 -15
  93. package/dist/lib/teams/agents.js +1 -1
  94. package/dist/lib/teams/parsers.d.ts +1 -1
  95. package/dist/lib/teams/parsers.js +153 -3
  96. package/dist/lib/teams/summarizer.js +18 -2
  97. package/dist/lib/teams/worktree.js +14 -3
  98. package/dist/lib/types.d.ts +6 -3
  99. package/dist/lib/types.js +6 -3
  100. package/dist/lib/versions.d.ts +10 -2
  101. package/dist/lib/versions.js +227 -35
  102. package/package.json +7 -7
  103. package/npm-shrinkwrap.json +0 -3162
@@ -2,7 +2,7 @@
2
2
  * Core agent configuration and detection module.
3
3
  *
4
4
  * Defines the canonical registry of all supported AI coding agents (Claude, Codex,
5
- * Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose, Roo) with their
5
+ * Gemini, Cursor, OpenCode, OpenClaw, Copilot, Amp, Kiro, Goose, Roo, Grok) with their
6
6
  * CLI commands, config paths, capability flags, and MCP integration points.
7
7
  *
8
8
  * Provides functions for detecting installed CLIs, resolving version-managed binaries,
@@ -18,7 +18,6 @@ import chalk from 'chalk';
18
18
  import { walkForFiles } from './fs-walk.js';
19
19
  import { getVersionsDir, getShimsDir, getCliVersionCachePath } from './state.js';
20
20
  import { resolveVersion, getVersionHomePath, getBinaryPath } from './versions.js';
21
- import { loadClaudeOauth } from './usage.js';
22
21
  const execFileAsync = promisify(execFile);
23
22
  const HOME = os.homedir();
24
23
  /**
@@ -77,6 +76,43 @@ function findInPath(command) {
77
76
  }
78
77
  return null;
79
78
  }
79
+ /** Grok-specific binary resolution.
80
+ * Grok does not live in node_modules/.bin. Its versioned binaries live in
81
+ * ~/.grok/downloads/ with names like `grok-0.1.218-macos-aarch64`.
82
+ * We still use the agents-cli version dir for *config isolation* via GROK_HOME.
83
+ */
84
+ function resolveGrokBinary(version) {
85
+ const grokDownloads = path.join(HOME, '.grok', 'downloads');
86
+ if (!fs.existsSync(grokDownloads))
87
+ return null;
88
+ const entries = fs.readdirSync(grokDownloads);
89
+ // Prefer exact version match in filename
90
+ if (version && version !== 'latest') {
91
+ const match = entries.find((e) => e.includes(version) && e.startsWith('grok-'));
92
+ if (match)
93
+ return path.join(grokDownloads, match);
94
+ }
95
+ // Fallback: the "current" symlink or the plain `grok-*` without version in name
96
+ const current = entries.find((e) => e === 'grok' || e.startsWith('grok-') && !e.match(/grok-\d/));
97
+ if (current)
98
+ return path.join(grokDownloads, current);
99
+ // Last resort: newest file by mtime
100
+ let latest = null;
101
+ let latestMtime = 0;
102
+ for (const e of entries) {
103
+ if (!e.startsWith('grok-'))
104
+ continue;
105
+ try {
106
+ const stat = fs.statSync(path.join(grokDownloads, e));
107
+ if (stat.mtimeMs > latestMtime) {
108
+ latestMtime = stat.mtimeMs;
109
+ latest = e;
110
+ }
111
+ }
112
+ catch { }
113
+ }
114
+ return latest ? path.join(grokDownloads, latest) : null;
115
+ }
80
116
  function splitCommandLine(command) {
81
117
  const args = [];
82
118
  let current = '';
@@ -339,6 +375,11 @@ export const AGENTS = {
339
375
  // configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
340
376
  // CLI but isolates its own state in the `antigravity-cli/` subdir. Per-version HOME
341
377
  // isolation works because the shim's configDirName carries the full nested path.
378
+ // Auth: Google OAuth on first launch, or ANTIGRAVITY_API_KEY env var for headless.
379
+ // Hooks: JSON entries under a top-level `hooks` key in settings.json; events are
380
+ // before_tool_call, after_model_call, on_loop_stop, on_error. Plugins are the
381
+ // renamed Gemini CLI extensions. Permissions live in settings.json under a
382
+ // `permissions` key with allow/deny arrays.
342
383
  antigravity: {
343
384
  id: 'antigravity',
344
385
  name: 'Antigravity',
@@ -354,30 +395,40 @@ export const AGENTS = {
354
395
  instructionsFile: 'AGENTS.md',
355
396
  format: 'markdown',
356
397
  variableSyntax: '{{args}}',
357
- supportsHooks: false,
358
- nativeAgentsSkillsDir: true,
359
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false, rulesImports: false },
398
+ supportsHooks: true,
399
+ capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, rulesImports: false },
360
400
  },
361
401
  // xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
362
- // first launch, or GROK_CODE_XAI_API_KEY env var for headless. MCP supported
363
- // out of the box; exact config file path verified at first install.
402
+ // first launch, or XAI_API_KEY env var for headless. MCP servers configured inline
403
+ // under [mcp_servers] in ~/.grok/config.toml. Hooks auto-discovered from
404
+ // ~/.grok/hooks/ (+ project .grok/hooks/) — events PreToolUse, PostToolUse, etc.
405
+ // Plugins live in ~/.grok/plugins/ with marketplaces. Permissions: --allow/--deny
406
+ // CLI flags or [permission] TOML block in ~/.grok/config.toml.
364
407
  grok: {
365
408
  id: 'grok',
366
409
  name: 'Grok',
367
- color: 'whiteBright',
410
+ color: 'cyanBright',
368
411
  cliCommand: 'grok',
369
412
  npmPackage: '',
370
- installScript: 'curl -fsSL https://x.ai/cli/install.sh | bash',
413
+ installScript: 'curl -fsSL https://x.ai/cli/install.sh | bash -s VERSION',
371
414
  configDir: path.join(HOME, '.grok'),
372
- commandsDir: path.join(HOME, '.grok', 'commands'),
373
- commandsSubdir: 'commands',
415
+ commandsDir: '', // Grok primarily uses skills + slash commands from skills
416
+ commandsSubdir: '',
374
417
  skillsDir: path.join(HOME, '.grok', 'skills'),
375
- hooksDir: 'hooks',
418
+ hooksDir: path.join(HOME, '.grok', 'hooks'),
376
419
  instructionsFile: 'AGENTS.md',
377
420
  format: 'markdown',
378
421
  variableSyntax: '$ARGUMENTS',
379
- supportsHooks: false,
380
- capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false },
422
+ supportsHooks: true,
423
+ capabilities: {
424
+ hooks: true,
425
+ mcp: true,
426
+ allowlist: true, // maps to Grok's granular Bash/Edit/Write/Read/Grep/WebFetch/MCPTool rules
427
+ skills: true,
428
+ commands: false, // covered by skills
429
+ plugins: true,
430
+ rulesImports: true,
431
+ },
381
432
  },
382
433
  };
383
434
  /** All registered agent IDs derived from the AGENTS registry. */
@@ -499,6 +550,18 @@ export async function getCliState(agentId) {
499
550
  }
500
551
  }
501
552
  // Non-version-managed: single PATH lookup + cached version read
553
+ // Special case for grok: it manages its own binaries in ~/.grok/downloads/
554
+ if (agentId === 'grok') {
555
+ const grokBin = resolveGrokBinary();
556
+ if (!grokBin) {
557
+ return { installed: false, version: null, path: null };
558
+ }
559
+ return {
560
+ installed: true,
561
+ version: await getCachedVersionForBinary(agentId, grokBin),
562
+ path: grokBin,
563
+ };
564
+ }
502
565
  const binaryPath = findInPath(agent.cliCommand);
503
566
  if (!binaryPath) {
504
567
  return { installed: false, version: null, path: null };
@@ -532,7 +595,7 @@ export function isConfigured(agentId) {
532
595
  */
533
596
  export async function getUnmanagedAgentInstalls() {
534
597
  const unmanaged = [];
535
- const candidates = ['claude', 'codex', 'gemini'];
598
+ const candidates = ['claude', 'codex', 'gemini', 'grok'];
536
599
  for (const agentId of candidates) {
537
600
  const agent = AGENTS[agentId];
538
601
  try {
@@ -611,13 +674,15 @@ export async function getAccountInfo(agentId, home) {
611
674
  ['org', organizationId],
612
675
  ]);
613
676
  const usageKey = buildIdentityKey(agentId, [['org', organizationId]]);
677
+ // Plan is derived from .claude.json's billingType only. Reading
678
+ // subscriptionType from the keychain item ("Claude Code-credentials-<hash>")
679
+ // forces a macOS Keychain ACL prompt on every `agents run` (one prompt per
680
+ // installed version under balanced rotation) because Claude Code writes its
681
+ // credentials with its own process in the ACL — our helper isn't trusted by
682
+ // that item. Callers that genuinely need subscriptionType (e.g. detailed
683
+ // `agents view`) should call loadClaudeOauth() directly.
614
684
  let plan = null;
615
- const keychainOauth = await loadClaudeOauth(home);
616
- if (keychainOauth?.subscriptionType) {
617
- plan = keychainOauth.subscriptionType.charAt(0).toUpperCase()
618
- + keychainOauth.subscriptionType.slice(1);
619
- }
620
- else if (oa?.billingType === 'stripe_subscription') {
685
+ if (oa?.billingType === 'stripe_subscription') {
621
686
  plan = 'Pro';
622
687
  }
623
688
  else if (oa?.billingType) {
@@ -698,6 +763,19 @@ export async function getAccountInfo(agentId, home) {
698
763
  const data = JSON.parse(await fs.promises.readFile(path.join(base, '.gemini', 'google_accounts.json'), 'utf-8'));
699
764
  return { ...empty, email: data.active || null, lastActive };
700
765
  }
766
+ case 'grok': {
767
+ // Grok stores auth in ~/.grok/auth.json
768
+ try {
769
+ const authPath = path.join(base, '.grok', 'auth.json');
770
+ if (fs.existsSync(authPath)) {
771
+ const data = JSON.parse(await fs.promises.readFile(authPath, 'utf-8'));
772
+ const email = data.email || data.user?.email || data.account?.email || null;
773
+ return { ...empty, email, lastActive };
774
+ }
775
+ }
776
+ catch { }
777
+ return { ...empty, lastActive };
778
+ }
701
779
  default:
702
780
  return { ...empty, lastActive };
703
781
  }
@@ -735,6 +813,12 @@ function getSessionDir(agentId, base) {
735
813
  return path.join(base, '.codex', 'sessions');
736
814
  case 'gemini':
737
815
  return path.join(base, '.gemini', 'tmp');
816
+ case 'grok':
817
+ return path.join(base, '.grok', 'sessions');
818
+ case 'copilot':
819
+ // Copilot persists sessions at ~/.copilot/session-state/<id>/events.jsonl.
820
+ // The events.jsonl is the canonical NDJSON event stream per session.
821
+ return path.join(base, '.copilot', 'session-state');
738
822
  default:
739
823
  return null;
740
824
  }
@@ -744,9 +828,12 @@ function getSessionExtension(agentId) {
744
828
  switch (agentId) {
745
829
  case 'claude':
746
830
  case 'codex':
831
+ case 'copilot':
747
832
  return '.jsonl';
748
833
  case 'gemini':
749
834
  return '.json';
835
+ case 'grok':
836
+ return '.json'; // sessions contain summary.json, events.jsonl, etc.
750
837
  default:
751
838
  return null;
752
839
  }
@@ -1167,7 +1254,7 @@ export function getMcpConfigPathForHome(agentId, home) {
1167
1254
  case 'antigravity':
1168
1255
  return path.join(home, '.gemini', 'antigravity-cli', 'mcp_config.json');
1169
1256
  case 'grok':
1170
- return path.join(home, '.grok', 'mcp.json');
1257
+ return path.join(home, '.grok', 'config.toml');
1171
1258
  default:
1172
1259
  return path.join(home, `.${agentId}`, 'settings.json');
1173
1260
  }
@@ -1203,7 +1290,7 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
1203
1290
  case 'antigravity':
1204
1291
  return path.join(cwd, '.gemini', 'antigravity-cli', 'mcp_config.json');
1205
1292
  case 'grok':
1206
- return path.join(cwd, '.grok', 'mcp.json');
1293
+ return path.join(cwd, '.grok', 'config.toml');
1207
1294
  default:
1208
1295
  return path.join(cwd, `.${agentId}`, 'settings.json');
1209
1296
  }
@@ -32,7 +32,14 @@ export interface BrowserDiscovery {
32
32
  wsUrl: string;
33
33
  browser: string;
34
34
  }
35
- export declare function discoverBrowserWsUrl(port: number, host?: string): Promise<BrowserDiscovery>;
35
+ export declare class BrowserCdpConnectionError extends Error {
36
+ readonly port: number;
37
+ readonly profileName: string;
38
+ readonly host: string;
39
+ constructor(port: number, profileName?: string, host?: string);
40
+ }
41
+ export declare function formatBrowserCdpConnectionError(port: number, profileName?: string, host?: string): string;
42
+ export declare function discoverBrowserWsUrl(port: number, host?: string, profileName?: string): Promise<BrowserDiscovery>;
36
43
  export declare function normalizeBrowserName(s: string): string;
37
44
  export declare function verifyBrowserIdentity(reported: string, expected: string, port: number, host?: string): void;
38
45
  export declare function listTargets(port: number, host?: string): Promise<Array<{
@@ -152,10 +152,47 @@ export class CDPClient {
152
152
  this.transport = null;
153
153
  }
154
154
  }
155
- export async function discoverBrowserWsUrl(port, host = 'localhost') {
156
- const response = await fetch(`http://${host}:${port}/json/version`);
155
+ export class BrowserCdpConnectionError extends Error {
156
+ port;
157
+ profileName;
158
+ host;
159
+ constructor(port, profileName = '<name>', host = 'localhost') {
160
+ super(formatBrowserCdpConnectionError(port, profileName, host));
161
+ this.port = port;
162
+ this.profileName = profileName;
163
+ this.host = host;
164
+ this.name = 'BrowserCdpConnectionError';
165
+ }
166
+ }
167
+ export function formatBrowserCdpConnectionError(port, profileName = '<name>', host = 'localhost') {
168
+ const target = host === 'localhost' || host === '127.0.0.1'
169
+ ? `port ${port}`
170
+ : `${host}:${port}`;
171
+ return [
172
+ `Could not connect to Chrome on ${target}.`,
173
+ `- Is Chrome running with --remote-debugging-port=${port}?`,
174
+ `- Try: agents browser start --profile ${profileName}`,
175
+ ].join('\n');
176
+ }
177
+ export async function discoverBrowserWsUrl(port, host = 'localhost', profileName = '<name>') {
178
+ // Node's fetch has no default timeout — a port that ACKs the TCP connect
179
+ // but never sends an HTTP response will hang here indefinitely. Bound the
180
+ // discovery probe so the caller can surface an actionable error in seconds,
181
+ // not minutes.
182
+ const controller = new AbortController();
183
+ const timer = setTimeout(() => controller.abort(), 3000);
184
+ let response;
185
+ try {
186
+ response = await fetch(`http://${host}:${port}/json/version`, { signal: controller.signal });
187
+ }
188
+ catch {
189
+ throw new BrowserCdpConnectionError(port, profileName, host);
190
+ }
191
+ finally {
192
+ clearTimeout(timer);
193
+ }
157
194
  if (!response.ok) {
158
- throw new Error(`Failed to discover browser: ${response.status}`);
195
+ throw new BrowserCdpConnectionError(port, profileName, host);
159
196
  }
160
197
  const data = (await response.json());
161
198
  const browserField = data.Browser || data.Product || '';
@@ -1,6 +1,19 @@
1
1
  import type { ChromeOptions } from './types.js';
2
2
  import type { BrowserType } from './types.js';
3
3
  export declare function findBrowserPath(browserType: BrowserType, customBinary?: string): string;
4
+ /**
5
+ * Walk the per-platform priority list and return the first browser that's
6
+ * actually installed on disk. Returns null if none of them are present.
7
+ *
8
+ * This is the auto-pick the `agents browser start` command uses when the user
9
+ * doesn't pass `--profile`. The intent matches "use whatever's preinstalled,"
10
+ * but constrained to Chromium-family binaries so CDP works without a new
11
+ * driver layer.
12
+ */
13
+ export declare function findFirstInstalledBrowser(platform?: string): {
14
+ browserType: BrowserType;
15
+ binary: string;
16
+ } | null;
4
17
  export interface LaunchResult {
5
18
  pid: number;
6
19
  port: number;
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import * as os from 'os';
5
5
  import { getProfileRuntimeDir } from './profiles.js';
6
6
  import { discoverBrowserWsUrl, registerPipeTransport } from './cdp.js';
7
- import { readBundle, resolveBundleEnv, bundleExists } from '../secrets/bundles.js';
7
+ import { readAndResolveBundleEnv, bundleExists } from '../secrets/bundles.js';
8
8
  import { writeProfileRuntime, readProfileRuntime } from './runtime-state.js';
9
9
  const BROWSER_PATHS = {
10
10
  darwin: {
@@ -65,6 +65,45 @@ export function findBrowserPath(browserType, customBinary) {
65
65
  }
66
66
  throw new Error(`Browser "${browserType}" not found. Install it first.`);
67
67
  }
68
+ // Per-platform Chromium-family priority list for "no --profile" auto-pick.
69
+ // Order is: most-likely-installed-and-stable first. Safari and Firefox are
70
+ // intentionally excluded — they don't speak the Chrome DevTools Protocol the
71
+ // way cdp.ts expects, so they'd need separate drivers.
72
+ const DEFAULT_BROWSER_PRIORITY = {
73
+ // macOS: Chrome leads (>70% of dev machines), then the rest of the family.
74
+ darwin: ['chrome', 'brave', 'edge', 'chromium', 'comet'],
75
+ // Linux: Chrome/Chromium first (apt/snap), then Brave/Edge if present.
76
+ linux: ['chrome', 'chromium', 'brave', 'edge'],
77
+ // Windows: Edge is preinstalled on every supported build, so it's the
78
+ // reliable always-there default.
79
+ win32: ['edge', 'chrome', 'brave'],
80
+ };
81
+ /**
82
+ * Walk the per-platform priority list and return the first browser that's
83
+ * actually installed on disk. Returns null if none of them are present.
84
+ *
85
+ * This is the auto-pick the `agents browser start` command uses when the user
86
+ * doesn't pass `--profile`. The intent matches "use whatever's preinstalled,"
87
+ * but constrained to Chromium-family binaries so CDP works without a new
88
+ * driver layer.
89
+ */
90
+ export function findFirstInstalledBrowser(platform = os.platform()) {
91
+ const priority = DEFAULT_BROWSER_PRIORITY[platform];
92
+ if (!priority)
93
+ return null;
94
+ const platformPaths = BROWSER_PATHS[platform];
95
+ if (!platformPaths)
96
+ return null;
97
+ for (const browserType of priority) {
98
+ const candidates = platformPaths[browserType] || [];
99
+ for (const p of candidates) {
100
+ if (fs.existsSync(p)) {
101
+ return { browserType, binary: p };
102
+ }
103
+ }
104
+ }
105
+ return null;
106
+ }
68
107
  export async function launchBrowser(profileName, browserType, port, options = {}, secrets, customBinary,
69
108
  // `electron: true` distinguishes Notion / VS Code-style apps from
70
109
  // regular Chrome — purely informational, stored in meta.json so the
@@ -99,6 +138,7 @@ isElectron = false) {
99
138
  '--no-first-run',
100
139
  '--no-default-browser-check',
101
140
  '--disable-features=DefaultBrowserSetting,ChromeWhatsNewUI',
141
+ '--disable-crash-reporter',
102
142
  ...(options.headless ? ['--headless=new'] : []),
103
143
  `--window-size=${viewport.width},${viewport.height}`,
104
144
  ...(viewport.x !== undefined && viewport.y !== undefined
@@ -109,8 +149,7 @@ isElectron = false) {
109
149
  let env = { ...process.env };
110
150
  if (secrets && bundleExists(secrets)) {
111
151
  try {
112
- const bundle = readBundle(secrets);
113
- const bundleEnv = resolveBundleEnv(bundle, { caller: 'browser profile' });
152
+ const { env: bundleEnv } = readAndResolveBundleEnv(secrets, { caller: 'browser profile' });
114
153
  env = { ...env, ...bundleEnv };
115
154
  }
116
155
  catch {
@@ -0,0 +1,51 @@
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
+ /** Result of resolving a URL to a domain-skill. */
17
+ export interface ResolvedDomainSkill {
18
+ /** Skill identifier — the directory name under domain-skills/. */
19
+ name: string;
20
+ /** Absolute path to the SKILL.md that was matched. */
21
+ path: string;
22
+ /** Full SKILL.md contents (frontmatter included). */
23
+ content: string;
24
+ /** Hostname the match was made against (post-www strip). */
25
+ hostname: string;
26
+ }
27
+ /** Where domain-skills live. Override via $AGENTS_BROWSER_DOMAIN_SKILLS_DIR for tests. */
28
+ export declare function domainSkillsRoot(): string;
29
+ /**
30
+ * Derive match candidates from a hostname. Order matters — earlier candidates
31
+ * are tried first.
32
+ *
33
+ * Examples:
34
+ * perplexity.ai -> ['perplexity.ai', 'perplexity']
35
+ * app.slack.com -> ['app.slack.com', 'slack.com', 'slack']
36
+ * mail.google.com -> ['mail.google.com', 'google.com', 'google', 'mail']
37
+ * higgsfield.ai -> ['higgsfield.ai', 'higgsfield']
38
+ */
39
+ export declare function hostnameMatchCandidates(hostname: string): string[];
40
+ /**
41
+ * Resolve a URL to its matching domain-skill, or null if none.
42
+ *
43
+ * Two-pass strategy:
44
+ * 1. Index every SKILL.md in the root and read its `domains:` frontmatter.
45
+ * If any pinned domain matches a candidate, return that skill.
46
+ * 2. Fall back to directory-name match against the candidate list.
47
+ *
48
+ * Errors (missing root, unreadable file, invalid URL) are swallowed and
49
+ * yield null — domain-skill discovery must never break browser start.
50
+ */
51
+ export declare function resolveDomainSkill(url: string): ResolvedDomainSkill | null;
@@ -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
+ }