@phnx-labs/agents-cli 1.20.72 → 1.20.73

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 (219) hide show
  1. package/CHANGELOG.md +299 -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 +8 -4
  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/feed.d.ts +13 -0
  16. package/dist/commands/feed.js +130 -29
  17. package/dist/commands/hq.d.ts +2 -0
  18. package/dist/commands/hq.js +58 -0
  19. package/dist/commands/lease.d.ts +19 -0
  20. package/dist/commands/lease.js +138 -11
  21. package/dist/commands/login.d.ts +2 -0
  22. package/dist/commands/login.js +123 -0
  23. package/dist/commands/logs.js +1 -1
  24. package/dist/commands/mine.d.ts +27 -0
  25. package/dist/commands/mine.js +207 -0
  26. package/dist/commands/monitors.js +79 -49
  27. package/dist/commands/plugins.js +1 -0
  28. package/dist/commands/profiles.js +158 -0
  29. package/dist/commands/repo.js +2 -60
  30. package/dist/commands/resources.d.ts +5 -0
  31. package/dist/commands/resources.js +95 -0
  32. package/dist/commands/routines.d.ts +2 -0
  33. package/dist/commands/routines.js +139 -37
  34. package/dist/commands/secrets.js +234 -47
  35. package/dist/commands/send.d.ts +15 -0
  36. package/dist/commands/send.js +62 -0
  37. package/dist/commands/sessions-tail.d.ts +1 -0
  38. package/dist/commands/sessions-tail.js +20 -9
  39. package/dist/commands/sessions.js +18 -3
  40. package/dist/commands/setup-mine.d.ts +18 -0
  41. package/dist/commands/setup-mine.js +106 -0
  42. package/dist/commands/setup-share.js +2 -2
  43. package/dist/commands/setup.js +3 -1
  44. package/dist/commands/share.d.ts +8 -1
  45. package/dist/commands/share.js +122 -37
  46. package/dist/commands/ssh.d.ts +9 -0
  47. package/dist/commands/ssh.js +196 -26
  48. package/dist/commands/sync.js +1 -1
  49. package/dist/commands/teams.js +11 -2
  50. package/dist/index.js +79 -10
  51. package/dist/lib/activity.d.ts +124 -0
  52. package/dist/lib/activity.js +542 -0
  53. package/dist/lib/agents.js +16 -5
  54. package/dist/lib/artifact-actions.d.ts +1 -1
  55. package/dist/lib/artifact-actions.js +1 -1
  56. package/dist/lib/ask-classifier.d.ts +2 -1
  57. package/dist/lib/ask-classifier.js +3 -3
  58. package/dist/lib/auth-health.d.ts +32 -4
  59. package/dist/lib/auth-health.js +40 -1
  60. package/dist/lib/auto-pull.js +8 -1
  61. package/dist/lib/brand.d.ts +40 -0
  62. package/dist/lib/brand.js +122 -0
  63. package/dist/lib/browser/drivers/ssh.js +4 -7
  64. package/dist/lib/browser/service.js +6 -8
  65. package/dist/lib/channels/providers/index.d.ts +2 -0
  66. package/dist/lib/channels/providers/index.js +20 -0
  67. package/dist/lib/channels/providers/mailbox.d.ts +2 -0
  68. package/dist/lib/channels/providers/mailbox.js +26 -0
  69. package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
  70. package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
  71. package/dist/lib/channels/providers/rush.d.ts +6 -0
  72. package/dist/lib/channels/providers/rush.js +55 -0
  73. package/dist/lib/channels/registry.d.ts +42 -0
  74. package/dist/lib/channels/registry.js +20 -0
  75. package/dist/lib/channels/resolve.d.ts +11 -0
  76. package/dist/lib/channels/resolve.js +12 -0
  77. package/dist/lib/cli-resources.d.ts +18 -0
  78. package/dist/lib/cli-resources.js +53 -4
  79. package/dist/lib/cloud/codex.js +6 -3
  80. package/dist/lib/cloud/factory.d.ts +1 -0
  81. package/dist/lib/cloud/factory.js +10 -4
  82. package/dist/lib/cloud/rush.d.ts +3 -1
  83. package/dist/lib/cloud/rush.js +6 -1
  84. package/dist/lib/cloud/types.d.ts +2 -0
  85. package/dist/lib/codex-home.d.ts +35 -0
  86. package/dist/lib/codex-home.js +136 -0
  87. package/dist/lib/crabbox/cli.d.ts +44 -0
  88. package/dist/lib/crabbox/cli.js +120 -20
  89. package/dist/lib/crabbox/lease.d.ts +52 -1
  90. package/dist/lib/crabbox/lease.js +117 -16
  91. package/dist/lib/crabbox/progress.d.ts +31 -0
  92. package/dist/lib/crabbox/progress.js +76 -0
  93. package/dist/lib/crabbox/runtimes.d.ts +4 -0
  94. package/dist/lib/crabbox/runtimes.js +33 -8
  95. package/dist/lib/crabbox/setup-copy.d.ts +87 -0
  96. package/dist/lib/crabbox/setup-copy.js +127 -0
  97. package/dist/lib/daemon.d.ts +5 -5
  98. package/dist/lib/daemon.js +9 -9
  99. package/dist/lib/drive-sync.js +6 -3
  100. package/dist/lib/event-stream.d.ts +36 -0
  101. package/dist/lib/event-stream.js +68 -0
  102. package/dist/lib/events.d.ts +1 -1
  103. package/dist/lib/exec.d.ts +11 -0
  104. package/dist/lib/exec.js +23 -2
  105. package/dist/lib/feed-policy.d.ts +1 -1
  106. package/dist/lib/feed-policy.js +14 -9
  107. package/dist/lib/feed-ranking.d.ts +32 -0
  108. package/dist/lib/feed-ranking.js +224 -0
  109. package/dist/lib/feed.d.ts +35 -2
  110. package/dist/lib/feed.js +66 -3
  111. package/dist/lib/fleet/auth-sync.d.ts +53 -0
  112. package/dist/lib/fleet/auth-sync.js +92 -0
  113. package/dist/lib/fleet/remote-login.d.ts +187 -0
  114. package/dist/lib/fleet/remote-login.js +556 -0
  115. package/dist/lib/format.d.ts +30 -3
  116. package/dist/lib/format.js +34 -5
  117. package/dist/lib/fs-atomic.d.ts +7 -2
  118. package/dist/lib/fs-atomic.js +8 -12
  119. package/dist/lib/git.d.ts +16 -0
  120. package/dist/lib/git.js +79 -2
  121. package/dist/lib/heal.js +11 -3
  122. package/dist/lib/hosts/progress.d.ts +28 -10
  123. package/dist/lib/hosts/progress.js +79 -22
  124. package/dist/lib/hosts/remote-cmd.js +4 -0
  125. package/dist/lib/hq/floor.d.ts +87 -0
  126. package/dist/lib/hq/floor.js +226 -0
  127. package/dist/lib/menubar/install-menubar.js +14 -20
  128. package/dist/lib/notify.d.ts +1 -0
  129. package/dist/lib/notify.js +3 -3
  130. package/dist/lib/open-url.d.ts +2 -0
  131. package/dist/lib/open-url.js +19 -0
  132. package/dist/lib/openclaw-keychain.d.ts +56 -0
  133. package/dist/lib/openclaw-keychain.js +236 -0
  134. package/dist/lib/overdue.js +10 -0
  135. package/dist/lib/permissions.d.ts +44 -1
  136. package/dist/lib/permissions.js +284 -7
  137. package/dist/lib/project-launch.d.ts +6 -12
  138. package/dist/lib/project-launch.js +13 -228
  139. package/dist/lib/project-resources.d.ts +7 -0
  140. package/dist/lib/project-resources.js +291 -0
  141. package/dist/lib/pty-client.d.ts +27 -0
  142. package/dist/lib/pty-client.js +136 -10
  143. package/dist/lib/refresh.js +14 -10
  144. package/dist/lib/resource-profiles.d.ts +26 -0
  145. package/dist/lib/resource-profiles.js +157 -0
  146. package/dist/lib/resources/permissions.js +7 -1
  147. package/dist/lib/resources/types.d.ts +1 -1
  148. package/dist/lib/resources.d.ts +1 -1
  149. package/dist/lib/resources.js +32 -3
  150. package/dist/lib/routines.d.ts +11 -0
  151. package/dist/lib/routines.js +56 -15
  152. package/dist/lib/runner.d.ts +1 -0
  153. package/dist/lib/runner.js +66 -5
  154. package/dist/lib/secrets/bundles.d.ts +16 -3
  155. package/dist/lib/secrets/bundles.js +206 -37
  156. package/dist/lib/secrets/icloud-import.d.ts +2 -2
  157. package/dist/lib/secrets/icloud-import.js +9 -6
  158. package/dist/lib/secrets/mcp.js +1 -1
  159. package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
  160. package/dist/lib/secrets/vault-age-helper.js +34 -0
  161. package/dist/lib/secrets/vault.d.ts +49 -0
  162. package/dist/lib/secrets/vault.js +397 -0
  163. package/dist/lib/self-heal/checks/path.js +3 -1
  164. package/dist/lib/self-heal/checks/shadowing.js +10 -2
  165. package/dist/lib/self-heal/checks/shims.js +19 -11
  166. package/dist/lib/session/cloud.d.ts +2 -2
  167. package/dist/lib/session/cloud.js +2 -2
  168. package/dist/lib/session/db.d.ts +4 -0
  169. package/dist/lib/session/db.js +44 -7
  170. package/dist/lib/session/discover.d.ts +2 -0
  171. package/dist/lib/session/discover.js +114 -3
  172. package/dist/lib/session/stream-render.d.ts +3 -0
  173. package/dist/lib/session/stream-render.js +130 -0
  174. package/dist/lib/session/types.d.ts +6 -0
  175. package/dist/lib/share/analytics.d.ts +13 -0
  176. package/dist/lib/share/analytics.js +45 -0
  177. package/dist/lib/share/config.d.ts +19 -5
  178. package/dist/lib/share/config.js +44 -8
  179. package/dist/lib/share/provision.d.ts +58 -6
  180. package/dist/lib/share/provision.js +97 -22
  181. package/dist/lib/share/publish.d.ts +36 -13
  182. package/dist/lib/share/publish.js +55 -8
  183. package/dist/lib/share/worker-template.js +83 -17
  184. package/dist/lib/shims.d.ts +9 -0
  185. package/dist/lib/shims.js +91 -13
  186. package/dist/lib/ssh-exec.d.ts +14 -0
  187. package/dist/lib/ssh-exec.js +57 -0
  188. package/dist/lib/staleness/detectors/hooks.js +25 -1
  189. package/dist/lib/staleness/detectors/permissions.js +66 -0
  190. package/dist/lib/staleness/detectors/workflows.js +20 -0
  191. package/dist/lib/staleness/writers/hooks.js +58 -4
  192. package/dist/lib/staleness/writers/permissions.js +2 -2
  193. package/dist/lib/staleness/writers/skills.js +1 -1
  194. package/dist/lib/staleness/writers/sources.d.ts +10 -1
  195. package/dist/lib/staleness/writers/sources.js +68 -1
  196. package/dist/lib/star-nudge.d.ts +45 -0
  197. package/dist/lib/star-nudge.js +91 -0
  198. package/dist/lib/startup/command-registry.d.ts +6 -1
  199. package/dist/lib/startup/command-registry.js +17 -3
  200. package/dist/lib/state.d.ts +2 -0
  201. package/dist/lib/state.js +3 -0
  202. package/dist/lib/subagents-registry.d.ts +2 -0
  203. package/dist/lib/subagents-registry.js +3 -0
  204. package/dist/lib/teams/parsers.js +214 -6
  205. package/dist/lib/teams/supervisor.d.ts +7 -0
  206. package/dist/lib/teams/supervisor.js +2 -1
  207. package/dist/lib/template.d.ts +1 -1
  208. package/dist/lib/template.js +1 -1
  209. package/dist/lib/triggers/webhook.js +36 -3
  210. package/dist/lib/types.d.ts +73 -0
  211. package/dist/lib/version.d.ts +40 -0
  212. package/dist/lib/version.js +94 -16
  213. package/dist/lib/versions.d.ts +11 -0
  214. package/dist/lib/versions.js +208 -76
  215. package/dist/lib/workflows.d.ts +2 -0
  216. package/dist/lib/workflows.js +88 -0
  217. package/package.json +2 -1
  218. package/dist/commands/daemon.d.ts +0 -10
  219. 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
