@phnx-labs/agents-cli 1.20.72 → 1.20.74

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 (234) hide show
  1. package/CHANGELOG.md +395 -0
  2. package/README.md +40 -4
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/activity.d.ts +12 -0
  5. package/dist/commands/activity.js +68 -0
  6. package/dist/commands/apply.d.ts +13 -0
  7. package/dist/commands/apply.js +28 -3
  8. package/dist/commands/browser.js +1 -1
  9. package/dist/commands/cloud.js +21 -11
  10. package/dist/commands/doctor.js +21 -5
  11. package/dist/commands/events.d.ts +11 -10
  12. package/dist/commands/events.js +30 -21
  13. package/dist/commands/exec.d.ts +34 -0
  14. package/dist/commands/exec.js +337 -39
  15. package/dist/commands/export.d.ts +11 -0
  16. package/dist/commands/export.js +215 -0
  17. package/dist/commands/feed.d.ts +13 -0
  18. package/dist/commands/feed.js +130 -29
  19. package/dist/commands/hq.d.ts +2 -0
  20. package/dist/commands/hq.js +58 -0
  21. package/dist/commands/import.js +51 -3
  22. package/dist/commands/lease.d.ts +19 -0
  23. package/dist/commands/lease.js +138 -11
  24. package/dist/commands/login.d.ts +2 -0
  25. package/dist/commands/login.js +123 -0
  26. package/dist/commands/logs.js +1 -1
  27. package/dist/commands/mine.d.ts +27 -0
  28. package/dist/commands/mine.js +207 -0
  29. package/dist/commands/monitors.js +79 -49
  30. package/dist/commands/plugins.js +1 -0
  31. package/dist/commands/profiles.js +158 -0
  32. package/dist/commands/repo.js +2 -60
  33. package/dist/commands/resources.d.ts +5 -0
  34. package/dist/commands/resources.js +95 -0
  35. package/dist/commands/routines.d.ts +2 -0
  36. package/dist/commands/routines.js +139 -37
  37. package/dist/commands/secrets.js +234 -47
  38. package/dist/commands/send.d.ts +15 -0
  39. package/dist/commands/send.js +62 -0
  40. package/dist/commands/sessions-tail.d.ts +1 -0
  41. package/dist/commands/sessions-tail.js +20 -9
  42. package/dist/commands/sessions.js +18 -3
  43. package/dist/commands/setup-mine.d.ts +18 -0
  44. package/dist/commands/setup-mine.js +106 -0
  45. package/dist/commands/setup-share.js +2 -2
  46. package/dist/commands/setup.js +9 -2
  47. package/dist/commands/share.d.ts +8 -1
  48. package/dist/commands/share.js +122 -37
  49. package/dist/commands/ssh.d.ts +9 -0
  50. package/dist/commands/ssh.js +196 -26
  51. package/dist/commands/sync.js +1 -1
  52. package/dist/commands/teams.js +11 -2
  53. package/dist/commands/versions.js +36 -5
  54. package/dist/commands/view.js +75 -27
  55. package/dist/index.js +80 -10
  56. package/dist/lib/activity.d.ts +124 -0
  57. package/dist/lib/activity.js +542 -0
  58. package/dist/lib/agents.d.ts +35 -0
  59. package/dist/lib/agents.js +79 -6
  60. package/dist/lib/artifact-actions.d.ts +1 -1
  61. package/dist/lib/artifact-actions.js +1 -1
  62. package/dist/lib/ask-classifier.d.ts +2 -1
  63. package/dist/lib/ask-classifier.js +3 -3
  64. package/dist/lib/auth-health.d.ts +32 -4
  65. package/dist/lib/auth-health.js +40 -1
  66. package/dist/lib/auto-pull.js +8 -1
  67. package/dist/lib/brand.d.ts +40 -0
  68. package/dist/lib/brand.js +122 -0
  69. package/dist/lib/browser/drivers/ssh.js +4 -7
  70. package/dist/lib/browser/service.js +6 -8
  71. package/dist/lib/channels/providers/index.d.ts +2 -0
  72. package/dist/lib/channels/providers/index.js +20 -0
  73. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  74. package/dist/lib/channels/providers/mailbox.js +26 -0
  75. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  76. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  77. package/dist/lib/channels/providers/rush.d.ts +6 -0
  78. package/dist/lib/channels/providers/rush.js +55 -0
  79. package/dist/lib/channels/registry.d.ts +42 -0
  80. package/dist/lib/channels/registry.js +20 -0
  81. package/dist/lib/channels/resolve.d.ts +11 -0
  82. package/dist/lib/channels/resolve.js +12 -0
  83. package/dist/lib/cli-resources.d.ts +18 -0
  84. package/dist/lib/cli-resources.js +53 -4
  85. package/dist/lib/cloud/codex.js +6 -3
  86. package/dist/lib/cloud/factory.d.ts +1 -0
  87. package/dist/lib/cloud/factory.js +10 -4
  88. package/dist/lib/cloud/rush.d.ts +3 -1
  89. package/dist/lib/cloud/rush.js +6 -1
  90. package/dist/lib/cloud/types.d.ts +2 -0
  91. package/dist/lib/codex-home.d.ts +35 -0
  92. package/dist/lib/codex-home.js +136 -0
  93. package/dist/lib/config-transfer.d.ts +16 -0
  94. package/dist/lib/config-transfer.js +56 -0
  95. package/dist/lib/crabbox/cli.d.ts +44 -0
  96. package/dist/lib/crabbox/cli.js +120 -20
  97. package/dist/lib/crabbox/lease.d.ts +52 -1
  98. package/dist/lib/crabbox/lease.js +117 -16
  99. package/dist/lib/crabbox/progress.d.ts +31 -0
  100. package/dist/lib/crabbox/progress.js +76 -0
  101. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  102. package/dist/lib/crabbox/runtimes.js +33 -8
  103. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  104. package/dist/lib/crabbox/setup-copy.js +127 -0
  105. package/dist/lib/daemon.d.ts +5 -5
  106. package/dist/lib/daemon.js +9 -9
  107. package/dist/lib/drive-sync.js +6 -3
  108. package/dist/lib/event-stream.d.ts +36 -0
  109. package/dist/lib/event-stream.js +68 -0
  110. package/dist/lib/events.d.ts +1 -1
  111. package/dist/lib/exec.d.ts +11 -0
  112. package/dist/lib/exec.js +23 -2
  113. package/dist/lib/export.d.ts +72 -0
  114. package/dist/lib/export.js +269 -0
  115. package/dist/lib/feed-policy.d.ts +1 -1
  116. package/dist/lib/feed-policy.js +14 -9
  117. package/dist/lib/feed-ranking.d.ts +32 -0
  118. package/dist/lib/feed-ranking.js +224 -0
  119. package/dist/lib/feed.d.ts +35 -2
  120. package/dist/lib/feed.js +66 -3
  121. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  122. package/dist/lib/fleet/auth-sync.js +92 -0
  123. package/dist/lib/fleet/remote-login.d.ts +187 -0
  124. package/dist/lib/fleet/remote-login.js +556 -0
  125. package/dist/lib/format.d.ts +30 -3
  126. package/dist/lib/format.js +34 -5
  127. package/dist/lib/fs-atomic.d.ts +7 -2
  128. package/dist/lib/fs-atomic.js +8 -12
  129. package/dist/lib/git.d.ts +16 -0
  130. package/dist/lib/git.js +79 -2
  131. package/dist/lib/heal.js +11 -3
  132. package/dist/lib/hosts/progress.d.ts +28 -10
  133. package/dist/lib/hosts/progress.js +79 -22
  134. package/dist/lib/hosts/remote-cmd.js +4 -0
  135. package/dist/lib/hq/floor.d.ts +87 -0
  136. package/dist/lib/hq/floor.js +226 -0
  137. package/dist/lib/import.d.ts +23 -0
  138. package/dist/lib/import.js +74 -2
  139. package/dist/lib/isolation-boundary-report.d.ts +10 -0
  140. package/dist/lib/isolation-boundary-report.js +35 -0
  141. package/dist/lib/menubar/install-menubar.js +14 -20
  142. package/dist/lib/notify.d.ts +1 -0
  143. package/dist/lib/notify.js +3 -3
  144. package/dist/lib/open-url.d.ts +2 -0
  145. package/dist/lib/open-url.js +19 -0
  146. package/dist/lib/openclaw-keychain.d.ts +56 -0
  147. package/dist/lib/openclaw-keychain.js +236 -0
  148. package/dist/lib/overdue.js +10 -0
  149. package/dist/lib/permissions.d.ts +44 -1
  150. package/dist/lib/permissions.js +284 -7
  151. package/dist/lib/project-launch.d.ts +6 -12
  152. package/dist/lib/project-launch.js +13 -228
  153. package/dist/lib/project-resources.d.ts +7 -0
  154. package/dist/lib/project-resources.js +306 -0
  155. package/dist/lib/pty-client.d.ts +27 -0
  156. package/dist/lib/pty-client.js +136 -10
  157. package/dist/lib/refresh.js +14 -10
  158. package/dist/lib/resource-profiles.d.ts +26 -0
  159. package/dist/lib/resource-profiles.js +157 -0
  160. package/dist/lib/resources/permissions.js +7 -1
  161. package/dist/lib/resources/types.d.ts +1 -1
  162. package/dist/lib/resources.d.ts +1 -1
  163. package/dist/lib/resources.js +32 -3
  164. package/dist/lib/routines.d.ts +11 -0
  165. package/dist/lib/routines.js +56 -15
  166. package/dist/lib/runner.d.ts +1 -0
  167. package/dist/lib/runner.js +66 -5
  168. package/dist/lib/secrets/bundles.d.ts +16 -3
  169. package/dist/lib/secrets/bundles.js +206 -37
  170. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  171. package/dist/lib/secrets/icloud-import.js +9 -6
  172. package/dist/lib/secrets/mcp.js +1 -1
  173. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  174. package/dist/lib/secrets/vault-age-helper.js +34 -0
  175. package/dist/lib/secrets/vault.d.ts +49 -0
  176. package/dist/lib/secrets/vault.js +397 -0
  177. package/dist/lib/self-heal/checks/path.js +3 -1
  178. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  179. package/dist/lib/self-heal/checks/shims.js +19 -11
  180. package/dist/lib/session/cloud.d.ts +2 -2
  181. package/dist/lib/session/cloud.js +2 -2
  182. package/dist/lib/session/db.d.ts +4 -0
  183. package/dist/lib/session/db.js +44 -7
  184. package/dist/lib/session/discover.d.ts +2 -0
  185. package/dist/lib/session/discover.js +114 -3
  186. package/dist/lib/session/stream-render.d.ts +3 -0
  187. package/dist/lib/session/stream-render.js +130 -0
  188. package/dist/lib/session/types.d.ts +6 -0
  189. package/dist/lib/share/analytics.d.ts +13 -0
  190. package/dist/lib/share/analytics.js +45 -0
  191. package/dist/lib/share/config.d.ts +19 -5
  192. package/dist/lib/share/config.js +44 -8
  193. package/dist/lib/share/provision.d.ts +58 -6
  194. package/dist/lib/share/provision.js +97 -22
  195. package/dist/lib/share/publish.d.ts +36 -13
  196. package/dist/lib/share/publish.js +55 -8
  197. package/dist/lib/share/worker-template.js +83 -17
  198. package/dist/lib/shims.d.ts +35 -0
  199. package/dist/lib/shims.js +159 -13
  200. package/dist/lib/ssh-exec.d.ts +14 -0
  201. package/dist/lib/ssh-exec.js +57 -0
  202. package/dist/lib/staleness/detectors/hooks.js +25 -1
  203. package/dist/lib/staleness/detectors/permissions.js +66 -0
  204. package/dist/lib/staleness/detectors/workflows.js +20 -0
  205. package/dist/lib/staleness/writers/hooks.js +58 -4
  206. package/dist/lib/staleness/writers/permissions.js +2 -2
  207. package/dist/lib/staleness/writers/skills.js +1 -1
  208. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  209. package/dist/lib/staleness/writers/sources.js +68 -1
  210. package/dist/lib/star-nudge.d.ts +45 -0
  211. package/dist/lib/star-nudge.js +91 -0
  212. package/dist/lib/startup/command-registry.d.ts +7 -1
  213. package/dist/lib/startup/command-registry.js +19 -3
  214. package/dist/lib/state.d.ts +2 -0
  215. package/dist/lib/state.js +17 -4
  216. package/dist/lib/subagents-registry.d.ts +2 -0
  217. package/dist/lib/subagents-registry.js +3 -0
  218. package/dist/lib/teams/parsers.js +214 -6
  219. package/dist/lib/teams/supervisor.d.ts +7 -0
  220. package/dist/lib/teams/supervisor.js +2 -1
  221. package/dist/lib/template.d.ts +1 -1
  222. package/dist/lib/template.js +1 -1
  223. package/dist/lib/triggers/webhook.js +36 -3
  224. package/dist/lib/types.d.ts +84 -0
  225. package/dist/lib/uninstall.js +1 -45
  226. package/dist/lib/version.d.ts +40 -0
  227. package/dist/lib/version.js +94 -16
  228. package/dist/lib/versions.d.ts +24 -0
  229. package/dist/lib/versions.js +269 -78
  230. package/dist/lib/workflows.d.ts +2 -0
  231. package/dist/lib/workflows.js +88 -0
  232. package/package.json +2 -1
  233. package/dist/commands/daemon.d.ts +0 -10
  234. package/dist/commands/daemon.js +0 -121
