@phnx-labs/agents-cli 1.20.34 → 1.20.36

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 (291) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/LICENSE +185 -21
  3. package/README.md +13 -5
  4. package/dist/commands/audit.d.ts +14 -0
  5. package/dist/commands/audit.js +68 -0
  6. package/dist/commands/browser.js +82 -8
  7. package/dist/commands/check.d.ts +15 -0
  8. package/dist/commands/check.js +84 -0
  9. package/dist/commands/cloud.js +143 -3
  10. package/dist/commands/computer-actions.d.ts +15 -0
  11. package/dist/commands/computer-actions.js +120 -22
  12. package/dist/commands/computer.js +81 -0
  13. package/dist/commands/cost.js +17 -12
  14. package/dist/commands/daemon.js +4 -1
  15. package/dist/commands/doctor.d.ts +1 -0
  16. package/dist/commands/doctor.js +54 -97
  17. package/dist/commands/events.d.ts +16 -0
  18. package/dist/commands/events.js +173 -0
  19. package/dist/commands/exec.d.ts +19 -0
  20. package/dist/commands/exec.js +283 -12
  21. package/dist/commands/hosts.js +37 -29
  22. package/dist/commands/inspect.d.ts +2 -0
  23. package/dist/commands/inspect.js +62 -16
  24. package/dist/commands/lock.d.ts +12 -0
  25. package/dist/commands/lock.js +70 -0
  26. package/dist/commands/logs.d.ts +17 -0
  27. package/dist/commands/logs.js +139 -0
  28. package/dist/commands/message.d.ts +15 -0
  29. package/dist/commands/message.js +56 -0
  30. package/dist/commands/models.d.ts +3 -0
  31. package/dist/commands/models.js +27 -8
  32. package/dist/commands/permissions.js +9 -2
  33. package/dist/commands/repo.d.ts +34 -0
  34. package/dist/commands/repo.js +243 -65
  35. package/dist/commands/resource-view.d.ts +20 -0
  36. package/dist/commands/resource-view.js +90 -28
  37. package/dist/commands/routines.js +101 -5
  38. package/dist/commands/secrets-migrate.js +106 -57
  39. package/dist/commands/secrets.d.ts +31 -18
  40. package/dist/commands/secrets.js +196 -91
  41. package/dist/commands/serve.d.ts +10 -0
  42. package/dist/commands/serve.js +37 -0
  43. package/dist/commands/sessions-inject.d.ts +14 -0
  44. package/dist/commands/sessions-inject.js +111 -0
  45. package/dist/commands/sessions-picker.d.ts +2 -0
  46. package/dist/commands/sessions-picker.js +24 -3
  47. package/dist/commands/sessions-resume.d.ts +2 -0
  48. package/dist/commands/sessions-resume.js +221 -0
  49. package/dist/commands/sessions-tail.d.ts +10 -0
  50. package/dist/commands/sessions-tail.js +11 -0
  51. package/dist/commands/sessions.d.ts +143 -1
  52. package/dist/commands/sessions.js +628 -80
  53. package/dist/commands/setup.js +4 -2
  54. package/dist/commands/sync.d.ts +3 -1
  55. package/dist/commands/sync.js +156 -4
  56. package/dist/commands/teams.js +217 -0
  57. package/dist/commands/versions.js +2 -4
  58. package/dist/commands/view.d.ts +3 -0
  59. package/dist/commands/view.js +19 -8
  60. package/dist/commands/wallet.d.ts +6 -0
  61. package/dist/commands/wallet.js +22 -5
  62. package/dist/commands/watchdog.d.ts +18 -0
  63. package/dist/commands/watchdog.js +238 -0
  64. package/dist/index.js +82 -32
  65. package/dist/lib/agents.js +30 -9
  66. package/dist/lib/audit/log.d.ts +92 -0
  67. package/dist/lib/audit/log.js +177 -0
  68. package/dist/lib/auto-pull.js +2 -1
  69. package/dist/lib/browser/cdp.js +5 -1
  70. package/dist/lib/browser/chrome.d.ts +10 -0
  71. package/dist/lib/browser/chrome.js +18 -7
  72. package/dist/lib/browser/drivers/ssh.d.ts +8 -0
  73. package/dist/lib/browser/drivers/ssh.js +77 -16
  74. package/dist/lib/browser/har.d.ts +84 -0
  75. package/dist/lib/browser/har.js +77 -0
  76. package/dist/lib/browser/ipc.d.ts +3 -0
  77. package/dist/lib/browser/ipc.js +37 -12
  78. package/dist/lib/browser/profiles.d.ts +1 -1
  79. package/dist/lib/browser/profiles.js +8 -10
  80. package/dist/lib/browser/refs.d.ts +65 -0
  81. package/dist/lib/browser/refs.js +73 -1
  82. package/dist/lib/browser/runtime-state.js +1 -0
  83. package/dist/lib/browser/service.d.ts +38 -2
  84. package/dist/lib/browser/service.js +183 -23
  85. package/dist/lib/browser/types.d.ts +14 -1
  86. package/dist/lib/budget/live-cloud.d.ts +42 -0
  87. package/dist/lib/budget/live-cloud.js +79 -0
  88. package/dist/lib/budget/live-team.d.ts +31 -0
  89. package/dist/lib/budget/live-team.js +115 -0
  90. package/dist/lib/cloud/codex.js +4 -0
  91. package/dist/lib/cloud/rush.d.ts +12 -1
  92. package/dist/lib/cloud/rush.js +13 -3
  93. package/dist/lib/cloud/types.d.ts +9 -0
  94. package/dist/lib/computer/dispatch.d.ts +8 -0
  95. package/dist/lib/computer/dispatch.js +125 -0
  96. package/dist/lib/computer/loop.d.ts +62 -0
  97. package/dist/lib/computer/loop.js +98 -0
  98. package/dist/lib/computer/model.d.ts +44 -0
  99. package/dist/lib/computer/model.js +157 -0
  100. package/dist/lib/concurrency.d.ts +19 -0
  101. package/dist/lib/concurrency.js +33 -0
  102. package/dist/lib/daemon.d.ts +92 -0
  103. package/dist/lib/daemon.js +222 -18
  104. package/dist/lib/devices/connect.d.ts +3 -2
  105. package/dist/lib/devices/connect.js +5 -3
  106. package/dist/lib/devices/registry.d.ts +7 -0
  107. package/dist/lib/devices/registry.js +24 -0
  108. package/dist/lib/devices/tailscale.js +1 -1
  109. package/dist/lib/drift.d.ts +52 -0
  110. package/dist/lib/drift.js +112 -0
  111. package/dist/lib/events.d.ts +10 -3
  112. package/dist/lib/events.js +101 -24
  113. package/dist/lib/exec.d.ts +17 -0
  114. package/dist/lib/exec.js +100 -12
  115. package/dist/lib/git.d.ts +27 -0
  116. package/dist/lib/git.js +56 -1
  117. package/dist/lib/hooks/cache.d.ts +6 -0
  118. package/dist/lib/hooks/cache.js +54 -12
  119. package/dist/lib/hooks.d.ts +27 -0
  120. package/dist/lib/hooks.js +135 -10
  121. package/dist/lib/hosts/dispatch.d.ts +15 -0
  122. package/dist/lib/hosts/dispatch.js +45 -13
  123. package/dist/lib/hosts/logs.d.ts +16 -0
  124. package/dist/lib/hosts/logs.js +74 -0
  125. package/dist/lib/hosts/option.js +1 -1
  126. package/dist/lib/hosts/passthrough.js +3 -1
  127. package/dist/lib/hosts/progress.d.ts +66 -0
  128. package/dist/lib/hosts/progress.js +125 -17
  129. package/dist/lib/hosts/ready.d.ts +51 -7
  130. package/dist/lib/hosts/ready.js +98 -25
  131. package/dist/lib/hosts/reconcile.d.ts +53 -0
  132. package/dist/lib/hosts/reconcile.js +81 -0
  133. package/dist/lib/hosts/registry.d.ts +19 -2
  134. package/dist/lib/hosts/registry.js +58 -2
  135. package/dist/lib/hosts/remote-cmd.d.ts +62 -1
  136. package/dist/lib/hosts/remote-cmd.js +70 -1
  137. package/dist/lib/hosts/remote-os.d.ts +17 -0
  138. package/dist/lib/hosts/remote-os.js +30 -0
  139. package/dist/lib/hosts/session-index.d.ts +34 -0
  140. package/dist/lib/hosts/session-index.js +56 -0
  141. package/dist/lib/hosts/tasks.d.ts +22 -0
  142. package/dist/lib/hosts/tasks.js +29 -0
  143. package/dist/lib/lock.d.ts +93 -0
  144. package/dist/lib/lock.js +207 -0
  145. package/dist/lib/loop.js +16 -1
  146. package/dist/lib/machine-id.d.ts +21 -0
  147. package/dist/lib/machine-id.js +26 -0
  148. package/dist/lib/mailbox-target.d.ts +36 -0
  149. package/dist/lib/mailbox-target.js +45 -0
  150. package/dist/lib/mailbox.d.ts +47 -0
  151. package/dist/lib/mailbox.js +194 -0
  152. package/dist/lib/mcp.d.ts +5 -0
  153. package/dist/lib/mcp.js +24 -8
  154. package/dist/lib/migrate.d.ts +19 -0
  155. package/dist/lib/migrate.js +134 -26
  156. package/dist/lib/overdue.js +3 -0
  157. package/dist/lib/permissions.d.ts +4 -0
  158. package/dist/lib/permissions.js +35 -0
  159. package/dist/lib/picker.d.ts +26 -0
  160. package/dist/lib/picker.js +127 -0
  161. package/dist/lib/platform/exec.d.ts +46 -0
  162. package/dist/lib/platform/exec.js +74 -0
  163. package/dist/lib/platform/process.d.ts +31 -0
  164. package/dist/lib/platform/process.js +34 -1
  165. package/dist/lib/platform/winpath.js +2 -0
  166. package/dist/lib/plugin-marketplace.d.ts +30 -0
  167. package/dist/lib/plugin-marketplace.js +215 -2
  168. package/dist/lib/plugins.d.ts +5 -0
  169. package/dist/lib/plugins.js +61 -10
  170. package/dist/lib/profiles.d.ts +25 -0
  171. package/dist/lib/profiles.js +22 -6
  172. package/dist/lib/pty-client.js +2 -1
  173. package/dist/lib/rotate.d.ts +61 -0
  174. package/dist/lib/rotate.js +52 -0
  175. package/dist/lib/routines.d.ts +40 -2
  176. package/dist/lib/routines.js +66 -8
  177. package/dist/lib/runner.d.ts +11 -2
  178. package/dist/lib/runner.js +49 -7
  179. package/dist/lib/scheduler.js +6 -1
  180. package/dist/lib/secrets/bundles.d.ts +60 -4
  181. package/dist/lib/secrets/bundles.js +131 -12
  182. package/dist/lib/secrets/filestore.d.ts +3 -0
  183. package/dist/lib/secrets/filestore.js +42 -16
  184. package/dist/lib/secrets/index.d.ts +43 -2
  185. package/dist/lib/secrets/index.js +102 -3
  186. package/dist/lib/secrets/mcp.d.ts +93 -0
  187. package/dist/lib/secrets/mcp.js +205 -0
  188. package/dist/lib/secrets/remote.js +12 -5
  189. package/dist/lib/secrets/sync.js +83 -4
  190. package/dist/lib/secrets/windows.js +14 -3
  191. package/dist/lib/serve/data.d.ts +81 -0
  192. package/dist/lib/serve/data.js +91 -0
  193. package/dist/lib/serve/page.d.ts +7 -0
  194. package/dist/lib/serve/page.js +140 -0
  195. package/dist/lib/serve/server.d.ts +46 -0
  196. package/dist/lib/serve/server.js +115 -0
  197. package/dist/lib/session/active.d.ts +61 -0
  198. package/dist/lib/session/active.js +196 -17
  199. package/dist/lib/session/discover.d.ts +37 -0
  200. package/dist/lib/session/discover.js +204 -27
  201. package/dist/lib/session/inject.d.ts +18 -0
  202. package/dist/lib/session/inject.js +21 -0
  203. package/dist/lib/session/parse.d.ts +6 -0
  204. package/dist/lib/session/parse.js +286 -2
  205. package/dist/lib/session/pid-registry.d.ts +22 -0
  206. package/dist/lib/session/pid-registry.js +104 -0
  207. package/dist/lib/session/provenance.d.ts +14 -2
  208. package/dist/lib/session/provenance.js +39 -8
  209. package/dist/lib/session/remote-active.d.ts +26 -0
  210. package/dist/lib/session/remote-active.js +153 -0
  211. package/dist/lib/session/remote-list.d.ts +51 -0
  212. package/dist/lib/session/remote-list.js +213 -0
  213. package/dist/lib/session/remote.d.ts +7 -1
  214. package/dist/lib/session/remote.js +18 -8
  215. package/dist/lib/session/render.js +1 -1
  216. package/dist/lib/session/sync/config.d.ts +1 -7
  217. package/dist/lib/session/sync/config.js +4 -11
  218. package/dist/lib/session/types.d.ts +18 -1
  219. package/dist/lib/session/types.js +1 -1
  220. package/dist/lib/shims.d.ts +44 -6
  221. package/dist/lib/shims.js +143 -57
  222. package/dist/lib/ssh-exec.d.ts +26 -3
  223. package/dist/lib/ssh-exec.js +47 -3
  224. package/dist/lib/ssh-tunnel.d.ts +24 -5
  225. package/dist/lib/ssh-tunnel.js +62 -63
  226. package/dist/lib/staleness/writers/hooks.js +1 -1
  227. package/dist/lib/startup/command-registry.d.ts +8 -0
  228. package/dist/lib/startup/command-registry.js +17 -1
  229. package/dist/lib/state.d.ts +13 -0
  230. package/dist/lib/state.js +103 -9
  231. package/dist/lib/sync-umbrella.d.ts +14 -7
  232. package/dist/lib/sync-umbrella.js +17 -9
  233. package/dist/lib/teams/forEach.d.ts +110 -0
  234. package/dist/lib/teams/forEach.js +186 -0
  235. package/dist/lib/teams/index.d.ts +1 -0
  236. package/dist/lib/teams/index.js +1 -0
  237. package/dist/lib/teams/pr-watch.d.ts +226 -0
  238. package/dist/lib/teams/pr-watch.js +371 -0
  239. package/dist/lib/teams/registry.js +25 -9
  240. package/dist/lib/teams/supervisor.d.ts +14 -1
  241. package/dist/lib/teams/supervisor.js +19 -0
  242. package/dist/lib/teams/worktree.d.ts +9 -0
  243. package/dist/lib/teams/worktree.js +32 -0
  244. package/dist/lib/terminal/backends/ghostty.d.ts +6 -0
  245. package/dist/lib/terminal/backends/ghostty.js +69 -0
  246. package/dist/lib/terminal/backends/index.d.ts +19 -0
  247. package/dist/lib/terminal/backends/index.js +31 -0
  248. package/dist/lib/terminal/backends/iterm.d.ts +6 -0
  249. package/dist/lib/terminal/backends/iterm.js +62 -0
  250. package/dist/lib/terminal/backends/tmux.d.ts +14 -0
  251. package/dist/lib/terminal/backends/tmux.js +23 -0
  252. package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
  253. package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
  254. package/dist/lib/terminal/engine.d.ts +39 -0
  255. package/dist/lib/terminal/engine.js +54 -0
  256. package/dist/lib/terminal/index.d.ts +17 -0
  257. package/dist/lib/terminal/index.js +8 -0
  258. package/dist/lib/terminal/inject.d.ts +204 -0
  259. package/dist/lib/terminal/inject.js +247 -0
  260. package/dist/lib/terminal/policy.d.ts +11 -0
  261. package/dist/lib/terminal/policy.js +11 -0
  262. package/dist/lib/terminal/quote.d.ts +11 -0
  263. package/dist/lib/terminal/quote.js +13 -0
  264. package/dist/lib/terminal/resolve.d.ts +64 -0
  265. package/dist/lib/terminal/resolve.js +90 -0
  266. package/dist/lib/terminal/shell.d.ts +6 -0
  267. package/dist/lib/terminal/shell.js +23 -0
  268. package/dist/lib/terminal/transport.d.ts +15 -0
  269. package/dist/lib/terminal/transport.js +40 -0
  270. package/dist/lib/terminal/types.d.ts +59 -0
  271. package/dist/lib/terminal/types.js +13 -0
  272. package/dist/lib/triggers/webhook.d.ts +85 -0
  273. package/dist/lib/triggers/webhook.js +141 -0
  274. package/dist/lib/types.d.ts +7 -0
  275. package/dist/lib/versions.d.ts +30 -3
  276. package/dist/lib/versions.js +129 -18
  277. package/dist/lib/watchdog/index.d.ts +3 -0
  278. package/dist/lib/watchdog/index.js +5 -0
  279. package/dist/lib/watchdog/read.d.ts +35 -0
  280. package/dist/lib/watchdog/read.js +149 -0
  281. package/dist/lib/watchdog/runner.d.ts +127 -0
  282. package/dist/lib/watchdog/runner.js +322 -0
  283. package/dist/lib/watchdog/watchdog.d.ts +40 -0
  284. package/dist/lib/watchdog/watchdog.js +166 -0
  285. package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
  286. package/dist/lib/watchdog/watchdogTail.js +154 -0
  287. package/dist/lib/whats-new.d.ts +9 -0
  288. package/dist/lib/whats-new.js +35 -0
  289. package/dist/lib/workflows.d.ts +166 -0
  290. package/dist/lib/workflows.js +193 -0
  291. package/package.json +5 -4
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Agent mailbox — a file-spool that lets a message reach an already-running
3
+ * agent mid-flight. One box per logical agent, keyed by a unique-per-launch id
4
+ * (session UUID / teams agentId / loop runId), so boxes are disjoint and a
5
+ * message can never reach the wrong agent (see docs / plan velvety-conjuring-popcorn).
6
+ *
7
+ * Layout: <root>/<mailboxId>/{inbox,processing,consumed}/<msgId>.json
8
+ * inbox/ pending, written by `agents message`
9
+ * processing/ claimed by a drain (claim-first = crash-safe)
10
+ * consumed/ archived after delivery (and dropped mismatches)
11
+ *
12
+ * A box has a SINGLE consumer — the owning agent, whose tool calls (and thus
13
+ * hook-driven drains) are sequential. Writers may be concurrent; each enqueue
14
+ * is atomic (temp-write + rename), so a drain never observes a partial file.
15
+ * Delivery is at-least-once: an interrupted drain leaves the message in
16
+ * processing/, and the next drain recovers it. Consumers dedup by `msgId`.
17
+ */
18
+ import * as fs from 'fs';
19
+ import * as path from 'path';
20
+ import { randomUUID } from 'crypto';
21
+ import { getMailboxRootDir } from './state.js';
22
+ /**
23
+ * A mailboxId must be a single, separator-free path segment. Real ids (session
24
+ * UUID / teams agentId / loop runId) already satisfy this; rejecting anything
25
+ * else fails loud instead of silently misrouting — a message whose id contained
26
+ * a `/` would nest under a different dir than the `to` stamp it is matched
27
+ * against, and would be dropped with no error. Also blocks `.`/`..` traversal
28
+ * once `agents message` starts accepting external target ids.
29
+ */
30
+ export function isValidMailboxId(mailboxId) {
31
+ return /^[A-Za-z0-9._-]+$/.test(mailboxId) && mailboxId !== '.' && mailboxId !== '..';
32
+ }
33
+ export function assertValidMailboxId(mailboxId) {
34
+ if (!isValidMailboxId(mailboxId)) {
35
+ throw new Error(`Invalid mailboxId ${JSON.stringify(mailboxId)}: must be a single path segment ` +
36
+ `matching [A-Za-z0-9._-] (no separators, not '.'/'..').`);
37
+ }
38
+ }
39
+ /** Absolute path to a box. `root` override is for tests. */
40
+ export function mailboxDir(mailboxId, root = getMailboxRootDir()) {
41
+ assertValidMailboxId(mailboxId);
42
+ return path.join(root, mailboxId);
43
+ }
44
+ function inboxDir(boxDir) { return path.join(boxDir, 'inbox'); }
45
+ function processingDir(boxDir) { return path.join(boxDir, 'processing'); }
46
+ function consumedDir(boxDir) { return path.join(boxDir, 'consumed'); }
47
+ /** Create the three sub-buckets. Idempotent. */
48
+ function ensureDirs(boxDir) {
49
+ fs.mkdirSync(inboxDir(boxDir), { recursive: true });
50
+ fs.mkdirSync(processingDir(boxDir), { recursive: true });
51
+ fs.mkdirSync(consumedDir(boxDir), { recursive: true });
52
+ }
53
+ let seq = 0;
54
+ /**
55
+ * `<epochMs>-<seq>-<rand>` — sorts by filename in FIFO order. The per-process
56
+ * monotonic `seq` breaks ties within the same millisecond (so a single writer's
57
+ * order is preserved); `rand` keeps it unique across processes/hosts.
58
+ */
59
+ function newMsgId() {
60
+ const s = String(seq++).padStart(6, '0');
61
+ return `${Date.now()}-${s}-${randomUUID().slice(0, 8)}`;
62
+ }
63
+ /**
64
+ * Enqueue a message into `boxDir` atomically. Returns the msgId. The `to` field
65
+ * is stamped so a drain can refuse a message that lands in the wrong box.
66
+ */
67
+ export function enqueue(boxDir, msg) {
68
+ assertValidMailboxId(msg.to);
69
+ ensureDirs(boxDir);
70
+ const msgId = newMsgId();
71
+ const record = {
72
+ msgId,
73
+ to: msg.to,
74
+ from: msg.from,
75
+ ts: new Date().toISOString(),
76
+ text: msg.text,
77
+ };
78
+ const target = path.join(inboxDir(boxDir), `${msgId}.json`);
79
+ const tmp = `${target}.${process.pid}.tmp`;
80
+ fs.writeFileSync(tmp, JSON.stringify(record, null, 2), 'utf-8');
81
+ fs.renameSync(tmp, target); // atomic on a single filesystem
82
+ return msgId;
83
+ }
84
+ /** Parse a message file. Returns null on missing/corrupt/invalid-shape. */
85
+ function readMessage(file) {
86
+ let raw;
87
+ try {
88
+ raw = fs.readFileSync(file, 'utf-8');
89
+ }
90
+ catch {
91
+ return null;
92
+ }
93
+ let parsed;
94
+ try {
95
+ parsed = JSON.parse(raw);
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ const m = parsed;
101
+ if (typeof m?.msgId !== 'string' || typeof m?.to !== 'string' || typeof m?.text !== 'string') {
102
+ return null;
103
+ }
104
+ return { msgId: m.msgId, to: m.to, from: m.from, ts: m.ts ?? '', text: m.text };
105
+ }
106
+ function jsonFiles(dir) {
107
+ let names;
108
+ try {
109
+ names = fs.readdirSync(dir);
110
+ }
111
+ catch {
112
+ return [];
113
+ }
114
+ return names.filter((n) => n.endsWith('.json')).sort();
115
+ }
116
+ /**
117
+ * Consume a file already sitting in `processing/`: read, verify it is addressed
118
+ * to this box, then archive to `consumed/`. Returns the message iff valid AND
119
+ * addressed here; a mismatched/corrupt file is archived (dropped) so it never
120
+ * loops. Returns null when the file vanished (a racing drain took it).
121
+ */
122
+ function consumeClaimed(boxDir, name, expectedTo) {
123
+ const src = path.join(processingDir(boxDir), name);
124
+ const dest = path.join(consumedDir(boxDir), name);
125
+ const msg = readMessage(src);
126
+ try {
127
+ fs.renameSync(src, dest);
128
+ }
129
+ catch {
130
+ return null; // already archived/claimed elsewhere
131
+ }
132
+ if (!msg || msg.to !== expectedTo)
133
+ return null; // dropped (corrupt or wrong box)
134
+ return msg;
135
+ }
136
+ /**
137
+ * Drain the box: return every pending message addressed to it, in FIFO order,
138
+ * removing them from the queue. Claim-first (inbox → processing → consumed) so
139
+ * an interrupted drain is recovered on the next call (at-least-once). Corrupt
140
+ * files and messages addressed to a different box are dropped, not returned.
141
+ *
142
+ * `boxId` defaults to the box's directory name — the id it was created under.
143
+ */
144
+ export function drain(boxDir, boxId = path.basename(boxDir)) {
145
+ ensureDirs(boxDir);
146
+ const out = [];
147
+ // 1. Recover orphans left in processing/ by a prior interrupted drain.
148
+ for (const name of jsonFiles(processingDir(boxDir))) {
149
+ const msg = consumeClaimed(boxDir, name, boxId);
150
+ if (msg)
151
+ out.push(msg);
152
+ }
153
+ // 2. Claim and consume pending inbox messages.
154
+ for (const name of jsonFiles(inboxDir(boxDir))) {
155
+ const from = path.join(inboxDir(boxDir), name);
156
+ const to = path.join(processingDir(boxDir), name);
157
+ try {
158
+ fs.renameSync(from, to); // atomic claim
159
+ }
160
+ catch {
161
+ continue; // vanished — a racing drain took it
162
+ }
163
+ const msg = consumeClaimed(boxDir, name, boxId);
164
+ if (msg)
165
+ out.push(msg);
166
+ }
167
+ return out;
168
+ }
169
+ /** Read pending messages (inbox + in-flight) without consuming them. FIFO. */
170
+ export function peek(boxDir, boxId = path.basename(boxDir)) {
171
+ const out = [];
172
+ for (const dir of [processingDir(boxDir), inboxDir(boxDir)]) {
173
+ for (const name of jsonFiles(dir)) {
174
+ const msg = readMessage(path.join(dir, name));
175
+ if (msg && msg.to === boxId)
176
+ out.push(msg);
177
+ }
178
+ }
179
+ return out;
180
+ }
181
+ /** Delete pending (not-yet-claimed) inbox messages. Returns the count removed. */
182
+ export function clear(boxDir) {
183
+ let n = 0;
184
+ for (const name of jsonFiles(inboxDir(boxDir))) {
185
+ try {
186
+ fs.unlinkSync(path.join(inboxDir(boxDir), name));
187
+ n++;
188
+ }
189
+ catch {
190
+ // already gone — ignore
191
+ }
192
+ }
193
+ return n;
194
+ }
package/dist/lib/mcp.d.ts CHANGED
@@ -39,6 +39,11 @@ export interface McpTargetOperationResult {
39
39
  * Parse an MCP server config from a YAML file.
40
40
  */
41
41
  export declare function parseMcpServerConfig(filePath: string): McpYamlConfig | null;
42
+ /**
43
+ * Validate an MCP server name. Rejects names that could be misinterpreted as
44
+ * command-line options or that contain characters unsafe for argv/identifier use.
45
+ */
46
+ export declare function validateMcpServerName(name: string): void;
42
47
  /**
43
48
  * List all MCP server configs from ~/.agents/mcp/.
44
49
  */
package/dist/lib/mcp.js CHANGED
@@ -34,6 +34,18 @@ export function parseMcpServerConfig(filePath) {
34
34
  }
35
35
  return validateMcpYamlConfig(parsed);
36
36
  }
37
+ /**
38
+ * Validate an MCP server name. Rejects names that could be misinterpreted as
39
+ * command-line options or that contain characters unsafe for argv/identifier use.
40
+ */
41
+ export function validateMcpServerName(name) {
42
+ if (name.startsWith('-')) {
43
+ throw new Error(`Invalid MCP server name '${name}': names cannot start with '-'`);
44
+ }
45
+ if (/[\s\0-\x1f\x7f]/.test(name)) {
46
+ throw new Error(`Invalid MCP server name '${name}': names cannot contain whitespace or control characters`);
47
+ }
48
+ }
37
49
  function validateMcpYamlConfig(parsed) {
38
50
  if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
39
51
  return null;
@@ -41,6 +53,7 @@ function validateMcpYamlConfig(parsed) {
41
53
  const config = parsed;
42
54
  if (typeof config.name !== 'string' || config.name.length === 0)
43
55
  return null;
56
+ validateMcpServerName(config.name);
44
57
  if (config.transport !== 'stdio' && config.transport !== 'http')
45
58
  return null;
46
59
  const result = {
@@ -205,12 +218,12 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
205
218
  envArgs.push('--env', `${key}=${value}`);
206
219
  }
207
220
  }
208
- // claude mcp add --scope user --transport stdio <name> [--env K=V]... -- <cmd> [args...]
221
+ // claude mcp add --scope user --transport stdio [--env K=V]... -- <name> <cmd> [args...]
209
222
  const args = [
210
223
  'mcp', 'add', '--scope', 'user', '--transport', 'stdio',
211
- server.name,
212
224
  ...envArgs,
213
225
  '--',
226
+ server.name,
214
227
  server.config.command,
215
228
  ...(server.config.args || [])
216
229
  ];
@@ -222,8 +235,8 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
222
235
  });
