@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
@@ -174,6 +174,133 @@ export function itemPicker(config) {
174
174
  const help = previewOpen
175
175
  ? chalk.gray(`↑↓ navigate · space: close preview · ⏎ ${enter} · esc: cancel`)
176
176
  : chalk.gray(`↑↓ navigate${hasPreview ? ' · space: preview' : ''} · ⏎ ${enter} · esc: cancel`);
177
+ const parts = [header];
178
+ if (cfg.subtitle)
179
+ parts.push(cfg.subtitle);
180
+ parts.push(page);
181
+ if (results.length === 0) {
182
+ parts.push(chalk.gray(` ${cfg.emptyMessage ?? 'No matches.'}`));
183
+ }
184
+ if (previewOpen && selected && cfg.buildPreview) {
185
+ const width = terminalWidth();
186
+ const separator = chalk.gray('─'.repeat(Math.min(width, 80)));
187
+ const fixedRows = renderedRows(header, width) +
188
+ renderedRows(parts.slice(1).join('\n'), width) +
189
+ renderedRows(separator, width) +
190
+ renderedRows(help, width);
191
+ const availablePreviewRows = terminalRows() - fixedRows;
192
+ const preview = limitPreviewHeight(cfg.buildPreview(selected.value), availablePreviewRows, width);
193
+ if (preview) {
194
+ parts.push(separator);
195
+ parts.push(preview);
196
+ }
197
+ }
198
+ parts.push(help);
199
+ return [header, parts.slice(1).join('\n')];
200
+ });
201
+ return prompt(config);
202
+ }
203
+ /**
204
+ * Multi-select variant of {@link itemPicker}. Same searchable, paginated list
205
+ * and preview pane, but `space` toggles a checkbox on the active row instead of
206
+ * the preview (preview moves to `tab`), and `enter` confirms every checked row.
207
+ *
208
+ * Returns the selected items (in the config's `items` order) or `null` on
209
+ * cancel. Pressing `enter` with nothing checked confirms just the highlighted
210
+ * row, so a quick single-pick still works.
211
+ */
212
+ export function multiItemPicker(config) {
213
+ const prompt = createPrompt((cfg, done) => {
214
+ const theme = makeTheme({});
215
+ const [status, setStatus] = useState('idle');
216
+ const [searchTerm, setSearchTerm] = useState(cfg.initialSearch ?? '');
217
+ const [previewOpen, setPreviewOpen] = useState(false);
218
+ const [selectedKeys, setSelectedKeys] = useState(new Set());
219
+ const [active, setActive] = useState(0);
220
+ const prefix = usePrefix({ status, theme });
221
+ const results = useMemo(() => {
222
+ const filtered = cfg.filter(searchTerm).slice(0, 200);
223
+ return filtered.map((item) => ({
224
+ value: item,
225
+ label: cfg.labelFor(item, searchTerm),
226
+ }));
227
+ }, [searchTerm]);
228
+ useEffect(() => {
229
+ if (active >= results.length)
230
+ setActive(0);
231
+ }, [results]);
232
+ const selected = results[active];
233
+ // Selected items resolved in the original list order for deterministic fan-out.
234
+ const collectSelected = () => cfg.items.filter((it) => selectedKeys.has(cfg.keyFor(it)));
235
+ useKeypress((key, rl) => {
236
+ if (isEnterKey(key)) {
237
+ const chosen = selectedKeys.size > 0 ? collectSelected() : selected ? [selected.value] : [];
238
+ if (chosen.length === 0)
239
+ return;
240
+ setStatus('done');
241
+ done(chosen);
242
+ return;
243
+ }
244
+ // space toggles the active row's checkbox; strip the space from the buffer
245
+ // so it never leaks into the filter.
246
+ if (isSpaceKey(key)) {
247
+ rl.clearLine(0);
248
+ if (selected) {
249
+ const k = cfg.keyFor(selected.value);
250
+ const next = new Set(selectedKeys);
251
+ if (next.has(k))
252
+ next.delete(k);
253
+ else
254
+ next.add(k);
255
+ setSelectedKeys(next);
256
+ }
257
+ return;
258
+ }
259
+ if (key.name === 'tab' && cfg.buildPreview) {
260
+ rl.clearLine(0);
261
+ setPreviewOpen(!previewOpen);
262
+ return;
263
+ }
264
+ if (isUpKey(key)) {
265
+ rl.clearLine(0);
266
+ if (results.length > 0)
267
+ setActive((active - 1 + results.length) % results.length);
268
+ return;
269
+ }
270
+ if (isDownKey(key)) {
271
+ rl.clearLine(0);
272
+ if (results.length > 0)
273
+ setActive((active + 1) % results.length);
274
+ return;
275
+ }
276
+ setSearchTerm(rl.line);
277
+ });
278
+ const message = theme.style.message(cfg.message, status);
279
+ const count = selectedKeys.size;
280
+ if (status === 'done') {
281
+ return `${prefix} ${message} ${chalk.cyan(`${count || 1} session${(count || 1) === 1 ? '' : 's'}`)}`;
282
+ }
283
+ const placeholder = '(type to filter · space to toggle · enter to resume)';
284
+ const searchStr = searchTerm ? chalk.cyan(searchTerm) : chalk.gray(placeholder);
285
+ const header = [prefix, message, searchStr].filter(Boolean).join(' ');
286
+ const page = usePagination({
287
+ items: results,
288
+ active,
289
+ renderItem({ item, isActive }) {
290
+ if (Separator.isSeparator(item))
291
+ return ` ${item.separator}`;
292
+ const checked = selectedKeys.has(cfg.keyFor(item.value));
293
+ const box = checked ? chalk.green('[x]') : chalk.gray('[ ]');
294
+ const cursor = isActive ? chalk.cyan('>') : ' ';
295
+ const row = isActive ? chalk.bold(item.label) : item.label;
296
+ return `${cursor} ${box} ${row}`;
297
+ },
298
+ pageSize: cfg.pageSize ?? 10,
299
+ loop: false,
300
+ });
301
+ const enter = cfg.enterHint ?? 'resume';
302
+ const countStr = count > 0 ? chalk.green(`${count} selected`) : chalk.gray('0 selected');
303
+ const help = chalk.gray(`${countStr}${chalk.gray(' · ↑↓ navigate · space toggle')}${cfg.buildPreview ? chalk.gray(' · tab preview') : ''}${chalk.gray(` · ⏎ ${enter} · esc cancel`)}`);
177
304
  const parts = [header, page];
178
305
  if (results.length === 0) {
179
306
  parts.push(chalk.gray(` ${cfg.emptyMessage ?? 'No matches.'}`));
@@ -17,3 +17,49 @@ export declare function needsWindowsShell(binary: string, platform?: NodeJS.Plat
17
17
  * shell would actually run, matching `which` semantics on POSIX.
18
18
  */
19
19
  export declare function findExecutable(name: string, platform?: NodeJS.Platform): string | null;
20
+ /**
21
+ * Absolute path of the POSIX shell for `sh -c <command string>` composition.
22
+ * `/bin/sh` on POSIX; on Windows there is no /bin/sh, so resolve Git's
23
+ * `sh.exe`/`bash.exe` from PATH (present wherever Git for Windows is — dev
24
+ * boxes and the GitHub windows runners alike). Falls back to the bare name so
25
+ * a missing shell surfaces as a clear spawn ENOENT on `sh` rather than a
26
+ * misleading one on `/bin/sh`.
27
+ */
28
+ export declare function posixShellPath(platform?: NodeJS.Platform): string;
29
+ /**
30
+ * Quote one argument for a Windows `cmd.exe` command line, as built by Node's
31
+ * `spawn(..., { shell: true })` on win32 (the `.cmd` agent shims, `agents secrets
32
+ * exec`, ...). cmd.exe does NO quoting of its own, so an unquoted arg with a space
33
+ * is split into several args, and a cmd metacharacter (`&|<>()^`) would be
34
+ * interpreted by the shell. We wrap any arg with whitespace, a quote, or a
35
+ * metacharacter in double quotes and escape embedded quotes / trailing
36
+ * backslashes per the CommandLineToArgvW rules, so the *child's* argv parse
37
+ * reconstructs the original argument.
38
+ *
39
+ * CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
40
+ * BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
41
+ * "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
42
+ * the callers here run a command whose `%`/`!`-bearing tokens are the caller's
43
+ * own (an agent prompt against the caller's shell, a bundle the caller owns), so
44
+ * caller-controlled `%`/`!` is not a privilege boundary. If that ever changes
45
+ * (composing an untrusted command line), route through a shell that disables
46
+ * expansion rather than relying on this quoter. An empty arg becomes `""`.
47
+ */
48
+ export declare function quoteWin32ExecArg(arg: string): string;
49
+ /**
50
+ * Compose a DEP0190-safe Windows shell command line from a command and its args.
51
+ *
52
+ * Node's `spawn(cmd, args, { shell: true })` on win32 concatenates `cmd` and
53
+ * every element of `args` into a single cmd.exe line WITHOUT escaping — that is
54
+ * both Node's DEP0190 deprecation (a future hard error) and a real injection
55
+ * surface, since user-controlled text (an agent prompt, a secrets-exec command)
56
+ * flows through `args`. Passing the fully-composed line as the SOLE `command`
57
+ * with an EMPTY args array sidesteps both: we quote every token with
58
+ * `quoteWin32ExecArg` so the child's CommandLineToArgvW parse reconstructs the
59
+ * exact original argv, and Node has no args array left to concatenate.
60
+ *
61
+ * Callers spawn the result as `spawn(line, [], { shell: true })`. A simple arg
62
+ * (no whitespace/quote/metachar) is passed through untouched, so the composed
63
+ * line is byte-identical to the old unquoted join for the common case.
64
+ */
65
+ export declare function composeWin32CommandLine(command: string, args: string[]): string;
@@ -39,3 +39,77 @@ export function findExecutable(name, platform = process.platform) {
39
39
  return null;
40
40
  }
41
41
  }
42
+ /**
43
+ * Absolute path of the POSIX shell for `sh -c <command string>` composition.
44
+ * `/bin/sh` on POSIX; on Windows there is no /bin/sh, so resolve Git's
45
+ * `sh.exe`/`bash.exe` from PATH (present wherever Git for Windows is — dev
46
+ * boxes and the GitHub windows runners alike). Falls back to the bare name so
47
+ * a missing shell surfaces as a clear spawn ENOENT on `sh` rather than a
48
+ * misleading one on `/bin/sh`.
49
+ */
50
+ export function posixShellPath(platform = process.platform) {
51
+ if (platform !== 'win32')
52
+ return '/bin/sh';
53
+ return findExecutable('sh', platform) ?? findExecutable('bash', platform) ?? 'sh';
54
+ }
55
+ /**
56
+ * Quote one argument for a Windows `cmd.exe` command line, as built by Node's
57
+ * `spawn(..., { shell: true })` on win32 (the `.cmd` agent shims, `agents secrets
58
+ * exec`, ...). cmd.exe does NO quoting of its own, so an unquoted arg with a space
59
+ * is split into several args, and a cmd metacharacter (`&|<>()^`) would be
60
+ * interpreted by the shell. We wrap any arg with whitespace, a quote, or a
61
+ * metacharacter in double quotes and escape embedded quotes / trailing
62
+ * backslashes per the CommandLineToArgvW rules, so the *child's* argv parse
63
+ * reconstructs the original argument.
64
+ *
65
+ * CAVEAT: cmd.exe expands `%VAR%` (always) and `!VAR!` (under delayed expansion)
66
+ * BEFORE argv parsing, and double-quoting does NOT suppress `%`/`!` (the
67
+ * "BatBadBut" / CVE-2024-1874 class). We deliberately do not escape `%`/`!`:
68
+ * the callers here run a command whose `%`/`!`-bearing tokens are the caller's
69
+ * own (an agent prompt against the caller's shell, a bundle the caller owns), so
70
+ * caller-controlled `%`/`!` is not a privilege boundary. If that ever changes
71
+ * (composing an untrusted command line), route through a shell that disables
72
+ * expansion rather than relying on this quoter. An empty arg becomes `""`.
73
+ */
74
+ export function quoteWin32ExecArg(arg) {
75
+ if (arg.length > 0 && !/[\s"&|<>()^]/.test(arg))
76
+ return arg;
77
+ let result = '"';
78
+ let backslashes = 0;
79
+ for (const ch of arg) {
80
+ if (ch === '\\') {
81
+ backslashes += 1;
82
+ continue;
83
+ }
84
+ if (ch === '"') {
85
+ // Double the run of backslashes, then escape this quote.
86
+ result += '\\'.repeat(backslashes * 2 + 1) + '"';
87
+ backslashes = 0;
88
+ continue;
89
+ }
90
+ result += '\\'.repeat(backslashes) + ch;
91
+ backslashes = 0;
92
+ }
93
+ // Trailing backslashes precede the closing quote → must be doubled.
94
+ result += '\\'.repeat(backslashes * 2) + '"';
95
+ return result;
96
+ }
97
+ /**
98
+ * Compose a DEP0190-safe Windows shell command line from a command and its args.
99
+ *
100
+ * Node's `spawn(cmd, args, { shell: true })` on win32 concatenates `cmd` and
101
+ * every element of `args` into a single cmd.exe line WITHOUT escaping — that is
102
+ * both Node's DEP0190 deprecation (a future hard error) and a real injection
103
+ * surface, since user-controlled text (an agent prompt, a secrets-exec command)
104
+ * flows through `args`. Passing the fully-composed line as the SOLE `command`
105
+ * with an EMPTY args array sidesteps both: we quote every token with
106
+ * `quoteWin32ExecArg` so the child's CommandLineToArgvW parse reconstructs the
107
+ * exact original argv, and Node has no args array left to concatenate.
108
+ *
109
+ * Callers spawn the result as `spawn(line, [], { shell: true })`. A simple arg
110
+ * (no whitespace/quote/metachar) is passed through untouched, so the composed
111
+ * line is byte-identical to the old unquoted join for the common case.
112
+ */
113
+ export function composeWin32CommandLine(command, args) {
114
+ return [command, ...args].map(quoteWin32ExecArg).join(' ');
115
+ }
@@ -9,6 +9,37 @@
9
9
  * process counts as success.
10
10
  */
11
11
  export declare function killTree(pid: number): void;
12
+ /**
13
+ * Spawn options for a long-lived background child (daemon, detached worker,
14
+ * sidecar server, fire-and-forget job).
15
+ *
16
+ * POSIX: `detached: true` — the child leads its own process group, so it
17
+ * survives the parent and group kills (`kill(-pid)`) still reach it.
18
+ *
19
+ * Windows: the child must not share the launcher's console (a console-close
20
+ * event when the launcher exits would tear it down, #556) and must not flash
21
+ * a window. How to get there depends on the child's stdio:
22
+ *
23
+ * - All stdio non-inherited ('ignore'/'pipe'): `windowsHide: true`, NOT
24
+ * detached. CREATE_NO_WINDOW gives the child its own hidden console that
25
+ * every console-subsystem descendant (powershell, git, a .cmd shim's cmd.exe
26
+ * wrapper) inherits — no window anywhere down the tree. `detached` would
27
+ * defeat it: DETACHED_PROCESS makes CreateProcess ignore CREATE_NO_WINDOW.
28
+ *
29
+ * - Any stdio slot redirected to an fd (log files — `fdStdio: true`): libuv
30
+ * skips CREATE_NO_WINDOW whenever a stdio fd is inherited, so windowsHide
31
+ * cannot engage and a non-detached child would share the launcher's console
32
+ * and die with it. Keep DETACHED_PROCESS: the child runs console-less and
33
+ * windowless; its console-tool spawns stay invisible because the leaf call
34
+ * sites pass their own `windowsHide` with piped stdio.
35
+ */
36
+ export declare function backgroundSpawnOptions(opts?: {
37
+ fdStdio?: boolean;
38
+ platform?: NodeJS.Platform;
39
+ }): {
40
+ detached: boolean;
41
+ windowsHide: boolean;
42
+ };
12
43
  /**
13
44
  * Is a process with this PID currently alive?
14
45
  *
@@ -17,7 +17,7 @@ export function killTree(pid) {
17
17
  return;
18
18
  if (process.platform === 'win32') {
19
19
  try {
20
- execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore' });
20
+ execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { stdio: 'ignore', windowsHide: true });
21
21
  }
22
22
  catch { /* already gone, or no such pid */ }
23
23
  }
@@ -28,6 +28,39 @@ export function killTree(pid) {
28
28
  catch { /* already gone */ }
29
29
  }
30
30
  }
31
+ /**
32
+ * Spawn options for a long-lived background child (daemon, detached worker,
33
+ * sidecar server, fire-and-forget job).
34
+ *
35
+ * POSIX: `detached: true` — the child leads its own process group, so it
36
+ * survives the parent and group kills (`kill(-pid)`) still reach it.
37
+ *
38
+ * Windows: the child must not share the launcher's console (a console-close
39
+ * event when the launcher exits would tear it down, #556) and must not flash
40
+ * a window. How to get there depends on the child's stdio:
41
+ *
42
+ * - All stdio non-inherited ('ignore'/'pipe'): `windowsHide: true`, NOT
43
+ * detached. CREATE_NO_WINDOW gives the child its own hidden console that
44
+ * every console-subsystem descendant (powershell, git, a .cmd shim's cmd.exe
45
+ * wrapper) inherits — no window anywhere down the tree. `detached` would
46
+ * defeat it: DETACHED_PROCESS makes CreateProcess ignore CREATE_NO_WINDOW.
47
+ *
48
+ * - Any stdio slot redirected to an fd (log files — `fdStdio: true`): libuv
49
+ * skips CREATE_NO_WINDOW whenever a stdio fd is inherited, so windowsHide
50
+ * cannot engage and a non-detached child would share the launcher's console
51
+ * and die with it. Keep DETACHED_PROCESS: the child runs console-less and
52
+ * windowless; its console-tool spawns stay invisible because the leaf call
53
+ * sites pass their own `windowsHide` with piped stdio.
54
+ */
55
+ export function backgroundSpawnOptions(opts = {}) {
56
+ const platform = opts.platform ?? process.platform;
57
+ if (platform === 'win32') {
58
+ return opts.fdStdio
59
+ ? { detached: true, windowsHide: true }
60
+ : { detached: false, windowsHide: true };
61
+ }
62
+ return { detached: true, windowsHide: false };
63
+ }
31
64
  /**
32
65
  * Is a process with this PID currently alive?
33
66
  *
@@ -107,6 +107,7 @@ function runPowerShell(script, extraEnv) {
107
107
  encoding: 'utf-8',
108
108
  env: extraEnv ? { ...process.env, ...extraEnv } : process.env,
109
109
  stdio: ['ignore', 'pipe', 'pipe'],
110
+ windowsHide: true,
110
111
  });
111
112
  }
112
113
  /** Parse the read script's stdout into the original value kind and RAW value. */
@@ -163,6 +164,7 @@ export function getEffectiveExecutionPolicy() {
163
164
  const out = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', 'Get-ExecutionPolicy'], {
164
165
  encoding: 'utf-8',
165
166
  stdio: ['ignore', 'pipe', 'pipe'],
167
+ windowsHide: true,
166
168
  }).trim();
167
169
  return out || null;
168
170
  }
@@ -144,6 +144,36 @@ export declare function syncMarketplaceManifest(spec: MarketplaceSpec, agent: Ag
144
144
  * marketplaces' entries are preserved untouched.
145
145
  */
146
146
  export declare function registerMarketplace(spec: MarketplaceSpec, agent: AgentId, versionHome: string): void;
147
+ /**
148
+ * Record a plugin in Droid's installed_plugins.json (user scope), pointing
149
+ * installPath at the marketplace install dir so no second copy is needed. This
150
+ * is what makes `droid plugin list` show the plugin (Active once enabledPlugins
151
+ * is set). Idempotent: re-running refreshes lastUpdated and preserves the
152
+ * original installedAt plus any non-user scope entries.
153
+ */
154
+ export declare function registerDroidInstalledPlugin(pluginName: string, marketplaceName: string, installDir: string, version: string, agent: AgentId, versionHome: string): void;
155
+ /** True when Droid's installed_plugins.json carries a user-scope entry for the plugin. */
156
+ export declare function isDroidPluginInstalled(pluginName: string, marketplaceName: string, agent: AgentId, versionHome: string): boolean;
157
+ /**
158
+ * Remove a plugin's user-scope entry from Droid's installed_plugins.json.
159
+ * Inverse of registerDroidInstalledPlugin. Drops the key when no scopes remain
160
+ * and deletes the file when the registry is empty.
161
+ */
162
+ export declare function unregisterDroidInstalledPlugin(pluginName: string, marketplaceName: string, agent: AgentId, versionHome: string): void;
163
+ /**
164
+ * Record a plugin in Copilot's config.json#installedPlugins (the registry that
165
+ * makes `copilot plugin list` see it). `cache_path` points at the marketplace
166
+ * install dir — no separate copy. Idempotent: re-running refreshes the entry and
167
+ * preserves the original installed_at plus every other config key.
168
+ */
169
+ export declare function registerCopilotInstalledPlugin(pluginName: string, marketplaceName: string, installDir: string, version: string, enabled: boolean, agent: AgentId, versionHome: string): void;
170
+ /**
171
+ * Remove a plugin's entry from Copilot's config.json#installedPlugins. Inverse
172
+ * of registerCopilotInstalledPlugin. Leaves the file (with an empty
173
+ * installedPlugins) rather than deleting it, since config.json may hold other
174
+ * Copilot-managed keys.
175
+ */
176
+ export declare function unregisterCopilotInstalledPlugin(pluginName: string, marketplaceName: string, agent: AgentId, versionHome: string): void;
147
177
  /**
148
178
  * Drop a marketplace entry from known_marketplaces.json. Called when the last
149
179
  * plugin under it is removed. Removes only its own entry; deletes the file only
@@ -127,7 +127,14 @@ export function marketplaceRoot(specOrName, agent, versionHome) {
127
127
  return path.join(pluginsRootForVersion(agent, versionHome), 'marketplaces', nameOf(specOrName));
128
128
  }
129
129
  export function marketplaceManifestPath(specOrName, agent, versionHome) {
130
- return path.join(marketplaceRoot(specOrName, agent, versionHome), '.claude-plugin', 'marketplace.json');
130
+ const root = marketplaceRoot(specOrName, agent, versionHome);
131
+ // Copilot resolves a marketplace's catalog from `marketplace.json` at the
132
+ // marketplace ROOT (also `.plugin/marketplace.json`, `.github/plugin/
133
+ // marketplace.json`) — NOT `.claude-plugin/marketplace.json`. Verified against
134
+ // GitHub Copilot CLI 1.0.56 (`plugin marketplace add`).
135
+ if (agent === 'copilot')
136
+ return path.join(root, 'marketplace.json');
137
+ return path.join(root, '.claude-plugin', 'marketplace.json');
131
138
  }
132
139
  export function pluginInstallDir(plugin, specOrName, agent, versionHome) {
133
140
  return path.join(marketplaceRoot(specOrName, agent, versionHome), 'plugins', plugin.name);
@@ -375,6 +382,14 @@ export function syncMarketplaceManifest(spec, agent, versionHome) {
375
382
  export function registerMarketplace(spec, agent, versionHome) {
376
383
  const name = marketplaceNameFor(spec);
377
384
  const root = marketplaceRoot(spec, agent, versionHome);
385
+ // Copilot diverges from Claude's known_marketplaces.json: it reads registered
386
+ // marketplaces from settings.json#extraKnownMarketplaces. Verified against
387
+ // GitHub Copilot CLI 1.0.56 (`plugin marketplace add <dir>` writes exactly
388
+ // this shape). A "directory"-source entry points at the on-disk catalog root.
389
+ if (agent === 'copilot') {
390
+ registerCopilotMarketplace(name, root, agent, versionHome);
391
+ return;
392
+ }
378
393
  const knownPath = knownMarketplacesPath(agent, versionHome);
379
394
  let known = {};
380
395
  if (fs.existsSync(knownPath)) {
@@ -385,14 +400,206 @@ export function registerMarketplace(spec, agent, versionHome) {
385
400
  known = {};
386
401
  }
387
402
  }
403
+ // Droid names the on-disk source type "local" (Claude/OpenClaw use
404
+ // "directory") and stamps autoUpdate — verified against `droid plugin
405
+ // marketplace add`. A "directory" entry is silently ignored by the Factory
406
+ // CLI, so the plugin never resolves.
407
+ const isDroid = agent === 'droid';
388
408
  known[name] = {
389
- source: { source: 'directory', path: root },
409
+ source: { source: isDroid ? 'local' : 'directory', path: root },
390
410
  installLocation: root,
391
411
  lastUpdated: new Date().toISOString(),
412
+ ...(isDroid ? { autoUpdate: true } : {}),
392
413
  };
393
414
  fs.mkdirSync(path.dirname(knownPath), { recursive: true });
394
415
  fs.writeFileSync(knownPath, JSON.stringify(known, null, 2) + '\n', 'utf-8');
395
416
  }
417
+ // ─── Droid installed-plugins registry ─────────────────────────────────────────
418
+ function installedPluginsPath(agent, versionHome) {
419
+ return path.join(pluginsRootForVersion(agent, versionHome), 'installed_plugins.json');
420
+ }
421
+ function readInstalledPlugins(agent, versionHome) {
422
+ const p = installedPluginsPath(agent, versionHome);
423
+ if (fs.existsSync(p)) {
424
+ try {
425
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
426
+ if (parsed && typeof parsed === 'object' && parsed.plugins && typeof parsed.plugins === 'object') {
427
+ return { schemaVersion: parsed.schemaVersion ?? 1, plugins: parsed.plugins };
428
+ }
429
+ }
430
+ catch { /* fall through to fresh registry */ }
431
+ }
432
+ return { schemaVersion: 1, plugins: {} };
433
+ }
434
+ /**
435
+ * Record a plugin in Droid's installed_plugins.json (user scope), pointing
436
+ * installPath at the marketplace install dir so no second copy is needed. This
437
+ * is what makes `droid plugin list` show the plugin (Active once enabledPlugins
438
+ * is set). Idempotent: re-running refreshes lastUpdated and preserves the
439
+ * original installedAt plus any non-user scope entries.
440
+ */
441
+ export function registerDroidInstalledPlugin(pluginName, marketplaceName, installDir, version, agent, versionHome) {
442
+ const registry = readInstalledPlugins(agent, versionHome);
443
+ const key = `${pluginName}@${marketplaceName}`;
444
+ const now = new Date().toISOString();
445
+ const existing = registry.plugins[key] ?? [];
446
+ const priorUser = existing.find(e => e.scope === 'user');
447
+ const others = existing.filter(e => e.scope !== 'user');
448
+ registry.plugins[key] = [
449
+ ...others,
450
+ {
451
+ scope: 'user',
452
+ installPath: installDir,
453
+ version,
454
+ installedAt: priorUser?.installedAt ?? now,
455
+ lastUpdated: now,
456
+ source: marketplaceName,
457
+ },
458
+ ];
459
+ const p = installedPluginsPath(agent, versionHome);
460
+ fs.mkdirSync(path.dirname(p), { recursive: true });
461
+ fs.writeFileSync(p, JSON.stringify(registry, null, 2) + '\n', 'utf-8');
462
+ }
463
+ /** True when Droid's installed_plugins.json carries a user-scope entry for the plugin. */
464
+ export function isDroidPluginInstalled(pluginName, marketplaceName, agent, versionHome) {
465
+ const registry = readInstalledPlugins(agent, versionHome);
466
+ const entries = registry.plugins[`${pluginName}@${marketplaceName}`];
467
+ return Array.isArray(entries) && entries.some(e => e.scope === 'user');
468
+ }
469
+ /**
470
+ * Remove a plugin's user-scope entry from Droid's installed_plugins.json.
471
+ * Inverse of registerDroidInstalledPlugin. Drops the key when no scopes remain
472
+ * and deletes the file when the registry is empty.
473
+ */
474
+ export function unregisterDroidInstalledPlugin(pluginName, marketplaceName, agent, versionHome) {
475
+ const p = installedPluginsPath(agent, versionHome);
476
+ if (!fs.existsSync(p))
477
+ return;
478
+ const registry = readInstalledPlugins(agent, versionHome);
479
+ const key = `${pluginName}@${marketplaceName}`;
480
+ const entries = registry.plugins[key];
481
+ if (!Array.isArray(entries))
482
+ return;
483
+ const kept = entries.filter(e => e.scope !== 'user');
484
+ if (kept.length > 0) {
485
+ registry.plugins[key] = kept;
486
+ }
487
+ else {
488
+ delete registry.plugins[key];
489
+ }
490
+ if (Object.keys(registry.plugins).length === 0) {
491
+ try {
492
+ fs.unlinkSync(p);
493
+ }
494
+ catch { /* ignore */ }
495
+ return;
496
+ }
497
+ fs.writeFileSync(p, JSON.stringify(registry, null, 2) + '\n', 'utf-8');
498
+ }
499
+ // ─── Copilot marketplace + installed-plugin registry ──────────────────────────
500
+ function copilotConfigPath(agent, versionHome) {
501
+ return path.join(versionHome, agentConfigDirName(agent), 'config.json');
502
+ }
503
+ function readCopilotSettings(agent, versionHome) {
504
+ const p = settingsPath(agent, versionHome);
505
+ if (fs.existsSync(p)) {
506
+ try {
507
+ const parsed = JSON.parse(fs.readFileSync(p, 'utf-8'));
508
+ if (parsed && typeof parsed === 'object')
509
+ return parsed;
510
+ }
511
+ catch { /* fall through */ }
512
+ }
513
+ return {};
514
+ }
515
+ function writeCopilotSettings(agent, versionHome, settings) {
516
+ const p = settingsPath(agent, versionHome);
517
+ fs.mkdirSync(path.dirname(p), { recursive: true });
518
+ fs.writeFileSync(p, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
519
+ }
520
+ /** Register a marketplace in Copilot's settings.json#extraKnownMarketplaces. */
521
+ function registerCopilotMarketplace(name, root, agent, versionHome) {
522
+ const settings = readCopilotSettings(agent, versionHome);
523
+ const known = (settings.extraKnownMarketplaces && typeof settings.extraKnownMarketplaces === 'object'
524
+ ? settings.extraKnownMarketplaces
525
+ : {});
526
+ known[name] = { source: { source: 'directory', path: root } };
527
+ settings.extraKnownMarketplaces = known;
528
+ writeCopilotSettings(agent, versionHome, settings);
529
+ }
530
+ /** Inverse of registerCopilotMarketplace: drop the entry, prune the empty key. */
531
+ function unregisterCopilotMarketplace(name, agent, versionHome) {
532
+ const p = settingsPath(agent, versionHome);
533
+ if (!fs.existsSync(p))
534
+ return;
535
+ const settings = readCopilotSettings(agent, versionHome);
536
+ const known = settings.extraKnownMarketplaces;
537
+ if (!known || !(name in known))
538
+ return;
539
+ delete known[name];
540
+ if (Object.keys(known).length === 0)
541
+ delete settings.extraKnownMarketplaces;
542
+ writeCopilotSettings(agent, versionHome, settings);
543
+ }
544
+ function readCopilotConfig(agent, versionHome) {
545
+ const p = copilotConfigPath(agent, versionHome);
546
+ if (fs.existsSync(p)) {
547
+ try {
548
+ // config.json may carry a leading `//` comment banner written by Copilot;
549
+ // strip line comments before parsing so we never clobber a real config.
550
+ const raw = fs.readFileSync(p, 'utf-8').replace(/^\s*\/\/.*$/gm, '');
551
+ const parsed = JSON.parse(raw);
552
+ if (parsed && typeof parsed === 'object') {
553
+ return { ...parsed, installedPlugins: Array.isArray(parsed.installedPlugins) ? parsed.installedPlugins : [] };
554
+ }
555
+ }
556
+ catch { /* fall through to fresh config */ }
557
+ }
558
+ return { installedPlugins: [] };
559
+ }
560
+ /**
561
+ * Record a plugin in Copilot's config.json#installedPlugins (the registry that
562
+ * makes `copilot plugin list` see it). `cache_path` points at the marketplace
563
+ * install dir — no separate copy. Idempotent: re-running refreshes the entry and
564
+ * preserves the original installed_at plus every other config key.
565
+ */
566
+ export function registerCopilotInstalledPlugin(pluginName, marketplaceName, installDir, version, enabled, agent, versionHome) {
567
+ const config = readCopilotConfig(agent, versionHome);
568
+ const now = new Date().toISOString();
569
+ const prior = config.installedPlugins.find(e => e.name === pluginName && e.marketplace === marketplaceName);
570
+ const others = config.installedPlugins.filter(e => !(e.name === pluginName && e.marketplace === marketplaceName));
571
+ config.installedPlugins = [
572
+ ...others,
573
+ {
574
+ name: pluginName,
575
+ marketplace: marketplaceName,
576
+ version,
577
+ installed_at: prior?.installed_at ?? now,
578
+ enabled,
579
+ cache_path: installDir,
580
+ },
581
+ ];
582
+ const p = copilotConfigPath(agent, versionHome);
583
+ fs.mkdirSync(path.dirname(p), { recursive: true });
584
+ fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n', 'utf-8');
585
+ }
586
+ /**
587
+ * Remove a plugin's entry from Copilot's config.json#installedPlugins. Inverse
588
+ * of registerCopilotInstalledPlugin. Leaves the file (with an empty
589
+ * installedPlugins) rather than deleting it, since config.json may hold other
590
+ * Copilot-managed keys.
591
+ */
592
+ export function unregisterCopilotInstalledPlugin(pluginName, marketplaceName, agent, versionHome) {
593
+ const p = copilotConfigPath(agent, versionHome);
594
+ if (!fs.existsSync(p))
595
+ return;
596
+ const config = readCopilotConfig(agent, versionHome);
597
+ const kept = config.installedPlugins.filter(e => !(e.name === pluginName && e.marketplace === marketplaceName));
598
+ if (kept.length === config.installedPlugins.length)
599
+ return;
600
+ config.installedPlugins = kept;
601
+ fs.writeFileSync(p, JSON.stringify(config, null, 2) + '\n', 'utf-8');
602
+ }
396
603
  /**
397
604
  * Drop a marketplace entry from known_marketplaces.json. Called when the last
398
605
  * plugin under it is removed. Removes only its own entry; deletes the file only
@@ -400,6 +607,12 @@ export function registerMarketplace(spec, agent, versionHome) {
400
607
  */
401
608
  export function unregisterMarketplace(specOrName, agent, versionHome) {
402
609
  const name = nameOf(specOrName);
610
+ // Copilot stores registrations in settings.json#extraKnownMarketplaces, not
611
+ // known_marketplaces.json. Mirror the branch in registerMarketplace.
612
+ if (agent === 'copilot') {
613
+ unregisterCopilotMarketplace(name, agent, versionHome);
614
+ return;
615
+ }
403
616
  const knownPath = knownMarketplacesPath(agent, versionHome);
404
617
  if (!fs.existsSync(knownPath))
405
618
  return;
@@ -148,6 +148,11 @@ export declare function marketplaceSpecForName(name: string | undefined, cwd?: s
148
148
  * MCP servers, bin/, settings.json, and permissions once the plugin lives at the
149
149
  * native install path and is marked enabled — see
150
150
  * https://code.claude.com/docs/en/plugins.
151
+ *
152
+ * Droid (Factory CLI) reuses this same marketplace layout but additionally
153
+ * requires an installed_plugins.json registry entry and a "local"-source
154
+ * known_marketplaces.json entry before `droid plugin list` will see the plugin
155
+ * (steps handled by registerMarketplace's droid branch + step 5c below).
151
156
  */
152
157
  export declare function syncPluginToVersion(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string, options?: {
153
158
  allowExecSurfaces?: boolean;