@@ -682,6 +678,75 @@ export function createShim(agent) {
682
678
  }
683
679
  return shimPath;
684
680
  }
681
+ // ─── White-label brand shims ──────────────────────────────────────────────────
682
+ // A brand shim is a PURE pass-through: unlike an agent shim (which resolves a
683
+ // version and execs the agent binary) or an alias shim (which injects a fixed
684
+ // subcommand), it forwards argv verbatim to the agents-cli entrypoint with
685
+ // `AGENTS_BRAND` set, so `<brand> <any-verb>` behaves exactly like
686
+ // `agents <any-verb>` but presents under the brand's name. See lib/brand.ts.
687
+ const BRAND_SHIM_MARKER = '# Brand shim:';
688
+ /** The POSIX pass-through shim for a brand. */
689
+ export function generateBrandShim(name) {
690
+ const agentsBin = shellQuote(getAgentsBinForGeneratedShim());
691
+ return `#!/bin/sh
692
+ # Auto-generated by agents-cli - do not edit
693
+ ${BRAND_SHIM_MARKER} ${name} -> agents-cli (white-label)
694
+ AGENTS_BIN=${agentsBin}
695
+ if [ -z "$AGENTS_BIN" ] || [ ! -x "$AGENTS_BIN" ]; then
696
+ echo "${name}: agents-cli entrypoint missing or not executable: $AGENTS_BIN" >&2
697
+ exit 127
698
+ fi
699
+ export AGENTS_BRAND=${name}
700
+ exec "$AGENTS_BIN" "$@"
701
+ `;
702
+ }
703
+ /** Write a brand's pass-through shim(s) onto PATH; returns the shim path. */
704
+ export function createBrandShim(name) {
705
+ ensureAgentsDir();
706
+ const shimsDir = getShimsDir();
707
+ const shimPath = path.join(shimsDir, name);
708
+ const targets = shimTargetsFor(process.platform);
709
+ if (targets.bash) {
710
+ fs.writeFileSync(shimPath, generateBrandShim(name), { mode: 0o755 });
711
+ }
712
+ if (targets.cmd) {
713
+ writeWindowsBrandShim(shimPath + '.cmd', name);
714
+ }
715
+ return shimPath;
716
+ }
717
+ /** Windows `.cmd` pass-through: set AGENTS_BRAND then forward argv to the entrypoint. */
718
+ function writeWindowsBrandShim(cmdPath, name) {
719
+ const indexJs = getAgentsBinForGeneratedShim();
720
+ const content = `@echo off\r\n` +
721
+ `rem Auto-generated by agents-cli - do not edit\r\n` +
722
+ `rem Brand shim: ${name}\r\n` +
723
+ `set AGENTS_BRAND=${name}\r\n` +
724
+ `node "${indexJs}" %*\r\n`;
725
+ fs.writeFileSync(cmdPath, content);
726
+ }
727
+ /** True when the file at the given path is an agents-cli brand shim. */
728
+ export function isBrandShim(filePath) {
729
+ try {
730
+ const head = fs.readFileSync(filePath, 'utf-8').slice(0, 300);
731
+ return head.includes(BRAND_SHIM_MARKER) || head.includes('rem Brand shim:');
732
+ }
733
+ catch {
734
+ return false;
735
+ }
736
+ }
737
+ /** Remove a brand's shim companions. Returns true if anything was removed. */
738
+ export function removeBrandShim(name) {
739
+ const shimsDir = getShimsDir();
740
+ const shimPath = path.join(shimsDir, name);
741
+ let removed = false;
742
+ for (const p of [shimPath, shimPath + '.cmd']) {
743
+ if (fs.existsSync(p) && isBrandShim(p)) {
744
+ fs.unlinkSync(p);
745
+ removed = true;
746
+ }
747
+ }
748
+ return removed;
749
+ }
685
750
  /**
686
751
  * Generate a Windows `.cmd` launcher that delegates to `agents __shim <spec>`.
687
752
  * `spec` is the agent's cliCommand for the default-version shim, or
@@ -829,12 +894,7 @@ export CLAUDE_CONFIG_DIR="$HOME/.agents/.history/versions/${agent}/${version}/ho
829
894
  export DISABLE_AUTOUPDATER="\${DISABLE_AUTOUPDATER:-1}"
830
895
  `
831
896
  : 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
- `
897
+ ? codexHomeShimBash(`$HOME/.agents/.history/versions/${agent}/${version}/home/${configDirName}`, `$HOME/.agents/.codex-homes/${version}`)
838
898
  : agent === 'copilot'
839
899
  ? `
840
900
  # Copilot honors COPILOT_HOME to relocate ~/.copilot (settings, mcp-config.json,
@@ -2368,6 +2428,24 @@ export function listAgentsWithInstalledVersions() {
2368
2428
  .map((entry) => entry.name)
2369
2429
  .filter((agent) => fs.readdirSync(path.join(versionsDir, agent), { withFileTypes: true }).some((entry) => entry.isDirectory()));
2370
2430
  }
2431
+ function isInstalledVersionIsolated(agent, version) {
2432
+ return fs.existsSync(path.join(getVersionsDir(), agent, version, '.isolated'));
2433
+ }
2434
+ export function listAgentsWithNonIsolatedInstalledVersions() {
2435
+ const versionsDir = getVersionsDir();
2436
+ if (!fs.existsSync(versionsDir)) {
2437
+ return [];
2438
+ }
2439
+ const entries = fs.readdirSync(versionsDir, { withFileTypes: true });
2440
+ return entries
2441
+ .filter((entry) => entry.isDirectory() && AGENTS[entry.name])
2442
+ .map((entry) => entry.name)
2443
+ .filter((agent) => {
2444
+ const agentVersionsDir = path.join(versionsDir, agent);
2445
+ return fs.readdirSync(agentVersionsDir, { withFileTypes: true })
2446
+ .some((entry) => entry.isDirectory() && !isInstalledVersionIsolated(agent, entry.name));
2447
+ });
2448
+ }
2371
2449
  /**
2372
2450
  * Create shims for all installed agents.
2373
2451
  */
