@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.
- package/CHANGELOG.md +67 -0
- package/README.md +70 -10
- package/dist/commands/browser.js +88 -16
- package/dist/commands/cli.d.ts +14 -0
- package/dist/commands/cli.js +244 -0
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer.js +18 -1
- package/dist/commands/doctor.d.ts +1 -1
- package/dist/commands/doctor.js +2 -2
- package/dist/commands/exec.js +3 -3
- package/dist/commands/factory.d.ts +3 -14
- package/dist/commands/factory.js +3 -3
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/mcp.js +29 -0
- package/dist/commands/plugins.js +11 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/prune.js +39 -160
- package/dist/commands/pull.js +56 -3
- package/dist/commands/routines.js +106 -13
- package/dist/commands/secrets.js +6 -8
- package/dist/commands/sessions.d.ts +36 -7
- package/dist/commands/sessions.js +130 -53
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +37 -28
- package/dist/commands/skills.js +3 -3
- package/dist/commands/teams.js +13 -0
- package/dist/commands/versions.d.ts +4 -3
- package/dist/commands/versions.js +147 -124
- package/dist/commands/view.js +12 -12
- package/dist/index.js +34 -6
- package/dist/lib/acp/harnesses.js +8 -0
- package/dist/lib/agents.js +162 -9
- package/dist/lib/browser/cdp.d.ts +8 -1
- package/dist/lib/browser/cdp.js +40 -3
- package/dist/lib/browser/chrome.d.ts +13 -0
- package/dist/lib/browser/chrome.js +42 -3
- package/dist/lib/browser/domain-skills.d.ts +51 -0
- package/dist/lib/browser/domain-skills.js +157 -0
- package/dist/lib/browser/drivers/local.js +45 -4
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/browser/ipc.d.ts +8 -1
- package/dist/lib/browser/ipc.js +37 -28
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +41 -1
- package/dist/lib/browser/service.d.ts +3 -0
- package/dist/lib/browser/service.js +21 -5
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/cli-resources.d.ts +109 -0
- package/dist/lib/cli-resources.js +255 -0
- package/dist/lib/cloud/rush.js +5 -5
- package/dist/lib/command-skills.js +0 -2
- package/dist/lib/computer-rpc.d.ts +3 -0
- package/dist/lib/computer-rpc.js +53 -0
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/exec.d.ts +3 -2
- package/dist/lib/exec.js +62 -6
- package/dist/lib/hooks.js +182 -0
- package/dist/lib/mcp.js +6 -0
- package/dist/lib/migrate.js +1 -1
- package/dist/lib/overdue.d.ts +26 -0
- package/dist/lib/overdue.js +101 -0
- package/dist/lib/permissions.js +5 -1
- package/dist/lib/plugin-marketplace.js +1 -1
- package/dist/lib/profiles-presets.js +37 -0
- package/dist/lib/registry.d.ts +18 -0
- package/dist/lib/registry.js +44 -0
- package/dist/lib/resources/mcp.js +43 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/rotate.js +10 -4
- package/dist/lib/routines-format.d.ts +35 -0
- package/dist/lib/routines-format.js +173 -0
- package/dist/lib/routines.d.ts +7 -1
- package/dist/lib/routines.js +32 -12
- package/dist/lib/runner.js +19 -5
- package/dist/lib/scheduler.js +8 -1
- package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/CodeResources +0 -0
- package/dist/lib/secrets/{AgentsKeychain.app/Contents/Info.plist → Agents CLI.app/Contents/Info.plist } +4 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/bundles.d.ts +33 -2
- package/dist/lib/secrets/bundles.js +249 -26
- package/dist/lib/secrets/index.d.ts +10 -1
- package/dist/lib/secrets/index.js +143 -48
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +3 -2
- package/dist/lib/session/db.d.ts +10 -4
- package/dist/lib/session/db.js +16 -16
- package/dist/lib/session/parse.d.ts +1 -0
- package/dist/lib/session/parse.js +44 -0
- package/dist/lib/session/types.d.ts +1 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/shims.d.ts +6 -2
- package/dist/lib/shims.js +88 -10
- package/dist/lib/state.d.ts +0 -1
- package/dist/lib/state.js +2 -15
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +153 -3
- package/dist/lib/teams/summarizer.js +18 -2
- package/dist/lib/teams/worktree.js +14 -3
- package/dist/lib/types.d.ts +7 -4
- package/dist/lib/types.js +6 -3
- package/dist/lib/versions.d.ts +10 -2
- package/dist/lib/versions.js +227 -35
- package/package.json +9 -9
- package/dist/lib/secrets/AgentsKeychain.app/Contents/MacOS/AgentsKeychain +0 -0
- package/npm-shrinkwrap.json +0 -3162
- /package/dist/lib/secrets/{AgentsKeychain.app → Agents CLI.app}/Contents/_CodeSignature/CodeResources +0 -0
- /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).
|
package/dist/lib/agents.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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<{
|
package/dist/lib/browser/cdp.js
CHANGED
|
@@ -152,10 +152,47 @@ export class CDPClient {
|
|
|
152
152
|
this.transport = null;
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
|
-
export
|
|
156
|
-
|
|
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
|
|
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 {
|
|
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
|
|
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
|
+
}
|