@phnx-labs/agents-cli 1.22.26 → 1.22.28
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 +293 -0
- package/README.md +80 -27
- package/dist/bin/agents +0 -0
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/doctor.js +24 -10
- package/dist/commands/exec.js +71 -26
- package/dist/commands/hosts.js +4 -3
- package/dist/commands/insights.js +39 -5
- package/dist/commands/output.js +100 -25
- package/dist/commands/perf.d.ts +10 -0
- package/dist/commands/perf.js +14 -6
- package/dist/commands/projects.js +24 -10
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +31 -4
- package/dist/commands/run-account-picker.d.ts +37 -0
- package/dist/commands/run-account-picker.js +101 -17
- package/dist/commands/secrets.js +86 -2
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +73 -7
- package/dist/commands/sessions.js +391 -49
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.js +22 -11
- package/dist/index.js +16 -20
- package/dist/lib/activity.js +45 -99
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +56 -3
- package/dist/lib/cloud/host.js +1 -0
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +4 -1
- package/dist/lib/devices/doctor-findings.js +15 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/resolve-target.d.ts +1 -0
- package/dist/lib/devices/resolve-target.js +9 -2
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +2 -2
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.js +9 -7
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/git.d.ts +9 -3
- package/dist/lib/git.js +39 -22
- package/dist/lib/hooks/profile.d.ts +12 -1
- package/dist/lib/hooks/profile.js +6 -1
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +14 -7
- package/dist/lib/hosts/dispatch.d.ts +10 -0
- package/dist/lib/hosts/dispatch.js +85 -25
- package/dist/lib/hosts/logs.js +11 -1
- package/dist/lib/hosts/passthrough.d.ts +27 -0
- package/dist/lib/hosts/passthrough.js +19 -10
- package/dist/lib/hosts/progress.d.ts +12 -1
- package/dist/lib/hosts/progress.js +41 -8
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +45 -4
- package/dist/lib/hosts/ready.js +124 -20
- package/dist/lib/hosts/reconcile.d.ts +2 -1
- package/dist/lib/hosts/reconcile.js +24 -7
- package/dist/lib/hosts/reconnect.d.ts +45 -12
- package/dist/lib/hosts/reconnect.js +94 -36
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +4 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +3 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/menubar/snapshot.js +15 -2
- package/dist/lib/models.d.ts +42 -6
- package/dist/lib/models.js +55 -153
- package/dist/lib/perf/db.js +16 -3
- package/dist/lib/perf/types.d.ts +12 -1
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/project-probe.d.ts +22 -3
- package/dist/lib/project-probe.js +105 -17
- package/dist/lib/project-status.d.ts +9 -0
- package/dist/lib/project-status.js +15 -0
- package/dist/lib/redact.js +8 -3
- package/dist/lib/refresh.js +22 -11
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/rotate.d.ts +22 -0
- package/dist/lib/rotate.js +26 -0
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/runner.d.ts +9 -1
- package/dist/lib/runner.js +182 -17
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- 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 +19 -3
- package/dist/lib/secrets/agent.js +61 -22
- package/dist/lib/secrets/audit.d.ts +1 -1
- package/dist/lib/secrets/audit.js +2 -0
- package/dist/lib/secrets/bundles.d.ts +3 -3
- package/dist/lib/secrets/bundles.js +15 -5
- package/dist/lib/secrets/filestore.d.ts +2 -0
- package/dist/lib/secrets/filestore.js +4 -0
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/session-store.d.ts +7 -0
- package/dist/lib/secrets/session-store.js +21 -0
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/active.d.ts +49 -0
- package/dist/lib/session/active.js +175 -11
- package/dist/lib/session/bash-command.d.ts +16 -0
- package/dist/lib/session/bash-command.js +65 -0
- package/dist/lib/session/db.d.ts +23 -2
- package/dist/lib/session/db.js +86 -20
- package/dist/lib/session/discover.d.ts +21 -1
- package/dist/lib/session/discover.js +124 -32
- package/dist/lib/session/insights.d.ts +19 -0
- package/dist/lib/session/insights.js +78 -10
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-bundle.js +1 -1
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +34 -1
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/staleness/detectors/skills.d.ts +2 -0
- package/dist/lib/staleness/detectors/skills.js +17 -2
- package/dist/lib/staleness/index.d.ts +10 -1
- package/dist/lib/staleness/index.js +28 -3
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/teams/agents.d.ts +1 -0
- package/dist/lib/teams/agents.js +38 -11
- package/dist/lib/teams/placement-probe.d.ts +1 -1
- package/dist/lib/teams/placement-probe.js +24 -24
- package/dist/lib/teams/remoteWorktree.d.ts +11 -7
- package/dist/lib/teams/remoteWorktree.js +29 -27
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/dist/lib/versions.d.ts +1 -0
- package/dist/lib/versions.js +40 -22
- package/package.json +3 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { select } from '@inquirer/prompts';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { agentLabel } from '../lib/agents.js';
|
|
3
|
-
import {
|
|
4
|
+
import { loginHint } from '../lib/signin-badge.js';
|
|
5
|
+
import { collectRunCandidates, readinessFromCandidate, isSignInRecoverable, } from '../lib/rotate.js';
|
|
4
6
|
import { compareVersions, getGlobalDefault } from '../lib/versions.js';
|
|
5
7
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
6
8
|
const CANCEL_SELECTION = '__agents_cancel_account_selection__';
|
|
@@ -30,14 +32,21 @@ export function formatAccountLimits(candidate) {
|
|
|
30
32
|
})
|
|
31
33
|
.join(' · ');
|
|
32
34
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Why a row cannot be picked. Only a THROTTLE disables a row: the account is
|
|
37
|
+
* signed in and out of capacity, so launching it just hammers an exhausted
|
|
38
|
+
* account (RUSH-2132) and nothing the user does at this prompt helps.
|
|
39
|
+
*
|
|
40
|
+
* An AUTH exclusion (`signed_out` / `revoked`) is deliberately NOT disabled —
|
|
41
|
+
* the harness's own TUI is the login surface, so picking that row and launching
|
|
42
|
+
* is the only way to sign in through agents-cli. Disabling it left a fully
|
|
43
|
+
* logged-out harness with no reachable account at all (RUSH-2334).
|
|
44
|
+
*/
|
|
45
|
+
function disabledReason(candidate, readiness) {
|
|
35
46
|
if (readiness.ready)
|
|
36
47
|
return undefined;
|
|
37
|
-
if (readiness
|
|
38
|
-
return
|
|
39
|
-
if (readiness.reason === 'revoked')
|
|
40
|
-
return 'needs re-login';
|
|
48
|
+
if (isSignInRecoverable(readiness))
|
|
49
|
+
return undefined;
|
|
41
50
|
if (readiness.reason === 'out_of_credits')
|
|
42
51
|
return 'out of credits';
|
|
43
52
|
const windows = candidate.usageSnapshot?.windows ?? [];
|
|
@@ -53,24 +62,44 @@ function disabledReason(candidate) {
|
|
|
53
62
|
/** Build aligned picker rows with usable accounts first and unsafe rows disabled. */
|
|
54
63
|
export function buildRunAccountChoices(candidates, globalDefault) {
|
|
55
64
|
const rows = candidates.map((candidate) => {
|
|
56
|
-
const
|
|
65
|
+
const readiness = readinessFromCandidate(candidate);
|
|
66
|
+
const disabled = disabledReason(candidate, readiness);
|
|
67
|
+
const signInRequired = isSignInRecoverable(readiness);
|
|
57
68
|
const version = candidate.version === globalDefault
|
|
58
69
|
? `${candidate.version} (default)`
|
|
59
70
|
: candidate.version;
|
|
71
|
+
// `revoked` is signed-in-but-rejected, so it reads as a re-login rather
|
|
72
|
+
// than "logged out"; every throttle is still a signed-in account.
|
|
73
|
+
const authReason = readiness.ready ? null : readiness.reason;
|
|
74
|
+
const status = authReason === 'revoked'
|
|
75
|
+
? 'needs re-login'
|
|
76
|
+
: authReason === 'signed_out'
|
|
77
|
+
? 'logged out'
|
|
78
|
+
: 'logged in';
|
|
60
79
|
return {
|
|
61
80
|
candidate,
|
|
62
81
|
account: candidate.accountLabel || 'account unavailable',
|
|
63
82
|
version,
|
|
64
|
-
status
|
|
83
|
+
status,
|
|
65
84
|
plan: candidate.usageSnapshot?.plan ?? candidate.plan ?? 'plan unavailable',
|
|
66
|
-
|
|
85
|
+
// An auth-blocked row shows what picking it DOES; its quota is moot until
|
|
86
|
+
// there is a credential to spend it with.
|
|
87
|
+
limits: authReason === 'revoked'
|
|
88
|
+
? 'launch to re-authenticate'
|
|
89
|
+
: authReason === 'signed_out'
|
|
90
|
+
? 'launch to sign in'
|
|
91
|
+
: formatAccountLimits(candidate),
|
|
67
92
|
disabled,
|
|
68
|
-
ready:
|
|
93
|
+
ready: readiness.ready,
|
|
94
|
+
signInRequired,
|
|
69
95
|
};
|
|
70
96
|
});
|
|
97
|
+
// Ready accounts first, then the ones a login would unlock (actionable), then
|
|
98
|
+
// the throttled rows the user can do nothing about at this prompt.
|
|
99
|
+
const rank = (row) => row.ready ? 0 : row.signInRequired ? 1 : 2;
|
|
71
100
|
rows.sort((a, b) => {
|
|
72
|
-
if (a
|
|
73
|
-
return a
|
|
101
|
+
if (rank(a) !== rank(b))
|
|
102
|
+
return rank(a) - rank(b);
|
|
74
103
|
const aDefault = a.candidate.version === globalDefault;
|
|
75
104
|
const bDefault = b.candidate.version === globalDefault;
|
|
76
105
|
if (aDefault !== bDefault)
|
|
@@ -92,8 +121,56 @@ export function buildRunAccountChoices(candidates, globalDefault) {
|
|
|
92
121
|
value: row.candidate.version,
|
|
93
122
|
disabled: row.disabled,
|
|
94
123
|
ready: row.ready,
|
|
124
|
+
signInRequired: row.signInRequired,
|
|
95
125
|
}));
|
|
96
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Whether a zero-healthy run may recover by launching for a login, or must keep
|
|
129
|
+
* failing loud. Three inputs, all of which have to hold:
|
|
130
|
+
*
|
|
131
|
+
* - `recoverable` — at least one excluded account is only auth-blocked. An
|
|
132
|
+
* all-throttled set is never launched (RUSH-2132): only a window reset clears it.
|
|
133
|
+
* - `tty` — a login needs a human present; off a TTY nobody can complete one.
|
|
134
|
+
* - `json` — `--json` marks a MACHINE consumer, which must never be handed a
|
|
135
|
+
* picker or dropped into a login TUI. This mirrors the canonical
|
|
136
|
+
* `Surface.interactive = tty && !json` in `commands/utils.ts`; a `--json` caller
|
|
137
|
+
* gets the parseable fail-loud error instead.
|
|
138
|
+
*/
|
|
139
|
+
export function signInLaunchDecision(input) {
|
|
140
|
+
const humanPresent = input.tty && !input.json;
|
|
141
|
+
return input.recoverable > 0 && humanPresent ? 'launch' : 'fail-loud';
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Choose which installed version to launch so the user can authenticate, when a
|
|
145
|
+
* strategy found zero healthy accounts but at least one is merely signed out
|
|
146
|
+
* (RUSH-2334). Returns the version to launch, or null if the user cancelled.
|
|
147
|
+
*
|
|
148
|
+
* A single candidate does NOT prompt — a one-item picker is pure noise, and the
|
|
149
|
+
* only thing to decide has one answer. Several candidates fall through to the
|
|
150
|
+
* normal account picker, which shows every account with its state so the choice
|
|
151
|
+
* is informed (throttled rows stay disabled there).
|
|
152
|
+
*
|
|
153
|
+
* Callers MUST have already confirmed an interactive terminal: off a TTY there
|
|
154
|
+
* is nobody to complete the login, and the run should fail loud instead.
|
|
155
|
+
*/
|
|
156
|
+
export async function pickSignInLaunchVersion(agent, recoverable, quiet = false) {
|
|
157
|
+
if (recoverable.length === 0)
|
|
158
|
+
return null;
|
|
159
|
+
if (recoverable.length > 1) {
|
|
160
|
+
const selected = await pickRunAccountCandidate(agent);
|
|
161
|
+
return selected?.version ?? null;
|
|
162
|
+
}
|
|
163
|
+
const [only] = recoverable;
|
|
164
|
+
if (!quiet) {
|
|
165
|
+
const readiness = readinessFromCandidate(only);
|
|
166
|
+
const why = !readiness.ready && readiness.reason === 'revoked'
|
|
167
|
+
? 'has no valid credential (the server rejected its token)'
|
|
168
|
+
: 'has no signed-in account';
|
|
169
|
+
process.stderr.write(chalk.yellow(`${agentLabel(agent)} ${why} — launching ${agent}@${only.version} so you can sign in.\n`));
|
|
170
|
+
process.stderr.write(chalk.gray(`Sign in with: ${loginHint(agent)}\n`));
|
|
171
|
+
}
|
|
172
|
+
return only.version;
|
|
173
|
+
}
|
|
97
174
|
/** Prompt for one safe installed account/version. A cancelled picker launches nothing. */
|
|
98
175
|
export async function pickRunAccountCandidate(agent) {
|
|
99
176
|
if (!isInteractiveTerminal()) {
|
|
@@ -107,9 +184,14 @@ export async function pickRunAccountCandidate(agent) {
|
|
|
107
184
|
throw new Error(`No installed ${agentLabel(agent)} versions are available. Run: agents add ${agent}@latest`);
|
|
108
185
|
}
|
|
109
186
|
const choices = buildRunAccountChoices(candidates, getGlobalDefault(agent));
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
187
|
+
// "Selectable" is broader than "ready": an auth-blocked row is pickable so the
|
|
188
|
+
// launch can carry you into the harness's login (RUSH-2334). Only offer the
|
|
189
|
+
// bail-out row when literally nothing can be chosen — i.e. every account is
|
|
190
|
+
// throttled, which no amount of signing in fixes.
|
|
191
|
+
const hasSelectableAccount = choices.some((choice) => !choice.disabled);
|
|
192
|
+
const needsSignIn = choices.some((choice) => choice.signInRequired);
|
|
193
|
+
const promptChoices = choices.map(({ ready: _ready, signInRequired: _signInRequired, ...choice }) => choice);
|
|
194
|
+
if (!hasSelectableAccount) {
|
|
113
195
|
promptChoices.push({
|
|
114
196
|
name: 'No usable accounts — cancel',
|
|
115
197
|
value: CANCEL_SELECTION,
|
|
@@ -117,7 +199,9 @@ export async function pickRunAccountCandidate(agent) {
|
|
|
117
199
|
}
|
|
118
200
|
try {
|
|
119
201
|
const version = await select({
|
|
120
|
-
message:
|
|
202
|
+
message: needsSignIn
|
|
203
|
+
? `Select a ${agentLabel(agent)} account for this run (pick a logged-out one to sign in):`
|
|
204
|
+
: `Select a ${agentLabel(agent)} account for this run:`,
|
|
121
205
|
choices: promptChoices,
|
|
122
206
|
loop: false,
|
|
123
207
|
});
|
package/dist/commands/secrets.js
CHANGED
|
@@ -24,8 +24,9 @@ import { encryptForFallback, decryptForFallback } from '../lib/secrets/filestore
|
|
|
24
24
|
import { getKeychainToken, hasKeychainToken, secretsKeychainItem, setKeychainToken, maybeAutoRekey, } from '../lib/secrets/index.js';
|
|
25
25
|
import { assertOpAvailable, createPasswordItem, deleteItemByTitle, extractSecrets, itemExistsByTitle, listItems, listVaults, } from '../lib/onepassword.js';
|
|
26
26
|
import { GLOBAL_HARNESS } from '../lib/secrets/scope.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
27
|
+
import { createSecretLease, selectLeasedEnv } from '../lib/secrets/lease.js';
|
|
28
|
+
import { secretsHoldMs, secretsAgentDurable, agentLoad, agentLock, agentPing, agentSocketExists, agentStatus, agentRevoke, ensureAgentRunning, runAgentLoadFromStdin, runSecretsAgent, uninstallSecretsAgentService, } from '../lib/secrets/agent.js';
|
|
29
|
+
import { activeLeaseSessions, saveSession, deleteBundleSessions, deleteAllSessions, deleteLeaseSession } from '../lib/secrets/session-store.js';
|
|
29
30
|
import { getCliVersionFresh } from '../lib/version.js';
|
|
30
31
|
import { readMeta } from '../lib/state.js';
|
|
31
32
|
import { parseDuration } from '../lib/hooks/cache.js';
|
|
@@ -2426,6 +2427,89 @@ Examples:
|
|
|
2426
2427
|
console.log(password);
|
|
2427
2428
|
}
|
|
2428
2429
|
});
|
|
2430
|
+
cmd
|
|
2431
|
+
.command('lease <bundle>')
|
|
2432
|
+
.description('Hold only an explicit subset of a bundle until an independent expiry.')
|
|
2433
|
+
.requiredOption('--keys <keys>', 'Comma-separated key subset')
|
|
2434
|
+
.requiredOption('--for <duration>', 'Lease duration, for example 30m or 8h')
|
|
2435
|
+
.option('--agent <agent>', 'Narrow the lease to one harness; default is global')
|
|
2436
|
+
.option('--durable', 'Keep the lease across sleep as well as broker restart')
|
|
2437
|
+
.action(async (name, opts) => {
|
|
2438
|
+
if (process.platform !== 'darwin') {
|
|
2439
|
+
throw new Error('Scoped lease brokering is not available on this platform yet.');
|
|
2440
|
+
}
|
|
2441
|
+
const seconds = parseDuration(opts.for);
|
|
2442
|
+
if (seconds === null)
|
|
2443
|
+
throw new Error(`Invalid lease duration '${opts.for}'.`);
|
|
2444
|
+
const ttlMs = seconds * 1000;
|
|
2445
|
+
const harness = opts.agent || GLOBAL_HARNESS;
|
|
2446
|
+
const { bundle, env } = readAndResolveBundleEnv(name, {
|
|
2447
|
+
noAgent: true,
|
|
2448
|
+
interactiveUnlock: true,
|
|
2449
|
+
caller: 'lease secrets',
|
|
2450
|
+
agent: harness,
|
|
2451
|
+
keyMode: 'storage',
|
|
2452
|
+
});
|
|
2453
|
+
const lease = createSecretLease({
|
|
2454
|
+
bundle: name,
|
|
2455
|
+
keys: opts.keys.split(','),
|
|
2456
|
+
availableKeys: Object.keys(env),
|
|
2457
|
+
ttlMs,
|
|
2458
|
+
harness,
|
|
2459
|
+
sleepPersist: opts.durable,
|
|
2460
|
+
});
|
|
2461
|
+
const leasedEnv = selectLeasedEnv(lease, env);
|
|
2462
|
+
if (!(await ensureAgentRunning()) || !(await agentLoad(name, bundle, leasedEnv, ttlMs, harness, lease))) {
|
|
2463
|
+
throw new Error('Could not load the scoped lease into the secrets broker.');
|
|
2464
|
+
}
|
|
2465
|
+
ensureDaemonStarted();
|
|
2466
|
+
saveSession(name, {
|
|
2467
|
+
bundle,
|
|
2468
|
+
env: leasedEnv,
|
|
2469
|
+
expiresAt: lease.expiresAt,
|
|
2470
|
+
sleepPersist: lease.sleepPersist,
|
|
2471
|
+
harness,
|
|
2472
|
+
lease,
|
|
2473
|
+
});
|
|
2474
|
+
emitSecretAudit({
|
|
2475
|
+
event: 'secrets.unlocked', bundle: name, operation: 'lease', source: lease.sleepPersist ? 'broker+durable' : 'broker',
|
|
2476
|
+
status: 'success', keys: lease.keys, keyCount: lease.keys.length, agent: harness, ttlMs: lease.expiresAt - lease.createdAt,
|
|
2477
|
+
});
|
|
2478
|
+
console.log(`${chalk.green('leased')} ${chalk.cyan(lease.id)} ${chalk.gray(`(${name}: ${lease.keys.join(', ')}, ${humanRemaining(lease.expiresAt)})`)}`);
|
|
2479
|
+
});
|
|
2480
|
+
cmd
|
|
2481
|
+
.command('leases')
|
|
2482
|
+
.description('List active scoped secret leases.')
|
|
2483
|
+
.action(async () => {
|
|
2484
|
+
const brokerLeases = (await agentStatus()).filter((entry) => entry.leaseId);
|
|
2485
|
+
const byId = new Map(brokerLeases.map((entry) => [entry.leaseId, entry]));
|
|
2486
|
+
for (const { name, lease } of activeLeaseSessions()) {
|
|
2487
|
+
if (!byId.has(lease.id))
|
|
2488
|
+
byId.set(lease.id, { name, expiresAt: lease.expiresAt, keyCount: lease.keys.length, harness: lease.harness, leaseId: lease.id, keys: lease.keys });
|
|
2489
|
+
}
|
|
2490
|
+
const leases = [...byId.values()];
|
|
2491
|
+
if (leases.length === 0) {
|
|
2492
|
+
console.log(chalk.gray('No active secret leases.'));
|
|
2493
|
+
return;
|
|
2494
|
+
}
|
|
2495
|
+
console.log(`${'ID'.padEnd(24)} ${'BUNDLE'.padEnd(24)} ${'KEYS'.padEnd(24)} EXPIRES IN`);
|
|
2496
|
+
for (const lease of leases) {
|
|
2497
|
+
console.log(`${lease.leaseId.padEnd(24)} ${lease.name.padEnd(24)} ${(lease.keys ?? []).join(',').padEnd(24)} ${humanRemaining(lease.expiresAt)}`);
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
cmd
|
|
2501
|
+
.command('revoke <lease-id>')
|
|
2502
|
+
.description('Revoke one scoped secret lease immediately.')
|
|
2503
|
+
.action(async (leaseId) => {
|
|
2504
|
+
if (agentSocketExists() && !(await agentPing()).reachable) {
|
|
2505
|
+
throw new Error('Cannot revoke while the secrets broker is using an incompatible protocol. Restart the broker and retry.');
|
|
2506
|
+
}
|
|
2507
|
+
const persisted = deleteLeaseSession(leaseId);
|
|
2508
|
+
const wiped = await agentRevoke(leaseId);
|
|
2509
|
+
if (wiped + persisted === 0)
|
|
2510
|
+
throw new Error(`Secret lease '${leaseId}' is not active.`);
|
|
2511
|
+
console.log(chalk.green(`Revoked secret lease ${leaseId}.`));
|
|
2512
|
+
});
|
|
2429
2513
|
cmd
|
|
2430
2514
|
.command('unlock [names...]')
|
|
2431
2515
|
.description('Hold a bundle in the secrets-agent after one Touch ID, so concurrent runs read it without re-prompting (macOS). With --host, unlock FILE-backed bundle(s) on a remote (the passphrase prompt surfaces over the SSH TTY); keychain/biometry bundles are GUI-only and can\'t be remote-unlocked.')
|
|
@@ -19,12 +19,17 @@ import { renderMarkdown } from '../lib/markdown.js';
|
|
|
19
19
|
import { itemPicker } from '../lib/picker.js';
|
|
20
20
|
import { classifyFileChanges, changeCounts, toolHistogram, detectTestResult } from '../lib/session/digest.js';
|
|
21
21
|
import { extractArtifacts, extractHooks, extractLinks, extractRepos, extractSkills } from '../lib/session/highlights.js';
|
|
22
|
-
/** A session whose transcript
|
|
23
|
-
* cross-machine fan-out): its `filePath` is on that peer's disk, so the
|
|
24
|
-
* can't parse it locally — it shows metadata + a "resume there" note
|
|
25
|
-
* Keys off `_remote`, not the machine tag, so locally-readable synced
|
|
26
|
-
* still parse their file normally.
|
|
27
|
-
|
|
22
|
+
/** A session whose transcript FILE is on another machine (folded in over the
|
|
23
|
+
* live cross-machine fan-out): its `filePath` is on that peer's disk, so the
|
|
24
|
+
* preview can't parse it locally — it shows metadata + a "resume there" note
|
|
25
|
+
* instead. Keys off `_remote`, not the machine tag, so locally-readable synced
|
|
26
|
+
* mirrors still parse their file normally.
|
|
27
|
+
*
|
|
28
|
+
* This is the READ rule and only the read rule. Whether a session may be
|
|
29
|
+
* RESUMED here is a different question with a different answer — a mirror is
|
|
30
|
+
* readable but not resumable — and `sessionOwnerDevice`
|
|
31
|
+
* (lib/session/resume-owner.ts) is the single place that answers it (RUSH-2022). */
|
|
32
|
+
function transcriptOnPeerOf(session) {
|
|
28
33
|
return session._remote ? session.machine : undefined;
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
@@ -98,7 +103,7 @@ function sanitizeMeta(s) {
|
|
|
98
103
|
const previewCache = new Map();
|
|
99
104
|
/** Build a cached multi-line preview string for display in the session picker. */
|
|
100
105
|
export function buildPreview(session) {
|
|
101
|
-
const remote =
|
|
106
|
+
const remote = transcriptOnPeerOf(session);
|
|
102
107
|
const cacheKey = remote ? `${remote}:${session.id}` : session.id;
|
|
103
108
|
const cached = previewCache.get(cacheKey);
|
|
104
109
|
if (cached)
|
|
@@ -11,7 +11,7 @@ interface SessionFilterOptions {
|
|
|
11
11
|
all?: boolean;
|
|
12
12
|
teams?: boolean;
|
|
13
13
|
inTeam?: string;
|
|
14
|
-
routine?: boolean;
|
|
14
|
+
routine?: boolean | string;
|
|
15
15
|
since?: string;
|
|
16
16
|
until?: string;
|
|
17
17
|
}
|
|
@@ -187,13 +187,33 @@ export declare function liveStatusWord(a: ActiveSession | undefined): string;
|
|
|
187
187
|
*/
|
|
188
188
|
export declare function isAwaitingUser(s: ActiveSession): boolean;
|
|
189
189
|
/**
|
|
190
|
-
* Whether a row belongs in an unqualified `--active` view
|
|
190
|
+
* Whether a row belongs in an unqualified `--active` view (RUSH-2336) — the
|
|
191
|
+
* ONE canonical selector every bare-active surface shares: the CLI's grouped
|
|
192
|
+
* table and `--json` (`renderActiveSessions`), the interactive browser's
|
|
193
|
+
* `--active` filter ({@link applyFilters} in `sessions-browser.ts`), `focus`'s
|
|
194
|
+
* attach gate (`isAttachableLiveSession`), and the menubar snapshot
|
|
195
|
+
* ({@link computeMenubarSnapshot}).
|
|
191
196
|
*
|
|
192
|
-
* The live registry deliberately retains terminally-dead
|
|
193
|
-
* `--closed` / `--crashed` and recovery to
|
|
194
|
-
* therefore is not, by itself, evidence
|
|
195
|
-
* Explicit lifecycle filters bypass this
|
|
196
|
-
* rows through {@link matchesLiveStatus}
|
|
197
|
+
* The live registry deliberately retains terminally-dead AND not-yet-started
|
|
198
|
+
* rows long enough for `--closed` / `--crashed` / `--queued` and recovery to
|
|
199
|
+
* find them. Presence in that registry therefore is not, by itself, evidence
|
|
200
|
+
* that a session is CURRENTLY active. Explicit lifecycle filters bypass this
|
|
201
|
+
* predicate and select those retained rows through {@link matchesLiveStatus}
|
|
202
|
+
* instead — this function only decides the unqualified default.
|
|
203
|
+
*
|
|
204
|
+
* - `queued` / `closed` / `crashed` are never active here: queued hasn't
|
|
205
|
+
* started yet, closed/crashed are unconditionally dead. All three stay
|
|
206
|
+
* reachable through their explicit filter.
|
|
207
|
+
* - A cloud row carries no local pid at all — it is active on the
|
|
208
|
+
* provider's own word (`cloudProvider` + `cloudTaskId`), never a
|
|
209
|
+
* fabricated pid.
|
|
210
|
+
* - Every other row is a real OS process (terminal/tmux/headless/team). It
|
|
211
|
+
* is active only when the row names the machine it runs on, carries a
|
|
212
|
+
* positive pid, AND has POSITIVELY verified that pid is alive
|
|
213
|
+
* (`pidAlive === true` — not merely "not known dead"). A live orphaned
|
|
214
|
+
* row, or a live-but-stuck abandoned row, still qualifies this way since
|
|
215
|
+
* both carry a genuinely alive pid; unknown liveness (an older peer's
|
|
216
|
+
* row, or a pid that could never be resolved) does not.
|
|
197
217
|
*/
|
|
198
218
|
export declare function isRunningLiveSession(s: ActiveSession): boolean;
|
|
199
219
|
/**
|
|
@@ -389,6 +409,22 @@ export declare function isBareBrowserListing(options: SessionsOptions, query: st
|
|
|
389
409
|
* `runSessionBrowser` picker cannot represent.
|
|
390
410
|
*/
|
|
391
411
|
export declare function hasNoBrowserDisqualifyingFlags(options: SessionsOptions, query: string | undefined): boolean;
|
|
412
|
+
/** Resolve a typed routine selector by exact name, substring, or one unambiguous typo. */
|
|
413
|
+
export declare function resolveRoutineName(query: string, names: readonly string[]): string | null;
|
|
414
|
+
export interface RoutineRunGroup {
|
|
415
|
+
runId: string;
|
|
416
|
+
timestamp: string;
|
|
417
|
+
sessions: SessionMeta[];
|
|
418
|
+
}
|
|
419
|
+
export interface RoutineChoice {
|
|
420
|
+
name: string;
|
|
421
|
+
lastRunAt: string;
|
|
422
|
+
runCount: number;
|
|
423
|
+
latestRunSessionCount: number;
|
|
424
|
+
}
|
|
425
|
+
export declare function buildRoutineRunGroups(sessions: SessionMeta[]): RoutineRunGroup[];
|
|
426
|
+
export declare function buildRoutineChoices(sessions: SessionMeta[]): RoutineChoice[];
|
|
427
|
+
export declare function filterSessionsByRoutine(sessions: SessionMeta[], routine: boolean | string, interactive: boolean): Promise<SessionMeta[] | null>;
|
|
392
428
|
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
393
429
|
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
394
430
|
export declare function renderSessionPreview(query: string, scope: {
|
|
@@ -478,6 +514,10 @@ export declare function buildOverviewGroups(pool: SessionMeta[], perProjectCap:
|
|
|
478
514
|
groups: OverviewGroup[];
|
|
479
515
|
projectCount: number;
|
|
480
516
|
};
|
|
517
|
+
export declare function printRoutineRunOverview(sessions: SessionMeta[], liveIndex: Map<string, ActiveSession> | undefined, opts?: {
|
|
518
|
+
hiddenCount?: number;
|
|
519
|
+
hiddenUnmanaged?: number;
|
|
520
|
+
}): void;
|
|
481
521
|
/**
|
|
482
522
|
* Render a resolved session to stdout — the non-follow view behind
|
|
483
523
|
* `agents logs <sessionId>`. Defaults to the concise `summary` digest (same as
|
|
@@ -577,6 +617,16 @@ export declare function pickSessionInteractive(sessions: SessionMeta[], message?
|
|
|
577
617
|
export declare const LIVE_ROW_PREFIX = "live:";
|
|
578
618
|
export declare function isIdlessLiveRow(s: SessionMeta): boolean;
|
|
579
619
|
export declare function handlePickedSession(picked: PickedSession): Promise<void>;
|
|
620
|
+
/**
|
|
621
|
+
* Resume `session` on the machine that owns it, when that is not this one.
|
|
622
|
+
* Returns true when it handled the resume (hopped, or reported an unreachable
|
|
623
|
+
* owner), false when the session is local and the caller should take over here.
|
|
624
|
+
*
|
|
625
|
+
* The one hop for a foreground, one-session-at-a-time resume — shared by the
|
|
626
|
+
* `agents sessions` picker and by `sessions resume`'s no-tab-backend path, which
|
|
627
|
+
* would otherwise reach `resumeSessionInPlace` and be refused (RUSH-2022).
|
|
628
|
+
*/
|
|
629
|
+
export declare function resumeOnOwnerIfRemote(session: SessionMeta): Promise<boolean>;
|
|
580
630
|
/**
|
|
581
631
|
* Resume a session in the current terminal — a foreground takeover of this
|
|
582
632
|
* process. Used by the single-select picker and by `sessions resume` when the
|
|
@@ -647,6 +697,22 @@ export declare function resolveSessionQuery(pool: SessionMeta[], query: string,
|
|
|
647
697
|
export declare function fleetNotFoundMessage(query: string, deviceCount: number, unreachable: string[]): string[];
|
|
648
698
|
/** Filter and rank sessions by a multi-term search query across metadata and content. */
|
|
649
699
|
export declare function filterSessionsByQuery(sessions: SessionMeta[], query: string | undefined): SessionMeta[];
|
|
700
|
+
/**
|
|
701
|
+
* Narrow a session list by --project and --agent before search resolution.
|
|
702
|
+
* Without this, a query like "scoped search" could match sessions in BOTH
|
|
703
|
+
* the project you specified AND elsewhere, producing an ambiguity error
|
|
704
|
+
* even though the user already pointed at the correct scope.
|
|
705
|
+
*/
|
|
706
|
+
export declare function applyScopeFilters(sessions: SessionMeta[], scope: {
|
|
707
|
+
agent?: string;
|
|
708
|
+
project?: string;
|
|
709
|
+
routine?: boolean | string;
|
|
710
|
+
}): SessionMeta[];
|
|
711
|
+
export declare function artifactLookupScope(agent?: string, project?: string, routine?: boolean | string): {
|
|
712
|
+
agent?: string;
|
|
713
|
+
project?: string;
|
|
714
|
+
routine?: boolean | string;
|
|
715
|
+
};
|
|
650
716
|
/**
|
|
651
717
|
* Whether a missed local id lookup should widen to a cross-machine sweep.
|
|
652
718
|
*
|