@platless/axom-cli 0.3.13 → 0.6.36

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 (320) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +119 -9
  3. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  6. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  7. package/dist/axom-patch/cli/account-limits/generate.py +123 -0
  8. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +81 -0
  9. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  10. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  12. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  13. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  14. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  15. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  16. package/dist/axom-patch/cli/nightly-task.md +194 -0
  17. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  18. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  19. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  20. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  21. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  22. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  23. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  24. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  25. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -5
  26. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -7
  27. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  28. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  29. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  30. package/dist/axom-patch/cli/runtime.json +39 -15
  31. package/dist/axom-patch/cli/zcode-account-limits.json +30 -0
  32. package/dist/axom-patch/cli/zcode-models.json +37 -0
  33. package/dist/axom-patch/cli/zcode-subagent-subwork.json +72 -0
  34. package/dist/cli.js +4 -9
  35. package/dist/commands/knowledge.js +47 -0
  36. package/dist/commands/update.js +65 -3
  37. package/dist/commands/workstation.js +73 -7
  38. package/dist/install/claude/commands.js +13 -13
  39. package/dist/keepers/acp/keeper.mjs +3083 -0
  40. package/dist/keepers/acp/manifest.json +9 -0
  41. package/dist/keepers/terminal/keeper.mjs +8469 -0
  42. package/dist/keepers/terminal/manifest.json +9 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  52. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  53. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  54. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  55. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  56. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  57. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  58. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  59. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  60. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  61. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  62. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  63. package/dist/mcp.js +1 -1
  64. package/dist/native/darwin-arm64/axom-quic +0 -0
  65. package/dist/test/keeperKit.js +67 -0
  66. package/dist/workstation/acp/acpAgents.js +2192 -636
  67. package/dist/workstation/acp/acpClient.js +84 -495
  68. package/dist/workstation/acp/adoption.js +10 -396
  69. package/dist/workstation/acp/binaryInstall.js +100 -27
  70. package/dist/workstation/acp/builds/gc.js +245 -0
  71. package/dist/workstation/acp/builds/install/binary.js +28 -0
  72. package/dist/workstation/acp/builds/install/npm.js +324 -0
  73. package/dist/workstation/acp/builds/install/uv.js +254 -0
  74. package/dist/workstation/acp/builds/migrate.js +195 -0
  75. package/dist/workstation/acp/builds/model.js +128 -0
  76. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  77. package/dist/workstation/acp/builds/store.js +352 -0
  78. package/dist/workstation/acp/commandTableProbe.js +68 -0
  79. package/dist/workstation/acp/configOptions.js +103 -1
  80. package/dist/workstation/acp/envSecrets.js +27 -0
  81. package/dist/workstation/acp/initializeResponse.js +290 -0
  82. package/dist/workstation/acp/installErrors.js +12 -2
  83. package/dist/workstation/acp/installProbe.js +1 -1
  84. package/dist/workstation/acp/managedPatches.js +239 -159
  85. package/dist/workstation/acp/managedRuntime.js +314 -0
  86. package/dist/workstation/acp/outbox.js +330 -0
  87. package/dist/workstation/acp/patchAssets.js +60 -3
  88. package/dist/workstation/acp/permission.js +42 -10
  89. package/dist/workstation/acp/processor/checkpoint.js +78 -0
  90. package/dist/workstation/acp/processor/commands.js +35 -0
  91. package/dist/workstation/acp/processor/control.js +282 -0
  92. package/dist/workstation/acp/processor/emit.js +26 -0
  93. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  94. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  95. package/dist/workstation/acp/processor/origins.js +48 -0
  96. package/dist/workstation/acp/processor/output.js +67 -0
  97. package/dist/workstation/acp/processor/projection.js +42 -0
  98. package/dist/workstation/acp/processor/record.js +128 -0
  99. package/dist/workstation/acp/processor/runtime.js +105 -0
  100. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  101. package/dist/workstation/acp/processor/session.js +496 -0
  102. package/dist/workstation/acp/processor/state.js +67 -0
  103. package/dist/workstation/acp/processor/step.js +442 -0
  104. package/dist/workstation/acp/processor/test/fixtureLog.js +58 -0
  105. package/dist/workstation/acp/processor/test/harness.js +120 -0
  106. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  107. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  108. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  109. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  110. package/dist/workstation/acp/processor/transport/local.js +269 -0
  111. package/dist/workstation/acp/processor/transport/types.js +78 -0
  112. package/dist/workstation/acp/processor/updates.js +390 -0
  113. package/dist/workstation/acp/processorSession.js +1348 -0
  114. package/dist/workstation/acp/registry/catalog.js +189 -41
  115. package/dist/workstation/acp/registry/launch.js +76 -119
  116. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  117. package/dist/workstation/acp/steering.js +58 -0
  118. package/dist/workstation/acp/switching.js +111 -0
  119. package/dist/workstation/acp/terminalLogin.js +16 -42
  120. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  121. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  122. package/dist/workstation/acp/test/daemonLife.js +106 -0
  123. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  124. package/dist/workstation/acp/test/keeperWorld.js +295 -0
  125. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  126. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  127. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  128. package/dist/workstation/acp/test/raw/sources.js +135 -0
  129. package/dist/workstation/acp/test/realAdapter.js +331 -0
  130. package/dist/workstation/acp/test/scriptAgent.js +214 -0
  131. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  132. package/dist/workstation/acp/timedCommand.js +4 -2
  133. package/dist/workstation/acp/uvxInstall.js +72 -122
  134. package/dist/workstation/agentFlow.js +103 -0
  135. package/dist/workstation/agentSessions.js +1289 -544
  136. package/dist/workstation/channels/agent.js +7 -5
  137. package/dist/workstation/channels/computer.js +69 -21
  138. package/dist/workstation/channels/computerExecs.js +200 -0
  139. package/dist/workstation/channels/management.js +172 -8
  140. package/dist/workstation/channels/url.js +1 -1
  141. package/dist/workstation/config.js +16 -4
  142. package/dist/workstation/connection/encryptedChannel.js +4 -1
  143. package/dist/workstation/control/bindingStore.js +200 -0
  144. package/dist/workstation/control/commands.js +124 -0
  145. package/dist/workstation/control/daemonIdentity.js +53 -0
  146. package/dist/workstation/control/enrollment.js +106 -0
  147. package/dist/workstation/control/inspect.js +71 -0
  148. package/dist/workstation/control/locks.js +63 -0
  149. package/dist/workstation/control/protocol.js +40 -0
  150. package/dist/workstation/control/restartGuard.js +46 -0
  151. package/dist/workstation/control/runtimeClient.js +97 -0
  152. package/dist/workstation/control/runtimeHost.js +148 -0
  153. package/dist/workstation/control/serverIdentity.js +35 -0
  154. package/dist/workstation/daemon.js +393 -12
  155. package/dist/workstation/daemonctl.js +103 -14
  156. package/dist/workstation/desktopControl/audio.js +99 -0
  157. package/dist/workstation/desktopControl/capabilities.js +92 -0
  158. package/dist/workstation/desktopControl/captureModes.js +9 -0
  159. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  160. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  161. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  162. package/dist/workstation/desktopControl/lobby.js +129 -0
  163. package/dist/workstation/desktopControl/macProvider.js +155 -0
  164. package/dist/workstation/desktopControl/media.js +331 -0
  165. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  166. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  167. package/dist/workstation/desktopControl/overview.js +58 -0
  168. package/dist/workstation/desktopControl/protocol.js +302 -0
  169. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  170. package/dist/workstation/desktopControl/rateController.js +226 -0
  171. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  172. package/dist/workstation/desktopControl/transport.js +27 -0
  173. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  174. package/dist/workstation/exec/attach.js +126 -0
  175. package/dist/workstation/exec/command.js +40 -0
  176. package/dist/workstation/exec/durable.js +517 -0
  177. package/dist/workstation/exec/fallback.js +106 -0
  178. package/dist/workstation/exec/policy.js +837 -0
  179. package/dist/workstation/exec/shaping.js +211 -0
  180. package/dist/workstation/exec.js +3 -112
  181. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  182. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  183. package/dist/workstation/files/client.js +455 -0
  184. package/dist/workstation/files/credentials.js +142 -0
  185. package/dist/workstation/files/errors.js +28 -0
  186. package/dist/workstation/files/fence.js +350 -0
  187. package/dist/workstation/files/find/fileIndex.js +153 -0
  188. package/dist/workstation/files/find/fuzzy.js +155 -0
  189. package/dist/workstation/files/host.js +122 -0
  190. package/dist/workstation/files/hostProtocol.js +2 -0
  191. package/dist/workstation/files/media/cache.js +620 -0
  192. package/dist/workstation/files/media/encoding.js +308 -0
  193. package/dist/workstation/files/media/estimator.js +100 -0
  194. package/dist/workstation/files/media/media.js +325 -0
  195. package/dist/workstation/files/media/packedAudio.js +23 -0
  196. package/dist/workstation/files/media/scheduler.js +551 -0
  197. package/dist/workstation/files/media/types.js +5 -0
  198. package/dist/workstation/files/scope.js +45 -0
  199. package/dist/workstation/files/structured.js +284 -0
  200. package/dist/workstation/{fileOps.js → files/text.js} +152 -39
  201. package/dist/workstation/files/uploads.js +351 -0
  202. package/dist/workstation/journal.js +65 -236
  203. package/dist/workstation/keepers/acp/client.js +101 -0
  204. package/dist/workstation/keepers/acp/envelope.js +62 -0
  205. package/dist/workstation/keepers/acp/host.js +419 -0
  206. package/dist/workstation/keepers/acp/main.js +18 -0
  207. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  208. package/dist/workstation/keepers/acp/params.js +137 -0
  209. package/dist/workstation/keepers/acp/protocol.js +66 -0
  210. package/dist/workstation/keepers/acp/recover.js +120 -0
  211. package/dist/workstation/keepers/acp/session.js +584 -0
  212. package/dist/workstation/keepers/acp/stderr.js +154 -0
  213. package/dist/workstation/keepers/kit/activity.js +103 -0
  214. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  215. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  216. package/dist/workstation/keepers/kit/bundle.js +124 -0
  217. package/dist/workstation/keepers/kit/client.js +432 -0
  218. package/dist/workstation/keepers/kit/identity.js +21 -0
  219. package/dist/workstation/keepers/kit/ipc.js +112 -0
  220. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  221. package/dist/workstation/keepers/kit/launcher.js +289 -0
  222. package/dist/workstation/keepers/kit/lock.js +123 -0
  223. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  224. package/dist/workstation/keepers/kit/paths.js +76 -0
  225. package/dist/workstation/keepers/kit/protocol.js +43 -0
  226. package/dist/workstation/keepers/kit/server.js +518 -0
  227. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  228. package/dist/workstation/keepers/legacy.js +296 -0
  229. package/dist/workstation/keepers/policy.js +576 -0
  230. package/dist/workstation/keepers/stop.js +34 -0
  231. package/dist/workstation/keepers/terminal/client.js +213 -0
  232. package/dist/workstation/keepers/terminal/exec.js +981 -0
  233. package/dist/workstation/keepers/terminal/generations.js +129 -0
  234. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  235. package/dist/workstation/keepers/terminal/main.js +43 -0
  236. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  237. package/dist/workstation/keepers/terminal/params.js +27 -0
  238. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  239. package/dist/workstation/keepers/terminal/pty.js +330 -0
  240. package/dist/workstation/managedInstall.js +57 -0
  241. package/dist/workstation/protocol/accountLimits.js +47 -0
  242. package/dist/workstation/protocol/agent.js +50 -19
  243. package/dist/workstation/protocol/common.js +30 -0
  244. package/dist/workstation/protocol/computer.js +15 -4
  245. package/dist/workstation/protocol/management.js +141 -4
  246. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  247. package/dist/workstation/rd/artifact.js +119 -0
  248. package/dist/workstation/rd/control.js +119 -0
  249. package/dist/workstation/rd/display.js +29 -0
  250. package/dist/workstation/rd/provider.js +222 -0
  251. package/dist/workstation/rd/sessions.js +391 -0
  252. package/dist/workstation/rd/state.js +45 -0
  253. package/dist/workstation/rd/update.js +171 -0
  254. package/dist/workstation/rd/upkeep.js +38 -0
  255. package/dist/workstation/register.js +27 -37
  256. package/dist/workstation/service/installer.js +104 -13
  257. package/dist/workstation/service/launchd.js +1 -0
  258. package/dist/workstation/storageMetrics.js +118 -0
  259. package/dist/workstation/systemMetrics.js +154 -93
  260. package/dist/workstation/toolchain.js +94 -0
  261. package/dist/workstation/tools/browser.js +28 -4
  262. package/dist/workstation/tools/client.js +9 -1
  263. package/dist/workstation/tools/info.js +50 -0
  264. package/dist/workstation/tools/keeperProcess.js +15 -5
  265. package/dist/workstation/tools/router.js +55 -0
  266. package/dist/workstation/tools/terminalRouter.js +222 -0
  267. package/dist/workstation/tools/validation.js +37 -1
  268. package/dist/workstation/update/artifact.js +2 -1
  269. package/dist/workstation/update/coordinator.js +3 -0
  270. package/dist/workstation/update/globalInstall.js +116 -0
  271. package/dist/workstation/update/npmInstaller.js +49 -23
  272. package/dist/workstation/update/ptyPreflight.js +21 -0
  273. package/dist/workstation/update/rdRollback.js +145 -0
  274. package/dist/workstation/update/runUpdate.js +3 -2
  275. package/dist/workstation/update/taskRegistry.js +105 -11
  276. package/dist/workstation/update/watchdog.js +16790 -334
  277. package/package.json +11 -8
  278. package/skills/axom/SKILL.md +11 -11
  279. package/skills/axom-definition/SKILL.md +3 -3
  280. package/skills/axom-platform/SKILL.md +3 -3
  281. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  282. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -49
  283. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  284. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  285. package/dist/commands/guide.js +0 -38
  286. package/dist/workstation/acp/acpSession.js +0 -1475
  287. package/dist/workstation/acp/contract.js +0 -499
  288. package/dist/workstation/acp/delegationMcp.js +0 -24
  289. package/dist/workstation/acp/draftInterceptor.js +0 -163
  290. package/dist/workstation/acp/qoderStartup.js +0 -45
  291. package/dist/workstation/acp/sessionScope.js +0 -19
  292. package/dist/workstation/filePaths.js +0 -174
  293. package/dist/workstation/fileWorker.js +0 -81
  294. package/dist/workstation/fileWorkerClient.js +0 -121
  295. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -106
  296. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -262
  297. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  298. package/dist/workstation/qoderAdapter/contract.js +0 -78
  299. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -30
  300. package/dist/workstation/qoderAdapter/history.js +0 -87
  301. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  302. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  303. package/dist/workstation/qoderAdapter/main.js +0 -93
  304. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  305. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  306. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  307. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  308. package/dist/workstation/qoderAdapter/profile.js +0 -93
  309. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -49
  310. package/dist/workstation/qoderAdapter/session.js +0 -319
  311. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  312. package/dist/workstation/qoderAdapter/subwork.js +0 -77
  313. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  314. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  315. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  316. package/dist/workstation/qoderAdapter/usage.js +0 -37
  317. package/dist/workstation/tools/core.js +0 -50
  318. package/dist/workstation/tools/files.js +0 -525
  319. package/dist/workstation/tools/terminal.js +0 -214
  320. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -1,37 +0,0 @@
