@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
@@ -18,7 +18,7 @@ import { listInstalledVersions, getVersionHomePath } from './versions.js';
18
18
  import { AGENTS, agentConfigDirName } from './agents.js';
19
19
  import { capableAgents, isCapable } from './capabilities.js';
20
20
  import { shouldInstallCommandAsSkill, installCommandSkillToVersion } from './command-skills.js';
21
- import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
21
+ import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, addPluginToSettings, removePluginFromSettings, removePluginFromMarketplace, registerDroidInstalledPlugin, unregisterDroidInstalledPlugin, isDroidPluginInstalled, registerCopilotInstalledPlugin, unregisterCopilotInstalledPlugin, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, discoverMarketplaces, marketplaceNameFor, MARKETPLACE_NAME, PROJECT_MARKETPLACE_NAME, SYSTEM_MARKETPLACE_NAME, } from './plugin-marketplace.js';
22
22
  const PLUGIN_MANIFEST_DIR = '.claude-plugin';
23
23
  const PLUGIN_MANIFEST_FILE = 'plugin.json';
24
24
  const USER_CONFIG_FILE = '.user-config.json';
@@ -172,16 +172,26 @@ export function loadPluginManifest(pluginRoot) {
172
172
  }
173
173
  }
174
174
  export function validatePluginName(name) {
175
- return name.length > 0
176
- && !/[/\\]/.test(name)
177
- && !name.includes('..')
178
- && !name.includes('\0');
175
+ if (!name || name.length === 0 || name === '.' || name === '..') {
176
+ return false;
177
+ }
178
+ const normalized = path.normalize(name);
179
+ if (normalized === '.' || normalized === '' || normalized === '..') {
180
+ return false;
181
+ }
182
+ if (/[/\\]/.test(name) || name.includes('\0')) {
183
+ return false;
184
+ }
185
+ if (path.basename(normalized) !== name) {
186
+ return false;
187
+ }
188
+ return true;
179
189
  }
