@phnx-labs/agents-cli 1.20.64 → 1.20.66

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.
Files changed (188) hide show
  1. package/CHANGELOG.md +51 -3
  2. package/README.md +156 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/apply.d.ts +12 -0
  5. package/dist/commands/apply.js +274 -0
  6. package/dist/commands/browser.js +2 -2
  7. package/dist/commands/cloud.js +32 -2
  8. package/dist/commands/doctor.js +4 -1
  9. package/dist/commands/exec.d.ts +48 -0
  10. package/dist/commands/exec.js +159 -49
  11. package/dist/commands/feed.js +25 -11
  12. package/dist/commands/hosts.js +44 -6
  13. package/dist/commands/mcp.js +55 -5
  14. package/dist/commands/monitors.d.ts +12 -0
  15. package/dist/commands/monitors.js +748 -0
  16. package/dist/commands/output.js +2 -2
  17. package/dist/commands/plugins.js +28 -7
  18. package/dist/commands/routines.js +23 -2
  19. package/dist/commands/secrets.d.ts +16 -0
  20. package/dist/commands/secrets.js +215 -64
  21. package/dist/commands/serve.js +31 -0
  22. package/dist/commands/sessions-browser.d.ts +82 -0
  23. package/dist/commands/sessions-browser.js +320 -0
  24. package/dist/commands/sessions-export.js +8 -3
  25. package/dist/commands/sessions.d.ts +17 -0
  26. package/dist/commands/sessions.js +157 -10
  27. package/dist/commands/share.d.ts +2 -0
  28. package/dist/commands/share.js +150 -0
  29. package/dist/commands/ssh.js +54 -3
  30. package/dist/commands/versions.js +7 -3
  31. package/dist/commands/view.d.ts +26 -0
  32. package/dist/commands/view.js +32 -9
  33. package/dist/commands/webhook.js +10 -2
  34. package/dist/index.js +35 -14
  35. package/dist/lib/agents.d.ts +46 -0
  36. package/dist/lib/agents.js +121 -3
  37. package/dist/lib/auto-dispatch-provider.js +7 -2
  38. package/dist/lib/auto-dispatch.d.ts +3 -0
  39. package/dist/lib/auto-dispatch.js +3 -0
  40. package/dist/lib/browser/chrome.js +2 -2
  41. package/dist/lib/cloud/antigravity.js +2 -2
  42. package/dist/lib/cloud/host.d.ts +59 -0
  43. package/dist/lib/cloud/host.js +224 -0
  44. package/dist/lib/cloud/registry.js +4 -0
  45. package/dist/lib/cloud/types.d.ts +6 -4
  46. package/dist/lib/computer-rpc.js +3 -1
  47. package/dist/lib/crabbox/cli.js +5 -1
  48. package/dist/lib/crabbox/runtimes.js +11 -2
  49. package/dist/lib/daemon.d.ts +20 -4
  50. package/dist/lib/daemon.js +62 -19
  51. package/dist/lib/devices/connect.d.ts +18 -1
  52. package/dist/lib/devices/connect.js +10 -2
  53. package/dist/lib/devices/fleet.d.ts +3 -2
  54. package/dist/lib/devices/fleet.js +9 -0
  55. package/dist/lib/devices/known-hosts.d.ts +62 -0
  56. package/dist/lib/devices/known-hosts.js +137 -0
  57. package/dist/lib/devices/registry.d.ts +15 -0
  58. package/dist/lib/devices/registry.js +9 -0
  59. package/dist/lib/exec.d.ts +19 -2
  60. package/dist/lib/exec.js +41 -13
  61. package/dist/lib/fleet/apply.d.ts +63 -0
  62. package/dist/lib/fleet/apply.js +214 -0
  63. package/dist/lib/fleet/auth-sync.d.ts +67 -0
  64. package/dist/lib/fleet/auth-sync.js +142 -0
  65. package/dist/lib/fleet/manifest.d.ts +29 -0
  66. package/dist/lib/fleet/manifest.js +127 -0
  67. package/dist/lib/fleet/types.d.ts +129 -0
  68. package/dist/lib/fleet/types.js +13 -0
  69. package/dist/lib/git.d.ts +27 -0
  70. package/dist/lib/git.js +34 -2
  71. package/dist/lib/hosts/dispatch.d.ts +29 -8
  72. package/dist/lib/hosts/dispatch.js +66 -20
  73. package/dist/lib/hosts/passthrough.js +2 -0
  74. package/dist/lib/hosts/providers/devices.d.ts +27 -0
  75. package/dist/lib/hosts/providers/devices.js +98 -0
  76. package/dist/lib/hosts/registry.d.ts +10 -16
  77. package/dist/lib/hosts/registry.js +17 -50
  78. package/dist/lib/hosts/remote-cmd.d.ts +23 -0
  79. package/dist/lib/hosts/remote-cmd.js +79 -4
  80. package/dist/lib/hosts/run-target.d.ts +84 -0
  81. package/dist/lib/hosts/run-target.js +99 -0
  82. package/dist/lib/hosts/types.d.ts +23 -5
  83. package/dist/lib/hosts/types.js +22 -4
  84. package/dist/lib/linear-autoclose.d.ts +30 -0
  85. package/dist/lib/linear-autoclose.js +22 -0
  86. package/dist/lib/mcp.d.ts +27 -1
  87. package/dist/lib/mcp.js +126 -12
  88. package/dist/lib/monitors/config.d.ts +161 -0
  89. package/dist/lib/monitors/config.js +372 -0
  90. package/dist/lib/monitors/dispatch.d.ts +28 -0
  91. package/dist/lib/monitors/dispatch.js +91 -0
  92. package/dist/lib/monitors/engine.d.ts +61 -0
  93. package/dist/lib/monitors/engine.js +205 -0
  94. package/dist/lib/monitors/sources/command.d.ts +11 -0
  95. package/dist/lib/monitors/sources/command.js +31 -0
  96. package/dist/lib/monitors/sources/device.d.ts +13 -0
  97. package/dist/lib/monitors/sources/device.js +45 -0
  98. package/dist/lib/monitors/sources/file.d.ts +14 -0
  99. package/dist/lib/monitors/sources/file.js +57 -0
  100. package/dist/lib/monitors/sources/http.d.ts +10 -0
  101. package/dist/lib/monitors/sources/http.js +34 -0
  102. package/dist/lib/monitors/sources/index.d.ts +14 -0
  103. package/dist/lib/monitors/sources/index.js +31 -0
  104. package/dist/lib/monitors/sources/poll.d.ts +9 -0
  105. package/dist/lib/monitors/sources/poll.js +9 -0
  106. package/dist/lib/monitors/sources/types.d.ts +18 -0
  107. package/dist/lib/monitors/sources/types.js +9 -0
  108. package/dist/lib/monitors/sources/webhook.d.ts +23 -0
  109. package/dist/lib/monitors/sources/webhook.js +47 -0
  110. package/dist/lib/monitors/sources/ws.d.ts +14 -0
  111. package/dist/lib/monitors/sources/ws.js +45 -0
  112. package/dist/lib/monitors/state.d.ts +69 -0
  113. package/dist/lib/monitors/state.js +144 -0
  114. package/dist/lib/picker.d.ts +53 -0
  115. package/dist/lib/picker.js +214 -1
  116. package/dist/lib/platform/exec.d.ts +16 -0
  117. package/dist/lib/platform/exec.js +17 -0
  118. package/dist/lib/plugins.d.ts +31 -1
  119. package/dist/lib/plugins.js +175 -15
  120. package/dist/lib/redact.d.ts +14 -1
  121. package/dist/lib/redact.js +47 -1
  122. package/dist/lib/remote-agents-json.js +7 -1
  123. package/dist/lib/rotate.d.ts +6 -3
  124. package/dist/lib/rotate.js +0 -1
  125. package/dist/lib/routines.d.ts +16 -0
  126. package/dist/lib/routines.js +19 -0
  127. package/dist/lib/runner.d.ts +1 -0
  128. package/dist/lib/runner.js +102 -9
  129. package/dist/lib/secrets/agent.d.ts +83 -10
  130. package/dist/lib/secrets/agent.js +237 -70
  131. package/dist/lib/secrets/bundles.d.ts +26 -0
  132. package/dist/lib/secrets/bundles.js +59 -8
  133. package/dist/lib/secrets/filestore.d.ts +9 -0
  134. package/dist/lib/secrets/filestore.js +21 -8
  135. package/dist/lib/secrets/index.d.ts +7 -0
  136. package/dist/lib/secrets/index.js +26 -6
  137. package/dist/lib/secrets/mcp.js +4 -2
  138. package/dist/lib/secrets/remote.d.ts +17 -0
  139. package/dist/lib/secrets/remote.js +40 -0
  140. package/dist/lib/self-update.d.ts +20 -0
  141. package/dist/lib/self-update.js +54 -1
  142. package/dist/lib/serve/control.d.ts +95 -0
  143. package/dist/lib/serve/control.js +260 -0
  144. package/dist/lib/serve/server.d.ts +35 -1
  145. package/dist/lib/serve/server.js +106 -76
  146. package/dist/lib/serve/stream.d.ts +43 -0
  147. package/dist/lib/serve/stream.js +116 -0
  148. package/dist/lib/serve/token.d.ts +35 -0
  149. package/dist/lib/serve/token.js +85 -0
  150. package/dist/lib/session/bundle.d.ts +14 -0
  151. package/dist/lib/session/bundle.js +12 -1
  152. package/dist/lib/session/remote-list.js +5 -1
  153. package/dist/lib/session/state.d.ts +7 -25
  154. package/dist/lib/session/state.js +16 -6
  155. package/dist/lib/session/sync/config.js +8 -2
  156. package/dist/lib/session/types.d.ts +30 -0
  157. package/dist/lib/share/capture.d.ts +29 -0
  158. package/dist/lib/share/capture.js +140 -0
  159. package/dist/lib/share/config.d.ts +35 -0
  160. package/dist/lib/share/config.js +100 -0
  161. package/dist/lib/share/og.d.ts +25 -0
  162. package/dist/lib/share/og.js +84 -0
  163. package/dist/lib/share/provision.d.ts +10 -0
  164. package/dist/lib/share/provision.js +91 -0
  165. package/dist/lib/share/publish.d.ts +57 -0
  166. package/dist/lib/share/publish.js +145 -0
  167. package/dist/lib/share/worker-template.d.ts +2 -0
  168. package/dist/lib/share/worker-template.js +82 -0
  169. package/dist/lib/shims.d.ts +13 -0
  170. package/dist/lib/shims.js +42 -2
  171. package/dist/lib/ssh-exec.d.ts +24 -0
  172. package/dist/lib/ssh-exec.js +15 -3
  173. package/dist/lib/ssh-tunnel.d.ts +19 -1
  174. package/dist/lib/ssh-tunnel.js +86 -7
  175. package/dist/lib/startup/command-registry.d.ts +3 -0
  176. package/dist/lib/startup/command-registry.js +6 -0
  177. package/dist/lib/state.d.ts +5 -0
  178. package/dist/lib/state.js +12 -0
  179. package/dist/lib/tmux/session.d.ts +7 -0
  180. package/dist/lib/tmux/session.js +3 -1
  181. package/dist/lib/triggers/webhook.d.ts +18 -0
  182. package/dist/lib/triggers/webhook.js +105 -0
  183. package/dist/lib/types.d.ts +36 -1
  184. package/dist/lib/usage.js +7 -5
  185. package/dist/lib/versions.js +14 -11
  186. package/dist/lib/workflows.d.ts +20 -0
  187. package/dist/lib/workflows.js +24 -0
  188. package/package.json +2 -1
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Interactive fleet-wide session browser — the human front-end of `agents sessions`.
3
+ *
4
+ * One canonical filter state (device / agent / project / window / running / teams),
5
+ * driven by single-key hotkeys, re-pulling live over the same fleet fan-out the flag
6
+ * surface uses. The identical state is expressible as flags (the agent front-end);
7
+ * `y` / `--print-cmd` round-trips a hand-built view into a copy-pasteable command.
8
+ *
9
+ * Built on {@link dynamicPicker}; every data source (discover, fleet, live index,
10
+ * preview, resume dispatch) is reused from the existing sessions plumbing.
11
+ */
12
+ import type { SessionMeta } from '../lib/session/types.js';
13
+ /**
14
+ * The single canonical filter state. Every field has a flag equivalent, so the
15
+ * same view is reachable interactively (hotkeys) or from the command line (flags).
16
+ */
17
+ export interface BrowserFilter {
18
+ /** running-only — the `R` key / `--active`. */
19
+ running: boolean;
20
+ /** include team-spawned sessions — the `C` key / `--teams`. */
21
+ teams: boolean;
22
+ /** filter to one agent, or all — the `A` key / `-a`. */
23
+ agent?: string;
24
+ /** filter to one machine, or all — the `D` key / `--device`. */
25
+ device?: string;
26
+ /** this-repo subtree vs every directory — the `P` key / `--all`. */
27
+ projectScope: 'repo' | 'all';
28
+ /** time window (undefined = all time) — the `W` key / `--since`. */
29
+ window?: string;
30
+ }
31
+ /** Return the next value in `[undefined, ...options]`, wrapping. */
32
+ export declare function cycle(current: string | undefined, options: string[]): string | undefined;
33
+ export declare function cycleWindow(current: string | undefined): string | undefined;
34
+ /**
35
+ * Cheap client-side match for the `S` search — a plain substring test over a
36
+ * row's visible fields. Deliberately NOT the FTS `filterSessionsByQuery`: that
37
+ * runs a content-index scan per call, which is fine once over a pool but a
38
+ * CPU sink when a picker calls it per-row on every keystroke.
39
+ */
40
+ export declare function sessionMatchesQuery(s: SessionMeta, query: string): boolean;
41
+ /**
42
+ * The canonical `ag sessions …` command for a filter state (+ optional search) —
43
+ * the agent-facing twin of the interactive view. Shared by the `y` hotkey and
44
+ * `--print-cmd`.
45
+ */
46
+ export declare function browserFilterToArgv(f: BrowserFilter, query?: string): string[];
47
+ /** Normalize a `--host`/`--device` token (`alias`, `user@host`, `host.domain`) to
48
+ * the canonical machine id the rows carry in `.machine`, so a flag seed matches
49
+ * (the `d` hotkey already cycles canonical ids). Mirrors sessions.ts `hostToken`. */
50
+ export declare function normalizeDeviceSeed(host: string | undefined): string | undefined;
51
+ /**
52
+ * The initial filter for the `--active` browser: fleet-wide (matches the static
53
+ * `renderActiveSessions`, which has no project scoping — the `p` hotkey narrows to
54
+ * this repo), running-only, with the device seed normalized and `--since` seeding
55
+ * the window. Pure, so the routing call site is unit-testable.
56
+ */
57
+ export declare function activeBrowserSeed(opts: {
58
+ teams?: boolean;
59
+ agent?: string;
60
+ host?: string[];
61
+ since?: string;
62
+ }): Partial<BrowserFilter>;
63
+ /**
64
+ * The initial filter for the bare interactive listing: current-repo subtree by
65
+ * default (matches the static overview's cwd scoping), `--all` widens to every
66
+ * directory, `--since` seeds the window.
67
+ */
68
+ export declare function bareBrowserSeed(opts: {
69
+ teams?: boolean;
70
+ agent?: string;
71
+ all?: boolean;
72
+ since?: string;
73
+ }): Partial<BrowserFilter>;
74
+ /**
75
+ * Launch the interactive session browser. `initial` seeds the filter (e.g.
76
+ * `{ running: true }` for `--active`). Resolves after the user resumes a session
77
+ * or cancels — the picked row is dispatched through the shared resume/focus path.
78
+ */
79
+ export declare function runSessionBrowser(initial?: Partial<BrowserFilter>, opts?: {
80
+ local?: boolean;
81
+ hosts?: string[];
82
+ }): Promise<void>;
@@ -0,0 +1,320 @@
1
+ /**
2
+ * Interactive fleet-wide session browser — the human front-end of `agents sessions`.
3
+ *
4
+ * One canonical filter state (device / agent / project / window / running / teams),
5
+ * driven by single-key hotkeys, re-pulling live over the same fleet fan-out the flag
6
+ * surface uses. The identical state is expressible as flags (the agent front-end);
7
+ * `y` / `--print-cmd` round-trips a hand-built view into a copy-pasteable command.
8
+ *
9
+ * Built on {@link dynamicPicker}; every data source (discover, fleet, live index,
10
+ * preview, resume dispatch) is reused from the existing sessions plumbing.
11
+ */
12
+ import { spawnSync } from 'child_process';
13
+ import { dynamicPicker } from '../lib/picker.js';
14
+ import { getActiveSessions } from '../lib/session/active.js';
15
+ import { discoverSessions } from '../lib/session/discover.js';
16
+ import { gatherRemoteList } from '../lib/session/remote-list.js';
17
+ import { machineId, normalizeHost } from '../lib/session/sync/config.js';
18
+ import { buildPreview } from './sessions-picker.js';
19
+ import { formatPickerLabel, pickerColumnsFor, ticketLabel, mergeLocalFirst, indexActiveBySessionId, handlePickedSession, shouldIncludeLocal, remoteHostsToDial, } from './sessions.js';
20
+ /** Ordered window cycle for the `W` key. `undefined` = all time. */
21
+ const WINDOW_CYCLE = [undefined, '1d', '7d', '30d'];
22
+ /** Return the next value in `[undefined, ...options]`, wrapping. */
23
+ export function cycle(current, options) {
24
+ const ring = [undefined, ...options];
25
+ const idx = ring.findIndex((v) => v === current);
26
+ return ring[(idx + 1) % ring.length];
27
+ }
28
+ export function cycleWindow(current) {
29
+ const idx = WINDOW_CYCLE.findIndex((v) => v === current);
30
+ return WINDOW_CYCLE[(idx + 1) % WINDOW_CYCLE.length];
31
+ }
32
+ function distinct(values) {
33
+ return [...new Set(values.filter((v) => !!v))].sort();
34
+ }
35
+ /**
36
+ * Cheap client-side match for the `S` search — a plain substring test over a
37
+ * row's visible fields. Deliberately NOT the FTS `filterSessionsByQuery`: that
38
+ * runs a content-index scan per call, which is fine once over a pool but a
39
+ * CPU sink when a picker calls it per-row on every keystroke.
40
+ */
41
+ export function sessionMatchesQuery(s, query) {
42
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
43
+ if (terms.length === 0)
44
+ return true;
45
+ const hay = [
46
+ s.shortId,
47
+ s.agent,
48
+ s.project,
49
+ s.cwd,
50
+ s.topic,
51
+ s.label,
52
+ ticketLabel(s),
53
+ s.machine,
54
+ ]
55
+ .filter(Boolean)
56
+ .join(' ')
57
+ .toLowerCase();
58
+ return terms.every((t) => hay.includes(t));
59
+ }
60
+ /**
61
+ * The canonical `ag sessions …` command for a filter state (+ optional search) —
62
+ * the agent-facing twin of the interactive view. Shared by the `y` hotkey and
63
+ * `--print-cmd`.
64
+ */
65
+ export function browserFilterToArgv(f, query = '') {
66
+ const a = ['sessions'];
67
+ if (f.running)
68
+ a.push('--active');
69
+ if (f.teams)
70
+ a.push('--teams');
71
+ if (f.agent)
72
+ a.push('-a', f.agent);
73
+ if (f.device)
74
+ a.push('--device', f.device);
75
+ if (f.projectScope === 'all')
76
+ a.push('--all');
77
+ if (f.window)
78
+ a.push('--since', f.window);
79
+ const q = query.trim();
80
+ if (q)
81
+ a.push(JSON.stringify(q));
82
+ return a;
83
+ }
84
+ /** Normalize a `--host`/`--device` token (`alias`, `user@host`, `host.domain`) to
85
+ * the canonical machine id the rows carry in `.machine`, so a flag seed matches
86
+ * (the `d` hotkey already cycles canonical ids). Mirrors sessions.ts `hostToken`. */
87
+ export function normalizeDeviceSeed(host) {
88
+ if (!host)
89
+ return undefined;
90
+ return normalizeHost(host.split('@').pop() || host);
91
+ }
92
+ /**
93
+ * The initial filter for the `--active` browser: fleet-wide (matches the static
94
+ * `renderActiveSessions`, which has no project scoping — the `p` hotkey narrows to
95
+ * this repo), running-only, with the device seed normalized and `--since` seeding
96
+ * the window. Pure, so the routing call site is unit-testable.
97
+ */
98
+ export function activeBrowserSeed(opts) {
99
+ return {
100
+ running: true,
101
+ teams: !!opts.teams,
102
+ agent: opts.agent,
103
+ projectScope: 'all',
104
+ device: normalizeDeviceSeed(opts.host?.[0]),
105
+ window: opts.since ?? '30d',
106
+ };
107
+ }
108
+ /**
109
+ * The initial filter for the bare interactive listing: current-repo subtree by
110
+ * default (matches the static overview's cwd scoping), `--all` widens to every
111
+ * directory, `--since` seeds the window.
112
+ */
113
+ export function bareBrowserSeed(opts) {
114
+ return {
115
+ teams: !!opts.teams,
116
+ agent: opts.agent,
117
+ projectScope: opts.all ? 'all' : 'repo',
118
+ window: opts.since ?? '30d',
119
+ };
120
+ }
121
+ /** Copy text to the OS clipboard (best-effort; silently no-ops if unavailable). */
122
+ function copyToClipboard(text) {
123
+ const candidates = process.platform === 'darwin'
124
+ ? [['pbcopy', []]]
125
+ : [
126
+ ['wl-copy', []],
127
+ ['xclip', ['-selection', 'clipboard']],
128
+ ['xsel', ['--clipboard', '--input']],
129
+ ];
130
+ for (const [cmd, args] of candidates) {
131
+ try {
132
+ const res = spawnSync(cmd, args, { input: text });
133
+ if (res.status === 0)
134
+ return true;
135
+ }
136
+ catch {
137
+ // try the next candidate
138
+ }
139
+ }
140
+ return false;
141
+ }
142
+ /** The transcript pool: the local index + (unless --local) a live fleet fan-out.
143
+ * The live index is fetched separately/lazily — it's the slow part and only the
144
+ * running filter needs it, so a bare browse stays instant.
145
+ *
146
+ * `hosts` is the explicit `--host`/`--device` scope (if any): it restricts which
147
+ * peers are dialed and whether local is included, honoring the flag's "scope,
148
+ * not add" contract instead of always sweeping the whole fleet. */
149
+ async function fetchRawPool(f, self, local, hosts) {
150
+ const since = f.window;
151
+ // Local pool: wide (every directory) — device/agent/project are applied in
152
+ // memory so a hotkey toggle is instant and doesn't re-hit the disk. Skipped
153
+ // when an explicit host scope excludes this machine.
154
+ let rows = shouldIncludeLocal(hosts, self)
155
+ ? await discoverSessions({
156
+ all: true,
157
+ cwd: process.cwd(),
158
+ since,
159
+ excludeTeamOrigin: !f.teams,
160
+ limit: 500,
161
+ sortBy: 'timestamp',
162
+ })
163
+ : [];
164
+ // Fleet: fold in peers' own indexes over SSH (no sync), same as the flag path.
165
+ // Skipped under --local. An explicit --host/--device scopes exactly which peers
166
+ // are dialed (undefined = sweep every online device). Best-effort — a fan-out
167
+ // failure leaves the local list intact.
168
+ if (!local) {
169
+ try {
170
+ const forwarded = ['sessions', '--all', '--json', '--limit', '500'];
171
+ if (since)
172
+ forwarded.push('--since', since);
173
+ if (f.teams)
174
+ forwarded.push('--teams');
175
+ const { sessions: remote } = await gatherRemoteList(forwarded, remoteHostsToDial(hosts, self));
176
+ if (remote.length > 0)
177
+ rows = mergeLocalFirst([...rows, ...remote], self);
178
+ }
179
+ catch {
180
+ // enrichment, never a hard dependency
181
+ }
182
+ }
183
+ return { key: `${since ?? 'all'}|${f.teams}`, rows };
184
+ }
185
+ /** Apply the cheap in-memory filters (agent / device / project / running). */
186
+ function applyFilters(rows, live, f, self) {
187
+ let out = rows;
188
+ if (f.agent)
189
+ out = out.filter((r) => r.agent === f.agent);
190
+ if (f.device)
191
+ out = out.filter((r) => (r.machine ?? self) === f.device);
192
+ if (f.projectScope === 'repo') {
193
+ const cwd = process.cwd();
194
+ out = out.filter((r) => !!r.cwd && (r.cwd === cwd || r.cwd.startsWith(cwd + '/')));
195
+ }
196
+ if (f.running)
197
+ out = out.filter((r) => live.has(r.id));
198
+ return out;
199
+ }
200
+ function headerFor(f) {
201
+ const bits = [
202
+ `device:${f.device ?? 'all'}`,
203
+ `agent:${f.agent ?? 'all'}`,
204
+ f.projectScope === 'repo' ? 'this repo' : 'all dirs',
205
+ `window:${f.window ?? 'all'}`,
206
+ ];
207
+ if (f.running)
208
+ bits.push('running');
209
+ if (f.teams)
210
+ bits.push('teams');
211
+ return bits.join(' · ');
212
+ }
213
+ function helpFor(_f, mode) {
214
+ if (mode === 'search') {
215
+ return 'type to filter · ↑↓ navigate · esc exit search · ⏎ resume';
216
+ }
217
+ return 's search · r running · c teams · a agent · d device · p project · w window · y copy-cmd · ⏎ resume · esc quit';
218
+ }
219
+ /**
220
+ * Launch the interactive session browser. `initial` seeds the filter (e.g.
221
+ * `{ running: true }` for `--active`). Resolves after the user resumes a session
222
+ * or cancels — the picked row is dispatched through the shared resume/focus path.
223
+ */
224
+ export async function runSessionBrowser(initial = {}, opts = {}) {
225
+ const self = machineId();
226
+ const local = opts.local ?? false;
227
+ const hosts = opts.hosts && opts.hosts.length > 0 ? opts.hosts : undefined;
228
+ // Updated after each load so the A/D cycles range over what's actually present.
229
+ let agentsInPool = [];
230
+ let devicesInPool = [];
231
+ let cols = {};
232
+ // Cache the transcript fetch, keyed by (window, teams); agent/device/project/
233
+ // running are applied in memory so their hotkeys don't re-fan-out the fleet.
234
+ let rawCache = null;
235
+ // The live index is slow (a full ps/tmux scan) and only the running filter
236
+ // needs it — fetch it once, lazily, the first time running is toggled on.
237
+ let liveCache = null;
238
+ // Generation guard: two quick keypresses can start overlapping loads whose
239
+ // SSH fan-outs settle out of order. dynamicPicker's own gen ref guards which
240
+ // rows become `items`, but the shared closure state below (cols / cycle pools /
241
+ // caches) is a side channel it can't see — so a stale load must never commit
242
+ // it. We compute into locals and only write the shared state as the latest load.
243
+ let loadGen = 0;
244
+ const initialFilter = {
245
+ running: initial.running ?? false,
246
+ teams: initial.teams ?? false,
247
+ agent: initial.agent,
248
+ device: initial.device,
249
+ projectScope: initial.projectScope ?? 'repo',
250
+ window: 'window' in initial ? initial.window : '30d',
251
+ };
252
+ const load = async (f) => {
253
+ const myGen = ++loadGen;
254
+ const key = `${f.window ?? 'all'}|${f.teams}`;
255
+ let pool = rawCache && rawCache.key === key ? rawCache : null;
256
+ if (!pool) {
257
+ const fetched = await fetchRawPool(f, self, local, hosts);
258
+ if (myGen !== loadGen)
259
+ return []; // superseded — don't touch shared state
260
+ pool = fetched;
261
+ }
262
+ // Only pay for the live scan when the running filter needs it.
263
+ let live = liveCache;
264
+ if (f.running && !live) {
265
+ try {
266
+ live = indexActiveBySessionId(await getActiveSessions());
267
+ }
268
+ catch {
269
+ live = new Map();
270
+ }
271
+ if (myGen !== loadGen)
272
+ return [];
273
+ }
274
+ // Latest load — commit shared state atomically (no await past this point, so
275
+ // no newer load can interleave between these writes).
276
+ rawCache = pool;
277
+ if (live)
278
+ liveCache = live;
279
+ agentsInPool = distinct(pool.rows.map((r) => r.agent));
280
+ devicesInPool = distinct(pool.rows.map((r) => r.machine ?? self));
281
+ const filtered = applyFilters(pool.rows, live ?? new Map(), f, self);
282
+ cols = pickerColumnsFor(filtered);
283
+ return filtered;
284
+ };
285
+ const picked = await dynamicPicker({
286
+ message: 'Sessions',
287
+ initialFilter,
288
+ load,
289
+ keyFor: (s) => s.id,
290
+ labelFor: (s, q) => formatPickerLabel(s, q, cols),
291
+ matches: sessionMatchesQuery,
292
+ buildPreview,
293
+ headerFor,
294
+ helpFor,
295
+ enterHint: 'resume',
296
+ emptyMessage: 'No sessions match this filter.',
297
+ loadingMessage: local ? 'Loading…' : 'Loading (reaching other machines)…',
298
+ keyBindings: {
299
+ r: (f) => ({ ...f, running: !f.running }),
300
+ c: (f) => ({ ...f, teams: !f.teams }),
301
+ a: (f) => ({ ...f, agent: cycle(f.agent, agentsInPool) }),
302
+ d: (f) => ({ ...f, device: cycle(f.device, devicesInPool) }),
303
+ p: (f) => ({ ...f, projectScope: f.projectScope === 'repo' ? 'all' : 'repo' }),
304
+ w: (f) => ({ ...f, window: cycleWindow(f.window) }),
305
+ },
306
+ onKey: (name, f, _active, query) => {
307
+ if (name === 'y') {
308
+ // Thread the live search query so the copied command reproduces the
309
+ // exact view — the human→agent bridge must include the search term.
310
+ const cmd = 'ag ' + browserFilterToArgv(f, query).join(' ');
311
+ const ok = copyToClipboard(cmd);
312
+ return ok ? `copied: ${cmd}` : cmd;
313
+ }
314
+ return undefined;
315
+ },
316
+ });
317
+ if (!picked)
318
+ return;
319
+ await handlePickedSession({ session: picked.item, action: 'resume' });
320
+ }
@@ -27,7 +27,8 @@ import { machineId } from '../lib/machine-id.js';
27
27
  import { getHistoryDir } from '../lib/state.js';
