@platless/axom-cli 0.3.13 → 0.5.9

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 (235) hide show
  1. package/dist/axom-patch/cli/README.md +30 -8
  2. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  3. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  6. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  7. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  8. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  9. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  10. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  12. package/dist/axom-patch/cli/nightly-task.md +152 -0
  13. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  14. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  15. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  16. package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
  17. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
  18. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  19. package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
  20. package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
  21. package/dist/axom-patch/cli/qoder-steering.json +15 -15
  22. package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
  23. package/dist/axom-patch/cli/runtime.json +34 -8
  24. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  25. package/dist/axom-patch/cli/zcode-models.json +38 -0
  26. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  27. package/dist/cli.js +33 -6
  28. package/dist/keepers/acp/keeper.mjs +3083 -0
  29. package/dist/keepers/acp/manifest.json +9 -0
  30. package/dist/keepers/terminal/keeper.mjs +8469 -0
  31. package/dist/keepers/terminal/manifest.json +9 -0
  32. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  33. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  34. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  35. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  36. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  37. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  38. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  39. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  40. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  41. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  42. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  52. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  53. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  54. package/dist/test/keeperKit.js +67 -0
  55. package/dist/workstation/acp/acpAgents.js +2163 -611
  56. package/dist/workstation/acp/acpClient.js +60 -497
  57. package/dist/workstation/acp/adoption.js +10 -396
  58. package/dist/workstation/acp/binaryInstall.js +100 -27
  59. package/dist/workstation/acp/builds/gc.js +245 -0
  60. package/dist/workstation/acp/builds/install/binary.js +28 -0
  61. package/dist/workstation/acp/builds/install/npm.js +235 -0
  62. package/dist/workstation/acp/builds/install/uv.js +254 -0
  63. package/dist/workstation/acp/builds/migrate.js +195 -0
  64. package/dist/workstation/acp/builds/model.js +128 -0
  65. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  66. package/dist/workstation/acp/builds/store.js +352 -0
  67. package/dist/workstation/acp/commandTableProbe.js +68 -0
  68. package/dist/workstation/acp/configOptions.js +103 -1
  69. package/dist/workstation/acp/envSecrets.js +27 -0
  70. package/dist/workstation/acp/initializeResponse.js +285 -0
  71. package/dist/workstation/acp/installErrors.js +12 -2
  72. package/dist/workstation/acp/installProbe.js +1 -1
  73. package/dist/workstation/acp/managedPatches.js +76 -160
  74. package/dist/workstation/acp/outbox.js +330 -0
  75. package/dist/workstation/acp/patchAssets.js +20 -3
  76. package/dist/workstation/acp/permission.js +42 -10
  77. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  78. package/dist/workstation/acp/processor/commands.js +35 -0
  79. package/dist/workstation/acp/processor/control.js +355 -0
  80. package/dist/workstation/acp/processor/emit.js +26 -0
  81. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  82. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  83. package/dist/workstation/acp/processor/origins.js +48 -0
  84. package/dist/workstation/acp/processor/output.js +67 -0
  85. package/dist/workstation/acp/processor/projection.js +42 -0
  86. package/dist/workstation/acp/processor/record.js +128 -0
  87. package/dist/workstation/acp/processor/runtime.js +181 -0
  88. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  89. package/dist/workstation/acp/processor/session.js +525 -0
  90. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  91. package/dist/workstation/acp/processor/state.js +74 -0
  92. package/dist/workstation/acp/processor/step.js +459 -0
  93. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  94. package/dist/workstation/acp/processor/test/harness.js +120 -0
  95. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  96. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  97. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  98. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  99. package/dist/workstation/acp/processor/transport/local.js +269 -0
  100. package/dist/workstation/acp/processor/transport/types.js +78 -0
  101. package/dist/workstation/acp/processor/updates.js +364 -0
  102. package/dist/workstation/acp/processorSession.js +1512 -0
  103. package/dist/workstation/acp/qoderStartup.js +1 -1
  104. package/dist/workstation/acp/registry/catalog.js +154 -32
  105. package/dist/workstation/acp/registry/launch.js +61 -119
  106. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  107. package/dist/workstation/acp/steering.js +58 -0
  108. package/dist/workstation/acp/switching.js +114 -0
  109. package/dist/workstation/acp/terminalLogin.js +16 -42
  110. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  111. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  112. package/dist/workstation/acp/test/daemonLife.js +106 -0
  113. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  114. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  115. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  116. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  117. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  118. package/dist/workstation/acp/test/raw/sources.js +249 -0
  119. package/dist/workstation/acp/test/realAdapter.js +377 -0
  120. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  121. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  122. package/dist/workstation/acp/timedCommand.js +4 -2
  123. package/dist/workstation/acp/uvxInstall.js +72 -122
  124. package/dist/workstation/agentFlow.js +103 -0
  125. package/dist/workstation/agentSessions.js +1324 -474
  126. package/dist/workstation/channels/agent.js +7 -5
  127. package/dist/workstation/channels/computer.js +69 -21
  128. package/dist/workstation/channels/computerExecs.js +200 -0
  129. package/dist/workstation/channels/management.js +84 -4
  130. package/dist/workstation/daemon.js +288 -7
  131. package/dist/workstation/daemonctl.js +28 -2
  132. package/dist/workstation/exec/attach.js +126 -0
  133. package/dist/workstation/exec/command.js +40 -0
  134. package/dist/workstation/exec/durable.js +517 -0
  135. package/dist/workstation/exec/fallback.js +106 -0
  136. package/dist/workstation/exec/policy.js +837 -0
  137. package/dist/workstation/exec/shaping.js +211 -0
  138. package/dist/workstation/exec.js +3 -112
  139. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  140. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  141. package/dist/workstation/files/client.js +455 -0
  142. package/dist/workstation/files/errors.js +28 -0
  143. package/dist/workstation/files/fence.js +350 -0
  144. package/dist/workstation/files/find/fileIndex.js +153 -0
  145. package/dist/workstation/files/find/fuzzy.js +155 -0
  146. package/dist/workstation/files/host.js +122 -0
  147. package/dist/workstation/files/hostProtocol.js +2 -0
  148. package/dist/workstation/files/media/cache.js +620 -0
  149. package/dist/workstation/files/media/encoding.js +308 -0
  150. package/dist/workstation/files/media/estimator.js +100 -0
  151. package/dist/workstation/files/media/media.js +325 -0
  152. package/dist/workstation/files/media/packedAudio.js +23 -0
  153. package/dist/workstation/files/media/scheduler.js +551 -0
  154. package/dist/workstation/files/media/types.js +5 -0
  155. package/dist/workstation/files/scope.js +45 -0
  156. package/dist/workstation/files/structured.js +284 -0
  157. package/dist/workstation/{fileOps.js → files/text.js} +117 -38
  158. package/dist/workstation/files/uploads.js +351 -0
  159. package/dist/workstation/journal.js +65 -236
  160. package/dist/workstation/keepers/acp/client.js +101 -0
  161. package/dist/workstation/keepers/acp/envelope.js +62 -0
  162. package/dist/workstation/keepers/acp/host.js +419 -0
  163. package/dist/workstation/keepers/acp/main.js +18 -0
  164. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  165. package/dist/workstation/keepers/acp/params.js +137 -0
  166. package/dist/workstation/keepers/acp/protocol.js +66 -0
  167. package/dist/workstation/keepers/acp/recover.js +120 -0
  168. package/dist/workstation/keepers/acp/session.js +584 -0
  169. package/dist/workstation/keepers/acp/stderr.js +154 -0
  170. package/dist/workstation/keepers/kit/activity.js +103 -0
  171. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  172. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  173. package/dist/workstation/keepers/kit/bundle.js +120 -0
  174. package/dist/workstation/keepers/kit/client.js +432 -0
  175. package/dist/workstation/keepers/kit/identity.js +21 -0
  176. package/dist/workstation/keepers/kit/ipc.js +112 -0
  177. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  178. package/dist/workstation/keepers/kit/launcher.js +249 -0
  179. package/dist/workstation/keepers/kit/lock.js +123 -0
  180. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  181. package/dist/workstation/keepers/kit/paths.js +75 -0
  182. package/dist/workstation/keepers/kit/protocol.js +43 -0
  183. package/dist/workstation/keepers/kit/server.js +518 -0
  184. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  185. package/dist/workstation/keepers/legacy.js +296 -0
  186. package/dist/workstation/keepers/policy.js +576 -0
  187. package/dist/workstation/keepers/stop.js +34 -0
  188. package/dist/workstation/keepers/terminal/client.js +213 -0
  189. package/dist/workstation/keepers/terminal/exec.js +981 -0
  190. package/dist/workstation/keepers/terminal/generations.js +129 -0
  191. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  192. package/dist/workstation/keepers/terminal/main.js +43 -0
  193. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  194. package/dist/workstation/keepers/terminal/params.js +27 -0
  195. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  196. package/dist/workstation/keepers/terminal/pty.js +330 -0
  197. package/dist/workstation/protocol/accountLimits.js +47 -0
  198. package/dist/workstation/protocol/agent.js +48 -4
  199. package/dist/workstation/protocol/common.js +30 -0
  200. package/dist/workstation/protocol/computer.js +15 -4
  201. package/dist/workstation/protocol/management.js +50 -2
  202. package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
  203. package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
  204. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  205. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  206. package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
  207. package/dist/workstation/qoderAdapter/history.js +1 -3
  208. package/dist/workstation/qoderAdapter/main.js +4 -2
  209. package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
  210. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  211. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  212. package/dist/workstation/qoderAdapter/session.js +1 -1
  213. package/dist/workstation/qoderAdapter/subwork.js +15 -7
  214. package/dist/workstation/systemMetrics.js +110 -1
  215. package/dist/workstation/tools/client.js +9 -1
  216. package/dist/workstation/tools/info.js +50 -0
  217. package/dist/workstation/tools/keeperProcess.js +15 -5
  218. package/dist/workstation/tools/router.js +55 -0
  219. package/dist/workstation/tools/terminalRouter.js +222 -0
  220. package/dist/workstation/tools/validation.js +37 -1
  221. package/dist/workstation/update/coordinator.js +3 -0
  222. package/dist/workstation/update/ptyPreflight.js +21 -0
  223. package/dist/workstation/update/taskRegistry.js +105 -11
  224. package/package.json +10 -7
  225. package/dist/workstation/acp/acpSession.js +0 -1475
  226. package/dist/workstation/acp/contract.js +0 -499
  227. package/dist/workstation/acp/draftInterceptor.js +0 -163
  228. package/dist/workstation/acp/sessionScope.js +0 -19
  229. package/dist/workstation/filePaths.js +0 -174
  230. package/dist/workstation/fileWorker.js +0 -81
  231. package/dist/workstation/fileWorkerClient.js +0 -121
  232. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  233. package/dist/workstation/tools/core.js +0 -50
  234. package/dist/workstation/tools/files.js +0 -525
  235. package/dist/workstation/tools/terminal.js +0 -214
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Adapter build GC (spec 2026-10-11 §6.3 "GC", analysis D §2.4; Task 24).
3
+ *
4
+ * A build is collected only when every one of these holds:
5
+ *
6
+ * 1. it is not any agent's `current` build;
7
+ * 2. no adapter process references it — every keeper-acp generation that runs is listed (spawn labels carry `agentId`/`buildId`),
8
+ * and the local fallback's processes too; a keeper-acp that cannot be listed means **no GC at all** (nothing proves the build is
9
+ * unused);
10
+ * 3. the daemon holds no lease on it (an open between resolving and spawning, a switch — both its builds —, a short-lived
11
+ * connection: those count as "seen" until they can no longer run);
12
+ * 4. it was last referenced more than {@link BUILD_GC_GRACE_MS} ago (a build never seen referenced in this daemon's life counts
13
+ * from the daemon's start; the build the pointer left counts from the move);
14
+ * 5. it is not the agent's one rollback build (the `previous` pointer, else the newest other build).
15
+ *
16
+ * Each agent is collected under its agent lock (install / switch / GC are serialised per agent). A record is removed from
17
+ * `builds.json` first, then its tree goes to `.trash/` — and only when no remaining record of any agent shares it (npm version
18
+ * directories can be shared; a version directory goes once nothing of it is recorded).
19
+ *
20
+ * ## What an older CLI left ({@link collectLegacy})
21
+ *
22
+ * The 15c binary migration left a symlink at each old `acp-adapters/<agent>/<version>` path; the pre-build patcher left
23
+ * `acp-adapters/patched-npm/`; a legacy version directory that could not be moved stays where it was. They are removed only when
24
+ * nothing references them: no build record points inside them and no running process's command line names them (the processes an
25
+ * older CLI started) — and a legacy version directory only once its agent has a build of its own (its migration ran). Symlinks are removed with `lstat` + `unlink` — never followed; directories go through `.trash/` (a rename moves
26
+ * the directory entry itself). `~/.npm/_npx` is npm's, never touched.
27
+ */
28
+ import { execFile } from 'node:child_process';
29
+ import { lstat, readdir, rmdir, unlink } from 'node:fs/promises';
30
+ import path from 'node:path';
31
+ import { processCommand } from '../../keepers/kit/lock.js';
32
+ import { RESERVED_ROOT_NAMES } from './migrate.js';
33
+ import { inside } from './model.js';
34
+ /** Spec §14: a build last referenced this long ago may go (one rollback build is kept per agent). */
35
+ export const BUILD_GC_GRACE_MS = 10 * 60_000;
36
+ export const buildKey = (agentId, buildId) => `${agentId}\u0000${buildId}`;
37
+ /** The rollback build GC keeps for an agent: the one its pointer left, else the most recently installed other one. */
38
+ export function rollbackBuildOf(entry) {
39
+ if (entry.previous !== undefined && entry.previous !== entry.current && Object.hasOwn(entry.builds, entry.previous))
40
+ return entry.previous;
41
+ const others = Object.values(entry.builds).filter((build) => build.buildId !== entry.current)
42
+ .sort((a, b) => (a.installedAt < b.installedAt ? 1 : a.installedAt > b.installedAt ? -1 : 0));
43
+ return others[0]?.buildId;
44
+ }
45
+ /** The directory that goes with a removed record once nothing else is recorded inside it: an npm build's whole version directory. */
46
+ function ownedDirOf(build) {
47
+ if (build.kind === 'npm' || build.kind === 'npm-patched') {
48
+ const parts = build.dir.split('/');
49
+ // npm/<sha16>/<version>/(pristine | patched/<digest>)
50
+ if (parts[0] === 'npm' && parts.length >= 4)
51
+ return parts.slice(0, 3).join('/');
52
+ }
53
+ return build.dir;
54
+ }
55
+ const within = (dir, candidate) => candidate === dir || candidate.startsWith(`${dir}/`);
56
+ export async function collectBuilds(inputs) {
57
+ const { store, now } = inputs;
58
+ const grace = inputs.graceMs ?? BUILD_GC_GRACE_MS;
59
+ const log = inputs.log ?? ((line) => console.info(line));
60
+ const report = { removed: [], discarded: [], kept: [], legacyRemoved: [] };
61
+ if (inputs.running === undefined)
62
+ return { ...report, skipped: 'keeper-acp could not be listed' };
63
+ const running = new Set(inputs.running.map((ref) => buildKey(ref.agentId, ref.buildId)));
64
+ const leased = new Set(inputs.leased.map((ref) => buildKey(ref.agentId, ref.buildId)));
65
+ const seen = (key) => inputs.lastSeen.get(key) ?? inputs.seenByDefault;
66
+ for (const agentId of Object.keys((await store.read()).agents)) {
67
+ await store.withAgentLock(agentId, async () => {
68
+ const entry = (await store.read()).agents[agentId];
69
+ if (!entry)
70
+ return;
71
+ const rollback = rollbackBuildOf(entry);
72
+ const collectable = [];
73
+ for (const build of Object.values(entry.builds)) {
74
+ const key = buildKey(agentId, build.buildId);
75
+ // The build the pointer left was referenced (as current) until the move.
76
+ if (build.buildId === entry.previous && entry.currentSetAt) {
77
+ const movedAt = Date.parse(entry.currentSetAt);
78
+ if (Number.isFinite(movedAt) && movedAt > seen(key))
79
+ inputs.lastSeen.set(key, movedAt);
80
+ }
81
+ let why;
82
+ if (build.buildId === entry.current)
83
+ why = 'current';
84
+ else if (running.has(key))
85
+ why = 'running';
86
+ else if (leased.has(key))
87
+ why = 'leased';
88
+ if (why !== undefined) {
89
+ inputs.lastSeen.set(key, Math.max(now, seen(key)));
90
+ report.kept.push({ agentId, buildId: build.buildId, why });
91
+ continue;
92
+ }
93
+ if (now - seen(key) < grace) {
94
+ report.kept.push({ agentId, buildId: build.buildId, why: 'grace' });
95
+ continue;
96
+ }
97
+ if (build.buildId === rollback) {
98
+ report.kept.push({ agentId, buildId: build.buildId, why: 'rollback' });
99
+ continue;
100
+ }
101
+ collectable.push(build.buildId);
102
+ }
103
+ if (collectable.length === 0)
104
+ return;
105
+ const removed = await store.removeBuilds(agentId, collectable);
106
+ const remaining = Object.values((await store.read()).agents).flatMap((agent) => Object.values(agent.builds).map((build) => build.dir));
107
+ for (const build of removed) {
108
+ report.removed.push({ agentId, buildId: build.buildId, dir: build.dir });
109
+ inputs.lastSeen.delete(buildKey(agentId, build.buildId));
110
+ for (const dir of new Set([build.dir, ownedDirOf(build)])) {
111
+ if (remaining.some((other) => within(dir, other) || within(other, dir)))
112
+ continue;
113
+ const discard = () => store.discard(inside(store.root, dir));
114
+ if (build.kind === 'uv')
115
+ await store.withTreeLock(dir, discard);
116
+ else
117
+ await discard();
118
+ report.discarded.push(dir);
119
+ }
120
+ await removeIfEmpty(path.dirname(inside(store.root, build.dir)), store.root);
121
+ }
122
+ log(`[acp-builds] GC: removed ${removed.map((build) => `${agentId} ${build.buildId}`).join(', ')}`);
123
+ });
124
+ }
125
+ return report;
126
+ }
127
+ /** Removes `dir` (and then its empty parents, up to `root`) while it is empty; never recursive. */
128
+ async function removeIfEmpty(dir, root) {
129
+ for (let at = dir; at.startsWith(root + path.sep) && at !== root; at = path.dirname(at)) {
130
+ try {
131
+ await rmdir(at);
132
+ }
133
+ catch {
134
+ return;
135
+ }
136
+ }
137
+ }
138
+ function psCommandLines() {
139
+ return new Promise((resolve) => {
140
+ execFile('ps', ['-A', '-o', 'args='], { maxBuffer: 64 * 1024 * 1024, timeout: 10_000 }, (error, stdout) => {
141
+ resolve(error ? undefined : stdout.split('\n').filter((line) => line.trim() !== ''));
142
+ });
143
+ });
144
+ }
145
+ /**
146
+ * Every running process's command line, for "does anything still run from this path". Linux reads `/proc/<pid>/cmdline` through
147
+ * the kit's `/proc` check (final review minor 11: container images without `ps` never collected legacy leftovers); elsewhere,
148
+ * and on a Linux without a readable `/proc`, `ps -A -o args=`. `undefined` where it cannot be read (Windows, neither source):
149
+ * legacy trees are then left alone.
150
+ */
151
+ export async function processCommandLines(deps = {}) {
152
+ const platform = deps.platform ?? process.platform;
153
+ if (platform === 'win32')
154
+ return undefined;
155
+ if (platform === 'linux') {
156
+ let entries;
157
+ try {
158
+ entries = await (deps.readProc ?? (() => readdir('/proc')))();
159
+ }
160
+ catch {
161
+ entries = undefined;
162
+ }
163
+ const pids = (entries ?? []).filter((entry) => /^\d+$/.test(entry)).map(Number);
164
+ if (pids.length > 0) {
165
+ const lines = [];
166
+ for (const pid of pids) {
167
+ const line = await processCommand(pid, { platform: 'linux', ...(deps.readCmdline ? { readCmdline: deps.readCmdline } : {}) });
168
+ // '': it ended meanwhile (or a kernel thread); null: not ours to read (another user's) — it cannot run from our 0700 tree.
169
+ if (line)
170
+ lines.push(line);
171
+ }
172
+ return lines;
173
+ }
174
+ }
175
+ return (deps.ps ?? psCommandLines)();
176
+ }
177
+ /**
178
+ * What an older CLI left under the `acp-adapters` root (see the file comment): the 15c legacy links, unmigrated legacy version
179
+ * directories, and `patched-npm/` — each removed only when no build record points inside it and no running process names it.
180
+ */
181
+ export async function collectLegacy(options) {
182
+ const { store, commandLines } = options;
183
+ const log = options.log ?? ((line) => console.info(line));
184
+ if (commandLines === undefined)
185
+ return [];
186
+ const root = store.root;
187
+ const document = await store.read();
188
+ const recorded = Object.values(document.agents).flatMap((agent) => Object.values(agent.builds).map((build) => inside(root, build.dir)));
189
+ const referenced = (target) => recorded.some((dir) => dir === target || dir.startsWith(target + path.sep)) || commandLines.some((line) => line.includes(target));
190
+ const removed = [];
191
+ let entries;
192
+ try {
193
+ entries = await readdir(root, { withFileTypes: true });
194
+ }
195
+ catch {
196
+ return [];
197
+ }
198
+ for (const entry of entries) {
199
+ const at = path.join(root, entry.name);
200
+ if (entry.name === 'patched-npm') {
201
+ if (!(await lstat(at)).isDirectory() || referenced(at))
202
+ continue;
203
+ await store.discard(at);
204
+ removed.push(at);
205
+ continue;
206
+ }
207
+ if (entry.name.startsWith('.') || RESERVED_ROOT_NAMES.has(entry.name) || !entry.isDirectory())
208
+ continue;
209
+ // `acp-adapters/<agentId>/`: legacy version directories and the links the migration left in their place.
210
+ let versions;
211
+ try {
212
+ versions = await readdir(at, { withFileTypes: true });
213
+ }
214
+ catch {
215
+ continue;
216
+ }
217
+ // A real legacy version directory is only garbage once the agent has a build of its own (its migration ran, or it was installed
218
+ // anew): before that it may be the only install of an agent whose migration is still to come.
219
+ const migrated = document.agents[entry.name]?.current !== undefined;
220
+ for (const version of versions) {
221
+ const target = path.join(at, version.name);
222
+ const stat = await lstat(target).catch(() => undefined);
223
+ if (!stat || referenced(target))
224
+ continue;
225
+ if (stat.isSymbolicLink()) {
226
+ // Never followed: the link itself goes, whatever it points at.
227
+ await unlink(target).catch(() => undefined);
228
+ }
229
+ else if (stat.isDirectory() && migrated) {
230
+ await store.discard(target);
231
+ }
232
+ else {
233
+ continue;
234
+ }
235
+ removed.push(target);
236
+ }
237
+ try {
238
+ await rmdir(at);
239
+ }
240
+ catch { /* not empty: something is still referenced there */ }
241
+ }
242
+ if (removed.length > 0)
243
+ log(`[acp-builds] GC: removed what an older CLI left: ${removed.map((p) => path.relative(root, p)).join(', ')}`);
244
+ return removed;
245
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Binary-distributed adapters as builds: `binaryInstall.ts` stages and renames the archive into
3
+ * `bin/<agentId>/<version>/`; this turns that tree into a {@link Build} record.
4
+ */
5
+ import { statSync } from 'node:fs';
6
+ import path from 'node:path';
7
+ import { binaryBuildDir, buildIdFor, inside } from '../model.js';
8
+ export function binaryBuild(options) {
9
+ const dir = binaryBuildDir(options.agentId, options.version);
10
+ const entry = path.posix.normalize(options.target.cmd.replace(/\\/g, '/'));
11
+ if (entry.startsWith('../') || path.posix.isAbsolute(entry))
12
+ throw new Error(`Adapter command escapes its install: ${options.target.cmd}`);
13
+ if (!statSync(inside(inside(options.root, dir), entry)).isFile()) {
14
+ throw new Error(`Agent '${options.agentId}' ${options.version} has no '${options.target.cmd}' in its archive`);
15
+ }
16
+ return {
17
+ buildId: buildIdFor(options.version),
18
+ agentId: options.agentId,
19
+ kind: 'binary',
20
+ version: options.version,
21
+ dir,
22
+ launch: { runtime: 'native', entry, args: [...(options.target.args ?? [])], env: { ...options.env } },
23
+ registryVersion: options.registryVersion,
24
+ source: options.source,
25
+ installedBy: options.installedBy,
26
+ installedAt: (options.now ?? new Date()).toISOString(),
27
+ };
28
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * npm-distributed adapters as daemon-owned builds (spec §6.2): `npm install --prefix <staging>` into a
3
+ * fresh staging tree, renamed to `npm/<sha16(name)>/<version>/pristine/`; when this CLI has patches for
4
+ * that version, a copy of pristine is patched into `patched/<digest>/`. Launch then runs the package's
5
+ * `bin` directly with the daemon's own Node — no npx, no network, nothing resolved at open time.
6
+ */
7
+ import { execFile } from 'node:child_process';
8
+ import { existsSync, statSync } from 'node:fs';
9
+ import { open, readFile } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { createPatchedTree, expectedPatchedFiles, patchDigest, selectPackagePatches, verifyPatchedFiles } from '../../managedPatches.js';
12
+ import { assertSafePackageName, assertSafeVersion, buildIdFor, inside, npmPatchedDir, npmPristineDir, } from '../model.js';
13
+ import { assertBuildRunsOn } from '../nodeGate.js';
14
+ import { publishStaged } from '../store.js';
15
+ /**
16
+ * The `npm` this daemon installs adapters with: `AXOM_WORKSTATION_NPM_PATH` when set (the service
17
+ * definition sets it, `workstation/config.ts`), else the one next to its own Node, else whatever `PATH` has.
18
+ */
19
+ export function defaultNpmCommand(nodePath = process.execPath, platform = process.platform, env = process.env) {
20
+ const configured = env.AXOM_WORKSTATION_NPM_PATH?.trim();
21
+ if (configured)
22
+ return configured;
23
+ const sibling = path.join(path.dirname(nodePath), platform === 'win32' ? 'npm.cmd' : 'npm');
24
+ return existsSync(sibling) ? sibling : platform === 'win32' ? 'npm.cmd' : 'npm';
25
+ }
26
+ /** `npm view <name> version --json`: the version npm's `latest` names right now, or `undefined` when npm cannot say. */
27
+ export function runNpmViewLatest(npmCommand, name, timeoutMs) {
28
+ return new Promise((resolve) => {
29
+ execFile(npmCommand, ['view', name, 'version', '--json'], { timeout: timeoutMs, windowsHide: true }, (error, stdout) => {
30
+ if (error) {
31
+ resolve(undefined);
32
+ return;
33
+ }
34
+ try {
35
+ const parsed = JSON.parse(stdout.toString());
36
+ resolve(typeof parsed === 'string' && parsed ? parsed : undefined);
37
+ }
38
+ catch {
39
+ resolve(undefined);
40
+ }
41
+ });
42
+ });
43
+ }
44
+ /** `npm install --prefix <prefix> --no-save --no-audit --no-fund <spec>`, argv only, never a shell. */
45
+ export function runNpmInstall(npmCommand, prefix, spec, timeoutMs) {
46
+ return new Promise((resolve) => {
47
+ execFile(npmCommand, ['install', '--prefix', prefix, '--no-save', '--no-audit', '--no-fund', spec], { timeout: timeoutMs, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, _stdout, stderr) => {
48
+ if (!error) {
49
+ resolve({ ok: true });
50
+ return;
51
+ }
52
+ const raw = stderr.toString();
53
+ const detail = raw.trim().split('\n').slice(-4).join(' ').trim();
54
+ resolve({ ok: false, error: detail ? `${error.message.split('\n')[0]}: ${detail}` : error.message, stderr: raw });
55
+ });
56
+ });
57
+ }
58
+ async function readPackage(packageDir) {
59
+ try {
60
+ return JSON.parse(await readFile(path.join(packageDir, 'package.json'), 'utf8'));
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ /** The installed version of `name` under `<root>/node_modules`, when it is really there. */
67
+ async function installedVersion(treeRoot, name) {
68
+ const metadata = await readPackage(inside(path.join(treeRoot, 'node_modules'), name));
69
+ return metadata?.name === name && typeof metadata.version === 'string' ? metadata.version : undefined;
70
+ }
71
+ /**
72
+ * Make sure `npm/<sha16(name)>/<version>/pristine` exists. `version` undefined = unpinned (npm's
73
+ * `latest`, the install-time ETARGET fallback): the version is read back off what npm installed.
74
+ * Caller holds the agent lock.
75
+ */
76
+ export async function ensurePristine(options) {
77
+ const { store, name } = options;
78
+ assertSafePackageName(name);
79
+ if (options.version === undefined) {
80
+ // Unpinned (the install-time ETARGET fallback): name the version first, so a `latest` that is
81
+ // already installed costs one metadata request and no download.
82
+ const latest = await runNpmViewLatest(options.npmCommand, name, options.timeoutMs);
83
+ let safe = false;
84
+ if (latest !== undefined) {
85
+ try {
86
+ assertSafeVersion(latest);
87
+ safe = true;
88
+ }
89
+ catch { /* an unusable answer: install unpinned and read the version back */ }
90
+ }
91
+ // Only the version check may fall through: a real install failure of `latest` is that failure,
92
+ // not a reason for a second, unpinned install.
93
+ if (safe)
94
+ return ensurePristine({ ...options, version: latest });
95
+ }
96
+ if (options.version !== undefined) {
97
+ const final = inside(store.root, npmPristineDir(name, options.version));
98
+ if ((await installedVersion(final, name)) === options.version)
99
+ return { ok: true, version: options.version, downloaded: false };
100
+ }
101
+ const staged = await store.stagingDir();
102
+ try {
103
+ const spec = options.version === undefined ? name : `${name}@${options.version}`;
104
+ const ran = await runNpmInstall(options.npmCommand, staged, spec, options.timeoutMs);
105
+ if (!ran.ok)
106
+ return ran;
107
+ const version = await installedVersion(staged, name);
108
+ if (!version)
109
+ return { ok: false, error: `npm reported success but ${spec} is not in the installed tree`, stderr: '' };
110
+ if (options.version !== undefined && version !== options.version) {
111
+ return { ok: false, error: `npm installed ${name}@${version} when ${spec} was asked for`, stderr: '' };
112
+ }
113
+ assertSafeVersion(version);
114
+ const { published } = await publishStaged(staged, inside(store.root, npmPristineDir(name, version)));
115
+ // Not published = that version's pristine was already there (we only learned the version now).
116
+ return { ok: true, version, downloaded: published };
117
+ }
118
+ finally {
119
+ // After a successful publish `staged` no longer exists; after a failure it is half-written.
120
+ await store.discard(staged);
121
+ }
122
+ }
123
+ /**
124
+ * The executable an npm package exposes, by npm's own `npm exec` rule: an explicit verified bin (from a
125
+ * patch recipe) when given; otherwise the only bin; otherwise the bin named like the package (scope
126
+ * stripped). Anything else is ambiguous and refused rather than guessed.
127
+ */
128
+ export function selectBin(name, bin, explicit) {
129
+ const unscoped = name.includes('/') ? name.slice(name.indexOf('/') + 1) : name;
130
+ let chosen;
131
+ if (explicit !== undefined)
132
+ chosen = typeof bin === 'object' && bin !== null ? bin[explicit] : undefined;
133
+ else if (typeof bin === 'string')
134
+ chosen = bin;
135
+ else if (typeof bin === 'object' && bin !== null) {
136
+ const entries = Object.entries(bin);
137
+ chosen = entries.length === 1 ? entries[0][1] : bin[unscoped];
138
+ }
139
+ if (typeof chosen !== 'string' || !chosen)
140
+ throw new Error(`Adapter package ${name} has no unambiguous executable`);
141
+ return chosen;
142
+ }
143
+ /** `node` unless the entry is evidently not JavaScript (a native executable, or a script for another interpreter). */
144
+ async function runtimeOf(file) {
145
+ const handle = await open(file, 'r');
146
+ try {
147
+ const head = Buffer.alloc(256);
148
+ const { bytesRead } = await handle.read(head, 0, head.length, 0);
149
+ const bytes = head.subarray(0, bytesRead);
150
+ const magic = bytes.length >= 4 ? bytes.readUInt32BE(0) : 0;
151
+ // ELF, Mach-O (both byte orders, 32/64, fat), PE.
152
+ if ([0x7f454c46, 0xfeedface, 0xfeedfacf, 0xcefaedfe, 0xcffaedfe, 0xcafebabe].includes(magic) || bytes.subarray(0, 2).toString() === 'MZ') {
153
+ return 'native';
154
+ }
155
+ const text = bytes.toString('utf8');
156
+ if (text.startsWith('#!'))
157
+ return /\bnode\b/.test(text.split('\n')[0]) ? 'node' : 'native';
158
+ return 'node';
159
+ }
160
+ finally {
161
+ await handle.close();
162
+ }
163
+ }
164
+ /**
165
+ * The build for `name@version` from its pristine tree: pristine itself when this CLI has no patch for
166
+ * that version, else a patched copy (created once, then reused). Not committed — the caller does
167
+ * that under the agent lock.
168
+ */
169
+ export async function npmBuild(options) {
170
+ const { store, name, version } = options;
171
+ const pristineRel = npmPristineDir(name, version);
172
+ const pristine = inside(store.root, pristineRel);
173
+ const recipes = await selectPackagePatches(name, version, inside(path.join(pristine, 'node_modules'), name), options.recipes);
174
+ let dir = pristineRel;
175
+ let patch = {};
176
+ if (recipes.length) {
177
+ const digest = patchDigest(recipes);
178
+ dir = npmPatchedDir(name, version, digest);
179
+ const final = inside(store.root, dir);
180
+ if (!existsSync(final)) {
181
+ const staged = await store.stagingDir();
182
+ try {
183
+ await createPatchedTree(pristine, staged, name, version, recipes);
184
+ await publishStaged(staged, final);
185
+ }
186
+ finally {
187
+ await store.discard(staged);
188
+ }
189
+ }
190
+ // Whoever published it, the tree's receipt must be for exactly this recipe set.
191
+ const receipt = JSON.parse(await readFile(path.join(final, 'axom-patches.json'), 'utf8'));
192
+ if (receipt.digest !== digest)
193
+ throw new Error('Axom adapter patch receipt does not match this CLI');
194
+ // Expected hashes come from the shipped recipes, never from the writable receipt alone.
195
+ const files = expectedPatchedFiles(recipes);
196
+ await verifyPatchedFiles(inside(path.join(final, 'node_modules'), name), files);
197
+ patch = { patchDigest: digest, patches: recipes.map((recipe) => recipe.id), patchedFiles: files };
198
+ }
199
+ const root = inside(store.root, dir);
200
+ const packageRel = `node_modules/${name}`;
201
+ const metadata = await readPackage(inside(root, packageRel));
202
+ if (metadata?.name !== name || metadata.version !== version)
203
+ throw new Error(`Adapter package identity changed under ${dir}`);
204
+ const explicit = [...new Set(recipes.map((recipe) => recipe.bin).filter((bin) => bin !== undefined))];
205
+ if (explicit.length > 1)
206
+ throw new Error('Axom patches disagree about the adapter executable');
207
+ const entryRel = path.posix.normalize(`${packageRel}/${selectBin(name, metadata.bin, explicit[0])}`);
208
+ const entry = inside(root, entryRel);
209
+ if (!entryRel.startsWith(`${packageRel}/`) || !statSync(entry).isFile())
210
+ throw new Error(`Adapter executable is missing in ${dir}`);
211
+ const launch = {
212
+ runtime: await runtimeOf(entry),
213
+ entry: entryRel,
214
+ args: [...options.args],
215
+ env: { ...options.env },
216
+ pathDirs: ['node_modules/.bin'],
217
+ };
218
+ const build = {
219
+ buildId: buildIdFor(version, patch.patchDigest),
220
+ agentId: options.agentId,
221
+ kind: patch.patchDigest ? 'npm-patched' : 'npm',
222
+ version,
223
+ package: name,
224
+ ...patch,
225
+ dir,
226
+ launch,
227
+ registryVersion: options.registryVersion,
228
+ source: options.source,
229
+ installedBy: options.installedBy,
230
+ installedAt: (options.now ?? new Date()).toISOString(),
231
+ };
232
+ // Never hand back (so never commit, never make current) a build its own Node gate refuses.
233
+ await assertBuildRunsOn(build, store.root, options.nodePath);
234
+ return build;
235
+ }