180
190
  export function assertPluginTargetContained(targetRoot, pluginsDir) {
181
191
  const resolvedPluginsDir = path.resolve(pluginsDir);
182
192
  const resolvedTargetRoot = path.resolve(targetRoot);
183
- if (resolvedTargetRoot !== resolvedPluginsDir
184
- && !resolvedTargetRoot.startsWith(`${resolvedPluginsDir}${path.sep}`)) {
193
+ if (resolvedTargetRoot === resolvedPluginsDir
194
+ || !resolvedTargetRoot.startsWith(`${resolvedPluginsDir}${path.sep}`)) {
185
195
  throw new Error(`Plugin install target escapes plugins directory: ${targetRoot}`);
186
196
  }
187
197
  }
@@ -416,7 +426,7 @@ export function marketplaceSpecForName(name, cwd = process.cwd()) {
416
426
  * carries, not just the user one.
417
427
  */
418
428
  function listVersionMarketplaceNames(agent, versionHome) {
419
- const dir = path.join(versionHome, `.${agent}`, 'plugins', 'marketplaces');
429
+ const dir = path.join(versionHome, agentConfigDirName(agent), 'plugins', 'marketplaces');
420
430
  if (!fs.existsSync(dir))
421
431
  return [];
422
432
  try {
@@ -444,6 +454,11 @@ function listVersionMarketplaceNames(agent, versionHome) {
444
454
  * MCP servers, bin/, settings.json, and permissions once the plugin lives at the
445
455
  * native install path and is marked enabled — see
446
456
  * https://code.claude.com/docs/en/plugins.
457
+ *
458
+ * Droid (Factory CLI) reuses this same marketplace layout but additionally
459
+ * requires an installed_plugins.json registry entry and a "local"-source
460
+ * known_marketplaces.json entry before `droid plugin list` will see the plugin
461
+ * (steps handled by registerMarketplace's droid branch + step 5c below).
447
462
  */
448
463
  export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
449
464
  const result = {
@@ -491,9 +506,25 @@ export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
491
506
  // .mcp.json, settings.json, permissions/) are only auto-enabled when the
492
507
  // caller explicitly opts in. addPluginToSettings does no gating — that moved
493
508
  // here, where plugin capabilities are inspected.
494
- if (options.allowExecSurfaces === true || !hasPluginExecSurfaces(inspectPluginCapabilities(plugin.root))) {
509
+ const enablePlugin = options.allowExecSurfaces === true || !hasPluginExecSurfaces(inspectPluginCapabilities(plugin.root));
510
+ if (enablePlugin) {
495
511
  addPluginToSettings(plugin.name, marketplaceName, agent, versionHome);
496
512
  }
513
+ // 5c. Droid diverges from Claude's marketplace+enabledPlugins model: it only
514
+ // "sees" a plugin that also has an entry in installed_plugins.json (with
515
+ // the marketplace registered as source "local"). Register the install so
516
+ // `droid plugin list` shows it — Active when enabled, Inactive otherwise.
517
+ if (agent === 'droid') {
518
+ registerDroidInstalledPlugin(plugin.name, marketplaceName, installDir, plugin.manifest.version, agent, versionHome);
519
+ }
520
+ // 5d. Copilot, like Droid, only "sees" a plugin that also has an entry in its
521
+ // auto-managed config.json#installedPlugins (registerMarketplace already
522
+ // wrote settings.json#extraKnownMarketplaces, and addPluginToSettings the
523
+ // enabledPlugins flag). cache_path points at the marketplace copy; the
524
+ // `enabled` flag mirrors the exec-surface trust gate above.
525
+ if (agent === 'copilot') {
526
+ registerCopilotInstalledPlugin(plugin.name, marketplaceName, installDir, plugin.manifest.version, enablePlugin, agent, versionHome);
527
+ }
497
528
  // 5b. Convert plugin commands/ to skills for agents that dropped command support
498
529
  // (Codex >= 0.117.0). Skill name is prefixed with plugin name to avoid
499
530
  // collision with standalone command skills.
@@ -718,7 +749,15 @@ function migrateLegacyFlatLayout(plugin, agent, versionHome) {
718
749
  export function isPluginSynced(plugin, agent, versionHome) {
719
750
  if (!isCapable(agent, 'plugins'))
720
751
  return false;
721
- return isInstalledInMarketplace(plugin.name, marketplaceSpecForName(plugin.marketplace), agent, versionHome);
752
+ const spec = marketplaceSpecForName(plugin.marketplace);
753
+ if (!isInstalledInMarketplace(plugin.name, spec, agent, versionHome))
754
+ return false;
755
+ // Droid additionally requires its installed_plugins.json registry entry —
756
+ // without it the marketplace copy is invisible to `droid plugin list`.
757
+ if (agent === 'droid') {
758
+ return isDroidPluginInstalled(plugin.name, marketplaceNameFor(spec), agent, versionHome);
759
+ }
760
+ return true;
722
761
  }
723
762
  // ─── Removal ─────────────────────────────────────────────────────────────────
724
763
  /**
@@ -746,6 +785,12 @@ export function removePluginFromVersion(pluginName, pluginRoot, agent, versionHo
746
785
  removedAny = true;
747
786
  }
748
787
  removePluginFromSettings(pluginName, name, agent, versionHome);
788
+ if (agent === 'droid') {
789
+ unregisterDroidInstalledPlugin(pluginName, name, agent, versionHome);
790
+ }
791
+ if (agent === 'copilot') {
792
+ unregisterCopilotInstalledPlugin(pluginName, name, agent, versionHome);
793
+ }
749
794
  // Refresh marketplace.json so it reflects what's left under plugins/.
750
795
  syncMarketplaceManifest(spec, agent, versionHome);
751
796
  // If we just removed the last plugin, drop the marketplace dir and the
@@ -930,6 +975,12 @@ export function cleanOrphanedPluginSkills(agent, versionHome, activePluginNames,
930
975
  fs.mkdirSync(trashDir, { recursive: true, mode: 0o700 });
931
976
  fs.renameSync(path.join(mktPluginsDir, entry.name), trashDest);
932
977
  removePluginFromSettings(entry.name, name, agent, versionHome);
978
+ if (agent === 'droid') {
979
+ unregisterDroidInstalledPlugin(entry.name, name, agent, versionHome);
980
+ }
981
+ if (agent === 'copilot') {
982
+ unregisterCopilotInstalledPlugin(entry.name, name, agent, versionHome);
983
+ }
933
984
  removed.push(entry.name);
934
985
  trashedHere = true;
935
986
  }
@@ -22,6 +22,14 @@ export interface Profile {
22
22
  description?: string;
23
23
  preset?: string;
24
24
  provider?: string;
25
+ /**
26
+ * Optional secondary model retried on the same host when the primary model
27
+ * env value hits a rate limit. Reuses the `--fallback` cascade in
28
+ * `runWithFallback` (src/lib/exec.ts) — the swap is expressed as an
29
+ * envOverride on a same-agent FallbackEntry, so only the model env var
30
+ * changes; auth, base URL, and every other profile env value are preserved.
31
+ */
32
+ fallback_model?: string;
25
33
  }
26
34
  /**
27
35
  * Stable, machine-readable summary used by `agents view` and `--json`.
@@ -59,6 +67,13 @@ export declare function profileHostLabel(profile: Profile): string;
59
67
  export declare function profileProviderLabel(profile: Profile): string;
60
68
  /** Return the configured model env value for display. */
61
69
  export declare function profileModelLabel(profile: Profile): string;
70
+ /**
71
+ * Return the env var key that carries this profile's model (e.g.
72
+ * `ANTHROPIC_MODEL`), or null when the profile has no recognizable model env.
73
+ * `fallback_model` swaps THIS key so provider selection, auth, and base URL
74
+ * are all preserved on retry.
75
+ */
76
+ export declare function profileModelEnvKey(profile: Profile): string | null;
62
77
  /**
63
78
  * Build a non-secret auth identity/status label for list surfaces.
64
79
  *
@@ -89,6 +104,16 @@ export interface ResolvedProfileRun {
89
104
  version?: string;
90
105
  env: Record<string, string>;
91
106
  profileName: string;
107
+ /**
108
+ * Same-host model swap for the `--fallback` cascade. Present only when the
109
+ * profile declares `fallback_model` AND the profile has an identifiable
110
+ * model env key to swap. `envKey` names the var (e.g. `ANTHROPIC_MODEL`),
111
+ * `model` is the value to write on the retry attempt.
112
+ */
113
+ fallbackModel?: {
114
+ envKey: string;
115
+ model: string;
116
+ };
92
117
  }
93
118
  /**
94
119
  * Resolve a name into (agent, version, env). Throws if the name is not a
@@ -111,16 +111,25 @@ const MODEL_ENV_KEYS = [
111
111
  ];
112
112
  /** Return the configured model env value for display. */
113
113
  export function profileModelLabel(profile) {
114
+ const key = profileModelEnvKey(profile);
115
+ return key ? profile.env[key] : '-';
116
+ }
117
+ /**
118
+ * Return the env var key that carries this profile's model (e.g.
119
+ * `ANTHROPIC_MODEL`), or null when the profile has no recognizable model env.
120
+ * `fallback_model` swaps THIS key so provider selection, auth, and base URL
121
+ * are all preserved on retry.
122
+ */
123
+ export function profileModelEnvKey(profile) {
114
124
  for (const key of MODEL_ENV_KEYS) {
115
- const value = profile.env[key];
116
- if (value)
117
- return value;
125
+ if (profile.env[key])
126
+ return key;
118
127
  }
119
128
  for (const [key, value] of Object.entries(profile.env)) {
120
129
  if ((key === 'MODEL' || key.endsWith('_MODEL')) && value)
121
- return value;
130
+ return key;
122
131
  }
123
- return '-';
132
+ return null;
124
133
  }
125
134
  function decodeJwtPayload(token) {
126
135
  const parts = token.split('.');
@@ -239,12 +248,19 @@ export function resolveProfileEnv(profile) {
239
248
  */
240
249
  export function resolveProfileForRun(name) {
241
250
  const profile = readProfile(name);
242
- return {
251
+ const resolved = {
243
252
  agent: profile.host.agent,
244
253
  version: profile.host.version,
245
254
  env: resolveProfileEnv(profile),
246
255
  profileName: profile.name,
247
256
  };
257
+ if (profile.fallback_model) {
258
+ const envKey = profileModelEnvKey(profile);
259
+ if (envKey) {
260
+ resolved.fallbackModel = { envKey, model: profile.fallback_model };
261
+ }
262
+ }
263
+ return resolved;
248
264
  }
249
265
  /**
250
266
  * Look up the preset a profile was created from, if any. Used by
@@ -10,6 +10,7 @@ import { spawn, execSync } from 'child_process';
10
10
  import { fileURLToPath } from 'url';
11
11
  import * as path from 'path';
12
12
  import { getSocketPath, getPtyLogPath, isPtyServerRunning } from './pty-server.js';
13
+ import { backgroundSpawnOptions } from './platform/process.js';
13
14
  const CONNECT_TIMEOUT_MS = 5000;
14
15
  const RESPONSE_TIMEOUT_MS = 30000;
15
16
  const IS_WINDOWS = process.platform === 'win32';
@@ -38,7 +39,7 @@ async function ensureServer() {
38
39
  const logFd = fs.openSync(logPath, 'a');
39
40
  const child = spawn(bin, args, {
40
41
  stdio: ['ignore', logFd, logFd],
41
- detached: true,
42
+ ...backgroundSpawnOptions({ fdStdio: true }),
42
43
  });
43
44
  child.unref();
44
45
  fs.closeSync(logFd);
@@ -5,6 +5,7 @@
5
5
  * authentication tokens so users maintain active sessions across version switches.
6
6
  */
7
7
  import type { AgentId, RunStrategy } from './types.js';
8
+ import type { FallbackEntry } from './exec.js';
8
9
  import { type AccountInfo } from './agents.js';
9
10
  import { type UsageSnapshot } from './usage.js';
10
11
  export interface RotateCandidate {
@@ -101,3 +102,63 @@ export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy,
101
102
  version: string | null;
102
103
  rotation: RotateResult | null;
103
104
  }>;
105
+ /**
106
+ * Cap on the number of healthy accounts a single run will re-dispatch through
107
+ * after a mid-run rate limit. Bounds the synthesized chain so a machine signed
108
+ * into many accounts can't turn one 429 into an unbounded cascade of retries.
109
+ */
110
+ export declare const DEFAULT_ROTATION_FAILOVER_LIMIT = 3;
111
+ /**
112
+ * Synthesize a same-agent, cross-account fallback chain from a pre-flight
113
+ * rotation result (issue #348: mid-run rate-limit failover).
114
+ *
115
+ * The account rotation picks ONE version pre-spawn; today a 429 mid-run kills
116
+ * the run with no recovery. `runWithFallback` + `detectRateLimit` already
117
+ * re-dispatch to the NEXT chain entry on a rate limit and hand off the session
118
+ * via `/continue <id>` — but only for explicit `--fallback` chains. This turns
119
+ * the OTHER healthy rotation candidates (every account except the one already
120
+ * picked as the primary) into `FallbackEntry`s so that SAME machinery re-runs
121
+ * the task on the next healthy account of the same agent when the primary 429s.
122
+ *
123
+ * Each account is a distinct installed version (its own home/auth), so the
124
+ * entries are same-agent, different-version — exactly what runWithFallback
125
+ * spawns and what buildFallbackPrompt continues (claude→claude via `/continue`).
126
+ * Candidates are consumed in `rotation.healthy` order, which is sorted by
127
+ * remaining capacity (most headroom first, see compareCandidates), so failover
128
+ * prefers the freshest account.
129
+ *
130
+ * Returns `[]` when there is no rotation (pinned strategy) or the picked account
131
+ * is the only healthy one — so single-account users and non-rotation runs are
132
+ * completely unchanged.
133
+ */
134
+ export declare function rotationFailoverChain(rotation: RotateResult | null, pickedVersion: string, limit?: number): FallbackEntry[];
135
+ /**
136
+ * Whether a run is eligible to have a mid-run rate-limit failover chain armed
137
+ * (issue #348). Failover injects synthesized `FallbackEntry`s into the same
138
+ * `fallback` array that `--fallback` uses — so it must NOT arm for run shapes
139
+ * that reject a non-empty fallback chain, or the run hard-exits on a flag the
140
+ * user never passed. Specifically:
141
+ *
142
+ * - `acp` and `loop` runs bail with "not compatible with --fallback yet" the
143
+ * moment `fallback.length > 0` (src/commands/exec.ts), so arming failover
144
+ * would break a previously-working `agents run … --loop` / `--acp`.
145
+ * - `resumeCheckpoint` runs take the loop path (same guard).
146
+ * - `interactive` / no-prompt runs can't be re-dispatched headlessly.
147
+ * - `explicitFallback` (a user `--fallback` chain OR a profile fallback already
148
+ * unshifted) defines its own recovery; don't layer rotation failover on top.
149
+ * - `hasRotation`/`hasVersion` gate on an actual pre-flight rotation having
150
+ * picked an account, so pinned and non-rotation runs are untouched.
151
+ *
152
+ * Pure so the arming matrix is unit-testable without invoking the run command.
153
+ */
154
+ export interface FailoverArmingContext {
155
+ hasRotation: boolean;
156
+ hasVersion: boolean;
157
+ hasPrompt: boolean;
158
+ explicitFallback: boolean;
159
+ interactive: boolean;
160
+ acp: boolean;
161
+ loop: boolean;
162
+ resumeCheckpoint: boolean;
163
+ }
164
+ export declare function shouldArmRotationFailover(ctx: FailoverArmingContext): boolean;
@@ -341,3 +341,55 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd()) {
341
341
  }
342
342
  return { version: fallback, rotation: null };
343
343
  }
344
+ /**
345
+ * Cap on the number of healthy accounts a single run will re-dispatch through
346
+ * after a mid-run rate limit. Bounds the synthesized chain so a machine signed
347
+ * into many accounts can't turn one 429 into an unbounded cascade of retries.
348
+ */
349
+ export const DEFAULT_ROTATION_FAILOVER_LIMIT = 3;
350
+ /**
351
+ * Synthesize a same-agent, cross-account fallback chain from a pre-flight
352
+ * rotation result (issue #348: mid-run rate-limit failover).
353
+ *
354
+ * The account rotation picks ONE version pre-spawn; today a 429 mid-run kills
355
+ * the run with no recovery. `runWithFallback` + `detectRateLimit` already
356
+ * re-dispatch to the NEXT chain entry on a rate limit and hand off the session
357
+ * via `/continue <id>` — but only for explicit `--fallback` chains. This turns
358
+ * the OTHER healthy rotation candidates (every account except the one already
359
+ * picked as the primary) into `FallbackEntry`s so that SAME machinery re-runs
360
+ * the task on the next healthy account of the same agent when the primary 429s.
361
+ *
362
+ * Each account is a distinct installed version (its own home/auth), so the
363
+ * entries are same-agent, different-version — exactly what runWithFallback
364
+ * spawns and what buildFallbackPrompt continues (claude→claude via `/continue`).
365
+ * Candidates are consumed in `rotation.healthy` order, which is sorted by
366
+ * remaining capacity (most headroom first, see compareCandidates), so failover
367
+ * prefers the freshest account.
368
+ *
369
+ * Returns `[]` when there is no rotation (pinned strategy) or the picked account
370
+ * is the only healthy one — so single-account users and non-rotation runs are
371
+ * completely unchanged.
372
+ */
373
+ export function rotationFailoverChain(rotation, pickedVersion, limit = DEFAULT_ROTATION_FAILOVER_LIMIT) {
374
+ if (!rotation || limit <= 0)
375
+ return [];
376
+ const chain = [];
377
+ for (const candidate of rotation.healthy) {
378
+ if (candidate.version === pickedVersion)
379
+ continue; // the primary account
380
+ chain.push({ agent: candidate.agent, version: candidate.version });
381
+ if (chain.length >= limit)
382
+ break;
383
+ }
384
+ return chain;
385
+ }
386
+ export function shouldArmRotationFailover(ctx) {
387
+ return (ctx.hasRotation &&
388
+ ctx.hasVersion &&
389
+ ctx.hasPrompt &&
390
+ !ctx.explicitFallback &&
391
+ !ctx.interactive &&
392
+ !ctx.acp &&
393
+ !ctx.loop &&
394
+ !ctx.resumeCheckpoint);
395
+ }
@@ -7,16 +7,50 @@
7
7
  * scheduling.
8
8
  */
9
9
  import type { AgentId } from './types.js';
10
+ import type { LoopConfig } from './loop.js';
10
11
  /** Tool/site/directory allow-list for sandboxed job execution. */
11
12
  export interface JobAllowConfig {
12
13
  tools?: string[];
13
14
  sites?: string[];
14
15
  dirs?: string[];
15
16
  }
16
- /** Full configuration for a scheduled routine (persisted as YAML). */
17
+ /** GitHub webhook events a routine can be triggered by. */
18
+ export type GithubTriggerEvent = 'pull_request' | 'push' | 'issue_comment' | 'workflow_run';
19
+ /** Canonical set of accepted GitHub trigger events — single source for validation. */
20
+ export declare const GITHUB_TRIGGER_EVENTS: readonly GithubTriggerEvent[];
21
+ /**
22
+ * Map a user-facing `--on` alias to a canonical GitHub trigger event.
23
+ * Accepts the canonical names plus friendly shortcuts (e.g. `pr`, `pr_opened`
24
+ * → `pull_request`, `comment` → `issue_comment`). Returns null when unknown.
25
+ */
26
+ export declare function normalizeTriggerEvent(input: string): GithubTriggerEvent | null;
27
+ /**
28
+ * Event-based fire condition for a routine — an alternative (or complement) to
29
+ * `schedule`. Currently only `github_event`: an incoming GitHub webhook whose
30
+ * event (and optional repo/branch) match fires the job through the same
31
+ * dispatch path a cron fire uses. See `src/lib/triggers/webhook.ts`.
32
+ */
33
+ export interface JobTrigger {
34
+ type: 'github_event';
35
+ event: GithubTriggerEvent;
36
+ /** `owner/name` — when set, only payloads for this repo match. */
37
+ repo?: string;
38
+ /** git branch (ref short name) — when set, only payloads for this branch match. */
39
+ branch?: string;
40
+ }
41
+ /**
42
+ * Full configuration for a routine (persisted as YAML).
43
+ *
44
+ * A job fires on a `schedule` (cron), on a `trigger` (event/webhook), or both.
45
+ * `schedule` remains a first-class field; trigger-only jobs omit it and are
46
+ * skipped by the cron scheduler (they fire only via the webhook receiver).
47
+ */
17
48
  export interface JobConfig {
18
49
  name: string;
19
- schedule: string;
50
+ /** Cron expression. Optional when `trigger` is set (event-only routine). */
51
+ schedule?: string;
52
+ /** Event/webhook fire condition. Optional when `schedule` is set. */
53
+ trigger?: JobTrigger;
20
54
  agent: AgentId;
21
55
  workflow?: string;
22
56
  mode: 'plan' | 'edit' | 'auto' | 'skip' | 'full';
@@ -33,6 +67,8 @@ export interface JobConfig {
33
67
  version?: string;
34
68
  runOnce?: boolean;
35
69
  endAt?: string;
70
+ /** When set, executeJob runs this job through the loop driver instead of once. */
71
+ loop?: LoopConfig;
36
72
  }
37
73
  /** Metadata for a single job execution, persisted as JSON in the run directory. */
38
74
  export interface RunMeta {
@@ -67,6 +103,8 @@ export declare function deleteJob(name: string): boolean;
67
103
  export declare function setJobEnabled(name: string, enabled: boolean): void;
68
104
  /** Validate a partial job config, returning a list of human-readable errors. */
69
105
  export declare function validateJob(config: Partial<JobConfig>): string[];
106
+ /** Validate a job trigger block, returning a list of human-readable errors. */
107
+ export declare function validateTrigger(trigger: unknown): string[];
70
108
  /** True when a job's endAt has already elapsed. False when endAt is unset or in the future. */
71
109
  export declare function isPastEndAt(config: Pick<JobConfig, 'endAt'>, now?: Date): boolean;
72
110
  /** Expand built-in and user-defined template variables in a job's prompt string. */
@@ -13,6 +13,33 @@ import { Cron } from 'croner';
13
13
  import { getRoutinesDir, getRunsDir, ensureAgentsDir, getProjectRoutinesDir } from './state.js';
14
14
  import { safeJoin } from './paths.js';
15
15
  import { ALL_AGENT_IDS } from './agents.js';
16
+ /** Canonical set of accepted GitHub trigger events — single source for validation. */
17
+ export const GITHUB_TRIGGER_EVENTS = [
18
+ 'pull_request',
19
+ 'push',
20
+ 'issue_comment',
21
+ 'workflow_run',
22
+ ];
23
+ /**
24
+ * Map a user-facing `--on` alias to a canonical GitHub trigger event.
25
+ * Accepts the canonical names plus friendly shortcuts (e.g. `pr`, `pr_opened`
26
+ * → `pull_request`, `comment` → `issue_comment`). Returns null when unknown.
27
+ */
28
+ export function normalizeTriggerEvent(input) {
29
+ const key = input.trim().toLowerCase();
30
+ const aliases = {
31
+ pull_request: 'pull_request',
32
+ pr: 'pull_request',
33
+ pr_opened: 'pull_request',
34
+ pull: 'pull_request',
35
+ push: 'push',
36
+ issue_comment: 'issue_comment',
37
+ comment: 'issue_comment',
38
+ workflow_run: 'workflow_run',
39
+ workflow: 'workflow_run',
40
+ };
41
+ return aliases[key] ?? null;
42
+ }
16
43
  /** Default values applied to every job config when fields are omitted. */
17
44
  const JOB_DEFAULTS = {
18
45
  mode: 'plan',
@@ -137,18 +164,28 @@ export function validateJob(config) {
137
164
  if (!config.name || typeof config.name !== 'string') {
138
165
  errors.push('name is required');
139
166
  }
140
- if (!config.schedule || typeof config.schedule !== 'string') {
141
- errors.push('schedule (cron expression) is required');
167
+ const hasSchedule = Boolean(config.schedule && typeof config.schedule === 'string');
168
+ const hasTrigger = config.trigger !== undefined;
169
+ if (!hasSchedule && !hasTrigger) {
170
+ errors.push('schedule (cron expression) or trigger is required');
142
171
  }
143
- else {
144
- // Validate cron expression is parseable
145
- try {
146
- new Cron(config.schedule);
172
+ if (config.schedule !== undefined) {
173
+ if (typeof config.schedule !== 'string') {
174
+ errors.push('schedule must be a cron expression string');
147
175
  }
148
- catch {
149
- errors.push(`invalid cron expression: "${config.schedule}"`);
176
+ else {
177
+ // Validate cron expression is parseable
178
+ try {
179
+ new Cron(config.schedule);
180
+ }
181
+ catch {
182
+ errors.push(`invalid cron expression: "${config.schedule}"`);
183
+ }
150
184
  }
151
185
  }
186
+ if (config.trigger !== undefined) {
187
+ errors.push(...validateTrigger(config.trigger));
188
+ }
152
189
  const hasAgent = Boolean(config.agent && typeof config.agent === 'string');
153
190
  const hasWorkflow = Boolean(config.workflow && typeof config.workflow === 'string');
154
191
  if (!hasAgent && !hasWorkflow) {
@@ -184,6 +221,27 @@ export function validateJob(config) {
184
221
  }
185
222
  return errors;
186
223
  }
224
+ /** Validate a job trigger block, returning a list of human-readable errors. */
225
+ export function validateTrigger(trigger) {
226
+ const errors = [];
227
+ if (!trigger || typeof trigger !== 'object') {
228
+ return ['trigger must be an object'];
229
+ }
230
+ const t = trigger;
231
+ if (t.type !== 'github_event') {
232
+ errors.push("trigger.type must be 'github_event'");
233
+ }
234
+ if (!t.event || !GITHUB_TRIGGER_EVENTS.includes(t.event)) {
235
+ errors.push(`trigger.event must be one of: ${GITHUB_TRIGGER_EVENTS.join(', ')}`);
236
+ }
237
+ if (t.repo !== undefined && (typeof t.repo !== 'string' || !/^[^/\s]+\/[^/\s]+$/.test(t.repo))) {
238
+ errors.push('trigger.repo must be in owner/name form');
239
+ }
240
+ if (t.branch !== undefined && typeof t.branch !== 'string') {
241
+ errors.push('trigger.branch must be a string');
242
+ }
243
+ return errors;
244
+ }
187
245
  function isParseableDate(value) {
188
246
  if (!value.trim())
189
247
  return false;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import type { JobConfig, RunMeta } from './routines.js';
10
10
  import type { AgentId } from './types.js';
11
+ import type { LoopDeps } from './loop.js';
11
12
  /** Result of a completed job execution, including metadata and optional report. */
12
13
  export interface RunResult {
13
14
  meta: RunMeta;
@@ -15,8 +16,16 @@ export interface RunResult {
15
16
  }
16
17
  /** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
17
18
  export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
18
- /** Execute a job synchronously (waits for completion or timeout before resolving). */
19
- export declare function executeJob(config: JobConfig): Promise<RunResult>;
19
+ /**
20
+ * Execute a job synchronously (waits for completion or timeout before resolving).
21
+ *
22
+ * When `config.loop` is set the job is routed through the loop driver (`runLoop`
23
+ * from loop.ts) instead of a single spawn — same driver as `agents run --loop` and
24
+ * workflow `loop:` blocks (issue #400). The optional `deps` parameter provides
25
+ * injectable seams (runIteration, sleep, writeCheckpoint) used by tests; production
26
+ * callers omit it and get the defaults.
27
+ */
28
+ export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
20
29
  /** Spawn a job as a detached process and return immediately with run metadata. */
21
30
  export declare function executeJobDetached(config: JobConfig): Promise<RunMeta>;
22
31
  /** Extract the final assistant message from a stream-JSON log file as a markdown report. */