@phnx-labs/agents-cli 1.22.23 → 1.22.24
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 +193 -0
- package/README.md +5 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +32 -7
- package/dist/commands/defaults.js +2 -0
- package/dist/commands/doctor.js +5 -0
- package/dist/commands/exec.js +80 -28
- package/dist/commands/feed.d.ts +18 -0
- package/dist/commands/feed.js +44 -1
- package/dist/commands/focus.d.ts +34 -3
- package/dist/commands/focus.js +300 -56
- package/dist/commands/go.d.ts +14 -1
- package/dist/commands/go.js +49 -5
- package/dist/commands/insights.js +6 -2
- package/dist/commands/inspect.js +39 -5
- package/dist/commands/menubar.js +6 -1
- package/dist/commands/models.js +1 -0
- package/dist/commands/modes.d.ts +12 -0
- package/dist/commands/modes.js +147 -0
- package/dist/commands/secrets-sync.js +11 -13
- package/dist/commands/secrets.d.ts +2 -0
- package/dist/commands/secrets.js +54 -17
- package/dist/commands/sessions-browser.d.ts +35 -0
- package/dist/commands/sessions-browser.js +140 -14
- package/dist/commands/sessions-resume.d.ts +2 -0
- package/dist/commands/sessions-resume.js +26 -11
- package/dist/commands/sessions.d.ts +10 -0
- package/dist/commands/sessions.js +34 -59
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/sync.js +246 -42
- package/dist/commands/view.js +2 -0
- package/dist/index.js +2 -1
- package/dist/lib/agent-modes.d.ts +49 -0
- package/dist/lib/agent-modes.js +70 -0
- package/dist/lib/doctor-diff.d.ts +3 -0
- package/dist/lib/doctor-diff.js +15 -13
- package/dist/lib/event-stream.d.ts +3 -1
- package/dist/lib/event-stream.js +14 -1
- package/dist/lib/exec.d.ts +12 -0
- package/dist/lib/exec.js +88 -9
- package/dist/lib/hooks/cache.js +36 -3
- package/dist/lib/hooks.d.ts +19 -7
- package/dist/lib/hooks.js +88 -38
- package/dist/lib/hosts/session-index.d.ts +4 -0
- package/dist/lib/hosts/session-index.js +7 -0
- package/dist/lib/manifest.d.ts +12 -2
- package/dist/lib/manifest.js +60 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +5 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +15 -2
- package/dist/lib/menubar/install-menubar.d.ts +52 -2
- package/dist/lib/menubar/install-menubar.js +128 -6
- package/dist/lib/refresh.d.ts +5 -0
- package/dist/lib/refresh.js +37 -33
- package/dist/lib/resource-inventory.d.ts +79 -0
- package/dist/lib/resource-inventory.js +122 -0
- package/dist/lib/resources.js +8 -5
- package/dist/lib/run-defaults.d.ts +2 -0
- package/dist/lib/run-defaults.js +23 -2
- package/dist/lib/runner.js +22 -17
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/secrets/bundles.js +157 -65
- package/dist/lib/secrets/filestore.d.ts +5 -3
- package/dist/lib/secrets/filestore.js +12 -8
- package/dist/lib/secrets/index.js +34 -8
- package/dist/lib/secrets/reaper.d.ts +10 -0
- package/dist/lib/secrets/reaper.js +50 -15
- package/dist/lib/secrets/sync-passphrase.d.ts +27 -0
- package/dist/lib/secrets/sync-passphrase.js +78 -0
- package/dist/lib/session/recovery.d.ts +37 -0
- package/dist/lib/session/recovery.js +95 -0
- package/dist/lib/shims.d.ts +2 -2
- package/dist/lib/shims.js +24 -5
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +5 -4
- package/dist/lib/tmux/session.d.ts +4 -2
- package/dist/lib/tmux/session.js +5 -5
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type SessionMeta } from '../lib/session/types.js';
|
|
13
13
|
import type { ActiveSession } from '../lib/session/active.js';
|
|
14
|
+
import { type LiveStatusFilter } from './sessions.js';
|
|
14
15
|
/**
|
|
15
16
|
* The single canonical filter state. Every field has a flag equivalent, so the
|
|
16
17
|
* same view is reachable interactively (hotkeys) or from the command line (flags).
|
|
@@ -32,6 +33,23 @@ export interface BrowserFilter {
|
|
|
32
33
|
projectScope: 'repo' | 'all';
|
|
33
34
|
/** time window (undefined = all time) — the `W` key / `--since`. */
|
|
34
35
|
window?: string;
|
|
36
|
+
/** individual live states; an OR-union and, like the CLI flags, running-only. */
|
|
37
|
+
statuses: LiveStatusFilter[];
|
|
38
|
+
/** named project scope, independent of the repo/all hotkey. */
|
|
39
|
+
project?: string;
|
|
40
|
+
/** upper time bound from --until. */
|
|
41
|
+
until?: string;
|
|
42
|
+
/** retain only routine-origin sessions. */
|
|
43
|
+
routine: boolean;
|
|
44
|
+
/** indexed resource filters shared by the flag and focus surfaces. */
|
|
45
|
+
skill?: string;
|
|
46
|
+
plugin?: string;
|
|
47
|
+
/** discovery cap. */
|
|
48
|
+
limit: number;
|
|
49
|
+
/** include unmanaged native-home transcripts. */
|
|
50
|
+
unmanaged: boolean;
|
|
51
|
+
/** candidate order shared with --sort. */
|
|
52
|
+
sort: 'timestamp' | 'cost' | 'duration';
|
|
35
53
|
}
|
|
36
54
|
/**
|
|
37
55
|
* Complete a seed into the filter the picker actually runs on.
|
|
@@ -97,6 +115,9 @@ export declare function bareBrowserSeed(opts: {
|
|
|
97
115
|
inTeam?: string;
|
|
98
116
|
favorites?: boolean;
|
|
99
117
|
}): Partial<BrowserFilter>;
|
|
118
|
+
/** Build the static peer query. Keeping it pure pins the per-device alias
|
|
119
|
+
* contract: latest/oldest travel unresolved and are resolved by each peer. */
|
|
120
|
+
export declare function remotePoolArgs(f: BrowserFilter, fixedFilters: boolean): string[];
|
|
100
121
|
/**
|
|
101
122
|
* A live session's stable row key: its session id when the agent reported one,
|
|
102
123
|
* else a per-machine handle so an id-less live session (a just-booted harness, a
|
|
@@ -135,6 +156,20 @@ export declare function mergeLiveIntoPool(rows: SessionMeta[], live: Map<string,
|
|
|
135
156
|
* listing that has no live rows to explain it.
|
|
136
157
|
*/
|
|
137
158
|
export declare function shouldShowHostColumn(f: BrowserFilter, live: Map<string, ActiveSession> | null, rows: SessionMeta[]): boolean;
|
|
159
|
+
/** Apply the cheap in-memory filters (agent / device / project / running / favorites). */
|
|
160
|
+
export declare function applyFilters(rows: SessionMeta[], live: Map<string, ActiveSession>, f: BrowserFilter, self: string, favorites: Set<string>): SessionMeta[];
|
|
161
|
+
/** One-shot form of the browser's canonical candidate pipeline. Focus uses this
|
|
162
|
+
* instead of maintaining a second discovery/filter implementation. */
|
|
163
|
+
export declare function collectSessionCandidates(initial: Partial<BrowserFilter>, opts?: {
|
|
164
|
+
local?: boolean;
|
|
165
|
+
hosts?: string[];
|
|
166
|
+
includeLive?: boolean;
|
|
167
|
+
}): Promise<{
|
|
168
|
+
sessions: SessionMeta[];
|
|
169
|
+
liveById: Map<string, ActiveSession>;
|
|
170
|
+
self: string;
|
|
171
|
+
unreachable: string[];
|
|
172
|
+
}>;
|
|
138
173
|
/**
|
|
139
174
|
* Launch the interactive session browser. `initial` seeds the filter (e.g.
|
|
140
175
|
* `{ running: true }` for `--active`). Resolves after the user resumes a session
|
|
@@ -16,11 +16,13 @@ import { dynamicPicker } from '../lib/picker.js';
|
|
|
16
16
|
import { isSessionTrackedAgent } from '../lib/session/types.js';
|
|
17
17
|
import { discoverSessions } from '../lib/session/discover.js';
|
|
18
18
|
import { gatherRemoteList } from '../lib/session/remote-list.js';
|
|
19
|
+
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
20
|
+
import { AGENTS } from '../lib/agents.js';
|
|
19
21
|
import { enrichTeamOrigins, safeTeamText } from '../lib/session/team-filter.js';
|
|
20
22
|
import { listFavorites, toggleFavorite } from '../lib/session/favorites.js';
|
|
21
23
|
import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
22
24
|
import { buildPreview } from './sessions-picker.js';
|
|
23
|
-
import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, } from './sessions.js';
|
|
25
|
+
import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, gatherActiveSessions, liveHostLabel, LIVE_ROW_PREFIX, cleanPreview, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, matchesTeam, formatLiveStatusHeadline, matchesLiveStatus, parseAgentFilter, } from './sessions.js';
|
|
24
26
|
/**
|
|
25
27
|
* Complete a seed into the filter the picker actually runs on.
|
|
26
28
|
*
|
|
@@ -44,6 +46,15 @@ export function buildInitialFilter(initial) {
|
|
|
44
46
|
team: initial.team,
|
|
45
47
|
projectScope: initial.projectScope ?? 'repo',
|
|
46
48
|
window: 'window' in initial ? initial.window : '30d',
|
|
49
|
+
statuses: initial.statuses ?? [],
|
|
50
|
+
project: initial.project,
|
|
51
|
+
until: initial.until,
|
|
52
|
+
routine: initial.routine ?? false,
|
|
53
|
+
skill: initial.skill,
|
|
54
|
+
plugin: initial.plugin,
|
|
55
|
+
limit: initial.limit ?? 500,
|
|
56
|
+
unmanaged: initial.unmanaged ?? false,
|
|
57
|
+
sort: initial.sort ?? 'timestamp',
|
|
47
58
|
};
|
|
48
59
|
}
|
|
49
60
|
/** Cache key for the transcript pool: every filter that changes what is FETCHED
|
|
@@ -54,7 +65,11 @@ function poolCacheKey(f) {
|
|
|
54
65
|
// fetches the same deep pool and is then narrowed in memory. Keying on the name
|
|
55
66
|
// would make `t` the one hotkey that re-fans-out the fleet on every step of the
|
|
56
67
|
// cycle, at up to REMOTE_TIMEOUT_MS per unreachable peer.
|
|
57
|
-
|
|
68
|
+
const versionScoped = f.agent?.includes('@') ? f.agent : '';
|
|
69
|
+
return [
|
|
70
|
+
f.window ?? 'all', f.until ?? '', f.teams, f.team ? 'team' : '', versionScoped,
|
|
71
|
+
f.project ?? '', f.routine, f.skill ?? '', f.plugin ?? '', f.limit, f.unmanaged, f.sort,
|
|
72
|
+
].join('|');
|
|
58
73
|
}
|
|
59
74
|
/** Pool size when a team filter is active; one team's rows can sit anywhere. */
|
|
60
75
|
const WHOLE_TEAM_POOL_LIMIT = 5000;
|
|
@@ -105,8 +120,10 @@ export function sessionMatchesQuery(s, query) {
|
|
|
105
120
|
*/
|
|
106
121
|
export function browserFilterToArgv(f, query = '') {
|
|
107
122
|
const a = ['sessions'];
|
|
108
|
-
if (f.running)
|
|
123
|
+
if (f.statuses.length === 0 && f.running)
|
|
109
124
|
a.push('--active');
|
|
125
|
+
for (const status of f.statuses)
|
|
126
|
+
a.push(`--${status === 'orphaned' ? 'orphan' : status}`);
|
|
110
127
|
if (f.teams)
|
|
111
128
|
a.push('--teams');
|
|
112
129
|
if (f.favorites)
|
|
@@ -121,6 +138,22 @@ export function browserFilterToArgv(f, query = '') {
|
|
|
121
138
|
a.push('--all');
|
|
122
139
|
if (f.window)
|
|
123
140
|
a.push('--since', f.window);
|
|
141
|
+
if (f.until)
|
|
142
|
+
a.push('--until', f.until);
|
|
143
|
+
if (f.project)
|
|
144
|
+
a.push('--project', f.project);
|
|
145
|
+
if (f.routine)
|
|
146
|
+
a.push('--routine');
|
|
147
|
+
if (f.skill)
|
|
148
|
+
a.push('--skill', f.skill);
|
|
149
|
+
if (f.plugin)
|
|
150
|
+
a.push('--plugin', f.plugin);
|
|
151
|
+
if (f.unmanaged)
|
|
152
|
+
a.push('--unmanaged');
|
|
153
|
+
if (f.sort !== 'timestamp')
|
|
154
|
+
a.push('--sort', f.sort === 'cost' ? 'cost' : 'duration');
|
|
155
|
+
if (f.limit !== 500)
|
|
156
|
+
a.push('--limit', String(f.limit));
|
|
124
157
|
const q = query.trim();
|
|
125
158
|
if (q)
|
|
126
159
|
a.push(JSON.stringify(q));
|
|
@@ -151,6 +184,7 @@ export function activeBrowserSeed(opts) {
|
|
|
151
184
|
// --all widens the window to all-time (project is already 'all' here);
|
|
152
185
|
// --since still overrides.
|
|
153
186
|
window: opts.since ?? (opts.all ? undefined : '30d'),
|
|
187
|
+
statuses: [],
|
|
154
188
|
};
|
|
155
189
|
}
|
|
156
190
|
/**
|
|
@@ -184,6 +218,7 @@ export function bareBrowserSeed(opts) {
|
|
|
184
218
|
// --all maxes every non-status filter: all dirs AND all-time. --since wins.
|
|
185
219
|
projectScope: opts.all || scoped || wholeTeam ? 'all' : 'repo',
|
|
186
220
|
window: opts.since ?? (opts.all || wholeTeam ? undefined : '30d'),
|
|
221
|
+
statuses: [],
|
|
187
222
|
};
|
|
188
223
|
}
|
|
189
224
|
/** Copy text to the OS clipboard (best-effort; silently no-ops if unavailable). */
|
|
@@ -214,8 +249,13 @@ function copyToClipboard(text) {
|
|
|
214
249
|
* `hosts` is the explicit `--host`/`--device` scope (if any): it restricts which
|
|
215
250
|
* peers are dialed and whether local is included, honoring the flag's "scope,
|
|
216
251
|
* not add" contract instead of always sweeping the whole fleet. */
|
|
217
|
-
async function fetchRawPool(f, self, local, hosts) {
|
|
252
|
+
async function fetchRawPool(f, self, local, hosts, fixedFilters = false) {
|
|
218
253
|
const since = f.window;
|
|
254
|
+
const selectedAgent = f.agent ? parseAgentFilter(f.agent) : {};
|
|
255
|
+
const parsedAgent = fixedFilters ? selectedAgent : {};
|
|
256
|
+
const localAgentVersion = parsedAgent.agent && parsedAgent.agent in AGENTS
|
|
257
|
+
? resolveVersionAliasLoose(parsedAgent.agent, parsedAgent.version)
|
|
258
|
+
: parsedAgent.version;
|
|
219
259
|
let unreachable = [];
|
|
220
260
|
// Local pool: wide (every directory) — device/agent/project are applied in
|
|
221
261
|
// memory so a hotkey toggle is instant and doesn't re-hit the disk. Skipped
|
|
@@ -223,16 +263,33 @@ async function fetchRawPool(f, self, local, hosts) {
|
|
|
223
263
|
let rows = shouldIncludeLocal(hosts, self)
|
|
224
264
|
? await discoverSessions({
|
|
225
265
|
all: true,
|
|
266
|
+
agent: parsedAgent.agent,
|
|
267
|
+
version: localAgentVersion,
|
|
268
|
+
includeUnmanaged: f.unmanaged,
|
|
226
269
|
cwd: process.cwd(),
|
|
227
270
|
since,
|
|
271
|
+
until: f.until,
|
|
272
|
+
project: f.project,
|
|
273
|
+
origin: f.routine ? 'routine' : undefined,
|
|
274
|
+
skill: f.skill,
|
|
275
|
+
plugin: f.plugin,
|
|
228
276
|
excludeTeamOrigin: !f.teams,
|
|
229
277
|
// A team filter reaches back past the usual browse window, so the pool it
|
|
230
278
|
// draws from has to as well — otherwise the newest 500 rows decide which
|
|
231
279
|
// teams exist.
|
|
232
|
-
limit: f.team ? WHOLE_TEAM_POOL_LIMIT :
|
|
233
|
-
sortBy:
|
|
280
|
+
limit: f.team ? WHOLE_TEAM_POOL_LIMIT : f.limit,
|
|
281
|
+
sortBy: f.sort,
|
|
234
282
|
})
|
|
235
283
|
: [];
|
|
284
|
+
// A synced mirror knows which VERSION produced a session, but it cannot know
|
|
285
|
+
// which version is currently installed as latest/oldest on the origin device.
|
|
286
|
+
// For an alias selector, keep only this device's rows here and accept peer
|
|
287
|
+
// rows only from the live query below, where that peer resolves its own
|
|
288
|
+
// inventory. If the peer is unavailable, an empty/partial result is honest;
|
|
289
|
+
// widening to cached sessions from every version is not.
|
|
290
|
+
if (selectedAgent.version === 'latest' || selectedAgent.version === 'oldest') {
|
|
291
|
+
rows = rows.filter((row) => (row.machine ?? self) === self);
|
|
292
|
+
}
|
|
236
293
|
// Fleet: fold in peers' own indexes over SSH (no sync), same as the flag path.
|
|
237
294
|
// Skipped under --local. An explicit --host/--device scopes exactly which peers
|
|
238
295
|
// are dialed (undefined = sweep every online device). Best-effort — a fan-out
|
|
@@ -251,11 +308,7 @@ async function fetchRawPool(f, self, local, hosts) {
|
|
|
251
308
|
// the same bug one hop out. A numeric --limit is forwarded rather than
|
|
252
309
|
// --in-team itself, which a peer on an older build would reject as an
|
|
253
310
|
// unknown option and fail the whole fan-out.
|
|
254
|
-
const forwarded =
|
|
255
|
-
if (since)
|
|
256
|
-
forwarded.push('--since', since);
|
|
257
|
-
if (f.teams)
|
|
258
|
-
forwarded.push('--teams');
|
|
311
|
+
const forwarded = remotePoolArgs(f, fixedFilters);
|
|
259
312
|
const remoteResult = await gatherRemoteList(forwarded, remoteHosts);
|
|
260
313
|
unreachable = remoteResult.unreachable;
|
|
261
314
|
if (remoteResult.sessions.length > 0)
|
|
@@ -272,6 +325,32 @@ async function fetchRawPool(f, self, local, hosts) {
|
|
|
272
325
|
rows = enrichTeamOrigins(rows);
|
|
273
326
|
return { key: poolCacheKey(f), rows, unreachable };
|
|
274
327
|
}
|
|
328
|
+
/** Build the static peer query. Keeping it pure pins the per-device alias
|
|
329
|
+
* contract: latest/oldest travel unresolved and are resolved by each peer. */
|
|
330
|
+
export function remotePoolArgs(f, fixedFilters) {
|
|
331
|
+
const forwarded = ['sessions', '--all', '--json', '--limit', String(f.team ? WHOLE_TEAM_POOL_LIMIT : f.limit)];
|
|
332
|
+
if (f.window)
|
|
333
|
+
forwarded.push('--since', f.window);
|
|
334
|
+
if (f.until)
|
|
335
|
+
forwarded.push('--until', f.until);
|
|
336
|
+
if (f.project)
|
|
337
|
+
forwarded.push('--project', f.project);
|
|
338
|
+
if (f.routine)
|
|
339
|
+
forwarded.push('--routine');
|
|
340
|
+
if (f.skill)
|
|
341
|
+
forwarded.push('--skill', f.skill);
|
|
342
|
+
if (f.plugin)
|
|
343
|
+
forwarded.push('--plugin', f.plugin);
|
|
344
|
+
if (f.unmanaged)
|
|
345
|
+
forwarded.push('--unmanaged');
|
|
346
|
+
if (f.sort !== 'timestamp')
|
|
347
|
+
forwarded.push('--sort', f.sort === 'cost' ? 'cost' : 'duration');
|
|
348
|
+
if (f.agent && (fixedFilters || f.agent.includes('@')))
|
|
349
|
+
forwarded.push('--agent', f.agent);
|
|
350
|
+
if (f.teams)
|
|
351
|
+
forwarded.push('--teams');
|
|
352
|
+
return forwarded;
|
|
353
|
+
}
|
|
275
354
|
/**
|
|
276
355
|
* A live session's stable row key: its session id when the agent reported one,
|
|
277
356
|
* else a per-machine handle so an id-less live session (a just-booted harness, a
|
|
@@ -366,27 +445,74 @@ export function shouldShowHostColumn(f, live, rows) {
|
|
|
366
445
|
return rows.some((r) => liveHostLabel(live.get(r.id)) !== '');
|
|
367
446
|
}
|
|
368
447
|
/** Apply the cheap in-memory filters (agent / device / project / running / favorites). */
|
|
369
|
-
function applyFilters(rows, live, f, self, favorites) {
|
|
448
|
+
export function applyFilters(rows, live, f, self, favorites) {
|
|
370
449
|
let out = rows;
|
|
371
450
|
// A projected live row is keyed by pid/task when it has no session id, and a
|
|
372
451
|
// favorite is always keyed by a real session id — so an id-less row can never
|
|
373
452
|
// be favorited and correctly drops out here.
|
|
374
453
|
if (f.favorites)
|
|
375
454
|
out = out.filter((r) => favorites.has(r.id));
|
|
376
|
-
if (f.agent)
|
|
377
|
-
|
|
455
|
+
if (f.agent) {
|
|
456
|
+
const { agent, version: rawVersion } = parseAgentFilter(f.agent);
|
|
457
|
+
const localVersion = agent && agent in AGENTS
|
|
458
|
+
? resolveVersionAliasLoose(agent, rawVersion)
|
|
459
|
+
: rawVersion;
|
|
460
|
+
const peerResolvedAlias = rawVersion === 'latest' || rawVersion === 'oldest';
|
|
461
|
+
out = out.filter((r) => {
|
|
462
|
+
if (r.agent !== agent)
|
|
463
|
+
return false;
|
|
464
|
+
// Only a LIVE peer result may claim it resolved latest/oldest against that
|
|
465
|
+
// device's installed inventory. A synced mirror is historical data, not a
|
|
466
|
+
// version-inventory oracle.
|
|
467
|
+
if (peerResolvedAlias && (r.machine ?? self) !== self)
|
|
468
|
+
return r._remote === true;
|
|
469
|
+
if (!localVersion)
|
|
470
|
+
return true;
|
|
471
|
+
return r.version === localVersion;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
378
474
|
if (f.device)
|
|
379
475
|
out = out.filter((r) => (r.machine ?? self) === f.device);
|
|
380
476
|
if (f.team)
|
|
381
477
|
out = out.filter((r) => matchesTeam(r, f.team));
|
|
478
|
+
if (f.project) {
|
|
479
|
+
const q = f.project.toLowerCase();
|
|
480
|
+
out = out.filter((r) => (r.project ?? '').toLowerCase().includes(q) || (r.cwd ?? '').toLowerCase().includes(q));
|
|
481
|
+
}
|
|
382
482
|
if (f.projectScope === 'repo') {
|
|
383
483
|
const cwd = process.cwd();
|
|
384
484
|
out = out.filter((r) => !!r.cwd && (r.cwd === cwd || r.cwd.startsWith(cwd + '/')));
|
|
385
485
|
}
|
|
386
486
|
if (f.running)
|
|
387
487
|
out = out.filter((r) => live.has(r.id));
|
|
488
|
+
if (f.statuses.length > 0) {
|
|
489
|
+
out = out.filter((r) => {
|
|
490
|
+
const active = live.get(r.id);
|
|
491
|
+
return !!active && f.statuses.some((status) => matchesLiveStatus(active, status));
|
|
492
|
+
});
|
|
493
|
+
}
|
|
388
494
|
return out;
|
|
389
495
|
}
|
|
496
|
+
/** One-shot form of the browser's canonical candidate pipeline. Focus uses this
|
|
497
|
+
* instead of maintaining a second discovery/filter implementation. */
|
|
498
|
+
export async function collectSessionCandidates(initial, opts = {}) {
|
|
499
|
+
const self = machineId();
|
|
500
|
+
const filter = buildInitialFilter(initial);
|
|
501
|
+
if (filter.statuses.length > 0)
|
|
502
|
+
filter.running = true;
|
|
503
|
+
const hosts = opts.hosts && opts.hosts.length > 0 ? opts.hosts : undefined;
|
|
504
|
+
const pool = await fetchRawPool(filter, self, opts.local ?? false, hosts, true);
|
|
505
|
+
let liveById = new Map();
|
|
506
|
+
if (filter.running || opts.includeLive) {
|
|
507
|
+
const { sessions } = await gatherActiveSessions({ local: opts.local ?? false, hosts });
|
|
508
|
+
liveById = indexLiveRows(sessions, self);
|
|
509
|
+
}
|
|
510
|
+
const rows = filter.running || opts.includeLive
|
|
511
|
+
? mergeLiveIntoPool(pool.rows, liveById, self)
|
|
512
|
+
: pool.rows;
|
|
513
|
+
const sessions = applyFilters(rows, liveById, filter, self, listFavorites());
|
|
514
|
+
return { sessions, liveById, self, unreachable: pool.unreachable };
|
|
515
|
+
}
|
|
390
516
|
/** Derive the SSH-launch origin tag for a picker row from the live index. Set
|
|
391
517
|
* only when the live session's provenance is ssh transport; `device` is the
|
|
392
518
|
* resolved origin device (absent → the row shows a bare `ssh`). Rows without a
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
2
3
|
import { type Backend, type EngineContext, type Packing } from '../lib/terminal/index.js';
|
|
3
4
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
4
5
|
export declare const CONFIRM_THRESHOLD = 5;
|
|
@@ -30,6 +31,7 @@ export declare function resumeSelectorInPlace(selector: string): Promise<void>;
|
|
|
30
31
|
export declare function dispatchSessionLifecycleInPlace(selector: string, hosts?: string[]): Promise<void>;
|
|
31
32
|
export declare function buildSessionLifecycleArgs(selector: string, hosts?: string[]): string[];
|
|
32
33
|
export declare function resolveResumePacking(options: Pick<ResumeOptions, 'splits'>): Packing;
|
|
34
|
+
export declare function resumeHostMismatch(session: Pick<SessionMeta, 'shortId' | 'machine'>, requestedHost: string, self?: string): string | null;
|
|
33
35
|
/**
|
|
34
36
|
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
35
37
|
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|
|
@@ -14,14 +14,15 @@ import { discoverSessions } from '../lib/session/discover.js';
|
|
|
14
14
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
15
15
|
import { multiItemPicker, itemPicker } from '../lib/picker.js';
|
|
16
16
|
import { buildPreview } from './sessions-picker.js';
|
|
17
|
-
import { filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
|
|
17
|
+
import { filterSessionsByQuery, formatPickerLabel, pickerColumnsFor, buildSessionRecoveryCommand, resumeSessionInPlace, parseAgentFilter, } from './sessions.js';
|
|
18
18
|
import { openSurfaces, availableBackends, detectCurrentBackend, currentContext, } from '../lib/terminal/index.js';
|
|
19
19
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
20
20
|
import { setHelpSections } from '../lib/help.js';
|
|
21
21
|
import { confirm } from '@inquirer/prompts';
|
|
22
22
|
import { spawn } from 'node:child_process';
|
|
23
|
-
import { buildCanonicalResumeCommand } from '../lib/session/resume-command.js';
|
|
24
23
|
import { looksLikeSessionId } from '../lib/session/discover.js';
|
|
24
|
+
import { machineId } from '../lib/session/sync/config.js';
|
|
25
|
+
import { sessionOriginDevice, sessionRecoveryDestinationMatches } from '../lib/session/recovery.js';
|
|
25
26
|
/** Opening more than this many live sessions at once asks for confirmation first. */
|
|
26
27
|
export const CONFIRM_THRESHOLD = 5;
|
|
27
28
|
export function registerSessionsResumeCommand(sessionsCmd) {
|
|
@@ -34,7 +35,7 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
34
35
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
35
36
|
.option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
|
|
36
37
|
.option('-n, --limit <n>', 'Maximum number of sessions to load into the picker', '200')
|
|
37
|
-
.option('--host <alias>', '
|
|
38
|
+
.option('--host <alias>', 'Open on the session origin host over SSH; the host must match every selected session')
|
|
38
39
|
.option('--iterm', 'Force the iTerm backend')
|
|
39
40
|
.option('--ghostty', 'Force the Ghostty backend')
|
|
40
41
|
.option('--tmux', 'Force the tmux backend')
|
|
@@ -65,8 +66,8 @@ export function registerSessionsResumeCommand(sessionsCmd) {
|
|
|
65
66
|
- Layout: one tab per session by default. --splits packs session pairs side by side in each tab.
|
|
66
67
|
- Backend: auto-detected from the terminal you're in (iTerm / Ghostty / tmux); override with --iterm/--ghostty/--tmux/--vscodium.
|
|
67
68
|
- --vscodium opens each session as an agent terminal tab in VSCodium via the swarm-ext extension (works with --host too).
|
|
68
|
-
- --host <alias>
|
|
69
|
-
-
|
|
69
|
+
- --host <alias> opens the terminal surface on that host only when it is the selected sessions' origin; recovery never migrates a session to another device.
|
|
70
|
+
- Recovery runs on the session's origin device: exact healthy origin uses native resume; otherwise a healthy version of the same harness receives /continue <id>.
|
|
70
71
|
`,
|
|
71
72
|
});
|
|
72
73
|
cmd.action(async (query, options) => {
|
|
@@ -127,17 +128,25 @@ async function sessionsResumeAction(query, options) {
|
|
|
127
128
|
}
|
|
128
129
|
if (!chosen || chosen.length === 0)
|
|
129
130
|
return;
|
|
130
|
-
|
|
131
|
+
if (options.host) {
|
|
132
|
+
const requestedHost = options.host;
|
|
133
|
+
const mismatches = chosen
|
|
134
|
+
.map((session) => resumeHostMismatch(session, requestedHost))
|
|
135
|
+
.filter((message) => message !== null);
|
|
136
|
+
if (mismatches.length > 0) {
|
|
137
|
+
for (const message of mismatches)
|
|
138
|
+
console.error(chalk.red(message));
|
|
139
|
+
process.exitCode = 1;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// 2. Route every selection through the owning device's recovery resolver.
|
|
131
144
|
const items = [];
|
|
132
145
|
for (const s of chosen) {
|
|
133
|
-
const command =
|
|
146
|
+
const command = buildSessionRecoveryCommand(s, !!options.host);
|
|
134
147
|
const cwd = s.cwd && fs.existsSync(s.cwd) ? s.cwd : process.cwd();
|
|
135
148
|
items.push({ session: s, cwd, command });
|
|
136
149
|
}
|
|
137
|
-
if (items.length === 0) {
|
|
138
|
-
console.log(chalk.gray('Nothing resumable in the selection.'));
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
150
|
// 3. Resolve the backend (and host).
|
|
142
151
|
const ctx = currentContext();
|
|
143
152
|
const backend = await resolveBackend(options, ctx, items.length);
|
|
@@ -221,6 +230,12 @@ async function spawnCliInPlace(args) {
|
|
|
221
230
|
export function resolveResumePacking(options) {
|
|
222
231
|
return options.splits ? 'two-per-tab' : 'tabs';
|
|
223
232
|
}
|
|
233
|
+
export function resumeHostMismatch(session, requestedHost, self = machineId()) {
|
|
234
|
+
const origin = sessionOriginDevice(session, self);
|
|
235
|
+
return sessionRecoveryDestinationMatches(session, requestedHost, self)
|
|
236
|
+
? null
|
|
237
|
+
: `Session ${session.shortId} originated on ${origin}; --host ${requestedHost} cannot move recovery to another device.`;
|
|
238
|
+
}
|
|
224
239
|
/**
|
|
225
240
|
* Decide which backend to launch into. Returns a concrete backend, `'inplace'`
|
|
226
241
|
* (resume in the current process — no GUI/tmux available), or `'cancel'` (the
|
|
@@ -573,6 +573,13 @@ export declare function handlePickedSession(picked: PickedSession): Promise<void
|
|
|
573
573
|
* version-pinned launcher is genuinely missing.
|
|
574
574
|
*/
|
|
575
575
|
export declare function resumeSessionInPlace(session: SessionMeta): Promise<void>;
|
|
576
|
+
/**
|
|
577
|
+
* Relaunch this CLI's one recovery path. The owning device resolves account
|
|
578
|
+
* health and either performs exact-home native resume or same-harness
|
|
579
|
+
* `/continue`; callers never guess which version home can see the transcript.
|
|
580
|
+
* `portable` is for an SSH/terminal-engine command that executes on a peer.
|
|
581
|
+
*/
|
|
582
|
+
export declare function buildSessionRecoveryCommand(session: Pick<SessionMeta, 'id'>, portable?: boolean): string[];
|
|
576
583
|
/**
|
|
577
584
|
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
578
585
|
*
|
|
@@ -594,6 +601,9 @@ interface AgentFilter {
|
|
|
594
601
|
agent?: SessionAgentId;
|
|
595
602
|
version?: string;
|
|
596
603
|
}
|
|
604
|
+
/** Resolve the harness portion of a sessions selector with the CLI's canonical
|
|
605
|
+
* alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
|
|
606
|
+
export declare function resolveSessionAgentName(name: string): SessionAgentId | null;
|
|
597
607
|
export declare function parseAgentFilter(agentName?: string): AgentFilter;
|
|
598
608
|
/**
|
|
599
609
|
* How a `sessions <query>` argument was resolved against the pool.
|
|
@@ -20,7 +20,7 @@ import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
|
20
20
|
import ora from 'ora';
|
|
21
21
|
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
22
22
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
23
|
-
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell,
|
|
23
|
+
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
24
24
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
25
25
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
26
26
|
import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
@@ -45,6 +45,8 @@ import { getShimsDir } from '../lib/state.js';
|
|
|
45
45
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
46
46
|
import { resolveSessionAlias } from '../lib/session/actor-sidecar.js';
|
|
47
47
|
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
48
|
+
import { getAgentsInvocation } from '../lib/daemon.js';
|
|
49
|
+
import { sessionRecoveryRunArgs } from '../lib/session/recovery.js';
|
|
48
50
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
49
51
|
import { sessionPicker, buildPreview, formatTodoCompact, githubRepoUrlFromCwd, } from './sessions-picker.js';
|
|
50
52
|
import { setHelpSections } from '../lib/help.js';
|
|
@@ -3017,34 +3019,23 @@ export async function resumeSessionInPlace(session) {
|
|
|
3017
3019
|
const cwd = session.cwd && fs.existsSync(session.cwd)
|
|
3018
3020
|
? session.cwd
|
|
3019
3021
|
: process.cwd();
|
|
3020
|
-
const resume =
|
|
3021
|
-
if (!resume) {
|
|
3022
|
-
console.log(chalk.yellow(`Resume is not supported for ${session.agent} sessions yet. Showing summary instead.`));
|
|
3023
|
-
await renderSession(session, 'summary', {});
|
|
3024
|
-
return;
|
|
3025
|
-
}
|
|
3022
|
+
const resume = buildSessionRecoveryCommand(session);
|
|
3026
3023
|
console.log(chalk.gray(`Resuming: ${resume.join(' ')} (cwd: ${cwd})`));
|
|
3027
|
-
// Resolve the (possibly version-pinned) launcher up front. On Windows the
|
|
3028
|
-
// agent shim is a `.cmd`/`.ps1` and, under the shell needed to run it (see
|
|
3029
|
-
// spawnResumeCommand), a missing command exits non-zero rather than emitting
|
|
3030
|
-
// an ENOENT `error` event — so detect a removed version here instead of
|
|
3031
|
-
// relying on that event, keeping the fallback working on every OS.
|
|
3032
|
-
// `resume[0]` is an absolute alias path when one exists on disk, so only a bare
|
|
3033
|
-
// name still needs a PATH lookup. Checking existsSync first is what keeps an
|
|
3034
|
-
// isolated install (shims deliberately off PATH) out of the fallback.
|
|
3035
|
-
const launcherFound = path.isAbsolute(resume[0])
|
|
3036
|
-
? fs.existsSync(resume[0])
|
|
3037
|
-
: !!findExecutable(resume[0]);
|
|
3038
|
-
if (!launcherFound && session.version) {
|
|
3039
|
-
const fallback = buildFallbackCommand(session);
|
|
3040
|
-
if (fallback) {
|
|
3041
|
-
console.log(chalk.gray(`Version ${session.version} is not installed. Resuming with the current version instead...`));
|
|
3042
|
-
await spawnResumeCommand(fallback, cwd);
|
|
3043
|
-
return;
|
|
3044
|
-
}
|
|
3045
|
-
}
|
|
3046
3024
|
await spawnResumeCommand(resume, cwd);
|
|
3047
3025
|
}
|
|
3026
|
+
/**
|
|
3027
|
+
* Relaunch this CLI's one recovery path. The owning device resolves account
|
|
3028
|
+
* health and either performs exact-home native resume or same-harness
|
|
3029
|
+
* `/continue`; callers never guess which version home can see the transcript.
|
|
3030
|
+
* `portable` is for an SSH/terminal-engine command that executes on a peer.
|
|
3031
|
+
*/
|
|
3032
|
+
export function buildSessionRecoveryCommand(session, portable = false) {
|
|
3033
|
+
const args = sessionRecoveryRunArgs(session);
|
|
3034
|
+
if (portable)
|
|
3035
|
+
return ['agents', ...args];
|
|
3036
|
+
const invocation = getAgentsInvocation(args);
|
|
3037
|
+
return [invocation.command, ...invocation.args];
|
|
3038
|
+
}
|
|
3048
3039
|
/**
|
|
3049
3040
|
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
3050
3041
|
*
|
|
@@ -3112,8 +3103,6 @@ function spawnResumeCommand(cmd, cwd) {
|
|
|
3112
3103
|
* isolated HOME where the JSONL was written — regardless of which version is
|
|
3113
3104
|
* currently the default. Falls back to the bare shim when version is unknown.
|
|
3114
3105
|
*
|
|
3115
|
-
* If the versioned binary is missing (version was removed), the ENOENT
|
|
3116
|
-
* handler in handlePickedSession retries via buildFallbackCommand.
|
|
3117
3106
|
*/
|
|
3118
3107
|
/**
|
|
3119
3108
|
* The agent's own resume invocation, given whichever launcher we resolved.
|
|
@@ -3180,19 +3169,6 @@ export function buildResumeCommand(session) {
|
|
|
3180
3169
|
return null;
|
|
3181
3170
|
}
|
|
3182
3171
|
}
|
|
3183
|
-
/**
|
|
3184
|
-
* Fallback when the pinned version really is gone: the same resume invocation
|
|
3185
|
-
* against the current version.
|
|
3186
|
-
*
|
|
3187
|
-
* This used to spawn `<cli> "/continue <id>"`, feeding a slash command into the
|
|
3188
|
-
* TUI as a prompt. Neither CLI has `/continue` — codex documents `/resume` — so
|
|
3189
|
-
* the agent received an unrecognised command and the session was not resumed at
|
|
3190
|
-
* all. Reusing resumeArgv keeps the two paths from drifting apart again.
|
|
3191
|
-
*/
|
|
3192
|
-
function buildFallbackCommand(session) {
|
|
3193
|
-
const cli = AGENTS[session.agent]?.cliCommand ?? session.agent;
|
|
3194
|
-
return resumeArgv(session.agent, session.id, cli);
|
|
3195
|
-
}
|
|
3196
3172
|
// ---------------------------------------------------------------------------
|
|
3197
3173
|
// Cloud session source (--cloud)
|
|
3198
3174
|
// ---------------------------------------------------------------------------
|
|
@@ -3263,25 +3239,24 @@ async function runCloudSessions(query, options) {
|
|
|
3263
3239
|
// Ensure the SessionMeta points at the local cache path for renderSession.
|
|
3264
3240
|
await renderSession({ ...meta, filePath: cachedPath }, mode, filterOpts, options);
|
|
3265
3241
|
}
|
|
3242
|
+
/** Resolve the harness portion of a sessions selector with the CLI's canonical
|
|
3243
|
+
* alias and single-typo rules. Kept pure so focus and the browser cannot drift. */
|
|
3244
|
+
export function resolveSessionAgentName(name) {
|
|
3245
|
+
const normalized = name.toLowerCase();
|
|
3246
|
+
if (SESSION_AGENTS.includes(normalized)) {
|
|
3247
|
+
return normalized;
|
|
3248
|
+
}
|
|
3249
|
+
const resolved = resolveAgentName(normalized);
|
|
3250
|
+
if (resolved && SESSION_AGENTS.includes(resolved)) {
|
|
3251
|
+
return resolved;
|
|
3252
|
+
}
|
|
3253
|
+
return fuzzyMatch(normalized, SESSION_AGENTS, FUZZY_PRESETS.agents);
|
|
3254
|
+
}
|
|
3266
3255
|
export function parseAgentFilter(agentName) {
|
|
3267
3256
|
if (!agentName)
|
|
3268
3257
|
return {};
|
|
3269
3258
|
const [name, version] = agentName.split('@', 2);
|
|
3270
|
-
|
|
3271
|
-
? name
|
|
3272
|
-
: null;
|
|
3273
|
-
if (!agent) {
|
|
3274
|
-
// Aliases and single-typo corrections (cladue -> claude). SESSION_AGENTS
|
|
3275
|
-
// includes ids (rush, hermes) that resolveAgentName doesn't know, so fall
|
|
3276
|
-
// back to fuzzy-matching the session list directly.
|
|
3277
|
-
const resolved = resolveAgentName(name);
|
|
3278
|
-
if (resolved && SESSION_AGENTS.includes(resolved)) {
|
|
3279
|
-
agent = resolved;
|
|
3280
|
-
}
|
|
3281
|
-
else {
|
|
3282
|
-
agent = fuzzyMatch(name, SESSION_AGENTS, FUZZY_PRESETS.agents);
|
|
3283
|
-
}
|
|
3284
|
-
}
|
|
3259
|
+
const agent = resolveSessionAgentName(name);
|
|
3285
3260
|
if (!agent) {
|
|
3286
3261
|
console.error(chalk.red(`Unknown agent: ${name}. Use: ${SESSION_AGENTS.join(', ')}`));
|
|
3287
3262
|
process.exit(1);
|
|
@@ -3930,7 +3905,7 @@ export function registerSessionsCommands(program) {
|
|
|
3930
3905
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
3931
3906
|
.option('--all', 'Widen every non-status filter to "all": every directory (not just this project) and all time (no window cap). Status filters like --active still compose; -a/--device/--since still narrow their axis.')
|
|
3932
3907
|
.option('--unmanaged', "Also show sessions from your own ~/.<agent> installs (hidden once agents-cli manages that agent)")
|
|
3933
|
-
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
3908
|
+
.option('--team, --teams', 'Include team-spawned sessions (hidden by default)')
|
|
3934
3909
|
.option('--in-team <name>', "Only this team: the session that spawned it plus (with --teams) its teammates. Spans every directory and all time, since a team's worktrees and history sit outside the default window.")
|
|
3935
3910
|
.option('--routine', 'Show only sessions archived from routine runs')
|
|
3936
3911
|
.option('-p, --project <name>', 'Filter by project name (searches across all directories)')
|
|
@@ -3997,7 +3972,7 @@ export function registerSessionsCommands(program) {
|
|
|
3997
3972
|
agents sessions --crashed
|
|
3998
3973
|
|
|
3999
3974
|
# --- Session lifecycle (one verb per intent) ---
|
|
4000
|
-
#
|
|
3975
|
+
# Focus a session (attach its living terminal, or recover an ended one)
|
|
4001
3976
|
agents sessions focus a1b2c3d4
|
|
4002
3977
|
# Attach only — never fork a copy (old: sessions go)
|
|
4003
3978
|
agents sessions focus a1b2c3d4 --attach-only
|
|
@@ -4049,7 +4024,7 @@ export function registerSessionsCommands(program) {
|
|
|
4049
4024
|
`,
|
|
4050
4025
|
notes: `
|
|
4051
4026
|
Session lifecycle (pick one verb — they are not synonyms):
|
|
4052
|
-
focus [
|
|
4027
|
+
focus [selector] attach a living pane, or recover on the origin device
|
|
4053
4028
|
focus [id] --attach-only attach only; never fork (replaces sessions go)
|
|
4054
4029
|
detach <id> interactive → headless continuation
|
|
4055
4030
|
attach <id> headless → interactive in this terminal
|
|
@@ -66,7 +66,7 @@ function printBackendNotes(backend) {
|
|
|
66
66
|
console.log(chalk.gray('backend: keychain — macOS reads may ask for Touch ID or the device password.'));
|
|
67
67
|
}
|
|
68
68
|
else if (backend === 'file') {
|
|
69
|
-
console.log(chalk.gray('backend: file —
|
|
69
|
+
console.log(chalk.gray('backend: file — encrypted at rest; headless reads need no passphrase by default, via a machine-local key at ~/.agents/.secrets-key/passphrase.'));
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
72
72
|
console.log(chalk.gray('backend: vault — synced ~/.agents/vault.age storage; unlock it with agents login.'));
|