@phnx-labs/agents-cli 1.20.28 → 1.20.30
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/dist/commands/computer-actions.js +6 -2
- package/dist/commands/computer.d.ts +12 -0
- package/dist/commands/computer.js +88 -13
- package/dist/commands/exec.js +22 -10
- package/dist/commands/inspect.js +1 -1
- package/dist/commands/models.js +8 -2
- package/dist/commands/secrets.js +93 -6
- package/dist/commands/sessions.js +157 -44
- package/dist/commands/ssh.d.ts +14 -0
- package/dist/commands/ssh.js +263 -0
- package/dist/commands/sync.js +70 -14
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +0 -4
- package/dist/lib/agents.js +54 -5
- package/dist/lib/browser/drivers/ssh.js +4 -35
- package/dist/lib/computer-rpc.d.ts +6 -1
- package/dist/lib/computer-rpc.js +86 -3
- package/dist/lib/devices/connect.d.ts +34 -0
- package/dist/lib/devices/connect.js +101 -0
- package/dist/lib/devices/registry.d.ts +78 -0
- package/dist/lib/devices/registry.js +168 -0
- package/dist/lib/devices/ssh-config.d.ts +21 -0
- package/dist/lib/devices/ssh-config.js +33 -0
- package/dist/lib/devices/tailscale.d.ts +31 -0
- package/dist/lib/devices/tailscale.js +126 -0
- package/dist/lib/exec.js +14 -0
- package/dist/lib/models.js +138 -5
- package/dist/lib/runner.js +7 -7
- package/dist/lib/secrets/remote.d.ts +67 -0
- package/dist/lib/secrets/remote.js +133 -0
- package/dist/lib/session/active.d.ts +13 -0
- package/dist/lib/session/active.js +79 -18
- package/dist/lib/session/cloud.js +2 -0
- package/dist/lib/session/db.d.ts +12 -0
- package/dist/lib/session/db.js +66 -9
- package/dist/lib/session/discover.d.ts +7 -0
- package/dist/lib/session/discover.js +309 -0
- package/dist/lib/session/parse.d.ts +22 -0
- package/dist/lib/session/parse.js +132 -2
- package/dist/lib/session/remote.d.ts +1 -1
- package/dist/lib/session/remote.js +8 -3
- package/dist/lib/session/state.d.ts +82 -0
- package/dist/lib/session/state.js +221 -0
- package/dist/lib/session/tail.d.ts +18 -0
- package/dist/lib/session/tail.js +57 -0
- package/dist/lib/session/types.d.ts +10 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/session/width.d.ts +29 -0
- package/dist/lib/session/width.js +91 -0
- package/dist/lib/shims.d.ts +17 -1
- package/dist/lib/shims.js +130 -6
- package/dist/lib/ssh-tunnel.d.ts +127 -0
- package/dist/lib/ssh-tunnel.js +346 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +3 -0
- package/dist/lib/state.d.ts +4 -0
- package/dist/lib/state.js +19 -1
- package/dist/lib/teams/agents.d.ts +11 -1
- package/dist/lib/teams/agents.js +16 -2
- package/dist/lib/types.d.ts +1 -0
- package/dist/lib/versions.d.ts +19 -0
- package/dist/lib/versions.js +84 -24
- package/package.json +1 -1
|
@@ -170,11 +170,15 @@ function emit(result, json, human) {
|
|
|
170
170
|
console.log(human());
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
|
-
// Add the shared --pid/--bundle target options to a verb.
|
|
173
|
+
// Add the shared --pid/--bundle/--host target options to a verb. `--host` routes
|
|
174
|
+
// the verb at a remote Windows device: the `computer` preAction hook hydrates
|
|
175
|
+
// COMPUTER_HELPER_TCP from the tunnel `start --host` recorded, so withClient's
|
|
176
|
+
// openComputerClient() transparently selects the TCP transport.
|
|
174
177
|
function addTargetOpts(cmd) {
|
|
175
178
|
return cmd
|
|
176
179
|
.option('--bundle <id>', 'Bundle id of the target app (default: frontmost allow-listed app)')
|
|
177
|
-
.option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10))
|
|
180
|
+
.option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10))
|
|
181
|
+
.option('--host <device>', 'Drive a remote Windows device (requires `agents computer start --host <device>` first)');
|
|
178
182
|
}
|
|
179
183
|
// Add the shared --id/--x/--y element-or-coords options to a verb.
|
|
180
184
|
function addElementOrCoordOpts(cmd) {
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import { Command } from 'commander';
|
|
2
2
|
import { resolveHelperExec, resolveSocketPath } from '../lib/computer-rpc.js';
|
|
3
|
+
/**
|
|
4
|
+
* Pure platform gate. The computer subsystem is macOS-only for LOCAL driving
|
|
5
|
+
* (Accessibility / launchctl). It is NOT blocked off macOS when a remote daemon
|
|
6
|
+
* is reachable — either a configured TCP endpoint (COMPUTER_HELPER_TCP, e.g. a
|
|
7
|
+
* Windows daemon over a tunnel) or a `--host <device>` remote invocation. Kept
|
|
8
|
+
* pure so the gating rule is unit-testable without a live command tree.
|
|
9
|
+
*/
|
|
10
|
+
export declare function shouldBlockOffPlatform(opts: {
|
|
11
|
+
platform: NodeJS.Platform;
|
|
12
|
+
tcpConfigured: boolean;
|
|
13
|
+
host?: string;
|
|
14
|
+
}): boolean;
|
|
3
15
|
export declare function registerComputerCommand(program: Command): void;
|
|
4
16
|
export declare function registerComputerSubcommands(program: Command): void;
|
|
5
17
|
export { resolveHelperExec as resolveHelperPath };
|
|
@@ -3,7 +3,8 @@ import * as fs from 'fs';
|
|
|
3
3
|
import * as os from 'os';
|
|
4
4
|
import * as path from 'path';
|
|
5
5
|
import { registerCommandGroups } from '../lib/help.js';
|
|
6
|
-
import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
|
|
6
|
+
import { openComputerClient, resolveHelperApp, resolveHelperExec, resolveSocketPath, resolveLogPath, resolvePolicyPath, resolvePeersPath, resolveTcpEndpoint, loadComputerAllowList, loadDefaultPeers, writeComputerPolicy, writeComputerPeers, } from '../lib/computer-rpc.js';
|
|
7
|
+
import { setupRemoteHelper, startRemoteTunnel, stopRemoteHelper, hydrateRemoteEnvFromState, } from '../lib/ssh-tunnel.js';
|
|
7
8
|
import { registerActionCommands, withClient, unwrap, pickTarget } from './computer-actions.js';
|
|
8
9
|
// Help groups — mirror `agents browser` so the mental model carries over.
|
|
9
10
|
const COMPUTER_HELP_GROUPS = [
|
|
@@ -12,15 +13,44 @@ const COMPUTER_HELP_GROUPS = [
|
|
|
12
13
|
{ title: 'Observe', names: ['apps', 'describe', 'screenshot', 'get-text'] },
|
|
13
14
|
{ title: 'Interact', names: ['launch', 'raise', 'click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus', 'wait'] },
|
|
14
15
|
];
|
|
16
|
+
// Subcommands that manage the `--host` remote path themselves (provisioning /
|
|
17
|
+
// tunnel lifecycle). Every other `--host`-bearing subcommand is a plain verb
|
|
18
|
+
// that just needs the TCP endpoint hydrated before it runs.
|
|
19
|
+
const REMOTE_LIFECYCLE = new Set(['setup', 'start', 'stop']);
|
|
20
|
+
/**
|
|
21
|
+
* Pure platform gate. The computer subsystem is macOS-only for LOCAL driving
|
|
22
|
+
* (Accessibility / launchctl). It is NOT blocked off macOS when a remote daemon
|
|
23
|
+
* is reachable — either a configured TCP endpoint (COMPUTER_HELPER_TCP, e.g. a
|
|
24
|
+
* Windows daemon over a tunnel) or a `--host <device>` remote invocation. Kept
|
|
25
|
+
* pure so the gating rule is unit-testable without a live command tree.
|
|
26
|
+
*/
|
|
27
|
+
export function shouldBlockOffPlatform(opts) {
|
|
28
|
+
if (opts.platform === 'darwin')
|
|
29
|
+
return false;
|
|
30
|
+
if (opts.tcpConfigured)
|
|
31
|
+
return false; // remote (Windows) daemon over a tunnel
|
|
32
|
+
if (opts.host)
|
|
33
|
+
return false; // remote path resolves its own endpoint
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
15
36
|
export function registerComputerCommand(program) {
|
|
16
37
|
const computer = program
|
|
17
38
|
.command('computer')
|
|
18
|
-
.description('Drive macOS apps via Accessibility — list, screenshot, click, type
|
|
19
|
-
// The whole subsystem is macOS Accessibility / TCC
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
39
|
+
.description('Drive macOS apps via Accessibility, or a remote Windows host with --host — list, screenshot, click, type')
|
|
40
|
+
// The whole subsystem is macOS Accessibility / TCC for LOCAL driving. Off
|
|
41
|
+
// macOS it still works against a remote daemon (COMPUTER_HELPER_TCP set, or
|
|
42
|
+
// a `--host <device>` invocation). Fail fast with a clear message only when
|
|
43
|
+
// neither remote path is available, instead of a downstream launchctl error.
|
|
44
|
+
.hook('preAction', async (_thisCommand, actionCommand) => {
|
|
45
|
+
const host = actionCommand.opts().host;
|
|
46
|
+
// Verbs with --host reconnect to the tunnel `start --host` recorded; this
|
|
47
|
+
// sets COMPUTER_HELPER_TCP so the shared client picks the TCP transport.
|
|
48
|
+
if (host && !REMOTE_LIFECYCLE.has(actionCommand.name())) {
|
|
49
|
+
hydrateRemoteEnvFromState(host);
|
|
50
|
+
}
|
|
51
|
+
if (shouldBlockOffPlatform({ platform: process.platform, tcpConfigured: resolveTcpEndpoint() != null, host })) {
|
|
52
|
+
console.error('agents computer: macOS only for local driving — it uses the macOS Accessibility API.');
|
|
53
|
+
console.error('For a remote Windows host: register it with `agents devices`, then use --host (or set COMPUTER_HELPER_TCP).');
|
|
24
54
|
process.exit(1);
|
|
25
55
|
}
|
|
26
56
|
});
|
|
@@ -94,6 +124,7 @@ function registerScreenshotCommand(program) {
|
|
|
94
124
|
.description('Capture a window (default: largest), enumerate windows (--list), or the whole display (--display)')
|
|
95
125
|
.option('--bundle <id>', 'Bundle id to capture (default: frontmost allow-listed app)')
|
|
96
126
|
.option('--pid <n>', 'Target pid directly (overrides --bundle)', (v) => parseInt(v, 10))
|
|
127
|
+
.option('--host <device>', 'Drive a remote Windows device (requires `agents computer start --host <device>` first)')
|
|
97
128
|
.option('--list', 'List the app\'s windows (id/title/layer/bounds) instead of capturing — reveals modals/popups')
|
|
98
129
|
.option('--window-id <n>', 'Capture a specific window by id (from --list)', (v) => parseInt(v, 10))
|
|
99
130
|
.option('--display', 'Capture the whole display the app is on (composites stacked modals)')
|
|
@@ -181,8 +212,23 @@ function registerSetupCommand(program) {
|
|
|
181
212
|
program
|
|
182
213
|
.command('setup')
|
|
183
214
|
.alias('install-helper')
|
|
184
|
-
.description('Install
|
|
185
|
-
.
|
|
215
|
+
.description('Install the helper — locally to /Applications/ (macOS), or to a remote Windows host with --host')
|
|
216
|
+
.option('--host <device>', 'Provision a remote Windows device (push the exe + register a LOGON task) instead of installing locally')
|
|
217
|
+
.action(async (opts) => {
|
|
218
|
+
if (opts.host) {
|
|
219
|
+
try {
|
|
220
|
+
const { target, taskName } = await setupRemoteHelper(opts.host);
|
|
221
|
+
console.log(`pushed computer-helper-win.exe to ${target}`);
|
|
222
|
+
console.log(`registered LOGON scheduled task "${taskName}" (interactive session, started now)`);
|
|
223
|
+
console.log('');
|
|
224
|
+
console.log(`Next: agents computer start --host ${opts.host}`);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
console.error(`error: ${err.message}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
186
232
|
const srcApp = resolveHelperApp();
|
|
187
233
|
if (!srcApp || !fs.existsSync(srcApp)) {
|
|
188
234
|
console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
|
|
@@ -266,8 +312,24 @@ function registerSetupCommand(program) {
|
|
|
266
312
|
function registerStartCommand(program) {
|
|
267
313
|
program
|
|
268
314
|
.command('start')
|
|
269
|
-
.description('Activate the helper daemon
|
|
270
|
-
.
|
|
315
|
+
.description('Activate the helper daemon — local launchd (macOS) or a remote Windows tunnel with --host')
|
|
316
|
+
.option('--host <device>', 'Open a tunnel to the remote Windows daemon and record it for --host verbs')
|
|
317
|
+
.action(async (opts) => {
|
|
318
|
+
if (opts.host) {
|
|
319
|
+
try {
|
|
320
|
+
const state = await startRemoteTunnel(opts.host);
|
|
321
|
+
console.log(`tunnel: 127.0.0.1:${state.localPort} -> ${state.target} (127.0.0.1:${state.remotePort})`);
|
|
322
|
+
console.log(`daemon: answering (ssh pid ${state.tunnelPid})`);
|
|
323
|
+
console.log('');
|
|
324
|
+
console.log(`Drive it: agents computer apps --host ${opts.host}`);
|
|
325
|
+
console.log(`Stop: agents computer stop --host ${opts.host}`);
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
console.error(`error: ${err.message}`);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
271
333
|
const home = os.homedir();
|
|
272
334
|
const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
273
335
|
const socketPath = resolveSocketPath();
|
|
@@ -440,8 +502,21 @@ function registerReloadCommand(program) {
|
|
|
440
502
|
function registerStopCommand(program) {
|
|
441
503
|
program
|
|
442
504
|
.command('stop')
|
|
443
|
-
.description('Deactivate the helper daemon (
|
|
444
|
-
.
|
|
505
|
+
.description('Deactivate the helper daemon — local launchd (macOS) or a remote Windows tunnel with --host')
|
|
506
|
+
.option('--host <device>', 'Tear down the remote tunnel and unregister the scheduled task')
|
|
507
|
+
.action(async (opts) => {
|
|
508
|
+
if (opts.host) {
|
|
509
|
+
try {
|
|
510
|
+
const { tunnelKilled, taskRemoved } = await stopRemoteHelper(opts.host);
|
|
511
|
+
console.log(`tunnel: ${tunnelKilled ? 'closed' : 'not running'}`);
|
|
512
|
+
console.log(`task: ${taskRemoved ? 'unregistered' : 'not removed (device offline?)'}`);
|
|
513
|
+
}
|
|
514
|
+
catch (err) {
|
|
515
|
+
console.error(`error: ${err.message}`);
|
|
516
|
+
process.exit(1);
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
445
520
|
const home = os.homedir();
|
|
446
521
|
const plistPath = path.join(home, 'Library', 'LaunchAgents', `${HELPER_LABEL}.plist`);
|
|
447
522
|
const socketPath = resolveSocketPath();
|
package/dist/commands/exec.js
CHANGED
|
@@ -324,11 +324,12 @@ export function registerRunCommand(program) {
|
|
|
324
324
|
process.stderr.write(chalk.gray(`[loop] stopped: ${result.stoppedBy} after ${result.iterations} iteration(s), ${result.tokens} tokens\n`));
|
|
325
325
|
process.exit(loopExitCode(result.stoppedBy));
|
|
326
326
|
}
|
|
327
|
-
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
327
|
+
const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor, headlessPlanStallCommand, nativeResume, resolveInteractive }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { splitBundleRef, resolveSshTarget, remoteResolveEnv }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef, resolveAllowedSubagents }, { resolveRunDefaults }, { getMcpServersByName, buildWorkflowMcpConfig }, { supports },] = await Promise.all([
|
|
328
328
|
import('../lib/exec.js'),
|
|
329
329
|
import('../lib/agents.js'),
|
|
330
330
|
import('../lib/profiles.js'),
|
|
331
331
|
import('../lib/secrets/bundles.js'),
|
|
332
|
+
import('../lib/secrets/remote.js'),
|
|
332
333
|
import('../lib/rotate.js'),
|
|
333
334
|
import('../lib/versions.js'),
|
|
334
335
|
import('../lib/plugins.js'),
|
|
@@ -776,17 +777,28 @@ export function registerRunCommand(program) {
|
|
|
776
777
|
// ones. Any resolution failure (missing keychain item, blocked exec ref)
|
|
777
778
|
// aborts before spawn so the agent never sees a partial env.
|
|
778
779
|
let secretsEnv = {};
|
|
779
|
-
for (const
|
|
780
|
+
for (const bundleRef of options.secrets) {
|
|
780
781
|
try {
|
|
781
|
-
const { bundle,
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
782
|
+
const { bundle: bundleName, host } = splitBundleRef(bundleRef);
|
|
783
|
+
if (host) {
|
|
784
|
+
// Remote bundle (`bundle@host`): resolve over SSH and inject
|
|
785
|
+
// ephemerally — values never touch this machine's keychain or disk.
|
|
786
|
+
const target = await resolveSshTarget(host);
|
|
787
|
+
const bundleEnv = await remoteResolveEnv(target, bundleName);
|
|
788
|
+
console.log(chalk.gray(`[secrets] Resolved ${bundleName}@${host}: ${Object.keys(bundleEnv).length} keys (remote, ephemeral)`));
|
|
789
|
+
secretsEnv = { ...secretsEnv, ...bundleEnv };
|
|
790
|
+
}
|
|
791
|
+
else {
|
|
792
|
+
const { bundle, env: bundleEnv } = readAndResolveBundleEnv(bundleName, { caller: `agent ${agent}` });
|
|
793
|
+
const entries = describeBundle(bundle);
|
|
794
|
+
const counts = {};
|
|
795
|
+
for (const e of entries) {
|
|
796
|
+
counts[e.kind] = (counts[e.kind] || 0) + 1;
|
|
797
|
+
}
|
|
798
|
+
const breakdown = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ');
|
|
799
|
+
console.log(chalk.gray(`[secrets] Resolved ${bundleName}: ${entries.length} keys (${breakdown})`));
|
|
800
|
+
secretsEnv = { ...secretsEnv, ...bundleEnv };
|
|
786
801
|
}
|
|
787
|
-
const breakdown = Object.entries(counts).map(([k, v]) => `${v} ${k}`).join(', ');
|
|
788
|
-
console.log(chalk.gray(`[secrets] Resolved ${bundleName}: ${entries.length} keys (${breakdown})`));
|
|
789
|
-
secretsEnv = { ...secretsEnv, ...bundleEnv };
|
|
790
802
|
}
|
|
791
803
|
catch (err) {
|
|
792
804
|
console.error(chalk.red(err.message));
|
package/dist/commands/inspect.js
CHANGED
|
@@ -1217,7 +1217,7 @@ function safeStat(p) {
|
|
|
1217
1217
|
}
|
|
1218
1218
|
}
|
|
1219
1219
|
const SESSION_AGENTS = new Set([
|
|
1220
|
-
'claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi',
|
|
1220
|
+
'claude', 'codex', 'gemini', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid',
|
|
1221
1221
|
]);
|
|
1222
1222
|
function safeCountSessions(agent) {
|
|
1223
1223
|
if (!SESSION_AGENTS.has(agent))
|
package/dist/commands/models.js
CHANGED
|
@@ -11,7 +11,7 @@ import { homeDir } from '../lib/platform/index.js';
|
|
|
11
11
|
import { resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
|
|
12
12
|
import { listInstalledVersions, getGlobalDefault, resolveVersion, resolveVersionAlias } from '../lib/versions.js';
|
|
13
13
|
import { getModelCatalog, locateModelSource } from '../lib/models.js';
|
|
14
|
-
const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'cursor', 'openclaw'];
|
|
14
|
+
const MODEL_CAPABLE_AGENTS = ['claude', 'codex', 'gemini', 'opencode', 'cursor', 'openclaw', 'antigravity', 'kimi'];
|
|
15
15
|
/**
|
|
16
16
|
* Agents that don't necessarily install under ~/.agents/versions (cursor ships
|
|
17
17
|
* via a curl script). For these, fall back to the PATH binary and synthesize
|
|
@@ -72,8 +72,14 @@ async function resolveTargets(agentSpec) {
|
|
|
72
72
|
if (!version && PATH_ONLY_AGENTS.has(agent)) {
|
|
73
73
|
version = fallbackPathVersion(agent);
|
|
74
74
|
}
|
|
75
|
-
if (version)
|
|
75
|
+
if (version) {
|
|
76
76
|
targets.push({ agent, version, isDefault: true });
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
// Surface the gap instead of silently dropping the agent -- an
|
|
80
|
+
// uninstalled model-capable agent should tell the user how to add it.
|
|
81
|
+
console.error(chalk.gray(`${agentLabel(agent)}: not installed (run 'agents add ${agent}@latest')`));
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
84
|
if (targets.length === 0) {
|
|
79
85
|
console.error(chalk.yellow('No installed agent versions found. Run `agents add claude@latest` to install one.'));
|
package/dist/commands/secrets.js
CHANGED
|
@@ -10,6 +10,7 @@ import chalk from 'chalk';
|
|
|
10
10
|
import * as fs from 'fs';
|
|
11
11
|
import { spawnSync } from 'child_process';
|
|
12
12
|
import { SSH_TARGET_RE, assertValidSshTarget } from '../lib/ssh-exec.js';
|
|
13
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
13
14
|
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, readBundle, renameBundle, rotateBundleSecret, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
14
15
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
15
16
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
@@ -160,6 +161,47 @@ export function bundleEnvToDotenv(env) {
|
|
|
160
161
|
}
|
|
161
162
|
return lines.join('\n') + '\n';
|
|
162
163
|
}
|
|
164
|
+
/**
|
|
165
|
+
* Browse `agents secrets <args>` on one or more remote hosts over SSH and print
|
|
166
|
+
* each host's stdout verbatim (lossless — no parsing). With >1 host the output
|
|
167
|
+
* is grouped under a `── <host> ──` header. `tty` forces an interactive ssh
|
|
168
|
+
* session (run sequentially) so a remote Touch-ID / passphrase prompt can
|
|
169
|
+
* surface (e.g. `view --reveal`); otherwise hosts are queried in parallel.
|
|
170
|
+
* Exits non-zero if any host fails.
|
|
171
|
+
*/
|
|
172
|
+
async function browseRemote(targets, args, tty) {
|
|
173
|
+
const multi = targets.length > 1;
|
|
174
|
+
let failures = 0;
|
|
175
|
+
const render = (name, res) => {
|
|
176
|
+
if (multi)
|
|
177
|
+
console.log(chalk.bold.cyan(`\n── ${name} ──`));
|
|
178
|
+
if (res.code === 0) {
|
|
179
|
+
if (res.stdout)
|
|
180
|
+
process.stdout.write(res.stdout.endsWith('\n') ? res.stdout : `${res.stdout}\n`);
|
|
181
|
+
if (res.stderr.trim())
|
|
182
|
+
process.stderr.write(chalk.gray(res.stderr));
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
failures++;
|
|
186
|
+
const msg = (res.stderr || res.stdout || '').trim();
|
|
187
|
+
const why = res.timedOut ? 'timed out' : res.code === null ? 'ssh failed' : `exit ${res.code}`;
|
|
188
|
+
console.error(chalk.red(`${name}: ${why}${msg ? `: ${msg}` : ''}`));
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
if (tty) {
|
|
192
|
+
for (const t of targets) {
|
|
193
|
+
const target = await resolveSshTarget(t);
|
|
194
|
+
render(t, remoteSecretsRaw(target, args, { tty: true }));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const resolved = await Promise.all(targets.map((t) => resolveSshTarget(t)));
|
|
199
|
+
const results = resolved.map((target) => remoteSecretsRaw(target, args));
|
|
200
|
+
targets.forEach((t, i) => render(t, results[i]));
|
|
201
|
+
}
|
|
202
|
+
if (failures > 0)
|
|
203
|
+
process.exit(1);
|
|
204
|
+
}
|
|
163
205
|
/** Strip ANSI escape sequences so padding can be computed on visible width. */
|
|
164
206
|
function visibleWidth(s) {
|
|
165
207
|
// eslint-disable-next-line no-control-regex
|
|
@@ -450,8 +492,15 @@ export function registerSecretsCommands(program) {
|
|
|
450
492
|
cmd
|
|
451
493
|
.command('list')
|
|
452
494
|
.alias('ls')
|
|
453
|
-
.description('List configured secrets bundles')
|
|
454
|
-
.
|
|
495
|
+
.description('List configured secrets bundles (use --host/--hosts to list bundles on other machines over SSH)')
|
|
496
|
+
.option('--host <target>', 'List bundles on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
497
|
+
.option('--hosts <list>', 'Comma-separated hosts to list in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
498
|
+
.action(async (opts) => {
|
|
499
|
+
const targets = parseHostsOption(opts);
|
|
500
|
+
if (targets.length > 0) {
|
|
501
|
+
await browseRemote(targets, ['list'], false);
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
455
504
|
const bundles = listBundles();
|
|
456
505
|
if (bundles.length === 0) {
|
|
457
506
|
console.log(chalk.gray('No secrets bundles configured.'));
|
|
@@ -481,8 +530,28 @@ export function registerSecretsCommands(program) {
|
|
|
481
530
|
.description('Show a bundle. Keychain values are masked by default — pass --reveal to see them.')
|
|
482
531
|
.option('--reveal', 'Print keychain-backed values in the clear (TTY only unless --plaintext)')
|
|
483
532
|
.option('--plaintext', 'Allow --reveal in non-interactive shells (use with care)')
|
|
533
|
+
.option('--host <target>', 'Show a bundle on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
534
|
+
.option('--hosts <list>', 'Comma-separated hosts to show in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
484
535
|
.action(async (name, opts) => {
|
|
485
536
|
try {
|
|
537
|
+
const targets = parseHostsOption(opts);
|
|
538
|
+
if (targets.length > 0) {
|
|
539
|
+
if (!name) {
|
|
540
|
+
console.error(chalk.red('A bundle name is required when viewing a remote host (interactive pick needs a local terminal).'));
|
|
541
|
+
process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
const args = ['view', name];
|
|
544
|
+
if (opts.reveal)
|
|
545
|
+
args.push('--reveal');
|
|
546
|
+
if (opts.plaintext)
|
|
547
|
+
args.push('--plaintext');
|
|
548
|
+
// With --reveal, force a TTY so the remote keychain prompt can surface
|
|
549
|
+
// (and the remote's "--reveal in a non-TTY needs --plaintext" gate is
|
|
550
|
+
// satisfied) — only when this side is itself interactive.
|
|
551
|
+
const tty = Boolean(opts.reveal) && Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
|
|
552
|
+
await browseRemote(targets, args, tty);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
486
555
|
const resolvedName = name ?? (await pickBundleName('view'));
|
|
487
556
|
const bundle = readBundle(resolvedName);
|
|
488
557
|
const entries = describeBundle(bundle);
|
|
@@ -1094,6 +1163,7 @@ Examples:
|
|
|
1094
1163
|
.option('--host <target...>', 'Push the bundle over SSH to this target (host alias or user@host); repeatable for multiple machines')
|
|
1095
1164
|
.option('--remote-backend <backend>', 'Backend for the bundle on the remote (with --host): keychain (default) or file (passphrase-encrypted, headless-readable). file forwards AGENTS_SECRETS_PASSPHRASE over stdin.', 'keychain')
|
|
1096
1165
|
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
1166
|
+
.option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
|
|
1097
1167
|
.action(async (bundleName, opts) => {
|
|
1098
1168
|
try {
|
|
1099
1169
|
const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
@@ -1204,11 +1274,21 @@ Examples:
|
|
|
1204
1274
|
console.log(chalk.green(`Exported to 1Password vault '${vault}': ${parts.join(', ')}.`));
|
|
1205
1275
|
return;
|
|
1206
1276
|
}
|
|
1277
|
+
if (opts.format && opts.format !== 'shell' && opts.format !== 'json') {
|
|
1278
|
+
console.error(chalk.red(`Invalid --format ${JSON.stringify(opts.format)}. Expected 'shell' or 'json'.`));
|
|
1279
|
+
process.exit(1);
|
|
1280
|
+
}
|
|
1207
1281
|
if (!opts.plaintext) {
|
|
1208
1282
|
console.error(chalk.red('export prints secrets in the clear and requires --plaintext (works for TTY and pipes alike).'));
|
|
1209
1283
|
process.exit(1);
|
|
1210
1284
|
}
|
|
1211
1285
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `export to shell` });
|
|
1286
|
+
if (opts.format === 'json') {
|
|
1287
|
+
// Lossless, machine-readable form consumed by `remoteResolveEnv` over
|
|
1288
|
+
// SSH. Single object of KEY -> value; values verbatim (newlines, quotes).
|
|
1289
|
+
process.stdout.write(JSON.stringify(env));
|
|
1290
|
+
return;
|
|
1291
|
+
}
|
|
1212
1292
|
const prefix = bundleToEnvPrefix(resolvedBundleName);
|
|
1213
1293
|
for (const [k, v] of Object.entries(env)) {
|
|
1214
1294
|
const exportKey = isReservedEnvName(k) ? `${prefix}_${k}` : k;
|
|
@@ -1226,17 +1306,24 @@ Examples:
|
|
|
1226
1306
|
});
|
|
1227
1307
|
cmd
|
|
1228
1308
|
.command('exec <bundle> [command...]')
|
|
1229
|
-
.description('Run a command with the bundle\'s secrets injected into the environment')
|
|
1309
|
+
.description('Run a command with the bundle\'s secrets injected into the environment (use --host to resolve the bundle from a remote machine, ephemerally)')
|
|
1310
|
+
.option('--host <target>', 'Resolve <bundle> on a remote host over SSH and inject it (ephemeral — never stored on this machine)')
|
|
1230
1311
|
.allowUnknownOption()
|
|
1231
|
-
.action(async (bundleName, commandParts) => {
|
|
1312
|
+
.action(async (bundleName, commandParts, execOpts) => {
|
|
1232
1313
|
try {
|
|
1233
1314
|
if (commandParts.length === 0) {
|
|
1234
1315
|
console.error(chalk.red('Usage: agents secrets exec <bundle> -- <command...>'));
|
|
1235
1316
|
process.exit(1);
|
|
1236
1317
|
}
|
|
1237
|
-
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
1238
1318
|
const [cmd, ...args] = commandParts;
|
|
1239
|
-
|
|
1319
|
+
let secretEnv;
|
|
1320
|
+
if (execOpts.host) {
|
|
1321
|
+
secretEnv = await remoteResolveEnv(await resolveSshTarget(execOpts.host), bundleName);
|
|
1322
|
+
}
|
|
1323
|
+
else {
|
|
1324
|
+
const { readAndResolveBundleEnv } = await import('../lib/secrets/bundles.js');
|
|
1325
|
+
secretEnv = readAndResolveBundleEnv(bundleName, { caller: `command ${cmd}` }).env;
|
|
1326
|
+
}
|
|
1240
1327
|
const { spawn } = await import('child_process');
|
|
1241
1328
|
const proc = spawn(cmd, args, {
|
|
1242
1329
|
stdio: 'inherit',
|