@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
@@ -2,8 +2,8 @@ import { execFile } from 'node:child_process';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { readFileSync, statfsSync, statSync } from 'node:fs';
4
4
  import os from 'node:os';
5
- import path from 'node:path';
6
5
  import { promisify } from 'node:util';
6
+ import { discoverStoragePools } from './storageMetrics.js';
7
7
  const execFileAsync = promisify(execFile);
8
8
  export const STORAGE_TOPOLOGY_REFRESH_MS = 10 * 60 * 1000;
9
9
  function cpuTotals(cpus) {
@@ -36,45 +36,6 @@ function linuxMemoryPressure(readFile) {
36
36
  function stablePoolId(key) {
37
37
  return createHash('sha256').update(key).digest('hex').slice(0, 16);
38
38
  }
39
- async function defaultIdentifyStorage(platform, cwd, device) {
40
- try {
41
- if (platform === 'darwin') {
42
- const mounted = await execFileAsync('/bin/df', ['-P', cwd], {
43
- timeout: 1_000,
44
- env: { ...process.env, LC_ALL: 'C' },
45
- });
46
- const mountFields = mounted.stdout.trim().split('\n').at(-1)?.trim().split(/\s+/) ?? [];
47
- const mountPoint = mountFields.slice(5).join(' ') || path.parse(cwd).root;
48
- const { stdout } = await execFileAsync('/usr/sbin/diskutil', ['info', mountPoint], {
49
- timeout: 1_000,
50
- env: { ...process.env, LC_ALL: 'C' },
51
- });
52
- const container = /^\s*APFS Container Reference:\s*(\S+)\s*$/m.exec(stdout)?.[1];
53
- const volume = /^\s*Volume Name:\s*(.+?)\s*$/m.exec(stdout)?.[1];
54
- return container
55
- ? { key: `apfs:${container}`, label: `APFS Container ${container}` }
56
- : { key: `dev:${device}`, label: volume };
57
- }
58
- if (platform === 'linux') {
59
- const { stdout } = await execFileAsync('findmnt', ['-n', '-P', '-o', 'FSTYPE,SOURCE,UUID,LABEL,TARGET', '--target', cwd], {
60
- timeout: 2_000,
61
- env: { ...process.env, LC_ALL: 'C' },
62
- });
63
- const fields = Object.fromEntries([...stdout.matchAll(/(\w+)="([^"]*)"/g)].map((match) => [match[1], match[2]]));
64
- if (fields.FSTYPE === 'btrfs' && fields.UUID)
65
- return { key: `btrfs:${fields.UUID}`, label: fields.SOURCE };
66
- if (fields.FSTYPE === 'zfs' && fields.SOURCE) {
67
- const pool = fields.SOURCE.split('/')[0];
68
- return { key: `zfs:${pool}`, label: pool };
69
- }
70
- return { key: `dev:${device}`, label: fields.LABEL || fields.TARGET || fields.SOURCE };
71
- }
72
- }
73
- catch {
74
- // Resource-pool recognition is best-effort. Device identity below is the conservative fallback.
75
- }
76
- return { key: `dev:${device}` };
77
- }
78
39
  async function defaultDarwinAvailableMemory() {
79
40
  try {
80
41
  const { stdout } = await execFileAsync('/usr/bin/vm_stat', [], { timeout: 2_000 });
@@ -100,6 +61,97 @@ async function defaultDarwinMemoryPressure() {
100
61
  return null;
101
62
  }
102
63
  }
64
+ /**
65
+ * Parses `ioreg -r -d 1 -w 0 -c AGXAccelerator` output. Apple Silicon publishes live GPU
66
+ * utilization (`Device Utilization %`, a driver-side running average) and GPU-resident unified
67
+ * memory (`In use system memory`) in each accelerator's `PerformanceStatistics`, without any
68
+ * privileges. Intel Macs expose no `AGXAccelerator` class, so they parse to null.
69
+ */
70
+ export function parseAppleAgxStats(output) {
71
+ const gpus = output.split('+-o ').slice(1).map((block, index) => {
72
+ const stats = /"PerformanceStatistics"\s*=\s*\{([^}]*)\}/.exec(block)?.[1];
73
+ if (stats === undefined)
74
+ return null;
75
+ const utilizationPercent = Number(/"Device Utilization %"\s*=\s*(\d+(?:\.\d+)?)/.exec(stats)?.[1]);
76
+ const memoryUsedBytes = Number(/"In use system memory"\s*=\s*(\d+)/.exec(stats)?.[1]);
77
+ return {
78
+ id: `apple:agx-${index}`,
79
+ name: /"model"\s*=\s*"([^"]+)"/.exec(block)?.[1] ?? 'Apple GPU',
80
+ utilization: Number.isFinite(utilizationPercent)
81
+ ? Math.max(0, Math.min(1, utilizationPercent / 100))
82
+ : null,
83
+ memoryTotalBytes: null,
84
+ memoryUsedBytes: Number.isFinite(memoryUsedBytes) && memoryUsedBytes >= 0 ? memoryUsedBytes : null,
85
+ coreCount: (() => {
86
+ const cores = Number(/"gpu-core-count"\s*=\s*(\d+)/.exec(block)?.[1]);
87
+ return Number.isFinite(cores) && cores > 0 ? cores : null;
88
+ })(),
89
+ };
90
+ }).filter((gpu) => gpu !== null);
91
+ return gpus.length > 0 ? gpus : null;
92
+ }
93
+ async function appleAgxGpuProbe() {
94
+ try {
95
+ const { stdout } = await execFileAsync('/usr/sbin/ioreg', ['-r', '-d', '1', '-w', '0', '-c', 'AGXAccelerator'], {
96
+ timeout: 2_000,
97
+ maxBuffer: 4 * 1024 * 1024,
98
+ });
99
+ return parseAppleAgxStats(stdout);
100
+ }
101
+ catch {
102
+ return null;
103
+ }
104
+ }
105
+ /** Parses `nvidia-smi --query-gpu=... --format=csv,noheader,nounits` output; `[N/A]` reads as null. */
106
+ export function parseNvidiaSmiGpus(output) {
107
+ const mib = (field) => {
108
+ if (field === undefined)
109
+ return null;
110
+ const value = Number(field);
111
+ return Number.isFinite(value) && value >= 0 ? Math.round(value * 1024 * 1024) : null;
112
+ };
113
+ const gpus = output.split(/\r?\n/).map((line) => {
114
+ const fields = line.split(',').map((field) => field.trim());
115
+ if (fields.length < 6)
116
+ return null;
117
+ const [, uuid, name, utilizationField, totalField, usedField] = fields;
118
+ if (!/^GPU-/.test(uuid) || name.length === 0)
119
+ return null;
120
+ const utilizationPercent = Number(utilizationField);
121
+ return {
122
+ id: `nvidia:${uuid}`,
123
+ name,
124
+ utilization: Number.isFinite(utilizationPercent)
125
+ ? Math.max(0, Math.min(1, utilizationPercent / 100))
126
+ : null,
127
+ memoryTotalBytes: mib(totalField),
128
+ memoryUsedBytes: mib(usedField),
129
+ coreCount: null,
130
+ };
131
+ }).filter((gpu) => gpu !== null);
132
+ return gpus.length > 0 ? gpus : null;
133
+ }
134
+ async function nvidiaSmiGpuProbe() {
135
+ try {
136
+ const { stdout } = await execFileAsync('nvidia-smi', [
137
+ '--query-gpu=index,uuid,name,utilization.gpu,memory.total,memory.used',
138
+ '--format=csv,noheader,nounits',
139
+ ], { timeout: 3_000 });
140
+ return parseNvidiaSmiGpus(stdout);
141
+ }
142
+ catch {
143
+ // Absent binary, driver hiccup, or unsupported GPU -- all read as "no reading", never as failure.
144
+ return null;
145
+ }
146
+ }
147
+ async function defaultProbeGpus(platform) {
148
+ const [apple, nvidia] = await Promise.all([
149
+ platform === 'darwin' ? appleAgxGpuProbe() : Promise.resolve(null),
150
+ nvidiaSmiGpuProbe(),
151
+ ]);
152
+ const gpus = [...(apple ?? []), ...(nvidia ?? [])];
153
+ return gpus.length > 0 ? gpus : null;
154
+ }
103
155
  /**
104
156
  * Low-overhead host sampler. CPU and memory are read only while a heartbeat is being built; storage
105
157
  * topology is discovered out of band and capacity reads use the cached representative paths.
@@ -114,6 +166,8 @@ export class SystemMetricsSampler {
114
166
  darwinMemoryRefreshInFlight = null;
115
167
  darwinMemoryAvailable = null;
116
168
  darwinMemoryPressure = null;
169
+ gpuRefreshInFlight = null;
170
+ gpus = null;
117
171
  constructor(workspaces, deps = {
118
172
  platform: process.platform,
119
173
  cpus: os.cpus,
@@ -123,15 +177,16 @@ export class SystemMetricsSampler {
123
177
  readFile: readFileSync,
124
178
  statfs: statfsSync,
125
179
  stat: statSync,
126
- identifyStorage: (cwd, device) => defaultIdentifyStorage(process.platform, cwd, device),
180
+ discoverStorage: () => discoverStoragePools(process.platform, workspaces),
127
181
  darwinAvailableMemory: defaultDarwinAvailableMemory,
128
182
  darwinMemoryPressure: defaultDarwinMemoryPressure,
183
+ probeGpus: () => defaultProbeGpus(process.platform),
129
184
  }) {
130
185
  this.workspaces = workspaces;
131
186
  this.deps = deps;
132
187
  }
133
188
  async start() {
134
- await Promise.all([this.refreshTopology(), this.refreshDarwinMemory()]);
189
+ await Promise.all([this.refreshTopology(), this.refreshDarwinMemory(), this.refreshGpus()]);
135
190
  this.refreshTimer = setInterval(() => this.refreshTopologyInBackground(), STORAGE_TOPOLOGY_REFRESH_MS);
136
191
  this.refreshTimer.unref();
137
192
  }
@@ -143,6 +198,7 @@ export class SystemMetricsSampler {
143
198
  snapshot() {
144
199
  try {
145
200
  this.refreshDarwinMemoryInBackground();
201
+ this.refreshGpusInBackground();
146
202
  const cpus = this.deps.cpus();
147
203
  const currentCpu = cpuTotals(cpus);
148
204
  const previousCpu = this.previousCpu;
@@ -166,20 +222,23 @@ export class SystemMetricsSampler {
166
222
  : (this.deps.platform === 'darwin' ? this.darwinMemoryPressure : null);
167
223
  const pools = new Map();
168
224
  for (const pool of this.storage) {
169
- try {
170
- const stats = this.deps.statfs(pool.cwd);
171
- const totalBytes = Number(stats.blocks) * Number(stats.bsize);
172
- const availableBytes = Number(stats.bavail) * Number(stats.bsize);
173
- pools.set(pool.key, {
174
- id: stablePoolId(pool.key),
175
- label: pool.label,
176
- workspaces: pool.workspaces,
177
- totalBytes,
178
- availableBytes,
179
- });
180
- }
181
- catch {
182
- // A removed/unmounted workspace must not suppress CPU and memory metrics.
225
+ for (const cwd of pool.paths) {
226
+ try {
227
+ if (String(this.deps.stat(cwd).dev) !== pool.devices[cwd])
228
+ continue;
229
+ const stats = this.deps.statfs(cwd);
230
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
231
+ const availableBytes = Number(stats.bavail) * Number(stats.bsize);
232
+ pools.set(pool.key, {
233
+ id: stablePoolId(pool.key),
234
+ label: pool.label,
235
+ workspaces: pool.workspaces,
236
+ totalBytes,
237
+ availableBytes,
238
+ });
239
+ break;
240
+ }
241
+ catch { }
183
242
  }
184
243
  }
185
244
  return {
@@ -192,6 +251,7 @@ export class SystemMetricsSampler {
192
251
  memoryTotalBytes,
193
252
  memoryAvailableBytes,
194
253
  storagePools: [...pools.values()],
254
+ gpus: this.gpus,
195
255
  };
196
256
  }
197
257
  catch {
@@ -218,6 +278,19 @@ export class SystemMetricsSampler {
218
278
  this.darwinMemoryRefreshInFlight = null;
219
279
  });
220
280
  }
281
+ refreshGpusInBackground() {
282
+ if (this.gpuRefreshInFlight)
283
+ return;
284
+ const refresh = this.refreshGpus();
285
+ this.gpuRefreshInFlight = refresh;
286
+ void refresh.finally(() => {
287
+ if (this.gpuRefreshInFlight === refresh)
288
+ this.gpuRefreshInFlight = null;
289
+ });
290
+ }
291
+ async refreshGpus() {
292
+ this.gpus = await this.deps.probeGpus().catch(() => null);
293
+ }
221
294
  async refreshDarwinMemory() {
222
295
  if (this.deps.platform !== 'darwin')
223
296
  return;
@@ -229,43 +302,31 @@ export class SystemMetricsSampler {
229
302
  this.darwinMemoryPressure = pressure;
230
303
  }
231
304
  async refreshTopology() {
232
- const candidates = this.workspaces.map(({ name, cwd }) => {
233
- try {
234
- this.deps.statfs(cwd);
235
- const device = String(this.deps.stat(cwd).dev);
236
- return { name, cwd, device };
237
- }
238
- catch {
239
- return null;
240
- }
241
- }).filter((candidate) => candidate !== null);
242
- const representatives = new Map();
243
- candidates.forEach(({ cwd, device }) => {
244
- if (!representatives.has(device))
245
- representatives.set(device, { cwd, device });
246
- });
247
- const identities = new Map(await Promise.all([...representatives.values()].map(async ({ cwd, device }) => [
248
- device,
249
- await this.deps.identifyStorage(cwd, device).catch(() => null),
250
- ])));
251
- const identified = candidates.map(({ name, cwd, device }) => {
252
- try {
253
- const identity = identities.get(device);
254
- const key = identity?.key ?? `dev:${device}`;
255
- return { name, cwd, key, label: identity?.label || path.parse(cwd).root || cwd };
256
- }
257
- catch {
258
- return null;
259
- }
260
- });
261
- const grouped = new Map();
262
- for (const entry of identified.filter((candidate) => candidate !== null)) {
263
- const existing = grouped.get(entry.key);
264
- if (existing)
265
- existing.workspaces.push(entry.name);
266
- else
267
- grouped.set(entry.key, { cwd: entry.cwd, key: entry.key, label: entry.label, workspaces: [entry.name] });
305
+ try {
306
+ const pools = await this.deps.discoverStorage();
307
+ this.storage = pools.map((pool) => {
308
+ const devices = Object.fromEntries(pool.paths.flatMap((cwd) => {
309
+ try {
310
+ return [[cwd, String(this.deps.stat(cwd).dev)]];
311
+ }
312
+ catch {
313
+ return [];
314
+ }
315
+ }));
316
+ const poolDevices = new Set(Object.values(devices));
317
+ const workspaces = this.workspaces.filter(({ cwd }) => {
318
+ try {
319
+ return poolDevices.has(String(this.deps.stat(cwd).dev));
320
+ }
321
+ catch {
322
+ return false;
323
+ }
324
+ }).map(({ name }) => name);
325
+ return { ...pool, workspaces, devices };
326
+ });
327
+ }
328
+ catch {
329
+ return;
268
330
  }
269
- this.storage = [...grouped.values()];
270
331
  }
271
332
  }
@@ -0,0 +1,94 @@
1
+ import { execFile } from 'node:child_process';
2
+ import path from 'node:path';
3
+ const PROBE_TIMEOUT_MS = 10_000;
4
+ const VERIFY_TIMEOUT_MS = 3_000;
5
+ /** Interpreters rarely appear or vanish; a slow refresh keeps the heartbeat itself free of any spawning. */
6
+ export const TOOLCHAIN_REFRESH_MS = 15 * 60_000;
7
+ /**
8
+ * Shells whose `-lc` runs a POSIX-ish command string with `command -v` (fish ≥ 3 included). Any other shell (csh, tcsh,
9
+ * nu, pwsh, …) is not probed: its answer is unknown, never "missing".
10
+ */
11
+ const PROBED_SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'ksh', 'mksh', 'fish']);
12
+ /** Separates the probe's answers; no leading `=` (zsh would expand a bare `=word`). */
13
+ const SPLIT = 'AXOM-PROBE-SPLIT';
14
+ const DONE = 'AXOM-PROBE-DONE';
15
+ const NAMES = ['python3', 'python', 'node'];
16
+ /**
17
+ * One `<shell> -lc` run of `command -v` for each interpreter, the way commands run on this workstation (exec spawns the
18
+ * same login shell), so a Python found only through the user's profile counts. Each answer is the last absolute-path
19
+ * line of its segment (a login profile may print its own lines first). Null = unknown: a shell we do not probe, a shell
20
+ * that could not run, or a run that did not reach its end marker.
21
+ */
22
+ export async function probeToolchain(shell, env = process.env, options = {}) {
23
+ if (!PROBED_SHELLS.has(path.basename(shell)))
24
+ return null;
25
+ const script = [...NAMES.map((name) => `command -v ${name}`), `echo ${DONE}`].join(`; echo ${SPLIT}; `);
26
+ const stdout = await run(shell, ['-lc', script], env, PROBE_TIMEOUT_MS);
27
+ if (stdout === undefined)
28
+ return null;
29
+ const lines = stdout.split('\n').map((line) => line.trim());
30
+ const doneAt = lines.lastIndexOf(DONE);
31
+ if (doneAt < 0)
32
+ return null;
33
+ const segments = [[]];
34
+ for (const line of lines.slice(0, doneAt)) {
35
+ if (line === SPLIT)
36
+ segments.push([]);
37
+ else
38
+ segments[segments.length - 1].push(line);
39
+ }
40
+ // `command -v python3; echo SPLIT; …; echo SPLIT; echo DONE`: one segment per name, then an empty one. A login
41
+ // profile's own output lands in the first segment, before the answer, so each answer is its segment's last path.
42
+ if (segments.length !== NAMES.length + 1)
43
+ return null;
44
+ const answers = segments.slice(0, NAMES.length);
45
+ const found = (index) => [...answers[index]].reverse().find((line) => path.isAbsolute(line)) ?? null;
46
+ let python3 = found(0);
47
+ if (python3 === '/usr/bin/python3' && (options.platform ?? process.platform) === 'darwin') {
48
+ if (!(await (options.macStubRuns ?? macCommandLineToolsInstalled)()))
49
+ python3 = null;
50
+ }
51
+ return { python: python3 ?? found(1), node: found(2) };
52
+ }
53
+ async function macCommandLineToolsInstalled() {
54
+ return (await run('/usr/bin/xcode-select', ['-p'], process.env, VERIFY_TIMEOUT_MS, true)) !== undefined;
55
+ }
56
+ /** The command's stdout; undefined = it did not run, timed out, or (with [strict]) exited non-zero. */
57
+ function run(file, args, env, timeout, strict = false) {
58
+ return new Promise((resolve) => {
59
+ execFile(file, args, { env, timeout, windowsHide: true }, (error, stdout) => {
60
+ const code = error?.code;
61
+ if (error && (strict || typeof code !== 'number')) {
62
+ resolve(undefined);
63
+ return;
64
+ }
65
+ resolve(String(stdout));
66
+ });
67
+ });
68
+ }
69
+ /** Holds the latest probe; the first answer arrives shortly after start, until then the heartbeat simply omits it. */
70
+ export class ToolchainProbe {
71
+ shell;
72
+ env;
73
+ latest = null;
74
+ timer;
75
+ constructor(shell, env = () => process.env) {
76
+ this.shell = shell;
77
+ this.env = env;
78
+ }
79
+ current() { return this.latest; }
80
+ start() {
81
+ if (process.platform === 'win32' || this.timer)
82
+ return;
83
+ const refresh = async () => {
84
+ const found = await probeToolchain(this.shell(), this.env()).catch(() => null);
85
+ if (found)
86
+ this.latest = found;
87
+ };
88
+ void refresh();
89
+ this.timer = setInterval(() => { void refresh(); }, TOOLCHAIN_REFRESH_MS);
90
+ this.timer.unref();
91
+ }
92
+ stop() { if (this.timer)
93
+ clearInterval(this.timer); this.timer = undefined; }
94
+ }
@@ -17,13 +17,31 @@ async function bounded(promise, timeout = TIMEOUT) {
17
17
  clearTimeout(timer);
18
18
  }
