@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
@@ -50,6 +50,14 @@ export const ACP_HARNESSES = {
50
50
  confidence: 'documented',
51
51
  source: 'https://docs.openclaw.ai/tools/acp-agents',
52
52
  },
53
+ grok: {
54
+ command: 'grok',
55
+ args: ['agent', 'stdio'],
56
+ installHint: 'see https://docs.x.ai/build/cli',
57
+ confidence: 'documented',
58
+ source: 'https://docs.x.ai/build/cli/headless-scripting',
59
+ },
60
+ // antigravity: no documented ACP support (May 2026).
53
61
  // goose: ACP over HTTP via `goosed`, not a clean stdio subcommand.
54
62
  // copilot, kiro: excluded for now (not installed in the reference environment,
55
63
  // no local verification possible).
@@ -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 = '';
@@ -335,6 +371,65 @@ export const AGENTS = {
335
371
  supportsHooks: false,
336
372
  capabilities: { hooks: false, mcp: true, allowlist: false, skills: true, commands: true, plugins: false },
337
373
  },
374
+ // Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
375
+ // configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
376
+ // CLI but isolates its own state in the `antigravity-cli/` subdir. Per-version HOME
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.
383
+ antigravity: {
384
+ id: 'antigravity',
385
+ name: 'Antigravity',
386
+ color: 'blueBright',
387
+ cliCommand: 'agy',
388
+ npmPackage: '',
389
+ installScript: 'curl -fsSL https://antigravity.google/cli/install.sh | bash',
390
+ configDir: path.join(HOME, '.gemini', 'antigravity-cli'),
391
+ commandsDir: path.join(HOME, '.gemini', 'antigravity-cli', 'commands'),
392
+ commandsSubdir: 'commands',
393
+ skillsDir: path.join(HOME, '.gemini', 'antigravity-cli', 'skills'),
394
+ hooksDir: 'hooks',
395
+ instructionsFile: 'AGENTS.md',
396
+ format: 'markdown',
397
+ variableSyntax: '{{args}}',
398
+ supportsHooks: true,
399
+ capabilities: { hooks: true, mcp: true, allowlist: true, skills: true, commands: true, plugins: true, rulesImports: false },
400
+ },
401
+ // xAI Grok Build CLI (`grok`) — early beta, SuperGrok Heavy. Auth via OAuth on
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.
407
+ grok: {
408
+ id: 'grok',
409
+ name: 'Grok',
410
+ color: 'cyanBright',
411
+ cliCommand: 'grok',
412
+ npmPackage: '',
413
+ installScript: 'curl -fsSL https://x.ai/cli/install.sh | bash -s VERSION',
414
+ configDir: path.join(HOME, '.grok'),
415
+ commandsDir: '', // Grok primarily uses skills + slash commands from skills
416
+ commandsSubdir: '',
417
+ skillsDir: path.join(HOME, '.grok', 'skills'),
418
+ hooksDir: path.join(HOME, '.grok', 'hooks'),
419
+ instructionsFile: 'AGENTS.md',
420
+ format: 'markdown',
421
+ variableSyntax: '$ARGUMENTS',
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
+ },
432
+ },
338
433
  };
339
434
  /** All registered agent IDs derived from the AGENTS registry. */
340
435
  export const ALL_AGENT_IDS = Object.keys(AGENTS);
@@ -455,6 +550,18 @@ export async function getCliState(agentId) {
455
550
  }
456
551
  }
457
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
+ }
458
565
  const binaryPath = findInPath(agent.cliCommand);
