@platless/axom-cli 0.5.9 → 0.6.52

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 (258) hide show
  1. package/README.md +25 -6
  2. package/dist/axom-patch/cli/README.md +129 -12
  3. package/dist/axom-patch/cli/account-limits/generate.py +15 -12
  4. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +20 -3
  5. package/dist/axom-patch/cli/claude-account-limits.json +9 -9
  6. package/dist/axom-patch/cli/claude-replay-lifecycle.json +16 -0
  7. package/dist/axom-patch/cli/codex-account-limits.json +11 -11
  8. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  9. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  10. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  11. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  12. package/dist/axom-patch/cli/nightly-task.md +55 -13
  13. package/dist/axom-patch/cli/probes/claude-replay-lifecycle.mjs +31 -0
  14. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  15. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  16. package/dist/axom-patch/cli/probes/zcode-env-account.mjs +81 -0
  17. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +7 -0
  18. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1687 -0
  19. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +277 -0
  20. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  21. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  22. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  23. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  24. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  25. package/dist/axom-patch/cli/runtime.json +19 -19
  26. package/dist/axom-patch/cli/zcode/env-account.mjs +13 -0
  27. package/dist/axom-patch/cli/zcode/generate-env-account.py +52 -0
  28. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  29. package/dist/axom-patch/cli/zcode-env-account.json +58 -0
  30. package/dist/axom-patch/cli/zcode-models.json +9 -10
  31. package/dist/axom-patch/cli/zcode-subagent-subwork.json +10 -11
  32. package/dist/cli.js +11 -45
  33. package/dist/commands/auth.js +4 -1
  34. package/dist/commands/knowledge.js +47 -0
  35. package/dist/commands/update.js +126 -3
  36. package/dist/commands/workstation.js +101 -164
  37. package/dist/commands/workstationRegistrations.js +116 -0
  38. package/dist/credentialBroker.js +13 -2
  39. package/dist/credentials.js +16 -0
  40. package/dist/install/claude/commands.js +15 -15
  41. package/dist/keepers/terminal/keeper.mjs +1089 -673
  42. package/dist/keepers/terminal/manifest.json +4 -4
  43. package/dist/mcp.js +1 -1
  44. package/dist/native/darwin-x64/axom-quic +0 -0
  45. package/dist/workstation/acp/acpAgents.js +113 -102
  46. package/dist/workstation/acp/acpClient.js +26 -0
  47. package/dist/workstation/acp/builds/install/npm.js +99 -10
  48. package/dist/workstation/acp/initializeResponse.js +6 -1
  49. package/dist/workstation/acp/managedPatches.js +189 -25
  50. package/dist/workstation/acp/managedRuntime.js +314 -0
  51. package/dist/workstation/acp/outbox.js +12 -5
  52. package/dist/workstation/acp/patchAssets.js +40 -0
  53. package/dist/workstation/acp/permission.js +2 -2
  54. package/dist/workstation/acp/processor/checkpoint.js +26 -34
  55. package/dist/workstation/acp/processor/control.js +7 -80
  56. package/dist/workstation/acp/processor/jsonrpc.js +8 -0
  57. package/dist/workstation/acp/processor/runtime.js +5 -81
  58. package/dist/workstation/acp/processor/session.js +54 -82
  59. package/dist/workstation/acp/processor/state.js +7 -13
  60. package/dist/workstation/acp/processor/step.js +3 -20
  61. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  62. package/dist/workstation/acp/processor/transport/local.js +19 -3
  63. package/dist/workstation/acp/processor/updates.js +156 -105
  64. package/dist/workstation/acp/processorSession.js +20 -173
  65. package/dist/workstation/acp/registry/catalog.js +65 -39
  66. package/dist/workstation/acp/registry/launch.js +16 -1
  67. package/dist/workstation/acp/switching.js +0 -3
  68. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  69. package/dist/workstation/acp/test/keeperWorld.js +32 -3
  70. package/dist/workstation/acp/test/raw/sources.js +16 -130
  71. package/dist/workstation/acp/test/realAdapter.js +1 -47
  72. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  73. package/dist/workstation/agentFlow.js +1 -1
  74. package/dist/workstation/agentSessions.js +114 -166
  75. package/dist/workstation/channels/agent.js +9 -1
  76. package/dist/workstation/channels/computer.js +27 -4
  77. package/dist/workstation/channels/management.js +130 -15
  78. package/dist/workstation/channels/url.js +1 -1
  79. package/dist/workstation/config.js +33 -9
  80. package/dist/workstation/connection/encryptedChannel.js +4 -1
  81. package/dist/workstation/control/bindingStore.js +204 -0
  82. package/dist/workstation/control/commands.js +125 -0
  83. package/dist/workstation/control/daemonIdentity.js +143 -0
  84. package/dist/workstation/control/deviceCommands.js +191 -0
  85. package/dist/workstation/control/deviceProtocol.js +68 -0
  86. package/dist/workstation/control/enrollment.js +106 -0
  87. package/dist/workstation/control/inspect.js +71 -0
  88. package/dist/workstation/control/locks.js +110 -0
  89. package/dist/workstation/control/protocol.js +40 -0
  90. package/dist/workstation/control/restartGuard.js +46 -0
  91. package/dist/workstation/control/rootFormat.js +16 -0
  92. package/dist/workstation/control/runtimeClient.js +152 -0
  93. package/dist/workstation/control/runtimeHost.js +235 -0
  94. package/dist/workstation/control/serverIdentity.js +35 -0
  95. package/dist/workstation/control/updateTransition.js +91 -0
  96. package/dist/workstation/daemon.js +266 -264
  97. package/dist/workstation/daemonctl.js +226 -25
  98. package/dist/workstation/desktopControl/audio.js +99 -0
  99. package/dist/workstation/desktopControl/automationDevice.js +219 -0
  100. package/dist/workstation/desktopControl/automationJson.js +74 -0
  101. package/dist/workstation/desktopControl/automationLocalState.js +23 -0
  102. package/dist/workstation/desktopControl/automationProtocol.js +253 -0
  103. package/dist/workstation/desktopControl/automationSchema.generated.js +1 -0
  104. package/dist/workstation/desktopControl/automationSession.js +81 -0
  105. package/dist/workstation/desktopControl/automationTransport.js +138 -0
  106. package/dist/workstation/desktopControl/capabilities.js +93 -0
  107. package/dist/workstation/desktopControl/captureModes.js +9 -0
  108. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  109. package/dist/workstation/desktopControl/deviceAdmission.js +229 -0
  110. package/dist/workstation/desktopControl/deviceOwnership.js +130 -0
  111. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  112. package/dist/workstation/desktopControl/helperProcess.js +602 -0
  113. package/dist/workstation/desktopControl/hostGrant.js +65 -0
  114. package/dist/workstation/desktopControl/lobby.js +131 -0
  115. package/dist/workstation/desktopControl/macProvider.js +156 -0
  116. package/dist/workstation/desktopControl/media.js +331 -0
  117. package/dist/workstation/desktopControl/nativeDeviceAdmission.js +207 -0
  118. package/dist/workstation/desktopControl/nativeProvider.js +1044 -0
  119. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  120. package/dist/workstation/desktopControl/overview.js +58 -0
  121. package/dist/workstation/desktopControl/ownershipProtocol.js +31 -0
  122. package/dist/workstation/desktopControl/protocol.js +311 -0
  123. package/dist/workstation/desktopControl/provider.js +1 -0
  124. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  125. package/dist/workstation/desktopControl/rateController.js +226 -0
  126. package/dist/workstation/desktopControl/supervisor.js +1774 -0
  127. package/dist/workstation/desktopControl/transport.js +27 -0
  128. package/dist/workstation/desktopControl/wallpaper.js +98 -0
  129. package/dist/workstation/exec/policy.js +5 -0
  130. package/dist/workstation/files/client.js +10 -1
  131. package/dist/workstation/files/credentials.js +142 -0
  132. package/dist/workstation/files/host.js +9 -2
  133. package/dist/workstation/files/text.js +42 -8
  134. package/dist/workstation/journal.js +5 -2
  135. package/dist/workstation/keepers/acp/client.js +38 -2
  136. package/dist/workstation/keepers/kit/buildVersion.js +35 -11
  137. package/dist/workstation/keepers/kit/bundle.js +21 -17
  138. package/dist/workstation/keepers/kit/client.js +2 -2
  139. package/dist/workstation/keepers/kit/launcher.js +123 -7
  140. package/dist/workstation/keepers/kit/paths.js +2 -1
  141. package/dist/workstation/keepers/legacy.js +16 -2
  142. package/dist/workstation/keepers/policy.js +5 -34
  143. package/dist/workstation/keepers/terminal/client.js +115 -23
  144. package/dist/workstation/keepers/terminal/exec.js +6 -3
  145. package/dist/workstation/keepers/terminal/frames.js +73 -0
  146. package/dist/workstation/keepers/terminal/generations.js +5 -8
  147. package/dist/workstation/keepers/terminal/gridProtocol.js +16 -0
  148. package/dist/workstation/keepers/terminal/input.js +146 -0
  149. package/dist/workstation/keepers/terminal/keeper.js +22 -8
  150. package/dist/workstation/keepers/terminal/main.js +1 -1
  151. package/dist/workstation/keepers/terminal/metadata.js +138 -0
  152. package/dist/workstation/keepers/terminal/protocol.js +4 -28
  153. package/dist/workstation/keepers/terminal/pty.js +1 -330
  154. package/dist/workstation/keepers/terminal/screen.js +246 -0
  155. package/dist/workstation/keepers/terminal/sessions.js +526 -0
  156. package/dist/workstation/managedInstall.js +57 -0
  157. package/dist/workstation/oauth/deviceAuthorization.js +31 -17
  158. package/dist/workstation/protocol/agent.js +2 -15
  159. package/dist/workstation/protocol/management.js +113 -5
  160. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  161. package/dist/workstation/rd/artifact.js +120 -0
  162. package/dist/workstation/rd/control.js +117 -0
  163. package/dist/workstation/rd/display.js +29 -0
  164. package/dist/workstation/rd/host.js +213 -0
  165. package/dist/workstation/rd/migrationReadiness.js +46 -0
  166. package/dist/workstation/rd/provider.js +256 -0
  167. package/dist/workstation/rd/sessions.js +391 -0
  168. package/dist/workstation/rd/state.js +57 -0
  169. package/dist/workstation/rd/update.js +168 -0
  170. package/dist/workstation/rd/upkeep.js +80 -0
  171. package/dist/workstation/register.js +27 -37
  172. package/dist/workstation/registrations/enrollment.js +250 -0
  173. package/dist/workstation/registrations/enrollmentPending.js +361 -0
  174. package/dist/workstation/registrations/migration.js +356 -0
  175. package/dist/workstation/registrations/model.js +34 -0
  176. package/dist/workstation/registrations/removal.js +222 -0
  177. package/dist/workstation/registrations/store.js +737 -0
  178. package/dist/workstation/runtime/desktopBinding.js +20 -0
  179. package/dist/workstation/runtime/deviceDesktop.js +193 -0
  180. package/dist/workstation/runtime/deviceRuntime.js +568 -0
  181. package/dist/workstation/runtime/deviceServices.js +117 -0
  182. package/dist/workstation/runtime/events.js +20 -0
  183. package/dist/workstation/runtime/ownership.js +100 -0
  184. package/dist/workstation/runtime/registrationRuntime.js +575 -0
  185. package/dist/workstation/runtime/remoteDesktopAdapter.js +186 -0
  186. package/dist/workstation/runtime/removalActivity.js +184 -0
  187. package/dist/workstation/service/installer.js +197 -14
  188. package/dist/workstation/service/launchd.js +2 -0
  189. package/dist/workstation/service/systemd.js +1 -0
  190. package/dist/workstation/storageMetrics.js +118 -0
  191. package/dist/workstation/systemMetrics.js +47 -92
  192. package/dist/workstation/toolchain.js +94 -0
  193. package/dist/workstation/tools/browser.js +28 -4
  194. package/dist/workstation/tools/client.js +8 -27
  195. package/dist/workstation/tools/info.js +6 -13
  196. package/dist/workstation/tools/terminalRouter.js +17 -172
  197. package/dist/workstation/tools/validation.js +55 -1
  198. package/dist/workstation/update/archiveEvidence.js +206 -0
  199. package/dist/workstation/update/artifact.js +2 -1
  200. package/dist/workstation/update/coordinator.js +34 -13
  201. package/dist/workstation/update/deviceCompatibility.js +119 -0
  202. package/dist/workstation/update/deviceCoordinator.js +369 -0
  203. package/dist/workstation/update/deviceHealth.js +104 -0
  204. package/dist/workstation/update/deviceHealthProtocol.js +26 -0
  205. package/dist/workstation/update/deviceRecoveryLifecycle.js +46 -0
  206. package/dist/workstation/update/deviceRollback.js +78 -0
  207. package/dist/workstation/update/globalInstall.js +186 -0
  208. package/dist/workstation/update/legacyBridge.js +414 -0
  209. package/dist/workstation/update/legacyOwner.js +39 -0
  210. package/dist/workstation/update/npmInstaller.js +51 -25
  211. package/dist/workstation/update/privateDurability.js +61 -0
  212. package/dist/workstation/update/rdRollback.js +145 -0
  213. package/dist/workstation/update/recoveryPlan.js +124 -0
  214. package/dist/workstation/update/runUpdate.js +10 -3
  215. package/dist/workstation/update/taskRegistry.js +161 -3
  216. package/dist/workstation/update/watchdog.js +29806 -333
  217. package/dist/workstation/update/watchdogProcess.js +72 -0
  218. package/package.json +11 -7
  219. package/skills/axom/SKILL.md +11 -11
  220. package/skills/axom-definition/SKILL.md +3 -3
  221. package/skills/axom-platform/SKILL.md +3 -3
  222. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  223. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  224. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  225. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  226. package/dist/commands/guide.js +0 -38
  227. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  228. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  229. package/dist/workstation/acp/delegationMcp.js +0 -24
  230. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  231. package/dist/workstation/acp/qoderStartup.js +0 -45
  232. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  233. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  234. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  235. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  236. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  237. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  238. package/dist/workstation/qoderAdapter/contract.js +0 -78
  239. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  240. package/dist/workstation/qoderAdapter/history.js +0 -85
  241. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  242. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  243. package/dist/workstation/qoderAdapter/main.js +0 -95
  244. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  245. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  246. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  247. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  248. package/dist/workstation/qoderAdapter/profile.js +0 -93
  249. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  250. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  251. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  252. package/dist/workstation/qoderAdapter/session.js +0 -319
  253. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  254. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  255. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  256. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  257. package/dist/workstation/qoderAdapter/usage.js +0 -37
  258. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/automationPreparation.js} +0 -0