19
19
  }
20
- /** All ancestors are checked: mkdir recursive must never follow a planted profile symlink. */
20
+ /**
21
+ * All ancestors are checked: mkdir recursive must never follow a planted profile symlink. The one
22
+ * symlinks allowed across are root-owned system symlinks (`/var -> /private/var` on macOS): stock
23
+ * macOS tmp dirs sit behind one, and refusing it would reject every per-user tmpdir. A root-owned
24
+ * symlink whose target is a root-owned, non-group-writable directory is not plantable by the user
25
+ * this runs as; anything else — every user-owned symlink — still fails the check below.
26
+ */
21
27
  async function ownedDirectory(dir, privateMode = false) {
22
28
  const absolute = path.resolve(dir);
23
29
  const parts = absolute.split(path.sep).filter(Boolean);
24
30
  let current = path.parse(absolute).root;
25
31
  const uid = process.getuid?.();
26
32
  let protectedByOwner = false;
33
+ const systemSymlink = async (stat) => {
34
+ if (stat.uid !== 0 || uid === 0)
35
+ return false;
36
+ try {
37
+ const target = await fs.realpath(current);
38
+ const targetStat = await fs.lstat(target);
39
+ return targetStat.isDirectory() && targetStat.uid === 0 && (targetStat.mode & 0o022) === 0;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ };
27
45
  for (const part of parts) {
28
46
  current = path.join(current, part);
29
47
  await fs.mkdir(current, { mode: 0o700 }).catch((e) => {
@@ -31,8 +49,14 @@ async function ownedDirectory(dir, privateMode = false) {
31
49
  throw e;
32
50
  });
33
51
  const stat = await fs.lstat(current);
52
+ if (stat.isSymbolicLink()) {
53
+ if (!(await systemSymlink(stat)))
54
+ throw Error("Unsafe browser directory");
55
+ // Root-owned system symlink: cross it; the link itself carries no mode or ownership
56
+ // expectation (Linux reports links 0777), its target was just verified.
57
+ continue;
58
+ }
34
59
  if (!stat.isDirectory() ||
35
- stat.isSymbolicLink() ||
36
60
  (!protectedByOwner &&
37
61
  (stat.mode & 0o022) !== 0 &&
38
62
  !(stat.mode & 0o1000)) ||
@@ -44,11 +68,11 @@ async function ownedDirectory(dir, privateMode = false) {
44
68
  if (uid !== undefined && stat.uid === uid && (stat.mode & 0o077) === 0)
45
69
  protectedByOwner = true;
46
70
  }
47
- const stat = await fs.lstat(absolute);
71
+ const stat = await fs.lstat(current);
48
72
  if (process.getuid && stat.uid !== process.getuid())
49
73
  throw Error("Browser directory is not owned by this user");
50
74
  if (privateMode)
51
- await fs.chmod(absolute, 0o700);
75
+ await fs.chmod(current, 0o700);
52
76
  }
53
77
  function allowedUrl(params) {
54
78
  const url = string(params, "url", 8192, "about:blank");
@@ -84,15 +84,23 @@ function pidAlive(pid) {
84
84
  return error.code === 'EPERM';
85
85
  }
86
86
  }
87
+ /**
88
+ * A tools keeper client. The daemon only ever makes the browser kind (`browser`). The kind is required — no default — so nothing can
89
+ * start the retired terminal-kind tools keeper by accident (spec §9; terminals run in keeper-terminal, files in the daemon's files
90
+ * host); an explicit `terminal` client exists only for the tests that pin how the retired keeper's startup and tools.info behave.
91
+ * (`keeperPaths` keeps its terminal default: `keepers/legacy.ts` finds a legacy tools keeper an older CLI left running by it.)
92
+ */
87
93
  export class ToolsClient {
88
94
  options;
89
95
  kind;
90
96
  paths;
91
97
  starting;
92
98
  browser;
93
- constructor(options, kind = 'terminal') {
99
+ constructor(options, kind) {
94
100
  this.options = options;
95
101
  this.kind = kind;
102
+ if (kind !== 'terminal' && kind !== 'browser')
103
+ throw new Error('ToolsClient needs an explicit kind (browser)');
96
104
  this.paths = keeperPaths(options, undefined, undefined, kind);
97
105
  }
98
106
  async probe(timeoutMs = 250) {
@@ -0,0 +1,50 @@
1
+ import { promises as fs } from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ /*
5
+ * `tools.info`, aggregated by the daemon. Consumers (analysis C §3.6): Workbench app.mjs (version), CoworkService
6
+ * (files / mediaPreviewVersion / fileFindVersion / home / userName), files.mjs (roots / downloads / terminal),
7
+ * terminal.mjs (home, terminal / terminalError), browser.mjs (browser* fields). Every field keeps the value and meaning
8
+ * the legacy tools keeper gave it (tools/core.ts at the merge-base, since deleted, + tools/client.ts), and answering it never
9
+ * starts any keeper:
10
+ * - file fields come from the files host's last `ready` (or are computed here before the first one); `files: true`
11
+ * is reported even while the host restarts, so a probe never mistakes the machine for an old CLI;
12
+ * - terminal fields come from keeper-terminal's hello capabilities when that keeper is already running (connecting to it,
13
+ * never starting it: keepers/terminal/client.ts `terminalStatus`), otherwise from a cached node-pty load check
14
+ * (update/ptyPreflight.ts);
15
+ * - browser fields come from the browser-kind ToolsClient, whose `tools.info` only returns metadata.
16
+ */
17
+ /** The legacy keeper always sent this (tools/core.ts at the merge-base); the browser metadata never removed it. */
18
+ export const BROWSER_DISABLED_ERROR = 'Browser tools are not enabled in this release';
19
+ export async function aggregateToolsInfo(sources) {
20
+ const [files, browser, terminal] = await Promise.all([sources.files(), sources.browser('tools.info', {}), sources.terminal()]);
21
+ return {
22
+ version: 1,
23
+ terminal: terminal.terminal,
24
+ browser: false,
25
+ ...files,
26
+ ...(terminal.terminalError ? { terminalError: terminal.terminalError } : {}),
27
+ browserError: BROWSER_DISABLED_ERROR,
28
+ ...browser,
29
+ };
30
+ }
31
+ /**
32
+ * The file fields before any files host was ready: the same canonical home and roots the host computes
33
+ * (scope.ts `configuredRoots`), except that a root which cannot be resolved is left out instead of failing the probe.
34
+ */
35
+ export async function computeFilesInfo(home, roots) {
36
+ const canonicalHome = await fs.realpath(home).catch(() => home);
37
+ const canonical = new Set([canonicalHome]);
38
+ for (const root of roots) {
39
+ try {
40
+ const real = await fs.realpath(root);
41
+ if ((await fs.stat(real)).isDirectory())
42
+ canonical.add(real);
43
+ }
44
+ catch { /* skipped, as the daemon skips it when starting the host */ }
45
+ }
46
+ return {
47
+ files: true, readOnlyFilesVersion: 1, filesystemFilesVersion: 1, mediaPreviewVersion: 2, fileFindVersion: 1,
48
+ home: canonicalHome, userName: os.userInfo().username, roots: [...canonical], downloads: path.join(canonicalHome, 'Downloads'),
49
+ };
50
+ }
@@ -2,18 +2,18 @@ import net from 'node:net';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
- import { ToolsCore } from './core.js';
6
5
  import { BrowserCore } from './browser.js';
7
6
  import os from 'node:os';
8
7
  import { privateDirectory } from './client.js';
9
8
  import { object, TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES } from './validation.js';
10
9
  export async function runKeeper(config) {
10
+ // Only the browser keeper runs from here (spec §9): keeper-terminal owns PTYs and the files host owns files.
11
+ if (config.kind !== 'browser')
12
+ throw new Error('Only the browser keeper runs from this entry');
11
13
  await privateDirectory(path.dirname(config.socket));
12
14
  const pidFile = path.join(path.dirname(config.socket), 'keeper.pid.json');
13
15
  await fs.writeFile(pidFile, JSON.stringify({ pid: process.pid, identity: config.identity }), { mode: 0o600 });
14
- const core = config.kind === 'browser' ? await BrowserCore.create({ home: config.home ?? os.homedir(), profileRoot: config.profileRoot, executablePath: config.executablePath, proxy: config.proxy }) : await ToolsCore.create({ home: config.home, roots: config.roots, uploadStateDir: path.join(path.dirname(config.socket), 'uploads') });
15
- if (config.kind !== 'browser')
16
- process.chdir(path.dirname(config.socket));
16
+ const core = await BrowserCore.create({ home: config.home ?? os.homedir(), profileRoot: config.profileRoot, executablePath: config.executablePath, proxy: config.proxy });
17
17
  const sockets = new Set();
18
18
  let pending = 0;
19
19
  const server = net.createServer({ allowHalfOpen: true }, socket => {
@@ -104,7 +104,17 @@ export async function runKeeper(config) {
104
104
  } };
105
105
  }
106
106
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
107
- const config = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'));
107
+ let config;
108
+ try {
109
+ config = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'));
110
+ }
111
+ catch { /* below */ }
112
+ // Anti-resurrection (spec §9): only the browser keeper still starts from this entry. A terminal-kind config (kind 'terminal',
113
+ // or no kind from older CLIs) comes from a legacy tools keeper unit restarting (`Restart=on-failure`) or an old launcher:
114
+ // keeper-terminal owns PTYs now and the files host owns files, so exit cleanly (0, which systemd does not restart) before
115
+ // touching the socket, the pid manifest or any state.
116
+ if (config?.kind !== 'browser')
117
+ process.exit(0);
108
118
  const keeper = await runKeeper(config);
109
119
  let stopping = false;
110
120
  const stop = () => { if (stopping)
@@ -0,0 +1,55 @@
1
+ import { isFilesToolMethod } from '../files/adapters/toolRequests.js';
2
+ import { keys, validateRequest } from './validation.js';
3
+ export function routeTool(method) {
4
+ if (method === 'tools.info')
5
+ return 'info';
6
+ if (method === 'browser.sharedText')
7
+ return 'shared-text';
8
+ if (isFilesToolMethod(method))
9
+ return 'files';
10
+ if (method.startsWith('terminal.'))
11
+ return 'terminal';
12
+ if (method.startsWith('browser.'))
13
+ return 'browser';
14
+ return 'unsupported';
15
+ }
16
+ /** A probe is answered even while the workstation drains and is not work a restart would interrupt. */
17
+ export function isProbeTool(method) { return routeTool(method) === 'info'; }
18
+ /**
19
+ * Answered while draining and not counted as a shell task: probes, and `terminal.*` (spec §4.1: a PTY lives in its keeper, not in
20
+ * the daemon, so a restart does not interrupt it; counting every poll made the activity flicker).
21
+ */
22
+ export function isUnaccountedTool(method) {
23
+ const route = routeTool(method);
24
+ return route === 'info' || route === 'terminal';
25
+ }
26
+ export class WorkstationTools {
27
+ deps;
28
+ constructor(deps) {
29
+ this.deps = deps;
30
+ }
31
+ /** One `tool_request` (not `browser.sharedText`). Rejects with the `tool_result.error` text. */
32
+ async request(method, params) {
33
+ // Validated at the daemon boundary as well as by whoever runs it.
34
+ validateRequest(method, params);
35
+ switch (routeTool(method)) {
36
+ case 'info':
37
+ keys(params, []);
38
+ return this.deps.info();
39
+ case 'files': return this.deps.files.tool(method, params);
40
+ case 'terminal':
41
+ if (!this.deps.terminal)
42
+ throw new Error('Workstation terminal keeper is not configured');
43
+ return this.deps.terminal(method, params);
44
+ case 'browser':
45
+ if (!this.deps.browser)
46
+ throw new Error('Workstation browser keeper is not configured');
47
+ return this.deps.browser(method, params);
48
+ default: throw new Error('Unsupported workstation tool');
49
+ }
50
+ }
51
+ /** One `file_*_request`; never rejects (errors are result frames). */
52
+ runFrame(request, signal) {
53
+ return this.deps.files.runFrame(request, signal);
54
+ }
55
+ }