@phnx-labs/agents-cli 1.20.72 → 1.20.73
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 +299 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.js +18 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/index.js +79 -10
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.js +16 -5
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +9 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +291 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +9 -0
- package/dist/lib/shims.js +91 -13
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +6 -1
- package/dist/lib/startup/command-registry.js +17 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +73 -0
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +11 -0
- package/dist/lib/versions.js +208 -76
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
package/dist/commands/secrets.js
CHANGED
|
@@ -10,13 +10,15 @@ import chalk from 'chalk';
|
|
|
10
10
|
import { padVisible, readStdinSync } from '../lib/format.js';
|
|
11
11
|
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
12
12
|
import * as fs from 'fs';
|
|
13
|
+
import * as os from 'os';
|
|
14
|
+
import * as path from 'path';
|
|
13
15
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
14
16
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
15
17
|
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
16
18
|
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveSshTarget, } from '../lib/secrets/remote.js';
|
|
17
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
18
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
19
|
-
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, } from '../lib/secrets/bundles.js';
|
|
21
|
+
import { bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, } from '../lib/secrets/bundles.js';
|
|
20
22
|
import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore.js';
|
|
21
23
|
import { getKeychainToken, getKeychainTokens, hasKeychainToken, secretsKeychainItem, setKeychainToken, } from '../lib/secrets/index.js';
|
|
22
24
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
@@ -29,6 +31,7 @@ import { emit } from '../lib/events.js';
|
|
|
29
31
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
30
32
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
31
33
|
import { discoverSyncedBundles, importSyncedBundle, } from '../lib/secrets/icloud-import.js';
|
|
34
|
+
import { getVaultSession, vaultExists } from '../lib/secrets/vault.js';
|
|
32
35
|
import { registerSecretsSyncCommands } from './secrets-sync.js';
|
|
33
36
|
import { registerSecretsMigrateAclCommand } from './secrets-migrate.js';
|
|
34
37
|
import { registerSecretsImportKeyringCommand } from './secrets-import.js';
|
|
@@ -495,8 +498,12 @@ function renderBundleRow(b, held, cols = terminalWidth()) {
|
|
|
495
498
|
`${padVisible(created, 9)} ` +
|
|
496
499
|
`${padVisible(updated, 9)} ` +
|
|
497
500
|
`${padVisible(used, 7)}`;
|
|
498
|
-
// Mark
|
|
499
|
-
const tag = b.backend === 'file'
|
|
501
|
+
// Mark non-keychain bundles so `list` distinguishes storage at a glance.
|
|
502
|
+
const tag = b.backend === 'file'
|
|
503
|
+
? chalk.magenta('[file] ')
|
|
504
|
+
: b.backend === 'vault'
|
|
505
|
+
? chalk.blue('[synced] ')
|
|
506
|
+
: '';
|
|
500
507
|
// Cap the free-form description to whatever space is left on the line so a long
|
|
501
508
|
// description can't push the row to 200+ chars and wrap into a smear.
|
|
502
509
|
const budget = cols - stringWidth(head) - 1 - stringWidth(tag);
|
|
@@ -510,7 +517,11 @@ function renderBundleRow(b, held, cols = terminalWidth()) {
|
|
|
510
517
|
function renderBundleCard(b, held, cols) {
|
|
511
518
|
const keys = describeBundle(b).length;
|
|
512
519
|
const used = b.last_used ? relativeAge(b.last_used) : (b.created_at ? 'never' : '?');
|
|
513
|
-
const tag = b.backend === 'file'
|
|
520
|
+
const tag = b.backend === 'file'
|
|
521
|
+
? chalk.magenta(' [file]')
|
|
522
|
+
: b.backend === 'vault'
|
|
523
|
+
? chalk.blue(' [synced]')
|
|
524
|
+
: '';
|
|
514
525
|
const meta = chalk.gray(`${keys} key${keys === 1 ? '' : 's'} · `) + renderPolicyCol(b, held) + chalk.gray(` · used ${used}`);
|
|
515
526
|
const line1 = `${chalk.cyan(b.name)} ${meta}${tag}`;
|
|
516
527
|
if (!b.description)
|
|
@@ -643,17 +654,17 @@ function countExpiringSoon(meta) {
|
|
|
643
654
|
* keychain-backed bundle to `file` — shared by every `import` source so the
|
|
644
655
|
* guard can't drift between them.
|
|
645
656
|
*/
|
|
646
|
-
function resolveImportBundle(name, backendOpt) {
|
|
647
|
-
const requestedBackend = parseBackendOpt(backendOpt);
|
|
657
|
+
function resolveImportBundle(name, backendOpt, synced = false) {
|
|
658
|
+
const requestedBackend = synced ? 'vault' : parseBackendOpt(backendOpt);
|
|
648
659
|
if (bundleExists(name)) {
|
|
649
660
|
const bundle = readBundle(name);
|
|
650
|
-
if (requestedBackend
|
|
651
|
-
throw new Error(`Bundle '${name}' already exists with a
|
|
652
|
-
|
|
661
|
+
if (requestedBackend !== 'keychain' && bundle.backend !== requestedBackend) {
|
|
662
|
+
throw new Error(`Bundle '${name}' already exists with a different backend; ` +
|
|
663
|
+
`delete it first to recreate it as ${requestedBackend === 'vault' ? 'synced' : `${requestedBackend}-backed`}.`);
|
|
653
664
|
}
|
|
654
665
|
return bundle;
|
|
655
666
|
}
|
|
656
|
-
return { name, backend: requestedBackend === '
|
|
667
|
+
return { name, backend: requestedBackend === 'keychain' ? undefined : requestedBackend, vars: {} };
|
|
657
668
|
}
|
|
658
669
|
/**
|
|
659
670
|
* Apply KEY=VALUE entries into a bundle (keychain item or plaintext literal),
|
|
@@ -661,7 +672,7 @@ function resolveImportBundle(name, backendOpt) {
|
|
|
661
672
|
* the .env, --from-file, and --from-ssh import paths.
|
|
662
673
|
*/
|
|
663
674
|
function applyEnvToBundle(bundle, env, opts) {
|
|
664
|
-
const
|
|
675
|
+
const storedItems = new Map();
|
|
665
676
|
let added = 0;
|
|
666
677
|
let skipped = 0;
|
|
667
678
|
for (const [key, value] of Object.entries(env)) {
|
|
@@ -674,12 +685,17 @@ function applyEnvToBundle(bundle, env, opts) {
|
|
|
674
685
|
}
|
|
675
686
|
else {
|
|
676
687
|
const item = secretsKeychainItem(bundle.name, key);
|
|
677
|
-
|
|
688
|
+
storedItems.set(item, value);
|
|
678
689
|
bundle.vars[key] = keychainRef(key);
|
|
679
690
|
}
|
|
680
691
|
added++;
|
|
681
692
|
}
|
|
682
|
-
|
|
693
|
+
if (storedItems.size > 0) {
|
|
694
|
+
writeBundleWithItems(bundle, storedItems);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
writeBundle(bundle);
|
|
698
|
+
}
|
|
683
699
|
return { added, skipped };
|
|
684
700
|
}
|
|
685
701
|
/** Register the `agents secrets` command tree. */
|
|
@@ -759,18 +775,14 @@ export function registerSecretsCommands(program) {
|
|
|
759
775
|
.description('List configured secrets bundles (use --host/--hosts to list bundles on other machines over SSH)')
|
|
760
776
|
.option('--host <target>', 'List bundles on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
761
777
|
.option('--hosts <list>', 'Comma-separated hosts to list in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
778
|
+
.option('--json', 'Emit machine-readable JSON (bundle metadata only — never secret values) instead of the table')
|
|
762
779
|
.action(async (opts) => {
|
|
763
780
|
const targets = parseHostsOption(opts);
|
|
764
781
|
if (targets.length > 0) {
|
|
765
|
-
await browseRemote(targets, ['list'], false);
|
|
782
|
+
await browseRemote(targets, opts.json ? ['list', '--json'] : ['list'], false);
|
|
766
783
|
return;
|
|
767
784
|
}
|
|
768
785
|
const bundles = listBundles();
|
|
769
|
-
if (bundles.length === 0) {
|
|
770
|
-
console.log(chalk.gray('No secrets bundles configured.'));
|
|
771
|
-
console.log(chalk.gray('Try: agents secrets create <name>'));
|
|
772
|
-
return;
|
|
773
|
-
}
|
|
774
786
|
// Cross-reference the secrets-agent so `daily` bundles that are currently
|
|
775
787
|
// held can show "· held Nh". Soft-fails to no hint if the broker is down.
|
|
776
788
|
const held = new Map();
|
|
@@ -783,6 +795,31 @@ export function registerSecretsCommands(program) {
|
|
|
783
795
|
/* broker not running — render policy without the countdown */
|
|
784
796
|
}
|
|
785
797
|
}
|
|
798
|
+
if (opts.json) {
|
|
799
|
+
// Discovery payload for agents: metadata only, no secret values. Gated on
|
|
800
|
+
// the explicit --json flag (not stdout.isTTY) so piping the human table to
|
|
801
|
+
// a pager never silently swaps formats.
|
|
802
|
+
const payload = bundles.map((b) => ({
|
|
803
|
+
name: b.name,
|
|
804
|
+
keys: describeBundle(b).length,
|
|
805
|
+
policy: bundlePolicy(b),
|
|
806
|
+
backend: b.backend === 'file' ? 'file' : 'keychain',
|
|
807
|
+
allowExec: Boolean(b.allow_exec),
|
|
808
|
+
expiringSoon: countExpiringSoon(b.meta),
|
|
809
|
+
description: b.description ?? null,
|
|
810
|
+
createdAt: b.created_at ?? null,
|
|
811
|
+
updatedAt: b.updated_at ?? null,
|
|
812
|
+
lastUsed: b.last_used ?? null,
|
|
813
|
+
heldExpiresAt: held.get(b.name) ?? null,
|
|
814
|
+
}));
|
|
815
|
+
process.stdout.write(JSON.stringify(payload) + '\n');
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (bundles.length === 0) {
|
|
819
|
+
console.log(chalk.gray('No secrets bundles configured.'));
|
|
820
|
+
console.log(chalk.gray('Try: agents secrets create <name>'));
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
786
823
|
const cols = terminalWidth();
|
|
787
824
|
if (cols >= SECRETS_WIDE) {
|
|
788
825
|
console.log(chalk.bold(`${'NAME'.padEnd(20)} ${'KEYS'.padEnd(5)} ${'POLICY'.padEnd(18)} ${'EXPIRING'.padEnd(9)} ${'CREATED'.padEnd(9)} ${'UPDATED'.padEnd(9)} ${'USED'.padEnd(7)} DESCRIPTION`));
|
|
@@ -802,6 +839,7 @@ export function registerSecretsCommands(program) {
|
|
|
802
839
|
.description('Show a bundle. Keychain values are masked by default — pass --reveal to see them.')
|
|
803
840
|
.option('--reveal', 'Print keychain-backed values in the clear (TTY only unless --plaintext)')
|
|
804
841
|
.option('--plaintext', 'Allow --reveal in non-interactive shells (use with care)')
|
|
842
|
+
.option('--json', 'Emit machine-readable JSON (values masked unless --reveal) instead of the human view')
|
|
805
843
|
.option('--host <target>', 'Show a bundle on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
806
844
|
.option('--hosts <list>', 'Comma-separated hosts to show in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
807
845
|
.action(async (name, opts) => {
|
|
@@ -817,6 +855,8 @@ export function registerSecretsCommands(program) {
|
|
|
817
855
|
args.push('--reveal');
|
|
818
856
|
if (opts.plaintext)
|
|
819
857
|
args.push('--plaintext');
|
|
858
|
+
if (opts.json)
|
|
859
|
+
args.push('--json');
|
|
820
860
|
// With --reveal, force a TTY so the remote keychain prompt can surface
|
|
821
861
|
// (and the remote's "--reveal in a non-TTY needs --plaintext" gate is
|
|
822
862
|
// satisfied) — only when this side is itself interactive.
|
|
@@ -835,6 +875,76 @@ export function registerSecretsCommands(program) {
|
|
|
835
875
|
process.exit(1);
|
|
836
876
|
}
|
|
837
877
|
const entries = describeBundle(bundle);
|
|
878
|
+
if (opts.json) {
|
|
879
|
+
// Machine-readable discovery for agents. Values are null unless --reveal
|
|
880
|
+
// (which still routes through the same non-TTY/plaintext gate + audit
|
|
881
|
+
// event as the human path). Gated on the explicit flag, not stdout.isTTY,
|
|
882
|
+
// so a piped human view never silently swaps formats.
|
|
883
|
+
const reveal = Boolean(opts.reveal);
|
|
884
|
+
if (reveal && !isInteractiveTerminal() && !opts.plaintext) {
|
|
885
|
+
console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
|
|
886
|
+
process.exit(1);
|
|
887
|
+
}
|
|
888
|
+
const revealed = new Map();
|
|
889
|
+
if (reveal) {
|
|
890
|
+
const items = entries
|
|
891
|
+
.filter((e) => e.kind === 'keychain')
|
|
892
|
+
.map((e) => secretsKeychainItem(bundle.name, e.detail));
|
|
893
|
+
try {
|
|
894
|
+
for (const [item, value] of getKeychainTokens(items))
|
|
895
|
+
revealed.set(item, value);
|
|
896
|
+
}
|
|
897
|
+
catch {
|
|
898
|
+
/* cancelled / batch failure — fall through to masked (null) values */
|
|
899
|
+
}
|
|
900
|
+
const exposed = revealed.size + entries.filter((e) => e.kind === 'literal').length;
|
|
901
|
+
if (exposed > 0) {
|
|
902
|
+
emit('secrets.get', {
|
|
903
|
+
module: 'secrets',
|
|
904
|
+
bundle: bundle.name,
|
|
905
|
+
operation: 'view --reveal --json',
|
|
906
|
+
source: 'reveal',
|
|
907
|
+
status: 'success',
|
|
908
|
+
keyCount: exposed,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
const keys = entries.map((e) => {
|
|
913
|
+
const row = {
|
|
914
|
+
key: e.key,
|
|
915
|
+
kind: e.kind,
|
|
916
|
+
detail: e.detail || null,
|
|
917
|
+
value: null,
|
|
918
|
+
};
|
|
919
|
+
if (e.kind === 'keychain') {
|
|
920
|
+
const item = secretsKeychainItem(bundle.name, e.detail);
|
|
921
|
+
row.stored = hasKeychainToken(item);
|
|
922
|
+
if (reveal && revealed.has(item))
|
|
923
|
+
row.value = revealed.get(item);
|
|
924
|
+
}
|
|
925
|
+
else if (e.kind === 'literal' && reveal) {
|
|
926
|
+
const raw = bundle.vars[e.key];
|
|
927
|
+
row.value =
|
|
928
|
+
typeof raw === 'string'
|
|
929
|
+
? raw
|
|
930
|
+
: (raw && typeof raw === 'object' && 'value' in raw ? raw.value : '');
|
|
931
|
+
}
|
|
932
|
+
return row;
|
|
933
|
+
});
|
|
934
|
+
process.stdout.write(JSON.stringify({
|
|
935
|
+
name: bundle.name,
|
|
936
|
+
description: bundle.description ?? null,
|
|
937
|
+
policy: bundlePolicy(bundle),
|
|
938
|
+
backend: bundle.backend === 'file' ? 'file' : 'keychain',
|
|
939
|
+
allowExec: Boolean(bundle.allow_exec),
|
|
940
|
+
createdAt: bundle.created_at ?? null,
|
|
941
|
+
updatedAt: bundle.updated_at ?? null,
|
|
942
|
+
lastUsed: bundle.last_used ?? null,
|
|
943
|
+
revealed: reveal,
|
|
944
|
+
keys,
|
|
945
|
+
}) + '\n');
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
838
948
|
console.log(chalk.bold(bundle.name));
|
|
839
949
|
if (bundle.description)
|
|
840
950
|
console.log(chalk.gray(safePrint(bundle.description)));
|
|
@@ -842,6 +952,8 @@ export function registerSecretsCommands(program) {
|
|
|
842
952
|
console.log(chalk.yellow('allow_exec: true'));
|
|
843
953
|
if (bundle.backend === 'file')
|
|
844
954
|
console.log(chalk.gray('backend: file (passphrase-encrypted; reads need AGENTS_SECRETS_PASSPHRASE, no Touch ID)'));
|
|
955
|
+
if (bundle.backend === 'vault')
|
|
956
|
+
console.log(chalk.gray('storage: synced (age-encrypted ~/.agents/vault.age; needs agents login)'));
|
|
845
957
|
if (bundlePolicy(bundle) === 'never') {
|
|
846
958
|
console.log(chalk.red.bold('policy: never — NO biometry ACL; reads are silent (no Touch ID, no user-presence check). Automation-only.'));
|
|
847
959
|
}
|
|
@@ -866,15 +978,18 @@ export function registerSecretsCommands(program) {
|
|
|
866
978
|
console.error(chalk.red('--reveal in a non-TTY requires --plaintext.'));
|
|
867
979
|
process.exit(1);
|
|
868
980
|
}
|
|
869
|
-
// Batch every
|
|
870
|
-
// Touch ID once
|
|
981
|
+
// Batch every backend read into one helper call where supported, so
|
|
982
|
+
// keychain --reveal pops Touch ID once and synced/file bundles use
|
|
983
|
+
// their declared storage.
|
|
871
984
|
const revealedValues = new Map();
|
|
872
985
|
if (reveal) {
|
|
873
986
|
const items = entries
|
|
874
987
|
.filter((e) => e.kind === 'keychain')
|
|
875
988
|
.map((e) => secretsKeychainItem(bundle.name, e.detail));
|
|
876
989
|
try {
|
|
877
|
-
const fetched =
|
|
990
|
+
const fetched = bundle.backend
|
|
991
|
+
? new Map(items.map((item) => [item, bundleItemStore(bundle.backend).get(item)]))
|
|
992
|
+
: getKeychainTokens(items);
|
|
878
993
|
for (const [item, value] of fetched)
|
|
879
994
|
revealedValues.set(item, value);
|
|
880
995
|
}
|
|
@@ -905,7 +1020,7 @@ export function registerSecretsCommands(program) {
|
|
|
905
1020
|
for (const e of entries) {
|
|
906
1021
|
if (e.kind === 'keychain') {
|
|
907
1022
|
const item = secretsKeychainItem(bundle.name, e.detail);
|
|
908
|
-
const stored =
|
|
1023
|
+
const stored = bundleItemStore(bundle.backend).has(item);
|
|
909
1024
|
const marker = stored ? chalk.green('stored') : chalk.red('missing');
|
|
910
1025
|
let valueCol = `[keychain:${e.detail}] ${marker}`;
|
|
911
1026
|
if (reveal && revealedValues.has(item)) {
|
|
@@ -964,14 +1079,14 @@ export function registerSecretsCommands(program) {
|
|
|
964
1079
|
// Ungated like the raw path (it IS the automation primitive); the
|
|
965
1080
|
// `secrets.get` audit event is emitted inside readAndResolveBundleEnv.
|
|
966
1081
|
try {
|
|
967
|
-
if (!bundleExists(item)) {
|
|
1082
|
+
if (!bundleExists(item) && !(vaultExists() && !getVaultSession().loggedIn)) {
|
|
968
1083
|
console.error(chalk.red(`Secrets bundle '${item}' not found.`));
|
|
969
1084
|
process.exit(1);
|
|
970
1085
|
}
|
|
971
1086
|
// `secrets get` is the scriptable automation primitive ($(agents secrets
|
|
972
1087
|
// get bundle KEY)); when embedded in a headless routine/CI script it must
|
|
973
1088
|
// not pop an unwatched Touch ID prompt. Interactive use still prompts.
|
|
974
|
-
const { env } = readAndResolveBundleEnv(item, { caller: 'secrets get', keys: [key], agentOnly: isHeadlessSecretsContext() });
|
|
1089
|
+
const { env } = readAndResolveBundleEnv(item, { caller: 'secrets get', keys: [key], keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
|
|
975
1090
|
if (!(key in env)) {
|
|
976
1091
|
console.error(chalk.red(`Key '${key}' not in bundle '${item}'.`));
|
|
977
1092
|
process.exit(1);
|
|
@@ -1026,7 +1141,8 @@ export function registerSecretsCommands(program) {
|
|
|
1026
1141
|
.option('--policy <policy>', 'prompt policy: daily (default, ask once a week), always (ask every time), or never (silent, NO biometry ACL — needs --i-understand)')
|
|
1027
1142
|
.addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
|
|
1028
1143
|
.option('--i-understand', 'Confirm creating a "never"-policy bundle (no biometry ACL) without an interactive prompt')
|
|
1029
|
-
.option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted
|
|
1144
|
+
.option('--backend <backend>', 'storage backend: keychain (default) or file (passphrase-encrypted)', 'keychain')
|
|
1145
|
+
.option('--synced', 'Store this bundle in the age-encrypted synced secrets file for user-managed cross-machine file sync')
|
|
1030
1146
|
.option('--force', 'Overwrite an existing bundle')
|
|
1031
1147
|
.action(async (name, opts) => {
|
|
1032
1148
|
try {
|
|
@@ -1036,7 +1152,7 @@ export function registerSecretsCommands(program) {
|
|
|
1036
1152
|
// inherits the configured default (`daily`) instead of being pinned.
|
|
1037
1153
|
const policyOpt = opts.policy ?? opts.tier;
|
|
1038
1154
|
const policy = policyOpt ? parsePolicyOpt(policyOpt) : undefined;
|
|
1039
|
-
const backend = parseBackendOpt(opts.backend);
|
|
1155
|
+
const backend = opts.synced ? 'vault' : parseBackendOpt(opts.backend);
|
|
1040
1156
|
if (bundleExists(resolvedName) && !opts.force) {
|
|
1041
1157
|
console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
|
|
1042
1158
|
process.exit(1);
|
|
@@ -1052,7 +1168,7 @@ export function registerSecretsCommands(program) {
|
|
|
1052
1168
|
name: resolvedName,
|
|
1053
1169
|
description: opts.description,
|
|
1054
1170
|
allow_exec: opts.allowExec,
|
|
1055
|
-
backend: backend === '
|
|
1171
|
+
backend: backend === 'keychain' ? undefined : backend,
|
|
1056
1172
|
policy,
|
|
1057
1173
|
vars: {},
|
|
1058
1174
|
};
|
|
@@ -1062,7 +1178,11 @@ export function registerSecretsCommands(program) {
|
|
|
1062
1178
|
: bundlePolicy(bundle) === 'always'
|
|
1063
1179
|
? 'policy: always ask'
|
|
1064
1180
|
: 'policy: never (NO biometry ACL)';
|
|
1065
|
-
const tags = [
|
|
1181
|
+
const tags = [
|
|
1182
|
+
policyTag,
|
|
1183
|
+
backend === 'file' ? 'backend: file' : null,
|
|
1184
|
+
backend === 'vault' ? 'synced' : null,
|
|
1185
|
+
].filter(Boolean);
|
|
1066
1186
|
console.log(chalk.green(`Bundle '${resolvedName}' created (${tags.join(', ')}).`));
|
|
1067
1187
|
if (bundlePolicy(bundle) === 'never') {
|
|
1068
1188
|
console.log(chalk.red('Stored without biometry protection — reads are silent. Automation-only; rotate anything sensitive out of it.'));
|
|
@@ -1070,6 +1190,9 @@ export function registerSecretsCommands(program) {
|
|
|
1070
1190
|
if (backend === 'file') {
|
|
1071
1191
|
console.log(chalk.gray('File-backed: items are AES-256-GCM encrypted under AGENTS_SECRETS_PASSPHRASE (no Touch ID).'));
|
|
1072
1192
|
}
|
|
1193
|
+
if (backend === 'vault') {
|
|
1194
|
+
console.log(chalk.gray('Synced: items are encrypted in ~/.agents/vault.age. Copy that file with your sync tool of choice.'));
|
|
1195
|
+
}
|
|
1073
1196
|
console.log(chalk.gray(`Try: agents secrets add ${resolvedName} MY_KEY`));
|
|
1074
1197
|
}
|
|
1075
1198
|
catch (err) {
|
|
@@ -1200,11 +1323,14 @@ export function registerSecretsCommands(program) {
|
|
|
1200
1323
|
secretValue = await promptForSecret(`Enter value for ${resolvedBundleName}.${resolvedKey}`);
|
|
1201
1324
|
}
|
|
1202
1325
|
const item = secretsKeychainItem(resolvedBundleName, resolvedKey);
|
|
1203
|
-
bundleItemStore(bundle.backend, { noAcl: bundlePolicy(bundle) === 'never' }).set(item, secretValue);
|
|
1204
1326
|
bundle.vars[resolvedKey] = keychainRef(resolvedKey);
|
|
1205
1327
|
applyMeta();
|
|
1206
|
-
|
|
1207
|
-
const where = bundle.backend === 'file'
|
|
1328
|
+
writeBundleWithItems(bundle, new Map([[item, secretValue]]));
|
|
1329
|
+
const where = bundle.backend === 'file'
|
|
1330
|
+
? 'encrypted file store'
|
|
1331
|
+
: bundle.backend === 'vault'
|
|
1332
|
+
? 'synced secrets file'
|
|
1333
|
+
: 'keychain';
|
|
1208
1334
|
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} stored in ${where} (${item}).`));
|
|
1209
1335
|
}
|
|
1210
1336
|
catch (err) {
|
|
@@ -1265,7 +1391,8 @@ Examples:
|
|
|
1265
1391
|
clearMeta: opts.clearMeta,
|
|
1266
1392
|
meta: metaPatch,
|
|
1267
1393
|
});
|
|
1268
|
-
|
|
1394
|
+
const where = bundle.backend === 'vault' ? 'synced secrets file' : bundle.backend === 'file' ? 'encrypted file store' : 'keychain';
|
|
1395
|
+
console.log(chalk.green(`${resolvedBundleName}.${resolvedKey} rotated in ${where}.`));
|
|
1269
1396
|
}
|
|
1270
1397
|
catch (err) {
|
|
1271
1398
|
if (isPromptCancelled(err))
|
|
@@ -1312,7 +1439,11 @@ Examples:
|
|
|
1312
1439
|
const item = secretsKeychainItem(resolvedBundleName, raw.slice('keychain:'.length));
|
|
1313
1440
|
const removed = bundleItemStore(bundle.backend).delete(item);
|
|
1314
1441
|
if (removed) {
|
|
1315
|
-
const where = bundle.backend === 'file'
|
|
1442
|
+
const where = bundle.backend === 'file'
|
|
1443
|
+
? 'encrypted file item'
|
|
1444
|
+
: bundle.backend === 'vault'
|
|
1445
|
+
? 'synced secrets item'
|
|
1446
|
+
: 'keychain item';
|
|
1316
1447
|
console.log(chalk.green(`Removed ${resolvedBundleName}.${resolvedKey} and purged ${where}.`));
|
|
1317
1448
|
return;
|
|
1318
1449
|
}
|
|
@@ -1415,7 +1546,8 @@ Examples:
|
|
|
1415
1546
|
.addOption(new Option('--from-1password', 'deprecated alias for --from 1password:<vault>').hideHelp())
|
|
1416
1547
|
.addOption(new Option('--vault <name>', 'deprecated: name the vault in --from 1password:<vault>').hideHelp())
|
|
1417
1548
|
.option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
|
|
1418
|
-
.option('--backend <backend>', 'When creating the bundle: keychain (default) or file (passphrase-encrypted
|
|
1549
|
+
.option('--backend <backend>', 'When creating the bundle: keychain (default) or file (passphrase-encrypted)', 'keychain')
|
|
1550
|
+
.option('--synced', 'When creating the bundle, store it in the age-encrypted synced secrets file')
|
|
1419
1551
|
.option('--force', 'Overwrite an existing key in the bundle')
|
|
1420
1552
|
.option('--purge', 'With --from icloud: delete the iCloud copies after a successful import (iCloud propagates the deletion to your other devices)')
|
|
1421
1553
|
.option('--from-file <path>', 'Import from an AES-256-GCM encrypted offline bundle file (needs AGENTS_SECRETS_PASSPHRASE; symmetric counterpart of export --to-file)')
|
|
@@ -1440,7 +1572,7 @@ Examples:
|
|
|
1440
1572
|
}
|
|
1441
1573
|
const env = importBundleFromFile(opts.fromFile, passphrase);
|
|
1442
1574
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
1443
|
-
const bundle = resolveImportBundle(resolvedBundleName, opts.backend);
|
|
1575
|
+
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
|
|
1444
1576
|
const { added, skipped } = applyEnvToBundle(bundle, env, opts);
|
|
1445
1577
|
console.log(chalk.green(`Imported ${added} key(s) from file${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
1446
1578
|
return;
|
|
@@ -1453,7 +1585,7 @@ Examples:
|
|
|
1453
1585
|
const resolvedBundleName = bundleName ?? (await pickBundleName('import into'));
|
|
1454
1586
|
const target = await resolveSshTarget(opts.host);
|
|
1455
1587
|
const env = await remoteResolveEnv(target, resolvedBundleName, { osLookupName: opts.host });
|
|
1456
|
-
const bundle = resolveImportBundle(resolvedBundleName, opts.backend);
|
|
1588
|
+
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
|
|
1457
1589
|
const { added, skipped } = applyEnvToBundle(bundle, env, opts);
|
|
1458
1590
|
console.log(chalk.green(`Imported ${added} key(s) from ${opts.host}${skipped ? `, skipped ${skipped} (already set, pass --force)` : ''}.`));
|
|
1459
1591
|
return;
|
|
@@ -1465,12 +1597,12 @@ Examples:
|
|
|
1465
1597
|
if (opts.from1password) {
|
|
1466
1598
|
console.log(chalk.yellow('--from-1password is deprecated; use --from 1password:<vault>.'));
|
|
1467
1599
|
}
|
|
1468
|
-
const requestedBackend = parseBackendOpt(opts.backend);
|
|
1600
|
+
const requestedBackend = opts.synced ? 'vault' : parseBackendOpt(opts.backend);
|
|
1469
1601
|
if (source.kind === 'icloud') {
|
|
1470
1602
|
await importFromICloud(bundleName, {
|
|
1471
1603
|
force: opts.force,
|
|
1472
1604
|
allPlaintext: opts.allPlaintext,
|
|
1473
|
-
backend: requestedBackend === '
|
|
1605
|
+
backend: requestedBackend === 'keychain' ? undefined : requestedBackend,
|
|
1474
1606
|
purge: opts.purge,
|
|
1475
1607
|
});
|
|
1476
1608
|
return;
|
|
@@ -1480,7 +1612,7 @@ Examples:
|
|
|
1480
1612
|
// to downgrade keychain -> file) or creates it with the requested backend
|
|
1481
1613
|
// so a single `import --backend file` works (what `export --host ...
|
|
1482
1614
|
// --remote-backend file` drives on the remote).
|
|
1483
|
-
const bundle = resolveImportBundle(resolvedBundleName, opts.backend);
|
|
1615
|
+
const bundle = resolveImportBundle(resolvedBundleName, opts.backend, opts.synced);
|
|
1484
1616
|
if (source.kind === '1password') {
|
|
1485
1617
|
assertOpAvailable();
|
|
1486
1618
|
const vault = await resolveVault(source.vault);
|
|
@@ -1530,7 +1662,7 @@ Examples:
|
|
|
1530
1662
|
throw new Error('--to-file needs AGENTS_SECRETS_PASSPHRASE set to encrypt the bundle. ' +
|
|
1531
1663
|
'Set it for this command, then supply the same value when importing.');
|
|
1532
1664
|
}
|
|
1533
|
-
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: 'export --to-file', agentOnly: isHeadlessSecretsContext() });
|
|
1665
|
+
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: 'export --to-file', keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
|
|
1534
1666
|
exportBundleToFile(env, opts.toFile, passphrase);
|
|
1535
1667
|
console.log(chalk.green(`Exported ${Object.keys(env).length} key(s) to ${opts.toFile}`));
|
|
1536
1668
|
return;
|
|
@@ -1557,7 +1689,7 @@ Examples:
|
|
|
1557
1689
|
'bundle at rest on the remote. Set it for this command, then unlock it the same way per run.');
|
|
1558
1690
|
}
|
|
1559
1691
|
}
|
|
1560
|
-
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export`, agentOnly: isHeadlessSecretsContext() });
|
|
1692
|
+
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `ssh export`, keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
|
|
1561
1693
|
const dotenv = bundleEnvToDotenv(env);
|
|
1562
1694
|
const keyCount = Object.keys(env).length;
|
|
1563
1695
|
// Drive the remote's own `agents secrets import --from -` so the values
|
|
@@ -1622,7 +1754,7 @@ Examples:
|
|
|
1622
1754
|
if (opts.to1password) {
|
|
1623
1755
|
assertOpAvailable();
|
|
1624
1756
|
const vault = await resolveVault(opts.vault);
|
|
1625
|
-
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `1Password vault ${vault}`, agentOnly: isHeadlessSecretsContext() });
|
|
1757
|
+
const { env } = readAndResolveBundleEnv(resolvedBundleName, { caller: `1Password vault ${vault}`, keyMode: 'storage', agentOnly: isHeadlessSecretsContext() });
|
|
1626
1758
|
let created = 0;
|
|
1627
1759
|
let overwritten = 0;
|
|
1628
1760
|
let skipped = 0;
|
|
@@ -1667,11 +1799,12 @@ Examples:
|
|
|
1667
1799
|
// terminal stdin, so it is not headless and still prompts.
|
|
1668
1800
|
const { env } = readAndResolveBundleEnv(resolvedBundleName, {
|
|
1669
1801
|
caller: `export to shell`,
|
|
1802
|
+
keyMode: 'process',
|
|
1670
1803
|
agentOnly: isHeadlessSecretsContext(),
|
|
1671
1804
|
});
|
|
1672
1805
|
if (opts.format === 'json') {
|
|
1673
|
-
//
|
|
1674
|
-
//
|
|
1806
|
+
// Machine-readable form consumed by `remoteResolveEnv` over SSH.
|
|
1807
|
+
// Single object of injected env KEY -> value; values verbatim.
|
|
1675
1808
|
process.stdout.write(JSON.stringify(env));
|
|
1676
1809
|
return;
|
|
1677
1810
|
}
|
|
@@ -1766,6 +1899,60 @@ Examples:
|
|
|
1766
1899
|
const { runSecretsMcpServer } = await import('../lib/secrets/mcp.js');
|
|
1767
1900
|
await runSecretsMcpServer({ version: getCliVersion() });
|
|
1768
1901
|
});
|
|
1902
|
+
const openclawKeychain = cmd
|
|
1903
|
+
.command('openclaw-keychain')
|
|
1904
|
+
.description('Migrate supported OpenClaw credentials from plaintext config to macOS Keychain-backed SecretRefs');
|
|
1905
|
+
openclawKeychain
|
|
1906
|
+
.command('migrate [config]')
|
|
1907
|
+
.description('Rewrite supported OpenClaw plaintext secrets to exec SecretRefs backed by macOS Keychain')
|
|
1908
|
+
.option('--account <name>', 'Keychain account name', 'openclaw')
|
|
1909
|
+
.option('--dry-run', 'Inspect the migration without writing Keychain or config changes')
|
|
1910
|
+
.action(async (configArg, opts) => {
|
|
1911
|
+
try {
|
|
1912
|
+
const configPath = configArg || path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
1913
|
+
const { migrateOpenClawConfigToKeychainRefs, storeOpenClawKeychainServices } = await import('../lib/openclaw-keychain.js');
|
|
1914
|
+
const before = fs.readFileSync(configPath, 'utf-8');
|
|
1915
|
+
const config = JSON.parse(before);
|
|
1916
|
+
const result = migrateOpenClawConfigToKeychainRefs(config, { account: opts.account });
|
|
1917
|
+
if (result.unsupportedEnvKeys.length > 0) {
|
|
1918
|
+
throw new Error(`OpenClaw top-level env keys have no supported SecretRef target: ${result.unsupportedEnvKeys.join(', ')}. ` +
|
|
1919
|
+
`Move them to a supported credential field before removing plaintext.`);
|
|
1920
|
+
}
|
|
1921
|
+
if (!opts.dryRun) {
|
|
1922
|
+
storeOpenClawKeychainServices(result.services, opts.account);
|
|
1923
|
+
if (result.changed)
|
|
1924
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
1925
|
+
}
|
|
1926
|
+
console.log(`OpenClaw Keychain migration ${opts.dryRun ? 'plan' : 'complete'}: ${result.services.length} secret${result.services.length === 1 ? '' : 's'}, ${result.replacedPaths.length} ref${result.replacedPaths.length === 1 ? '' : 's'}, ${result.removedEnvPaths.length} env entr${result.removedEnvPaths.length === 1 ? 'y' : 'ies'} removed.`);
|
|
1927
|
+
for (const p of result.replacedPaths)
|
|
1928
|
+
console.log(`ref ${p}`);
|
|
1929
|
+
for (const p of result.removedEnvPaths)
|
|
1930
|
+
console.log(`removed ${p}`);
|
|
1931
|
+
process.exit(0);
|
|
1932
|
+
}
|
|
1933
|
+
catch (err) {
|
|
1934
|
+
console.error(chalk.red(err.message));
|
|
1935
|
+
process.exit(1);
|
|
1936
|
+
}
|
|
1937
|
+
});
|
|
1938
|
+
openclawKeychain
|
|
1939
|
+
.command('resolve')
|
|
1940
|
+
.description('OpenClaw exec SecretRef resolver for Keychain service ids')
|
|
1941
|
+
.option('--account <name>', 'Keychain account name', 'openclaw')
|
|
1942
|
+
.action(async (opts) => {
|
|
1943
|
+
try {
|
|
1944
|
+
const { readOpenClawKeychainService, resolveOpenClawKeychainRequest } = await import('../lib/openclaw-keychain.js');
|
|
1945
|
+
const raw = readStdinSync();
|
|
1946
|
+
const request = raw.trim() ? JSON.parse(raw) : {};
|
|
1947
|
+
const response = resolveOpenClawKeychainRequest(request, (service) => readOpenClawKeychainService(service, opts.account));
|
|
1948
|
+
process.stdout.write(JSON.stringify(response));
|
|
1949
|
+
process.exit(0);
|
|
1950
|
+
}
|
|
1951
|
+
catch {
|
|
1952
|
+
process.stdout.write(JSON.stringify({ protocolVersion: 1, values: {}, errors: { value: { code: 'NOT_FOUND' } } }));
|
|
1953
|
+
process.exit(1);
|
|
1954
|
+
}
|
|
1955
|
+
});
|
|
1769
1956
|
cmd
|
|
1770
1957
|
.command('generate [length]')
|
|
1771
1958
|
.description('Generate a random password')
|
|
@@ -1921,7 +2108,7 @@ Examples:
|
|
|
1921
2108
|
try {
|
|
1922
2109
|
// noAgent: read the real keychain (one Touch ID) rather than the
|
|
1923
2110
|
// agent we're about to populate.
|
|
1924
|
-
const { bundle, env } = readAndResolveBundleEnv(name, { noAgent: true, caller: 'unlock' });
|
|
2111
|
+
const { bundle, env } = readAndResolveBundleEnv(name, { noAgent: true, caller: 'unlock', keyMode: 'storage' });
|
|
1925
2112
|
if (await agentLoad(name, bundle, env, ttlMs)) {
|
|
1926
2113
|
loaded++;
|
|
1927
2114
|
// Persist a durable session snapshot so the unlock survives a daemon
|
|
@@ -2160,7 +2347,7 @@ function parseBackendOpt(raw) {
|
|
|
2160
2347
|
const v = (raw ?? 'keychain').toLowerCase();
|
|
2161
2348
|
if (v === 'keychain' || v === 'file')
|
|
2162
2349
|
return v;
|
|
2163
|
-
console.error(chalk.red(`Invalid --backend '${raw}'. Use 'keychain' or 'file'.`));
|
|
2350
|
+
console.error(chalk.red(`Invalid --backend '${raw}'. Use 'keychain' or 'file'. For cross-machine file sync, pass --synced.`));
|
|
2164
2351
|
process.exit(1);
|
|
2165
2352
|
}
|
|
2166
2353
|
/** Human-readable "locks in 3 hours" / "locks in 5 minutes" from an epoch-ms expiry. */
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents send <text> --channel <name> --to <target>` — deliver a message over
|
|
3
|
+
* any registered channel (mailbox, telegram, imessage, slack, discord, apps).
|
|
4
|
+
*
|
|
5
|
+
* `agents notify <text>` — owner-facing alias. Fills channel + target from the
|
|
6
|
+
* `notify.owner` block in agents.yaml when not passed, so routines/agents ping
|
|
7
|
+
* the owner without hardcoding a chat id / host / account.
|
|
8
|
+
*
|
|
9
|
+
* Both go through one `doSend()` seam: resolve the provider for the channel
|
|
10
|
+
* (notify.transports picks the transport per host) and call provider.send().
|
|
11
|
+
* The agent mailbox is just one channel; `agents message` stays the richer
|
|
12
|
+
* agent-control command (feed-claim / identity / PTY / resume) on the same spool.
|
|
13
|
+
*/
|
|
14
|
+
import type { Command } from 'commander';
|
|
15
|
+
export declare function registerSendCommand(program: Command): void;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { die } from '../lib/format.js';
|
|
3
|
+
import { readMeta } from '../lib/state.js';
|
|
4
|
+
import { registerBuiltinProviders } from '../lib/channels/providers/index.js';
|
|
5
|
+
import { resolveTransport } from '../lib/channels/resolve.js';
|
|
6
|
+
async function doSend(text, channel, to, opts) {
|
|
7
|
+
registerBuiltinProviders();
|
|
8
|
+
const provider = resolveTransport(channel, readMeta());
|
|
9
|
+
const result = await provider.send(text, {
|
|
10
|
+
target: to,
|
|
11
|
+
thread: opts.thread,
|
|
12
|
+
attachments: opts.attachment,
|
|
13
|
+
from: opts.from,
|
|
14
|
+
dryRun: opts.dryRun,
|
|
15
|
+
});
|
|
16
|
+
if (opts.json) {
|
|
17
|
+
console.log(JSON.stringify(result));
|
|
18
|
+
if (!result.ok)
|
|
19
|
+
process.exit(1);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!result.ok) {
|
|
23
|
+
die(`send failed [${result.channel} → ${result.id}]: ${result.error ?? 'unknown error'}`);
|
|
24
|
+
}
|
|
25
|
+
const suffix = result.msgId ? chalk.dim(` (${result.msgId})`) : '';
|
|
26
|
+
const dry = opts.dryRun ? chalk.dim(' [dry-run]') : '';
|
|
27
|
+
console.log(chalk.green(`Sent via ${result.channel} → ${result.id}`) + suffix + dry);
|
|
28
|
+
}
|
|
29
|
+
export function registerSendCommand(program) {
|
|
30
|
+
program
|
|
31
|
+
.command('send <text>')
|
|
32
|
+
.description('Send a message through a channel provider (mailbox, telegram, imessage, slack, discord).')
|
|
33
|
+
.requiredOption('--channel <name>', 'channel / provider name')
|
|
34
|
+
.requiredOption('--to <target>', 'channel-specific recipient id')
|
|
35
|
+
.option('--thread <id>', 'channel thread id / timestamp')
|
|
36
|
+
.option('--attachment <path...>', 'file attachment path (repeatable)')
|
|
37
|
+
.option('--from <who>', 'sender label (mailbox)')
|
|
38
|
+
.option('--json', 'output JSON')
|
|
39
|
+
.option('--dry-run', 'resolve + build but do not send')
|
|
40
|
+
.action(async (text, opts) => {
|
|
41
|
+
await doSend(text, opts.channel, opts.to, opts);
|
|
42
|
+
});
|
|
43
|
+
program
|
|
44
|
+
.command('notify <text>')
|
|
45
|
+
.description('Notify the owner (channel + target default to notify.owner in agents.yaml).')
|
|
46
|
+
.option('--channel <name>', 'override owner channel')
|
|
47
|
+
.option('--to <target>', 'override owner target')
|
|
48
|
+
.option('--thread <id>', 'channel thread id / timestamp')
|
|
49
|
+
.option('--attachment <path...>', 'file attachment path (repeatable)')
|
|
50
|
+
.option('--json', 'output JSON')
|
|
51
|
+
.option('--dry-run', 'resolve + build but do not send')
|
|
52
|
+
.action(async (text, opts) => {
|
|
53
|
+
const owner = readMeta().notify?.owner;
|
|
54
|
+
const channel = opts.channel ?? owner?.channel;
|
|
55
|
+
const to = opts.to ?? owner?.to;
|
|
56
|
+
if (!channel || !to) {
|
|
57
|
+
die('notify: no --channel/--to given and no notify.owner.{channel,to} in agents.yaml. ' +
|
|
58
|
+
'Set notify.owner or pass --channel/--to.');
|
|
59
|
+
}
|
|
60
|
+
await doSend(text, channel, to, opts);
|
|
61
|
+
});
|
|
62
|
+
}
|