28
28
  import { loadR2Config } from '../lib/session/sync/config.js';
29
29
  import { resolveSyncEncKey, generateSyncEncKey } from '../lib/session/sync/transcript-crypto.js';
30
- import { buildRecord, makeHeader, mergeRecords, serializeBundle, specForAgent, } from '../lib/session/bundle.js';
30
+ import { buildRecord, makeHeader, mergeRecords, serializeBundle, writeBundleFile, specForAgent, } from '../lib/session/bundle.js';
31
+ import { knownSecretValuesFromEnv } from '../lib/redact.js';
31
32
  import { pullBundlesFromHosts } from '../lib/session/remote-bundle.js';
32
33
  import { setHelpSections } from '../lib/help.js';
33
34
  /** Default cap when exporting a scope (not explicit ids) and the user gave no -n. */
@@ -119,11 +120,15 @@ async function runExport(selectors, command) {
119
120
  // 4. Resolve encryption key (opt-in) + redaction (default on via parent --no-redact).
120
121
  const encryptKey = g.encrypt ? resolveExportKey() : null;
121
122
  const redact = g.redact !== false;
123
+ // Value-aware redaction: mask live credential values already in the
124
+ // environment (e.g. an injected secrets bundle) verbatim, whatever their
125
+ // format. Only meaningful when redacting.
126
+ const knownSecrets = redact ? knownSecretValuesFromEnv() : undefined;
122
127
  // 5. Build records + header.
123
128
  const records = [];
124
129
  for (const f of files) {
125
130
  try {
126
- records.push(buildRecord(f, { redact, encryptKey }));
131
+ records.push(buildRecord(f, { redact, encryptKey, knownSecrets }));
127
132
  }
128
133
  catch (err) {
129
134
  process.stderr.write(chalk.yellow(`Skipped ${f.agent}/${f.sessionId} (${f.relKey}): ${err.message}\n`));
@@ -193,7 +198,7 @@ function emitBundle(header, records, g) {
193
198
  return;
194
199
  }
195
200
  const outPath = g.output || defaultBundlePath();
196
- fs.writeFileSync(outPath, wire, 'utf-8');
201
+ writeBundleFile(outPath, wire);
197
202
  process.stderr.write(chalk.green(`Exported ${header.sessions} session${header.sessions === 1 ? '' : 's'} ` +
198
203
  `(${header.count} file${header.count === 1 ? '' : 's'}${header.encrypted ? ', encrypted' : ''}${header.redacted ? ', redacted' : ''}) ` +
199
204
  `→ ${outPath}\n`));
@@ -193,6 +193,7 @@ export declare function formatPickerLabel(s: SessionMeta, query: string, cols?:
193
193
  */
194
194
  export declare function formatPickerTip(sessions: SessionMeta[]): string;
195
195
  export declare function pickSessionInteractive(sessions: SessionMeta[], message?: string, initialSearch?: string, hiddenCount?: number, enterHint?: string): Promise<PickedSession | null>;
196
+ export declare function handlePickedSession(picked: PickedSession): Promise<void>;
196
197
  /**
197
198
  * Resume a session in the current terminal — a foreground takeover of this
198
199
  * process. Used by the single-select picker and by `sessions resume` when the
@@ -201,6 +202,22 @@ export declare function pickSessionInteractive(sessions: SessionMeta[], message?
201
202
  * binary is missing (ENOENT).
202
203
  */
203
204
  export declare function resumeSessionInPlace(session: SessionMeta): Promise<void>;
205
+ /**
206
+ * Map a resume argv to the spawn(command, args, {shell}) triple.
207
+ *
208
+ * On Windows the agent launcher is a `.cmd`/PATHEXT shim and needs shell:true.
209
+ * With shell:true, Node concatenates args into the cmd.exe line unescaped
210
+ * (DEP0190 + injection). A session.id derived from a filename can carry
211
+ * metacharacters (`&|<>`); compose a fully-quoted line and pass an EMPTY args
212
+ * array so cmd.exe cannot reparse them (RUSH-1753). See composeWin32CommandLine.
213
+ *
214
+ * `platform` is injectable so the win32 shell path is unit-testable on any host.
215
+ */
216
+ export declare function resumeSpawnInvocation(cmd: string[], platform?: NodeJS.Platform): {
217
+ command: string;
218
+ args: string[];
219
+ shell: boolean;
220
+ };
204
221
  /**
205
222
  * Build the shell command that resumes a picked session.
206
223
  *