@phnx-labs/agents-cli 1.20.78 → 1.20.82
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 +487 -0
- package/README.md +25 -25
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +9 -0
- package/dist/commands/activity.js +153 -24
- package/dist/commands/apply.js +11 -4
- package/dist/commands/cli.js +1 -1
- package/dist/commands/commands.d.ts +1 -1
- package/dist/commands/commands.js +7 -6
- package/dist/commands/defaults.js +3 -11
- package/dist/commands/doctor.d.ts +65 -0
- package/dist/commands/doctor.js +575 -31
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.js +24 -5
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +87 -11
- package/dist/commands/harness.js +2 -2
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +8 -4
- package/dist/commands/logs.js +6 -0
- package/dist/commands/models.js +1 -1
- package/dist/commands/packages.js +6 -6
- package/dist/commands/permissions.js +1 -1
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pull.d.ts +1 -1
- package/dist/commands/pull.js +4 -4
- package/dist/commands/repo.d.ts +78 -0
- package/dist/commands/repo.js +187 -11
- package/dist/commands/routines.js +12 -2
- package/dist/commands/rules.js +13 -12
- package/dist/commands/secrets.d.ts +3 -1
- package/dist/commands/secrets.js +100 -32
- package/dist/commands/sessions-inject.d.ts +7 -4
- package/dist/commands/sessions-inject.js +18 -18
- package/dist/commands/sessions-picker.js +32 -3
- package/dist/commands/sessions-resume.d.ts +16 -0
- package/dist/commands/sessions-resume.js +14 -13
- package/dist/commands/sessions.d.ts +31 -0
- package/dist/commands/sessions.js +141 -16
- package/dist/commands/set.d.ts +15 -0
- package/dist/commands/set.js +79 -0
- package/dist/commands/setup-fleet.d.ts +21 -0
- package/dist/commands/setup-fleet.js +201 -0
- package/dist/commands/setup-secrets.d.ts +20 -0
- package/dist/commands/setup-secrets.js +223 -0
- package/dist/commands/setup.js +17 -3
- package/dist/commands/skills.js +1 -1
- package/dist/commands/sync.js +7 -2
- package/dist/commands/teams-picker.js +0 -1
- package/dist/commands/teams.d.ts +33 -1
- package/dist/commands/teams.js +208 -44
- package/dist/commands/versions.js +10 -5
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +11 -0
- package/dist/index.js +5 -8
- package/dist/lib/acp/harnesses.js +0 -7
- package/dist/lib/activity.d.ts +156 -0
- package/dist/lib/activity.js +282 -0
- package/dist/lib/agents.d.ts +9 -2
- package/dist/lib/agents.js +72 -63
- package/dist/lib/browser/service.js +3 -0
- package/dist/lib/browser/types.d.ts +7 -0
- package/dist/lib/capabilities.js +6 -2
- package/dist/lib/crabbox/lease.js +2 -2
- package/dist/lib/crabbox/setup-copy.d.ts +4 -4
- package/dist/lib/crabbox/setup-copy.js +4 -4
- package/dist/lib/daemon.d.ts +18 -0
- package/dist/lib/daemon.js +47 -6
- package/dist/lib/devices/fleet.d.ts +1 -1
- package/dist/lib/devices/fleet.js +1 -1
- package/dist/lib/doctor-diff.d.ts +20 -0
- package/dist/lib/doctor-diff.js +6 -1
- package/dist/lib/drift.d.ts +31 -11
- package/dist/lib/drift.js +58 -7
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +10 -1
- package/dist/lib/exec.js +35 -8
- package/dist/lib/feed-post.d.ts +28 -1
- package/dist/lib/feed-post.js +110 -2
- package/dist/lib/fleet/apply.js +8 -1
- package/dist/lib/fleet/auth-sync.d.ts +18 -1
- package/dist/lib/fleet/auth-sync.js +25 -11
- package/dist/lib/fleet/remote-login.js +5 -0
- package/dist/lib/git.d.ts +14 -14
- package/dist/lib/git.js +41 -44
- package/dist/lib/hooks.d.ts +42 -0
- package/dist/lib/hooks.js +137 -93
- package/dist/lib/hosts/dispatch.d.ts +28 -1
- package/dist/lib/hosts/dispatch.js +33 -5
- package/dist/lib/hosts/option.js +2 -2
- package/dist/lib/hosts/passthrough.d.ts +22 -1
- package/dist/lib/hosts/passthrough.js +238 -13
- package/dist/lib/hosts/run-target.d.ts +2 -0
- package/dist/lib/hosts/run-target.js +1 -0
- package/dist/lib/hq/floor.d.ts +1 -1
- package/dist/lib/hq/floor.js +6 -1
- package/dist/lib/mcp.js +3 -71
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/notify-desktop.d.ts +15 -0
- package/dist/lib/menubar/notify-desktop.js +41 -5
- package/dist/lib/merged-resources.d.ts +11 -0
- package/dist/{commands/resources.js → lib/merged-resources.js} +11 -19
- package/dist/lib/permissions.d.ts +0 -32
- package/dist/lib/permissions.js +4 -157
- package/dist/lib/picker.js +1 -1
- package/dist/lib/platform/process.d.ts +16 -5
- package/dist/lib/platform/process.js +54 -0
- package/dist/lib/plugins.d.ts +0 -8
- package/dist/lib/plugins.js +4 -110
- package/dist/lib/project-resources.js +4 -1
- package/dist/lib/refresh.d.ts +5 -3
- package/dist/lib/refresh.js +7 -5
- package/dist/lib/resources/commands.js +4 -1
- package/dist/lib/resources/mcp.js +0 -4
- package/dist/lib/resources/permissions.d.ts +1 -1
- package/dist/lib/resources/permissions.js +1 -5
- package/dist/lib/resources/rules.js +4 -1
- package/dist/lib/resources/skills.js +4 -1
- package/dist/lib/resources/subagents.js +4 -0
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/routines.d.ts +20 -0
- package/dist/lib/routines.js +6 -0
- package/dist/lib/run-defaults.d.ts +1 -0
- package/dist/lib/run-defaults.js +9 -0
- package/dist/lib/runner.d.ts +0 -12
- package/dist/lib/runner.js +49 -10
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +16 -0
- package/dist/lib/secrets/agent.js +105 -3
- package/dist/lib/secrets/audit.d.ts +46 -0
- package/dist/lib/secrets/audit.js +56 -0
- package/dist/lib/secrets/bundles.d.ts +11 -8
- package/dist/lib/secrets/bundles.js +31 -23
- package/dist/lib/secrets/index.d.ts +2 -1
- package/dist/lib/secrets/index.js +8 -2
- package/dist/lib/secrets/remote.d.ts +75 -2
- package/dist/lib/secrets/remote.js +139 -5
- package/dist/lib/secrets/sync.js +3 -3
- package/dist/lib/session/active.d.ts +110 -26
- package/dist/lib/session/active.js +161 -50
- package/dist/lib/session/actor-sidecar.d.ts +23 -0
- package/dist/lib/session/actor-sidecar.js +101 -0
- package/dist/lib/session/db.d.ts +4 -1
- package/dist/lib/session/db.js +65 -8
- package/dist/lib/session/discover.d.ts +4 -0
- package/dist/lib/session/discover.js +13 -0
- package/dist/lib/session/hook-sessions.d.ts +9 -0
- package/dist/lib/session/hook-sessions.js +57 -8
- package/dist/lib/session/pid-registry.d.ts +13 -0
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +16 -0
- package/dist/lib/staleness/detectors/commands.js +2 -2
- package/dist/lib/staleness/detectors/permissions.js +1 -45
- package/dist/lib/staleness/writers/commands.js +4 -5
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +1 -2
- package/dist/lib/startup/command-registry.js +2 -4
- package/dist/lib/subagents-registry.js +5 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -12
- package/dist/lib/teams/agents.d.ts +0 -2
- package/dist/lib/teams/agents.js +10 -27
- package/dist/lib/teams/index.d.ts +1 -1
- package/dist/lib/teams/index.js +1 -1
- package/dist/lib/types.d.ts +17 -9
- package/dist/lib/usage.d.ts +28 -5
- package/dist/lib/usage.js +271 -8
- package/dist/lib/versions.js +7 -1
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdog.js +25 -13
- package/package.json +1 -2
- package/dist/commands/check.d.ts +0 -15
- package/dist/commands/check.js +0 -180
- package/dist/commands/resources.d.ts +0 -5
package/dist/commands/secrets.js
CHANGED
|
@@ -15,7 +15,7 @@ import * as path from 'path';
|
|
|
15
15
|
import { SSH_TARGET_RE, assertValidSshTarget, sshExec } from '../lib/ssh-exec.js';
|
|
16
16
|
import { quoteWin32ExecArg, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
17
17
|
import { ensureDaemonStarted, isDaemonRunning } from '../lib/daemon.js';
|
|
18
|
-
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, } from '../lib/secrets/remote.js';
|
|
18
|
+
import { parseHostsOption, remoteResolveEnv, remoteSecretsRaw, remoteSecretsStream, resolveHostSshTarget, verifyRemoteKeychainPush, keychainWriteFailureMessage, } from '../lib/secrets/remote.js';
|
|
19
19
|
import { remoteShellFor, buildWindowsStdinImportCommand } from '../lib/hosts/remote-cmd.js';
|
|
20
20
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
21
21
|
import { bundleBackend, bundleExists, bundleItemStore, bundlePolicy, deleteBundle, describeBundle, keychainItemsForBundle, keychainRef, listBundles, migrateLegacyBundles, parseDotenv, readAndResolveBundleEnv, isHeadlessSecretsContext, readBundle, readBundleIfDecryptable, renameBundle, rotateBundleSecret, sanitizeProcessEnv, validateBundleName, validateEnvKey, validateExpiresFutureDated, validateSecretType, writeBundle, writeBundleWithItems, } from '../lib/secrets/bundles.js';
|
|
@@ -29,6 +29,7 @@ import { getCliVersionFresh } from '../lib/version.js';
|
|
|
29
29
|
import { readMeta } from '../lib/state.js';
|
|
30
30
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
31
31
|
import { emit, query } from '../lib/events.js';
|
|
32
|
+
import { emitSecretAudit } from '../lib/secrets/audit.js';
|
|
32
33
|
import { frequentlyPromptedBundles } from '../lib/secrets/unlock-hints.js';
|
|
33
34
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
34
35
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -468,7 +469,7 @@ export function renderPolicyCol(b, held) {
|
|
|
468
469
|
if (bundlePolicy(b) === 'always')
|
|
469
470
|
return chalk.yellow('always · prompt');
|
|
470
471
|
const exp = held?.get(b.name);
|
|
471
|
-
return exp ? chalk.green(`
|
|
472
|
+
return exp ? chalk.green(`hold · held ${compactRemaining(exp)}`) : chalk.gray('hold');
|
|
472
473
|
}
|
|
473
474
|
/** Human-readable hold window for `secrets status`. Sub-hour values render in
|
|
474
475
|
* minutes (so a near-floor `holdMs` never shows a confusing "0 hours"), whole
|
|
@@ -669,7 +670,7 @@ function countExpiringSoon(meta) {
|
|
|
669
670
|
* guard can't drift between them.
|
|
670
671
|
*/
|
|
671
672
|
function resolveImportBundle(name, backendOpt, synced = false) {
|
|
672
|
-
const requestedBackend = synced ? 'vault' :
|
|
673
|
+
const requestedBackend = synced ? 'vault' : resolveBackendOpt(backendOpt);
|
|
673
674
|
if (bundleExists(name)) {
|
|
674
675
|
const bundle = readBundle(name);
|
|
675
676
|
if (requestedBackend !== 'keychain' && bundle.backend !== requestedBackend) {
|
|
@@ -735,7 +736,7 @@ export function registerSecretsCommands(program) {
|
|
|
735
736
|
agents secrets view prod
|
|
736
737
|
|
|
737
738
|
# Stop a noisy automation bundle from prompting every run: ask once a week
|
|
738
|
-
agents secrets policy prod
|
|
739
|
+
agents secrets policy prod hold
|
|
739
740
|
|
|
740
741
|
# Eval the bundle into your current shell
|
|
741
742
|
eval "$(agents secrets export prod --plaintext)"
|
|
@@ -753,17 +754,21 @@ export function registerSecretsCommands(program) {
|
|
|
753
754
|
|
|
754
755
|
Touch ID noise: macOS pops a prompt per bundle per process. Each bundle has
|
|
755
756
|
a prompt policy, shown in the POLICY column of 'agents secrets list':
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
757
|
+
hold (default) ask once, then serve it silently from the local agent
|
|
758
|
+
for the hold window ('secrets.agent.holdMs', 7d by
|
|
759
|
+
default), until sleep / logout or 'lock' (a bare
|
|
760
|
+
screen-lock does NOT drop it). Accepts the older names
|
|
761
|
+
'daily' and 'session'.
|
|
759
762
|
always ask for Touch ID every time — never auto-held.
|
|
760
|
-
|
|
763
|
+
never no biometry ACL at all — reads are silent forever, even
|
|
764
|
+
if the agent is down. Automation credentials only.
|
|
765
|
+
The default is 'hold' (one Touch ID per hold window); change it globally with
|
|
761
766
|
'secrets.policy' in agents.yaml, or per bundle with 'agents secrets policy
|
|
762
767
|
<bundle> always'. 'agents secrets unlock <bundle>' holds any bundle after one
|
|
763
768
|
prompt regardless of policy. Nothing on disk.
|
|
764
769
|
|
|
765
770
|
See also:
|
|
766
|
-
agents secrets policy <bundle>
|
|
771
|
+
agents secrets policy <bundle> hold ask once per hold window (7d by default), not every run
|
|
767
772
|
agents secrets unlock <bundle> hold a bundle after one Touch ID
|
|
768
773
|
agents secrets lock wipe held bundles (re-prompt next read)
|
|
769
774
|
agents secrets status show held bundles + when they lock
|
|
@@ -789,6 +794,8 @@ export function registerSecretsCommands(program) {
|
|
|
789
794
|
.description('List configured secrets bundles (use --host/--hosts to list bundles on other machines over SSH)')
|
|
790
795
|
.option('--host <target>', 'List bundles on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
791
796
|
.option('--hosts <list>', 'Comma-separated hosts to list in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
797
|
+
.option('--device <target>', 'Alias for --host')
|
|
798
|
+
.option('--devices <list>', 'Alias for --hosts')
|
|
792
799
|
.option('--json', 'Emit machine-readable JSON (bundle metadata only — never secret values) instead of the table')
|
|
793
800
|
.action(async (opts) => {
|
|
794
801
|
const targets = parseHostsOption(opts);
|
|
@@ -856,6 +863,8 @@ export function registerSecretsCommands(program) {
|
|
|
856
863
|
.option('--json', 'Emit machine-readable JSON (values masked unless --reveal) instead of the human view')
|
|
857
864
|
.option('--host <target>', 'Show a bundle on a remote host over SSH (enrolled `agents hosts` name, ssh-config alias, or user@host)')
|
|
858
865
|
.option('--hosts <list>', 'Comma-separated hosts to show in one shot, e.g. yosemite-s0,yosemite-s1')
|
|
866
|
+
.option('--device <target>', 'Alias for --host')
|
|
867
|
+
.option('--devices <list>', 'Alias for --hosts')
|
|
859
868
|
.action(async (name, opts) => {
|
|
860
869
|
try {
|
|
861
870
|
const targets = parseHostsOption(opts);
|
|
@@ -913,8 +922,8 @@ export function registerSecretsCommands(program) {
|
|
|
913
922
|
}
|
|
914
923
|
const exposed = revealed.size + entries.filter((e) => e.kind === 'literal').length;
|
|
915
924
|
if (exposed > 0) {
|
|
916
|
-
|
|
917
|
-
|
|
925
|
+
emitSecretAudit({
|
|
926
|
+
event: 'secrets.get',
|
|
918
927
|
bundle: bundle.name,
|
|
919
928
|
operation: 'view --reveal --json',
|
|
920
929
|
source: 'reveal',
|
|
@@ -972,8 +981,8 @@ export function registerSecretsCommands(program) {
|
|
|
972
981
|
console.log(chalk.red.bold('policy: never — NO biometry ACL; reads are silent (no Touch ID, no user-presence check). Automation-only.'));
|
|
973
982
|
}
|
|
974
983
|
else {
|
|
975
|
-
console.log(bundlePolicy(bundle) === '
|
|
976
|
-
? chalk.gray('policy:
|
|
984
|
+
console.log(bundlePolicy(bundle) === 'hold'
|
|
985
|
+
? chalk.gray('policy: hold (ask once, then held for the hold window — 7d by default — until sleep / logout; screen-lock does not drop it)')
|
|
977
986
|
: chalk.gray('policy: always (asks for Touch ID every time — never auto-held)'));
|
|
978
987
|
}
|
|
979
988
|
if (bundle.created_at)
|
|
@@ -1021,8 +1030,8 @@ export function registerSecretsCommands(program) {
|
|
|
1021
1030
|
const literalCount = entries.filter((e) => e.kind === 'literal').length;
|
|
1022
1031
|
const exposedCount = revealedValues.size + literalCount;
|
|
1023
1032
|
if (exposedCount > 0) {
|
|
1024
|
-
|
|
1025
|
-
|
|
1033
|
+
emitSecretAudit({
|
|
1034
|
+
event: 'secrets.get',
|
|
1026
1035
|
bundle: bundle.name,
|
|
1027
1036
|
operation: 'view --reveal',
|
|
1028
1037
|
source: 'reveal',
|
|
@@ -1079,7 +1088,7 @@ export function registerSecretsCommands(program) {
|
|
|
1079
1088
|
const value = getKeychainToken(item);
|
|
1080
1089
|
// Raw item reads bypass readAndResolveBundleEnv, so audit here too.
|
|
1081
1090
|
// `item` is the keychain service name, never the value.
|
|
1082
|
-
|
|
1091
|
+
emitSecretAudit({ event: 'secrets.get', item, source: 'raw-item', status: 'success' });
|
|
1083
1092
|
process.stdout.write(value.endsWith('\n') ? value : `${value}\n`);
|
|
1084
1093
|
}
|
|
1085
1094
|
catch {
|
|
@@ -1153,10 +1162,10 @@ export function registerSecretsCommands(program) {
|
|
|
1153
1162
|
.description('Create an empty bundle')
|
|
1154
1163
|
.option('--description <text>', 'Free-form description')
|
|
1155
1164
|
.option('--allow-exec', 'Allow exec: refs in this bundle (off by default)')
|
|
1156
|
-
.option('--policy <policy>',
|
|
1165
|
+
.option('--policy <policy>', "prompt policy: hold (default, ask once per hold window — secrets.agent.holdMs, 7d by default), always (ask every time), or never (silent, NO biometry ACL — needs --i-understand). 'daily'/'session' are accepted aliases for 'hold'.")
|
|
1157
1166
|
.addOption(new Option('--tier <policy>', 'deprecated alias for --policy').hideHelp())
|
|
1158
1167
|
.option('--i-understand', 'Confirm creating a "never"-policy bundle (no biometry ACL) without an interactive prompt')
|
|
1159
|
-
.option('--backend <backend>', 'storage backend: keychain
|
|
1168
|
+
.option('--backend <backend>', 'storage backend: keychain or file (defaults to agents.yaml secrets.backend)')
|
|
1160
1169
|
.option('--synced', 'Store this bundle in the age-encrypted synced secrets file for user-managed cross-machine file sync')
|
|
1161
1170
|
.option('--force', 'Overwrite an existing bundle')
|
|
1162
1171
|
.action(async (name, opts) => {
|
|
@@ -1167,7 +1176,7 @@ export function registerSecretsCommands(program) {
|
|
|
1167
1176
|
// inherits the configured default (`daily`) instead of being pinned.
|
|
1168
1177
|
const policyOpt = opts.policy ?? opts.tier;
|
|
1169
1178
|
const policy = policyOpt ? parsePolicyOpt(policyOpt) : undefined;
|
|
1170
|
-
const backend = opts.synced ? 'vault' :
|
|
1179
|
+
const backend = opts.synced ? 'vault' : resolveBackendOpt(opts.backend);
|
|
1171
1180
|
if (bundleExists(resolvedName) && !opts.force) {
|
|
1172
1181
|
console.error(chalk.red(`Bundle '${resolvedName}' already exists. Use --force to overwrite.`));
|
|
1173
1182
|
process.exit(1);
|
|
@@ -1188,8 +1197,8 @@ export function registerSecretsCommands(program) {
|
|
|
1188
1197
|
vars: {},
|
|
1189
1198
|
};
|
|
1190
1199
|
writeBundle(bundle);
|
|
1191
|
-
const policyTag = bundlePolicy(bundle) === '
|
|
1192
|
-
? 'policy:
|
|
1200
|
+
const policyTag = bundlePolicy(bundle) === 'hold'
|
|
1201
|
+
? 'policy: hold'
|
|
1193
1202
|
: bundlePolicy(bundle) === 'always'
|
|
1194
1203
|
? 'policy: always ask'
|
|
1195
1204
|
: 'policy: never (NO biometry ACL)';
|
|
@@ -1572,7 +1581,7 @@ Examples:
|
|
|
1572
1581
|
.addOption(new Option('--from-1password', 'deprecated alias for --from 1password:<vault>').hideHelp())
|
|
1573
1582
|
.addOption(new Option('--vault <name>', 'deprecated: name the vault in --from 1password:<vault>').hideHelp())
|
|
1574
1583
|
.option('--all-plaintext', 'Store every imported value as a literal in the bundle metadata (skip keychain item creation)')
|
|
1575
|
-
.option('--backend <backend>', 'When creating the bundle: keychain
|
|
1584
|
+
.option('--backend <backend>', 'When creating the bundle: keychain or file (defaults to agents.yaml secrets.backend)')
|
|
1576
1585
|
.option('--synced', 'When creating the bundle, store it in the age-encrypted synced secrets file')
|
|
1577
1586
|
.option('--force', 'Overwrite an existing key in the bundle')
|
|
1578
1587
|
.option('--purge', 'With --from icloud: delete the iCloud copies after a successful import (iCloud propagates the deletion to your other devices)')
|
|
@@ -1623,7 +1632,7 @@ Examples:
|
|
|
1623
1632
|
if (opts.from1password) {
|
|
1624
1633
|
console.log(chalk.yellow('--from-1password is deprecated; use --from 1password:<vault>.'));
|
|
1625
1634
|
}
|
|
1626
|
-
const requestedBackend = opts.synced ? 'vault' :
|
|
1635
|
+
const requestedBackend = opts.synced ? 'vault' : resolveBackendOpt(opts.backend);
|
|
1627
1636
|
if (source.kind === 'icloud') {
|
|
1628
1637
|
await importFromICloud(bundleName, {
|
|
1629
1638
|
force: opts.force,
|
|
@@ -1674,12 +1683,18 @@ Examples:
|
|
|
1674
1683
|
.option('--to-1password', 'Push every key in the bundle as a PASSWORD item in a 1Password vault')
|
|
1675
1684
|
.option('--vault <name>', '1Password vault name (used with --to-1password)')
|
|
1676
1685
|
.option('--host <target...>', 'Push the bundle over SSH to this target (host alias or user@host); repeatable for multiple machines')
|
|
1686
|
+
.option('--device <target...>', 'Alias for --host; repeatable')
|
|
1677
1687
|
.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')
|
|
1678
1688
|
.option('--force', 'Overwrite existing keys/items on the target (used with --to-1password and --host)')
|
|
1679
1689
|
.option('--format <shell|json>', 'Output for --plaintext export: shell (default) or json (lossless, machine-readable; used by remote resolve)', 'shell')
|
|
1680
1690
|
.option('--to-file <path>', 'Write the bundle as an AES-256-GCM encrypted offline file (needs AGENTS_SECRETS_PASSPHRASE; symmetric counterpart of import --from-file)')
|
|
1681
1691
|
.action(async (bundleName, opts) => {
|
|
1682
1692
|
try {
|
|
1693
|
+
// `--device` is an alias for `--host` (fleet vocabulary parity with
|
|
1694
|
+
// `agents activity`/`run --device`); fold it into the host list up front so
|
|
1695
|
+
// every downstream branch sees one resolved target list.
|
|
1696
|
+
if (opts.device?.length)
|
|
1697
|
+
opts.host = [...(opts.host ?? []), ...opts.device];
|
|
1683
1698
|
const { readAndResolveBundleEnv, bundleToEnvPrefix, isReservedEnvName } = await import('../lib/secrets/bundles.js');
|
|
1684
1699
|
const resolvedBundleName = bundleName ?? (await pickBundleName('export'));
|
|
1685
1700
|
if (opts.toFile) {
|
|
@@ -1770,6 +1785,29 @@ Examples:
|
|
|
1770
1785
|
console.error(chalk.red(`${host}: remote import failed (exit ${res.code})${msg ? `: ${msg}` : ''}`));
|
|
1771
1786
|
continue;
|
|
1772
1787
|
}
|
|
1788
|
+
// A keychain-backed push to a macOS remote over headless SSH can land
|
|
1789
|
+
// the bundle metadata but no READABLE value items: the remote login
|
|
1790
|
+
// keychain is locked in the non-interactive SSH context, so Security
|
|
1791
|
+
// accepts the write but the biometry-ACL'd item is unreadable, and the
|
|
1792
|
+
// remote `import` still exits 0. Read the bundle back the same way a
|
|
1793
|
+
// release will (drops the plaintext, keeps only key presence; the
|
|
1794
|
+
// remote's headless `agentOnly` guard fails fast, so no Touch ID) and
|
|
1795
|
+
// FAIL LOUDLY if the keys didn't materialize — rather than leave a
|
|
1796
|
+
// metadata-only bundle that breaks later with "stored item not found".
|
|
1797
|
+
// The file backend is headless-readable by construction, so skip it.
|
|
1798
|
+
if (remoteBackend === 'keychain') {
|
|
1799
|
+
const verdict = verifyRemoteKeychainPush(host, resolvedBundleName, Object.keys(env), { osLookupName: host });
|
|
1800
|
+
if (!verdict.ok) {
|
|
1801
|
+
failures++;
|
|
1802
|
+
if (verdict.kind === 'locked-keychain') {
|
|
1803
|
+
console.error(chalk.red(keychainWriteFailureMessage(host, resolvedBundleName, verdict.reason)));
|
|
1804
|
+
}
|
|
1805
|
+
else {
|
|
1806
|
+
console.error(chalk.red(`${host}: pushed '${resolvedBundleName}' but could not verify it on the remote: ${verdict.reason}`));
|
|
1807
|
+
}
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
}
|
|
1773
1811
|
const remoteMsg = (res.stdout || '').trim().split('\n').map((l) => l.trim()).filter(Boolean).pop();
|
|
1774
1812
|
console.log(chalk.green(`${host} -> '${resolvedBundleName}': ${remoteMsg || `${keyCount} key(s) exported`}`));
|
|
1775
1813
|
}
|
|
@@ -2152,13 +2190,29 @@ Examples:
|
|
|
2152
2190
|
loaded++;
|
|
2153
2191
|
// Persist a durable session snapshot so the unlock survives a daemon
|
|
2154
2192
|
// restart / upgrade (and sleep too, with --durable). session-store.ts.
|
|
2193
|
+
const durable = opts.durable ?? secretsAgentDurable();
|
|
2155
2194
|
saveSession(name, {
|
|
2156
2195
|
bundle,
|
|
2157
2196
|
env,
|
|
2158
2197
|
expiresAt: Date.now() + ttlMs,
|
|
2159
|
-
sleepPersist:
|
|
2198
|
+
sleepPersist: durable,
|
|
2160
2199
|
harness,
|
|
2161
2200
|
});
|
|
2201
|
+
// Audit the GRANT itself — the broker + durable session now serve this
|
|
2202
|
+
// bundle prompt-free for the whole TTL, to every reader in `harness`
|
|
2203
|
+
// scope. The pre-read above emits `secrets.get`; this records the
|
|
2204
|
+
// longer-lived unlock a `secrets.get` does not capture. Key NAMES only.
|
|
2205
|
+
emitSecretAudit({
|
|
2206
|
+
event: 'secrets.unlocked',
|
|
2207
|
+
bundle: name,
|
|
2208
|
+
operation: 'unlock',
|
|
2209
|
+
source: durable ? 'broker+durable' : 'broker',
|
|
2210
|
+
status: 'success',
|
|
2211
|
+
keyCount: Object.keys(env).length,
|
|
2212
|
+
keys: Object.keys(env).sort(),
|
|
2213
|
+
agent: harness,
|
|
2214
|
+
ttlMs,
|
|
2215
|
+
});
|
|
2162
2216
|
console.log(`${chalk.green('unlocked')} ${chalk.cyan(name)} ${chalk.gray(`(${Object.keys(env).length} keys, ${humanRemaining(Date.now() + ttlMs)})`)}`);
|
|
2163
2217
|
}
|
|
2164
2218
|
else {
|
|
@@ -2278,7 +2332,7 @@ Examples:
|
|
|
2278
2332
|
cmd
|
|
2279
2333
|
.command('policy <bundle> [policy]')
|
|
2280
2334
|
.alias('tier')
|
|
2281
|
-
.description("Show or set a bundle's prompt policy:
|
|
2335
|
+
.description("Show or set a bundle's prompt policy: hold (default, ask once per hold window — secrets.agent.holdMs, 7d by default), always (ask every time), or never (silent, NO biometry ACL). 'daily'/'session' are accepted aliases for 'hold'.")
|
|
2282
2336
|
.option('--i-understand', 'Confirm switching to the "never" policy (no biometry ACL) without an interactive prompt')
|
|
2283
2337
|
.action(async (bundleName, policyArg, opts) => {
|
|
2284
2338
|
try {
|
|
@@ -2295,12 +2349,12 @@ Examples:
|
|
|
2295
2349
|
return;
|
|
2296
2350
|
}
|
|
2297
2351
|
bundle.policy = next;
|
|
2298
|
-
// writeBundle evicts any broker-held copy, so tightening
|
|
2352
|
+
// writeBundle evicts any broker-held copy, so tightening hold ->
|
|
2299
2353
|
// always/never takes effect NOW: the next read re-prompts (`always`)
|
|
2300
2354
|
// or reads its no-ACL item directly (`never`).
|
|
2301
2355
|
writeBundle(bundle);
|
|
2302
2356
|
console.log(chalk.green(`${bundle.name} policy set to ${next}.`));
|
|
2303
|
-
if (next === '
|
|
2357
|
+
if (next === 'hold') {
|
|
2304
2358
|
console.log(chalk.gray('Held by the secrets-agent for ~7 days after one unlock (auto-cache is on by default; disable with `secrets.agent.auto: false` in agents.yaml).'));
|
|
2305
2359
|
}
|
|
2306
2360
|
else if (next === 'always') {
|
|
@@ -2375,11 +2429,11 @@ export function parsePolicyOpt(raw) {
|
|
|
2375
2429
|
const v = (raw ?? 'always').toLowerCase();
|
|
2376
2430
|
if (v === 'always' || v === 'biometry')
|
|
2377
2431
|
return 'always';
|
|
2378
|
-
if (v === 'daily' || v === 'session')
|
|
2379
|
-
return '
|
|
2432
|
+
if (v === 'hold' || v === 'daily' || v === 'session')
|
|
2433
|
+
return 'hold';
|
|
2380
2434
|
if (v === 'never' || v === 'none')
|
|
2381
2435
|
return 'never';
|
|
2382
|
-
throw new Error(`Invalid policy '${raw}'. Use 'always', '
|
|
2436
|
+
throw new Error(`Invalid policy '${raw}'. Use 'always', 'hold', or 'never'.`);
|
|
2383
2437
|
}
|
|
2384
2438
|
/**
|
|
2385
2439
|
* Gate a create/switch to the `never` prompt-policy behind an explicit,
|
|
@@ -2414,13 +2468,27 @@ async function confirmNeverPolicyInteractive(bundleName) {
|
|
|
2414
2468
|
return confirm({ message: `Store '${bundleName}' WITHOUT biometry protection?`, default: false });
|
|
2415
2469
|
}
|
|
2416
2470
|
/** Validate a --backend value, exiting with a clear message on a bad one. */
|
|
2417
|
-
function
|
|
2418
|
-
const
|
|
2471
|
+
export function secretsDefaultBackend() {
|
|
2472
|
+
const raw = readMeta().secrets?.backend;
|
|
2473
|
+
if (raw === undefined)
|
|
2474
|
+
return 'keychain';
|
|
2475
|
+
if (raw === 'keychain' || raw === 'file' || raw === 'vault')
|
|
2476
|
+
return raw;
|
|
2477
|
+
console.error(chalk.red(`Invalid secrets.backend '${raw}'. Use 'keychain', 'file', or 'vault'.`));
|
|
2478
|
+
process.exit(1);
|
|
2479
|
+
}
|
|
2480
|
+
function parseBackendOpt(raw, defaultBackend = 'keychain') {
|
|
2481
|
+
const v = (raw ?? defaultBackend).toLowerCase();
|
|
2482
|
+
if (!raw && v === 'vault')
|
|
2483
|
+
return 'vault';
|
|
2419
2484
|
if (v === 'keychain' || v === 'file')
|
|
2420
2485
|
return v;
|
|
2421
2486
|
console.error(chalk.red(`Invalid --backend '${raw}'. Use 'keychain' or 'file'. For cross-machine file sync, pass --synced.`));
|
|
2422
2487
|
process.exit(1);
|
|
2423
2488
|
}
|
|
2489
|
+
function resolveBackendOpt(raw) {
|
|
2490
|
+
return parseBackendOpt(raw, raw === undefined ? secretsDefaultBackend() : 'keychain');
|
|
2491
|
+
}
|
|
2424
2492
|
/** Human-readable "locks in 3 hours" / "locks in 5 minutes" from an epoch-ms expiry. */
|
|
2425
2493
|
function humanRemaining(expiresAt) {
|
|
2426
2494
|
const ms = expiresAt - Date.now();
|
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
|
|
5
5
|
* (RUSH-1415) can shell out to nudge a stalled agent with "continue".
|
|
6
6
|
*
|
|
7
|
-
* Resolution: find the active session by id,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Resolution: find the active session by id, then resolve its exact split through
|
|
8
|
+
* the SAME canonical resolver the watchdog uses — `resolveInjectTargetForSession`
|
|
9
|
+
* (lib/terminal/resolve.ts), precedence tmux > iterm > vscodium > pty. Sharing one
|
|
10
|
+
* resolver keeps the manual unblock path and the watchdog in agreement on which
|
|
11
|
+
* sessions are addressable (a prior duplicate resolver read only `provenance.reply`
|
|
12
|
+
* and could not address a VSCodium/Cursor terminal the watchdog handled fine).
|
|
13
|
+
* `--pane`/`--pty` target a backend directly when the handle is already known.
|
|
11
14
|
*/
|
|
12
15
|
import type { Command } from 'commander';
|
|
13
16
|
/** Attach the `inject` subcommand to an existing `sessions` command. */
|
|
@@ -4,33 +4,32 @@
|
|
|
4
4
|
* (`injectIntoTerminal`, src/lib/terminal/inject.ts), so a native watchdog
|
|
5
5
|
* (RUSH-1415) can shell out to nudge a stalled agent with "continue".
|
|
6
6
|
*
|
|
7
|
-
* Resolution: find the active session by id,
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* Resolution: find the active session by id, then resolve its exact split through
|
|
8
|
+
* the SAME canonical resolver the watchdog uses — `resolveInjectTargetForSession`
|
|
9
|
+
* (lib/terminal/resolve.ts), precedence tmux > iterm > vscodium > pty. Sharing one
|
|
10
|
+
* resolver keeps the manual unblock path and the watchdog in agreement on which
|
|
11
|
+
* sessions are addressable (a prior duplicate resolver read only `provenance.reply`
|
|
12
|
+
* and could not address a VSCodium/Cursor terminal the watchdog handled fine).
|
|
13
|
+
* `--pane`/`--pty` target a backend directly when the handle is already known.
|
|
11
14
|
*/
|
|
12
15
|
import chalk from 'chalk';
|
|
13
16
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
14
|
-
import {
|
|
17
|
+
import { resolveInjectTargetForSession } from '../lib/terminal/resolve.js';
|
|
15
18
|
import { injectIntoTerminal } from '../lib/terminal/index.js';
|
|
16
19
|
import { setHelpSections } from '../lib/help.js';
|
|
17
|
-
/** Resolve a session id (short or full) to an addressable terminal target
|
|
20
|
+
/** Resolve a session id (short or full) to an addressable terminal target, via the
|
|
21
|
+
* same resolver the watchdog uses so both agree on what is reachable. */
|
|
18
22
|
async function resolveTarget(sessionId) {
|
|
19
23
|
const sessions = await getActiveSessions();
|
|
20
24
|
const match = sessions.find((s) => s.sessionId === sessionId || (s.sessionId != null && s.sessionId.startsWith(sessionId)));
|
|
21
25
|
if (!match)
|
|
22
26
|
return { target: null, reason: `No active session matches "${sessionId}".` };
|
|
23
|
-
const
|
|
24
|
-
if (!
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
reason: `Session "${sessionId}" has no addressable reply rail (not running under tmux). Relaunch it under a tmux/pty rail to inject.`,
|
|
28
|
-
};
|
|
27
|
+
const resolution = resolveInjectTargetForSession(match);
|
|
28
|
+
if (!resolution.addressable) {
|
|
29
|
+
// Surface the resolver's precise reason (host/rail), not a generic "not tmux".
|
|
30
|
+
return { target: null, reason: `Session "${sessionId}": ${resolution.reason}` };
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
if (!target)
|
|
32
|
-
return { target: null, reason: `Session "${sessionId}" reply rail is not injectable.` };
|
|
33
|
-
return { target };
|
|
32
|
+
return { target: resolution.target };
|
|
34
33
|
}
|
|
35
34
|
async function runInject(sessionId, text, options) {
|
|
36
35
|
// Direct-target shortcuts skip the session lookup — the watchdog often already
|
|
@@ -97,8 +96,9 @@ export function registerSessionsInjectCommand(sessionsCmd) {
|
|
|
97
96
|
notes: `
|
|
98
97
|
- Ink-TUI Enter semantics: by default the text and Enter are two separate
|
|
99
98
|
writes, which is what Claude's Ink TUI needs. --combined fuses them.
|
|
100
|
-
-
|
|
101
|
-
|
|
99
|
+
- A session is addressable by id when it resolves to a precise split —
|
|
100
|
+
tmux, iTerm, a VSCodium/Cursor/VS Code integrated terminal, or a pty
|
|
101
|
+
(resolveInjectTargetForSession). Use --pane/--pty for direct targeting.
|
|
102
102
|
- Built on the Terminal Engine (src/lib/terminal): --host runs the tmux /
|
|
103
103
|
AppleScript spec over the same SSH transport the launch engine uses.
|
|
104
104
|
`,
|
|
@@ -11,7 +11,7 @@ import chalk from 'chalk';
|
|
|
11
11
|
import { truncate, humanDuration } from '../lib/format.js';
|
|
12
12
|
import { parseSession, sanitizeForTerminal } from '../lib/session/parse.js';
|
|
13
13
|
import { cleanSessionPrompt, extractSessionTopic } from '../lib/session/prompt.js';
|
|
14
|
-
import { linkPath, linkUrl, relativeToCwd } from '../lib/session/render.js';
|
|
14
|
+
import { linkPath, linkUrl, relativeToCwd, shortenModel } from '../lib/session/render.js';
|
|
15
15
|
import { linearIssueUrl } from '../lib/session/linear.js';
|
|
16
16
|
import { extractTodoProgress, WORKTREE_RE } from '../lib/session/state.js';
|
|
17
17
|
import { renderMarkdown } from '../lib/markdown.js';
|
|
@@ -137,7 +137,7 @@ function displayAgent(agent) {
|
|
|
137
137
|
}
|
|
138
138
|
const DOT = chalk.gray(' · ');
|
|
139
139
|
function formatHeader(session, events) {
|
|
140
|
-
const model = extractModel(events);
|
|
140
|
+
const model = extractModel(events) || session.model;
|
|
141
141
|
const { startedAgo, duration } = extractTiming(events);
|
|
142
142
|
const totalMessages = session.messageCount ?? countMessages(events);
|
|
143
143
|
const totalTokens = session.tokenCount;
|
|
@@ -147,7 +147,7 @@ function formatHeader(session, events) {
|
|
|
147
147
|
if (session.shortId)
|
|
148
148
|
line1.push(chalk.dim(session.shortId));
|
|
149
149
|
if (model)
|
|
150
|
-
line1.push(chalk.bold.white(model));
|
|
150
|
+
line1.push(chalk.bold.white(shortenModel(model)));
|
|
151
151
|
if (session.account)
|
|
152
152
|
line1.push(chalk.gray(session.account));
|
|
153
153
|
// Line 2: cwd · project · branch · started X ago · lasted Y
|
|
@@ -285,6 +285,8 @@ function formatCompactPreview(events, session) {
|
|
|
285
285
|
let planFile = '';
|
|
286
286
|
/** Latest checklist from the transcript (Claude TodoWrite / Codex update_plan). */
|
|
287
287
|
let latestTodos;
|
|
288
|
+
let subAgentCount = 0;
|
|
289
|
+
const toolTags = new Set();
|
|
288
290
|
for (const event of events) {
|
|
289
291
|
if (event.type === 'message') {
|
|
290
292
|
if (event.role === 'user' && !firstUser && event.content) {
|
|
@@ -298,6 +300,11 @@ function formatCompactPreview(events, session) {
|
|
|
298
300
|
}
|
|
299
301
|
else if (event.type === 'tool_use' && !event._local) {
|
|
300
302
|
const tool = event.tool || '';
|
|
303
|
+
const command = event.command || '';
|
|
304
|
+
for (const tag of classifySessionTool(tool, command))
|
|
305
|
+
toolTags.add(tag);
|
|
306
|
+
if (isSubAgentTool(tool, command))
|
|
307
|
+
subAgentCount++;
|
|
301
308
|
const p = event.path || event.args?.file_path || event.args?.path || '';
|
|
302
309
|
if (['Read', 'read_file', 'view_file', 'cat_file', 'get_file'].includes(tool) && p) {
|
|
303
310
|
filesRead.add(p);
|
|
@@ -369,6 +376,13 @@ function formatCompactPreview(events, session) {
|
|
|
369
376
|
if (activity.length) {
|
|
370
377
|
lines.push(chalk.cyan('Changes: ') + activity.join(chalk.gray(' · ')));
|
|
371
378
|
}
|
|
379
|
+
const metadata = [
|
|
380
|
+
...toolTags,
|
|
381
|
+
subAgentCount ? `${subAgentCount} sub-agent${subAgentCount === 1 ? '' : 's'}` : '',
|
|
382
|
+
].filter(Boolean);
|
|
383
|
+
if (metadata.length) {
|
|
384
|
+
lines.push(chalk.cyan('Meta: ') + chalk.gray(metadata.join(' · ')));
|
|
385
|
+
}
|
|
372
386
|
// Tool mix (top 4) — what kind of work this was.
|
|
373
387
|
const hist = toolHistogram(toolCounts, 4);
|
|
374
388
|
if (hist.length) {
|
|
@@ -403,6 +417,21 @@ function formatCompactPreview(events, session) {
|
|
|
403
417
|
}
|
|
404
418
|
return lines.map(l => ' ' + l).join('\n');
|
|
405
419
|
}
|
|
420
|
+
function classifySessionTool(tool, command) {
|
|
421
|
+
const toolName = tool.toLowerCase();
|
|
422
|
+
const commandUses = (surface) => (new RegExp(`\\b(?:agents|ag)\\b[^\\n;&|]*\\b${surface}\\b`).test(command.toLowerCase()));
|
|
423
|
+
const tags = [];
|
|
424
|
+
if (/browser|webfetch|websearch/.test(toolName) || commandUses('browser'))
|
|
425
|
+
tags.push('browser');
|
|
426
|
+
if (/computer/.test(toolName) || commandUses('computer'))
|
|
427
|
+
tags.push('computer');
|
|
428
|
+
return tags;
|
|
429
|
+
}
|
|
430
|
+
function isSubAgentTool(tool, command) {
|
|
431
|
+
if (/^(Agent|Task)$/i.test(tool))
|
|
432
|
+
return true;
|
|
433
|
+
return /\b(?:agents|ag)\b[^\n;&|]*\b(?:run|cloud\s+run|teams\s+(?:add|start))\b/.test(command);
|
|
434
|
+
}
|
|
406
435
|
/**
|
|
407
436
|
* Unique directories the session touched, compact and human-readable.
|
|
408
437
|
* Prefer `session.dirsTouched` when the parser teammate has populated it;
|
|
@@ -1,2 +1,18 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import { type Packing } from '../lib/terminal/index.js';
|
|
3
|
+
interface ResumeOptions {
|
|
4
|
+
agent?: string;
|
|
5
|
+
all?: boolean;
|
|
6
|
+
teams?: boolean;
|
|
7
|
+
since?: string;
|
|
8
|
+
limit?: string;
|
|
9
|
+
host?: string;
|
|
10
|
+
iterm?: boolean;
|
|
11
|
+
ghostty?: boolean;
|
|
12
|
+
tmux?: boolean;
|
|
13
|
+
vscodium?: boolean;
|
|
14
|
+
splits?: boolean;
|
|
15
|
+
}
|
|
2
16
|
export declare function registerSessionsResumeCommand(sessionsCmd: Command): void;
|
|
17
|
+
export declare function resolveResumePacking(options: Pick<ResumeOptions, 'splits'>): Packing;
|
|
18
|
+
export {};
|
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Unlike the single-select picker behind bare `agents sessions` (which resumes
|
|
6
6
|
* one session in place), this opens a checkbox picker, then asks where the
|
|
7
|
-
* chosen sessions should resume. By default
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* chosen sessions should resume. By default each session opens in its own tab in
|
|
8
|
+
* the terminal you're in — iTerm / Ghostty / tmux, locally or on a remote host
|
|
9
|
+
* via --host. `--splits` opts into packing two sessions side by side per tab.
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import chalk from 'chalk';
|
|
@@ -36,25 +36,24 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
36
36
|
.option('--ghostty', 'Force the Ghostty backend')
|
|
37
37
|
.option('--tmux', 'Force the tmux backend')
|
|
38
38
|
.option('--vscodium', 'Force the VSCodium agent-terminal backend (swarm-ext)')
|
|
39
|
-
.option('--
|
|
39
|
+
.option('--splits', 'Pack two sessions side by side per tab (default: one tab per session)');
|
|
40
40
|
setHelpSections(cmd, {
|
|
41
41
|
examples: `
|
|
42
|
-
# Pick several sessions;
|
|
42
|
+
# Pick several sessions; each opens in its own tab
|
|
43
43
|
agents sessions resume
|
|
44
44
|
|
|
45
45
|
# Pre-filter the pool before selecting (space in the filter → use [query])
|
|
46
46
|
agents sessions resume "auth middleware"
|
|
47
47
|
|
|
48
|
-
# Force a backend /
|
|
48
|
+
# Force a backend / side-by-side splits / a remote host
|
|
49
49
|
agents sessions resume --ghostty
|
|
50
50
|
agents sessions resume --vscodium
|
|
51
|
-
agents sessions resume --
|
|
51
|
+
agents sessions resume --splits
|
|
52
52
|
agents sessions resume --host zion --tmux
|
|
53
53
|
`,
|
|
54
54
|
notes: `
|
|
55
55
|
- space toggles a session, enter confirms; tab toggles the preview pane.
|
|
56
|
-
- Layout:
|
|
57
|
-
- VSCodium defaults to one editor tab per session (matches swarm-ext's native agent-terminal UX); the others default to two-per-tab.
|
|
56
|
+
- Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
|
|
58
57
|
- Backend: auto-detected from the terminal you're in (iTerm / Ghostty / tmux); override with --iterm/--ghostty/--tmux/--vscodium.
|
|
59
58
|
- --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --host too).
|
|
60
59
|
- --host <alias> resumes on a remote machine over the same SSH transport as 'sessions --host' (defaults to tmux).
|
|
@@ -153,10 +152,9 @@ async function sessionsResumeAction(query, options) {
|
|
|
153
152
|
await resumeSessionInPlace(it.session);
|
|
154
153
|
return;
|
|
155
154
|
}
|
|
156
|
-
// 5b. Fan out through the engine.
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
const packing = options.tabs || backend === 'vscodium-agent' ? 'tabs' : 'two-per-tab';
|
|
155
|
+
// 5b. Fan out through the engine. Full-width tabs are the default for batch
|
|
156
|
+
// recovery; callers can explicitly opt into pairs of side-by-side panes.
|
|
157
|
+
const packing = resolveResumePacking(options);
|
|
160
158
|
const where = options.host ? `${backend} on ${options.host}` : backend;
|
|
161
159
|
console.log(chalk.gray(`Opening ${items.length} session${items.length === 1 ? '' : 's'} in ${where} (${packing})…`));
|
|
162
160
|
const results = await openSurfaces(items.map((it) => ({ cwd: it.cwd, command: it.command })), { backend, host: options.host, packing });
|
|
@@ -174,6 +172,9 @@ async function sessionsResumeAction(query, options) {
|
|
|
174
172
|
});
|
|
175
173
|
console.log(chalk.gray(`\nOpened ${opened}/${items.length} in ${where}.`));
|
|
176
174
|
}
|
|
175
|
+
export function resolveResumePacking(options) {
|
|
176
|
+
return options.splits ? 'two-per-tab' : 'tabs';
|
|
177
|
+
}
|
|
177
178
|
/**
|
|
178
179
|
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
179
180
|
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|