@@ -35,6 +35,7 @@ Type=simple
35
35
  WorkingDirectory=${systemdToken(context.homeDir)}
36
36
  ${environment('HOME', context.homeDir)}
37
37
  ${environment('AXOM_WORKSTATION_CONFIG_DIR', context.configDir)}
38
+ ${environment('AXOM_WORKSTATION_RUNTIME', 'device-v2')}
38
39
  ${environment('AXOM_WORKSTATION_NPM_PATH', context.npmPath)}
39
40
  ${environment('AXOM_WORKSTATION_NPM_PREFIX', context.npmPrefix)}
40
41
  ${environment('PATH', context.pathEnv)}
@@ -0,0 +1,118 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { statSync } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { promisify } from 'node:util';
5
+ const execFileAsync = promisify(execFile);
6
+ const LOCAL_FILESYSTEMS = new Set([
7
+ 'ext2', 'ext3', 'ext4', 'xfs', 'btrfs', 'zfs', 'f2fs', 'bcachefs',
8
+ 'vfat', 'exfat', 'ntfs', 'ntfs3', 'fuseblk',
9
+ ]);
10
+ export function linuxStoragePools(mounts) {
11
+ const pools = new Map();
12
+ for (const mount of mounts) {
13
+ if (!LOCAL_FILESYSTEMS.has(mount.fstype) || mount.options?.split(',').includes('ro'))
14
+ continue;
15
+ if (/^\/(?:boot|efi)(?:\/|$)/.test(mount.target))
16
+ continue;
17
+ const source = mount.source.split('[')[0];
18
+ const key = mount.fstype === 'zfs'
19
+ ? `zfs:${source.split('/')[0]}`
20
+ : mount.uuid ? `${mount.fstype}:${mount.uuid}` : `dev:${mount['maj:min']}`;
21
+ const existing = pools.get(key);
22
+ if (existing)
23
+ existing.paths.push(mount.target);
24
+ else
25
+ pools.set(key, { key, label: mount.label || source, paths: [mount.target] });
26
+ }
27
+ if (pools.size === 0) {
28
+ const root = mounts.find((mount) => mount.target === '/' && mount.fstype === 'overlay');
29
+ if (root)
30
+ pools.set('overlay:/', { key: 'overlay:/', label: 'Container storage', paths: ['/'] });
31
+ }
32
+ return [...pools.values()];
33
+ }
34
+ export function darwinStoragePools(disks, physicalDisks, containers) {
35
+ const physical = new Set(physicalDisks.flatMap((disk) => [disk.DeviceIdentifier, ...(disk.Partitions ?? []).map((part) => part.DeviceIdentifier)]));
36
+ const volumes = disks.flatMap((disk) => [disk, ...(disk.Partitions ?? []), ...(disk.APFSVolumes ?? [])]);
37
+ const pools = [];
38
+ for (const container of containers) {
39
+ if (!container.PhysicalStores.some((store) => physical.has(store.DeviceIdentifier)))
40
+ continue;
41
+ const userVolumes = container.Volumes.filter((volume) => volume.Roles.length === 0 || volume.Roles.includes('Data'));
42
+ userVolumes.sort((first, second) => Number(second.Roles.includes('Data')) - Number(first.Roles.includes('Data')));
43
+ const mounted = userVolumes.flatMap((volume) => {
44
+ const disk = volumes.find((candidate) => candidate.DeviceIdentifier === volume.DeviceIdentifier);
45
+ return disk?.MountPoint ? [{ name: volume.Name, mount: disk.MountPoint }] : [];
46
+ });
47
+ if (mounted.length === 0)
48
+ continue;
49
+ const system = container.Volumes.find((volume) => volume.Roles.includes('System'));
50
+ pools.push({
51
+ key: `apfs:${container.APFSContainerUUID}`,
52
+ label: system?.Name || mounted[0].name,
53
+ paths: mounted.map((volume) => volume.mount),
54
+ });
55
+ }
56
+ for (const volume of volumes) {
57
+ if (!physical.has(volume.DeviceIdentifier) || !volume.MountPoint)
58
+ continue;
59
+ if (!['Apple_HFS', 'Microsoft Basic Data', 'Windows_NTFS', 'DOS_FAT_32', 'DOS_FAT_16', 'ExFAT'].includes(volume.Content ?? ''))
60
+ continue;
61
+ pools.push({
62
+ key: `volume:${volume.VolumeUUID || volume.DiskUUID || volume.DeviceIdentifier}`,
63
+ label: volume.VolumeName || volume.MountPoint,
64
+ paths: [volume.MountPoint],
65
+ });
66
+ }
67
+ return pools;
68
+ }
69
+ async function diskutilJson(args) {
70
+ const { stdout } = await execFileAsync('/usr/sbin/diskutil', args, {
71
+ timeout: 5_000, maxBuffer: 4 * 1024 * 1024,
72
+ });
73
+ const json = await new Promise((resolve, reject) => {
74
+ const child = execFile('/usr/bin/plutil', ['-convert', 'json', '-o', '-', '-'], {
75
+ timeout: 2_000, maxBuffer: 4 * 1024 * 1024,
76
+ }, (error, output) => error ? reject(error) : resolve(output));
77
+ child.stdin?.on('error', reject);
78
+ child.stdin?.end(stdout);
79
+ });
80
+ return JSON.parse(json);
81
+ }
82
+ export async function discoverStoragePools(platform, workspaces) {
83
+ if (platform === 'linux') {
84
+ const { stdout } = await execFileAsync('findmnt', [
85
+ '--json', '--list', '--output', 'TARGET,SOURCE,FSTYPE,UUID,LABEL,MAJ:MIN,OPTIONS',
86
+ ], { timeout: 5_000, maxBuffer: 4 * 1024 * 1024, env: { ...process.env, LC_ALL: 'C' } });
87
+ const mounts = JSON.parse(stdout).filesystems;
88
+ return linuxStoragePools(mounts.filter((mount) => {
89
+ try {
90
+ return statSync(mount.target).isDirectory();
91
+ }
92
+ catch {
93
+ return false;
94
+ }
95
+ }));
96
+ }
97
+ if (platform === 'darwin') {
98
+ const [all, physical, apfs] = await Promise.all([
99
+ diskutilJson(['list', '-plist']),
100
+ diskutilJson(['list', '-plist', 'physical']),
101
+ diskutilJson(['apfs', 'list', '-plist']),
102
+ ]);
103
+ return darwinStoragePools(all.AllDisksAndPartitions, physical.AllDisksAndPartitions, apfs.Containers ?? []);
104
+ }
105
+ const pools = new Map();
106
+ for (const { cwd } of workspaces) {
107
+ try {
108
+ const key = `dev:${statSync(cwd).dev}`;
109
+ const existing = pools.get(key);
110
+ if (existing)
111
+ existing.paths.push(cwd);
112
+ else
113
+ pools.set(key, { key, label: path.parse(cwd).root, paths: [cwd] });
114
+ }
115
+ catch { }
116
+ }
117
+ return [...pools.values()];
118
+ }
@@ -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 });
@@ -216,7 +177,7 @@ export class SystemMetricsSampler {
216
177
  readFile: readFileSync,
217
178
  statfs: statfsSync,
218
179
  stat: statSync,
219
- identifyStorage: (cwd, device) => defaultIdentifyStorage(process.platform, cwd, device),
180
+ discoverStorage: () => discoverStoragePools(process.platform, workspaces),
220
181
  darwinAvailableMemory: defaultDarwinAvailableMemory,
221
182
  darwinMemoryPressure: defaultDarwinMemoryPressure,
222
183
  probeGpus: () => defaultProbeGpus(process.platform),
@@ -234,6 +195,9 @@ export class SystemMetricsSampler {
234
195
  clearInterval(this.refreshTimer);
235
196
  this.refreshTimer = null;
236
197
  }
198
+ refreshWorkspaces() {
199
+ this.refreshTopologyInBackground();
200
+ }
237
201
  snapshot() {
238
202
  try {
239
203
  this.refreshDarwinMemoryInBackground();
@@ -261,20 +225,23 @@ export class SystemMetricsSampler {
261
225
  : (this.deps.platform === 'darwin' ? this.darwinMemoryPressure : null);
262
226
  const pools = new Map();
263
227
  for (const pool of this.storage) {
264
- try {
265
- const stats = this.deps.statfs(pool.cwd);
266
- const totalBytes = Number(stats.blocks) * Number(stats.bsize);
267
- const availableBytes = Number(stats.bavail) * Number(stats.bsize);
268
- pools.set(pool.key, {
269
- id: stablePoolId(pool.key),
270
- label: pool.label,
271
- workspaces: pool.workspaces,
272
- totalBytes,
273
- availableBytes,
274
- });
275
- }
276
- catch {
277
- // A removed/unmounted workspace must not suppress CPU and memory metrics.
228
+ for (const cwd of pool.paths) {
229
+ try {
230
+ if (String(this.deps.stat(cwd).dev) !== pool.devices[cwd])
231
+ continue;
232
+ const stats = this.deps.statfs(cwd);
233
+ const totalBytes = Number(stats.blocks) * Number(stats.bsize);
234
+ const availableBytes = Number(stats.bavail) * Number(stats.bsize);
235
+ pools.set(pool.key, {
236
+ id: stablePoolId(pool.key),
237
+ label: pool.label,
238
+ workspaces: pool.workspaces,
239
+ totalBytes,
240
+ availableBytes,
241
+ });
242
+ break;
243
+ }
244
+ catch { }
278
245
  }
279
246
  }
280
247
  return {
@@ -338,43 +305,31 @@ export class SystemMetricsSampler {
338
305
  this.darwinMemoryPressure = pressure;
339
306
  }
340
307
  async refreshTopology() {
341
- const candidates = this.workspaces.map(({ name, cwd }) => {
342
- try {
343
- this.deps.statfs(cwd);
344
- const device = String(this.deps.stat(cwd).dev);
345
- return { name, cwd, device };
346
- }
347
- catch {
348
- return null;
349
- }
350
- }).filter((candidate) => candidate !== null);
351
- const representatives = new Map();
352
- candidates.forEach(({ cwd, device }) => {
353
- if (!representatives.has(device))
354
- representatives.set(device, { cwd, device });
355
- });
356
- const identities = new Map(await Promise.all([...representatives.values()].map(async ({ cwd, device }) => [
357
- device,
358
- await this.deps.identifyStorage(cwd, device).catch(() => null),
359
- ])));
360
- const identified = candidates.map(({ name, cwd, device }) => {
361
- try {
362
- const identity = identities.get(device);
363
- const key = identity?.key ?? `dev:${device}`;
364
- return { name, cwd, key, label: identity?.label || path.parse(cwd).root || cwd };
365
- }
366
- catch {
367
- return null;
368
- }
369
- });
370
- const grouped = new Map();
371
- for (const entry of identified.filter((candidate) => candidate !== null)) {
372
- const existing = grouped.get(entry.key);
373
- if (existing)
374
- existing.workspaces.push(entry.name);
375
- else
376
- grouped.set(entry.key, { cwd: entry.cwd, key: entry.key, label: entry.label, workspaces: [entry.name] });
308
+ try {
309
+ const pools = await this.deps.discoverStorage();
310
+ this.storage = pools.map((pool) => {
311
+ const devices = Object.fromEntries(pool.paths.flatMap((cwd) => {
312
+ try {
313
+ return [[cwd, String(this.deps.stat(cwd).dev)]];
314
+ }
315
+ catch {
316
+ return [];
317
+ }
318
+ }));
319
+ const poolDevices = new Set(Object.values(devices));
320
+ const workspaces = this.workspaces.filter(({ cwd }) => {
321
+ try {
322
+ return poolDevices.has(String(this.deps.stat(cwd).dev));
323
+ }
324
+ catch {
325
+ return false;
326
+ }
327
+ }).map(({ name }) => name);
328
+ return { ...pool, workspaces, devices };
329
+ });
330
+ }
331
+ catch {
332
+ return;
377
333
  }
378
- this.storage = [...grouped.values()];
379
334
  }
380
335
  }
@@ -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");
@@ -8,16 +8,13 @@ import { spawn } from 'node:child_process';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import { object, TOOL_REQUEST_BYTES, TOOL_RESULT_BYTES, validateRequest } from './validation.js';
10
10
  /** Unix socket paths must remain below macOS/Linux's ~104-byte sockaddr_un limit. */
11
- export function keeperPaths(options, temporaryDirectory = os.tmpdir(), platform = process.platform, kind = 'terminal') {
12
- const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server, ...(kind === 'browser' ? ['browser'] : [])])).digest('hex').slice(0, 32);
13
- const namespace = `axom-${kind === 'browser' ? 'browser' : 'tools'}-${process.getuid?.() ?? 'user'}`;
14
- // macOS /var/folders/.../T often exhausts sockaddr_un before adding a private namespace. Honor
15
- // TMPDIR where it fits on Linux; the short system temporary root is checked for private ownership.
11
+ export function keeperPaths(options, temporaryDirectory = os.tmpdir(), platform = process.platform, kind = 'browser') {
12
+ const identity = createHash('sha256').update(JSON.stringify([path.resolve(options.configDir), options.workstationId, options.server, 'browser'])).digest('hex').slice(0, 32);
13
+ const namespace = `axom-browser-${process.getuid?.() ?? 'user'}`;
16
14
  let dir = path.join(platform === 'darwin' ? '/tmp' : temporaryDirectory, namespace, identity);
17
15
  if (Buffer.byteLength(path.join(dir, 'keeper.sock')) > 100)
18
16
  dir = path.join('/tmp', namespace, identity);
19
- // Browser ports are disjoint from terminal startup and the normal Linux ephemeral range.
20
- return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: kind === 'browser' ? 10000 + Number.parseInt(identity.slice(0, 8), 16) % 8000 : 20000 + Number.parseInt(identity.slice(0, 8), 16) % 40000, unit: `axom-${kind === 'browser' ? 'browser' : 'tools'}-keeper-${identity}` };
17
+ return { dir, socket: path.join(dir, 'keeper.sock'), lock: path.join(dir, 'startup.lock'), pidFile: path.join(dir, 'keeper.pid.json'), lockPort: 10000 + Number.parseInt(identity.slice(0, 8), 16) % 8000, unit: `axom-browser-keeper-${identity}` };
21
18
  }
22
19
  export async function privateDirectory(dir) {
23
20
  await fs.mkdir(dir, { recursive: true, mode: 0o700 });
@@ -84,22 +81,16 @@ function pidAlive(pid) {
84
81
  return error.code === 'EPERM';
85
82
  }
86
83
  }
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
- */
84
+ /** Browser supervisor client. Native terminals use TerminalKeeperClient exclusively. */
93
85
  export class ToolsClient {
94
86
  options;
95
87
  kind;
96
88
  paths;
97
89
  starting;
98
- browser;
99
90
  constructor(options, kind) {
100
91
  this.options = options;
101
92
  this.kind = kind;
102
- if (kind !== 'terminal' && kind !== 'browser')
93
+ if (kind !== 'browser')
103
94
  throw new Error('ToolsClient needs an explicit kind (browser)');
104
95
  this.paths = keeperPaths(options, undefined, undefined, kind);
105
96
  }
@@ -121,26 +112,16 @@ export class ToolsClient {
121
112
  // Desktop sharing is optional. Checking terminal/files must not start a
122
113
  // separate browser or require its dependencies when no VNC was configured.
123
114
  const metadata = { browser: false, browserMode: 'shared', browserBinding: browserCapability(undefined) };
124
- return this.kind === 'browser' ? metadata : { ...await this.localRequest(method, params), ...metadata };
115
+ return metadata;
125
116
  }
126
117
  if (binding && (method.startsWith('browser.') || method === 'tools.info')) {
127
118
  const metadata = { browser: true, browserMode: 'shared', browserBinding: browserCapability(binding) };
128
119
  if (method === 'browser.list')
129
120
  return { ...metadata, sessions: [] };
130
121
  if (method === 'tools.info')
131
- return this.kind === 'browser' ? metadata : { ...await this.localRequest(method, params), ...metadata };
122
+ return metadata;
132
123
  throw new Error('Shared browser uses the live desktop viewer; legacy browser session operations are unavailable');
133
124
  }
134
- if (this.kind === 'terminal' && (method.startsWith('browser.') || method === 'tools.info')) {
135
- this.browser ??= new ToolsClient(this.options, 'browser');
136
- if (method.startsWith('browser.'))
137
- return this.browser.request(method, params);
138
- const [terminal, browser] = await Promise.all([this.localRequest(method, params), this.browser.request(method, params).catch(error => ({ browser: false, browserError: error.message }))]);
139
- const result = { ...terminal, ...browser, browserMode: 'legacy', browserBinding: browserCapability(undefined) };
140
- if (result.browser)
141
- delete result.browserError;
142
- return result;
143
- }
144
125
  return this.localRequest(method, params);
145
126
  }
146
127
  async localRequest(method, params) {
@@ -1,19 +1,10 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import os from 'node:os';
3
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
- */
4
+ import { TERMINAL_GRID_SCHEMA, TERMINAL_GRID_CAPABILITY } from '../keepers/terminal/gridProtocol.js';
5
+ /* tools.info is read-only discovery. Schema/capability describe the native contract; terminal availability
6
+ * comes from a current running keeper hello. The daemon's absent-keeper source is false, never a load probe
7
+ * promising that a future keeper will start. Files/browser discovery retain their independent metadata. */
17
8
  /** The legacy keeper always sent this (tools/core.ts at the merge-base); the browser metadata never removed it. */
18
9
  export const BROWSER_DISABLED_ERROR = 'Browser tools are not enabled in this release';
19
10
  export async function aggregateToolsInfo(sources) {
@@ -21,6 +12,8 @@ export async function aggregateToolsInfo(sources) {
21
12
  return {
22
13
  version: 1,
23
14
  terminal: terminal.terminal,
15
+ terminalSchema: TERMINAL_GRID_SCHEMA,
16
+ terminalCapability: TERMINAL_GRID_CAPABILITY,
24
17
  browser: false,
25
18
  ...files,
26
19
  ...(terminal.terminalError ? { terminalError: terminal.terminalError } : {}),