@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
@@ -0,0 +1,222 @@
1
+ import { accessSync, constants, statSync } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { CREATE_KEYS, PTY_OPERATIONS } from '../keepers/terminal/pty.js';
5
+ import { PTY_SESSION_LIMIT } from '../keepers/terminal/protocol.js';
6
+ import { TERMINAL_NOT_FOUND } from '../keepers/legacy.js';
7
+ import { identifier, integer, keys } from './validation.js';
8
+ const LEGACY = 'legacy';
9
+ /** The single-keeper form of {@link KitGenerations}: one generation named `kit`, running when it answers a hello. */
10
+ function singleKit(kit) {
11
+ return {
12
+ servingGeneration: async () => ({ gen: 'kit', client: kit }),
13
+ running: async () => [{ gen: 'kit', client: kit }],
14
+ };
15
+ }
16
+ export class TerminalRouter {
17
+ deps;
18
+ owners = new Map();
19
+ kits;
20
+ /** Clients of the generations seen in the last listing, by generation. */
21
+ clients = new Map();
22
+ constructor(deps) {
23
+ this.deps = deps;
24
+ if (!deps.kits && !deps.kit)
25
+ throw new Error('TerminalRouter needs keeper-terminal');
26
+ this.kits = deps.kits ?? singleKit(deps.kit);
27
+ }
28
+ /** The current owner map (tests and diagnostics): `legacy`, or a generation (`kit` with a single keeper-terminal). */
29
+ ownerOf(id) { return this.owners.get(id); }
30
+ async request(method, params) {
31
+ const op = method.startsWith('terminal.') ? method.slice('terminal.'.length) : '';
32
+ if (op === 'list') {
33
+ keys(params, []);
34
+ return { sessions: await this.list() };
35
+ }
36
+ if (op === 'create')
37
+ return { ...await this.create(params) };
38
+ if (!PTY_OPERATIONS.has(op))
39
+ throw new Error('Unsupported workstation tool');
40
+ const id = identifier(params, 'id');
41
+ const owner = await this.resolveOwner(id);
42
+ if (!owner)
43
+ throw new Error(TERMINAL_NOT_FOUND);
44
+ const kit = owner === LEGACY ? undefined : this.clients.get(owner);
45
+ if (owner !== LEGACY && !kit)
46
+ throw new Error(TERMINAL_NOT_FOUND);
47
+ try {
48
+ const result = owner === LEGACY
49
+ ? await this.deps.legacy.request(method, params)
50
+ : { ...await kit[op].call(kit, params) };
51
+ if (op === 'close')
52
+ this.owners.delete(id);
53
+ return result;
54
+ }
55
+ catch (error) {
56
+ // The owner no longer has it (closed elsewhere, keeper restarted): forget it; the next request looks it up again.
57
+ if (error instanceof Error && error.message === TERMINAL_NOT_FOUND)
58
+ this.owners.delete(id);
59
+ throw error;
60
+ }
61
+ }
62
+ async list() {
63
+ const { legacy, kits } = await this.refresh();
64
+ const seen = new Set((legacy ?? []).map((session) => session.id));
65
+ const out = [...(legacy ?? [])];
66
+ for (const sessions of kits.values())
67
+ for (const session of sessions ?? [])
68
+ if (!seen.has(session.id)) {
69
+ seen.add(session.id);
70
+ out.push(session);
71
+ }
72
+ return out;
73
+ }
74
+ /** Lists every side once (keeper-terminal generations only if running) and refreshes the owners of every side that answered. */
75
+ async refresh() {
76
+ const [legacy, kits] = await Promise.all([this.legacySessions(), this.kitRefresh()]);
77
+ if (legacy)
78
+ this.replaceOwners(LEGACY, legacy);
79
+ else if (!this.deps.legacy?.live)
80
+ this.replaceOwners(LEGACY, []);
81
+ return { legacy, kits };
82
+ }
83
+ /**
84
+ * Lists every running keeper-terminal generation (never starts one) and refreshes their owners: a generation that answered gets
85
+ * exactly its sessions; one that did not answer keeps its owners; one that is no longer running loses them (its PTYs are gone).
86
+ */
87
+ async kitRefresh() {
88
+ const listed = new Map();
89
+ let running;
90
+ try {
91
+ running = await this.kits.running();
92
+ }
93
+ catch {
94
+ return listed;
95
+ }
96
+ await Promise.all(running.map(async ({ gen, client }) => {
97
+ this.clients.set(gen, client);
98
+ listed.set(gen, await this.sessionsOf(client));
99
+ }));
100
+ for (const [gen, sessions] of listed)
101
+ if (sessions)
102
+ this.replaceOwners(gen, sessions);
103
+ for (const [id, owner] of [...this.owners])
104
+ if (owner !== LEGACY && !listed.has(owner))
105
+ this.owners.delete(id);
106
+ for (const gen of [...this.clients.keys()])
107
+ if (!listed.has(gen))
108
+ this.clients.delete(gen);
109
+ return listed;
110
+ }
111
+ replaceOwners(side, sessions) {
112
+ for (const [id, owner] of this.owners)
113
+ if (owner === side)
114
+ this.owners.delete(id);
115
+ for (const session of sessions)
116
+ if (side === LEGACY || !this.owners.has(session.id))
117
+ this.owners.set(session.id, side);
118
+ }
119
+ /** The legacy keeper's sessions; `sessions()` probes it first (read-only), so a daemon that just started finds it too. */
120
+ async legacySessions() {
121
+ return this.deps.legacy ? this.deps.legacy.sessions() : null;
122
+ }
123
+ /** One generation's sessions if it is already running; null when it is not (or did not answer): nothing is started here. */
124
+ async sessionsOf(client) {
125
+ try {
126
+ if (!await client.runningHello())
127
+ return null;
128
+ return (await client.list()).sessions;
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ }
134
+ async resolveOwner(id) {
135
+ const known = this.owners.get(id);
136
+ if ((known !== undefined && known !== LEGACY && this.clients.has(known)) || (known === LEGACY && this.deps.legacy?.live))
137
+ return known;
138
+ await this.refresh();
139
+ return this.owners.get(id);
140
+ }
141
+ async create(params) {
142
+ // The legacy keeper's checks, wording and order (tools/terminal.ts at the merge-base: id, clientId, keys, cols, rows, name,
143
+ // then existing, limit, cwd), all before the cwd is resolved.
144
+ const id = identifier(params, 'id');
145
+ identifier(params, 'clientId');
146
+ keys(params, [...CREATE_KEYS]);
147
+ integer(params, 'cols', 20, 300, 100);
148
+ integer(params, 'rows', 5, 100, 30);
149
+ const name = params.name;
150
+ if (name !== undefined && (typeof name !== 'string' || !name.length || name.length > 128 || /[\x00-\x1f]/.test(name)))
151
+ throw new Error('Invalid terminal name');
152
+ const legacy = this.deps.legacy;
153
+ const knownOwner = this.owners.get(id);
154
+ if (legacy && (knownOwner === undefined || knownOwner === LEGACY)) {
155
+ // The legacy keeper never gains a session after the upgrade (creates go to keeper-terminal), so the ids of its first
156
+ // successful list answer "is this a retry of one of its sessions?" for good — even while it is busy or hung.
157
+ if (legacy.knownIds === null) {
158
+ const sessions = await legacy.sessions(); // bounded; null at once when it is busy or absent
159
+ if (sessions)
160
+ this.replaceOwners(LEGACY, sessions);
161
+ }
162
+ if (legacy.live && legacy.knownIds?.has(id))
163
+ return legacy.request('terminal.create', params);
164
+ }
165
+ // Duplicate and limit first (the legacy order), against the keeper-terminal generations already running: an idempotent create
166
+ // (in whichever generation has the id) costs no files-host round trip. keeper-terminal re-checks both when it really creates.
167
+ const listed = await this.kitRefresh();
168
+ for (const [gen, sessions] of listed) {
169
+ const existing = sessions?.find((session) => session.id === id);
170
+ if (existing) {
171
+ this.owners.set(id, gen);
172
+ return existing;
173
+ }
174
+ }
175
+ const serving = await this.kits.servingGeneration();
176
+ const running = listed.get(serving.gen);
177
+ if (running && running.length >= PTY_SESSION_LIMIT)
178
+ throw new Error('Terminal session limit exceeded');
179
+ if (params.cwd !== undefined && typeof params.cwd !== 'string')
180
+ throw new Error('Invalid cwd');
181
+ const launch = await this.deps.launch(params.cwd);
182
+ const created = await serving.client.create({ ...params, ...launch });
183
+ this.clients.set(serving.gen, serving.client);
184
+ this.owners.set(id, serving.gen);
185
+ return created;
186
+ }
187
+ }
188
+ /**
189
+ * The shell a new PTY runs (as a login shell, `-l`): what the legacy keeper chose (`$SHELL`, else the account's shell, else
190
+ * `/bin/sh`), restricted to an absolute path to an executable file, which keeper-terminal requires.
191
+ */
192
+ export function terminalShell(env = process.env) {
193
+ let account;
194
+ try {
195
+ account = os.userInfo().shell ?? undefined;
196
+ }
197
+ catch { /* no passwd entry (some containers) */ }
198
+ for (const candidate of [env.SHELL, account, '/bin/sh']) {
199
+ if (!candidate || !path.isAbsolute(candidate))
200
+ continue;
201
+ try {
202
+ if (statSync(candidate).isFile()) {
203
+ accessSync(candidate, constants.X_OK);
204
+ return candidate;
205
+ }
206
+ }
207
+ catch { /* next */ }
208
+ }
209
+ return '/bin/sh';
210
+ }
211
+ /**
212
+ * The environment a new PTY gets: the daemon's own (the legacy keeper gave its own process.env, which was the daemon's when it
213
+ * was spawned), never the access token (commands/workstation.ts already drops it from the daemon; dropped again here so a
214
+ * future spawn path cannot leak it). keeper-terminal adds TERM and COLORTERM.
215
+ */
216
+ export function terminalEnv(env = process.env) {
217
+ const out = {};
218
+ for (const [name, value] of Object.entries(env))
219
+ if (value !== undefined && name !== 'AXOM_ACCESS_TOKEN')
220
+ out[name] = value;
221
+ return out;
222
+ }
@@ -6,7 +6,9 @@ export const TOOL_METHODS = new Set([
6
6
  'browser.list', 'browser.create', 'browser.attach', 'browser.frame', 'browser.navigate', 'browser.input', 'browser.takeover', 'browser.detach', 'browser.close',
7
7
  'tools.info', 'terminal.list', 'terminal.create', 'terminal.attach', 'terminal.poll',
8
8
  'terminal.write', 'terminal.resize', 'terminal.takeover', 'terminal.detach', 'terminal.close',
9
- 'files.list', 'files.read', 'files.mkdir', 'files.rename', 'files.delete',
9
+ 'files.media.prepare', 'files.media.read', 'files.media.cancel',
10
+ 'files.media.open', 'files.media.status', 'files.media.seek', 'files.media.interest', 'files.media.object', 'files.media.release',
11
+ 'files.list', 'files.find', 'files.read', 'files.mkdir', 'files.rename', 'files.delete',
10
12
  'files.upload.begin', 'files.upload.chunk', 'files.upload.finish', 'files.upload.cancel',
11
13
  ]);
12
14
  export function object(value) {
@@ -47,4 +49,38 @@ export function validateRequest(method, params) {
47
49
  throw new Error('Unsupported workstation tool');
48
50
  if (!object(params) || Buffer.byteLength(JSON.stringify(params)) > TOOL_REQUEST_BYTES)
49
51
  throw new Error('Invalid or oversized tool parameters');
52
+ const shape = MEDIA_SHAPES[method];
53
+ if (shape) {
54
+ keys(params, [...shape, 'readOnly', 'filesystem']);
55
+ string(params, 'path');
56
+ identifier(params, 'consumerId');
57
+ if (method !== 'files.media.open' && !/^[a-f0-9]{64}$/.test(string(params, 'version', 64)))
58
+ throw new Error('Invalid version');
59
+ for (const flag of ['readOnly', 'filesystem', 'hls', 'playing']) {
60
+ if (params[flag] !== undefined && typeof params[flag] !== 'boolean')
61
+ throw new Error(`Invalid ${flag}`);
62
+ }
63
+ if (method === 'files.media.open' && typeof params.hls !== 'boolean')
64
+ throw new Error('Invalid hls');
65
+ if (method === 'files.media.seek' || method === 'files.media.interest')
66
+ integer(params, 'positionMs', 0, 21_600_000);
67
+ if (method === 'files.media.seek')
68
+ integer(params, 'revision', 0, Number.MAX_SAFE_INTEGER);
69
+ if (method === 'files.media.interest' && typeof params.playing !== 'boolean')
70
+ throw new Error('Invalid playing');
71
+ if (method === 'files.media.object') {
72
+ if (!/^[A-Za-z0-9_-]+$/.test(string(params, 'objectId', 128)))
73
+ throw new Error('Invalid objectId');
74
+ integer(params, 'offset', 0, Number.MAX_SAFE_INTEGER);
75
+ integer(params, 'length', 1, 256 * 1024);
76
+ }
77
+ }
50
78
  }
79
+ const MEDIA_SHAPES = {
80
+ 'files.media.open': ['path', 'consumerId', 'hls'],
81
+ 'files.media.status': ['path', 'version', 'consumerId'],
82
+ 'files.media.seek': ['path', 'version', 'consumerId', 'positionMs', 'revision'],
83
+ 'files.media.interest': ['path', 'version', 'consumerId', 'positionMs', 'playing'],
84
+ 'files.media.object': ['path', 'version', 'consumerId', 'objectId', 'offset', 'length'],
85
+ 'files.media.release': ['path', 'version', 'consumerId'],
86
+ };
@@ -1,3 +1,4 @@
1
+ import { deploymentDownloadURL } from '../control/serverIdentity.js';
1
2
  import { createHash, timingSafeEqual } from 'node:crypto';
2
3
  import { createWriteStream, promises as fs } from 'node:fs';
3
4
  import { tmpdir } from 'node:os';
@@ -16,7 +17,7 @@ function safeRelease(release, server) {
16
17
  if ((configured.protocol !== 'http:' && configured.protocol !== 'https:') || configured.username || configured.password) {
17
18
  throw new Error('Configured server URL must be an HTTP(S) origin without credentials');
18
19
  }
19
- const url = new URL(release.url, configured);
20
+ const url = new URL(deploymentDownloadURL(server, release.url));
20
21
  // The release descriptor arrives over the authenticated management connection, but keeping the
21
22
  // download on that exact origin prevents an injected relative/absolute URL from turning the
22
23
  // client into a credentialed downloader. HTTP remains supported for private-network development
@@ -253,6 +253,9 @@ export class UpdateCoordinator {
253
253
  this.options.tasks.accepting = true;
254
254
  return this.transition(this.base(command, 'SUCCEEDED'));
255
255
  }
256
+ // Keeper-hosted agent sessions outlive this process: detach them first (head checkpoint, adapter untouched — no
257
+ // `session/cancel`, no failed turn), so the next daemon adopts them where they are (spec §5.5.5).
258
+ await this.options.tasks.detachDurableSessions();
256
259
  await this.options.restart();
257
260
  return restarting;
258
261
  }
@@ -0,0 +1,116 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ /**
5
+ * `npm install --global <archive>` reinstalls an already-present package by renaming the old
6
+ * install directory aside ("retire") and extracting the new one in its place. On at least one
7
+ * observed host (a container whose target directory sits on the image's read-only layers, backed
8
+ * by `fuse-overlayfs`) that retire-then-extract sequence is unreliable: `npm`'s own tar/arborist
9
+ * step intermittently reports `TAR_ENTRY_ERROR EEXIST` on the package's top-level `package.json`,
10
+ * immediately followed by `ENOENT` when npm reads the same path back — on the *same* path, even
11
+ * starting from an empty directory, and reproducibly across repeated attempts. A plain `tar`
12
+ * extraction of the identical archive onto the identical path, and a non-global `npm install` run
13
+ * inside an already-extracted directory, both succeed every time: the bug is specific to npm's
14
+ * own global-package retire/link machinery, not to the archive, the dependencies, or the
15
+ * filesystem's ability to write there at all.
16
+ *
17
+ * This is therefore a fallback, not the primary path: it only runs after a real `npm install
18
+ * --global` attempt has already failed, and it reimplements only what that attempt was observably
19
+ * unable to do reliably --- replace the target directory's contents and its `bin` entry --- using
20
+ * the two operations proven to work. It intentionally does not run on Windows, where npm's global
21
+ * `bin` entries are shim files (`.cmd`/`.ps1`), not symlinks, and this path has no evidence either
22
+ * way.
23
+ */
24
+ export async function attemptGlobalInstallFallback(npmPath, npmPrefix, archive, env, spawn = nodeSpawn) {
25
+ if (process.platform === 'win32')
26
+ throw new Error('No tested fallback for a global npm install on Windows');
27
+ const manifest = await readArchivePackageJson(archive, spawn);
28
+ const name = manifest.name;
29
+ if (typeof name !== 'string' || name.length === 0)
30
+ throw new Error('Archive package.json has no usable "name"');
31
+ const bin = manifest.bin;
32
+ const target = path.join(npmPrefix, 'lib/node_modules', name);
33
+ await fs.rm(target, { recursive: true, force: true });
34
+ await fs.mkdir(target, { recursive: true });
35
+ await run(spawn, 'tar', ['-xzf', archive, '-C', target, '--strip-components=1'], {});
36
+ const installEnv = { ...process.env, ...env };
37
+ delete installEnv.NPM_CONFIG_PREFIX;
38
+ delete installEnv.npm_config_prefix;
39
+ await run(spawn, npmPath, ['install', '--omit=dev', '--ignore-scripts', '--no-audit', '--no-fund'], { cwd: target, env: installEnv });
40
+ await relinkBinaries(npmPrefix, target, name, bin);
41
+ }
42
+ async function readArchivePackageJson(archive, spawn) {
43
+ const raw = await capture(spawn, 'tar', ['-xzf', archive, '-O', 'package/package.json'], {});
44
+ try {
45
+ return JSON.parse(raw);
46
+ }
47
+ catch {
48
+ throw new Error('Archive package.json is not valid JSON');
49
+ }
50
+ }
51
+ /** Reproduces npm's own `bin` linking: a relative symlink from `<prefix>/bin/<name>` to the script. */
52
+ async function relinkBinaries(npmPrefix, target, packageName, bin) {
53
+ const entries = binEntries(packageName, bin);
54
+ if (entries.length === 0)
55
+ return;
56
+ const binDir = path.join(npmPrefix, 'bin');
57
+ await fs.mkdir(binDir, { recursive: true });
58
+ for (const [command, relativeScript] of entries) {
59
+ const script = path.join(target, relativeScript);
60
+ const link = path.join(binDir, command);
61
+ await fs.rm(link, { force: true });
62
+ await fs.symlink(path.relative(binDir, script), link);
63
+ await fs.chmod(script, 0o755).catch(() => undefined);
64
+ }
65
+ }
66
+ /** Exported for direct unit testing; same resolution npm itself applies to a package.json `bin` field. */
67
+ export function binEntries(packageName, bin) {
68
+ if (typeof bin === 'string') {
69
+ const short = packageName.startsWith('@') ? packageName.split('/')[1] ?? packageName : packageName;
70
+ return [[short, bin]];
71
+ }
72
+ if (bin && typeof bin === 'object') {
73
+ return Object.entries(bin).filter((entry) => typeof entry[1] === 'string');
74
+ }
75
+ return [];
76
+ }
77
+ function run(spawn, command, args, options) {
78
+ return new Promise((resolve, reject) => {
79
+ let child;
80
+ try {
81
+ child = spawn(command, args, { ...options, shell: false, stdio: 'ignore', windowsHide: true });
82
+ }
83
+ catch (error) {
84
+ reject(error instanceof Error ? error : new Error(String(error)));
85
+ return;
86
+ }
87
+ child.once('error', reject);
88
+ child.once('close', (code, signal) => {
89
+ if (code === 0)
90
+ resolve();
91
+ else
92
+ reject(new Error(`${command} failed: exit ${code ?? signal ?? 'unknown'}`));
93
+ });
94
+ });
95
+ }
96
+ function capture(spawn, command, args, options) {
97
+ return new Promise((resolve, reject) => {
98
+ let child;
99
+ try {
100
+ child = spawn(command, args, { ...options, shell: false, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
101
+ }
102
+ catch (error) {
103
+ reject(error instanceof Error ? error : new Error(String(error)));
104
+ return;
105
+ }
106
+ let stdout = '';
107
+ child.stdout?.on('data', (chunk) => { stdout += chunk.toString(); });
108
+ child.once('error', reject);
109
+ child.once('close', (code, signal) => {
110
+ if (code === 0)
111
+ resolve(stdout);
112
+ else
113
+ reject(new Error(`${command} failed: exit ${code ?? signal ?? 'unknown'}`));
114
+ });
115
+ });
116
+ }
@@ -1,6 +1,8 @@
1
+ import { withWorkstationLock } from '../control/locks.js';
1
2
  import { spawn as nodeSpawn } from 'node:child_process';
2
3
  import { redactPaths, redactSecrets } from './redact.js';
3
4
  import { verifyOptionalPty } from './watchdog.js';
5
+ import { attemptGlobalInstallFallback } from './globalInstall.js';
4
6
  const STDERR_LIMIT = 4096;
5
7
  function absolute(value, field) {
6
8
  if (!value.startsWith('/') || /[\u0000-\u001f\u007f]/.test(value))
@@ -21,33 +23,57 @@ export class NpmInstaller {
21
23
  this.spawn = options.spawn ?? nodeSpawn;
22
24
  }
23
25
  async install(archive) {
26
+ if (this.options.configDir && !this.options.installationLockHeld)
27
+ return withWorkstationLock(this.options.configDir, 'installation', () => this.installUnlocked(archive));
28
+ return this.installUnlocked(archive);
29
+ }
30
+ async installUnlocked(archive) {
24
31
  absolute(archive, 'archive');
25
- // No `--prefix`: npm installs into its own configured global prefix, which is what puts `axom`
26
- // on the user's PATH. Forcing a private prefix put the binary somewhere nothing looks, so every
27
- // machine needed a hand-made symlink that nothing in this package created or documented.
28
- // `npmPrefix` survives for the writability pre-check, the service definition, and rollback —
29
- // it is derived from where this package actually sits, so it still names npm's real prefix.
32
+ // Pin the already selected installation, including ordinary global npm installs.
33
+ const env = { ...process.env, ...this.options.env, NPM_CONFIG_PREFIX: this.options.npmPrefix };
34
+ delete env.npm_config_prefix;
30
35
  const args = ['install', '--global', archive, '--ignore-scripts', '--no-audit', '--no-fund'];
31
- await new Promise((resolve, reject) => {
32
- let stderr = '';
33
- let child;
36
+ try {
37
+ await new Promise((resolve, reject) => {
38
+ let stderr = '';
39
+ let child;
40
+ try {
41
+ child = this.spawn(this.options.npmPath, args, { env, shell: false, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
42
+ }
43
+ catch (error) {
44
+ reject(new Error(`npm install could not start: ${sanitizeNpmError(error instanceof Error ? error.message : String(error))}`));
45
+ return;
46
+ }
47
+ child.stderr?.on('data', (chunk) => { if (stderr.length < STDERR_LIMIT)
48
+ stderr += chunk.toString().slice(0, STDERR_LIMIT - stderr.length); });
49
+ child.once('error', (error) => reject(new Error(`npm install could not start: ${sanitizeNpmError(error.message)}`)));
50
+ child.once('close', (code, signal) => {
51
+ if (code === 0)
52
+ resolve();
53
+ else
54
+ reject(new Error(`npm install failed: ${sanitizeNpmError(stderr) || `exit ${code ?? signal ?? 'unknown'}`}`));
55
+ });
56
+ });
57
+ }
58
+ catch (primaryError) {
59
+ // `npm install --global` reinstalling an already-present package renames the old install
60
+ // directory aside and extracts the new one in its place; that sequence has been observed to
61
+ // fail unreliably on at least one host/filesystem combination even from an empty target (see
62
+ // globalInstall.ts). The fallback reproduces only the two operations proven to work there. A
63
+ // fallback failure surfaces the *original* error: it is the one a real npm bug report needs.
64
+ //
65
+ // Both console lines below are the only record of how often this fallback actually fires
66
+ // fleet-wide — there is no telemetry channel for "the primary path failed but we recovered",
67
+ // and a daemon's own stdout/stderr is already collected wherever this process's logs are.
68
+ console.warn(`npm install --global failed, attempting the tar/local-install fallback: ${sanitizeNpmError(primaryError instanceof Error ? primaryError.message : String(primaryError))}`);
34
69
  try {
35
- child = this.spawn(this.options.npmPath, args, { shell: false, stdio: ['ignore', 'ignore', 'pipe'], windowsHide: true });
70
+ await attemptGlobalInstallFallback(this.options.npmPath, this.options.npmPrefix, archive, this.options.env, this.spawn);
36
71
  }
37
- catch (error) {
38
- reject(new Error(`npm install could not start: ${sanitizeNpmError(error instanceof Error ? error.message : String(error))}`));
39
- return;
72
+ catch {
73
+ throw primaryError;
40
74
  }
41
- child.stderr?.on('data', (chunk) => { if (stderr.length < STDERR_LIMIT)
42
- stderr += chunk.toString().slice(0, STDERR_LIMIT - stderr.length); });
43
- child.once('error', (error) => reject(new Error(`npm install could not start: ${sanitizeNpmError(error.message)}`)));
44
- child.once('close', (code, signal) => {
45
- if (code === 0)
46
- resolve();
47
- else
48
- reject(new Error(`npm install failed: ${sanitizeNpmError(stderr) || `exit ${code ?? signal ?? 'unknown'}`}`));
49
- });
50
- });
75
+ console.warn('npm install --global fallback succeeded; the global reinstall step is unreliable on this host and should be investigated');
76
+ }
51
77
  await verifyOptionalPty(this.options.npmPrefix, this.spawn);
52
78
  }
53
79
  /** Packs the installed package locally; `--json` gives npm's own name/version validation input. */
@@ -60,7 +86,7 @@ export class NpmInstaller {
60
86
  let stderr = '';
61
87
  let child;
62
88
  try {
63
- child = this.spawn(this.options.npmPath, args, { shell: false, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
89
+ child = this.spawn(this.options.npmPath, args, { ...(this.options.env ? { env: this.options.env } : {}), shell: false, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true });
64
90
  }
65
91
  catch (error) {
66
92
  reject(new Error(`npm pack could not start: ${sanitizeNpmError(error instanceof Error ? error.message : String(error))}`));
@@ -1,5 +1,6 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import path from 'node:path';
3
+ import { PTY_UNAVAILABLE_ERROR } from '../keepers/terminal/protocol.js';
3
4
  const require = createRequire(import.meta.url);
4
5
  /**
5
6
  * Says once, at daemon start, when the installed CLI's optional `node-pty` does not load. There is
@@ -22,3 +23,23 @@ export async function preflightOptionalPty(context, deps = {}) {
22
23
  (deps.warn ?? console.warn)('[daemon] Optional node-pty does not load on this platform; the terminal tool stays unavailable.');
23
24
  }
24
25
  }
26
+ export { PTY_UNAVAILABLE_ERROR };
27
+ /**
28
+ * A cached load check of the node-pty copy the terminal would use (the same `require('node-pty')` from this package),
29
+ * done at most once per daemon. Answering `tools.info` must never start a keeper just to learn this.
30
+ */
31
+ export function cachedPtyStatus(load = () => require('node-pty')) {
32
+ let status;
33
+ return () => {
34
+ if (status)
35
+ return status;
36
+ try {
37
+ load();
38
+ status = { terminal: true };
39
+ }
40
+ catch {
41
+ status = { terminal: false, terminalError: PTY_UNAVAILABLE_ERROR };
42
+ }
43
+ return status;
44
+ };
45
+ }