1
- export const QODER_CREDITS_LABEL = 'Qoder Credits(厂商报告)';
2
- const validCredits = (value) => typeof value === 'number' &&
3
- Number.isFinite(value) && value >= 0;
4
- /** Qoder's report is cumulative. A missing/invalid report never erases a prior valid value. */
5
- export function applyCredits(previous, reported) {
6
- if (validCredits(reported))
7
- return reported;
8
- return validCredits(previous) ? previous : undefined;
9
- }
10
- function reportedCredits(value) {
11
- if (!value || typeof value !== 'object' || Array.isArray(value))
12
- return undefined;
13
- const input = value;
14
- for (const key of ['total_credits', 'totalCredits', 'credits'])
15
- if (validCredits(input[key]))
16
- return input[key];
17
- return undefined;
18
- }
19
- /**
20
- * Keeps vendor credits separate from ACP token/USD counters. Qoder's SDK does not
21
- * provide equivalent token or USD values, so zero placeholders are intentionally omitted.
22
- */
23
- export function normalizeQoderUsage(value, previousCredits) {
24
- const input = value && typeof value === 'object' && !Array.isArray(value)
25
- ? value : {};
26
- const scope = input.cumulativeScope === 'unknown' ? 'unknown' : 'session';
27
- const reported = reportedCredits(value);
28
- if (scope === 'unknown') {
29
- return { credits: undefined, creditsLabel: QODER_CREDITS_LABEL, creditsScope: scope,
30
- inputTokens: undefined, outputTokens: undefined, costUsd: undefined,
31
- ...(validCredits(previousCredits) ? { previousCredits } : {}) };
32
- }
33
- const credits = applyCredits(previousCredits, reported);
34
- return { credits, creditsLabel: QODER_CREDITS_LABEL, creditsScope: scope,
35
- inputTokens: undefined, outputTokens: undefined, costUsd: undefined,
36
- ...(validCredits(previousCredits) ? { previousCredits } : {}) };
37
- }
@@ -1,50 +0,0 @@
1
- import os from 'node:os';
2
- import { StructuredFiles } from './files.js';
3
- import { PersistentTerminals } from './terminal.js';
4
- import { keys, validateRequest, TOOL_RESULT_BYTES } from './validation.js';
5
- /** All mutations are serialized across clients; shell processes continue independently. */
6
- export class ToolsCore {
7
- files;
8
- terminals;
9
- queue = Promise.resolve();
10
- disposed = false;
11
- timer;
12
- constructor(files, terminals, ttl) {
13
- this.files = files;
14
- this.terminals = terminals;
15
- this.timer = setInterval(() => { this.queue = this.queue.then(() => this.files.expire()).catch(() => { }); }, Math.min(ttl, 60_000));
16
- this.timer.unref();
17
- }
18
- static async create(options = {}) {
19
- const files = await StructuredFiles.create(options.home ?? os.homedir(), options.roots ?? [], options.uploadTtlMs, options.uploadStateDir);
20
- return new ToolsCore(files, new PersistentTerminals(files, options), options.uploadTtlMs ?? 15 * 60_000);
21
- }
22
- dispatch(method, params = {}) {
23
- const task = this.queue.then(async () => {
24
- if (this.disposed)
25
- throw new Error('Workstation tools keeper stopped');
26
- validateRequest(method, params);
27
- let result;
28
- if (method === 'tools.info') {
29
- keys(params, []);
30
- result = { version: 1, terminal: this.terminals.available, browser: false, files: true, home: this.files.home, roots: this.files.roots, downloads: this.files.downloads, ...(this.terminals.error ? { terminalError: this.terminals.error } : {}), browserError: 'Browser tools are not enabled in this release' };
31
- }
32
- else if (method.startsWith('terminal.'))
33
- result = await this.terminals.dispatch(method, params);
34
- else
35
- result = await this.files.dispatch(method, params);
36
- if (Buffer.byteLength(JSON.stringify(result)) > TOOL_RESULT_BYTES - 1024)
37
- throw new Error('Tool result too large');
38
- return result;
39
- });
40
- this.queue = task.catch(() => { });
41
- return task;
42
- }
43
- async dispose() {
44
- this.disposed = true;
45
- clearInterval(this.timer);
46
- await this.queue;
47
- await this.terminals.dispose();
48
- await this.files.dispose();
49
- }
50
- }
@@ -1,525 +0,0 @@
1
- import { constants, lstatSync, linkSync, renameSync } from 'node:fs';
2
- import { promises as fs } from 'node:fs';
3
- import path from 'node:path';
4
- import { createHash, randomUUID } from 'node:crypto';
5
- import { base64, CHUNK_BYTES, identifier, integer, keys, string } from './validation.js';
6
- const MAX_UPLOAD_BYTES = 100 * 1024 * 1024;
7
- const MAX_UPLOADS = 8;
8
- const TEMP_PREFIX = '.axom-upload-';
9
- /** Canonical roots and no descendant symlinks. Tools never compose a shell command. */
10
- export class StructuredFiles {
11
- home;
12
- roots;
13
- downloads;
14
- ttl;
15
- stateDir;
16
- uploads = new Map();
17
- pin;
18
- busy = false;
19
- constructor(home, roots, downloads, ttl, stateDir) {
20
- this.home = home;
21
- this.roots = roots;
22
- this.downloads = downloads;
23
- this.ttl = ttl;
24
- this.stateDir = stateDir;
25
- }
26
- static async create(home, roots, ttl = 15 * 60_000, stateDir) {
27
- home = await fs.realpath(home);
28
- const canonical = new Set([home]);
29
- for (const root of roots) {
30
- const real = await fs.realpath(root);
31
- if (!(await fs.stat(real)).isDirectory())
32
- throw new Error('File root must be a directory');
33
- canonical.add(real);
34
- }
35
- const downloads = path.join(home, 'Downloads');
36
- if (stateDir) {
37
- await fs.mkdir(stateDir, { recursive: true, mode: 0o700 });
38
- const stat = await fs.lstat(stateDir);
39
- if (!stat.isDirectory() || stat.isSymbolicLink() || (process.getuid && stat.uid !== process.getuid()))
40
- throw new Error('Unsafe upload state directory');
41
- await fs.chmod(stateDir, 0o700);
42
- }
43
- const files = new StructuredFiles(home, [...canonical], downloads, ttl, stateDir);
44
- await files.recover();
45
- return files;
46
- }
47
- async recover() {
48
- if (!this.stateDir)
49
- return;
50
- for (const name of await fs.readdir(this.stateDir)) {
51
- if (!/^[0-9a-f-]{36}\.json$/.test(name))
52
- continue;
53
- const manifest = path.join(this.stateDir, name);
54
- try {
55
- const stat = await fs.lstat(manifest);
56
- if (!stat.isFile() || stat.size > 8192)
57
- continue;
58
- const value = JSON.parse(await fs.readFile(manifest, 'utf8'));
59
- if (typeof value.dir !== 'string' || path.basename(value.dir) !== TEMP_PREFIX + name.slice(0, -5) || value.temp !== path.join(value.dir, 'payload'))
60
- throw new Error('Invalid orphan upload');
61
- await this.resolve(value.dir, false, true);
62
- const directory = await fs.lstat(value.dir);
63
- if (!directory.isDirectory() || !this.same(directory, value.dirIdentity))
64
- throw new Error('Orphan upload directory changed');
65
- await this.pinned(value.dir, async () => {
66
- for (const entry of await fs.readdir('.')) {
67
- if (entry !== 'payload' && !/^commit-[0-9a-f-]{36}$/.test(entry))
68
- continue;
69
- const item = await fs.lstat(entry);
70
- if (item.isFile() && this.same(item, value.identity))
71
- await fs.unlink(entry);
72
- }
73
- });
74
- await fs.rmdir(value.dir);
75
- }
76
- catch { /* Replaced entries are never followed or deleted during cleanup. */ }
77
- await fs.unlink(manifest).catch(() => { });
78
- }
79
- }
80
- same(stat, identity) { return stat.dev === identity.dev && stat.ino === identity.ino; }
81
- /** Staging names must live beneath a root protected from other-UID rename/write. The destination
82
- * parent remains kernel-pinned; staging can be on any allowed root on that same filesystem. */
83
- async stagingRoot(dev) {
84
- const uid = process.getuid?.();
85
- for (const root of this.roots) {
86
- const rootStat = await fs.lstat(root);
87
- if (!rootStat.isDirectory() || rootStat.dev !== dev || rootStat.uid !== uid || (rootStat.mode & 0o022))
88
- continue;
89
- let child = root;
90
- let childStat = rootStat;
91
- let trusted = true;
92
- while (path.dirname(child) !== child) {
93
- const parent = path.dirname(child);
94
- const stat = await fs.lstat(parent);
95
- if (!stat.isDirectory() || ((stat.mode & 0o022) && !((stat.mode & 0o1000) && childStat.uid === uid))) {
96
- trusted = false;
97
- break;
98
- }
99
- child = parent;
100
- childStat = stat;
101
- }
102
- if (trusted)
103
- return root;
104
- }
105
- throw new Error('Upload requires a trusted private staging root on the destination filesystem');
106
- }
107
- async verifyUpload(upload) {
108
- const directory = await fs.lstat(upload.dir);
109
- if (!directory.isDirectory() || !this.same(directory, upload.dirIdentity) || (directory.mode & 0o077))
110
- throw new Error('Upload staging directory changed');
111
- const opened = await upload.handle.stat();
112
- const named = await fs.lstat(upload.temp);
113
- if (!opened.isFile() || opened.size !== upload.size || !named.isFile() || !this.same(named, opened))
114
- throw new Error('Upload temporary file changed');
115
- for (const [offset, receipt] of upload.chunks) {
116
- const data = Buffer.alloc(receipt.length);
117
- let read = 0;
118
- while (read < data.length) {
119
- const result = await upload.handle.read(data, read, data.length - read, offset + read);
120
- if (!result.bytesRead)
121
- throw new Error('Upload integrity changed');
122
- read += result.bytesRead;
123
- }
124
- if (createHash('sha256').update(data).digest('hex') !== receipt.digest)
125
- throw new Error('Upload integrity changed');
126
- }
127
- const after = await upload.handle.stat();
128
- if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs || after.ctimeMs !== opened.ctimeMs)
129
- throw new Error('Upload integrity changed during verification');
130
- return after;
131
- }
132
- inside(target, root) { return target === root || target.startsWith(root + path.sep); }
133
- async resolve(raw, missingLeaf = false, mutation = false) {
134
- if (!path.isAbsolute(raw) || raw.includes('\0') || raw.includes('\\') || raw.split(path.sep).includes('..') || raw.length > 4096)
135
- throw new Error('Invalid file path');
136
- const target = path.resolve(raw);
137
- const root = this.roots.filter(root => this.inside(target, root)).sort((a, b) => b.length - a.length)[0];
138
- if (!root)
139
- throw new Error('Path outside allowed roots');
140
- if (mutation && this.roots.includes(target))
141
- throw new Error('Cannot modify file root');
142
- // Detect root replacement as well as symlink components below it.
143
- if (await fs.realpath(root) !== root)
144
- throw new Error('File root changed');
145
- let cursor = root;
146
- const parts = path.relative(root, target).split(path.sep).filter(Boolean);
147
- for (let i = 0; i < parts.length; i++) {
148
- cursor = path.join(cursor, parts[i]);
149
- let stat;
150
- try {
151
- stat = await fs.lstat(cursor);
152
- }
153
- catch (error) {
154
- if (error.code === 'ENOENT' && missingLeaf && i === parts.length - 1)
155
- return target;
156
- throw error;
157
- }
158
- if (stat.isSymbolicLink())
159
- throw new Error('Symbolic links are not allowed');
160
- if (i < parts.length - 1 && !stat.isDirectory())
161
- throw new Error('Parent is not a directory');
162
- }
163
- return target;
164
- }
165
- async regularHandle(target, flags = constants.O_RDONLY) {
166
- const handle = await fs.open(this.leaf(target), flags | constants.O_NOFOLLOW | constants.O_NONBLOCK);
167
- try {
168
- if (!(await handle.stat()).isFile())
169
- throw new Error('Not a regular file');
170
- return handle;
171
- }
172
- catch (error) {
173
- await handle.close();
174
- throw error;
175
- }
176
- }
177
- /** The keeper is a dedicated process and ToolsCore serializes ALL calls/expiry. Pin cwd before
178
- * touching any relative leaf: ancestor replacement after validation cannot redirect file IO.
179
- * PTY callbacks use no paths, and PTY spawn always receives an explicit absolute cwd. */
180
- async pinned(parent, action) {
181
- if (this.busy)
182
- throw new Error('Concurrent file access is not supported');
183
- const previous = process.cwd();
184
- this.busy = true;
185
- try {
186
- process.chdir(parent);
187
- if (await fs.realpath('.') !== parent)
188
- throw new Error('File parent escaped allowed roots');
189
- this.pin = parent;
190
- return await action();
191
- }
192
- finally {
193
- this.pin = undefined;
194
- process.chdir(previous);
195
- this.busy = false;
196
- }
197
- }
198
- leaf(target) {
199
- if (!this.pin)
200
- throw new Error('File directory is not pinned');
201
- if (target === this.pin)
202
- return '.';
203
- if (path.dirname(target) !== this.pin)
204
- throw new Error('File path escaped pinned directory');
205
- return path.basename(target);
206
- }
207
- async dispatch(method, p) {
208
- await this.expire();
209
- let parent;
210
- if (method === 'files.upload.chunk' || method === 'files.upload.finish' || method === 'files.upload.cancel') {
211
- const upload = this.uploads.get(identifier(p, 'uploadId'));
212
- if (!upload && method === 'files.upload.cancel') {
213
- keys(p, ['uploadId']);
214
- return { ok: true };
215
- }
216
- if (!upload)
217
- throw new Error('Upload not found or expired');
218
- parent = path.dirname(upload.target);
219
- }
220
- else {
221
- const target = await this.resolve(string(p, 'path', 4096, method === 'files.list' ? this.home : undefined), method === 'files.mkdir' || method === 'files.upload.begin', !['files.list', 'files.read'].includes(method));
222
- parent = method === 'files.list' ? target : path.dirname(target);
223
- }
224
- return this.pinned(parent, async () => {
225
- if (method.startsWith('files.upload.') && method !== 'files.upload.begin') {
226
- const upload = this.uploads.get(identifier(p, 'uploadId'));
227
- const stat = await fs.stat('.');
228
- if (stat.dev !== upload.parent.dev || stat.ino !== upload.parent.ino) {
229
- if (method === 'files.upload.cancel') {
230
- await this.abandon(upload);
231
- return { ok: true };
232
- }
233
- throw new Error('Upload parent changed');
234
- }
235
- }
236
- return this.perform(method, p);
237
- });
238
- }
239
- async perform(method, p) {
240
- switch (method) {
241
- case 'files.list': {
242
- keys(p, ['path']);
243
- const target = await this.resolve(string(p, 'path', 4096, this.home));
244
- const listing = await fs.opendir(this.leaf(target));
245
- const entries = [];
246
- let visited = 0;
247
- try {
248
- for await (const entry of listing) {
249
- if (++visited > 10000 || entries.length >= 2000)
250
- throw new Error('Directory entry limit exceeded');
251
- if (entry.name.startsWith(TEMP_PREFIX) || entry.isSymbolicLink())
252
- continue;
253
- const entryPath = path.join(target, entry.name);
254
- let stat;
255
- try {
256
- await this.resolve(entryPath);
257
- stat = await fs.lstat(this.leaf(entryPath));
258
- }
259
- catch {
260
- continue;
261
- }
262
- if (!stat.isFile() && !stat.isDirectory())
263
- continue;
264
- entries.push({ name: entry.name, path: entryPath, directory: stat.isDirectory(), size: stat.size, modifiedAt: stat.mtimeMs, createdAt: stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.ctimeMs > 0 ? stat.ctimeMs : stat.mtimeMs });
265
- }
266
- }
267
- finally {
268
- await listing.close().catch(() => { });
269
- }
270
- entries.sort((a, b) => Number(b.directory) - Number(a.directory) || String(a.name).localeCompare(String(b.name)));
271
- const parent = path.dirname(target);
272
- return { path: target, parent: this.roots.includes(target) || !this.roots.some(root => this.inside(parent, root)) ? null : parent, entries, roots: this.roots };
273
- }
274
- case 'files.read': {
275
- keys(p, ['path', 'offset', 'length']);
276
- const offset = integer(p, 'offset', 0, Number.MAX_SAFE_INTEGER, 0);
277
- const length = integer(p, 'length', 1, CHUNK_BYTES, CHUNK_BYTES);
278
- const target = await this.resolve(string(p, 'path'));
279
- const handle = await this.regularHandle(target);
280
- try {
281
- const stat = await handle.stat();
282
- const buffer = Buffer.alloc(length);
283
- const { bytesRead } = await handle.read(buffer, 0, length, offset);
284
- return { data: buffer.subarray(0, bytesRead).toString('base64'), size: stat.size, eof: offset + bytesRead >= stat.size, name: path.basename(target) };
285
- }
286
- finally {
287
- await handle.close();
288
- }
289
- }
290
- case 'files.mkdir': {
291
- keys(p, ['path']);
292
- const target = await this.resolve(string(p, 'path'), true, true);
293
- await fs.mkdir(this.leaf(target));
294
- return { ok: true };
295
- }
296
- case 'files.rename': {
297
- keys(p, ['path', 'name']);
298
- const target = await this.resolve(string(p, 'path'), false, true);
299
- const name = string(p, 'name', 255);
300
- if (name === '.' || name === '..' || /[/\\]/.test(name) || name.startsWith(TEMP_PREFIX))
301
- throw new Error('Invalid new name');
302
- const destination = await this.resolve(path.join(path.dirname(target), name), true, true);
303
- // Node rename() overwrites files. Hard-link commits are exclusive for files; for directories,
304
- // reserve the destination with mkdir and replace only that empty reservation atomically.
305
- const stat = await fs.lstat(this.leaf(target));
306
- if (stat.isFile()) {
307
- await fs.link(this.leaf(target), this.leaf(destination));
308
- await fs.unlink(this.leaf(target));
309
- }
310
- else if (stat.isDirectory()) {
311
- await fs.mkdir(this.leaf(destination), { mode: stat.mode });
312
- try {
313
- await fs.rename(this.leaf(target), this.leaf(destination));
314
- }
315
- catch (error) {
316
- await fs.rmdir(this.leaf(destination)).catch(() => { });
317
- throw error;
318
- }
319
- }
320
- else
321
- throw new Error('Not a regular file or directory');
322
- return { ok: true };
323
- }
324
- case 'files.delete': {
325
- keys(p, ['path']);
326
- const target = await this.resolve(string(p, 'path'), false, true);
327
- const stat = await fs.lstat(this.leaf(target));
328
- if (stat.isDirectory())
329
- await fs.rmdir(this.leaf(target));
330
- else if (stat.isFile())
331
- await fs.unlink(this.leaf(target));
332
- else
333
- throw new Error('Not a regular file or directory');
334
- return { ok: true };
335
- }
336
- case 'files.upload.begin': {
337
- keys(p, ['path', 'size', 'overwrite']);
338
- const size = integer(p, 'size', 0, MAX_UPLOAD_BYTES);
339
- if (p.overwrite !== undefined && typeof p.overwrite !== 'boolean')
340
- throw new Error('Invalid overwrite');
341
- if (this.uploads.size >= MAX_UPLOADS)
342
- throw new Error('Upload limit exceeded');
343
- const target = await this.resolve(string(p, 'path'), true, true);
344
- if (path.basename(target).startsWith(TEMP_PREFIX))
345
- throw new Error('Reserved upload filename');
346
- let existing;
347
- try {
348
- existing = await fs.lstat(this.leaf(target));
349
- }
350
- catch (error) {
351
- if (error.code !== 'ENOENT')
352
- throw error;
353
- }
354
- if (existing && (p.overwrite !== true || !existing.isFile()))
355
- throw new Error('Target already exists');
356
- const id = randomUUID();
357
- const parent = await fs.stat('.');
358
- const root = await this.stagingRoot(parent.dev);
359
- const dir = path.join(root, `${TEMP_PREFIX}${id}`);
360
- const temp = path.join(dir, 'payload');
361
- const manifest = this.stateDir ? path.join(this.stateDir, `${id}.json`) : undefined;
362
- await fs.mkdir(dir, { mode: 0o700 });
363
- let handle;
364
- try {
365
- handle = await fs.open(temp, constants.O_RDWR | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600);
366
- }
367
- catch (error) {
368
- await fs.rmdir(dir).catch(() => { });
369
- throw error;
370
- }
371
- const directory = await fs.lstat(dir);
372
- const inode = await handle.stat();
373
- const dirIdentity = { dev: directory.dev, ino: directory.ino };
374
- const identity = { dev: inode.dev, ino: inode.ino };
375
- try {
376
- if (manifest)
377
- await fs.writeFile(manifest, JSON.stringify({ temp, dir, identity, dirIdentity }), { flag: 'wx', mode: 0o600 });
378
- }
379
- catch (error) {
380
- await handle.close();
381
- await fs.unlink(temp).catch(() => { });
382
- await fs.rmdir(dir).catch(() => { });
383
- throw error;
384
- }
385
- this.uploads.set(id, { id, manifest, target, temp, dir, dirIdentity, identity, handle, size, offset: 0, overwrite: p.overwrite === true, touched: Date.now(), parent: { dev: parent.dev, ino: parent.ino }, chunks: new Map(), commits: new Set() });
386
- return { uploadId: id };
387
- }
388
- case 'files.upload.chunk': {
389
- keys(p, ['uploadId', 'offset', 'data']);
390
- const upload = this.upload(p);
391
- const offset = integer(p, 'offset', 0, MAX_UPLOAD_BYTES);
392
- const bytes = base64(p.data);
393
- const digest = createHash('sha256').update(bytes).digest('hex');
394
- const receipt = upload.chunks.get(offset);
395
- if (receipt && receipt.length === bytes.length && receipt.digest === digest)
396
- return { offset: upload.offset };
397
- if (upload.chunks.size >= 16384)
398
- throw new Error('Upload chunk count limit exceeded');
399
- if (offset !== upload.offset || !bytes.length || offset + bytes.length > upload.size)
400
- throw new Error('Upload offset or size mismatch');
401
- let written = 0;
402
- while (written < bytes.length) {
403
- const result = await upload.handle.write(bytes, written, bytes.length - written, offset + written);
404
- if (!result.bytesWritten)
405
- throw new Error('Upload write failed');
406
- written += result.bytesWritten;
407
- }
408
- upload.chunks.set(offset, { length: bytes.length, digest });
409
- upload.offset += bytes.length;
410
- return { offset: upload.offset };
411
- }
412
- case 'files.upload.finish': {
413
- keys(p, ['uploadId']);
414
- const upload = this.upload(p);
415
- if (upload.offset !== upload.size)
416
- throw new Error('Upload size mismatch');
417
- await this.resolve(upload.target, true, true);
418
- const parent = await fs.stat('.');
419
- if (parent.dev !== upload.parent.dev || parent.ino !== upload.parent.ino)
420
- throw new Error('Upload parent changed');
421
- await upload.handle.sync();
422
- await this.verifyUpload(upload);
423
- const commit = path.join(upload.dir, `commit-${randomUUID()}`);
424
- upload.commits.add(commit);
425
- await fs.link(upload.temp, commit);
426
- // The link may have raced source replacement. Check the actual linked inode before ever
427
- // touching the destination; the final source lives in a trusted 0700 directory.
428
- const linked = await fs.lstat(commit);
429
- const opened = await upload.handle.stat();
430
- if (!linked.isFile() || !this.same(linked, opened))
431
- throw new Error('Upload staging integrity changed');
432
- const verified = await this.verifyUpload(upload);
433
- // No asynchronous yield between the final identity guard and atomic commit. Other-UID
434
- // processes cannot replace the private staging entry; unrestricted same-UID tampering is
435
- // outside this boundary (that UID can also control the keeper/PTY or ptrace its process).
436
- const directory = lstatSync(upload.dir);
437
- const source = lstatSync(commit);
438
- const original = lstatSync(upload.temp);
439
- if (!directory.isDirectory() || !this.same(directory, upload.dirIdentity) || (directory.mode & 0o077)
440
- || !source.isFile() || !original.isFile() || !this.same(source, verified) || !this.same(original, verified)
441
- || source.size !== upload.size || source.mtimeMs !== verified.mtimeMs || source.ctimeMs !== verified.ctimeMs)
442
- throw new Error('Upload commit source changed');
443
- if (upload.overwrite) {
444
- try {
445
- if (!lstatSync(this.leaf(upload.target)).isFile())
446
- throw new Error('Target is not a regular file');
447
- }
448
- catch (error) {
449
- if (error.code !== 'ENOENT')
450
- throw error;
451
- }
452
- renameSync(commit, this.leaf(upload.target));
453
- }
454
- else
455
- linkSync(commit, this.leaf(upload.target));
456
- await this.clean(upload);
457
- return { ok: true, path: upload.target };
458
- }
459
- case 'files.upload.cancel': {
460
- keys(p, ['uploadId']);
461
- const id = identifier(p, 'uploadId');
462
- const upload = this.uploads.get(id);
463
- if (upload)
464
- await this.clean(upload);
465
- return { ok: true };
466
- }
467
- default: throw new Error('Unsupported file tool');
468
- }
469
- }
470
- upload(p) {
471
- const upload = this.uploads.get(identifier(p, 'uploadId'));
472
- if (!upload)
473
- throw new Error('Upload not found or expired');
474
- upload.touched = Date.now();
475
- return upload;
476
- }
477
- async clean(upload) {
478
- this.uploads.delete(upload.id);
479
- await upload.handle.close().catch(() => { });
480
- try {
481
- const directory = await fs.lstat(upload.dir);
482
- if (directory.isDirectory() && this.same(directory, upload.dirIdentity)) {
483
- for (const item of [upload.temp, ...upload.commits]) {
484
- try {
485
- const stat = await fs.lstat(item);
486
- if (stat.isFile() && this.same(stat, upload.identity))
487
- await fs.unlink(item);
488
- }
489
- catch { /* Preserve substituted entries. */ }
490
- }
491
- await fs.rmdir(upload.dir).catch(() => { });
492
- }
493
- }
494
- catch { /* Inaccessible private staging paths are not followed. */ }
495
- if (upload.manifest)
496
- await fs.unlink(upload.manifest).catch(() => { });
497
- }
498
- async abandon(upload) {
499
- this.uploads.delete(upload.id);
500
- await upload.handle.close().catch(() => { });
501
- // Retain the private journal when its original parent cannot be safely resolved for cleanup.
502
- // A later keeper startup can retry; never remove a same-name file in a replacement directory.
503
- }
504
- async expire() {
505
- for (const upload of this.uploads.values())
506
- if (Date.now() - upload.touched >= this.ttl) {
507
- await this.pinned(path.dirname(upload.target), async () => {
508
- const stat = await fs.stat('.');
509
- if (stat.dev !== upload.parent.dev || stat.ino !== upload.parent.ino)
510
- throw new Error('Upload parent changed');
511
- await this.clean(upload);
512
- }).catch(() => this.abandon(upload));
513
- }
514
- }
515
- async dispose() {
516
- for (const upload of this.uploads.values()) {
517
- await this.pinned(path.dirname(upload.target), async () => {
518
- const stat = await fs.stat('.');
519
- if (stat.dev !== upload.parent.dev || stat.ino !== upload.parent.ino)
520
- throw new Error('Upload parent changed');
521
- await this.clean(upload);
522
- }).catch(() => this.abandon(upload));
523
- }
524
- }
525
- }