223
236
  }
224
237
  else {
225
- // claude mcp add --scope user --transport http <name> <url>
226
- execFileSync(binaryPath, ['mcp', 'add', '--scope', 'user', '--transport', 'http', server.name, server.config.url], {
238
+ // claude mcp add --scope user --transport http -- <name> <url>
239
+ execFileSync(binaryPath, ['mcp', 'add', '--scope', 'user', '--transport', 'http', '--', server.name, server.config.url], {
227
240
  stdio: 'pipe',
228
241
  timeout: 30000,
229
242
  env: execEnv,
@@ -237,10 +250,11 @@ function installMcpViaClaude(binaryPath, server, versionHome) {
237
250
  */
238
251
  function installMcpViaCodex(binaryPath, server, versionHome) {
239
252
  if (server.config.transport === 'stdio') {
240
- // codex mcp add <name> -- <cmd> [args...]
253
+ // codex mcp add -- <name> <cmd> [args...]
241
254
  const args = [
242
- 'mcp', 'add', server.name,
255
+ 'mcp', 'add',
243
256
  '--',
257
+ server.name,
244
258
  server.config.command,
245
259
  ...(server.config.args || [])
246
260
  ];
@@ -272,11 +286,12 @@ export async function registerMcpCommandToTargets(targets, name, commandSpec, sc
272
286
  }
273
287
  function registerMcpCommand(agentId, name, commandSpec, scope, transport, options = {}) {
274
288
  try {
289
+ validateMcpServerName(name);
275
290
  const bin = options.binary || AGENTS[agentId].cliCommand;
276
291
  const commandArgs = [commandSpec.command, ...commandSpec.args];
277
292
  const args = agentId === 'claude'
278
- ? ['mcp', 'add', '--transport', transport, '--scope', scope, name, '--', ...commandArgs]
279
- : ['mcp', 'add', name, '--', ...commandArgs];
293
+ ? ['mcp', 'add', '--transport', transport, '--scope', scope, '--', name, ...commandArgs]
294
+ : ['mcp', 'add', '--', name, ...commandArgs];
280
295
  const env = options.home ? { ...process.env, HOME: options.home } : process.env;
281
296
  execFileSync(bin, args, { stdio: 'pipe', timeout: 30000, env, shell: needsWindowsShell(bin) });
282
297
  return { success: true };
@@ -508,6 +523,7 @@ export function installMcpServers(agentId, version, versionHome, mcpNames, optio
508
523
  * Write an MCP server config to ~/.agents/mcp/.
509
524
  */
510
525
  export function writeMcpServerConfig(config) {
526
+ validateMcpServerName(config.name);
511
527
  const mcpDir = getUserMcpDir();
512
528
  fs.mkdirSync(mcpDir, { recursive: true });
513
529
  const fileName = `${config.name.toLowerCase().replace(/[^a-z0-9]/g, '-')}.yaml`;
@@ -25,6 +25,25 @@
25
25
  * LEGACY_SYSTEM_DIR" without duplicating data.
26
26
  */
27
27
  export declare function foldLegacySystemRepo(): void;
28
+ /**
29
+ * Migrate a legacy single-repo agents.yaml into ~/.agents/agents.yaml.
30
+ *
31
+ * The post-fold system dir (~/.agents/.system) is a pull-only mirror of the
32
+ * npm-shipped defaults. Its agents.yaml is a TRACKED file that readMeta() reads
33
+ * in place as the defaults base (see SYSTEM_META_FILE in state.ts). Deleting or
34
+ * moving a tracked file out of the mirror dirties its working tree, which
35
+ * permanently wedges `agents setup`, `agents sync`, and background auto-pull
36
+ * ("Working tree has uncommitted changes.") because the mirror sync refuses a
37
+ * dirty tree. So when agents.yaml is tracked there, treat the mirror as strictly
38
+ * read-only and leave the file alone.
39
+ *
40
+ * Only an UNTRACKED agents.yaml is residue from the pre-split single-repo layout
41
+ * (or a legacy ~/.agents-system fold) and is safe to move/drop.
42
+ *
43
+ * Params default to the real on-disk locations; they are injectable so tests can
44
+ * drive a fixture tree without touching the user's ~/.agents.
45
+ */
46
+ export declare function migrateAgentsYaml(systemDir?: string, userDir?: string): void;
28
47
  /**
29
48
  * Repair self-referential agent binary symlinks.
30
49
  *
@@ -8,6 +8,9 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as os from 'os';
10
10
  import * as yaml from 'yaml';
11
+ import { execSync } from 'child_process';
12
+ import { atomicWriteFileSync } from './fs-atomic.js';
13
+ import { machineId } from './machine-id.js';
11
14
  import { AGENTS, agentConfigDirName, findInPath } from './agents.js';
12
15
  import { createLink } from './platform/index.js';
13
16
  const HOME = process.env.HOME ?? os.homedir();
@@ -79,21 +82,46 @@ export function foldLegacySystemRepo() {
79
82
  }
80
83
  catch { /* best-effort */ }
81
84
  }
85
+ /** True when `relPath` is a file tracked by a git repo rooted at `repoDir`. */
86
+ function isTrackedInGitRepo(repoDir, relPath) {
87
+ try {
88
+ execSync(`git ls-files --error-unmatch -- ${relPath}`, { cwd: repoDir, stdio: 'ignore' });
89
+ return true;
90
+ }
91
+ catch {
92
+ // Not a git repo, or the file is untracked — either way, not tracked.
93
+ return false;
94
+ }
95
+ }
82
96
  /**
83
- * Move ~/.agents-system/agents.yaml -> ~/.agents/agents.yaml.
84
- * No-op if user file already exists or system file absent.
85
- * (This is also handled inline in state.ts readMeta, but is exposed here
86
- * for explicit migration calls from postinstall / CLI entry points.)
97
+ * Migrate a legacy single-repo agents.yaml into ~/.agents/agents.yaml.
98
+ *
99
+ * The post-fold system dir (~/.agents/.system) is a pull-only mirror of the
100
+ * npm-shipped defaults. Its agents.yaml is a TRACKED file that readMeta() reads
101
+ * in place as the defaults base (see SYSTEM_META_FILE in state.ts). Deleting or
102
+ * moving a tracked file out of the mirror dirties its working tree, which
103
+ * permanently wedges `agents setup`, `agents sync`, and background auto-pull
104
+ * ("Working tree has uncommitted changes.") because the mirror sync refuses a
105
+ * dirty tree. So when agents.yaml is tracked there, treat the mirror as strictly
106
+ * read-only and leave the file alone.
107
+ *
108
+ * Only an UNTRACKED agents.yaml is residue from the pre-split single-repo layout
109
+ * (or a legacy ~/.agents-system fold) and is safe to move/drop.
110
+ *
111
+ * Params default to the real on-disk locations; they are injectable so tests can
112
+ * drive a fixture tree without touching the user's ~/.agents.
87
113
  */
88
- function migrateAgentsYaml() {
89
- const src = path.join(SYSTEM_DIR, 'agents.yaml');
90
- const dest = path.join(USER_DIR, 'agents.yaml');
114
+ export function migrateAgentsYaml(systemDir = SYSTEM_DIR, userDir = USER_DIR) {
115
+ const src = path.join(systemDir, 'agents.yaml');
116
+ const dest = path.join(userDir, 'agents.yaml');
91
117
  if (!fs.existsSync(src))
92
118
  return;
119
+ // Tracked in the system mirror → npm-shipped defaults; never mutate. readMeta()
120
+ // already surfaces it in place, so no user-dir copy is needed either.
121
+ if (isTrackedInGitRepo(systemDir, 'agents.yaml'))
122
+ return;
93
123
  if (fs.existsSync(dest)) {
94
- // User copy is authoritative — drop the stale system leftover. The system
95
- // repo (npm-shipped) does not track agents.yaml; any copy here is residue
96
- // from the pre-split layout.
124
+ // User copy is authoritative — drop the untracked stale system leftover.
97
125
  try {
98
126
  fs.unlinkSync(src);
99
127
  }
@@ -101,7 +129,7 @@ function migrateAgentsYaml() {
101
129
  return;
102
130
  }
103
131
  try {
104
- fs.mkdirSync(USER_DIR, { recursive: true, mode: 0o700 });
132
+ fs.mkdirSync(userDir, { recursive: true, mode: 0o700 });
105
133
  fs.renameSync(src, dest);
106
134
  console.error('Migrated agents.yaml to ~/.agents/');
107
135
  }
@@ -656,22 +684,31 @@ function migratePermissionSetsToPresets() {
656
684
  * switching is owned by `agents use`, not the migrator.
657
685
  */
658
686
  function repairAgentConfigSymlinks() {
659
- let yaml;
660
- try {
661
- yaml = fs.readFileSync(path.join(USER_DIR, 'agents.yaml'), 'utf-8');
662
- }
663
- catch {
664
- return;
665
- }
666
- const agentsBlock = yaml.match(/^agents:\s*\n((?: [^\n]*\n)+)/m);
667
- if (!agentsBlock)
668
- return;
687
+ // Version pins live in the per-device file post-split (~/.agents/devices/<machine>/
688
+ // agents.yaml); central agents.yaml may still carry them mid-transition. Read
689
+ // device first (authoritative), then central, first-seen-agent wins.
669
690
  const defaults = [];
670
- for (const line of agentsBlock[1].split('\n')) {
671
- const m = line.match(/^\s+([a-z][a-z0-9_-]*):\s*([^\s#]+)/);
672
- if (m)
673
- defaults.push({ agent: m[1], version: m[2] });
674
- }
691
+ const collectPins = (file) => {
692
+ let text;
693
+ try {
694
+ text = fs.readFileSync(file, 'utf-8');
695
+ }
696
+ catch {
697
+ return;
698
+ }
699
+ const block = text.match(/^agents:\s*\n((?: [^\n]*\n)+)/m);
700
+ if (!block)
701
+ return;
702
+ for (const line of block[1].split('\n')) {
703
+ const m = line.match(/^\s+([a-z][a-z0-9_-]*):\s*([^\s#]+)/);
704
+ if (m && !defaults.some((d) => d.agent === m[1]))
705
+ defaults.push({ agent: m[1], version: m[2] });
706
+ }
707
+ };
708
+ collectPins(path.join(USER_DIR, 'devices', machineId(), 'agents.yaml'));
709
+ collectPins(path.join(USER_DIR, 'agents.yaml'));
710
+ if (defaults.length === 0)
711
+ return;
675
712
  let repaired = 0;
676
713
  for (const { agent, version } of defaults) {
677
714
  const configDirName = agent in AGENTS ? agentConfigDirName(agent) : `.${agent}`;
@@ -1625,6 +1662,73 @@ function migrateVersionResourcesToPatterns() {
1625
1662
  console.error('Migrated agents.yaml versions: entries to pattern format');
1626
1663
  }
1627
1664
  }
1665
+ /**
1666
+ * Split the machine-local fields out of the committed central agents.yaml so it
1667
+ * becomes portable and syncs cleanly (no more skip-worktree band-aid):
1668
+ * agents: -> ~/.agents/devices/<machineId>/agents.yaml (committed, per-device)
1669
+ * versions: -> ~/.agents/.history/version-resources.json (gitignored, machine-local)
1670
+ * Then clear any externally-set skip-worktree bit. Idempotent: no-op once central
1671
+ * carries neither field. Operates on raw YAML (never through state.ts).
1672
+ */
1673
+ function migrateSplitDeviceLocalMeta() {
1674
+ const metaFile = path.join(USER_DIR, 'agents.yaml');
1675
+ if (!fs.existsSync(metaFile))
1676
+ return;
1677
+ let meta;
1678
+ try {
1679
+ meta = yaml.parse(fs.readFileSync(metaFile, 'utf-8')) || {};
1680
+ }
1681
+ catch {
1682
+ return;
1683
+ }
1684
+ const agents = meta.agents;
1685
+ const versions = meta.versions;
1686
+ const hasLocal = (!!agents && Object.keys(agents).length > 0) || (!!versions && Object.keys(versions).length > 0);
1687
+ const HEADER = '# agents-cli metadata\n# Auto-generated - do not edit manually\n# https://github.com/phnx-labs/agents-cli\n\n';
1688
+ // Only rewrite central when it actually carries machine-local fields — a
1689
+ // machine whose agents.yaml is already portable-only is left untouched.
1690
+ if (hasLocal) {
1691
+ // agents: -> per-device file (merge, existing device entries win).
1692
+ if (agents && Object.keys(agents).length > 0) {
1693
+ const devicePath = path.join(USER_DIR, 'devices', machineId(), 'agents.yaml');
1694
+ let existing = {};
1695
+ try {
1696
+ const dm = yaml.parse(fs.readFileSync(devicePath, 'utf-8'));
1697
+ if (dm?.agents)
1698
+ existing = dm.agents;
1699
+ }
1700
+ catch { /* absent */ }
1701
+ fs.mkdirSync(path.dirname(devicePath), { recursive: true });
1702
+ atomicWriteFileSync(devicePath, HEADER + yaml.stringify({ agents: { ...agents, ...existing } }));
1703
+ }
1704
+ // versions: -> machine-local history JSON (merge, existing wins).
1705
+ if (versions && Object.keys(versions).length > 0) {
1706
+ const vrPath = path.join(USER_DIR, '.history', 'version-resources.json');
1707
+ let existing = {};
1708
+ try {
1709
+ existing = JSON.parse(fs.readFileSync(vrPath, 'utf-8')) || {};
1710
+ }
1711
+ catch { /* absent */ }
1712
+ fs.mkdirSync(path.dirname(vrPath), { recursive: true });
1713
+ atomicWriteFileSync(vrPath, JSON.stringify({ ...versions, ...existing }, null, 2) + '\n');
1714
+ }
1715
+ // Strip machine-local fields from central and rewrite (portable only) — after
1716
+ // the device/history writes above, so a crash never loses data.
1717
+ delete meta.agents;
1718
+ delete meta.versions;
1719
+ atomicWriteFileSync(metaFile, HEADER + yaml.stringify(meta));
1720
+ }
1721
+ // Always clear any skip-worktree bit (idempotent, best-effort) so agents.yaml
1722
+ // syncs cleanly on every machine — even one that had nothing to split. Runs
1723
+ // after any rewrite so the tracked content already matches the split shape.
1724
+ try {
1725
+ execSync('git update-index --no-skip-worktree agents.yaml', { cwd: USER_DIR, stdio: 'ignore' });
1726
+ }
1727
+ catch { /* not a git repo / bit not set */ }
1728
+ if (hasLocal) {
1729
+ console.error('Split agents.yaml: agents: -> devices/, versions: -> .history/version-resources.json');
1730
+ }
1731
+ }
1628
1732
  /**
1629
1733
  * Rename the legacy `extras-extras/` plugin-marketplace dir to `agents-extras/`
1630
1734
  * inside every installed agent version-home, and rewrite cross-references in
@@ -1805,6 +1909,10 @@ export async function runMigration() {
1805
1909
  foldUserHooksYamlIntoAgentsYaml();
1806
1910
  foldBrowserProfilesIntoAgentsYaml();
1807
1911
  migrateVersionResourcesToPatterns();
1912
+ // Split machine-local fields (agents:/versions:) out of the committed central
1913
+ // agents.yaml. After migrateVersionResourcesToPatterns so versions: is already
1914
+ // in pattern form when it moves to the history file.
1915
+ migrateSplitDeviceLocalMeta();
1808
1916
  // Bucket moves: collapse runtime state into ~/.agents/.history and ~/.agents/.cache.
1809
1917
  migrateRuntimeToHistory();
1810
1918
  migrateRuntimeToCache();
@@ -44,6 +44,9 @@ export function detectOverdueJobs(now = new Date()) {
44
44
  for (const job of listJobs()) {
45
45
  if (!job.enabled || job.runOnce)
46
46
  continue;
47
+ // Trigger-only jobs (no cron schedule) never have an expected fire time.
48
+ if (!job.schedule)
49
+ continue;
47
50
  let expected = null;
48
51
  try {
49
52
  const cronOptions = { paused: true };
@@ -2,6 +2,10 @@ import type { AgentId, PermissionSet, InstalledPermission, ClaudePermissions, Op
2
2
  /** Filename used for Codex Starlark deny-rules generated from permission groups. */
3
3
  export declare const CODEX_RULES_FILENAME = "agents-deny.rules";
4
4
  export type ParsedRules = PermissionSet;
5
+ export declare const COMPUTER_PERMISSION_RULE_PREFIX = "Computer";
6
+ export declare const COMPUTER_APP_GATED_VERBS: readonly ["screenshot", "describe", "get-text", "launch", "raise", "click", "right-click", "type", "type-text", "key", "drag", "scroll", "ax-action", "focus", "wait"];
7
+ export declare const COMPUTER_INPUT_GATED_VERBS: readonly ["raise", "click", "right-click", "type", "type-text", "key", "drag", "scroll", "ax-action", "focus"];
8
+ export declare function formatComputerPermissionGrantHint(bundleId?: string): string;
5
9
  export declare function containsBroadGrants(rules: ParsedRules): {
6
10
  broad: string[];
7
11
  reason: string;
@@ -25,6 +25,41 @@ const HOME = os.homedir();
25
25
  // the writer in `applyPermissionsToVersion` handles the format dispatch.)
26
26
  /** Filename used for Codex Starlark deny-rules generated from permission groups. */
27
27
  export const CODEX_RULES_FILENAME = 'agents-deny.rules';
28
+ export const COMPUTER_PERMISSION_RULE_PREFIX = 'Computer';
29
+ export const COMPUTER_APP_GATED_VERBS = [
30
+ 'screenshot',
31
+ 'describe',
32
+ 'get-text',
33
+ 'launch',
34
+ 'raise',
35
+ 'click',
36
+ 'right-click',
37
+ 'type',
38
+ 'type-text',
39
+ 'key',
40
+ 'drag',
41
+ 'scroll',
42
+ 'ax-action',
43
+ 'focus',
44
+ 'wait',
45
+ ];
46
+ export const COMPUTER_INPUT_GATED_VERBS = [
47
+ 'raise',
48
+ 'click',
49
+ 'right-click',
50
+ 'type',
51
+ 'type-text',
52
+ 'key',
53
+ 'drag',
54
+ 'scroll',
55
+ 'ax-action',
56
+ 'focus',
57
+ ];
58
+ export function formatComputerPermissionGrantHint(bundleId) {
59
+ const target = bundleId && bundleId.length > 0 ? bundleId : '<bundle-id>';
60
+ return `add Computer(${target}) to a permissions group, then \`agents computer reload\`\n` +
61
+ `app-targeted computer verbs are gated by Computer(<bundle-id>): ${COMPUTER_APP_GATED_VERBS.join(', ')}`;
62
+ }
28
63
  export function containsBroadGrants(rules) {
29
64
  const broad = [];
30
65
  const reasons = new Set();
@@ -8,6 +8,8 @@
8
8
  /** Configuration for the interactive picker prompt. */
9
9
  export interface PickerConfig<T> {
10
10
  message: string;
11
+ /** Optional dim hint line rendered directly under the header (above the rows). */
12
+ subtitle?: string;
11
13
  items: T[];
12
14
  filter: (query: string) => T[];
13
15
  labelFor: (item: T, query: string) => string;
@@ -22,7 +24,31 @@ export interface PickerConfig<T> {
22
24
  export interface PickedItem<T> {
23
25
  item: T;
24
26
  }
27
+ /** Configuration for the multi-select picker prompt. */
28
+ export interface MultiPickerConfig<T> {
29
+ message: string;
30
+ items: T[];
31
+ filter: (query: string) => T[];
32
+ labelFor: (item: T, query: string) => string;
33
+ /** Stable identity for an item — drives the selected set. */
34
+ keyFor: (item: T) => string;
35
+ buildPreview?: (item: T) => string;
36
+ pageSize?: number;
37
+ initialSearch?: string;
38
+ emptyMessage?: string;
39
+ enterHint?: string;
40
+ }
25
41
  /** Clip a picker preview so the full prompt can fit in the terminal viewport. */
26
42
  export declare function limitPreviewHeight(preview: string, maxRows: number, width: number): string;
27
43
  /** Show an interactive fuzzy-filter picker and return the selected item, or null on cancel. */
28
44
  export declare function itemPicker<T>(config: PickerConfig<T>): Promise<PickedItem<T> | null>;
45
+ /**
46
+ * Multi-select variant of {@link itemPicker}. Same searchable, paginated list
47
+ * and preview pane, but `space` toggles a checkbox on the active row instead of
48
+ * the preview (preview moves to `tab`), and `enter` confirms every checked row.
49
+ *
50
+ * Returns the selected items (in the config's `items` order) or `null` on
51
+ * cancel. Pressing `enter` with nothing checked confirms just the highlighted
52
+ * row, so a quick single-pick still works.
53
+ */
54
+ export declare function multiItemPicker<T>(config: MultiPickerConfig<T>): Promise<T[] | null>;