@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
@@ -158,6 +158,17 @@ export interface ExecOptions {
158
158
  * `--interactive` takes precedence over `--headless`; the CLI layer rejects passing both.
159
159
  */
160
160
  export declare function resolveInteractive(options: Pick<ExecOptions, 'interactive' | 'headless' | 'prompt'>): boolean;
161
+ /**
162
+ * True when a run resolved to *inferred* interactive intent — no prompt and no
163
+ * explicit `--interactive` — but there is no terminal to host the REPL. Launching
164
+ * would attach a TUI to a dead stdin and hang forever, so the caller should fail
165
+ * fast with the headless alternatives instead (RUSH-1829).
166
+ *
167
+ * An explicit `--interactive` is the caller's deliberate choice and is never
168
+ * blocked (they may be driving a PTY we can't detect). Pure — the TTY state is a
169
+ * parameter so this is unit-testable without touching `process.std*`.
170
+ */
171
+ export declare function inferredInteractiveWithoutTty(options: Pick<ExecOptions, 'interactive' | 'headless' | 'prompt'>, isTty: boolean): boolean;
161
172
  /**
162
173
  * Decide whether spawnAgent must capture (PIPE + tee) the child's stdout so the
163
174
  * live budget watcher can parse it (issue #346, FIX 3).
package/dist/lib/exec.js CHANGED
@@ -15,7 +15,8 @@ import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion }
15
15
  import { resolveModel, buildReasoningFlags } from './models.js';
16
16
  import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
17
17
  import { sanitizeProcessEnv } from './secrets/bundles.js';
18
- import { getShimsDir } from './state.js';
18
+ import { getShimsDir, getHistoryDir } from './state.js';
19
+ import { resolveCodexHome } from './codex-home.js';
19
20
  import { readCodexConfiguredModel } from './shims.js';
20
21
  import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
21
22
  import { recordRunName } from './session/run-names.js';
@@ -153,6 +154,21 @@ export function resolveInteractive(options) {
153
154
  return false;
154
155
  return options.prompt === undefined;
155
156
  }
157
+ /**
158
+ * True when a run resolved to *inferred* interactive intent — no prompt and no
159
+ * explicit `--interactive` — but there is no terminal to host the REPL. Launching
160
+ * would attach a TUI to a dead stdin and hang forever, so the caller should fail
161
+ * fast with the headless alternatives instead (RUSH-1829).
162
+ *
163
+ * An explicit `--interactive` is the caller's deliberate choice and is never
164
+ * blocked (they may be driving a PTY we can't detect). Pure — the TTY state is a
165
+ * parameter so this is unit-testable without touching `process.std*`.
166
+ */
167
+ export function inferredInteractiveWithoutTty(options, isTty) {
168
+ if (options.interactive === true)
169
+ return false;
170
+ return resolveInteractive(options) && !isTty;
171
+ }
156
172
  /**
157
173
  * Decide whether spawnAgent must capture (PIPE + tee) the child's stdout so the
158
174
  * live budget watcher can parse it (issue #346, FIX 3).
@@ -241,7 +257,12 @@ export function buildExecEnv(options) {
241
257
  ? resolvedVersion
242
258
  : (resolvedVersion && isVersionInstalled('codex', resolvedVersion) ? resolvedVersion : null);
243
259
  if (version) {
244
- result.CODEX_HOME = path.join(getVersionHomePath('codex', version), '.codex');
260
+ // On macOS the deep versioned home overflows the Unix-socket SUN_LEN
261
+ // limit for codex's app-server control socket; resolve to a short,
262
+ // SUN_LEN-safe home (migrating once if needed). See codex-home.ts.
263
+ const versionedHome = path.join(getVersionHomePath('codex', version), '.codex');
264
+ const agentsUserDir = path.dirname(getHistoryDir());
265
+ result.CODEX_HOME = resolveCodexHome(versionedHome, agentsUserDir, version);
245
266
  }
246
267
  delete result.CLAUDE_CONFIG_DIR;
247
268
  delete result.COPILOT_HOME;
@@ -0,0 +1,72 @@
1
+ import type { AgentId } from './types.js';
2
+ export type ExportMode = 'merge' | 'replace' | 'staged';
3
+ /** Filename of the provenance receipt written into the agent's config dir. */
4
+ export declare const RECEIPT_NAME = ".agents-cli-export.json";
5
+ /** Suffix for the incoming copy of a file the user already has. */
6
+ export declare const CONFLICT_SUFFIX = ".from-agents-cli";
7
+ /** Why an export cannot proceed. Each maps to a distinct user-facing remedy. */
8
+ export type ExportBlocker = {
9
+ kind: 'not-installed';
10
+ } | {
11
+ kind: 'not-isolated';
12
+ } | {
13
+ kind: 'no-config';
14
+ source: string;
15
+ } | {
16
+ kind: 'dest-adopted';
17
+ realPath: string;
18
+ adoptedVersion: string;
19
+ };
20
+ /** What the destination `~/.<agent>` currently is. */
21
+ export type DestKind = 'absent' | 'real-dir' | 'foreign-symlink';
22
+ /** One file the export would place, relative to the config dir root. */
23
+ export interface ExportEntry {
24
+ /** Path relative to the config dir, e.g. `prompts/review.md`. */
25
+ rel: string;
26
+ /** Absolute source file inside the isolated home. */
27
+ source: string;
28
+ /** Absolute path this file will be written to. */
29
+ target: string;
30
+ /**
31
+ * Set when the user already has `rel`. `target` then points at the
32
+ * `.from-agents-cli` sibling, and `existing` is the untouched original.
33
+ */
34
+ existing?: string;
35
+ }
36
+ export interface ExportPlan {
37
+ agent: AgentId;
38
+ version: string;
39
+ mode: ExportMode;
40
+ /** The isolated config dir, e.g. `<versionDir>/home/.codex`. */
41
+ source: string;
42
+ /** The user's real config dir, e.g. `~/.codex`. */
43
+ dest: string;
44
+ destKind: DestKind;
45
+ /** Files with no counterpart in `dest` — written in place. */
46
+ writes: ExportEntry[];
47
+ /** Files the user already has — written alongside, never over. */
48
+ conflicts: ExportEntry[];
49
+ /** replace mode only: where `dest` gets moved first. */
50
+ backupPath: string | null;
51
+ /** staged mode only: the subdirectory receiving the whole tree. */
52
+ stagedPath: string | null;
53
+ receiptPath: string;
54
+ blocker: ExportBlocker | null;
55
+ }
56
+ export interface ExportResult {
57
+ exported: boolean;
58
+ dest: string;
59
+ written: string[];
60
+ conflicts: Array<{
61
+ path: string;
62
+ theirs: string;
63
+ }>;
64
+ backupPath: string | null;
65
+ stagedPath: string | null;
66
+ receiptPath: string | null;
67
+ errors: string[];
68
+ }
69
+ /** Build a read-only plan. Performs no mutations. */
70
+ export declare function planExport(agent: AgentId, version: string, timestamp: number, mode?: ExportMode): ExportPlan;
71
+ /** Execute a plan from {@link planExport}. */
72
+ export declare function executeExport(plan: ExportPlan, timestamp: number): ExportResult;
@@ -0,0 +1,269 @@
1
+ /**
2
+ * `agents export` — copy an ISOLATED install's config back out to the user's real
3
+ * `~/.<agent>`, so they can keep their settings and delete agents-cli entirely.
4
+ *
5
+ * Isolation is currently a one-way door: `agents add <agent>@<v> --isolated` builds a
6
+ * self-contained home under the version dir, and nothing ever brings that work back.
7
+ * Reversibility is what makes a sandbox safe to try.
8
+ *
9
+ * Three modes, because "export" means different things depending on which config you
10
+ * consider authoritative:
11
+ *
12
+ * merge (default) — additive. Copy only paths the user doesn't already have. A
13
+ * collision is NOT silently skipped: the incoming file is written
14
+ * beside theirs as `<name>.from-agents-cli` so they can diff and
15
+ * take the parts they want. Nothing of theirs is ever modified.
16
+ * replace — the isolated config becomes `~/.<agent>`; theirs moves to
17
+ * `backups/<agent>/<ts>`. For when you've been living in the
18
+ * sandbox and want to promote it wholesale.
19
+ * staged — write the whole tree to `~/.<agent>/.agents-export-<ts>/` and
20
+ * activate nothing. For inspecting first.
21
+ *
22
+ * Every mode leaves a receipt at `~/.<agent>/.agents-cli-export.json` recording what
23
+ * came from the export. That is what makes provenance answerable ("which of these
24
+ * files are mine?") and the whole operation reversible.
25
+ *
26
+ * Deliberately NOT implemented: key-level merging of file *contents*. The TOML
27
+ * library here does not preserve comments across parse+stringify, so auto-merging a
28
+ * `config.toml` would silently delete the user's comments. Handing over both files
29
+ * and a diff is honest; rewriting their config behind a "merge" flag is not.
30
+ *
31
+ * Split into a read-only {@link planExport} and a mutating {@link executeExport} so
32
+ * `--dry-run` and the real run share one code path, mirroring `lib/uninstall.ts`.
33
+ */
34
+ import * as fs from 'fs';
35
+ import * as path from 'path';
36
+ import { AGENTS } from './agents.js';
37
+ import { getAgentConfigPath, getConfigSymlinkVersion } from './shims.js';
38
+ import { getVersionHomePath, isVersionIsolated, isVersionInstalled } from './versions.js';
39
+ import { getUserAgentsDir, getBackupsDir } from './state.js';
40
+ import { copyDirStrippingAgentsSymlinks, moveDirCrossDevice } from './config-transfer.js';
41
+ /** Filename of the provenance receipt written into the agent's config dir. */
42
+ export const RECEIPT_NAME = '.agents-cli-export.json';
43
+ /** Suffix for the incoming copy of a file the user already has. */
44
+ export const CONFLICT_SUFFIX = '.from-agents-cli';
45
+ /** The config dir's basename (`.codex`), used to locate it inside a version home. */
46
+ function configBasename(agent) {
47
+ return path.basename(AGENTS[agent].configDir);
48
+ }
49
+ /**
50
+ * Every regular file under `root`, as paths relative to it. Symlinks pointing back
51
+ * into `~/.agents` are omitted for the same reason `copyDirStrippingAgentsSymlinks`
52
+ * drops them: they dangle the moment `~/.agents` is disposed, and an export that
53
+ * leaves dangling links is not an export.
54
+ */
55
+ function walkFiles(root, agentsDir, rel = '') {
56
+ const inside = agentsDir + path.sep;
57
+ const out = [];
58
+ let entries;
59
+ try {
60
+ entries = fs.readdirSync(path.join(root, rel), { withFileTypes: true });
61
+ }
62
+ catch {
63
+ return out;
64
+ }
65
+ for (const entry of entries) {
66
+ const childRel = rel ? path.join(rel, entry.name) : entry.name;
67
+ const abs = path.join(root, childRel);
68
+ if (entry.isSymbolicLink()) {
69
+ try {
70
+ const tgt = path.resolve(path.dirname(abs), fs.readlinkSync(abs));
71
+ if (tgt === agentsDir || tgt.startsWith(inside))
72
+ continue;
73
+ }
74
+ catch {
75
+ continue;
76
+ }
77
+ out.push(childRel);
78
+ continue;
79
+ }
80
+ if (entry.isDirectory()) {
81
+ out.push(...walkFiles(root, agentsDir, childRel));
82
+ continue;
83
+ }
84
+ out.push(childRel);
85
+ }
86
+ return out;
87
+ }
88
+ /** Build a read-only plan. Performs no mutations. */
89
+ export function planExport(agent, version, timestamp, mode = 'merge') {
90
+ const source = path.join(getVersionHomePath(agent, version), configBasename(agent));
91
+ const dest = getAgentConfigPath(agent);
92
+ const base = {
93
+ agent,
94
+ version,
95
+ mode,
96
+ source,
97
+ dest,
98
+ destKind: 'absent',
99
+ writes: [],
100
+ conflicts: [],
101
+ backupPath: null,
102
+ stagedPath: null,
103
+ receiptPath: path.join(dest, RECEIPT_NAME),
104
+ };
105
+ if (!isVersionInstalled(agent, version))
106
+ return { ...base, blocker: { kind: 'not-installed' } };
107
+ if (!isVersionIsolated(agent, version))
108
+ return { ...base, blocker: { kind: 'not-isolated' } };
109
+ if (!fs.existsSync(source))
110
+ return { ...base, blocker: { kind: 'no-config', source } };
111
+ // Refuse when `~/.<agent>` is a symlink agents-cli owns: the agent has a normal
112
+ // install that adopted it, so writing there would land inside a version home
113
+ // rather than in the user's real config, and silently mutate that other install.
114
+ const adopted = getConfigSymlinkVersion(agent);
115
+ if (adopted !== null) {
116
+ return { ...base, blocker: { kind: 'dest-adopted', realPath: dest, adoptedVersion: adopted } };
117
+ }
118
+ let destKind = 'absent';
119
+ try {
120
+ destKind = fs.lstatSync(dest).isSymbolicLink() ? 'foreign-symlink' : 'real-dir';
121
+ }
122
+ catch {
123
+ destKind = 'absent';
124
+ }
125
+ const agentsDir = getUserAgentsDir();
126
+ const rels = walkFiles(source, agentsDir).sort();
127
+ if (mode === 'staged') {
128
+ const stagedPath = path.join(dest, `.agents-export-${timestamp}`);
129
+ return {
130
+ ...base,
131
+ destKind,
132
+ stagedPath,
133
+ writes: rels.map((rel) => ({
134
+ rel,
135
+ source: path.join(source, rel),
136
+ target: path.join(stagedPath, rel),
137
+ })),
138
+ blocker: null,
139
+ };
140
+ }
141
+ if (mode === 'replace') {
142
+ return {
143
+ ...base,
144
+ destKind,
145
+ backupPath: destKind === 'real-dir' ? path.join(getBackupsDir(), agent, String(timestamp)) : null,
146
+ writes: rels.map((rel) => ({
147
+ rel,
148
+ source: path.join(source, rel),
149
+ target: path.join(dest, rel),
150
+ })),
151
+ blocker: null,
152
+ };
153
+ }
154
+ // merge: additive. Anything the user already has becomes a conflict written
155
+ // alongside, so their file is never the thing that changes.
156
+ const writes = [];
157
+ const conflicts = [];
158
+ for (const rel of rels) {
159
+ const target = path.join(dest, rel);
160
+ let exists = false;
161
+ try {
162
+ fs.lstatSync(target);
163
+ exists = true;
164
+ }
165
+ catch {
166
+ exists = false;
167
+ }
168
+ if (exists) {
169
+ conflicts.push({
170
+ rel,
171
+ source: path.join(source, rel),
172
+ target: target + CONFLICT_SUFFIX,
173
+ existing: target,
174
+ });
175
+ }
176
+ else {
177
+ writes.push({ rel, source: path.join(source, rel), target });
178
+ }
179
+ }
180
+ return { ...base, destKind, writes, conflicts, blocker: null };
181
+ }
182
+ /** Copy one file, creating parent dirs. Symlinks are recreated, not followed. */
183
+ function placeFile(entry) {
184
+ fs.mkdirSync(path.dirname(entry.target), { recursive: true });
185
+ const st = fs.lstatSync(entry.source);
186
+ if (st.isSymbolicLink()) {
187
+ const link = fs.readlinkSync(entry.source);
188
+ try {
189
+ fs.unlinkSync(entry.target);
190
+ }
191
+ catch {
192
+ /* nothing there */
193
+ }
194
+ fs.symlinkSync(link, entry.target);
195
+ return;
196
+ }
197
+ fs.copyFileSync(entry.source, entry.target);
198
+ }
199
+ function writeReceipt(plan, result, timestamp) {
200
+ const receipt = {
201
+ exportedAt: new Date(timestamp).toISOString(),
202
+ from: `${plan.agent}@${plan.version} (isolated)`,
203
+ mode: plan.mode,
204
+ written: result.written,
205
+ conflicts: result.conflicts,
206
+ ...(result.backupPath ? { backupPath: result.backupPath } : {}),
207
+ ...(result.stagedPath ? { stagedAt: result.stagedPath } : {}),
208
+ note: plan.mode === 'merge'
209
+ ? `Files under "written" came from this export. Paths under "conflicts" already existed and were NOT modified — the incoming version sits beside yours with the ${CONFLICT_SUFFIX} suffix.`
210
+ : plan.mode === 'replace'
211
+ ? 'This config was replaced wholesale by the export; the previous one is at backupPath.'
212
+ : 'Nothing was activated — the exported tree sits under stagedAt.',
213
+ };
214
+ fs.mkdirSync(path.dirname(plan.receiptPath), { recursive: true });
215
+ fs.writeFileSync(plan.receiptPath, JSON.stringify(receipt, null, 2) + '\n');
216
+ }
217
+ /** Execute a plan from {@link planExport}. */
218
+ export function executeExport(plan, timestamp) {
219
+ const result = {
220
+ exported: false,
221
+ dest: plan.dest,
222
+ written: [],
223
+ conflicts: [],
224
+ backupPath: null,
225
+ stagedPath: null,
226
+ receiptPath: null,
227
+ errors: [],
228
+ };
229
+ if (plan.blocker) {
230
+ result.errors.push(`refusing to export: ${plan.blocker.kind}`);
231
+ return result;
232
+ }
233
+ try {
234
+ if (plan.mode === 'replace') {
235
+ if (plan.backupPath) {
236
+ fs.mkdirSync(path.dirname(plan.backupPath), { recursive: true });
237
+ moveDirCrossDevice(plan.dest, plan.backupPath);
238
+ result.backupPath = plan.backupPath;
239
+ }
240
+ else if (plan.destKind === 'foreign-symlink') {
241
+ // Not ours to back up, but it must go or cpSync would write through it.
242
+ fs.unlinkSync(plan.dest);
243
+ }
244
+ copyDirStrippingAgentsSymlinks(plan.source, plan.dest, getUserAgentsDir());
245
+ result.written = plan.writes.map((w) => w.rel);
246
+ }
247
+ else {
248
+ // merge and staged both place individual files; neither disturbs anything
249
+ // the user already has.
250
+ for (const entry of plan.writes) {
251
+ placeFile(entry);
252
+ result.written.push(entry.rel);
253
+ }
254
+ for (const entry of plan.conflicts) {
255
+ placeFile(entry);
256
+ result.conflicts.push({ path: entry.rel, theirs: entry.target });
257
+ }
258
+ if (plan.stagedPath)
259
+ result.stagedPath = plan.stagedPath;
260
+ }
261
+ writeReceipt(plan, result, timestamp);
262
+ result.receiptPath = plan.receiptPath;
263
+ result.exported = true;
264
+ }
265
+ catch (err) {
266
+ result.errors.push(err instanceof Error ? err.message : String(err));
267
+ }
268
+ return result;
269
+ }
@@ -27,4 +27,4 @@ export interface PolicyResult {
27
27
  * Apply policy to a single open block. Returns the action taken (none/defaulted/parked).
28
28
  * Caller is responsible for persistence/logging side effects not owned by feed.ts.
29
29
  */
30
- export declare function applyPolicyToBlock(block: OpenBlock, policy: FeedPolicy, now: Date, root?: string): PolicyResult;
30
+ export declare function applyPolicyToBlock(block: OpenBlock, policy: FeedPolicy, now: Date, root?: string, mailboxRoot?: string): PolicyResult;
@@ -28,12 +28,12 @@ export const DEFAULT_POLICY = {
28
28
  export function getPolicyPath(root) {
29
29
  return path.join(root ?? getUserAgentsDir(), POLICY_FILE);
30
30
  }
31
- function normalizeClassPolicy(raw) {
31
+ function normalizeClassPolicy(raw, fallback) {
32
32
  const p = (raw ?? {});
33
- const timeout = typeof p.timeoutMinutes === 'number' ? p.timeoutMinutes : DEFAULT_POLICY.approval.timeoutMinutes;
33
+ const timeout = typeof p.timeoutMinutes === 'number' ? p.timeoutMinutes : fallback.timeoutMinutes;
34
34
  return {
35
35
  timeoutMinutes: Math.max(1, Math.round(timeout)),
36
- safeDefault: typeof p.safeDefault === 'string' ? p.safeDefault : undefined,
36
+ safeDefault: typeof p.safeDefault === 'string' ? p.safeDefault : fallback.safeDefault,
37
37
  };
38
38
  }
39
39
  export function loadPolicy(root) {
@@ -45,8 +45,8 @@ export function loadPolicy(root) {
45
45
  const p = parsed;
46
46
  const threshold = p.phoneNotifyThreshold;
47
47
  return {
48
- approval: normalizeClassPolicy(p.approval ?? { timeoutMinutes: DEFAULT_POLICY.approval.timeoutMinutes, safeDefault: DEFAULT_POLICY.approval.safeDefault }),
49
- decision: normalizeClassPolicy(p.decision ?? { timeoutMinutes: DEFAULT_POLICY.decision.timeoutMinutes }),
48
+ approval: normalizeClassPolicy(p.approval, DEFAULT_POLICY.approval),
49
+ decision: normalizeClassPolicy(p.decision, DEFAULT_POLICY.decision),
50
50
  phoneNotifyThreshold: threshold === 'low' || threshold === 'medium' || threshold === 'high' ? threshold : DEFAULT_POLICY.phoneNotifyThreshold,
51
51
  };
52
52
  }
@@ -71,16 +71,21 @@ export function minutesElapsed(block, now) {
71
71
  return 0;
72
72
  return (now.getTime() - ts) / 60_000;
73
73
  }
74
+ function timeoutMinutesForBlock(block, policy) {
75
+ if (typeof block.timeoutMinutes === 'number' && Number.isFinite(block.timeoutMinutes) && block.timeoutMinutes > 0) {
76
+ return Math.max(1, Math.round(block.timeoutMinutes));
77
+ }
78
+ return policy[blockClass(block)].timeoutMinutes;
79
+ }
74
80
  export function isTimedOut(block, policy, now) {
75
- const cls = blockClass(block);
76
81
  const minutes = minutesElapsed(block, now);
77
- return minutes >= policy[cls].timeoutMinutes;
82
+ return minutes >= timeoutMinutesForBlock(block, policy);
78
83
  }
79
84
  /**
80
85
  * Apply policy to a single open block. Returns the action taken (none/defaulted/parked).
81
86
  * Caller is responsible for persistence/logging side effects not owned by feed.ts.
82
87
  */
83
- export function applyPolicyToBlock(block, policy, now, root) {
88
+ export function applyPolicyToBlock(block, policy, now, root, mailboxRoot) {
84
89
  if (block.answer || block.parkedAt || block.defaultedAt) {
85
90
  return { blockId: block.blockId, action: 'none' };
86
91
  }
@@ -97,7 +102,7 @@ export function applyPolicyToBlock(block, policy, now, root) {
97
102
  if (!claim.ok) {
98
103
  return { blockId: block.blockId, action: 'none' };
99
104
  }
100
- const msgId = enqueue(mailboxDir(block.mailboxId, root ?? undefined), {
105
+ const msgId = enqueue(mailboxDir(block.mailboxId, mailboxRoot), {
101
106
  to: block.mailboxId,
102
107
  text: safeDefault,
103
108
  from: 'policy',
@@ -0,0 +1,32 @@
1
+ import type { OpenBlock, FeedAskStats } from './feed.js';
2
+ import type { Classification } from './ask-classifier.js';
3
+ import type { ActiveSession } from './session/active.js';
4
+ import type { SessionMeta } from './session/types.js';
5
+ export interface FeedSessionSignal {
6
+ sessionId?: string;
7
+ mailboxId?: string;
8
+ kind?: string;
9
+ host?: string;
10
+ context?: ActiveSession['context'];
11
+ runtime?: string;
12
+ pid?: number;
13
+ cwd?: string;
14
+ startedAtMs?: number;
15
+ status?: ActiveSession['status'];
16
+ tokPerSec?: number;
17
+ costUsd?: number;
18
+ durationMs?: number;
19
+ cloudProvider?: string;
20
+ cloudTaskId?: string;
21
+ }
22
+ export interface RankedFeedBlock extends OpenBlock {
23
+ delayRank: NonNullable<OpenBlock['delayRank']>;
24
+ }
25
+ export declare function buildSessionSignals(active: ActiveSession[], metas?: SessionMeta[]): FeedSessionSignal[];
26
+ export declare function decisionIrreducibility(c: Classification): number;
27
+ export declare function burnUsdPerHour(signal: FeedSessionSignal | undefined, block: OpenBlock): number;
28
+ export declare function rankFeedBlocks(blocks: OpenBlock[], signals?: FeedSessionSignal[], now?: Date): RankedFeedBlock[];
29
+ export declare function recentAskCount(stats: FeedAskStats, now?: Date): number;
30
+ export declare function needyControlCards(stats: FeedAskStats[], signals?: FeedSessionSignal[], now?: Date, threshold?: number): OpenBlock[];
31
+ export declare function runawayControlCards(signals: FeedSessionSignal[], now?: Date): OpenBlock[];
32
+ export declare function synthesizeControlCards(signals: FeedSessionSignal[], stats: FeedAskStats[], now?: Date): OpenBlock[];