@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.
- package/CHANGELOG.md +51 -3
- package/README.md +156 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.d.ts +48 -0
- package/dist/commands/exec.js +159 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +748 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/plugins.js +28 -7
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +157 -10
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +54 -3
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +46 -0
- package/dist/lib/agents.js +121 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +66 -20
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +79 -4
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +205 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +45 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +175 -15
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +83 -10
- package/dist/lib/secrets/agent.js +237 -70
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +36 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- 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
|
-
|
|
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
|
*
|