package/dist/lib/shims.js CHANGED
@@ -17,6 +17,7 @@ import { IS_WINDOWS, prependToWindowsUserPath } from './platform/index.js';
17
17
  import { getShimsDir, getVersionsDir, getBackupsDir, getHistoryDir, ensureAgentsDir } from './state.js';
18
18
  export { getShimsDir };
19
19
  import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from './agents.js';
20
+ import { codexHomeShimBash } from './codex-home.js';
20
21
  /**
21
22
  * Files and directories to always skip during conflict detection and migration.
22
23
  * These are never user config that should be migrated.
@@ -261,12 +262,7 @@ if [ "\$(uname -s)" = "Linux" ] && [ -z "\${CLAUDE_CODE_OAUTH_TOKEN:-}" ] && [ -
261
262
  fi
262
263
  `
263
264
  : agent === 'codex'
264
- ? `
265
- # Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
266
- # from CODEX_HOME. Point it at the versioned home so permissions/rules
267
- # written by agents-cli actually take effect.
268
- export CODEX_HOME="$VERSION_DIR/home/${configDirName}"
269
- `
265
+ ? codexHomeShimBash(`$VERSION_DIR/home/${configDirName}`, `$AGENTS_USER_DIR/.codex-homes/$VERSION`)
270
266
  : agent === 'copilot'
271
267
  ? `
272
268
  # GitHub Copilot CLI honors COPILOT_HOME to relocate its config and state
@@ -665,6 +661,9 @@ export function shimTargetsFor(platform) {
665
661
  * Create a shim for an agent.
666
662
  */
