@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,211 @@
1
+ // Exec result shaping (spec §4.2, §14; analysis E §6.2): the daemon turns an exec's raw stdout/stderr BYTES into the `exec_exit`
2
+ // strings. Per stream: the first 32 KiB and the last 32 KiB, with an omission marker between them when bytes were left out. Bytes
3
+ // are counted, never UTF-16 code units, and UTF-8 is decoded by the daemon with chunk boundaries that never split a character
4
+ // (no U+FFFD that the command did not print). Used by both the durable (keeper) path and the in-process fallback.
5
+ //
6
+ // The state is serialisable for the exec checkpoint: per stream the byte count, the head length, the rolling tail (≤ 32 KiB) and
7
+ // the streaming decoder's held bytes (≤ 3). The head bytes themselves are NOT in the checkpoint: after a daemon restart they are
8
+ // re-read from keeper-terminal's retained head reserve, which survives commits (keepers/terminal/exec.ts `survivesCommit`).
9
+ export const RESULT_HEAD_BYTES = 32 * 1024;
10
+ export const RESULT_TAIL_BYTES = 32 * 1024;
11
+ /** The exact text placed between the head and the tail of a stream whose middle was left out. */
12
+ export function omissionMarker(omittedBytes) {
13
+ return `\n[... ${omittedBytes} bytes omitted ...]\n`;
14
+ }
15
+ const EMPTY = Buffer.alloc(0);
16
+ // ignoreBOM: a leading U+FEFF the command printed is output like any other character. Not fatal: invalid bytes the command
17
+ // really printed become U+FFFD exactly as a one-shot decode of all its bytes would make them.
18
+ const decoder = new TextDecoder('utf-8', { ignoreBOM: true });
19
+ export const decodeUtf8 = (bytes) => (bytes.length === 0 ? '' : decoder.decode(bytes));
20
+ /**
21
+ * Splits `bytes` before a trailing incomplete UTF-8 sequence (a lead byte still waiting for its continuation bytes). Decoding
22
+ * `complete` now and `pending + later bytes` later gives exactly what decoding everything at once gives: the cut is always right
23
+ * before a lead byte, which ends any earlier sequence in a UTF-8 decoder anyway.
24
+ */
25
+ export function splitIncompleteUtf8(bytes) {
26
+ let i = bytes.length - 1;
27
+ let continuation = 0;
28
+ while (i >= 0 && continuation < 3 && (bytes[i] & 0xc0) === 0x80) {
29
+ i -= 1;
30
+ continuation += 1;
31
+ }
32
+ if (i < 0)
33
+ return [bytes, EMPTY];
34
+ const lead = bytes[i];
35
+ const need = lead >= 0xc2 && lead <= 0xdf ? 2 : lead >= 0xe0 && lead <= 0xef ? 3 : lead >= 0xf0 && lead <= 0xf4 ? 4 : 1;
36
+ if (need > 1 && bytes.length - i < need)
37
+ return [bytes.subarray(0, i), bytes.subarray(i)];
38
+ return [bytes, EMPTY];
39
+ }
40
+ /** Number of leading continuation bytes (at most 3): where a tail cut in the middle of a character starts its first whole one. */
41
+ function leadingContinuation(bytes) {
42
+ let n = 0;
43
+ while (n < 3 && n < bytes.length && (bytes[n] & 0xc0) === 0x80)
44
+ n += 1;
45
+ return n;
46
+ }
47
+ /** A streaming UTF-8 decoder whose state (the held bytes of an incomplete character) can be checkpointed. */
48
+ export class Utf8Stream {
49
+ pending = EMPTY;
50
+ constructor(pending) { if (pending)
51
+ this.pending = Buffer.from(pending); }
52
+ push(bytes) {
53
+ const all = this.pending.length > 0 ? Buffer.concat([this.pending, bytes]) : bytes;
54
+ const [complete, pending] = splitIncompleteUtf8(all);
55
+ this.pending = Buffer.from(pending);
56
+ return decodeUtf8(complete);
57
+ }
58
+ /** Decodes whatever is held (an incomplete character becomes U+FFFD) and starts over. */
59
+ flush() { const text = decodeUtf8(this.pending); this.pending = EMPTY; return text; }
60
+ get held() { return this.pending; }
61
+ }
62
+ /** One stream (stdout or stderr) of one exec. */
63
+ export class ShapedStream {
64
+ total = 0;
65
+ head = [];
66
+ headLen = 0;
67
+ tail = [];
68
+ tailLen = 0;
69
+ lost = 0;
70
+ decoder;
71
+ constructor(state, headBytes) {
72
+ if (!state) {
73
+ this.decoder = new Utf8Stream();
74
+ return;
75
+ }
76
+ this.total = state.total;
77
+ // A head that could not be re-read in full stays short: the missing bytes read as omitted, never as wrong bytes.
78
+ const head = (headBytes ?? EMPTY).subarray(0, state.headLen);
79
+ if (head.length > 0) {
80
+ this.head = [Buffer.from(head)];
81
+ this.headLen = head.length;
82
+ }
83
+ const tail = Buffer.from(state.tail, 'base64');
84
+ if (tail.length > 0) {
85
+ this.tail = [tail];
86
+ this.tailLen = tail.length;
87
+ }
88
+ this.lost = state.lost;
89
+ this.decoder = new Utf8Stream(Buffer.from(state.pending, 'base64'));
90
+ }
91
+ get bytes() { return this.total; }
92
+ /** Bytes the streaming decoder holds (an incomplete character waiting for its continuation bytes). */
93
+ get heldBytes() { return this.decoder.held.length; }
94
+ /** Head bytes this stream holds (restore re-reads exactly this many). */
95
+ get headLength() { return this.headLen; }
96
+ /**
97
+ * Adds the chunk that starts at stream offset `so` and returns its streaming text (decoded with held bytes; an incomplete
98
+ * trailing character is held for the next chunk). A chunk past the expected offset means the bytes between were lost (keeper
99
+ * quota): they count as omitted, the tail restarts after them and the decoder flushes what it held. Bytes already seen are skipped.
100
+ */
101
+ push(so, chunk) {
102
+ let text = '';
103
+ if (so < this.total) {
104
+ chunk = chunk.subarray(Math.min(chunk.length, this.total - so));
105
+ so = this.total;
106
+ if (chunk.length === 0)
107
+ return '';
108
+ }
109
+ if (so > this.total) {
110
+ this.lost += so - this.total;
111
+ this.total = so;
112
+ this.tail = [];
113
+ this.tailLen = 0;
114
+ text += this.decoder.flush();
115
+ }
116
+ let rest = chunk;
117
+ // The head takes bytes only while it is contiguous from offset 0.
118
+ if (this.headLen === this.total && this.headLen < RESULT_HEAD_BYTES) {
119
+ const take = rest.subarray(0, RESULT_HEAD_BYTES - this.headLen);
120
+ this.head.push(Buffer.from(take));
121
+ this.headLen += take.length;
122
+ rest = rest.subarray(take.length);
123
+ }
124
+ if (rest.length > 0)
125
+ this.appendTail(rest);
126
+ this.total += chunk.length;
127
+ return text + this.decoder.push(chunk);
128
+ }
129
+ appendTail(bytes) {
130
+ this.tail.push(Buffer.from(bytes.length > RESULT_TAIL_BYTES ? bytes.subarray(bytes.length - RESULT_TAIL_BYTES) : bytes));
131
+ this.tailLen += Math.min(bytes.length, RESULT_TAIL_BYTES);
132
+ while (this.tailLen - this.tail[0].length >= RESULT_TAIL_BYTES)
133
+ this.tailLen -= this.tail.shift().length;
134
+ if (this.tailLen > RESULT_TAIL_BYTES) {
135
+ const cut = this.tailLen - RESULT_TAIL_BYTES;
136
+ this.tail[0] = this.tail[0].subarray(cut);
137
+ this.tailLen -= cut;
138
+ }
139
+ }
140
+ /** Text still held by the streaming decoder (at the end of the stream). */
141
+ flushStreaming() { return this.decoder.flush(); }
142
+ /** The `exec_exit` string: everything when nothing was left out, else head + marker + tail cut at whole characters. */
143
+ result() {
144
+ const head = Buffer.concat(this.head);
145
+ const tail = Buffer.concat(this.tail);
146
+ const omitted = this.total - head.length - tail.length;
147
+ if (omitted === 0)
148
+ return { text: decodeUtf8(Buffer.concat([head, tail])), truncated: false };
149
+ const [headText, headCut] = splitIncompleteUtf8(head);
150
+ const skip = leadingContinuation(tail);
151
+ return {
152
+ text: decodeUtf8(headText) + omissionMarker(omitted + headCut.length + skip) + decodeUtf8(tail.subarray(skip)),
153
+ truncated: true,
154
+ };
155
+ }
156
+ state() {
157
+ return {
158
+ total: this.total, headLen: this.headLen, tail: Buffer.concat(this.tail).toString('base64'),
159
+ pending: this.decoder.held.toString('base64'), lost: this.lost,
160
+ };
161
+ }
162
+ }
163
+ /** Both streams of one exec. */
164
+ export class ExecShaper {
165
+ stdout;
166
+ stderr;
167
+ constructor(state, heads) {
168
+ this.stdout = new ShapedStream(state?.stdout, heads?.stdout);
169
+ this.stderr = new ShapedStream(state?.stderr, heads?.stderr);
170
+ }
171
+ stream(name) { return name === 'stdout' ? this.stdout : this.stderr; }
172
+ /** Neither streaming decoder holds bytes: decoding may restart here from nothing and produce the same text from then on. */
173
+ get clean() { return this.stdout.heldBytes === 0 && this.stderr.heldBytes === 0; }
174
+ result() {
175
+ const out = this.stdout.result();
176
+ const err = this.stderr.result();
177
+ return { stdout: out.text, stderr: err.text, truncated: out.truncated || err.truncated };
178
+ }
179
+ state() { return { stdout: this.stdout.state(), stderr: this.stderr.state() }; }
180
+ }
181
+ /**
182
+ * The streaming text of an exec's records exactly as {@link ShapedStream.push} produces it (held bytes of an incomplete character
183
+ * go out with the next chunk of the same stream; a jump in the stream offset flushes what is held), but WITHOUT the head/tail
184
+ * bookkeeping and starting from any point where decoding is clean (no held bytes): the stream offset is taken from the first
185
+ * chunk. Decoding the same records from any clean point yields, for every later record, the same text the daemon's shaper
186
+ * produced for it — which is what makes `exec_output` chunks identical across re-attaches (exec/attach.ts).
187
+ */
188
+ export class ExecWireText {
189
+ streams = {
190
+ stdout: { total: undefined, decoder: new Utf8Stream() },
191
+ stderr: { total: undefined, decoder: new Utf8Stream() },
192
+ };
193
+ push(name, so, chunk) {
194
+ const stream = this.streams[name];
195
+ let text = '';
196
+ if (stream.total === undefined)
197
+ stream.total = so;
198
+ if (so < stream.total) {
199
+ chunk = chunk.subarray(Math.min(chunk.length, stream.total - so));
200
+ so = stream.total;
201
+ if (chunk.length === 0)
202
+ return '';
203
+ }
204
+ if (so > stream.total) {
205
+ stream.total = so;
206
+ text += stream.decoder.flush();
207
+ }
208
+ stream.total += chunk.length;
209
+ return text + stream.decoder.push(chunk);
210
+ }
211
+ }
@@ -1,112 +1,3 @@
1
- import { spawn } from 'node:child_process';
2
- import path from 'node:path';
3
- const MAX_BYTES = 65536;
4
- export function runExec(req, onOutput, signal) {
5
- const running = startExec(req, onOutput);
6
- const onAbort = () => { void running.cancel(); };
7
- if (signal?.aborted)
8
- onAbort();
9
- else
10
- signal?.addEventListener('abort', onAbort, { once: true });
11
- void running.result.finally(() => signal?.removeEventListener('abort', onAbort));
12
- return running.result;
13
- }
14
- /** Starts an exec in its own process group and exposes the same cancellation used by force update. */
15
- export function startExec(req, onOutput) {
16
- let cancel = async () => { };
17
- const result = new Promise((resolve) => {
18
- const env = { ...process.env, ...req.env };
19
- if (req.pathPrepend.length > 0) {
20
- env.PATH = [...req.pathPrepend, env.PATH ?? ''].filter(Boolean).join(path.delimiter);
21
- }
22
- // macOS and Linux create a new session/process group for a detached child. Every command it
23
- // launches (pipelines and background jobs included) stays in that group, so cancellation does
24
- // not leave descendants running after the shell parent is gone.
25
- const child = spawn(req.shell, ['-lc', req.command], { cwd: req.cwd, env, detached: process.platform !== 'win32' });
26
- let stdout = '';
27
- let stderr = '';
28
- let truncated = false;
29
- let timedOut = false;
30
- let cancelled = false;
31
- let settled = false;
32
- const capture = (buf, stream) => {
33
- const text = buf.toString();
34
- if (req.streaming && onOutput)
35
- onOutput(stream, text);
36
- if (stream === 'stdout') {
37
- if (stdout.length >= MAX_BYTES) {
38
- truncated = true;
39
- return;
40
- }
41
- if (stdout.length + text.length > MAX_BYTES) {
42
- stdout += text.slice(0, MAX_BYTES - stdout.length);
43
- truncated = true;
44
- }
45
- else {
46
- stdout += text;
47
- }
48
- }
49
- else {
50
- if (stderr.length >= MAX_BYTES) {
51
- truncated = true;
52
- return;
53
- }
54
- if (stderr.length + text.length > MAX_BYTES) {
55
- stderr += text.slice(0, MAX_BYTES - stderr.length);
56
- truncated = true;
57
- }
58
- else {
59
- stderr += text;
60
- }
61
- }
62
- };
63
- child.stdout.on('data', (b) => capture(b, 'stdout'));
64
- child.stderr.on('data', (b) => capture(b, 'stderr'));
65
- const terminateCommandGroup = () => {
66
- if (child.pid && process.platform !== 'win32') {
67
- try {
68
- process.kill(-child.pid, 'SIGKILL');
69
- return;
70
- }
71
- catch {
72
- // The group may have already exited between pid observation and signal delivery.
73
- }
74
- }
75
- child.kill('SIGKILL');
76
- };
77
- let timer;
78
- if (req.timeoutSeconds && req.timeoutSeconds > 0) {
79
- timer = setTimeout(() => { timedOut = true; terminateCommandGroup(); }, req.timeoutSeconds * 1000);
80
- }
81
- const abort = () => { cancelled = true; terminateCommandGroup(); };
82
- cancel = async () => { abort(); };
83
- child.on('close', (code) => {
84
- if (settled)
85
- return;
86
- settled = true;
87
- if (timer)
88
- clearTimeout(timer);
89
- resolve({
90
- t: 'exec_exit', id: req.id,
91
- exitCode: timedOut || cancelled ? null : code,
92
- stdout, stderr, timedOut, truncated,
93
- });
94
- });
95
- child.on('error', (err) => {
96
- if (settled)
97
- return;
98
- settled = true;
99
- if (timer)
100
- clearTimeout(timer);
101
- resolve({
102
- t: 'exec_exit', id: req.id,
103
- exitCode: null,
104
- stdout,
105
- stderr: stderr + String(err?.message ?? err),
106
- timedOut: false,
107
- truncated,
108
- });
109
- });
110
- });
111
- return { result, cancel: async () => cancel() };
112
- }
1
+ // The daemon's in-process exec, kept at its historical path for existing importers. It is now the FALLBACK implementation
2
+ // (exec/fallback.ts): commands normally run in keeper-terminal through the exec policy (exec/policy.ts).
3
+ export { runExec, startExec } from './exec/fallback.js';
@@ -0,0 +1,51 @@
1
+ import { toFileError } from '../errors.js';
2
+ import { requestRoot } from '../scope.js';
3
+ import { listDir, makeDir, readText, writeText, } from '../text.js';
4
+ /** Same shape as channels/computer.ts `FileExecutors`, backed by the files module instead of the fork-per-request worker. */
5
+ export const requestRootFileExecutors = {
6
+ readFile: async (request, signal) => readText(requestRoot(request.root), { path: request.path, startLine: request.startLine, maxLines: request.maxLines }, signal),
7
+ writeFile: async (request, signal) => writeText(requestRoot(request.root), { path: request.path, content: request.content, mode: request.mode, expectedSha256: request.expectedSha256 }, signal),
8
+ listDir: async (request, signal) => listDir(requestRoot(request.root), { path: request.path, startAfter: request.startAfter, maxEntries: request.maxEntries }, signal),
9
+ createDir: async (request, signal) => makeDir(requestRoot(request.root), { path: request.path, parents: request.parents }, signal),
10
+ };
11
+ /** Any thrown value as a wire error: one of the 14 codes plus a message that never carries an OS path. */
12
+ export function fileErrorPayload(error) {
13
+ const mapped = toFileError(error);
14
+ return { code: mapped.code, message: mapped.message };
15
+ }
16
+ /** The error a request cancelled while under way reports (channels/computer.ts sends exactly these). */
17
+ export function cancelledFilePayload(t) {
18
+ if (t === 'file_write_request')
19
+ return { code: 'outcome_unknown', message: 'Write outcome is unknown after cancellation' };
20
+ if (t === 'file_mkdir_request')
21
+ return { code: 'outcome_unknown', message: 'Directory creation outcome is unknown after cancellation' };
22
+ return { code: 'workstation_offline', message: 'Read was cancelled' };
23
+ }
24
+ export function fileErrorFrame(request, error) {
25
+ if (request.t === 'file_read_request')
26
+ return { t: 'file_read_result', id: request.id, error };
27
+ if (request.t === 'file_write_request')
28
+ return { t: 'file_write_result', id: request.id, error };
29
+ if (request.t === 'file_ls_request')
30
+ return { t: 'file_ls_result', id: request.id, error };
31
+ return { t: 'file_mkdir_result', id: request.id, error };
32
+ }
33
+ /** Executes one request frame in the request-root scope and returns its result frame; never throws. */
34
+ export async function runFileFrame(request, signal, hooks) {
35
+ try {
36
+ const scope = requestRoot(request.root);
37
+ if (request.t === 'file_read_request') {
38
+ return { t: 'file_read_result', id: request.id, data: await readText(scope, { path: request.path, startLine: request.startLine, maxLines: request.maxLines }, signal, hooks) };
39
+ }
40
+ if (request.t === 'file_write_request') {
41
+ return { t: 'file_write_result', id: request.id, data: await writeText(scope, { path: request.path, content: request.content, mode: request.mode, expectedSha256: request.expectedSha256 }, signal, hooks) };
42
+ }
43
+ if (request.t === 'file_ls_request') {
44
+ return { t: 'file_ls_result', id: request.id, data: await listDir(scope, { path: request.path, startAfter: request.startAfter, maxEntries: request.maxEntries }, signal, hooks) };
45
+ }
46
+ return { t: 'file_mkdir_result', id: request.id, data: await makeDir(scope, { path: request.path, parents: request.parents }, signal, hooks) };
47
+ }
48
+ catch (error) {
49
+ return fileErrorFrame(request, signal?.aborted ? cancelledFilePayload(request.t) : fileErrorPayload(error));
50
+ }
51
+ }
@@ -0,0 +1,121 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { TOOL_METHODS, TOOL_RESULT_BYTES, validateRequest } from '../../tools/validation.js';
5
+ import { WorkstationMedia } from '../media/media.js';
6
+ import { StructuredFiles } from '../structured.js';
7
+ import { UPLOAD_TTL_MS, UploadStore } from '../uploads.js';
8
+ /*
9
+ * Wire adapter for the Files tab: `tool_request{method: files.*}` <-> `tool_result{ok, result, error: string}`.
10
+ * Failures are thrown as errors whose `message` is the `tool_result.error` text (the channel truncates it to 1024);
11
+ * an expired media consumer is a successful result `{errorCode: 'media-consumer-expired'}` (see media/media.ts).
12
+ * Unlike the legacy ToolsCore there is no global queue: only `pinned()` sections are mutually exclusive.
13
+ */
14
+ export const FILES_TOOL_METHODS = new Set([...TOOL_METHODS].filter(method => method.startsWith('files.')));
15
+ export function isFilesToolMethod(method) { return FILES_TOOL_METHODS.has(method); }
16
+ /** `files.*` methods that change the filesystem: when the process running one is lost, its outcome is unknown. */
17
+ export const FILES_MUTATING_METHODS = new Set([
18
+ 'files.mkdir', 'files.rename', 'files.delete', 'files.upload.begin', 'files.upload.chunk', 'files.upload.finish', 'files.upload.cancel',
19
+ ]);
20
+ /** `tool_result.error` text for a thrown value. */
21
+ export function toolErrorMessage(error) { return error instanceof Error ? error.message : 'Workstation tool failed'; }
22
+ export class FileTools {
23
+ files;
24
+ media;
25
+ filesystem;
26
+ filesystemMedia;
27
+ disposed = false;
28
+ inFlight = new Set();
29
+ timer;
30
+ constructor(files, media, filesystem, filesystemMedia, ttl) {
31
+ this.files = files;
32
+ this.media = media;
33
+ this.filesystem = filesystem;
34
+ this.filesystemMedia = filesystemMedia;
35
+ this.timer = setInterval(() => { void Promise.allSettled([this.files.expire(), this.filesystem.expire()]); }, Math.min(ttl, 60_000));
36
+ this.timer.unref();
37
+ }
38
+ /** Every path option must be absolute: a relative one would resolve against whatever directory a section pinned. */
39
+ static async create(options = {}) {
40
+ for (const [name, value] of [['home', options.home], ['uploadStateDir', options.uploadStateDir], ['mediaCache', options.mediaCache], ...(options.roots ?? []).map(root => ['roots', root])]) {
41
+ if (value !== undefined && !path.isAbsolute(value))
42
+ throw new Error(`File tools option ${name} must be an absolute path`);
43
+ }
44
+ const ttl = options.uploadTtlMs ?? UPLOAD_TTL_MS;
45
+ const files = await StructuredFiles.create(options.home ?? os.homedir(), options.roots ?? [], ttl, options.uploadStateDir);
46
+ const media = new WorkstationMedia(files, options.mediaCache ?? path.join(files.home, '.cache', 'axom', 'media'));
47
+ const filesystem = await StructuredFiles.create(files.home, [], ttl, options.uploadStateDir ? path.join(options.uploadStateDir, 'filesystem') : undefined, true);
48
+ const filesystemMedia = new WorkstationMedia(filesystem, options.mediaCache ?? path.join(files.home, '.cache', 'axom', 'media'));
49
+ return new FileTools(files, media, filesystem, filesystemMedia, ttl);
50
+ }
51
+ info() {
52
+ return { files: true, readOnlyFilesVersion: 1, filesystemFilesVersion: 1, mediaPreviewVersion: 2, fileFindVersion: 1, home: this.files.home, userName: os.userInfo().username, roots: this.files.roots, downloads: this.files.downloads };
53
+ }
54
+ /** Uploads in flight; the daemon samples it as activity. */
55
+ get activeUploads() { return this.files.activeUploads + this.filesystem.activeUploads; }
56
+ /** Media encodes running or queued; the daemon samples it as activity. */
57
+ get activeEncodes() { return this.media.activeEncodes + this.filesystemMedia.activeEncodes; }
58
+ dispatch(method, params = {}) {
59
+ const call = this.run(method, params);
60
+ this.inFlight.add(call);
61
+ void call.finally(() => this.inFlight.delete(call)).catch(() => { });
62
+ return call;
63
+ }
64
+ async run(method, params) {
65
+ if (this.disposed)
66
+ throw new Error('Workstation files host stopped');
67
+ if (params.filesystem !== undefined && typeof params.filesystem !== 'boolean')
68
+ throw new Error('Invalid filesystem');
69
+ if (params.filesystem !== undefined && !method.startsWith('files.'))
70
+ throw new Error('Unexpected tool parameter');
71
+ if (params.filesystem === true && params.readOnly !== undefined)
72
+ throw new Error('Conflicting file scopes');
73
+ const filesystem = params.filesystem === true;
74
+ const { filesystem: _filesystem, ...scopedParams } = params;
75
+ validateRequest(method, scopedParams);
76
+ if (!isFilesToolMethod(method))
77
+ throw new Error('Unsupported workstation tool');
78
+ const files = filesystem ? this.filesystem : this.files;
79
+ const media = filesystem ? this.filesystemMedia : this.media;
80
+ const result = method.startsWith('files.media.') ? await media.dispatch(method, scopedParams) : await files.dispatch(method, scopedParams);
81
+ if (Buffer.byteLength(JSON.stringify(result)) > TOOL_RESULT_BYTES - 1024)
82
+ throw new Error('Tool result too large');
83
+ return result;
84
+ }
85
+ /**
86
+ * `terminal.create`'s working directory, resolved exactly as the legacy tools keeper did (`StructuredFiles.resolve(cwd ??
87
+ * home)`, configured-roots scope, same error texts). The terminal keeper itself checks that it is a directory.
88
+ */
89
+ resolveTerminalCwd(cwd) {
90
+ if (cwd !== undefined && typeof cwd !== 'string')
91
+ return Promise.reject(new Error('Invalid cwd'));
92
+ return this.files.resolve(cwd ?? this.files.home);
93
+ }
94
+ /**
95
+ * Cleans the orphaned upload temporaries recorded in `stateDir` by another process that has stopped for good (the retired
96
+ * legacy tools keeper's `<sockdir>/uploads`), inside this instance's configured roots. A missing directory is nothing to do;
97
+ * an unsafe one (symlink, not a directory, another owner) is refused.
98
+ */
99
+ async recoverUploads(stateDir) {
100
+ if (typeof stateDir !== 'string' || !path.isAbsolute(stateDir))
101
+ throw new Error('Upload state directory must be an absolute path');
102
+ const stat = await fs.lstat(stateDir).catch((error) => { if (error.code === 'ENOENT')
103
+ return null; throw error; });
104
+ if (!stat)
105
+ return;
106
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
107
+ throw new Error('Unsafe upload state directory');
108
+ await new UploadStore(this.files.roots, UPLOAD_TTL_MS, stateDir).recover((raw, missingLeaf, mutation) => this.files.resolve(raw, missingLeaf, mutation));
109
+ await fs.rmdir(stateDir).catch(() => { }); // only when recover left it empty
110
+ }
111
+ /** Refuses new calls, waits for the ones in flight (as ToolsCore awaited its queue), then stops media and uploads. */
112
+ async dispose() {
113
+ this.disposed = true;
114
+ clearInterval(this.timer);
115
+ await Promise.allSettled([...this.inFlight]);
116
+ await this.media.dispose();
117
+ await this.files.dispose();
118
+ await this.filesystemMedia.dispose();
119
+ await this.filesystem.dispose();
120
+ }
121
+ }