@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,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
+ }
@@ -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>;
@@ -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
+ }
@@ -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
- export declare function updatePlugin(name: string): Promise<{
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
  }>;