@phnx-labs/agents-cli 1.20.61 → 1.20.63
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 +57 -0
- package/README.md +10 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +13 -3
- package/dist/commands/exec.js +51 -1
- package/dist/commands/funnel.d.ts +5 -0
- package/dist/commands/funnel.js +62 -0
- package/dist/commands/hosts.js +42 -0
- package/dist/commands/repo.d.ts +4 -4
- package/dist/commands/repo.js +30 -19
- package/dist/commands/routines.js +73 -16
- package/dist/commands/sessions-sync.d.ts +1 -0
- package/dist/commands/sessions-sync.js +16 -2
- package/dist/commands/sessions.js +8 -1
- package/dist/commands/setup.js +9 -0
- package/dist/commands/ssh.js +72 -2
- package/dist/commands/sync-provision.d.ts +23 -0
- package/dist/commands/sync-provision.js +107 -0
- package/dist/commands/webhook.d.ts +9 -0
- package/dist/commands/webhook.js +93 -0
- package/dist/index.js +6 -2
- package/dist/lib/agents.d.ts +26 -0
- package/dist/lib/agents.js +100 -28
- package/dist/lib/browser/ipc.js +5 -4
- package/dist/lib/browser/profiles.d.ts +13 -0
- package/dist/lib/browser/profiles.js +17 -0
- package/dist/lib/browser/service.d.ts +12 -1
- package/dist/lib/browser/service.js +48 -13
- package/dist/lib/browser/sessions-list.d.ts +40 -0
- package/dist/lib/browser/sessions-list.js +190 -0
- package/dist/lib/commands.js +29 -0
- package/dist/lib/convert.d.ts +11 -0
- package/dist/lib/convert.js +22 -0
- package/dist/lib/daemon.js +2 -0
- package/dist/lib/devices/fleet.d.ts +62 -0
- package/dist/lib/devices/fleet.js +128 -0
- package/dist/lib/doctor-diff.js +17 -1
- package/dist/lib/funnel.d.ts +5 -0
- package/dist/lib/funnel.js +23 -0
- package/dist/lib/git.d.ts +21 -5
- package/dist/lib/git.js +64 -14
- package/dist/lib/goose-commands.d.ts +41 -0
- package/dist/lib/goose-commands.js +176 -0
- package/dist/lib/hooks.js +134 -0
- package/dist/lib/hosts/credentials.d.ts +28 -0
- package/dist/lib/hosts/credentials.js +48 -0
- package/dist/lib/hosts/dispatch.d.ts +25 -0
- package/dist/lib/hosts/dispatch.js +68 -2
- package/dist/lib/hosts/passthrough.d.ts +13 -10
- package/dist/lib/hosts/passthrough.js +119 -29
- package/dist/lib/mailbox-gc.js +4 -16
- package/dist/lib/migrate.d.ts +12 -0
- package/dist/lib/migrate.js +55 -1
- package/dist/lib/permissions.d.ts +15 -0
- package/dist/lib/permissions.js +99 -0
- package/dist/lib/plugins.d.ts +20 -0
- package/dist/lib/plugins.js +118 -0
- package/dist/lib/resources/permissions.js +2 -0
- package/dist/lib/routines.d.ts +29 -10
- package/dist/lib/routines.js +47 -15
- package/dist/lib/session/active.d.ts +8 -1
- package/dist/lib/session/active.js +1 -0
- package/dist/lib/session/parse.js +12 -0
- package/dist/lib/session/state.d.ts +33 -0
- package/dist/lib/session/state.js +40 -0
- package/dist/lib/session/sync/agents.d.ts +2 -0
- package/dist/lib/session/sync/agents.js +39 -1
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +6 -1
- package/dist/lib/session/sync/provision.d.ts +49 -0
- package/dist/lib/session/sync/provision.js +91 -0
- package/dist/lib/session/sync/sync.d.ts +3 -0
- package/dist/lib/session/sync/sync.js +26 -6
- package/dist/lib/session/sync/transcript-crypto.d.ts +77 -0
- package/dist/lib/session/sync/transcript-crypto.js +147 -0
- package/dist/lib/staleness/detectors/permissions.js +23 -0
- package/dist/lib/staleness/detectors/subagents.js +36 -0
- package/dist/lib/staleness/writers/commands.js +7 -0
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/staleness/writers/subagents.js +22 -3
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +11 -0
- package/dist/lib/state.d.ts +10 -2
- package/dist/lib/state.js +14 -2
- package/dist/lib/subagents.d.ts +32 -0
- package/dist/lib/subagents.js +238 -0
- package/dist/lib/triggers/webhook.d.ts +70 -27
- package/dist/lib/triggers/webhook.js +264 -43
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadFeed, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -174,6 +174,8 @@ Run and dispatch:
|
|
|
174
174
|
defaults Configure run defaults by agent/version selector
|
|
175
175
|
teams Coordinate multiple agents on shared work
|
|
176
176
|
routines Run agents on a cron schedule (scheduler auto-starts)
|
|
177
|
+
webhook Receive signed GitHub/Linear webhooks for trigger routines
|
|
178
|
+
funnel Expose a webhook receiver through Tailscale Funnel
|
|
177
179
|
sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
|
|
178
180
|
logs [id] Show a run's log — host-dispatch task or session; -f to follow
|
|
179
181
|
browser Automate a browser — navigate, click, screenshot, console, network
|
|
@@ -702,6 +704,8 @@ async function registerAllEagerCommands() {
|
|
|
702
704
|
await reg(loadLogs);
|
|
703
705
|
await reg(loadEvents);
|
|
704
706
|
await reg(loadAudit);
|
|
707
|
+
await reg(loadWebhook);
|
|
708
|
+
await reg(loadFunnel);
|
|
705
709
|
await reg(loadFeed);
|
|
706
710
|
await reg(loadSsh);
|
|
707
711
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
@@ -875,7 +879,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
875
879
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
876
880
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
877
881
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
878
|
-
const sentinelValue = '
|
|
882
|
+
const sentinelValue = 'v13';
|
|
879
883
|
let needRun = true;
|
|
880
884
|
try {
|
|
881
885
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -130,6 +130,32 @@ export interface AccountInfo {
|
|
|
130
130
|
}
|
|
131
131
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
132
132
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
133
|
+
/**
|
|
134
|
+
* Antigravity (`agy`) stores its OAuth token via the Go keyring library
|
|
135
|
+
* (zalando/go-keyring), which is platform-split:
|
|
136
|
+
*
|
|
137
|
+
* - macOS: login keychain, service `gemini`, account `antigravity` — no file.
|
|
138
|
+
* - Linux with Secret Service (libsecret / gnome-keyring): attributes
|
|
139
|
+
* service=`gemini`, username=`antigravity` (go-keyring's Secret Service
|
|
140
|
+
* mapping of service+user). Prefer this over the file when a keyring
|
|
141
|
+
* daemon is running.
|
|
142
|
+
* - Linux without Secret Service: file fallback at
|
|
143
|
+
* `~/.gemini/antigravity-cli/antigravity-oauth-token`.
|
|
144
|
+
*
|
|
145
|
+
* Probe the OS keyring for existence after the file check. On macOS,
|
|
146
|
+
* `security find-generic-password` without `-w` is metadata-only (never
|
|
147
|
+
* prompts). On Linux, `secret-tool lookup` exit 0 means the item exists
|
|
148
|
+
* (stdout is the secret — discarded, never logged). Cached per process —
|
|
149
|
+
* the keyring is account-global, so one probe covers every installed version.
|
|
150
|
+
* Returns false when the platform has no probe (Windows) or the tool is
|
|
151
|
+
* missing. Guard with `AGENTS_NO_KEYCHAIN_PROBE=1` for hermetic tests.
|
|
152
|
+
*/
|
|
153
|
+
export declare function antigravityOsKeyringProbe(platform?: NodeJS.Platform): {
|
|
154
|
+
cmd: string;
|
|
155
|
+
args: string[];
|
|
156
|
+
} | null;
|
|
157
|
+
/** @internal test hook — clear the per-process keyring probe cache. */
|
|
158
|
+
export declare function __resetAntigravityKeychainCacheForTest(): void;
|
|
133
159
|
export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
|
|
134
160
|
/**
|
|
135
161
|
* Determine when the agent was last used by checking session file mtimes,
|
package/dist/lib/agents.js
CHANGED
|
@@ -291,7 +291,15 @@ export const AGENTS = {
|
|
|
291
291
|
format: 'markdown',
|
|
292
292
|
variableSyntax: '$ARGUMENTS',
|
|
293
293
|
supportsHooks: true,
|
|
294
|
-
|
|
294
|
+
// Subagents: `.cursor/agents/<name>.md` (project) or `~/.cursor/agents/<name>.md`
|
|
295
|
+
// (user), Markdown with YAML frontmatter (name, description, model, readonly,
|
|
296
|
+
// is_background — no `color`). Shipped in cursor-agent CLI 2026.01 (Cursor 2.4,
|
|
297
|
+
// 2026-01-22); cursor-agent uses CalVer build tags (e.g. 2025.11.25-<hash>), so
|
|
298
|
+
// gate at `>= 2026.1.22`. The `agents sync` path enforces this (versions.ts skips
|
|
299
|
+
// + warns for pre-2.4 installs); the direct `subagents add --agents cursor` path
|
|
300
|
+
// writes unconditionally, same as the other since-gated agents.
|
|
301
|
+
// See transformSubagentForCursor / https://cursor.com/docs/subagents.
|
|
302
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: true, plugins: true, subagents: { since: '2026.1.22' }, rules: { file: '.cursorrules' }, workflows: false, memory: false, modes: ['edit', 'skip'] }, // allowlist: ~/.cursor/cli-config.json
|
|
295
303
|
},
|
|
296
304
|
opencode: {
|
|
297
305
|
id: 'opencode',
|
|
@@ -329,7 +337,11 @@ export const AGENTS = {
|
|
|
329
337
|
format: 'markdown',
|
|
330
338
|
variableSyntax: '{{ARGUMENTS}}',
|
|
331
339
|
supportsHooks: true,
|
|
332
|
-
|
|
340
|
+
// allowlist: maps blanket (whole-tool) rules to ~/.openclaw/openclaw.json
|
|
341
|
+
// tools.alsoAllow (allow) / tools.deny (deny). OpenClaw gates at tool
|
|
342
|
+
// granularity only, so sub-command/path/domain patterns are skipped.
|
|
343
|
+
// OpenClaw is self-updating (no pinned since), so `true` is correct.
|
|
344
|
+
capabilities: { hooks: true, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: true, commands: false, plugins: true, subagents: true, rules: { file: 'workspace/AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'] },
|
|
333
345
|
},
|
|
334
346
|
copilot: {
|
|
335
347
|
id: 'copilot',
|
|
@@ -421,7 +433,12 @@ export const AGENTS = {
|
|
|
421
433
|
// Plugins: Open Plugins under ~/.agents/plugins/<name>/ (same layout as
|
|
422
434
|
// agents-cli source). Version isolation copies into versionHome/.agents/plugins/.
|
|
423
435
|
// Workflows sync as Goose recipe YAML; permissions sync to permission.yaml.
|
|
424
|
-
|
|
436
|
+
// Commands: a Goose slash command is a recipe YAML under
|
|
437
|
+
// ~/.config/goose/commands/<name>.yaml, registered in ~/.config/goose/config.yaml
|
|
438
|
+
// under `slash_commands: [{ command, recipe_path }]` (see goose-commands.ts).
|
|
439
|
+
// Subagents: recipe YAML named agents under ~/.config/goose/agents/<name>.yaml
|
|
440
|
+
// (goose auto-discovers and delegates to them by name in autonomous mode).
|
|
441
|
+
capabilities: { hooks: { since: '1.34.0' }, mcp: true, mcpHttp: false, mcpHeaders: false, allowlist: true, skills: { since: '1.25.0' }, commands: true, plugins: true, subagents: true, rules: { file: 'AGENTS.md' }, workflows: true, memory: false, modes: ['edit'] },
|
|
425
442
|
},
|
|
426
443
|
// Google Antigravity CLI (`agy`) — official replacement for Gemini CLI as of IO 2026.
|
|
427
444
|
// configDir nests inside `~/.gemini/` since agy shares the parent dir with the Gemini
|
|
@@ -610,16 +627,25 @@ export const AGENTS = {
|
|
|
610
627
|
instructionsFile: 'MEMORY.md',
|
|
611
628
|
format: 'markdown',
|
|
612
629
|
variableSyntax: '$ARGUMENTS',
|
|
613
|
-
supportsHooks:
|
|
630
|
+
supportsHooks: true,
|
|
631
|
+
// Plugins: Hermes loads plugins from a flat `~/.hermes/plugins/<name>/` dir
|
|
632
|
+
// with a `plugin.yaml` manifest; a plugin only loads once its name is in the
|
|
633
|
+
// `plugins.enabled` allowlist in `~/.hermes/config.yaml` (deny-list
|
|
634
|
+
// `plugins.disabled` wins). Not the Claude marketplace layout, so it installs
|
|
635
|
+
// via a flat-copy branch (mirrors goose) plus a YAML allowlist toggle.
|
|
636
|
+
// See https://hermes-agent.nousresearch.com/docs/user-guide/features/plugins
|
|
614
637
|
capabilities: {
|
|
615
|
-
hooks
|
|
638
|
+
// Lifecycle hooks land in ~/.hermes/config.yaml under a `hooks:` block
|
|
639
|
+
// (YAML, shared with `mcp_servers`); gated to Hermes ≥ 0.11.0 which
|
|
640
|
+
// introduced the configurable hook runner.
|
|
641
|
+
hooks: { since: '0.11.0' },
|
|
616
642
|
mcp: true,
|
|
617
643
|
mcpHttp: true,
|
|
618
644
|
mcpHeaders: false,
|
|
619
645
|
allowlist: false,
|
|
620
646
|
skills: true,
|
|
621
647
|
commands: false,
|
|
622
|
-
plugins:
|
|
648
|
+
plugins: true,
|
|
623
649
|
subagents: false,
|
|
624
650
|
rules: { file: 'MEMORY.md' },
|
|
625
651
|
workflows: false,
|
|
@@ -638,14 +664,20 @@ export const AGENTS = {
|
|
|
638
664
|
npmPackage: '',
|
|
639
665
|
installScript: 'curl -fsSL https://forgecode.dev/cli | sh',
|
|
640
666
|
configDir: path.join(HOME, '.forge'),
|
|
641
|
-
commandsDir: '',
|
|
642
|
-
commandsSubdir: '',
|
|
667
|
+
commandsDir: path.join(HOME, '.forge', 'commands'),
|
|
668
|
+
commandsSubdir: 'commands',
|
|
643
669
|
skillsDir: path.join(HOME, '.forge', 'skills'),
|
|
644
670
|
hooksDir: 'hooks',
|
|
645
671
|
instructionsFile: 'AGENTS.md',
|
|
646
672
|
format: 'markdown',
|
|
647
673
|
variableSyntax: '$ARGUMENTS',
|
|
648
674
|
supportsHooks: false,
|
|
675
|
+
// Commands: ForgeCode reads Markdown slash commands from `~/.forge/commands/<name>.md`
|
|
676
|
+
// (also the shared `~/.agents/commands/`); the filename is the command name.
|
|
677
|
+
// Subagents: named `.md` agent definitions with YAML frontmatter under
|
|
678
|
+
// `~/.forge/agents/<name>.md` — same Markdown+frontmatter shape as Droid/Copilot
|
|
679
|
+
// (no `color` field), so transformSubagentForForge aliases transformSubagentForDroid.
|
|
680
|
+
// See https://forgecode.dev/docs/commands/ and /docs/agent-definition-guide/.
|
|
649
681
|
capabilities: {
|
|
650
682
|
hooks: false,
|
|
651
683
|
mcp: true,
|
|
@@ -653,9 +685,9 @@ export const AGENTS = {
|
|
|
653
685
|
mcpHeaders: false,
|
|
654
686
|
allowlist: false,
|
|
655
687
|
skills: true,
|
|
656
|
-
commands:
|
|
688
|
+
commands: true,
|
|
657
689
|
plugins: false,
|
|
658
|
-
subagents:
|
|
690
|
+
subagents: true,
|
|
659
691
|
rules: { file: 'AGENTS.md' },
|
|
660
692
|
workflows: false,
|
|
661
693
|
memory: false,
|
|
@@ -995,31 +1027,69 @@ function decryptDroidCredential(base) {
|
|
|
995
1027
|
}
|
|
996
1028
|
let cachedAgyKeychainSignedIn;
|
|
997
1029
|
/**
|
|
998
|
-
* Antigravity (`agy
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
1001
|
-
*
|
|
1002
|
-
*
|
|
1003
|
-
*
|
|
1004
|
-
*
|
|
1030
|
+
* Antigravity (`agy`) stores its OAuth token via the Go keyring library
|
|
1031
|
+
* (zalando/go-keyring), which is platform-split:
|
|
1032
|
+
*
|
|
1033
|
+
* - macOS: login keychain, service `gemini`, account `antigravity` — no file.
|
|
1034
|
+
* - Linux with Secret Service (libsecret / gnome-keyring): attributes
|
|
1035
|
+
* service=`gemini`, username=`antigravity` (go-keyring's Secret Service
|
|
1036
|
+
* mapping of service+user). Prefer this over the file when a keyring
|
|
1037
|
+
* daemon is running.
|
|
1038
|
+
* - Linux without Secret Service: file fallback at
|
|
1039
|
+
* `~/.gemini/antigravity-cli/antigravity-oauth-token`.
|
|
1040
|
+
*
|
|
1041
|
+
* Probe the OS keyring for existence after the file check. On macOS,
|
|
1042
|
+
* `security find-generic-password` without `-w` is metadata-only (never
|
|
1043
|
+
* prompts). On Linux, `secret-tool lookup` exit 0 means the item exists
|
|
1044
|
+
* (stdout is the secret — discarded, never logged). Cached per process —
|
|
1045
|
+
* the keyring is account-global, so one probe covers every installed version.
|
|
1046
|
+
* Returns false when the platform has no probe (Windows) or the tool is
|
|
1047
|
+
* missing. Guard with `AGENTS_NO_KEYCHAIN_PROBE=1` for hermetic tests.
|
|
1005
1048
|
*/
|
|
1049
|
+
export function antigravityOsKeyringProbe(platform = process.platform) {
|
|
1050
|
+
if (platform === 'darwin') {
|
|
1051
|
+
return {
|
|
1052
|
+
cmd: 'security',
|
|
1053
|
+
args: ['find-generic-password', '-s', 'gemini', '-a', 'antigravity'],
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
if (platform === 'linux') {
|
|
1057
|
+
// go-keyring secret_service attributes: "service" + "username" (not
|
|
1058
|
+
// "account" — that flag is the macOS security(1) spelling of the same user).
|
|
1059
|
+
return {
|
|
1060
|
+
cmd: 'secret-tool',
|
|
1061
|
+
args: ['lookup', 'service', 'gemini', 'username', 'antigravity'],
|
|
1062
|
+
};
|
|
1063
|
+
}
|
|
1064
|
+
return null;
|
|
1065
|
+
}
|
|
1066
|
+
/** @internal test hook — clear the per-process keyring probe cache. */
|
|
1067
|
+
export function __resetAntigravityKeychainCacheForTest() {
|
|
1068
|
+
cachedAgyKeychainSignedIn = undefined;
|
|
1069
|
+
}
|
|
1006
1070
|
async function antigravityKeychainSignedIn() {
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
// Test isolation: the real macOS keychain can't be sandboxed per-test, so
|
|
1010
|
-
// allow suites asserting "signed out" to opt out of the probe (same spirit as
|
|
1011
|
-
// AGENTS_REAL_HOME). Not cached, so tests can toggle it.
|
|
1071
|
+
// Test isolation first (before cache): real OS keyrings can't be sandboxed
|
|
1072
|
+
// per-test. Same spirit as AGENTS_REAL_HOME. Not cached, so tests can toggle.
|
|
1012
1073
|
if (process.env.AGENTS_NO_KEYCHAIN_PROBE === '1')
|
|
1013
1074
|
return false;
|
|
1014
|
-
if (
|
|
1075
|
+
if (cachedAgyKeychainSignedIn !== undefined)
|
|
1076
|
+
return cachedAgyKeychainSignedIn;
|
|
1077
|
+
const probe = antigravityOsKeyringProbe();
|
|
1078
|
+
if (!probe) {
|
|
1015
1079
|
cachedAgyKeychainSignedIn = false;
|
|
1016
1080
|
return false;
|
|
1017
1081
|
}
|
|
1018
1082
|
try {
|
|
1019
|
-
|
|
1083
|
+
// Discard stdout: Linux secret-tool lookup prints the secret value.
|
|
1084
|
+
await execFileAsync(probe.cmd, probe.args, {
|
|
1085
|
+
timeout: 3000,
|
|
1086
|
+
// encoding so stdout is a string we can drop without ever logging it
|
|
1087
|
+
encoding: 'utf8',
|
|
1088
|
+
});
|
|
1020
1089
|
cachedAgyKeychainSignedIn = true;
|
|
1021
1090
|
}
|
|
1022
1091
|
catch {
|
|
1092
|
+
// Missing tool (ENOENT), missing item, locked collection, timeout → signed out.
|
|
1023
1093
|
cachedAgyKeychainSignedIn = false;
|
|
1024
1094
|
}
|
|
1025
1095
|
return cachedAgyKeychainSignedIn;
|
|
@@ -1250,10 +1320,12 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1250
1320
|
// Antigravity (`agy`) stores a consumer Google OAuth grant (access +
|
|
1251
1321
|
// refresh token, no id_token) — presence of a refresh token is the only
|
|
1252
1322
|
// signed-in signal we can derive without a network call. Storage is
|
|
1253
|
-
// platform-split
|
|
1254
|
-
// ~/.gemini/antigravity-cli/antigravity-oauth-token
|
|
1255
|
-
//
|
|
1256
|
-
//
|
|
1323
|
+
// platform-split via go-keyring:
|
|
1324
|
+
// - file ~/.gemini/antigravity-cli/antigravity-oauth-token (Linux
|
|
1325
|
+
// fallback when no Secret Service is available)
|
|
1326
|
+
// - macOS keychain / Linux libsecret (service gemini + user
|
|
1327
|
+
// antigravity) when a keyring daemon is present
|
|
1328
|
+
// Check the file first, then the OS keyring probe.
|
|
1257
1329
|
const tokenPath = resolveAccountCredentialPath(base, '.gemini', 'antigravity-cli', 'antigravity-oauth-token');
|
|
1258
1330
|
if (tokenPath) {
|
|
1259
1331
|
const data = JSON.parse(await fs.promises.readFile(tokenPath, 'utf-8'));
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -432,11 +432,12 @@ export class BrowserIPCServer {
|
|
|
432
432
|
return { ok: true };
|
|
433
433
|
}
|
|
434
434
|
case 'set-download-path': {
|
|
435
|
-
if (!request.task
|
|
436
|
-
return { ok: false, error: 'Task
|
|
435
|
+
if (!request.task) {
|
|
436
|
+
return { ok: false, error: 'Task required' };
|
|
437
437
|
}
|
|
438
|
-
|
|
439
|
-
|
|
438
|
+
// downloadPath optional: omitted → service defaults to the profile's downloads dir.
|
|
439
|
+
const resolved = await this.service.setDownloadPath(request.task, request.downloadPath, request.tabId);
|
|
440
|
+
return { ok: true, downloadPath: resolved };
|
|
440
441
|
}
|
|
441
442
|
case 'wait-download': {
|
|
442
443
|
if (!request.task) {
|
|
@@ -11,6 +11,19 @@ export declare const DEFAULT_BROWSER_PROFILE_NAME = "default";
|
|
|
11
11
|
export declare function getConfiguredDefaultProfileName(): string | undefined;
|
|
12
12
|
export declare function getBrowserRuntimeDir(): string;
|
|
13
13
|
export declare function getProfileRuntimeDir(name: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* Default destination for browser downloads for a profile. Set browser-global at
|
|
16
|
+
* connect time (see BrowserService), so downloads land here even when the agent
|
|
17
|
+
* never calls `browser download --path`. Keyed by the same composite name as the
|
|
18
|
+
* runtime dir, so every profile is one self-contained tree.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getProfileDownloadsDir(name: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Per-profile directory for a task's captured artifacts (screenshots, PDFs,
|
|
23
|
+
* recordings). Replaces the legacy global `browser/sessions/<task>/` root — the
|
|
24
|
+
* one-shot migration in migrate.ts folds old captures into here.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getProfileSessionsDir(name: string, task: string): string;
|
|
14
27
|
export declare function listProfiles(): Promise<BrowserProfile[]>;
|
|
15
28
|
export declare function getProfile(name: string): Promise<BrowserProfile | null>;
|
|
16
29
|
/**
|
|
@@ -19,6 +19,23 @@ export function getBrowserRuntimeDir() {
|
|
|
19
19
|
export function getProfileRuntimeDir(name) {
|
|
20
20
|
return path.join(getBrowserRuntimeDir(), name);
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Default destination for browser downloads for a profile. Set browser-global at
|
|
24
|
+
* connect time (see BrowserService), so downloads land here even when the agent
|
|
25
|
+
* never calls `browser download --path`. Keyed by the same composite name as the
|
|
26
|
+
* runtime dir, so every profile is one self-contained tree.
|
|
27
|
+
*/
|
|
28
|
+
export function getProfileDownloadsDir(name) {
|
|
29
|
+
return path.join(getProfileRuntimeDir(name), 'downloads');
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Per-profile directory for a task's captured artifacts (screenshots, PDFs,
|
|
33
|
+
* recordings). Replaces the legacy global `browser/sessions/<task>/` root — the
|
|
34
|
+
* one-shot migration in migrate.ts folds old captures into here.
|
|
35
|
+
*/
|
|
36
|
+
export function getProfileSessionsDir(name, task) {
|
|
37
|
+
return path.join(getProfileRuntimeDir(name), 'sessions', task);
|
|
38
|
+
}
|
|
22
39
|
function configToProfile(name, config) {
|
|
23
40
|
validateRemoteBrowserBinaries(config);
|
|
24
41
|
return {
|
|
@@ -226,7 +226,18 @@ export declare class BrowserService {
|
|
|
226
226
|
timeout?: number;
|
|
227
227
|
tabHint?: string;
|
|
228
228
|
}): Promise<void>;
|
|
229
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Point the browser's default download destination at the profile's downloads
|
|
231
|
+
* dir, browser-global, at connect time. Without this a download the agent never
|
|
232
|
+
* explicitly routed (`browser download --path`) falls to Chromium's own default
|
|
233
|
+
* — for an attached user browser, wherever that browser was last configured,
|
|
234
|
+
* which is how downloads used to escape into random locations. Sent on the root
|
|
235
|
+
* session (no sessionId) so every current and future tab inherits it. Best
|
|
236
|
+
* effort: a remote CDP endpoint that doesn't expose the Browser domain must not
|
|
237
|
+
* fail the whole connect.
|
|
238
|
+
*/
|
|
239
|
+
private applyDefaultDownloadBehavior;
|
|
240
|
+
setDownloadPath(taskId: string, downloadPath?: string, tabHint?: string): Promise<string>;
|
|
230
241
|
waitForDownload(taskId: string, timeout?: number): Promise<string>;
|
|
231
242
|
private findTaskBySession;
|
|
232
243
|
shutdown(): Promise<void>;
|
|
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import { execFile } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
6
|
import { BrowserCdpConnectionError, CDPClient, discoverBrowserWsUrl, verifyBrowserIdentity, } from './cdp.js';
|
|
7
|
-
import { getProfile, getProfileRuntimeDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, } from './profiles.js';
|
|
7
|
+
import { getProfile, getProfileRuntimeDir, getProfileDownloadsDir, getProfileSessionsDir, getBrowserRuntimeDir, listProfiles, extractConfiguredPort, resolveEndpoint, } from './profiles.js';
|
|
8
8
|
import { killChrome, getRunningChromeInfo, launchBrowser, allocatePort } from './chrome.js';
|
|
9
9
|
import { connectLocal } from './drivers/local.js';
|
|
10
10
|
import { connectSSH, shellQuote } from './drivers/ssh.js';
|
|
@@ -308,7 +308,9 @@ export class BrowserService {
|
|
|
308
308
|
}
|
|
309
309
|
else if (!conn) {
|
|
310
310
|
conn = await this.connectProfile(effectiveProfile, resolved.target);
|
|
311
|
+
conn.profileName = composite;
|
|
311
312
|
this.connections.set(composite, conn);
|
|
313
|
+
await this.applyDefaultDownloadBehavior(conn, composite);
|
|
312
314
|
}
|
|
313
315
|
// Browsers launch with --no-startup-window (session-cookie persistence,
|
|
314
316
|
// see launchBrowser), so a bare `start` with no --url would otherwise
|
|
@@ -645,7 +647,7 @@ export class BrowserService {
|
|
|
645
647
|
return result.result.value;
|
|
646
648
|
}
|
|
647
649
|
async screenshot(taskId, tabHint, outputPath, quality = 'compressed') {
|
|
648
|
-
const { conn, task } = await this.findTask(taskId);
|
|
650
|
+
const { conn, task, profileName } = await this.findTask(taskId);
|
|
649
651
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
650
652
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
651
653
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -678,7 +680,7 @@ export class BrowserService {
|
|
|
678
680
|
}
|
|
679
681
|
extension = 'jpg';
|
|
680
682
|
}
|
|
681
|
-
const sessionsDir =
|
|
683
|
+
const sessionsDir = getProfileSessionsDir(profileName, task.name);
|
|
682
684
|
const automaticPath = path.join(sessionsDir, `${Date.now()}.${extension}`);
|
|
683
685
|
const finalPath = resolveScreenshotOutputPath(outputPath, automaticPath);
|
|
684
686
|
await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
|
|
@@ -695,7 +697,7 @@ export class BrowserService {
|
|
|
695
697
|
* — without it, dark-mode pages render on a blank sheet.
|
|
696
698
|
*/
|
|
697
699
|
async printToPdf(taskId, tabHint, outputPath) {
|
|
698
|
-
const { conn, task } = await this.findTask(taskId);
|
|
700
|
+
const { conn, task, profileName } = await this.findTask(taskId);
|
|
699
701
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
700
702
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
701
703
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -705,7 +707,7 @@ export class BrowserService {
|
|
|
705
707
|
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
706
708
|
const { data } = (await conn.cdp.send('Page.printToPDF', { printBackground: true, preferCSSPageSize: true }, sessionId));
|
|
707
709
|
const buffer = Buffer.from(data, 'base64');
|
|
708
|
-
const sessionsDir =
|
|
710
|
+
const sessionsDir = getProfileSessionsDir(profileName, task.name);
|
|
709
711
|
const automaticPath = path.join(sessionsDir, `${Date.now()}.pdf`);
|
|
710
712
|
const finalPath = resolveScreenshotOutputPath(outputPath, automaticPath);
|
|
711
713
|
await fs.promises.mkdir(path.dirname(finalPath), { recursive: true });
|
|
@@ -723,7 +725,7 @@ export class BrowserService {
|
|
|
723
725
|
if (this.recordings.has(taskId)) {
|
|
724
726
|
throw new Error(`Task "${taskId}" is already recording. Call record stop first.`);
|
|
725
727
|
}
|
|
726
|
-
const { conn, task } = await this.findTask(taskId);
|
|
728
|
+
const { conn, task, profileName } = await this.findTask(taskId);
|
|
727
729
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
728
730
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
729
731
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -739,7 +741,7 @@ export class BrowserService {
|
|
|
739
741
|
throw new Error('--duration must be between 1 and 3600 seconds');
|
|
740
742
|
if (maxMb < 1 || maxMb > 500)
|
|
741
743
|
throw new Error('--max-mb must be between 1 and 500');
|
|
742
|
-
const recordingsDir = path.join(
|
|
744
|
+
const recordingsDir = path.join(getProfileSessionsDir(profileName, task.name), 'recordings');
|
|
743
745
|
await fs.promises.mkdir(recordingsDir, { recursive: true });
|
|
744
746
|
const outputPath = path.join(recordingsDir, `${Date.now()}.webm`);
|
|
745
747
|
// Resolve ffmpeg lazily so non-recording paths don't pay the import cost.
|
|
@@ -1481,20 +1483,50 @@ export class BrowserService {
|
|
|
1481
1483
|
throw new Error(`Wait condition "${type}:${value}" not met within ${timeout}ms`);
|
|
1482
1484
|
}
|
|
1483
1485
|
// ─── Downloads ───────────────────────────────────────────────────────────────
|
|
1486
|
+
/**
|
|
1487
|
+
* Point the browser's default download destination at the profile's downloads
|
|
1488
|
+
* dir, browser-global, at connect time. Without this a download the agent never
|
|
1489
|
+
* explicitly routed (`browser download --path`) falls to Chromium's own default
|
|
1490
|
+
* — for an attached user browser, wherever that browser was last configured,
|
|
1491
|
+
* which is how downloads used to escape into random locations. Sent on the root
|
|
1492
|
+
* session (no sessionId) so every current and future tab inherits it. Best
|
|
1493
|
+
* effort: a remote CDP endpoint that doesn't expose the Browser domain must not
|
|
1494
|
+
* fail the whole connect.
|
|
1495
|
+
*/
|
|
1496
|
+
async applyDefaultDownloadBehavior(conn, profileName) {
|
|
1497
|
+
const downloadPath = getProfileDownloadsDir(profileName);
|
|
1498
|
+
try {
|
|
1499
|
+
await fs.promises.mkdir(downloadPath, { recursive: true });
|
|
1500
|
+
await conn.cdp.send('Browser.setDownloadBehavior', {
|
|
1501
|
+
behavior: 'allow',
|
|
1502
|
+
downloadPath,
|
|
1503
|
+
eventsEnabled: true,
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
catch {
|
|
1507
|
+
// Best effort: a remote CDP endpoint (ssh://, ws(s)://) may not expose the
|
|
1508
|
+
// Browser domain. Downloads then keep the endpoint's own default; the connect
|
|
1509
|
+
// must still succeed.
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1484
1512
|
async setDownloadPath(taskId, downloadPath, tabHint) {
|
|
1485
|
-
const { conn, task } = await this.findTask(taskId);
|
|
1513
|
+
const { conn, task, profileName } = await this.findTask(taskId);
|
|
1486
1514
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1487
1515
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1488
1516
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
1489
1517
|
if (!target)
|
|
1490
1518
|
throw new Error(`Tab ${shortId} not found`);
|
|
1491
1519
|
const sessionId = await this.getSessionId(conn, target.targetId);
|
|
1520
|
+
// No explicit --path: fall back to the profile's downloads dir, the same
|
|
1521
|
+
// destination already set browser-global at connect (applyDefaultDownloadBehavior).
|
|
1522
|
+
const resolvedPath = downloadPath ?? getProfileDownloadsDir(profileName);
|
|
1523
|
+
await fs.promises.mkdir(resolvedPath, { recursive: true });
|
|
1492
1524
|
await conn.cdp.send('Browser.setDownloadBehavior', {
|
|
1493
1525
|
behavior: 'allow',
|
|
1494
|
-
downloadPath,
|
|
1526
|
+
downloadPath: resolvedPath,
|
|
1495
1527
|
eventsEnabled: true,
|
|
1496
1528
|
}, sessionId);
|
|
1497
|
-
this.pendingDownloads.set(taskId, { path:
|
|
1529
|
+
this.pendingDownloads.set(taskId, { path: resolvedPath, completed: false });
|
|
1498
1530
|
conn.cdp.on('Browser.downloadProgress', (params) => {
|
|
1499
1531
|
if (params.state === 'completed') {
|
|
1500
1532
|
const dl = this.pendingDownloads.get(taskId);
|
|
@@ -1504,6 +1536,7 @@ export class BrowserService {
|
|
|
1504
1536
|
}
|
|
1505
1537
|
}
|
|
1506
1538
|
});
|
|
1539
|
+
return resolvedPath;
|
|
1507
1540
|
}
|
|
1508
1541
|
async waitForDownload(taskId, timeout = 60000) {
|
|
1509
1542
|
const start = Date.now();
|
|
@@ -1590,11 +1623,13 @@ export class BrowserService {
|
|
|
1590
1623
|
pid,
|
|
1591
1624
|
electron: true,
|
|
1592
1625
|
targetFilter: profile.targetFilter,
|
|
1626
|
+
profileName: forkName,
|
|
1593
1627
|
forkedFrom: profile.name,
|
|
1594
1628
|
tasks: new Map(),
|
|
1595
1629
|
sessionCache: new Map(),
|
|
1596
1630
|
};
|
|
1597
1631
|
this.connections.set(forkName, connection);
|
|
1632
|
+
await this.applyDefaultDownloadBehavior(connection, forkName);
|
|
1598
1633
|
return { forkName, connection };
|
|
1599
1634
|
}
|
|
1600
1635
|
/**
|
|
@@ -1776,12 +1811,12 @@ export class BrowserService {
|
|
|
1776
1811
|
if (!task) {
|
|
1777
1812
|
throw new Error(`Task "${taskId}" not found on profile "${profileName}"`);
|
|
1778
1813
|
}
|
|
1779
|
-
return { conn, task };
|
|
1814
|
+
return { conn, task, profileName };
|
|
1780
1815
|
}
|
|
1781
|
-
for (const [, conn] of this.connections) {
|
|
1816
|
+
for (const [key, conn] of this.connections) {
|
|
1782
1817
|
const task = conn.tasks.get(taskId);
|
|
1783
1818
|
if (task) {
|
|
1784
|
-
return { conn, task };
|
|
1819
|
+
return { conn, task, profileName: conn.profileName ?? key };
|
|
1785
1820
|
}
|
|
1786
1821
|
}
|
|
1787
1822
|
throw new Error(`Task "${taskId}" not found`);
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export type ArtifactKind = 'screenshot' | 'pdf' | 'recording' | 'download';
|
|
2
|
+
export interface BrowserArtifact {
|
|
3
|
+
kind: ArtifactKind;
|
|
4
|
+
/** Owning task for session captures; undefined for downloads. */
|
|
5
|
+
task?: string;
|
|
6
|
+
name: string;
|
|
7
|
+
path: string;
|
|
8
|
+
bytes: number;
|
|
9
|
+
mtimeMs: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ProfileArtifacts {
|
|
12
|
+
profile: string;
|
|
13
|
+
artifacts: BrowserArtifact[];
|
|
14
|
+
}
|
|
15
|
+
/** Every capture for one profile, newest first. */
|
|
16
|
+
export declare function listProfileArtifacts(profile: string): BrowserArtifact[];
|
|
17
|
+
/**
|
|
18
|
+
* Captures grouped by profile. With `only` set, returns just that profile (even
|
|
19
|
+
* when empty); otherwise every profile dir on disk that has at least one capture.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listBrowserSessions(only?: string): ProfileArtifacts[];
|
|
22
|
+
/** Human table for the CLI. Returns lines (no trailing newline). */
|
|
23
|
+
export declare function renderBrowserSessions(groups: ProfileArtifacts[]): string;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve `--open <sel>`: `latest` (newest across the groups) or a filename
|
|
26
|
+
* substring match. Returns the absolute path, or null if nothing matched.
|
|
27
|
+
*/
|
|
28
|
+
export declare function resolveArtifact(groups: ProfileArtifacts[], selector: string): string | null;
|
|
29
|
+
/** Open a file in the OS default app. Returns true on success. */
|
|
30
|
+
export declare function openArtifact(filePath: string): boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Shared CLI action for `agents browser sessions` and `agents sessions --browser`.
|
|
33
|
+
* `open` is the Commander value for `--open [selector]`: undefined when the flag
|
|
34
|
+
* is absent, `true` when passed bare (defaults to 'latest'), or the selector string.
|
|
35
|
+
*/
|
|
36
|
+
export declare function runBrowserSessions(opts: {
|
|
37
|
+
profile?: string;
|
|
38
|
+
open?: string | boolean;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
}): void;
|