667
663
  export function createShim(agent) {
664
+ // A bare shim puts agents-cli first on PATH for this agent — the opposite of what
665
+ // an isolated-only install promises.
666
+ assertIsolationBoundary(agent, 'create the bare shim');
668
667
  ensureAgentsDir();
669
668
  const shimsDir = getShimsDir();
670
669
  const agentConfig = AGENTS[agent];
@@ -682,6 +681,75 @@ export function createShim(agent) {
682
681
  }
683
682
  return shimPath;
684
683
  }
684
+ // ─── White-label brand shims ──────────────────────────────────────────────────
685
+ // A brand shim is a PURE pass-through: unlike an agent shim (which resolves a
686
+ // version and execs the agent binary) or an alias shim (which injects a fixed
687
+ // subcommand), it forwards argv verbatim to the agents-cli entrypoint with
688
+ // `AGENTS_BRAND` set, so `<brand> <any-verb>` behaves exactly like
689
+ // `agents <any-verb>` but presents under the brand's name. See lib/brand.ts.
690
+ const BRAND_SHIM_MARKER = '# Brand shim:';
691
+ /** The POSIX pass-through shim for a brand. */
692
+ export function generateBrandShim(name) {
693
+ const agentsBin = shellQuote(getAgentsBinForGeneratedShim());
694
+ return `#!/bin/sh
695
+ # Auto-generated by agents-cli - do not edit
696
+ ${BRAND_SHIM_MARKER} ${name} -> agents-cli (white-label)
697
+ AGENTS_BIN=${agentsBin}
698
+ if [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then
699
+ echo "${name}: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2
700
+ exit 127
701
+ fi
702
+ export AGENTS_BRAND=${name}
703
+ exec "$AGENTS_BIN" "$@"
704
+ `;
705
+ }
706
+ /** Write a brand's pass-through shim(s) onto PATH; returns the shim path. */
707
+ export function createBrandShim(name) {
708
+ ensureAgentsDir();
709
+ const shimsDir = getShimsDir();
710
+ const shimPath = path.join(shimsDir, name);
711
+ const targets = shimTargetsFor(process.platform);
712
+ if (targets.bash) {
713
+ fs.writeFileSync(shimPath, generateBrandShim(name), { mode: 0o755 });
714
+ }
715
+ if (targets.cmd) {
716
+ writeWindowsBrandShim(shimPath + '.cmd', name);
717
+ }
718
+ return shimPath;
719
+ }
720
+ /** Windows `.cmd` pass-through: set AGENTS_BRAND then forward argv to the entrypoint. */
721
+ function writeWindowsBrandShim(cmdPath, name) {
722
+ const indexJs = getAgentsBinForGeneratedShim();
723
+ const content = `@echo off\r\n` +
724
+ `rem Auto-generated by agents-cli - do not edit\r\n` +
725
+ `rem Brand shim: ${name}\r\n` +
726
+ `set AGENTS_BRAND=${name}\r\n` +
727
+ `node "${indexJs}" %*\r\n`;
728
+ fs.writeFileSync(cmdPath, content);
729
+ }
730
+ /** True when the file at the given path is an agents-cli brand shim. */
731
+ export function isBrandShim(filePath) {
732
+ try {
733
+ const head = fs.readFileSync(filePath, 'utf-8').slice(0, 300);
734
+ return head.includes(BRAND_SHIM_MARKER) || head.includes('rem Brand shim:');
735
+ }
736
+ catch {
737
+ return false;
738
+ }
739
+ }
740
+ /** Remove a brand's shim companions. Returns true if anything was removed. */
741
+ export function removeBrandShim(name) {
742
+ const shimsDir = getShimsDir();
743
+ const shimPath = path.join(shimsDir, name);
744
+ let removed = false;
745
+ for (const p of [shimPath, shimPath + '.cmd']) {
746
+ if (fs.existsSync(p) && isBrandShim(p)) {
747
+ fs.unlinkSync(p);
748
+ removed = true;
749
+ }
750
+ }
751
+ return removed;
752
+ }
685
753
  /**
686
754
  * Generate a Windows `.cmd` launcher that delegates to `agents __shim <spec>`.
687
755
  * `spec` is the agent's cliCommand for the default-version shim, or
@@ -829,12 +897,7 @@ export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/ho
829
897
  export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
830
898
  `
831
899
  : agent === 'codex'
832
- ? `
833
- # Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
834
- # from CODEX_HOME. Point direct aliases at the versioned home so permissions
835
- # and rules written by agents-cli actually take effect.
836
- export CODEX_HOME="$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}"
837
- `
900
+ ? codexHomeShimBash(`$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$HOME/.agents/.codex-homes/${version}`)
838
901
  : agent === 'copilot'
839
902
  ? `
840
903
  # Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
@@ -1262,6 +1325,8 @@ export function carryForwardAuthFiles(agent, toConfigDir) {
1262
1325
  }
1263
1326
  }
1264
1327
  export async function switchConfigSymlink(agent, version) {
1328
+ // Moves the user's real ~/.<agent> aside and symlinks it into a version home.
1329
+ assertIsolationBoundary(agent, 'repoint your real config directory');
1265
1330
  const configPath = getAgentConfigPath(agent);
1266
1331
  const versionConfigPath = getVersionConfigPath(agent, version);
1267
1332
  // Ensure version config directory exists
@@ -1366,6 +1431,8 @@ export async function switchConfigSymlink(agent, version) {
1366
1431
  * ALL installed versions so they inherit the current account.
1367
1432
  */