@@ -2382,7 +2460,7 @@ function ensureAllShims() {
2382
2460
  const agent = entry.name;
2383
2461
  const agentVersionsDir = path.join(versionsDir, agent);
2384
2462
  const versions = fs.readdirSync(agentVersionsDir, { withFileTypes: true })
2385
- .filter((e) => e.isDirectory());
2463
+ .filter((e) => e.isDirectory() && !isInstalledVersionIsolated(agent, e.name));
2386
2464
  if (versions.length > 0 && !shimExists(agent)) {
2387
2465
  createShim(agent);
2388
2466
  }
@@ -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. */
@@ -35,6 +35,50 @@ function isLiveDir(p) {
35
35
  return false;
36
36
  }
37
37
  }
38
+ function readPluginManifest(pluginRoot) {
39
+ const manifestPath = path.join(pluginRoot, '.claude-plugin', 'plugin.json');
40
+ if (!isLiveFile(manifestPath))
41
+ return null;
42
+ try {
43
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
44
+ if (!manifest.name || !manifest.version)
45
+ return null;
46
+ return manifest;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ function pluginSupportsAgent(manifest, agent) {
53
+ if (!agent)
54
+ return true;
55
+ return !manifest.agents || manifest.agents.length === 0 || manifest.agents.includes(agent);
56
+ }
57
+ function pluginSkillDirs(options = {}) {
58
+ const dirs = [];
59
+ for (const base of trustedSourceBases()) {
60
+ const pluginsDir = path.join(base.dir, 'plugins');
61
+ let entries;
62
+ try {
63
+ entries = fs.readdirSync(pluginsDir, { withFileTypes: true });
64
+ }
65
+ catch {
66
+ continue;
67
+ }
68
+ for (const entry of entries) {
69
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
70
+ continue;
71
+ if (options.plugins && !options.plugins.has(entry.name))
72
+ continue;
73
+ const pluginRoot = path.join(pluginsDir, entry.name);
74
+ const manifest = readPluginManifest(pluginRoot);
75
+ if (!manifest || !pluginSupportsAgent(manifest, options.agent))
76
+ continue;
77
+ dirs.push(path.join(pluginRoot, 'skills'));
78
+ }
79
+ }
80
+ return dirs;
81
+ }
38
82
  /** Find the trusted source for a command markdown by name. */
39
83
  export function resolveCommandSource(name) {
40
84
  const candidates = [
@@ -45,14 +89,36 @@ export function resolveCommandSource(name) {
45
89
  return candidates.find(isLiveFile) ?? null;
46
90
  }
47
91
  /** Find the trusted source directory for a skill by name. */
48
- export function resolveSkillSource(name) {
92
+ export function resolveSkillSource(name, options = {}) {
49
93
  const candidates = [
50
94
  safeJoin(path.join(getUserAgentsDir(), 'skills'), name),
51
95
  safeJoin(getSkillsDir(), name),
52
96
  ...getEnabledExtraRepos().map((e) => safeJoin(path.join(e.dir, 'skills'), name)),
97
+ ...pluginSkillDirs(options).map((skillsDir) => safeJoin(skillsDir, name)),
53
98
  ];
54
99
  return candidates.find(isLiveDir) ?? null;
55
100
  }
101
+ /** List trusted plugin-bundled skill names, filtered to an optional plugin/agent scope. */
102
+ export function listPluginSkillNames(options = {}) {
103
+ const names = new Set();
104
+ for (const skillsDir of pluginSkillDirs(options)) {
105
+ let entries;
106
+ try {
107
+ entries = fs.readdirSync(skillsDir, { withFileTypes: true });
108
+ }
109
+ catch {
110
+ continue;
111
+ }
112
+ for (const entry of entries) {
113
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
114
+ continue;
115
+ if (isLiveFile(path.join(skillsDir, entry.name, 'SKILL.md'))) {
116
+ names.add(entry.name);
117
+ }
118
+ }
119
+ }
120
+ return Array.from(names);
121
+ }
56
122
  /** Find the trusted source file for a hook by name. */
57
123
  export function resolveHookSource(name) {
58
124
  const candidates = [
@@ -68,5 +134,6 @@ export function trustedSkillRoots() {
68
134
  path.join(getUserAgentsDir(), 'skills'),
69
135
  getSkillsDir(),
70
136
  ...getEnabledExtraRepos().map((e) => path.join(e.dir, 'skills')),
137
+ ...pluginSkillDirs(),
71
138
  ];
72
139
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * One-time "star us on GitHub" nudge.
3
+ *
4
+ * Printed a single time, ever, after a user's first *successful* headline run
5
+ * (`agents run` / `agents teams`). Modelled on the existing warn-once sentinel
6
+ * pattern (see maybeWarnMultiInstall in src/index.ts): a marker under the
7
+ * regenerable runtime-state dir records that the hint was shown so it never
8
+ * repeats. It is a plain inline line — not a toast, not a nag — and stays out
9
+ * of the way of non-interactive, CI, quiet, and JSON output.
10
+ */
11
+ /** Canonical GitHub repo the nudge points at. */
12
+ export declare const REPO_URL = "https://github.com/phnx-labs/agents-cli";
13
+ /** Has the star nudge already been shown on this machine? */
14
+ export declare function hasShownStarNudge(): boolean;
15
+ /** Inputs to the pure show/skip decision (kept side-effect free for testing). */
16
+ export interface StarNudgeContext {
17
+ /** Caller asked for quiet / JSON output. */
18
+ quiet?: boolean;
19
+ /** stdout is attached to an interactive terminal. */
20
+ isTTY: boolean;
21
+ /** CI is set in the environment. */
22
+ ci: boolean;
23
+ /** User opted out via AGENTS_NO_NUDGE=1. */
24
+ optedOut: boolean;
25
+ /** The one-time sentinel already exists. */
26
+ alreadyShown: boolean;
27
+ }
28
+ /**
29
+ * Pure decision: should the one-time star nudge be shown? Skipped for
30
+ * quiet/JSON output, non-interactive terminals (pipes, redirects), CI, an
31
+ * explicit opt-out, or once it has already been shown.
32
+ */
33
+ export declare function shouldShowStarNudge(ctx: StarNudgeContext): boolean;
34
+ /**
35
+ * Show the one-time star nudge if it hasn't been shown yet and the context is
36
+ * appropriate. Best-effort by contract: never throws, never blocks the run it
37
+ * follows.
38
+ *
39
+ * Skipped when: the caller asked for quiet/JSON output, stdout is not an
40
+ * interactive terminal (pipes, redirects), CI is set, or the user opted out
41
+ * with AGENTS_NO_NUDGE=1.
42
+ */
43
+ export declare function maybeShowStarNudge(opts?: {
44
+ quiet?: boolean;
45
+ }): void;