@phnx-labs/agents-cli 1.20.62 → 1.20.64
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 +62 -0
- package/README.md +19 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +13 -3
- package/dist/commands/exec.js +96 -28
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +27 -8
- 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/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/repo.d.ts +4 -4
- package/dist/commands/repo.js +30 -19
- package/dist/commands/routines.js +92 -29
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +279 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions-sync.d.ts +1 -0
- package/dist/commands/sessions-sync.js +16 -2
- package/dist/commands/sessions.js +12 -1
- package/dist/commands/setup.js +9 -0
- package/dist/commands/ssh.js +170 -5
- package/dist/commands/sync-provision.d.ts +23 -0
- package/dist/commands/sync-provision.js +107 -0
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/view.d.ts +1 -1
- package/dist/commands/webhook.d.ts +9 -0
- package/dist/commands/webhook.js +93 -0
- package/dist/index.js +7 -2
- package/dist/lib/agents.d.ts +44 -0
- package/dist/lib/agents.js +85 -35
- package/dist/lib/browser/drivers/ssh.js +19 -2
- 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/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +158 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +24 -0
- package/dist/lib/daemon.js +8 -1
- package/dist/lib/devices/fleet.d.ts +62 -0
- package/dist/lib/devices/fleet.js +128 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- 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/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/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/migrate.d.ts +12 -0
- package/dist/lib/migrate.js +55 -1
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/routines.d.ts +50 -12
- package/dist/lib/routines.js +82 -27
- package/dist/lib/runner.js +255 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/session/bundle.d.ts +150 -0
- package/dist/lib/session/bundle.js +189 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/sync/agents.d.ts +56 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.d.ts +8 -0
- package/dist/lib/session/sync/config.js +6 -1
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- 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 +26 -2
- package/dist/lib/session/sync/sync.js +192 -69
- package/dist/lib/session/sync/transcript-crypto.d.ts +77 -0
- package/dist/lib/session/sync/transcript-crypto.js +147 -0
- package/dist/lib/ssh-tunnel.js +13 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +16 -0
- package/dist/lib/state.d.ts +10 -2
- package/dist/lib/state.js +14 -2
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/triggers/webhook.d.ts +70 -27
- package/dist/lib/triggers/webhook.js +264 -43
- package/dist/lib/types.d.ts +9 -0
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +162 -22
- package/package.json +1 -1
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive provisioning flow for cross-machine session sync. Collects R2
|
|
3
|
+
* credentials, writes the `r2.backups` bundle (via the pure lib helper), probes
|
|
4
|
+
* connectivity, and opts the machine into the `session-sync` beta. Shared by
|
|
5
|
+
* `agents setup` (offered, opt-in) and `agents sessions sync --setup` (explicit).
|
|
6
|
+
*
|
|
7
|
+
* The prompt/UI lives here in the command layer; the credential-writing and
|
|
8
|
+
* connectivity logic is the pure, unit-tested `lib/session/sync/provision.ts`.
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import ora from 'ora';
|
|
12
|
+
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
13
|
+
import { isSyncConfigured } from '../lib/session/sync/config.js';
|
|
14
|
+
import { writeSyncBundle, probeR2Connectivity, readStoredEncKey } from '../lib/session/sync/provision.js';
|
|
15
|
+
import { setBetaEnabled } from '../lib/beta.js';
|
|
16
|
+
const SYNC_BETA = 'session-sync';
|
|
17
|
+
const nonEmpty = (v) => (v.trim().length > 0 ? true : 'required');
|
|
18
|
+
/**
|
|
19
|
+
* Run the guided session-sync setup. Never throws for expected outcomes
|
|
20
|
+
* (non-interactive shell, user declines, cancels, or a failed probe) — returns
|
|
21
|
+
* quietly so a first-run `agents setup` is never blocked by the optional step.
|
|
22
|
+
*/
|
|
23
|
+
export async function promptAndProvisionSessionSync(opts = {}) {
|
|
24
|
+
if (!isInteractiveTerminal()) {
|
|
25
|
+
if (opts.explicit)
|
|
26
|
+
console.error(chalk.red('Session-sync setup needs an interactive terminal.'));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const alreadyConfigured = isSyncConfigured();
|
|
30
|
+
const { confirm, input, password } = await import('@inquirer/prompts');
|
|
31
|
+
try {
|
|
32
|
+
if (!opts.explicit) {
|
|
33
|
+
if (alreadyConfigured)
|
|
34
|
+
return; // first-run setup: nothing to do
|
|
35
|
+
const want = await confirm({
|
|
36
|
+
message: 'Set up cross-machine session sync (encrypted transcripts via Cloudflare R2)?',
|
|
37
|
+
default: false,
|
|
38
|
+
});
|
|
39
|
+
if (!want)
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
else if (alreadyConfigured) {
|
|
43
|
+
const reconfigure = await confirm({
|
|
44
|
+
message: 'Session sync is already configured. Re-enter R2 credentials?',
|
|
45
|
+
default: false,
|
|
46
|
+
});
|
|
47
|
+
if (!reconfigure) {
|
|
48
|
+
const key = readStoredEncKey();
|
|
49
|
+
if (key) {
|
|
50
|
+
console.log(chalk.dim('\nShared encryption key (paste on every other machine you sync):'));
|
|
51
|
+
console.log(' ' + chalk.cyan(key));
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
console.log(chalk.dim('\nR2 credentials — Cloudflare dashboard > R2 > Manage API Tokens (needs read+write):'));
|
|
57
|
+
const accountId = (await input({ message: 'R2 account ID:', validate: nonEmpty })).trim();
|
|
58
|
+
const bucketName = (await input({ message: 'R2 bucket name:', validate: nonEmpty })).trim();
|
|
59
|
+
const accessKeyId = (await password({ message: 'R2 access key ID:', mask: true })).trim();
|
|
60
|
+
const secretAccessKey = (await password({ message: 'R2 secret access key:', mask: true })).trim();
|
|
61
|
+
const endpoint = (await input({
|
|
62
|
+
message: 'S3 endpoint override (blank = Cloudflare R2):',
|
|
63
|
+
default: '',
|
|
64
|
+
})).trim();
|
|
65
|
+
// Encryption key: the first machine mints one; every other machine pastes it
|
|
66
|
+
// so the whole fabric shares a single key (and can decrypt each other).
|
|
67
|
+
const firstMachine = await confirm({ message: 'Is this the FIRST machine in your sync fabric?', default: true });
|
|
68
|
+
let encKey;
|
|
69
|
+
if (!firstMachine) {
|
|
70
|
+
encKey = (await password({
|
|
71
|
+
message: 'Paste the shared R2_SYNC_ENC_KEY from your first machine:',
|
|
72
|
+
mask: true,
|
|
73
|
+
validate: nonEmpty,
|
|
74
|
+
})).trim();
|
|
75
|
+
}
|
|
76
|
+
const { encKeyAction } = writeSyncBundle({
|
|
77
|
+
accountId, bucketName, accessKeyId, secretAccessKey,
|
|
78
|
+
endpoint: endpoint || undefined,
|
|
79
|
+
encKey: encKey || undefined,
|
|
80
|
+
});
|
|
81
|
+
const spinner = ora('Validating R2 read+write...').start();
|
|
82
|
+
const probe = await probeR2Connectivity();
|
|
83
|
+
if (!probe.ok) {
|
|
84
|
+
spinner.fail(`R2 probe failed: ${probe.error}`);
|
|
85
|
+
console.log(chalk.yellow('Credentials were saved to the r2.backups bundle but not verified. Fix them and re-run ' +
|
|
86
|
+
'`agents sessions sync --setup`, or rotate one key with `agents secrets rotate r2.backups <KEY>`.'));
|
|
87
|
+
return; // do not auto-enable sync against credentials that don't work
|
|
88
|
+
}
|
|
89
|
+
spinner.succeed('R2 read+write verified.');
|
|
90
|
+
setBetaEnabled([SYNC_BETA], true);
|
|
91
|
+
console.log(chalk.green('\nSession sync configured and enabled') + chalk.dim(' (the daemon syncs every ~90s).'));
|
|
92
|
+
if (encKeyAction === 'generated') {
|
|
93
|
+
const key = readStoredEncKey();
|
|
94
|
+
console.log(chalk.bold('\nShared encryption key — paste this on every OTHER machine you sync:'));
|
|
95
|
+
console.log(' ' + chalk.cyan(key ?? '(unavailable)'));
|
|
96
|
+
console.log(chalk.dim('Anyone with this key can decrypt your transcripts. Keep it secret.'));
|
|
97
|
+
}
|
|
98
|
+
console.log(chalk.dim('\nSync now with: agents sessions sync'));
|
|
99
|
+
}
|
|
100
|
+
catch (err) {
|
|
101
|
+
if (isPromptCancelled(err)) {
|
|
102
|
+
console.log(chalk.yellow('\nSession-sync setup cancelled.'));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
throw err;
|
|
106
|
+
}
|
|
107
|
+
}
|
package/dist/commands/usage.d.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Lists every installed agent with the best available usage snapshot:
|
|
5
5
|
* - claude: live OAuth API call (cached for 2 minutes)
|
|
6
6
|
* - codex: parsed from latest session log's rate_limits event
|
|
7
|
+
* - kimi: live Kimi Code /usages API call (cached for 2 minutes)
|
|
8
|
+
* - droid: live Factory billing/limits API call (cached for 2 minutes)
|
|
7
9
|
* - others: marked as "not exposed by CLI" (Gemini, OpenCode, Cursor, etc.
|
|
8
10
|
* don't publish per-account usage today)
|
|
9
11
|
*/
|
package/dist/commands/usage.js
CHANGED
|
@@ -3,8 +3,13 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
|
|
4
4
|
import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
|
|
5
5
|
import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Agents whose CLI surfaces usage data we can read today. Kept in sync with the
|
|
8
|
+
* live/last-seen sources `getUsageInfo` dispatches on in `../lib/usage.js`
|
|
9
|
+
* (claude, codex, kimi, droid) — an agent with a usage source but missing here
|
|
10
|
+
* would wrongly print "does not publish usage data" for a signed-in account.
|
|
11
|
+
*/
|
|
12
|
+
const USAGE_SUPPORTED = new Set(['claude', 'codex', 'kimi', 'droid']);
|
|
8
13
|
export function registerUsageCommand(program) {
|
|
9
14
|
addHostOption(program.command('usage [agent]'))
|
|
10
15
|
.description('Show rate-limit / quota usage per agent')
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents webhook` — localhost receiver for signed public webhook ingress.
|
|
3
|
+
*
|
|
4
|
+
* The receiver intentionally binds localhost by default. Public exposure is a
|
|
5
|
+
* separate `agents funnel up <host>` step so the HTTP process can be tested and
|
|
6
|
+
* rotated without changing the Tailscale Funnel config.
|
|
7
|
+
*/
|
|
8
|
+
import type { Command } from 'commander';
|
|
9
|
+
export declare function registerWebhookCommand(program: Command): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
3
|
+
import { startWebhookServer } from '../lib/triggers/webhook.js';
|
|
4
|
+
const DEFAULT_HOST = '127.0.0.1';
|
|
5
|
+
const DEFAULT_PORT = 8787;
|
|
6
|
+
function positiveInt(value, fallback) {
|
|
7
|
+
const parsed = Number.parseInt(value ?? '', 10);
|
|
8
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
9
|
+
}
|
|
10
|
+
function readWebhookSecrets(bundleName) {
|
|
11
|
+
const { env } = readAndResolveBundleEnv(bundleName, {
|
|
12
|
+
caller: 'webhook serve',
|
|
13
|
+
});
|
|
14
|
+
const secrets = {};
|
|
15
|
+
if (env.GITHUB_WEBHOOK_SECRET)
|
|
16
|
+
secrets.github = env.GITHUB_WEBHOOK_SECRET;
|
|
17
|
+
if (env.LINEAR_WEBHOOK_SECRET)
|
|
18
|
+
secrets.linear = env.LINEAR_WEBHOOK_SECRET;
|
|
19
|
+
if (!secrets.github && !secrets.linear) {
|
|
20
|
+
throw new Error(`Bundle '${bundleName}' must contain GITHUB_WEBHOOK_SECRET or LINEAR_WEBHOOK_SECRET.`);
|
|
21
|
+
}
|
|
22
|
+
return secrets;
|
|
23
|
+
}
|
|
24
|
+
function waitForListening(server) {
|
|
25
|
+
if (server.listening)
|
|
26
|
+
return Promise.resolve();
|
|
27
|
+
return new Promise((resolve, reject) => {
|
|
28
|
+
const cleanup = () => {
|
|
29
|
+
server.off('listening', onListening);
|
|
30
|
+
server.off('error', onError);
|
|
31
|
+
};
|
|
32
|
+
const onListening = () => {
|
|
33
|
+
cleanup();
|
|
34
|
+
resolve();
|
|
35
|
+
};
|
|
36
|
+
const onError = (err) => {
|
|
37
|
+
cleanup();
|
|
38
|
+
reject(err);
|
|
39
|
+
};
|
|
40
|
+
server.once('listening', onListening);
|
|
41
|
+
server.once('error', onError);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export function registerWebhookCommand(program) {
|
|
45
|
+
const webhook = program
|
|
46
|
+
.command('webhook')
|
|
47
|
+
.description('Run a localhost signed webhook receiver for routine triggers.');
|
|
48
|
+
webhook
|
|
49
|
+
.command('serve')
|
|
50
|
+
.description('Receive signed GitHub/Linear webhooks on /hooks/<source> and fire matching routines.')
|
|
51
|
+
.requiredOption('--secrets-bundle <name>', 'agents secrets bundle containing GITHUB_WEBHOOK_SECRET and/or LINEAR_WEBHOOK_SECRET')
|
|
52
|
+
.option('--host <addr>', `Bind address (default ${DEFAULT_HOST})`, DEFAULT_HOST)
|
|
53
|
+
.option('-p, --port <n>', `Local port (default ${DEFAULT_PORT})`, String(DEFAULT_PORT))
|
|
54
|
+
.option('--rate-limit <n>', 'Accepted deliveries per source per minute', '60')
|
|
55
|
+
.action(async (opts) => {
|
|
56
|
+
let secrets;
|
|
57
|
+
try {
|
|
58
|
+
secrets = readWebhookSecrets(opts.secretsBundle);
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
console.error(chalk.red(err.message));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
const port = positiveInt(opts.port, DEFAULT_PORT);
|
|
65
|
+
const rateLimit = positiveInt(opts.rateLimit, 60);
|
|
66
|
+
try {
|
|
67
|
+
const server = startWebhookServer({
|
|
68
|
+
host: opts.host ?? DEFAULT_HOST,
|
|
69
|
+
port,
|
|
70
|
+
secrets,
|
|
71
|
+
rateLimitPerMinute: rateLimit,
|
|
72
|
+
onDelivery: (webhook, fired) => {
|
|
73
|
+
console.log(`${new Date().toISOString()} ${webhook.source}:${webhook.event} ` +
|
|
74
|
+
`${fired.length ? `fired ${fired.map((f) => f.jobName).join(', ')}` : 'no match'}`);
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
await waitForListening(server);
|
|
78
|
+
const address = server.address();
|
|
79
|
+
const bound = typeof address === 'object' && address ? address.port : port;
|
|
80
|
+
console.log(`${chalk.green('agents webhook')} ${chalk.dim('→')} ${chalk.cyan(`http://${opts.host ?? DEFAULT_HOST}:${bound}`)}`);
|
|
81
|
+
console.log(chalk.dim('signed · localhost by default · endpoints: /hooks/github, /hooks/linear · Ctrl-C to stop'));
|
|
82
|
+
const shutdown = () => {
|
|
83
|
+
server.close(() => process.exit(0));
|
|
84
|
+
};
|
|
85
|
+
process.on('SIGINT', shutdown);
|
|
86
|
+
process.on('SIGTERM', shutdown);
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
console.error(chalk.red(`Could not start webhook receiver: ${err.message}`));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
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, loadMailboxes, } 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,7 +704,10 @@ 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);
|
|
710
|
+
await reg(loadMailboxes);
|
|
706
711
|
await reg(loadSsh);
|
|
707
712
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
708
713
|
registerJobsCronAliasCommand(program, 'cron');
|
|
@@ -875,7 +880,7 @@ if (process.env.AGENTS_SKIP_MIGRATION !== '1') {
|
|
|
875
880
|
// Bumping the suffix re-runs migrations for every user; binary releases that
|
|
876
881
|
// don't change the schema must NOT re-run (they would destroy user content
|
|
877
882
|
// when migration steps overlap with user-authored paths). See issue #20.
|
|
878
|
-
const sentinelValue = '
|
|
883
|
+
const sentinelValue = 'v13';
|
|
879
884
|
let needRun = true;
|
|
880
885
|
try {
|
|
881
886
|
if (fs.existsSync(sentinel) && fs.readFileSync(sentinel, 'utf-8').trim() === sentinelValue) {
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -130,6 +130,48 @@ 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
|
+
/** Decrypted contents of Droid's auth.v2.file (subset we consume). */
|
|
134
|
+
export interface DroidAuthPayload {
|
|
135
|
+
access_token?: string;
|
|
136
|
+
active_organization_id?: string | null;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
|
|
140
|
+
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
141
|
+
* in ~/.factory/auth.v2.key. On the keyfile-v2 source there is no OS-keychain /
|
|
142
|
+
* device binding — the key is on disk — so we can decrypt locally with no
|
|
143
|
+
* network call. Every failure (missing key file — e.g. a keyring-v2/legacy
|
|
144
|
+
* login with no on-disk key, a bad GCM tag, or malformed JSON) returns null.
|
|
145
|
+
* Never throws. Shared by account identity below and the Droid usage fetcher
|
|
146
|
+
* in usage.ts.
|
|
147
|
+
*/
|
|
148
|
+
export declare function decryptDroidAuthPayload(base: string): DroidAuthPayload | null;
|
|
149
|
+
/**
|
|
150
|
+
* Antigravity (`agy`) stores its OAuth token via the Go keyring library
|
|
151
|
+
* (zalando/go-keyring), which is platform-split:
|
|
152
|
+
*
|
|
153
|
+
* - macOS: login keychain, service `gemini`, account `antigravity` — no file.
|
|
154
|
+
* - Linux with Secret Service (libsecret / gnome-keyring): attributes
|
|
155
|
+
* service=`gemini`, username=`antigravity` (go-keyring's Secret Service
|
|
156
|
+
* mapping of service+user). Prefer this over the file when a keyring
|
|
157
|
+
* daemon is running.
|
|
158
|
+
* - Linux without Secret Service: file fallback at
|
|
159
|
+
* `~/.gemini/antigravity-cli/antigravity-oauth-token`.
|
|
160
|
+
*
|
|
161
|
+
* Probe the OS keyring for existence after the file check. On macOS,
|
|
162
|
+
* `security find-generic-password` without `-w` is metadata-only (never
|
|
163
|
+
* prompts). On Linux, `secret-tool lookup` exit 0 means the item exists
|
|
164
|
+
* (stdout is the secret — discarded, never logged). Cached per process —
|
|
165
|
+
* the keyring is account-global, so one probe covers every installed version.
|
|
166
|
+
* Returns false when the platform has no probe (Windows) or the tool is
|
|
167
|
+
* missing. Guard with `AGENTS_NO_KEYCHAIN_PROBE=1` for hermetic tests.
|
|
168
|
+
*/
|
|
169
|
+
export declare function antigravityOsKeyringProbe(platform?: NodeJS.Platform): {
|
|
170
|
+
cmd: string;
|
|
171
|
+
args: string[];
|
|
172
|
+
} | null;
|
|
173
|
+
/** @internal test hook — clear the per-process keyring probe cache. */
|
|
174
|
+
export declare function __resetAntigravityKeychainCacheForTest(): void;
|
|
133
175
|
export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
|
|
134
176
|
/**
|
|
135
177
|
* Determine when the agent was last used by checking session file mtimes,
|
|
@@ -147,6 +189,8 @@ export declare function resolveLastActive(agentId: AgentId, base: string, config
|
|
|
147
189
|
* Used during init to show approximate session count to user.
|
|
148
190
|
*/
|
|
149
191
|
export declare function countSessionFiles(agentId: AgentId): number;
|
|
192
|
+
/** Decode the payload section of a JWT token without verifying its signature. */
|
|
193
|
+
export declare function decodeJwtPayload(token: string): Record<string, any> | null;
|
|
150
194
|
/** Register an MCP server with an agent's CLI via `mcp add`. */
|
|
151
195
|
export declare function registerMcp(agentId: AgentId, name: string, command: string, scope?: 'user' | 'project', transport?: string, options?: {
|
|
152
196
|
home?: string;
|
package/dist/lib/agents.js
CHANGED
|
@@ -984,15 +984,12 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
984
984
|
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
985
985
|
* in ~/.factory/auth.v2.key. On the keyfile-v2 source there is no OS-keychain /
|
|
986
986
|
* device binding — the key is on disk — so we can decrypt locally with no
|
|
987
|
-
* network call.
|
|
988
|
-
*
|
|
989
|
-
*
|
|
990
|
-
*
|
|
991
|
-
* Every failure (missing key file — e.g. a keyring-v2/legacy login with no
|
|
992
|
-
* on-disk key, a bad GCM tag, malformed JSON, or no email claim) returns null so
|
|
993
|
-
* the caller falls back to the file-presence signed-in signal. Never throws.
|
|
987
|
+
* network call. Every failure (missing key file — e.g. a keyring-v2/legacy
|
|
988
|
+
* login with no on-disk key, a bad GCM tag, or malformed JSON) returns null.
|
|
989
|
+
* Never throws. Shared by account identity below and the Droid usage fetcher
|
|
990
|
+
* in usage.ts.
|
|
994
991
|
*/
|
|
995
|
-
function
|
|
992
|
+
export function decryptDroidAuthPayload(base) {
|
|
996
993
|
const filePath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.file');
|
|
997
994
|
const keyPath = resolveAccountCredentialPath(base, '.factory', 'auth.v2.key');
|
|
998
995
|
if (!filePath || !keyPath)
|
|
@@ -1012,46 +1009,97 @@ function decryptDroidCredential(base) {
|
|
|
1012
1009
|
decipher.final(),
|
|
1013
1010
|
]).toString('utf-8');
|
|
1014
1011
|
const cred = JSON.parse(plaintext);
|
|
1015
|
-
|
|
1016
|
-
if (!claims)
|
|
1017
|
-
return null;
|
|
1018
|
-
return {
|
|
1019
|
-
email: typeof claims.email === 'string' ? claims.email : null,
|
|
1020
|
-
orgId: normalizeIdentityPart(claims.org_id ?? cred.active_organization_id),
|
|
1021
|
-
role: typeof claims.role === 'string' ? claims.role : null,
|
|
1022
|
-
};
|
|
1012
|
+
return cred && typeof cred === 'object' ? cred : null;
|
|
1023
1013
|
}
|
|
1024
1014
|
catch {
|
|
1025
1015
|
return null;
|
|
1026
1016
|
}
|
|
1027
1017
|
}
|
|
1018
|
+
/**
|
|
1019
|
+
* Derive Droid account identity from the decrypted credential. The
|
|
1020
|
+
* `access_token` is a WorkOS JWT carrying an `email` claim (plus org_id /
|
|
1021
|
+
* role). We decode the claim WITHOUT verifying `exp`: the email is stable
|
|
1022
|
+
* identity for display, not an authorization decision, so an expired token
|
|
1023
|
+
* still yields the right address. Returns null when the credential can't be
|
|
1024
|
+
* decrypted or has no decodable claims, so the caller falls back to the
|
|
1025
|
+
* file-presence signed-in signal.
|
|
1026
|
+
*/
|
|
1027
|
+
function decryptDroidCredential(base) {
|
|
1028
|
+
const cred = decryptDroidAuthPayload(base);
|
|
1029
|
+
const claims = typeof cred?.access_token === 'string' ? decodeJwtPayload(cred.access_token) : null;
|
|
1030
|
+
if (!claims)
|
|
1031
|
+
return null;
|
|
1032
|
+
return {
|
|
1033
|
+
email: typeof claims.email === 'string' ? claims.email : null,
|
|
1034
|
+
orgId: normalizeIdentityPart(claims.org_id ?? cred?.active_organization_id),
|
|
1035
|
+
role: typeof claims.role === 'string' ? claims.role : null,
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1028
1038
|
let cachedAgyKeychainSignedIn;
|
|
1029
1039
|
/**
|
|
1030
|
-
* Antigravity (`agy
|
|
1031
|
-
*
|
|
1032
|
-
*
|
|
1033
|
-
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1036
|
-
*
|
|
1040
|
+
* Antigravity (`agy`) stores its OAuth token via the Go keyring library
|
|
1041
|
+
* (zalando/go-keyring), which is platform-split:
|
|
1042
|
+
*
|
|
1043
|
+
* - macOS: login keychain, service `gemini`, account `antigravity` — no file.
|
|
1044
|
+
* - Linux with Secret Service (libsecret / gnome-keyring): attributes
|
|
1045
|
+
* service=`gemini`, username=`antigravity` (go-keyring's Secret Service
|
|
1046
|
+
* mapping of service+user). Prefer this over the file when a keyring
|
|
1047
|
+
* daemon is running.
|
|
1048
|
+
* - Linux without Secret Service: file fallback at
|
|
1049
|
+
* `~/.gemini/antigravity-cli/antigravity-oauth-token`.
|
|
1050
|
+
*
|
|
1051
|
+
* Probe the OS keyring for existence after the file check. On macOS,
|
|
1052
|
+
* `security find-generic-password` without `-w` is metadata-only (never
|
|
1053
|
+
* prompts). On Linux, `secret-tool lookup` exit 0 means the item exists
|
|
1054
|
+
* (stdout is the secret — discarded, never logged). Cached per process —
|
|
1055
|
+
* the keyring is account-global, so one probe covers every installed version.
|
|
1056
|
+
* Returns false when the platform has no probe (Windows) or the tool is
|
|
1057
|
+
* missing. Guard with `AGENTS_NO_KEYCHAIN_PROBE=1` for hermetic tests.
|
|
1037
1058
|
*/
|
|
1059
|
+
export function antigravityOsKeyringProbe(platform = process.platform) {
|
|
1060
|
+
if (platform === 'darwin') {
|
|
1061
|
+
return {
|
|
1062
|
+
cmd: 'security',
|
|
1063
|
+
args: ['find-generic-password', '-s', 'gemini', '-a', 'antigravity'],
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
if (platform === 'linux') {
|
|
1067
|
+
// go-keyring secret_service attributes: "service" + "username" (not
|
|
1068
|
+
// "account" — that flag is the macOS security(1) spelling of the same user).
|
|
1069
|
+
return {
|
|
1070
|
+
cmd: 'secret-tool',
|
|
1071
|
+
args: ['lookup', 'service', 'gemini', 'username', 'antigravity'],
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
return null;
|
|
1075
|
+
}
|
|
1076
|
+
/** @internal test hook — clear the per-process keyring probe cache. */
|
|
1077
|
+
export function __resetAntigravityKeychainCacheForTest() {
|
|
1078
|
+
cachedAgyKeychainSignedIn = undefined;
|
|
1079
|
+
}
|
|
1038
1080
|
async function antigravityKeychainSignedIn() {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
// Test isolation: the real macOS keychain can't be sandboxed per-test, so
|
|
1042
|
-
// allow suites asserting "signed out" to opt out of the probe (same spirit as
|
|
1043
|
-
// AGENTS_REAL_HOME). Not cached, so tests can toggle it.
|
|
1081
|
+
// Test isolation first (before cache): real OS keyrings can't be sandboxed
|
|
1082
|
+
// per-test. Same spirit as AGENTS_REAL_HOME. Not cached, so tests can toggle.
|
|
1044
1083
|
if (process.env.AGENTS_NO_KEYCHAIN_PROBE === '1')
|
|
1045
1084
|
return false;
|
|
1046
|
-
if (
|
|
1085
|
+
if (cachedAgyKeychainSignedIn !== undefined)
|
|
1086
|
+
return cachedAgyKeychainSignedIn;
|
|
1087
|
+
const probe = antigravityOsKeyringProbe();
|
|
1088
|
+
if (!probe) {
|
|
1047
1089
|
cachedAgyKeychainSignedIn = false;
|
|
1048
1090
|
return false;
|
|
1049
1091
|
}
|
|
1050
1092
|
try {
|
|
1051
|
-
|
|
1093
|
+
// Discard stdout: Linux secret-tool lookup prints the secret value.
|
|
1094
|
+
await execFileAsync(probe.cmd, probe.args, {
|
|
1095
|
+
timeout: 3000,
|
|
1096
|
+
// encoding so stdout is a string we can drop without ever logging it
|
|
1097
|
+
encoding: 'utf8',
|
|
1098
|
+
});
|
|
1052
1099
|
cachedAgyKeychainSignedIn = true;
|
|
1053
1100
|
}
|
|
1054
1101
|
catch {
|
|
1102
|
+
// Missing tool (ENOENT), missing item, locked collection, timeout → signed out.
|
|
1055
1103
|
cachedAgyKeychainSignedIn = false;
|
|
1056
1104
|
}
|
|
1057
1105
|
return cachedAgyKeychainSignedIn;
|
|
@@ -1282,10 +1330,12 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1282
1330
|
// Antigravity (`agy`) stores a consumer Google OAuth grant (access +
|
|
1283
1331
|
// refresh token, no id_token) — presence of a refresh token is the only
|
|
1284
1332
|
// signed-in signal we can derive without a network call. Storage is
|
|
1285
|
-
// platform-split
|
|
1286
|
-
// ~/.gemini/antigravity-cli/antigravity-oauth-token
|
|
1287
|
-
//
|
|
1288
|
-
//
|
|
1333
|
+
// platform-split via go-keyring:
|
|
1334
|
+
// - file ~/.gemini/antigravity-cli/antigravity-oauth-token (Linux
|
|
1335
|
+
// fallback when no Secret Service is available)
|
|
1336
|
+
// - macOS keychain / Linux libsecret (service gemini + user
|
|
1337
|
+
// antigravity) when a keyring daemon is present
|
|
1338
|
+
// Check the file first, then the OS keyring probe.
|
|
1289
1339
|
const tokenPath = resolveAccountCredentialPath(base, '.gemini', 'antigravity-cli', 'antigravity-oauth-token');
|
|
1290
1340
|
if (tokenPath) {
|
|
1291
1341
|
const data = JSON.parse(await fs.promises.readFile(tokenPath, 'utf-8'));
|
|
@@ -1524,7 +1574,7 @@ export function countSessionFiles(agentId) {
|
|
|
1524
1574
|
return count;
|
|
1525
1575
|
}
|
|
1526
1576
|
/** Decode the payload section of a JWT token without verifying its signature. */
|
|
1527
|
-
function decodeJwtPayload(token) {
|
|
1577
|
+
export function decodeJwtPayload(token) {
|
|
1528
1578
|
const payload = token.split('.')[1];
|
|
1529
1579
|
if (!payload)
|
|
1530
1580
|
return null;
|
|
@@ -9,7 +9,7 @@ import { writeProfileRuntime, clearProfileRuntime } from '../runtime-state.js';
|
|
|
9
9
|
// is the shared hardened baseline (BatchMode + ConnectTimeout + keepalive) —
|
|
10
10
|
// reuse it so the raw-ssh spawns below fail fast on an unreachable host instead
|
|
11
11
|
// of hanging on the default ~127s TCP timeout, rather than re-listing options.
|
|
12
|
-
import { shellQuote, SSH_OPTS } from '../../ssh-exec.js';
|
|
12
|
+
import { shellQuote, SSH_OPTS, assertValidSshTarget } from '../../ssh-exec.js';
|
|
13
13
|
export { shellQuote };
|
|
14
14
|
// The `ssh -L` tunnel spawn is shared with `agents computer --host`; it lives in
|
|
15
15
|
// the single ssh-tunnel helper. Calling it with no options preserves this
|
|
@@ -292,6 +292,12 @@ export function buildKillCmd(remoteOs, port) {
|
|
|
292
292
|
return `pids=$(lsof -ti ${shellQuote(`:${port}`)} 2>/dev/null); [ -z "$pids" ] || kill -9 $pids 2>/dev/null || true`;
|
|
293
293
|
}
|
|
294
294
|
export async function ensureRemoteBrowser(user, host, browserType, port, remoteOs, customBinary) {
|
|
295
|
+
// `user`/`host` derive from an ssh:// profile endpoint (git-tracked user
|
|
296
|
+
// config). Validate the composed target before it reaches the raw `ssh` spawn:
|
|
297
|
+
// an endpoint like `ssh://-Fattack@victim` would otherwise place `-Fattack` at
|
|
298
|
+
// the target position, where OpenSSH parses it as `-F <file>` and an
|
|
299
|
+
// attacker-supplied ssh config's ProxyCommand yields local code execution.
|
|
300
|
+
assertValidSshTarget(`${user}@${host}`);
|
|
295
301
|
const remoteCmd = buildLaunchCmd(remoteOs, browserType, port, customBinary);
|
|
296
302
|
return new Promise((resolve, reject) => {
|
|
297
303
|
// Capture stderr so a real launch failure (missing exe, auth denied, bad
|
|
@@ -362,7 +368,18 @@ export function killRemoteBrowser(user, host, remoteOs, port) {
|
|
|
362
368
|
return runSSHCommand(user, host, buildKillCmd(remoteOs, port));
|
|
363
369
|
}
|
|
364
370
|
function runSSHCommand(user, host, cmd) {
|
|
365
|
-
return new Promise((resolve) => {
|
|
371
|
+
return new Promise((resolve, reject) => {
|
|
372
|
+
// Reject an option-injecting target (e.g. a `-`-leading user from a crafted
|
|
373
|
+
// ssh:// profile) before it reaches the raw `ssh` spawn. Reject rather than
|
|
374
|
+
// throw synchronously so `killRemoteBrowser(...).catch()` stays best-effort.
|
|
375
|
+
// See ensureRemoteBrowser.
|
|
376
|
+
try {
|
|
377
|
+
assertValidSshTarget(`${user}@${host}`);
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
reject(err);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
366
383
|
// Reuse the shared hardened baseline so a hung TCP SYN to an unreachable
|
|
367
384
|
// host is bounded by ConnectTimeout (the local 3s kill below only bounds a
|
|
368
385
|
// connected-but-slow command, not the connect itself). Options precede the
|
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>;
|