@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
@@ -0,0 +1,34 @@
1
+ /**
2
+ * OpenClaw Telegram provider — the mac-mini path.
3
+ *
4
+ * A DISTINCT provider from the rush-backed `telegram` (not a fallback): it
5
+ * reuses the existing openclaw argv builder (lib/notify.ts). Hosts that have
6
+ * openclaw on PATH (mac-mini) can map `transports.telegram: openclaw-telegram`;
7
+ * hosts with a live rush daemon (zion) map `transports.telegram: telegram`.
8
+ */
9
+ import { execFile } from 'child_process';
10
+ import { promisify } from 'util';
11
+ import { buildOpenClawNotifyArgs } from '../../notify.js';
12
+ const execFileAsync = promisify(execFile);
13
+ export const openclawTelegramProvider = {
14
+ name: 'openclaw-telegram',
15
+ async send(text, opts) {
16
+ const name = 'openclaw-telegram';
17
+ if (opts.dryRun) {
18
+ return { ok: true, channel: name, id: opts.target };
19
+ }
20
+ try {
21
+ await execFileAsync('which', ['openclaw']);
22
+ }
23
+ catch {
24
+ return { ok: false, channel: name, id: opts.target, error: 'openclaw CLI not found on PATH' };
25
+ }
26
+ try {
27
+ await execFileAsync('openclaw', buildOpenClawNotifyArgs(text, { channel: 'telegram', target: opts.target }));
28
+ return { ok: true, channel: name, id: opts.target };
29
+ }
30
+ catch (err) {
31
+ return { ok: false, channel: name, id: opts.target, error: err.message };
32
+ }
33
+ },
34
+ };
@@ -0,0 +1,6 @@
1
+ import type { ChannelProvider, SendOptions } from '../registry.js';
2
+ export type RushChannel = 'telegram' | 'imessage' | 'slack' | 'discord';
3
+ export declare const RUSH_CHANNELS: RushChannel[];
4
+ /** Build the `rush send` argv (exported for tests). */
5
+ export declare function buildRushSendArgs(channel: RushChannel, text: string, opts: SendOptions): string[];
6
+ export declare const rushProviders: ChannelProvider[];
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Rush-daemon channel providers — telegram / imessage / slack / discord.
3
+ *
4
+ * These shell out to the already-built `rush send` CLI, which routes through the
5
+ * rush daemon's live channel gateways over ~/.rush/daemon.sock. We do NOT import
6
+ * rush's Go internals (different repo, internal package) — the CLI boundary is
7
+ * the contract. `rush send --json` prints {"ok":true,"channel":..,"id":..}.
8
+ */
9
+ import { execFile } from 'child_process';
10
+ import { promisify } from 'util';
11
+ const execFileAsync = promisify(execFile);
12
+ export const RUSH_CHANNELS = ['telegram', 'imessage', 'slack', 'discord'];
13
+ /** Build the `rush send` argv (exported for tests). */
14
+ export function buildRushSendArgs(channel, text, opts) {
15
+ const args = ['send', text, '--channel', channel, '--id', opts.target, '--json'];
16
+ if (opts.thread)
17
+ args.push('--thread', opts.thread);
18
+ for (const a of opts.attachments ?? [])
19
+ args.push('--attachment', a);
20
+ return args;
21
+ }
22
+ function rushProvider(channel) {
23
+ return {
24
+ name: channel,
25
+ async send(text, opts) {
26
+ if (opts.dryRun) {
27
+ return { ok: true, channel, id: opts.target, attachments: opts.attachments };
28
+ }
29
+ // Preflight: rush must be on PATH (mirrors notify.ts's openclaw check).
30
+ try {
31
+ await execFileAsync('which', ['rush']);
32
+ }
33
+ catch {
34
+ return { ok: false, channel, id: opts.target, error: 'rush CLI not found on PATH' };
35
+ }
36
+ try {
37
+ const { stdout } = await execFileAsync('rush', buildRushSendArgs(channel, text, opts));
38
+ const parsed = JSON.parse(stdout);
39
+ return {
40
+ ok: parsed.ok === true,
41
+ channel,
42
+ id: opts.target,
43
+ attachments: parsed.attachments,
44
+ error: parsed.ok === true ? undefined : 'rush send returned ok=false',
45
+ };
46
+ }
47
+ catch (err) {
48
+ // Daemon down surfaces here as the exec error (ErrDaemonDown), as does a
49
+ // non-JSON stdout (parse throw). Fail loud — never silently reroute.
50
+ return { ok: false, channel, id: opts.target, error: err.message };
51
+ }
52
+ },
53
+ };
54
+ }
55
+ export const rushProviders = RUSH_CHANNELS.map(rushProvider);
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Channel-provider registry for `agents send` / `agents notify`.
3
+ *
4
+ * One primitive, many channels. A provider knows how to deliver a message over
5
+ * exactly one channel — the agent mailbox, a rush-daemon gateway (telegram /
6
+ * imessage / slack / discord), openclaw, or a future "app". Providers register
7
+ * themselves at module load; the command layer only ever calls
8
+ * `resolveChannelProvider(name).send(...)` — no channel is special-cased above
9
+ * this seam, so new channels (apps) slot in without touching the command.
10
+ */
11
+ export interface SendOptions {
12
+ /** Channel-specific recipient id: a chat id, a mailbox/agent id, a Slack C0…, etc. */
13
+ target: string;
14
+ /** Channel thread id / timestamp (slack/telegram threads). */
15
+ thread?: string;
16
+ /** Absolute paths to file attachments. */
17
+ attachments?: string[];
18
+ /** Sender label (used by the mailbox provider). */
19
+ from?: string;
20
+ /** Resolve + build the delivery but do not actually send. */
21
+ dryRun?: boolean;
22
+ }
23
+ export interface SendResult {
24
+ ok: boolean;
25
+ /** Channel/provider name as surfaced back to the caller. */
26
+ channel: string;
27
+ /** Resolved recipient id, echoed for --json parity with `rush send`. */
28
+ id: string;
29
+ error?: string;
30
+ /** Echoed attachment paths (rush shape parity). */
31
+ attachments?: string[];
32
+ /** Mailbox provider returns the enqueued message id. */
33
+ msgId?: string;
34
+ }
35
+ export interface ChannelProvider {
36
+ /** Stable name used in `--channel` and as a `notify.transports` value. */
37
+ name: string;
38
+ send(text: string, opts: SendOptions): Promise<SendResult>;
39
+ }
40
+ export declare function registerChannelProvider(p: ChannelProvider): void;
41
+ export declare function resolveChannelProvider(name: string): ChannelProvider | undefined;
42
+ export declare function listChannelProviders(): string[];
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Channel-provider registry for `agents send` / `agents notify`.
3
+ *
4
+ * One primitive, many channels. A provider knows how to deliver a message over
5
+ * exactly one channel — the agent mailbox, a rush-daemon gateway (telegram /
6
+ * imessage / slack / discord), openclaw, or a future "app". Providers register
7
+ * themselves at module load; the command layer only ever calls
8
+ * `resolveChannelProvider(name).send(...)` — no channel is special-cased above
9
+ * this seam, so new channels (apps) slot in without touching the command.
10
+ */
11
+ const REGISTRY = new Map();
12
+ export function registerChannelProvider(p) {
13
+ REGISTRY.set(p.name, p);
14
+ }
15
+ export function resolveChannelProvider(name) {
16
+ return REGISTRY.get(name);
17
+ }
18
+ export function listChannelProviders() {
19
+ return [...REGISTRY.keys()].sort();
20
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Transport resolution: map a user-facing channel name to the provider that
3
+ * actually delivers it, using `notify.transports` from agents.yaml.
4
+ *
5
+ * Default is name-identity (`--channel slack` -> `slack` provider) — NOT a
6
+ * fallback to a different transport. Only telegram is dual-homed (rush vs
7
+ * openclaw-telegram); config picks. An unmapped-to-unregistered name dies loud.
8
+ */
9
+ import type { Meta } from '../types.js';
10
+ import { type ChannelProvider } from './registry.js';
11
+ export declare function resolveTransport(channel: string, meta: Meta): ChannelProvider;
@@ -0,0 +1,12 @@
1
+ import { die } from '../format.js';
2
+ import { resolveChannelProvider, listChannelProviders } from './registry.js';
3
+ export function resolveTransport(channel, meta) {
4
+ const providerName = meta.notify?.transports?.[channel] ?? channel;
5
+ const provider = resolveChannelProvider(providerName);
6
+ if (!provider) {
7
+ die(`No channel provider '${providerName}'` +
8
+ (providerName === channel ? '' : ` (mapped from channel '${channel}' via notify.transports)`) +
9
+ `. Registered: ${listChannelProviders().join(', ')}.`);
10
+ }
11
+ return provider;
12
+ }
@@ -110,6 +110,24 @@ export interface InstallResult {
110
110
  /** Set when the install runner threw or exited non-zero. */
111
111
  error?: string;
112
112
  }
113
+ /**
114
+ * Resolve the directory a `binary` install method downloads/extracts into.
115
+ *
116
+ * `/usr/local/bin` used to be hardcoded here, which fails on Apple Silicon
117
+ * Macs where that directory is root-owned and not user-writable (Homebrew
118
+ * moved on to /opt/homebrew but left /usr/local/bin behind). Resolution
119
+ * order:
120
+ * 1. `AGENTS_CLI_BIN_DIR` — explicit override, used as-is when set.
121
+ * 2. `~/.local/bin` — the XDG user-bin dir already used for shims (see
122
+ * posixpath.ts / shims.ts); created with mkdir -p if missing.
123
+ * 3. `/usr/local/bin` — last resort, for hosts where it's still writable.
124
+ * When it isn't either, throw an actionable error naming the env var and
125
+ * ~/.local/bin instead of letting curl/tar fail with a bare EACCES.
126
+ *
127
+ * Called once per install/dry-run so the same directory is used everywhere
128
+ * that method is described or executed.
129
+ */
130
+ export declare function resolveBinDir(): string;
113
131
  /**
114
132
  * Display-only rendering of how a method would be run, for `--dry-run` and
115
133
  * status output. Not used by installCli — execution goes through runInstallMethod
@@ -22,6 +22,7 @@ import { spawnSync } from 'child_process';
22
22
  import * as yaml from 'yaml';
23
23
  import { listResources, resolveResource } from './resources.js';
24
24
  import { composeWin32CommandLine } from './platform/index.js';
25
+ import { localBinDir } from './platform/posixpath.js';
25
26
  // ─── Validation primitives ───────────────────────────────────────────────────
26
27
  /** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
27
28
  const SAFE_CHECK_TOKEN = /^[a-zA-Z0-9_./-]+$/;
@@ -340,6 +341,52 @@ export function describeMethod(method) {
340
341
  const spec = method.binary[key];
341
342
  return spec ? `download ${spec.url}` : 'binary download';
342
343
  }
344
+ /** Env var that overrides where `binary` installs write their downloaded file(s). */
345
+ const BIN_DIR_ENV = 'AGENTS_CLI_BIN_DIR';
346
+ /** Historical default install dir — kept only as a last-resort fallback. */
347
+ const LEGACY_BIN_DIR = '/usr/local/bin';
348
+ function isWritableDir(dir, opts) {
349
+ try {
350
+ if (opts.create)
351
+ fs.mkdirSync(dir, { recursive: true });
352
+ fs.accessSync(dir, fs.constants.W_OK);
353
+ return true;
354
+ }
355
+ catch {
356
+ return false;
357
+ }
358
+ }
359
+ /**
360
+ * Resolve the directory a `binary` install method downloads/extracts into.
361
+ *
362
+ * `/usr/local/bin` used to be hardcoded here, which fails on Apple Silicon
363
+ * Macs where that directory is root-owned and not user-writable (Homebrew
364
+ * moved on to /opt/homebrew but left /usr/local/bin behind). Resolution
365
+ * order:
366
+ * 1. `AGENTS_CLI_BIN_DIR` — explicit override, used as-is when set.
367
+ * 2. `~/.local/bin` — the XDG user-bin dir already used for shims (see
368
+ * posixpath.ts / shims.ts); created with mkdir -p if missing.
369
+ * 3. `/usr/local/bin` — last resort, for hosts where it's still writable.
370
+ * When it isn't either, throw an actionable error naming the env var and
371
+ * ~/.local/bin instead of letting curl/tar fail with a bare EACCES.
372
+ *
373
+ * Called once per install/dry-run so the same directory is used everywhere
374
+ * that method is described or executed.
375
+ */
376
+ export function resolveBinDir() {
377
+ const override = process.env[BIN_DIR_ENV];
378
+ if (override && override.trim())
379
+ return override.trim();
380
+ const local = localBinDir();
381
+ if (isWritableDir(local, { create: true }))
382
+ return local;
383
+ if (isWritableDir(LEGACY_BIN_DIR, { create: false }))
384
+ return LEGACY_BIN_DIR;
385
+ throw new Error(`${LEGACY_BIN_DIR} is not writable (EACCES) and ~/.local/bin could not be created or ` +
386
+ `written to either. Set ${BIN_DIR_ENV} to a writable directory, e.g.:\n` +
387
+ ` export ${BIN_DIR_ENV}="$HOME/.local/bin"\n` +
388
+ `then re-run the install.`);
389
+ }
343
390
  /**
344
391
  * Display-only rendering of how a method would be run, for `--dry-run` and
345
392
  * status output. Not used by installCli — execution goes through runInstallMethod
@@ -359,9 +406,10 @@ export function buildInstallCommand(method) {
359
406
  const spec = method.binary[key];
360
407
  if (!spec)
361
408
  return 'binary download';
409
+ const binDir = resolveBinDir();
362
410
  return spec.extract
363
- ? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C /usr/local/bin ${spec.extract}`
364
- : `curl -fsSL ${spec.url} -o /usr/local/bin/agents-cli-downloaded`;
411
+ ? `curl -fsSL ${spec.url} -o /tmp/agents-cli-bin.tgz && tar -xzf /tmp/agents-cli-bin.tgz -C ${binDir} ${spec.extract}`
412
+ : `curl -fsSL ${spec.url} -o ${path.join(binDir, 'agents-cli-downloaded')}`;
365
413
  }
366
414
  /**
367
415
  * Execute an install method via spawnSync with argv arrays. Each branch
@@ -425,6 +473,7 @@ function runInstallMethod(method) {
425
473
  if (!spec)
426
474
  throw new Error(`no binary declared for ${key}`);
427
475
  assertHttpsUrl(spec.url);
476
+ const binDir = resolveBinDir();
428
477
  if (spec.extract) {
429
478
  assertSafePathSegment(spec.extract);
430
479
  const tmp = path.join(os.tmpdir(), `agents-cli-bin-${process.pid}-${Date.now()}.tgz`);
@@ -433,7 +482,7 @@ function runInstallMethod(method) {
433
482
  if (dl.status !== 0) {
434
483
  throw new Error(`binary download failed (status ${dl.status ?? 'unknown'})`);
435
484
  }
436
- const x = spawnSync('tar', ['-xzf', tmp, '-C', '/usr/local/bin', spec.extract], {
485
+ const x = spawnSync('tar', ['-xzf', tmp, '-C', binDir, spec.extract], {
437
486
  stdio: 'inherit',
438
487
  });
439
488
  if (x.status !== 0) {
@@ -448,7 +497,7 @@ function runInstallMethod(method) {
448
497
  }
449
498
  }
450
499
  else {
451
- const r = spawnSync('curl', ['-fsSL', spec.url, '-o', '/usr/local/bin/agents-cli-downloaded'], { stdio: 'inherit' });
500
+ const r = spawnSync('curl', ['-fsSL', spec.url, '-o', path.join(binDir, 'agents-cli-downloaded')], { stdio: 'inherit' });
452
501
  if (r.status !== 0) {
453
502
  throw new Error(`binary download failed (status ${r.status ?? 'unknown'})`);
454
503
  }
@@ -30,12 +30,15 @@ function codexAvailable() {
30
30
  return findCodexBinary() !== null;
31
31
  }
32
32
  /** Spawn the codex CLI with the given arguments and capture its output. */
33
- function runCodex(args) {
33
+ function runCodex(args, env) {
34
34
  const bin = findCodexBinary();
35
35
  if (!bin)
36
36
  return Promise.resolve({ stdout: '', stderr: 'codex not found', code: 127 });
37
37
  return new Promise((resolve) => {
38
- const proc = spawn(bin, args, { stdio: ['pipe', 'pipe', 'pipe'] });
38
+ const proc = spawn(bin, args, {
39
+ stdio: ['pipe', 'pipe', 'pipe'],
40
+ env: env ? { ...process.env, ...env } : process.env,
41
+ });
39
42
  let stdout = '';
40
43
  let stderr = '';
41
44
  proc.stdout.on('data', (d) => { stdout += d.toString(); });
@@ -120,7 +123,7 @@ export class CodexCloudProvider {
120
123
  if (options.branch)
121
124
  args.push('--branch', options.branch);
122
125
  args.push(options.prompt);
123
- const { stdout, stderr, code } = await runCodex(args);
126
+ const { stdout, stderr, code } = await runCodex(args, options.env);
124
127
  if (code !== 0) {
125
128
  throw new Error(`codex cloud exec failed: ${stderr || stdout}`);
126
129
  }
@@ -53,6 +53,7 @@ export declare function buildSshArgs(computer: string, remoteBin: string, remote
53
53
  droidBin: string;
54
54
  user: string;
55
55
  port?: string;
56
+ env?: Record<string, string>;
56
57
  }): string[];
57
58
  /** Map a droid stream-json `result.subtype` / `is_error` to a CloudTaskStatus. */
58
59
  export declare function mapResultStatus(line: {
@@ -22,6 +22,7 @@
22
22
  import { spawn, execFileSync } from 'child_process';
23
23
  import * as fs from 'fs';
24
24
  import * as path from 'path';
25
+ import { controlOpts, sshConnectOpts } from '../ssh-exec.js';
25
26
  import { MissingTargetError } from './types.js';
26
27
  import { getShimsDir } from '../state.js';
27
28
  const SHIMS_DIR = getShimsDir();
@@ -105,11 +106,16 @@ export function buildExecArgs(prompt, opts) {
105
106
  * shell-quoted into a single remote command string.
106
107
  */
107
108
  export function buildSshArgs(computer, remoteBin, remoteArgv, opts) {
108
- const remoteCmd = [remoteBin, ...remoteArgv].map(shellQuote).join(' ');
109
+ const envPrefix = Object.entries(opts.env ?? {}).map(([key, value]) => {
110
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
111
+ throw new Error(`Invalid runtime env key for Factory dispatch: ${key}`);
112
+ }
113
+ return `${key}=${shellQuote(value)}`;
114
+ });
115
+ const remoteCmd = [...envPrefix, remoteBin, ...remoteArgv].map((part, idx) => idx < envPrefix.length ? part : shellQuote(part)).join(' ');
109
116
  const proxy = `ProxyCommand=${opts.droidBin} computer ssh ${computer} --proxy --port %p`;
110
117
  return [
111
- '-o', proxy,
112
- '-o', 'StrictHostKeyChecking=accept-new',
118
+ ...sshConnectOpts(controlOpts(), ['-o', proxy]),
113
119
  '-p', opts.port ?? '22',
114
120
  `${opts.user}@${computer}`,
115
121
  remoteCmd,
@@ -247,7 +253,7 @@ export class FactoryCloudProvider {
247
253
  const autonomy = resolveAutonomy(options.providerOptions?.autonomy ?? options.providerOptions?.mode, this.defaultAutonomy);
248
254
  const user = options.providerOptions?.user ?? 'droid';
249
255
  const execArgs = buildExecArgs(options.prompt, { autonomy });
250
- const sshArgs = buildSshArgs(computer, 'droid', execArgs, { droidBin, user });
256
+ const sshArgs = buildSshArgs(computer, 'droid', execArgs, { droidBin, user, env: options.env });
251
257
  const { events, status, summary, sessionId } = await this.runRemote(sshArgs);
252
258
  const now = new Date().toISOString();
253
259
  const id = sessionId ?? `droid-${Date.now()}`;
@@ -64,7 +64,7 @@ export declare function accountTokensFingerprint(tokens: AccountTokenEntry[]): s
64
64
  * Build the POST body for /api/v1/cloud-runs. Exported so tests can verify
65
65
  * the back-compat shape (singular fields + repos[]) without needing real
66
66
  * GitHub installations or a live Rush session. `findInstallation` is the
67
- * only other I/O and it's tested by the halo/proxy integration suite.
67
+ * only other I/O and it's tested by the cloud proxy integration suite.
68
68
  */
69
69
  export declare function buildDispatchBody(input: {
70
70
  agent?: string;
@@ -89,6 +89,8 @@ export declare function buildDispatchBody(input: {
89
89
  * MAX_IMAGES_PER_DISPATCH — extras are dropped, never sent. Omitted when empty.
90
90
  */
91
91
  images?: ImageAttachment[] | null;
92
+ /** Runtime env vars mounted into the cloud agent process. */
93
+ env?: Record<string, string> | null;
92
94
  }): Record<string, unknown>;
93
95
  export declare class RushCloudProvider implements CloudProvider {
94
96
  id: "rush";
@@ -255,7 +255,7 @@ export function accountTokensFingerprint(tokens) {
255
255
  * Build the POST body for /api/v1/cloud-runs. Exported so tests can verify
256
256
  * the back-compat shape (singular fields + repos[]) without needing real
257
257
  * GitHub installations or a live Rush session. `findInstallation` is the
258
- * only other I/O and it's tested by the halo/proxy integration suite.
258
+ * only other I/O and it's tested by the cloud proxy integration suite.
259
259
  */
260
260
  export function buildDispatchBody(input) {
261
261
  if (input.resolvedRepos.length === 0) {
@@ -286,6 +286,9 @@ export function buildDispatchBody(input) {
286
286
  if (input.images && input.images.length > 0) {
287
287
  body.images = input.images.slice(0, MAX_IMAGES_PER_DISPATCH);
288
288
  }
289
+ if (input.env && Object.keys(input.env).length > 0) {
290
+ body.env = input.env;
291
+ }
289
292
  return body;
290
293
  }
291
294
  export class RushCloudProvider {
@@ -358,6 +361,7 @@ export class RushCloudProvider {
358
361
  strategy,
359
362
  skills: options.skills,
360
363
  images: options.images,
364
+ env: options.env,
361
365
  });
362
366
  let res = await api('POST', '/api/v1/cloud-runs', token, body);
363
367
  // Server detects drift (new account or rotated token) by comparing the
@@ -402,6 +406,7 @@ export class RushCloudProvider {
402
406
  accountTokens,
403
407
  skills: options.skills,
404
408
  images: options.images,
409
+ env: options.env,
405
410
  });
406
411
  res = await api('POST', '/api/v1/cloud-runs', token, retryBody);
407
412
  // Persist consent on first successful upload so we don't re-prompt
@@ -181,6 +181,8 @@ export interface DispatchOptions {
181
181
  images?: ImageAttachment[];
182
182
  /** Provider-specific options (e.g., codex env ID, factory computer name). */
183
183
  providerOptions?: Record<string, unknown>;
184
+ /** Runtime env vars to inject into the remote agent process when the provider supports it. */
185
+ env?: Record<string, string>;
184
186
  }
185
187
  /**
186
188
  * Collapse `repo` + `repos` into a single deduped list. Exported so callers,
@@ -0,0 +1,35 @@
1
+ /** macOS Unix-domain socket path cap: `sizeof(sockaddr_un.sun_path)` in <sys/un.h>. */
2
+ export declare const SUN_LEN = 104;
3
+ /** Suffix codex appends to CODEX_HOME to reach its app-server control socket. */
4
+ export declare const CODEX_CONTROL_SOCKET_SUFFIX = "/app-server-control/app-server-control.sock";
5
+ /**
6
+ * True when this CODEX_HOME's derived control-socket path would overflow
7
+ * SUN_LEN on macOS.
8
+ */
9
+ export declare function codexHomeOverflowsSunLen(home: string): boolean;
10
+ /**
11
+ * The short, per-version codex home used when the versioned home overflows.
12
+ * `~/.agents/.codex-homes/<version>/.codex` keeps the socket path well under
13
+ * SUN_LEN while staying stable across reboots (unlike $TMPDIR) and per-version
14
+ * isolated.
15
+ */
16
+ export declare function shortCodexHome(agentsUserDir: string, version: string): string;
17
+ /**
18
+ * Resolve a macOS SUN_LEN-safe CODEX_HOME for the given versioned home,
19
+ * migrating the home to a short real path (once, idempotently) when needed.
20
+ *
21
+ * On non-darwin platforms, or when the versioned home already fits, the
22
+ * versioned home is returned unchanged. If migration fails for any reason the
23
+ * versioned home is returned (no worse than the pre-fix behavior).
24
+ */
25
+ export declare function resolveCodexHome(versionedHome: string, agentsUserDir: string, version: string, platform?: NodeJS.Platform): string;
26
+ /**
27
+ * Emit the bash block that a generated codex shim uses to export a
28
+ * SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome}.
29
+ *
30
+ * @param homeExpr shell expression for the versioned codex home
31
+ * (e.g. `$VERSION_DIR/home/.codex`)
32
+ * @param shortBaseExpr shell expression for the per-version short base dir
33
+ * (e.g. `$AGENTS_USER_DIR/.codex-homes/$VERSION`)
34
+ */
35
+ export declare function codexHomeShimBash(homeExpr: string, shortBaseExpr: string): string;
@@ -0,0 +1,136 @@
1
+ /**
2
+ * macOS SUN_LEN-safe CODEX_HOME resolution.
3
+ *
4
+ * Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
5
+ * from CODEX_HOME and, for its app-server daemon, binds a Unix-domain control
6
+ * socket at `$CODEX_HOME/app-server-control/app-server-control.sock`. macOS
7
+ * caps Unix socket paths at 104 bytes (SUN_LEN — `sizeof(sockaddr_un.sun_path)`
8
+ * in `<sys/un.h>`). agents-cli points CODEX_HOME at the deep versioned home
9
+ * (`~/.agents/.history/versions/codex/<version>/home/.codex`), which for a
10
+ * typical user is long enough that the derived socket path exceeds 104 bytes.
11
+ * `codex app-server daemon start` then fails with `path must be shorter than
12
+ * SUN_LEN`, and every codex spawn on macOS dies (this took down all OpenClaw
13
+ * agents on mac-mini — RUSH-1866).
14
+ *
15
+ * Codex exposes no socket-path override and resolves symlinks before binding,
16
+ * so a short symlink pointing at the deep home does NOT help (the socket lives
17
+ * at the resolved target). The only fix is to make the *real* CODEX_HOME short.
18
+ * On macOS, when the versioned home's socket path would overflow, we relocate
19
+ * the home once to a short real directory under `~/.agents/.codex-homes/` and
20
+ * leave a symlink behind at the versioned path, then point CODEX_HOME at the
21
+ * short real path. Config, auth, and state migrate intact; there is no socket
22
+ * or sqlite fragmentation because a single physical home simply moves.
23
+ *
24
+ * This module is the single source of truth for that logic. `exec.ts` calls
25
+ * the TS resolver for `agents run`/`agents exec`; `shims.ts` emits the bash
26
+ * equivalent (`codexHomeShimBash`) into the generated codex shims. Keep the two
27
+ * in lockstep.
28
+ */
29
+ import * as fs from 'fs';
30
+ import * as path from 'path';
31
+ /** macOS Unix-domain socket path cap: `sizeof(sockaddr_un.sun_path)` in <sys/un.h>. */
32
+ export const SUN_LEN = 104;
33
+ /** Suffix codex appends to CODEX_HOME to reach its app-server control socket. */
34
+ export const CODEX_CONTROL_SOCKET_SUFFIX = '/app-server-control/app-server-control.sock';
35
+ /**
36
+ * True when this CODEX_HOME's derived control-socket path would overflow
37
+ * SUN_LEN on macOS.
38
+ */
39
+ export function codexHomeOverflowsSunLen(home) {
40
+ return home.length + CODEX_CONTROL_SOCKET_SUFFIX.length > SUN_LEN;
41
+ }
42
+ /**
43
+ * The short, per-version codex home used when the versioned home overflows.
44
+ * `~/.agents/.codex-homes/<version>/.codex` keeps the socket path well under
45
+ * SUN_LEN while staying stable across reboots (unlike $TMPDIR) and per-version
46
+ * isolated.
47
+ */
48
+ export function shortCodexHome(agentsUserDir, version) {
49
+ return path.join(agentsUserDir, '.codex-homes', version, '.codex');
50
+ }
51
+ /**
52
+ * Resolve a macOS SUN_LEN-safe CODEX_HOME for the given versioned home,
53
+ * migrating the home to a short real path (once, idempotently) when needed.
54
+ *
55
+ * On non-darwin platforms, or when the versioned home already fits, the
56
+ * versioned home is returned unchanged. If migration fails for any reason the
57
+ * versioned home is returned (no worse than the pre-fix behavior).
58
+ */
59
+ export function resolveCodexHome(versionedHome, agentsUserDir, version, platform = process.platform) {
60
+ if (platform !== 'darwin')
61
+ return versionedHome;
62
+ if (!codexHomeOverflowsSunLen(versionedHome))
63
+ return versionedHome;
64
+ const short = shortCodexHome(agentsUserDir, version);
65
+ try {
66
+ if (!fs.existsSync(short)) {
67
+ fs.mkdirSync(path.dirname(short), { recursive: true });
68
+ const st = fs.lstatSync(versionedHome, { throwIfNoEntry: false });
69
+ if (st && st.isDirectory() && !st.isSymbolicLink()) {
70
+ // Migrate the existing deep home so config/auth/state stay intact,
71
+ // then leave a symlink so anything referencing the versioned path
72
+ // still resolves.
73
+ fs.renameSync(versionedHome, short);
74
+ fs.symlinkSync(short, versionedHome);
75
+ }
76
+ else if (!st) {
77
+ // Fresh install: create the short home and link the versioned path to it.
78
+ fs.mkdirSync(short, { recursive: true });
79
+ fs.symlinkSync(short, versionedHome);
80
+ }
81
+ // If versionedHome is already a symlink (migrated by a prior run or the
82
+ // shim), leave it; `short` will be populated below.
83
+ }
84
+ }
85
+ catch {
86
+ // Migration lost a race or hit a permission error. Fall back to the
87
+ // versioned home rather than crash the invocation.
88
+ if (!fs.existsSync(short))
89
+ return versionedHome;
90
+ }
91
+ return fs.existsSync(short) ? short : versionedHome;
92
+ }
93
+ /**
94
+ * Emit the bash block that a generated codex shim uses to export a
95
+ * SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome}.
96
+ *
97
+ * @param homeExpr shell expression for the versioned codex home
98
+ * (e.g. `$VERSION_DIR/home/.codex`)
99
+ * @param shortBaseExpr shell expression for the per-version short base dir
100
+ * (e.g. `$AGENTS_USER_DIR/.codex-homes/$VERSION`)
101
+ */
102
+ export function codexHomeShimBash(homeExpr, shortBaseExpr) {
103
+ return `
104
+ # Codex reads its config (approval_policy, sandbox_mode, MCP servers, rules)
105
+ # from CODEX_HOME and binds a Unix control socket at
106
+ # "\$CODEX_HOME/app-server-control/app-server-control.sock" for its app-server
107
+ # daemon. macOS caps Unix socket paths at 104 bytes (SUN_LEN); the deep
108
+ # versioned home overflows, so "codex app-server daemon start" fails with
109
+ # "path must be shorter than SUN_LEN" and every codex spawn dies (RUSH-1866).
110
+ # Codex has no socket-path override and resolves symlinks before binding, so a
111
+ # short symlink to the deep home does NOT help. On macOS, when the derived
112
+ # socket path would overflow, relocate the home once to a short real dir and
113
+ # leave a symlink behind, then point CODEX_HOME at the short real path. A
114
+ # caller-provided CODEX_HOME always wins.
115
+ if [ -z "\${CODEX_HOME:-}" ]; then
116
+ CODEX_HOME="${homeExpr}"
117
+ # 43 = length of "/app-server-control/app-server-control.sock"; 104 = SUN_LEN.
118
+ if [ "\$(uname -s)" = "Darwin" ] && [ "\$(( \${#CODEX_HOME} + 43 ))" -gt 104 ]; then
119
+ _codex_short="${shortBaseExpr}/.codex"
120
+ if [ ! -e "\$_codex_short" ]; then
121
+ mkdir -p "${shortBaseExpr}"
122
+ if [ -d "\$CODEX_HOME" ] && [ ! -L "\$CODEX_HOME" ]; then
123
+ if mv "\$CODEX_HOME" "\$_codex_short" 2>/dev/null; then
124
+ ln -snf "\$_codex_short" "\$CODEX_HOME"
125
+ fi
126
+ elif [ ! -e "\$CODEX_HOME" ]; then
127
+ mkdir -p "\$_codex_short"
128
+ ln -snf "\$_codex_short" "\$CODEX_HOME"
129
+ fi
130
+ fi
131
+ [ -d "\$_codex_short" ] && CODEX_HOME="\$_codex_short"
132
+ fi
133
+ fi
134
+ export CODEX_HOME="\$CODEX_HOME"
135
+ `;
136
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Move `source` onto `dest` across possibly-different volumes. `renameSync` is
3
+ * atomic but throws EXDEV when `~/.agents` lives on a different filesystem than
4
+ * `$HOME`; fall back to copy-then-remove so the restore still completes. The
5
+ * source is removed only after the copy succeeds, so a mid-copy failure never
6
+ * destroys the sole surviving copy.
7
+ */
8
+ export declare function moveDirCrossDevice(source: string, dest: string): void;
9
+ /**
10
+ * Copy `source` to `dest`, dropping any symlink whose target resolves back into
11
+ * `~/.agents`. Managed resources (skills/commands) are synced into a version home
12
+ * as symlinks into `~/.agents`; copying them verbatim would leave the result full
13
+ * of links that dangle the moment `~/.agents` is disposed. Stripping them yields a
14
+ * clean, self-contained copy — which is the entire point of an export.
15
+ */
16
+ export declare function copyDirStrippingAgentsSymlinks(source: string, dest: string, agentsDir: string): void;