@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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native per-monitor state-diff store.
|
|
3
|
+
*
|
|
4
|
+
* This is the one genuinely new primitive monitors add over routines: a
|
|
5
|
+
* last-observed-*value* store. Routines persist per-*run* metadata but have no
|
|
6
|
+
* last-seen value, so hand-built watchers (the RUSH-1107 SSL watcher) re-invented
|
|
7
|
+
* state-diffing through a markdown memory file every time. This store kills that.
|
|
8
|
+
*
|
|
9
|
+
* Layout (sibling of the runs layout, atomic writes like writeRunMeta):
|
|
10
|
+
* ~/.agents/.history/monitors/<name>/state.json # last-seen hash/value + fire bookkeeping
|
|
11
|
+
* ~/.agents/.history/monitors/<name>/fires/<id>/… # fire history
|
|
12
|
+
*/
|
|
13
|
+
import type { MonitorEvent } from './config.js';
|
|
14
|
+
/** Persisted last-seen state for one monitor. */
|
|
15
|
+
export interface MonitorState {
|
|
16
|
+
monitorName: string;
|
|
17
|
+
/** Hash of the last-seen de-dupe signature (see hasChanged). */
|
|
18
|
+
lastHash: string;
|
|
19
|
+
/** The last-seen raw observation (truncated for storage). */
|
|
20
|
+
lastValue: string;
|
|
21
|
+
/** RFC3339 timestamp of the last observation. */
|
|
22
|
+
lastSeenAt: string;
|
|
23
|
+
/** RFC3339 timestamp of the last fire, when the monitor has ever fired. */
|
|
24
|
+
lastFiredAt?: string;
|
|
25
|
+
/** Epoch-ms timestamps of recent fires, for the rate-limit / firehose guard. */
|
|
26
|
+
fireTimes?: number[];
|
|
27
|
+
}
|
|
28
|
+
/** Per-monitor history root, with the (untrusted) name contained to one segment. */
|
|
29
|
+
export declare function getMonitorHistoryDir(name: string): string;
|
|
30
|
+
/** Directory holding a monitor's fire history. */
|
|
31
|
+
export declare function getMonitorFiresDir(name: string): string;
|
|
32
|
+
/** Read a monitor's last-seen state, or null if it has never been observed. */
|
|
33
|
+
export declare function readState(name: string): MonitorState | null;
|
|
34
|
+
/** Persist a monitor's state atomically (temp file + rename, like writeRunMeta). */
|
|
35
|
+
export declare function writeStateRaw(state: MonitorState): void;
|
|
36
|
+
/**
|
|
37
|
+
* Record a new observation as the monitor's last-seen state, preserving fire
|
|
38
|
+
* bookkeeping. Truncates the stored raw value so a firehose can't bloat disk.
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeState(name: string, value: string, dedupeKey?: string, extra?: Partial<Pick<MonitorState, 'lastFiredAt' | 'fireTimes'>>): MonitorState;
|
|
41
|
+
/**
|
|
42
|
+
* The de-dupe signature for an observation. When `dedupeKey` is set, the
|
|
43
|
+
* signature is the first regex match of dedupeKey against the observation (so
|
|
44
|
+
* "the same event" is same matched token); otherwise it is the full observation.
|
|
45
|
+
* An unmatched dedupeKey falls back to the full observation.
|
|
46
|
+
*/
|
|
47
|
+
export declare function dedupeSignature(observation: string, dedupeKey?: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* True when `observation`'s de-dupe signature differs from the monitor's
|
|
50
|
+
* last-seen signature (or the monitor has never been observed). Pure read — the
|
|
51
|
+
* caller persists the new value via writeState only on a real fire.
|
|
52
|
+
*/
|
|
53
|
+
export declare function hasChanged(name: string, observation: string, dedupeKey?: string): boolean;
|
|
54
|
+
/**
|
|
55
|
+
* Append a fire timestamp and return the pruned window (fires within `windowMs`).
|
|
56
|
+
* The engine uses the returned length to decide whether the rate limit tripped.
|
|
57
|
+
*/
|
|
58
|
+
export declare function recordFireTime(name: string, now: number, windowMs: number): number[];
|
|
59
|
+
/** Write a fire record to fires/<id>/event.json and return the fire id. */
|
|
60
|
+
export declare function writeFireRecord(event: MonitorEvent, meta?: Record<string, unknown>): string;
|
|
61
|
+
/** A single fire history entry (as read back from disk). */
|
|
62
|
+
export interface FireRecord extends MonitorEvent {
|
|
63
|
+
runId?: string;
|
|
64
|
+
action?: string;
|
|
65
|
+
ok?: boolean;
|
|
66
|
+
error?: string;
|
|
67
|
+
}
|
|
68
|
+
/** List a monitor's fire history, chronologically ascending. */
|
|
69
|
+
export declare function listFires(name: string): FireRecord[];
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native per-monitor state-diff store.
|
|
3
|
+
*
|
|
4
|
+
* This is the one genuinely new primitive monitors add over routines: a
|
|
5
|
+
* last-observed-*value* store. Routines persist per-*run* metadata but have no
|
|
6
|
+
* last-seen value, so hand-built watchers (the RUSH-1107 SSL watcher) re-invented
|
|
7
|
+
* state-diffing through a markdown memory file every time. This store kills that.
|
|
8
|
+
*
|
|
9
|
+
* Layout (sibling of the runs layout, atomic writes like writeRunMeta):
|
|
10
|
+
* ~/.agents/.history/monitors/<name>/state.json # last-seen hash/value + fire bookkeeping
|
|
11
|
+
* ~/.agents/.history/monitors/<name>/fires/<id>/… # fire history
|
|
12
|
+
*/
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import { createHash } from 'crypto';
|
|
16
|
+
import { getMonitorsHistoryDir, ensureAgentsDir } from '../state.js';
|
|
17
|
+
import { safeJoin } from '../paths.js';
|
|
18
|
+
const MAX_STORED_VALUE = 4096;
|
|
19
|
+
/** Per-monitor history root, with the (untrusted) name contained to one segment. */
|
|
20
|
+
export function getMonitorHistoryDir(name) {
|
|
21
|
+
return safeJoin(getMonitorsHistoryDir(), name);
|
|
22
|
+
}
|
|
23
|
+
function getStatePath(name) {
|
|
24
|
+
return path.join(getMonitorHistoryDir(name), 'state.json');
|
|
25
|
+
}
|
|
26
|
+
/** Directory holding a monitor's fire history. */
|
|
27
|
+
export function getMonitorFiresDir(name) {
|
|
28
|
+
return path.join(getMonitorHistoryDir(name), 'fires');
|
|
29
|
+
}
|
|
30
|
+
/** Read a monitor's last-seen state, or null if it has never been observed. */
|
|
31
|
+
export function readState(name) {
|
|
32
|
+
const statePath = getStatePath(name);
|
|
33
|
+
if (!fs.existsSync(statePath))
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/** Persist a monitor's state atomically (temp file + rename, like writeRunMeta). */
|
|
43
|
+
export function writeStateRaw(state) {
|
|
44
|
+
ensureAgentsDir();
|
|
45
|
+
const dir = getMonitorHistoryDir(state.monitorName);
|
|
46
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
47
|
+
const statePath = path.join(dir, 'state.json');
|
|
48
|
+
const tmp = `${statePath}.tmp-${process.pid}`;
|
|
49
|
+
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf-8');
|
|
50
|
+
fs.renameSync(tmp, statePath);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Record a new observation as the monitor's last-seen state, preserving fire
|
|
54
|
+
* bookkeeping. Truncates the stored raw value so a firehose can't bloat disk.
|
|
55
|
+
*/
|
|
56
|
+
export function writeState(name, value, dedupeKey, extra = {}) {
|
|
57
|
+
const prev = readState(name);
|
|
58
|
+
const state = {
|
|
59
|
+
monitorName: name,
|
|
60
|
+
lastHash: hashSignature(value, dedupeKey),
|
|
61
|
+
lastValue: value.length > MAX_STORED_VALUE ? value.slice(0, MAX_STORED_VALUE) : value,
|
|
62
|
+
lastSeenAt: new Date().toISOString(),
|
|
63
|
+
...(prev?.lastFiredAt ? { lastFiredAt: prev.lastFiredAt } : {}),
|
|
64
|
+
...(prev?.fireTimes ? { fireTimes: prev.fireTimes } : {}),
|
|
65
|
+
...extra,
|
|
66
|
+
};
|
|
67
|
+
writeStateRaw(state);
|
|
68
|
+
return state;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The de-dupe signature for an observation. When `dedupeKey` is set, the
|
|
72
|
+
* signature is the first regex match of dedupeKey against the observation (so
|
|
73
|
+
* "the same event" is same matched token); otherwise it is the full observation.
|
|
74
|
+
* An unmatched dedupeKey falls back to the full observation.
|
|
75
|
+
*/
|
|
76
|
+
export function dedupeSignature(observation, dedupeKey) {
|
|
77
|
+
if (!dedupeKey)
|
|
78
|
+
return observation;
|
|
79
|
+
try {
|
|
80
|
+
const m = new RegExp(dedupeKey).exec(observation);
|
|
81
|
+
if (m)
|
|
82
|
+
return m[1] ?? m[0];
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
/* invalid regex — fall back to full observation */
|
|
86
|
+
}
|
|
87
|
+
return observation;
|
|
88
|
+
}
|
|
89
|
+
function hashSignature(observation, dedupeKey) {
|
|
90
|
+
return createHash('sha256').update(dedupeSignature(observation, dedupeKey)).digest('hex');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* True when `observation`'s de-dupe signature differs from the monitor's
|
|
94
|
+
* last-seen signature (or the monitor has never been observed). Pure read — the
|
|
95
|
+
* caller persists the new value via writeState only on a real fire.
|
|
96
|
+
*/
|
|
97
|
+
export function hasChanged(name, observation, dedupeKey) {
|
|
98
|
+
const prev = readState(name);
|
|
99
|
+
if (!prev)
|
|
100
|
+
return true;
|
|
101
|
+
return prev.lastHash !== hashSignature(observation, dedupeKey);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Append a fire timestamp and return the pruned window (fires within `windowMs`).
|
|
105
|
+
* The engine uses the returned length to decide whether the rate limit tripped.
|
|
106
|
+
*/
|
|
107
|
+
export function recordFireTime(name, now, windowMs) {
|
|
108
|
+
const prev = readState(name);
|
|
109
|
+
const times = [...(prev?.fireTimes ?? []), now].filter((t) => now - t <= windowMs);
|
|
110
|
+
return times;
|
|
111
|
+
}
|
|
112
|
+
/** Write a fire record to fires/<id>/event.json and return the fire id. */
|
|
113
|
+
export function writeFireRecord(event, meta = {}) {
|
|
114
|
+
ensureAgentsDir();
|
|
115
|
+
const fireId = event.firedAt.replace(/[:.]/g, '-');
|
|
116
|
+
const fireDir = safeJoin(getMonitorFiresDir(event.monitorName), fireId);
|
|
117
|
+
fs.mkdirSync(fireDir, { recursive: true });
|
|
118
|
+
fs.writeFileSync(path.join(fireDir, 'event.json'), JSON.stringify({ ...event, ...meta }, null, 2), 'utf-8');
|
|
119
|
+
return fireId;
|
|
120
|
+
}
|
|
121
|
+
/** List a monitor's fire history, chronologically ascending. */
|
|
122
|
+
export function listFires(name) {
|
|
123
|
+
const dir = getMonitorFiresDir(name);
|
|
124
|
+
if (!fs.existsSync(dir))
|
|
125
|
+
return [];
|
|
126
|
+
const ids = fs
|
|
127
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
128
|
+
.filter((e) => e.isDirectory())
|
|
129
|
+
.map((e) => e.name)
|
|
130
|
+
.sort();
|
|
131
|
+
const fires = [];
|
|
132
|
+
for (const id of ids) {
|
|
133
|
+
const eventPath = path.join(dir, id, 'event.json');
|
|
134
|
+
if (!fs.existsSync(eventPath))
|
|
135
|
+
continue;
|
|
136
|
+
try {
|
|
137
|
+
fires.push(JSON.parse(fs.readFileSync(eventPath, 'utf-8')));
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* skip corrupt record */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return fires;
|
|
144
|
+
}
|
package/dist/lib/picker.d.ts
CHANGED
|
@@ -52,3 +52,56 @@ export declare function itemPicker<T>(config: PickerConfig<T>): Promise<PickedIt
|
|
|
52
52
|
* row, so a quick single-pick still works.
|
|
53
53
|
*/
|
|
54
54
|
export declare function multiItemPicker<T>(config: MultiPickerConfig<T>): Promise<T[] | null>;
|
|
55
|
+
/** Configuration for the dynamic (async-refetch) picker prompt. */
|
|
56
|
+
export interface DynamicPickerConfig<T, F> {
|
|
57
|
+
message: string;
|
|
58
|
+
/** The initial filter state. Changing it (via a keybinding) re-runs {@link load}. */
|
|
59
|
+
initialFilter: F;
|
|
60
|
+
/** Async loader for the current filter state. Its result is the row pool. */
|
|
61
|
+
load: (filter: F) => Promise<T[]>;
|
|
62
|
+
labelFor: (item: T, query: string) => string;
|
|
63
|
+
/** Stable identity for an item (used for the active-row cursor across reloads). */
|
|
64
|
+
keyFor: (item: T) => string;
|
|
65
|
+
/** Client-side text filter over the loaded pool (the `S` search). */
|
|
66
|
+
matches?: (item: T, query: string) => boolean;
|
|
67
|
+
buildPreview?: (item: T) => string;
|
|
68
|
+
/** Dim summary of the current filter state, rendered in the header. */
|
|
69
|
+
headerFor?: (filter: F) => string;
|
|
70
|
+
/** The hotkey-legend help line; receives the mode so it can adapt. */
|
|
71
|
+
helpFor?: (filter: F, mode: 'nav' | 'search') => string;
|
|
72
|
+
/**
|
|
73
|
+
* Single-key bindings (by key name) that transform the filter. Returning the
|
|
74
|
+
* SAME reference is a no-op; a new object triggers a reload.
|
|
75
|
+
*/
|
|
76
|
+
keyBindings?: Record<string, (filter: F) => F>;
|
|
77
|
+
/**
|
|
78
|
+
* Side-effecting keys that don't change the filter (e.g. `y` copies a command).
|
|
79
|
+
* Receives the live search `query` so the effect can be search-aware. Return a
|
|
80
|
+
* short string to flash under the list.
|
|
81
|
+
*/
|
|
82
|
+
onKey?: (name: string, filter: F, active: T | undefined, query: string) => string | void;
|
|
83
|
+
/** Key that enters search mode (default `s`). */
|
|
84
|
+
searchKey?: string;
|
|
85
|
+
/** Key that toggles the preview pane (default `tab`). */
|
|
86
|
+
previewKey?: string;
|
|
87
|
+
pageSize?: number;
|
|
88
|
+
emptyMessage?: string;
|
|
89
|
+
loadingMessage?: string;
|
|
90
|
+
enterHint?: string;
|
|
91
|
+
}
|
|
92
|
+
/** The result returned when the user selects a row: the item plus the live filter. */
|
|
93
|
+
export interface DynamicPicked<T, F> {
|
|
94
|
+
item: T;
|
|
95
|
+
filter: F;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Async-refetch variant of {@link itemPicker}. Holds a `filter` object in state and
|
|
99
|
+
* re-runs `load(filter)` whenever a keybinding mutates it (with a loading placeholder
|
|
100
|
+
* while the fetch — e.g. an SSH fleet fan-out — is in flight). A separate `S` search
|
|
101
|
+
* mode filters the loaded pool client-side. `enter` returns the active row + the live
|
|
102
|
+
* filter; `esc` cancels (from search mode, `esc` first exits search).
|
|
103
|
+
*
|
|
104
|
+
* Same render/pagination/preview machinery as the static pickers — only the data
|
|
105
|
+
* source and keymap are dynamic.
|
|
106
|
+
*/
|
|
107
|
+
export declare function dynamicPicker<T, F>(config: DynamicPickerConfig<T, F>): Promise<DynamicPicked<T, F> | null>;
|
package/dist/lib/picker.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* display, and keyboard navigation. Used by sessions, teams, and other
|
|
13
13
|
* interactive pickers throughout the CLI.
|
|
14
14
|
*/
|
|
15
|
-
import { createPrompt, useState, useKeypress, useEffect, useMemo, usePagination, usePrefix, makeTheme, isEnterKey, isUpKey, isDownKey, isSpaceKey, Separator, } from '@inquirer/core';
|
|
15
|
+
import { createPrompt, useState, useKeypress, useEffect, useMemo, useRef, usePagination, usePrefix, makeTheme, isEnterKey, isUpKey, isDownKey, isSpaceKey, isBackspaceKey, Separator, } from '@inquirer/core';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import { stripVTControlCharacters } from 'node:util';
|
|
18
18
|
const DEFAULT_TERMINAL_ROWS = 24;
|
|
@@ -324,3 +324,216 @@ export function multiItemPicker(config) {
|
|
|
324
324
|
});
|
|
325
325
|
return prompt(config);
|
|
326
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Async-refetch variant of {@link itemPicker}. Holds a `filter` object in state and
|
|
329
|
+
* re-runs `load(filter)` whenever a keybinding mutates it (with a loading placeholder
|
|
330
|
+
* while the fetch — e.g. an SSH fleet fan-out — is in flight). A separate `S` search
|
|
331
|
+
* mode filters the loaded pool client-side. `enter` returns the active row + the live
|
|
332
|
+
* filter; `esc` cancels (from search mode, `esc` first exits search).
|
|
333
|
+
*
|
|
334
|
+
* Same render/pagination/preview machinery as the static pickers — only the data
|
|
335
|
+
* source and keymap are dynamic.
|
|
336
|
+
*/
|
|
337
|
+
export function dynamicPicker(config) {
|
|
338
|
+
const prompt = createPrompt((cfg, done) => {
|
|
339
|
+
const theme = makeTheme({});
|
|
340
|
+
const [status, setStatus] = useState('idle');
|
|
341
|
+
const [filter, setFilter] = useState(() => cfg.initialFilter);
|
|
342
|
+
const [items, setItems] = useState([]);
|
|
343
|
+
const [loading, setLoading] = useState(true);
|
|
344
|
+
const [query, setQuery] = useState('');
|
|
345
|
+
const [mode, setMode] = useState('nav');
|
|
346
|
+
const [previewOpen, setPreviewOpen] = useState(false);
|
|
347
|
+
const [active, setActive] = useState(0);
|
|
348
|
+
const [flash, setFlash] = useState('');
|
|
349
|
+
const prefix = usePrefix({ status, theme });
|
|
350
|
+
// Guards against a slow load resolving after a newer filter superseded it.
|
|
351
|
+
const gen = useRef(0);
|
|
352
|
+
useEffect(() => {
|
|
353
|
+
const my = ++gen.current;
|
|
354
|
+
setLoading(true);
|
|
355
|
+
Promise.resolve(cfg.load(filter))
|
|
356
|
+
.then((rows) => {
|
|
357
|
+
if (my !== gen.current)
|
|
358
|
+
return;
|
|
359
|
+
setItems(rows);
|
|
360
|
+
setLoading(false);
|
|
361
|
+
setActive(0);
|
|
362
|
+
})
|
|
363
|
+
.catch(() => {
|
|
364
|
+
if (my !== gen.current)
|
|
365
|
+
return;
|
|
366
|
+
setItems([]);
|
|
367
|
+
setLoading(false);
|
|
368
|
+
});
|
|
369
|
+
}, [filter]);
|
|
370
|
+
const results = useMemo(() => {
|
|
371
|
+
const q = query.trim();
|
|
372
|
+
const pool = q && cfg.matches ? items.filter((it) => cfg.matches(it, q)) : items;
|
|
373
|
+
return pool.slice(0, 200).map((item) => ({
|
|
374
|
+
value: item,
|
|
375
|
+
label: cfg.labelFor(item, q),
|
|
376
|
+
}));
|
|
377
|
+
}, [items, query]);
|
|
378
|
+
useEffect(() => {
|
|
379
|
+
if (active >= results.length)
|
|
380
|
+
setActive(0);
|
|
381
|
+
}, [results]);
|
|
382
|
+
const selected = results[active];
|
|
383
|
+
const finish = () => {
|
|
384
|
+
if (!selected)
|
|
385
|
+
return;
|
|
386
|
+
setStatus('done');
|
|
387
|
+
done({ item: selected.value, filter });
|
|
388
|
+
};
|
|
389
|
+
useKeypress((key, rl) => {
|
|
390
|
+
if (isEnterKey(key)) {
|
|
391
|
+
finish();
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Search mode: we own the query buffer (readline's line doesn't survive
|
|
395
|
+
// across renders here, so we build it from key events). Clear readline
|
|
396
|
+
// every keystroke so nothing leaks, then append the typed character.
|
|
397
|
+
if (mode === 'search') {
|
|
398
|
+
if (key.name === 'escape') {
|
|
399
|
+
// Exit search but KEEP the query as an active filter, so hotkeys (and
|
|
400
|
+
// the y copy-cmd) operate on the searched view. A second esc in nav
|
|
401
|
+
// clears it. Enter also confirms the highlighted row directly.
|
|
402
|
+
rl.clearLine(0);
|
|
403
|
+
setMode('nav');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
if (isUpKey(key)) {
|
|
407
|
+
rl.clearLine(0);
|
|
408
|
+
if (results.length > 0)
|
|
409
|
+
setActive((active - 1 + results.length) % results.length);
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
if (isDownKey(key)) {
|
|
413
|
+
rl.clearLine(0);
|
|
414
|
+
if (results.length > 0)
|
|
415
|
+
setActive((active + 1) % results.length);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (isBackspaceKey(key)) {
|
|
419
|
+
rl.clearLine(0);
|
|
420
|
+
setQuery(query.slice(0, -1));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const seq = key.sequence;
|
|
424
|
+
rl.clearLine(0);
|
|
425
|
+
if (seq && seq.length === 1 && seq >= ' ' && !key.ctrl) {
|
|
426
|
+
setQuery(query + seq);
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
// Nav mode: single keys are hotkeys — clear the readline buffer so a hotkey
|
|
431
|
+
// letter (r/b/c/…) never accumulates as stray input.
|
|
432
|
+
rl.clearLine(0);
|
|
433
|
+
if (key.name === 'escape') {
|
|
434
|
+
// First esc clears an active search filter; a second (no filter) cancels.
|
|
435
|
+
if (query) {
|
|
436
|
+
setQuery('');
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
done(null);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
if (isUpKey(key)) {
|
|
443
|
+
if (results.length > 0)
|
|
444
|
+
setActive((active - 1 + results.length) % results.length);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
if (isDownKey(key)) {
|
|
448
|
+
if (results.length > 0)
|
|
449
|
+
setActive((active + 1) % results.length);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (flash)
|
|
453
|
+
setFlash('');
|
|
454
|
+
if (key.name === (cfg.searchKey ?? 's')) {
|
|
455
|
+
setMode('search');
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
if (cfg.buildPreview && key.name === (cfg.previewKey ?? 'tab')) {
|
|
459
|
+
setPreviewOpen(!previewOpen);
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
const binding = cfg.keyBindings?.[key.name ?? ''];
|
|
463
|
+
if (binding) {
|
|
464
|
+
const next = binding(filter);
|
|
465
|
+
if (!Object.is(next, filter))
|
|
466
|
+
setFilter(next);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if (cfg.onKey) {
|
|
470
|
+
const msg = cfg.onKey(key.name ?? '', filter, selected?.value, query);
|
|
471
|
+
if (msg)
|
|
472
|
+
setFlash(msg);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
const message = theme.style.message(cfg.message, status);
|
|
476
|
+
if (status === 'done') {
|
|
477
|
+
return `${prefix} ${message}`;
|
|
478
|
+
}
|
|
479
|
+
const headerBits = [prefix, message];
|
|
480
|
+
if (cfg.headerFor)
|
|
481
|
+
headerBits.push(chalk.gray(cfg.headerFor(filter)));
|
|
482
|
+
if (mode === 'search') {
|
|
483
|
+
headerBits.push(query ? chalk.cyan('/' + query) : chalk.gray('/ (type to filter)'));
|
|
484
|
+
}
|
|
485
|
+
else if (query) {
|
|
486
|
+
headerBits.push(chalk.cyan('/' + query));
|
|
487
|
+
}
|
|
488
|
+
const header = headerBits.filter(Boolean).join(' ');
|
|
489
|
+
const page = usePagination({
|
|
490
|
+
items: results,
|
|
491
|
+
active,
|
|
492
|
+
renderItem({ item, isActive }) {
|
|
493
|
+
if (Separator.isSeparator(item))
|
|
494
|
+
return ` ${item.separator}`;
|
|
495
|
+
const cursor = isActive ? chalk.cyan('>') : ' ';
|
|
496
|
+
const row = isActive ? chalk.bold(item.label) : item.label;
|
|
497
|
+
return `${cursor} ${row}`;
|
|
498
|
+
},
|
|
499
|
+
pageSize: cfg.pageSize ?? 12,
|
|
500
|
+
loop: false,
|
|
501
|
+
});
|
|
502
|
+
const help = chalk.gray(cfg.helpFor
|
|
503
|
+
? cfg.helpFor(filter, mode)
|
|
504
|
+
: mode === 'search'
|
|
505
|
+
? '↑↓ navigate · esc exit search · ⏎ ' + (cfg.enterHint ?? 'select')
|
|
506
|
+
: 's search · ↑↓ navigate · ⏎ ' + (cfg.enterHint ?? 'select') + ' · esc cancel');
|
|
507
|
+
const parts = [header];
|
|
508
|
+
if (loading) {
|
|
509
|
+
parts.push(chalk.gray(` ${cfg.loadingMessage ?? 'Loading…'}`));
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
parts.push(page);
|
|
513
|
+
if (results.length === 0) {
|
|
514
|
+
parts.push(chalk.gray(` ${cfg.emptyMessage ?? 'No matches.'}`));
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
if (previewOpen && selected && cfg.buildPreview && !loading) {
|
|
518
|
+
const width = terminalWidth();
|
|
519
|
+
const separator = chalk.gray('─'.repeat(Math.min(width, 80)));
|
|
520
|
+
const flashRows = flash ? renderedRows(flash, width) : 0;
|
|
521
|
+
const fixedRows = renderedRows(header, width) +
|
|
522
|
+
renderedRows(parts.slice(1).join('\n'), width) +
|
|
523
|
+
renderedRows(separator, width) +
|
|
524
|
+
renderedRows(help, width) +
|
|
525
|
+
flashRows;
|
|
526
|
+
const availablePreviewRows = terminalRows() - fixedRows;
|
|
527
|
+
const preview = limitPreviewHeight(cfg.buildPreview(selected.value), availablePreviewRows, width);
|
|
528
|
+
if (preview) {
|
|
529
|
+
parts.push(separator);
|
|
530
|
+
parts.push(preview);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
if (flash)
|
|
534
|
+
parts.push(chalk.green(flash));
|
|
535
|
+
parts.push(help);
|
|
536
|
+
return [header, parts.slice(1).join('\n')];
|
|
537
|
+
});
|
|
538
|
+
return prompt(config);
|
|
539
|
+
}
|
|
@@ -66,3 +66,19 @@ export declare function quoteWin32ExecArg(arg: string): string;
|
|
|
66
66
|
* line is byte-identical to the old unquoted join for the common case.
|
|
67
67
|
*/
|
|
68
68
|
export declare function composeWin32CommandLine(command: string, args: string[]): string;
|
|
69
|
+
/**
|
|
70
|
+
* DEP0190-safe execFile/spawn spec when the binary may need `shell: true` on Windows.
|
|
71
|
+
*
|
|
72
|
+
* When `needsWindowsShell(bin)` is true, compose ONE fully-quoted command line via
|
|
73
|
+
* `composeWin32CommandLine` and return an EMPTY args array so Node never concatenates
|
|
74
|
+
* user-controlled argv (MCP command/args, prompts, …) into the cmd.exe line unescaped.
|
|
75
|
+
* Off that path the original bin+args are returned unchanged.
|
|
76
|
+
*
|
|
77
|
+
* Call as: `const s = execFileShellSpec(bin, args); execFile(s.command, s.args, { shell: s.shell })`.
|
|
78
|
+
* Pure; optional `platform` override keeps the win32 branch unit-testable on any host.
|
|
79
|
+
*/
|
|
80
|
+
export declare function execFileShellSpec(bin: string, args: string[], platform?: NodeJS.Platform): {
|
|
81
|
+
command: string;
|
|
82
|
+
args: string[];
|
|
83
|
+
shell: boolean;
|
|
84
|
+
};
|
|
@@ -119,3 +119,20 @@ export function quoteWin32ExecArg(arg) {
|
|
|
119
119
|
export function composeWin32CommandLine(command, args) {
|
|
120
120
|
return [command, ...args].map(quoteWin32ExecArg).join(' ');
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* DEP0190-safe execFile/spawn spec when the binary may need `shell: true` on Windows.
|
|
124
|
+
*
|
|
125
|
+
* When `needsWindowsShell(bin)` is true, compose ONE fully-quoted command line via
|
|
126
|
+
* `composeWin32CommandLine` and return an EMPTY args array so Node never concatenates
|
|
127
|
+
* user-controlled argv (MCP command/args, prompts, …) into the cmd.exe line unescaped.
|
|
128
|
+
* Off that path the original bin+args are returned unchanged.
|
|
129
|
+
*
|
|
130
|
+
* Call as: `const s = execFileShellSpec(bin, args); execFile(s.command, s.args, { shell: s.shell })`.
|
|
131
|
+
* Pure; optional `platform` override keeps the win32 branch unit-testable on any host.
|
|
132
|
+
*/
|
|
133
|
+
export function execFileShellSpec(bin, args, platform = process.platform) {
|
|
134
|
+
if (!needsWindowsShell(bin, platform)) {
|
|
135
|
+
return { command: bin, args, shell: false };
|
|
136
|
+
}
|
|
137
|
+
return { command: composeWin32CommandLine(bin, args), args: [], shell: true };
|
|
138
|
+
}
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -316,7 +316,37 @@ export declare function getUpstreamManifestVersion(info: PluginSourceInfo): stri
|
|
|
316
316
|
* Update an installed plugin by re-pulling from its original source.
|
|
317
317
|
* Returns true if the update succeeded.
|
|
318
318
|
*/
|
|
319
|
-
|
|
319
|
+
/**
|
|
320
|
+
* Labels of exec surfaces present in `after` that were NOT present in `before`.
|
|
321
|
+
* Used by updatePlugin to distinguish a newly-appearing execution surface
|
|
322
|
+
* (upstream compromise → renewed consent required) from one the user already
|
|
323
|
+
* trusted (leave enablement alone).
|
|
324
|
+
*/
|
|
325
|
+
export declare function newExecSurfaceLabels(before: PluginCapabilities, after: PluginCapabilities): string[];
|
|
326
|
+
/**
|
|
327
|
+
* Re-fetch a plugin from its recorded source and apply the update to disk.
|
|
328
|
+
*
|
|
329
|
+
* Security (RUSH-1757): a plugin's upstream is mutable. `updatePlugin` never
|
|
330
|
+
* mutates the live plugin tree before it has inspected the incoming content —
|
|
331
|
+
* the new revision is fetched into a **quarantine** dir first, its capabilities
|
|
332
|
+
* are diffed against the current on-disk baseline, and the update is applied to
|
|
333
|
+
* `plugin.root` only after the trust decision. If the update introduces a NEW
|
|
334
|
+
* executable surface (hooks/, .mcp.json, bin/, scripts/, settings.json,
|
|
335
|
+
* permissions/) that the current revision did not carry, the update is refused
|
|
336
|
+
* unless `options.allowExecSurfaces` is set — the last-good content is kept in
|
|
337
|
+
* place, so a benign-then-compromised upstream can never execute on the next
|
|
338
|
+
* update without renewed consent. A surface the user already trusted is not a
|
|
339
|
+
* "new" surface and does not re-trigger the gate.
|
|
340
|
+
*/
|
|
341
|
+
export declare function updatePlugin(name: string, options?: {
|
|
342
|
+
allowExecSurfaces?: boolean;
|
|
343
|
+
}): Promise<{
|
|
320
344
|
success: boolean;
|
|
321
345
|
error?: string;
|
|
346
|
+
/** True when the update was refused because it introduced new exec surfaces. */
|
|
347
|
+
blockedByExecSurfaces?: boolean;
|
|
348
|
+
/** Labels of exec surfaces newly introduced by this update, if any. */
|
|
349
|
+
newExecSurfaces?: string[];
|
|
350
|
+
/** Whether the applied revision carries executable surfaces (for the caller's re-sync). */
|
|
351
|
+
hasExecSurfaces?: boolean;
|
|
322
352
|
}>;
|