459
566
  if (!binaryPath) {
460
567
  return { installed: false, version: null, path: null };
@@ -488,7 +595,7 @@ export function isConfigured(agentId) {
488
595
  */
489
596
  export async function getUnmanagedAgentInstalls() {
490
597
  const unmanaged = [];
491
- const candidates = ['claude', 'codex', 'gemini'];
598
+ const candidates = ['claude', 'codex', 'gemini', 'grok'];
492
599
  for (const agentId of candidates) {
493
600
  const agent = AGENTS[agentId];
494
601
  try {
@@ -567,13 +674,15 @@ export async function getAccountInfo(agentId, home) {
567
674
  ['org', organizationId],
568
675
  ]);
569
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.
570
684
  let plan = null;
571
- const keychainOauth = await loadClaudeOauth(home);
572
- if (keychainOauth?.subscriptionType) {
573
- plan = keychainOauth.subscriptionType.charAt(0).toUpperCase()
574
- + keychainOauth.subscriptionType.slice(1);
575
- }
576
- else if (oa?.billingType === 'stripe_subscription') {
685
+ if (oa?.billingType === 'stripe_subscription') {
577
686
  plan = 'Pro';
578
687
  }
579
688
  else if (oa?.billingType) {
@@ -654,6 +763,19 @@ export async function getAccountInfo(agentId, home) {
654
763
  const data = JSON.parse(await fs.promises.readFile(path.join(base, '.gemini', 'google_accounts.json'), 'utf-8'));
655
764
  return { ...empty, email: data.active || null, lastActive };
656
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
+ }
657
779
  default:
658
780
  return { ...empty, lastActive };
659
781
  }
@@ -691,6 +813,12 @@ function getSessionDir(agentId, base) {
691
813
  return path.join(base, '.codex', 'sessions');
692
814
  case 'gemini':
693
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');
694
822
  default:
695
823
  return null;
696
824
  }
@@ -700,9 +828,12 @@ function getSessionExtension(agentId) {
700
828
  switch (agentId) {
701
829
  case 'claude':
702
830
  case 'codex':
831
+ case 'copilot':
703
832
  return '.jsonl';
704
833
  case 'gemini':
705
834
  return '.json';
835
+ case 'grok':
836
+ return '.json'; // sessions contain summary.json, events.jsonl, etc.
706
837
  default:
707
838
  return null;
708
839
  }
@@ -1084,6 +1215,12 @@ function getUserMcpConfigPath(agentId) {
1084
1215
  case 'openclaw':
1085
1216
  // OpenClaw uses openclaw.json
1086
1217
  return path.join(agent.configDir, 'openclaw.json');
1218
+ case 'antigravity':
1219
+ // agy uses mcp_config.json inside its nested config dir (~/.gemini/antigravity-cli/)
1220
+ return path.join(agent.configDir, 'mcp_config.json');
1221
+ case 'grok':
1222
+ // grok mcp.json — exact field schema verified at first install
1223
+ return path.join(agent.configDir, 'mcp.json');
1087
1224
  default:
1088
1225
  // Gemini and others use settings.json
1089
1226
  return path.join(agent.configDir, 'settings.json');
@@ -1114,6 +1251,10 @@ export function getMcpConfigPathForHome(agentId, home) {
1114
1251
  return path.join(home, '.config', 'goose', 'config.yaml');
1115
1252
  case 'roo':
1116
1253
  return path.join(home, '.roo', 'mcp.json');
1254
+ case 'antigravity':
1255
+ return path.join(home, '.gemini', 'antigravity-cli', 'mcp_config.json');
1256
+ case 'grok':
1257
+ return path.join(home, '.grok', 'config.toml');
1117
1258
  default:
1118
1259
  return path.join(home, `.${agentId}`, 'settings.json');
1119
1260
  }
@@ -1146,6 +1287,10 @@ function getProjectMcpConfigPath(agentId, cwd = process.cwd()) {
1146
1287
  return path.join(cwd, '.goose', 'config.yaml');
1147
1288
  case 'roo':
1148
1289
  return path.join(cwd, '.roo', 'mcp.json');
1290
+ case 'antigravity':
1291
+ return path.join(cwd, '.gemini', 'antigravity-cli', 'mcp_config.json');
1292
+ case 'grok':
1293
+ return path.join(cwd, '.grok', 'config.toml');
1149
1294
  default:
1150
1295
  return path.join(cwd, `.${agentId}`, 'settings.json');
1151
1296
  }
@@ -1275,6 +1420,14 @@ export const AGENT_NAME_ALIASES = {
1275
1420
  roo: 'roo',
1276
1421
  'roo-code': 'roo',
1277
1422
  roocode: 'roo',
1423
+ antigravity: 'antigravity',
1424
+ 'google-antigravity': 'antigravity',
1425
+ agy: 'antigravity',
1426
+ ag: 'antigravity',
1427
+ grok: 'grok',
1428
+ 'grok-build': 'grok',
1429
+ 'xai-grok': 'grok',
1430
+ gk: 'grok',
1278
1431
  };
1279
1432
  /** Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId. */
1280
1433
  export function resolveAgentName(input) {
@@ -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);
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
+ }