1368
1433
  export function switchHomeFileSymlinks(agent, version) {
1434
+ // Same, for home-level files such as ~/.claude.json.
1435
+ assertIsolationBoundary(agent, 'repoint your home-level config files');
1369
1436
  const agentConfig = AGENTS[agent];
1370
1437
  const homeFiles = agentConfig.homeFiles;
1371
1438
  if (!homeFiles || homeFiles.length === 0)
@@ -2016,6 +2083,9 @@ function canonicalOrNull(p) {
2016
2083
  * - Never records our own shim as the "original" (would loop).
2017
2084
  */
2018
2085
  export function adoptShadowingLauncher(agent, overrides) {
2086
+ // Repoints the user's OWN launcher symlink at our shim — the most invasive thing
2087
+ // in the codebase, and the one with no isolated-scoped equivalent at all.
2088
+ assertIsolationBoundary(agent, 'adopt your launcher');
2019
2089
  const shimsDir = overrides?.shimsDir ?? getShimsDir();
2020
2090
  const shimPath = path.join(shimsDir, AGENTS[agent].cliCommand);
2021
2091
  const shimReal = canonical(shimPath);
@@ -2368,6 +2438,82 @@ export function listAgentsWithInstalledVersions() {
2368
2438
  .map((entry) => entry.name)
2369
2439
  .filter((agent) => fs.readdirSync(path.join(versionsDir, agent), { withFileTypes: true }).some((entry) => entry.isDirectory()));
2370
2440
  }
2441
+ function isInstalledVersionIsolated(agent, version) {
2442
+ return fs.existsSync(path.join(getVersionsDir(), agent, version, '.isolated'));
2443
+ }
2444
+ /**
2445
+ * Thrown when an operation would carry an isolated-only agent across the isolation
2446
+ * boundary. Callers that can explain the situation catch it and print guidance; the
2447
+ * throw is what makes the boundary a property of the code rather than a convention
2448
+ * every future call site has to remember.
2449
+ */
2450
+ export class IsolationBoundaryError extends Error {
2451
+ agent;
2452
+ operation;
2453
+ constructor(agent, operation) {
2454
+ super(`${agent} is installed only as isolated copies; "${operation}" would adopt it into your local setup.`);
2455
+ this.agent = agent;
2456
+ this.operation = operation;
2457
+ this.name = 'IsolationBoundaryError';
2458
+ }
2459
+ }
2460
+ /**
2461
+ * True when EVERY installed version of `agent` is isolated (and at least one is).
2462
+ *
2463
+ * This is the switch: installing with `--isolated` is itself the act of opting in,
2464
+ * so there is no mode to set and none to forget. It is per-agent, so an isolated
2465
+ * codex constrains nothing about claude. And the escape hatch is inherent — remove
2466
+ * the isolated copies and the agent is ordinary again, which means the state that
2467
+ * grants protection is the same state you delete to drop it.
2468
+ *
2469
+ * An agent with any NORMAL version is not protected: that install already owns the
2470
+ * launcher and the real `~/.<agent>`, so there is no boundary left to defend.
2471
+ */
2472
+ export function isIsolationProtected(agent) {
2473
+ const agentVersionsDir = path.join(getVersionsDir(), agent);
2474
+ let dirs;
2475
+ try {
2476
+ dirs = fs.readdirSync(agentVersionsDir, { withFileTypes: true })
2477
+ .filter((e) => e.isDirectory())
2478
+ .map((e) => e.name);
2479
+ }
2480
+ catch {
2481
+ return false;
2482
+ }
2483
+ // Count only directories that are actually an install. A bare version dir is
2484
+ // scaffolding, not a non-isolated version — and treating it as one would disable
2485
+ // protection at exactly the wrong moment: an adopting path that does
2486
+ // `mkdirSync(<version>/home)` before adopting would flip this to false with its own
2487
+ // first line and then walk straight through the gates. Ignoring scaffolding biases
2488
+ // the predicate toward protecting, which is the safe direction to fail in.
2489
+ const installed = dirs.filter((v) => {
2490
+ const dir = path.join(agentVersionsDir, v);
2491
+ return fs.existsSync(path.join(dir, 'node_modules')) || fs.existsSync(path.join(dir, 'package.json'));
2492
+ });
2493
+ if (installed.length === 0)
2494
+ return false;
2495
+ return installed.every((v) => isInstalledVersionIsolated(agent, v));
2496
+ }
2497
+ /** Refuse `operation` when `agent` is isolated-only. */
2498
+ export function assertIsolationBoundary(agent, operation) {
2499
+ if (isIsolationProtected(agent))
2500
+ throw new IsolationBoundaryError(agent, operation);
2501
+ }
2502
+ export function listAgentsWithNonIsolatedInstalledVersions() {
2503
+ const versionsDir = getVersionsDir();
2504
+ if (!fs.existsSync(versionsDir)) {
2505
+ return [];
2506
+ }
2507
+ const entries = fs.readdirSync(versionsDir, { withFileTypes: true });
2508
+ return entries
2509
+ .filter((entry) => entry.isDirectory() && AGENTS[entry.name])
2510
+ .map((entry) => entry.name)
2511
+ .filter((agent) => {
2512
+ const agentVersionsDir = path.join(versionsDir, agent);
2513
+ return fs.readdirSync(agentVersionsDir, { withFileTypes: true })
2514
+ .some((entry) => entry.isDirectory() && !isInstalledVersionIsolated(agent, entry.name));
2515
+ });
2516
+ }
2371
2517
  /**
2372
2518
  * Create shims for all installed agents.
2373
2519
  */
@@ -2382,7 +2528,7 @@ function ensureAllShims() {
2382
2528
  const agent = entry.name;
2383
2529
  const agentVersionsDir = path.join(versionsDir, agent);
2384
2530
  const versions = fs.readdirSync(agentVersionsDir, { withFileTypes: true })
2385
- .filter((e) => e.isDirectory());
2531
+ .filter((e) => e.isDirectory() && !isInstalledVersionIsolated(agent, e.name));
2386
2532
  if (versions.length > 0 && !shimExists(agent)) {
2387
2533
  createShim(agent);
2388
2534
  }
@@ -89,6 +89,20 @@ export interface SshExecRawResult {
89
89
  * U+FFFD replacement char (which would desync a byte offset from the wire).
90
90
  */
91
91
  export declare function sshExecRaw(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecRawResult;
92
+ export interface SshExecRawStreamOptions extends SshExecOptions {
93
+ onStdout: (chunk: Buffer) => void;
94
+ onStderr?: (chunk: Buffer) => void;
95
+ signal?: AbortSignal;
96
+ }
97
+ /**
98
+ * Stream raw stdout from a long-lived remote command over ssh.
99
+ *
100
+ * Unlike {@link sshStream}, this does not inherit local stdio: callers receive
101
+ * byte-exact stdout chunks and decide how to account for them. That matters for
102
+ * offset-resumed log following where a UTF-8 decode boundary must not shift the
103
+ * byte cursor.
104
+ */
105
+ export declare function sshExecRawStream(target: string, remoteCmd: string, opts: SshExecRawStreamOptions): Promise<Omit<SshExecRawResult, 'stdout'>>;
92
106
  /** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
93
107
  export declare function sshReachable(target: string, timeoutMs?: number): boolean;
94
108
  export interface SshStreamOptions {
@@ -205,6 +205,63 @@ export function sshExecRaw(target, remoteCmd, opts = {}) {
205
205
  timedOut,
206
206
  };
207
207
  }
208
+ /**
209
+ * Stream raw stdout from a long-lived remote command over ssh.
210
+ *
211
+ * Unlike {@link sshStream}, this does not inherit local stdio: callers receive
212
+ * byte-exact stdout chunks and decide how to account for them. That matters for
213
+ * offset-resumed log following where a UTF-8 decode boundary must not shift the
214
+ * byte cursor.
215
+ */
216
+ export function sshExecRawStream(target, remoteCmd, opts) {
217
+ assertValidSshTarget(target);
218
+ const mux = opts.multiplex === false ? [] : controlOpts();
219
+ const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
220
+ return new Promise((resolve) => {
221
+ const child = spawn('ssh', args, {
222
+ stdio: ['pipe', 'pipe', 'pipe'],
223
+ windowsHide: true,
224
+ });
225
+ const stderr = [];
226
+ let settled = false;
227
+ let timedOut = false;
228
+ let aborted = false;
229
+ const finish = (code) => {
230
+ if (settled)
231
+ return;
232
+ settled = true;
233
+ if (timer)
234
+ clearTimeout(timer);
235
+ opts.signal?.removeEventListener('abort', abort);
236
+ resolve({ code, stderr: Buffer.concat(stderr), timedOut });
237
+ };
238
+ const abort = () => {
239
+ aborted = true;
240
+ child.kill('SIGTERM');
241
+ };
242
+ const timer = opts.timeoutMs
243
+ ? setTimeout(() => {
244
+ timedOut = true;
245
+ child.kill('SIGTERM');
246
+ }, opts.timeoutMs)
247
+ : null;
248
+ child.stdout.on('data', (chunk) => { opts.onStdout(chunk); });
249
+ child.stderr.on('data', (chunk) => {
250
+ stderr.push(chunk);
251
+ opts.onStderr?.(chunk);
252
+ });
253
+ child.stdin.on('error', () => { });
254
+ child.stdin.end(opts.input ?? '');
255
+ opts.signal?.addEventListener('abort', abort, { once: true });
256
+ child.on('error', (err) => {
257
+ stderr.push(Buffer.from(err.message));
258
+ finish(null);
259
+ });
260
+ child.on('close', (code) => {
261
+ finish(aborted ? null : code);
262
+ });
263
+ });
264
+ }
208
265
  /** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
209
266
  export function sshReachable(target, timeoutMs = 10000) {
210
267
  return sshExec(target, 'true', { timeoutMs, multiplex: true }).code === 0;
@@ -8,6 +8,30 @@ import * as path from 'path';
8
8
  import { capableAgents } from '../../capabilities.js';
9
9
  import { resolveHookSource } from '../writers/sources.js';
10
10
  import { lazyAgentMap } from '../writers/lazy-map.js';
11
+ function hookSourcesMatch(src, dest) {
12
+ const srcStat = fs.lstatSync(src);
13
+ if (srcStat.isSymbolicLink())
14
+ return false;
15
+ if (srcStat.isDirectory()) {
16
+ if (!fs.existsSync(dest) || !fs.lstatSync(dest).isDirectory())
17
+ return false;
18
+ const entries = fs.readdirSync(src, { withFileTypes: true });
19
+ for (const entry of entries) {
20
+ if (entry.isSymbolicLink())
21
+ continue;
22
+ const srcPath = path.join(src, entry.name);
23
+ const destPath = path.join(dest, entry.name);
24
+ if (!hookSourcesMatch(srcPath, destPath))
25
+ return false;
26
+ }
27
+ return true;
28
+ }
29
+ if (!srcStat.isFile())
30
+ return false;
31
+ if (!fs.existsSync(dest) || !fs.lstatSync(dest).isFile())
32
+ return false;
33
+ return fs.readFileSync(src, 'utf-8') === fs.readFileSync(dest, 'utf-8');
34
+ }
11
35
  function buildHooksDetector(agent) {
12
36
  return {
13
37
  kind: 'hooks',
@@ -26,7 +50,7 @@ function buildHooksDetector(agent) {
26
50
  continue;
27
51
  }
28
52
  try {
29
- if (fs.readFileSync(src, 'utf-8') === fs.readFileSync(path.join(hooksDir, hook), 'utf-8')) {
53
+ if (hookSourcesMatch(src, path.join(hooksDir, hook))) {
30
54
  synced.push(hook);
31
55
  }
32
56
  }
@@ -288,6 +288,69 @@ function buildOpenClawDetector() {
288
288
  },
289
289
  };
290
290
  }
291
+ function buildCopilotDetector() {
292
+ return {
293
+ kind: 'permissions',
294
+ agent: 'copilot',
295
+ list({ versionHome, cwd }) {
296
+ const configPath = path.join(versionHome, '.copilot', 'permissions-config.json');
297
+ if (!fs.existsSync(configPath))
298
+ return [];
299
+ try {
300
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
301
+ const locations = config.locations && typeof config.locations === 'object' && !Array.isArray(config.locations)
302
+ ? config.locations
303
+ : {};
304
+ const location = locations[path.resolve(cwd)];
305
+ const hasApprovals = Array.isArray(location?.tool_approvals) && location.tool_approvals.length > 0;
306
+ const hasDirectories = Array.isArray(location?.allowed_directories) && location.allowed_directories.length > 0;
307
+ if (hasApprovals || hasDirectories)
308
+ return discoverPermissionGroups().map(g => g.name);
309
+ }
310
+ catch { /* parse fail */ }
311
+ return [];
312
+ },
313
+ };
314
+ }
315
+ function buildForgeDetector() {
316
+ return {
317
+ kind: 'permissions',
318
+ agent: 'forge',
319
+ list({ versionHome }) {
320
+ const permissionsPath = path.join(versionHome, '.forge', 'permissions.yaml');
321
+ if (!fs.existsSync(permissionsPath))
322
+ return [];
323
+ try {
324
+ const config = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
325
+ if (config && Array.isArray(config.policies) && config.policies.length > 0) {
326
+ return discoverPermissionGroups().map(g => g.name);
327
+ }
328
+ }
329
+ catch { /* parse fail */ }
330
+ return [];
331
+ },
332
+ };
333
+ }
334
+ function buildHermesDetector() {
335
+ return {
336
+ kind: 'permissions',
337
+ agent: 'hermes',
338
+ list({ versionHome }) {
339
+ const configPath = path.join(versionHome, '.hermes', 'config.yaml');
340
+ if (!fs.existsSync(configPath))
341
+ return [];
342
+ try {
343
+ const config = yaml.parse(fs.readFileSync(configPath, 'utf-8'));
344
+ const hasAllow = Array.isArray(config?.command_allowlist) && config.command_allowlist.length > 0;
345
+ const hasDeny = Array.isArray(config?.approvals?.deny) && config.approvals.deny.length > 0;
346
+ if (hasAllow || hasDeny)
347
+ return discoverPermissionGroups().map(g => g.name);
348
+ }
349
+ catch { /* parse fail */ }
350
+ return [];
351
+ },
352
+ };
353
+ }
291
354
  const handlers = {
292
355
  claude: buildClaudeDetector,
293
356
  codex: buildCodexDetector,
@@ -301,6 +364,9 @@ const handlers = {
301
364
  droid: buildDroidDetector,
302
365
  kiro: buildKiroDetector,
303
366
  openclaw: buildOpenClawDetector,
367
+ copilot: buildCopilotDetector,
368
+ forge: buildForgeDetector,
369
+ hermes: buildHermesDetector,
304
370
  };
305
371
  export const permissionsDetectors = lazyAgentMap(() => {
306
372
  const m = {};
@@ -73,6 +73,26 @@ function buildWorkflowsDetector(agent) {
73
73
  .filter(d => d.isFile() && d.name.endsWith('.yaml') && !d.name.startsWith('.'))
74
74
  .map(d => d.name.slice(0, -'.yaml'.length));
75
75
  }
76
+ if (agent === 'openclaw') {
77
+ const dir = path.join(versionHome, '.openclaw', 'workflows');
78
+ if (!fs.existsSync(dir))
79
+ return [];
80
+ return fs.readdirSync(dir, { withFileTypes: true })
81
+ .filter(d => d.isFile() && d.name.endsWith('.lobster') && !d.name.startsWith('.'))
82
+ .filter(d => {
83
+ try {
84
+ const parsed = yaml.parse(fs.readFileSync(path.join(dir, d.name), 'utf-8'));
85
+ const env = parsed && typeof parsed === 'object' && parsed.env && typeof parsed.env === 'object' && !Array.isArray(parsed.env)
86
+ ? parsed.env
87
+ : {};
88
+ return env.AGENTS_CLI_WORKFLOW === d.name.slice(0, -'.lobster'.length);
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ })
94
+ .map(d => d.name.slice(0, -'.lobster'.length));
95
+ }
76
96
  const workflowsDir = path.join(versionHome, 'workflows');
77
97
  if (!fs.existsSync(workflowsDir))
78
98
  return [];
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Hooks writer — copies trusted hook script files into `{agentDir}/hooks/`.
2
+ * Hooks writer — copies trusted hook sources into `{agentDir}/hooks/`.
3
3
  * Caller filters by `supports(agent, 'hooks', version)` before invoking.
4
4
  * Orphan sweep (delete hooks in version-home that aren't in `availableNames`)
5
5
  * stays in the orchestrator since it depends on the broader available set.
@@ -12,6 +12,60 @@ import { safeJoin } from '../../paths.js';
12
12
  import { registerHooksToSettings } from '../../hooks.js';
13
13
  import { resolveHookSource } from './sources.js';
14
14
  import { lazyAgentMap } from './lazy-map.js';
15
+ function removePath(target) {
16
+ try {
17
+ const stat = fs.lstatSync(target);
18
+ if (stat.isSymbolicLink() || stat.isFile()) {
19
+ fs.unlinkSync(target);
20
+ }
21
+ else if (stat.isDirectory()) {
22
+ fs.rmSync(target, { recursive: true, force: true });
23
+ }
24
+ }
25
+ catch {
26
+ /* already gone */
27
+ }
28
+ }
29
+ function copyDir(src, dest) {
30
+ fs.mkdirSync(dest, { recursive: true });
31
+ const entries = fs.readdirSync(src, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ if (entry.isSymbolicLink())
34
+ continue;
35
+ const srcPath = safeJoin(src, entry.name);
36
+ const destPath = safeJoin(dest, entry.name);
37
+ if (entry.isDirectory()) {
38
+ copyDir(srcPath, destPath);
39
+ }
40
+ else if (entry.isFile()) {
41
+ fs.copyFileSync(srcPath, destPath);
42
+ fs.chmodSync(destPath, fs.statSync(srcPath).mode);
43
+ }
44
+ }
45
+ }
46
+ function copyHookSource(src, dest) {
47
+ let stat;
48
+ try {
49
+ stat = fs.lstatSync(src);
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ if (stat.isSymbolicLink())
55
+ return false;
56
+ removePath(dest);
57
+ if (stat.isDirectory()) {
58
+ copyDir(src, dest);
59
+ return true;
60
+ }
61
+ if (stat.isFile()) {
62
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
63
+ fs.copyFileSync(src, dest);
64
+ fs.chmodSync(dest, 0o755);
65
+ return true;
66
+ }
67
+ return false;
68
+ }
15
69
  function buildHooksWriter(agent) {
16
70
  return {
17
71
  kind: 'hooks',
@@ -26,9 +80,9 @@ function buildHooksWriter(agent) {
26
80
  if (!srcFile)
27
81
  continue;
28
82
  const destFile = safeJoin(hooksTarget, hook);
29
- fs.copyFileSync(srcFile, destFile);
30
- fs.chmodSync(destFile, 0o755);
31
- synced.push(hook);
83
+ if (copyHookSource(srcFile, destFile)) {
84
+ synced.push(hook);
85
+ }
32
86
  }
33
87
  // Native hook registration in settings.json/hooks.json. Grok is included
34
88
  // so subrule-bundled guards (absolute paths outside the central hooks/
@@ -5,7 +5,7 @@ function buildPermissionsWriter(agent) {
5
5
  return {
6
6
  kind: 'permissions',
7
7
  agent,
8
- write({ versionHome, selection }) {
8
+ write({ versionHome, selection, cwd }) {
9
9
  if (selection.length === 0)
10
10
  return { synced: [] };
11
11
  const built = buildPermissionsFromGroups(selection);
@@ -13,7 +13,7 @@ function buildPermissionsWriter(agent) {
13
13
  const hasDeny = (built.deny?.length ?? 0) > 0;
14
14
  if (!hasAllow && !hasDeny)
15
15
  return { synced: [] };
16
- const r = applyPermsToVersion(agent, built, versionHome, true);
16
+ const r = applyPermsToVersion(agent, built, versionHome, true, cwd);
17
17
  return { synced: r.success ? selection : [] };
18
18
  },
19
19
  };
@@ -56,7 +56,7 @@ function buildSkillsWriter(agent) {
56
56
  fs.mkdirSync(skillsTarget, { recursive: true });
57
57
  const synced = [];
58
58
  for (const skill of selection) {
59
- const srcDir = resolveSkillSource(skill);
59
+ const srcDir = resolveSkillSource(skill, { agent });
60
60
  if (!srcDir)
61
61
  continue;
62
62
  const destDir = safeJoin(skillsTarget, skill);
@@ -1,3 +1,4 @@
1
+ import type { AgentId } from '../../types.js';
1
2
  export type EnabledExtra = {
2
3
  alias: string;
3
4
  dir: string;
@@ -9,7 +10,15 @@ export declare function trustedSourceBases(): {
9
10
  /** Find the trusted source for a command markdown by name. */
10
11
  export declare function resolveCommandSource(name: string): string | null;
11
12
  /** Find the trusted source directory for a skill by name. */
12
- export declare function resolveSkillSource(name: string): string | null;
13
+ export declare function resolveSkillSource(name: string, options?: {
14
+ agent?: AgentId;
15
+ plugins?: Set<string>;
16
+ }): string | null;
17
+ /** List trusted plugin-bundled skill names, filtered to an optional plugin/agent scope. */
18
+ export declare function listPluginSkillNames(options?: {
19
+ agent?: AgentId;
20
+ plugins?: Set<string>;
21
+ }): string[];
13
22
  /** Find the trusted source file for a hook by name. */
14
23
  export declare function resolveHookSource(name: string): string | null;
15
24
  /** All trusted command-skill source roots, used to dedup name collisions for commands-as-skills writes. */