@platless/axom-cli 0.5.9 → 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 (163) hide show
  1. package/README.md +18 -5
  2. package/dist/axom-patch/cli/README.md +100 -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/codex-account-limits.json +11 -11
  7. package/dist/axom-patch/cli/codex-terminal-output.json +16 -0
  8. package/dist/axom-patch/cli/commandcode-account-limits.json +9 -9
  9. package/dist/axom-patch/cli/kimi-quota.mjs +12 -0
  10. package/dist/axom-patch/cli/kimi-subagents.mjs +173 -0
  11. package/dist/axom-patch/cli/nightly-task.md +55 -13
  12. package/dist/axom-patch/cli/probes/codex-terminal-output.mjs +33 -0
  13. package/dist/axom-patch/cli/probes/kimi-subagent-subwork.mjs +82 -0
  14. package/dist/axom-patch/cli/qoder/dist/axom-acp-compat.mjs +1591 -0
  15. package/dist/axom-patch/cli/qoder/releases/1.1.75.json +360 -0
  16. package/dist/axom-patch/cli/qoder-background-terminal.md +13 -6
  17. package/dist/axom-patch/cli/qoder-foreground-output.md +14 -8
  18. package/dist/axom-patch/cli/qoder-native-acp.json +227 -0
  19. package/dist/axom-patch/cli/qoder-steering.md +20 -25
  20. package/dist/axom-patch/cli/qoder-subagent-subwork.md +17 -68
  21. package/dist/axom-patch/cli/runtime.json +16 -18
  22. package/dist/axom-patch/cli/zcode-account-limits.json +10 -11
  23. package/dist/axom-patch/cli/zcode-models.json +1 -2
  24. package/dist/axom-patch/cli/zcode-subagent-subwork.json +9 -10
  25. package/dist/cli.js +4 -36
  26. package/dist/commands/knowledge.js +47 -0
  27. package/dist/commands/update.js +65 -3
  28. package/dist/commands/workstation.js +73 -7
  29. package/dist/install/claude/commands.js +13 -13
  30. package/dist/mcp.js +1 -1
  31. package/dist/native/darwin-arm64/axom-quic +0 -0
  32. package/dist/workstation/acp/acpAgents.js +102 -98
  33. package/dist/workstation/acp/acpClient.js +26 -0
  34. package/dist/workstation/acp/builds/install/npm.js +99 -10
  35. package/dist/workstation/acp/initializeResponse.js +6 -1
  36. package/dist/workstation/acp/managedPatches.js +189 -25
  37. package/dist/workstation/acp/managedRuntime.js +314 -0
  38. package/dist/workstation/acp/patchAssets.js +40 -0
  39. package/dist/workstation/acp/processor/checkpoint.js +25 -34
  40. package/dist/workstation/acp/processor/control.js +5 -78
  41. package/dist/workstation/acp/processor/runtime.js +5 -81
  42. package/dist/workstation/acp/processor/session.js +51 -80
  43. package/dist/workstation/acp/processor/state.js +6 -13
  44. package/dist/workstation/acp/processor/step.js +3 -20
  45. package/dist/workstation/acp/processor/test/fixtureLog.js +9 -16
  46. package/dist/workstation/acp/processor/updates.js +131 -105
  47. package/dist/workstation/acp/processorSession.js +8 -172
  48. package/dist/workstation/acp/registry/catalog.js +65 -39
  49. package/dist/workstation/acp/registry/launch.js +16 -1
  50. package/dist/workstation/acp/switching.js +0 -3
  51. package/dist/workstation/acp/test/fakeAgent.js +13 -0
  52. package/dist/workstation/acp/test/keeperWorld.js +28 -0
  53. package/dist/workstation/acp/test/raw/sources.js +16 -130
  54. package/dist/workstation/acp/test/realAdapter.js +1 -47
  55. package/dist/workstation/acp/test/scriptAgent.js +11 -3
  56. package/dist/workstation/agentFlow.js +1 -1
  57. package/dist/workstation/agentSessions.js +38 -143
  58. package/dist/workstation/channels/management.js +95 -11
  59. package/dist/workstation/channels/url.js +1 -1
  60. package/dist/workstation/config.js +16 -4
  61. package/dist/workstation/connection/encryptedChannel.js +4 -1
  62. package/dist/workstation/control/bindingStore.js +200 -0
  63. package/dist/workstation/control/commands.js +124 -0
  64. package/dist/workstation/control/daemonIdentity.js +53 -0
  65. package/dist/workstation/control/enrollment.js +106 -0
  66. package/dist/workstation/control/inspect.js +71 -0
  67. package/dist/workstation/control/locks.js +63 -0
  68. package/dist/workstation/control/protocol.js +40 -0
  69. package/dist/workstation/control/restartGuard.js +46 -0
  70. package/dist/workstation/control/runtimeClient.js +97 -0
  71. package/dist/workstation/control/runtimeHost.js +148 -0
  72. package/dist/workstation/control/serverIdentity.js +35 -0
  73. package/dist/workstation/daemon.js +106 -6
  74. package/dist/workstation/daemonctl.js +77 -14
  75. package/dist/workstation/desktopControl/audio.js +99 -0
  76. package/dist/workstation/desktopControl/capabilities.js +92 -0
  77. package/dist/workstation/desktopControl/captureModes.js +9 -0
  78. package/dist/workstation/desktopControl/captureTargetLabel.js +9 -0
  79. package/dist/workstation/desktopControl/deviceSockets.js +246 -0
  80. package/dist/workstation/desktopControl/helperProcess.js +353 -0
  81. package/dist/workstation/desktopControl/lobby.js +129 -0
  82. package/dist/workstation/desktopControl/macProvider.js +155 -0
  83. package/dist/workstation/desktopControl/media.js +331 -0
  84. package/dist/workstation/desktopControl/nativeProvider.js +1018 -0
  85. package/dist/workstation/desktopControl/networkEvidence.js +43 -0
  86. package/dist/workstation/desktopControl/overview.js +58 -0
  87. package/dist/workstation/desktopControl/protocol.js +302 -0
  88. package/dist/workstation/desktopControl/quicMedia.js +240 -0
  89. package/dist/workstation/desktopControl/rateController.js +226 -0
  90. package/dist/workstation/desktopControl/supervisor.js +1681 -0
  91. package/dist/workstation/desktopControl/transport.js +27 -0
  92. package/dist/workstation/desktopControl/wallpaper.js +78 -0
  93. package/dist/workstation/files/credentials.js +142 -0
  94. package/dist/workstation/files/text.js +42 -8
  95. package/dist/workstation/keepers/kit/bundle.js +21 -17
  96. package/dist/workstation/keepers/kit/launcher.js +47 -7
  97. package/dist/workstation/keepers/kit/paths.js +2 -1
  98. package/dist/workstation/managedInstall.js +57 -0
  99. package/dist/workstation/protocol/agent.js +2 -15
  100. package/dist/workstation/protocol/management.js +93 -4
  101. package/dist/workstation/protocol/runtimeDiagnostics.js +15 -0
  102. package/dist/workstation/rd/artifact.js +119 -0
  103. package/dist/workstation/rd/control.js +119 -0
  104. package/dist/workstation/rd/display.js +29 -0
  105. package/dist/workstation/rd/provider.js +222 -0
  106. package/dist/workstation/rd/sessions.js +391 -0
  107. package/dist/workstation/rd/state.js +45 -0
  108. package/dist/workstation/rd/update.js +171 -0
  109. package/dist/workstation/rd/upkeep.js +38 -0
  110. package/dist/workstation/register.js +27 -37
  111. package/dist/workstation/service/installer.js +104 -13
  112. package/dist/workstation/service/launchd.js +1 -0
  113. package/dist/workstation/storageMetrics.js +118 -0
  114. package/dist/workstation/systemMetrics.js +44 -92
  115. package/dist/workstation/toolchain.js +94 -0
  116. package/dist/workstation/tools/browser.js +28 -4
  117. package/dist/workstation/update/artifact.js +2 -1
  118. package/dist/workstation/update/globalInstall.js +116 -0
  119. package/dist/workstation/update/npmInstaller.js +49 -23
  120. package/dist/workstation/update/rdRollback.js +145 -0
  121. package/dist/workstation/update/runUpdate.js +3 -2
  122. package/dist/workstation/update/watchdog.js +16790 -334
  123. package/package.json +5 -5
  124. package/skills/axom/SKILL.md +11 -11
  125. package/skills/axom-definition/SKILL.md +3 -3
  126. package/skills/axom-platform/SKILL.md +3 -3
  127. package/dist/axom-patch/cli/probes/qoder-steering.mjs +0 -89
  128. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +0 -61
  129. package/dist/axom-patch/cli/qoder-steering.json +0 -52
  130. package/dist/axom-patch/cli/qoder-subagent-origin.json +0 -73
  131. package/dist/commands/guide.js +0 -38
  132. package/dist/runtime-deps/qoder-sdk/package-lock.json +0 -1189
  133. package/dist/runtime-deps/qoder-sdk/package.json +0 -8
  134. package/dist/workstation/acp/delegationMcp.js +0 -24
  135. package/dist/workstation/acp/processor/sideChannel.js +0 -80
  136. package/dist/workstation/acp/qoderStartup.js +0 -45
  137. package/dist/workstation/acp/test/raw/qoderCaptures.js +0 -125
  138. package/dist/workstation/qoderAdapter/backgroundProcess.js +0 -118
  139. package/dist/workstation/qoderAdapter/backgroundTerminal.js +0 -193
  140. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +0 -372
  141. package/dist/workstation/qoderAdapter/callbackContext.js +0 -76
  142. package/dist/workstation/qoderAdapter/completedContinuation.js +0 -139
  143. package/dist/workstation/qoderAdapter/contract.js +0 -78
  144. package/dist/workstation/qoderAdapter/foregroundOutput.js +0 -36
  145. package/dist/workstation/qoderAdapter/history.js +0 -85
  146. package/dist/workstation/qoderAdapter/identityIndex.js +0 -191
  147. package/dist/workstation/qoderAdapter/inputQueue.js +0 -42
  148. package/dist/workstation/qoderAdapter/main.js +0 -95
  149. package/dist/workstation/qoderAdapter/mapper.js +0 -117
  150. package/dist/workstation/qoderAdapter/observerPlugin.js +0 -245
  151. package/dist/workstation/qoderAdapter/observerReceiver.js +0 -187
  152. package/dist/workstation/qoderAdapter/permissions.js +0 -61
  153. package/dist/workstation/qoderAdapter/profile.js +0 -93
  154. package/dist/workstation/qoderAdapter/sdkDriver.js +0 -48
  155. package/dist/workstation/qoderAdapter/sdkRuntime.js +0 -93
  156. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +0 -145
  157. package/dist/workstation/qoderAdapter/session.js +0 -319
  158. package/dist/workstation/qoderAdapter/subagentMeta.js +0 -28
  159. package/dist/workstation/qoderAdapter/subwork.js +0 -85
  160. package/dist/workstation/qoderAdapter/taskControl.js +0 -38
  161. package/dist/workstation/qoderAdapter/taskLedger.js +0 -163
  162. package/dist/workstation/qoderAdapter/usage.js +0 -37
  163. /package/dist/workstation/{qoderAdapter/types.js → desktopControl/provider.js} +0 -0
@@ -1,12 +1,17 @@
1
1
  import os from 'node:os';
2
+ import { BindingStore } from './control/bindingStore.js';
3
+ import { stopRuntime } from './control/runtimeClient.js';
4
+ import { LocalControlError } from './control/protocol.js';
5
+ import { normalizeWorkstationServer } from './control/serverIdentity.js';
2
6
  import path from 'node:path';
3
7
  import { fileURLToPath } from 'node:url';
4
- import { clientDir, loadClientConfig } from './config.js';
8
+ import { clientDir, loadClientConfig, updateServiceContext } from './config.js';
5
9
  import { createServiceManager } from './service/installer.js';
6
10
  import { buildServicePath } from './service/types.js';
7
11
  import { loginShellPath } from './service/loginShell.js';
8
12
  import { KeeperLauncher } from './keepers/kit/launcher.js';
9
13
  import { stopKeepers } from './keepers/stop.js';
14
+ import { assertRuntimeRestartSafe } from './control/restartGuard.js';
10
15
  const __filename = fileURLToPath(import.meta.url);
11
16
  /**
12
17
  * The package's real npm bin entry (`package.json`'s `bin.axom`), given the directory a compiled
@@ -36,19 +41,21 @@ export function serviceContext() {
36
41
  throw new Error('Axomwork Workstation user service supervision is supported only on macOS and Linux.');
37
42
  }
38
43
  const directory = path.dirname(__filename);
39
- const nodeDirectory = path.dirname(process.execPath);
44
+ const managed = updateServiceContext();
45
+ const nodeDirectory = path.dirname(managed.nodePath);
40
46
  return {
41
47
  platform: process.platform,
42
48
  username: os.userInfo().username,
43
49
  userId: process.getuid?.() ?? os.userInfo().uid,
44
50
  homeDir: os.homedir(),
45
- nodePath: process.execPath,
51
+ nodePath: managed.nodePath,
46
52
  cliPath: CLI_PATH,
47
- npmPath: process.env.AXOM_WORKSTATION_NPM_PATH ?? path.join(nodeDirectory, 'npm'),
53
+ npmPath: managed.npmPath,
48
54
  // dist/workstation/ is nested under lib/node_modules/@platless/axom-cli; climb to npm's prefix.
49
- npmPrefix: process.env.AXOM_WORKSTATION_NPM_PREFIX ?? path.resolve(directory, '../../../../../..'),
55
+ npmPrefix: managed.npmPrefix,
50
56
  configDir: clientDir(),
51
- pathEnv: buildServicePath(process.env.PATH, nodeDirectory),
57
+ pathEnv: buildServicePath(managed.env?.PATH ?? process.env.PATH, nodeDirectory),
58
+ ...(managed.managed ? { npmConfigPrefix: managed.managed.prefix } : {}),
52
59
  };
53
60
  }
54
61
  /** The workstation's keepers stopped (see {@link DaemonControllerDeps.stopKeepers}); an unregistered client has none. */
@@ -62,6 +69,10 @@ async function stopRegisteredKeepers() {
62
69
  });
63
70
  }
64
71
  const defaultDeps = {
72
+ identity: () => new BindingStore(clientDir()).inspect(),
73
+ runtime: async () => (await (await import('./control/inspect.js')).inspectWorkstation(clientDir())).runtime,
74
+ shutdown: generation => stopRuntime(clientDir(), generation),
75
+ restartSafe: generation => assertRuntimeRestartSafe(clientDir(), generation),
65
76
  service: (context) => createServiceManager(context ?? serviceContext()),
66
77
  config: () => loadClientConfig(clientDir()),
67
78
  stopKeepers: stopRegisteredKeepers,
@@ -72,6 +83,22 @@ const defaultDeps = {
72
83
  */
73
84
  export function createDaemonController(overrides = {}) {
74
85
  const deps = { ...defaultDeps, ...overrides };
86
+ async function checked(options) {
87
+ const runtime = await deps.runtime();
88
+ if (runtime.state === 'unknown')
89
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'Cannot verify the running workstation');
90
+ if (options) {
91
+ const identity = await deps.identity();
92
+ if (identity.revision !== options.guard.revision || runtime.generation !== options.guard.runtimeGeneration)
93
+ throw new LocalControlError('BINDING_CONFLICT', 'Workstation changed; check it again');
94
+ }
95
+ if (runtime.state === 'running') {
96
+ const config = await deps.config();
97
+ if (!runtime.binding || runtime.binding.server !== normalizeWorkstationServer(config.server) || runtime.binding.workstationId !== config.workstationId)
98
+ throw new LocalControlError('INSTANCE_OCCUPIED', 'Another workstation binding is running');
99
+ }
100
+ return runtime;
101
+ }
75
102
  return {
76
103
  async install() {
77
104
  // The definition is written from the login shell's PATH when one can be had, unioned with
@@ -94,21 +121,57 @@ export function createDaemonController(overrides = {}) {
94
121
  finally {
95
122
  await deps.stopKeepers();
96
123
  } },
97
- async start() { await deps.service().start(); },
98
- async stop() { try {
99
- await deps.service().stop();
124
+ async start(options) { if ((await checked(options)).state === 'running')
125
+ return; await deps.service().start(); },
126
+ async stop(options) { const runtime = await checked(options); try {
127
+ if (runtime.mode === 'foreground' && runtime.generation)
128
+ await deps.shutdown(runtime.generation);
129
+ else
130
+ await deps.service().stop();
100
131
  }
101
132
  finally {
102
133
  await deps.stopKeepers();
103
134
  } },
104
135
  // A restart keeps the keepers: terminals, commands and agent sessions go on under the next daemon.
105
- async restart() { await deps.service().restart(); },
136
+ async restart(options) {
137
+ const runtime = await checked(options);
138
+ if (runtime.state === 'running' && runtime.generation)
139
+ await deps.restartSafe(runtime.generation);
140
+ if (runtime.mode === 'foreground' && runtime.generation)
141
+ await deps.shutdown(runtime.generation);
142
+ await deps.service().restart();
143
+ },
144
+ async setEnabled(enabled, options) {
145
+ const runtime = await checked(options);
146
+ const service = deps.service();
147
+ if (!service.setEnabled)
148
+ throw new LocalControlError('UNSUPPORTED_PROTOCOL', 'Service enablement is unavailable');
149
+ if (enabled)
150
+ await service.setEnabled(true);
151
+ else {
152
+ try {
153
+ await service.setEnabled(false);
154
+ if (runtime.mode === 'foreground' && runtime.generation) {
155
+ const current = await deps.runtime();
156
+ if (current.state === 'running' && current.generation === runtime.generation)
157
+ await deps.shutdown(runtime.generation);
158
+ else if (current.state !== 'stopped')
159
+ throw new LocalControlError('OUTCOME_UNKNOWN', 'The running workstation changed during shutdown');
160
+ }
161
+ }
162
+ finally {
163
+ await deps.stopKeepers();
164
+ }
165
+ }
166
+ },
106
167
  async status() {
168
+ const runtime = await deps.runtime();
169
+ if (runtime.state === 'running' && runtime.binding)
170
+ return { running: true, server: runtime.binding.server, workstationId: runtime.binding.workstationId };
171
+ if (runtime.state === 'unknown')
172
+ return { running: false, uncertain: true };
107
173
  const state = await deps.service().status();
108
- if (!state.running)
109
- return { running: false };
110
- const config = await deps.config().catch(() => null);
111
- return { running: true, server: config?.server, workstationId: config?.workstationId };
174
+ return state.running ? { running: true, uncertain: true } : { running: false };
112
175
  },
113
176
  };
114
177
  }
@@ -0,0 +1,99 @@
1
+ import { z } from 'zod';
2
+ export const audioMode = z.enum(['off', 'auto', 'high', 'low']);
3
+ export const AUDIO_HEADER_BYTES = 40;
4
+ export const AUDIO_MAX_PAYLOAD = 1000;
5
+ export const audioConfiguration = z.strictObject({
6
+ commandId: z.string().regex(/^[0-9a-f]{32}$/), audioEpoch: z.number().int().min(1).max(0xffffffff),
7
+ sampleRate: z.literal(48000), channels: z.literal(2), framesPerPacket: z.literal(960),
8
+ bitrate: z.number().int().min(64000).max(256000), lookaheadFrames: z.number().int().min(0).max(960),
9
+ });
10
+ function requireAudio(value) {
11
+ if (!value)
12
+ throw new Error('invalid_audio');
13
+ }
14
+ export function decodeAudio(bytes) {
15
+ requireAudio(bytes.length >= 41 && bytes.length <= 1040 && bytes.subarray(0, 4).equals(Buffer.from('AXDA')));
16
+ requireAudio(bytes[4] === 1 && bytes[5] === 0 && bytes.readUInt16BE(6) === 40 &&
17
+ bytes.readUInt16BE(32) === 960 && bytes[34] === 2 && bytes[35] === 0 &&
18
+ bytes.readUInt16BE(38) === 0 && bytes.readUInt16BE(36) === bytes.length - 40);
19
+ const packet = { epoch: bytes.readUInt32BE(8), sequence: bytes.readUInt32BE(12),
20
+ firstSample: Number(bytes.readBigUInt64BE(16)), captureTimestampUs: Number(bytes.readBigUInt64BE(24)),
21
+ payload: bytes.subarray(40) };
22
+ validate(packet);
23
+ return packet;
24
+ }
25
+ function validate(packet) {
26
+ requireAudio(Number.isInteger(packet.epoch) && packet.epoch >= 1 && packet.epoch <= 0xffffffff &&
27
+ Number.isInteger(packet.sequence) && packet.sequence >= 1 && packet.sequence <= 0xffffffff &&
28
+ Number.isSafeInteger(packet.firstSample) && packet.firstSample === (packet.sequence - 1) * 960 &&
29
+ Number.isSafeInteger(packet.captureTimestampUs) && packet.captureTimestampUs >= 0 &&
30
+ packet.payload.length >= 1 && packet.payload.length <= AUDIO_MAX_PAYLOAD);
31
+ }
32
+ export function encodeAudio(packet) {
33
+ validate(packet);
34
+ const bytes = Buffer.alloc(AUDIO_HEADER_BYTES + packet.payload.length);
35
+ bytes.write('AXDA');
36
+ bytes[4] = 1;
37
+ bytes.writeUInt16BE(40, 6);
38
+ bytes.writeUInt32BE(packet.epoch, 8);
39
+ bytes.writeUInt32BE(packet.sequence, 12);
40
+ bytes.writeBigUInt64BE(BigInt(packet.firstSample), 16);
41
+ bytes.writeBigUInt64BE(BigInt(packet.captureTimestampUs), 24);
42
+ bytes.writeUInt16BE(960, 32);
43
+ bytes[34] = 2;
44
+ bytes.writeUInt16BE(packet.payload.length, 36);
45
+ packet.payload.copy(bytes, 40);
46
+ return bytes;
47
+ }
48
+ export class AudioRateController {
49
+ mode;
50
+ ladder = [64000, 96000, 128000, 192000, 256000];
51
+ current = 128000;
52
+ bad = 0;
53
+ healthy = 0;
54
+ lastObservation = -Infinity;
55
+ lastDown = -Infinity;
56
+ lastUp = -Infinity;
57
+ constructor(mode = 'off') {
58
+ this.mode = mode;
59
+ }
60
+ get bitrate() {
61
+ return this.mode === 'off' ? 0 : this.mode === 'high' ? 256000 : this.mode === 'low' ? 64000 : this.current;
62
+ }
63
+ select(mode) {
64
+ audioMode.parse(mode);
65
+ if (mode !== this.mode) {
66
+ this.current = mode === 'auto' ? 128000 : this.current;
67
+ this.bad = this.healthy = 0;
68
+ this.lastObservation = this.lastDown = this.lastUp = -Infinity;
69
+ }
70
+ this.mode = mode;
71
+ return this.bitrate;
72
+ }
73
+ observe(now, feedback) {
74
+ if (this.mode !== 'auto' || !Number.isFinite(now) || now - this.lastObservation < 500)
75
+ return this.bitrate;
76
+ if (![feedback.queueMs, feedback.lateRatio, feedback.lostRatio, feedback.underruns].every(Number.isFinite) ||
77
+ feedback.queueMs < 0 || feedback.lateRatio < 0 || feedback.lateRatio > 1 ||
78
+ feedback.lostRatio < 0 || feedback.lostRatio > 1 || feedback.underruns < 0)
79
+ return this.bitrate;
80
+ this.lastObservation = now;
81
+ const adverse = feedback.congested || feedback.queueMs > 40 || feedback.lateRatio > 0.02 || feedback.lostRatio > 0.03;
82
+ const recovered = !adverse && feedback.headroom && feedback.queueMs < 15 &&
83
+ feedback.lateRatio < 0.005 && feedback.lostRatio < 0.005 && feedback.underruns === 0;
84
+ this.bad = adverse ? this.bad + 1 : 0;
85
+ this.healthy = recovered ? this.healthy + 1 : 0;
86
+ const index = this.ladder.indexOf(this.current);
87
+ if (this.bad >= 2 && now - this.lastDown >= 1000 && index > 0) {
88
+ this.current = this.ladder[index - 1];
89
+ this.bad = this.healthy = 0;
90
+ this.lastDown = now;
91
+ }
92
+ else if (this.healthy >= 10 && now - Math.max(this.lastUp, this.lastDown) >= 5000 && index < this.ladder.length - 1) {
93
+ this.current = this.ladder[index + 1];
94
+ this.bad = this.healthy = 0;
95
+ this.lastUp = now;
96
+ }
97
+ return this.bitrate;
98
+ }
99
+ }
@@ -0,0 +1,92 @@
1
+ import { canonicalDesktopJson, decodeCapabilityReport, decodeDesktopManagementFrame } from './protocol.js';
2
+ export const DESKTOP_PROBE_CAP_MS = 4000;
3
+ /** Failure telemetry only: no backend, geometry or input is eligible for admission. */
4
+ const unavailable = () => ({
5
+ protocols: { min: 2, max: 2 }, providerId: 'discovery', providerVersion: 'unknown', configured: true,
6
+ permission: 'not_applicable', health: 'unavailable', reason: 'probe_failed', formats: ['h264-420-v1'], encoderBackends: [],
7
+ input: { pointer: false, physicalKeyboard: false, unicodeText: false },
8
+ displays: [{ id: '0'.repeat(32), label: 'Unavailable', width: 2, height: 2, revision: 1, resize: 'none', modes: [] }],
9
+ });
10
+ export async function capabilitiesForWire(options) {
11
+ if (options.selectedWire < 9)
12
+ return undefined;
13
+ let timer;
14
+ const generation = options.generation ?? 'local';
15
+ if (!options.provider)
16
+ return { generation, revision: 1, desktop: null };
17
+ try {
18
+ const cap = Math.max(1, Math.min(DESKTOP_PROBE_CAP_MS, options.probeCapMs ?? DESKTOP_PROBE_CAP_MS));
19
+ const desktop = await Promise.race([
20
+ Promise.resolve().then(() => options.provider.probe()),
21
+ new Promise(resolve => { timer = setTimeout(() => resolve(unavailable()), cap); timer.unref?.(); }),
22
+ ]);
23
+ const report = decodeCapabilityReport(JSON.stringify({ generation, revision: 1, desktop: desktop && { ...desktop, lobby: true } }));
24
+ decodeDesktopManagementFrame(JSON.stringify({ t: 'workstation_capabilities', ...report }));
25
+ return report;
26
+ }
27
+ catch {
28
+ return { generation, revision: 1, desktop: unavailable() };
29
+ }
30
+ finally {
31
+ clearTimeout(timer);
32
+ }
33
+ }
34
+ export class DesktopCapabilityReporter {
35
+ options;
36
+ current;
37
+ canonical;
38
+ running = false;
39
+ again = false;
40
+ disposed = false;
41
+ probe;
42
+ constructor(options) {
43
+ this.options = options;
44
+ }
45
+ get revision() { return this.current?.revision; }
46
+ refresh() {
47
+ if (this.disposed || this.options.selectedWire < 9)
48
+ return;
49
+ if (this.running) {
50
+ this.again = true;
51
+ return;
52
+ }
53
+ this.running = true;
54
+ void this.report().finally(() => {
55
+ this.running = false;
56
+ if (this.again) {
57
+ this.again = false;
58
+ this.refresh();
59
+ }
60
+ });
61
+ }
62
+ async report() {
63
+ const provider = this.options.provider;
64
+ // A wedged probe remains a single outstanding operation, even across repeated requests.
65
+ const boundedProvider = provider && { ...provider, probe: () => {
66
+ if (!this.probe) {
67
+ const pending = Promise.resolve().then(() => provider.probe());
68
+ this.probe = pending;
69
+ void pending.then(() => { if (this.probe === pending)
70
+ this.probe = undefined; }, () => { if (this.probe === pending)
71
+ this.probe = undefined; });
72
+ }
73
+ return this.probe;
74
+ } };
75
+ const report = await capabilitiesForWire({ ...this.options, provider: boundedProvider });
76
+ if (this.disposed || !report)
77
+ return;
78
+ const canonical = canonicalDesktopJson(report.desktop);
79
+ const revision = this.current ? this.current.revision + (canonical === this.canonical ? 0 : 1) : 1;
80
+ if (!Number.isSafeInteger(revision)) {
81
+ this.dispose();
82
+ return;
83
+ }
84
+ this.canonical = canonical;
85
+ this.current = { ...report, revision };
86
+ try {
87
+ this.options.send(this.current);
88
+ }
89
+ catch { /* a later request/heartbeat retries the snapshot */ }
90
+ }
91
+ dispose() { this.disposed = true; this.again = false; }
92
+ }
@@ -0,0 +1,9 @@
1
+ /** Capture sampling presets preserve the source aspect and never change its display mode. */
2
+ export function captureSamplingModes(width, height, pixelScale = 1) {
3
+ const w = width * pixelScale, h = height * pixelScale;
4
+ const modes = [1280, 1920, 2560, 3840, 4096].map(edge => {
5
+ const scale = Math.min(1, edge / Math.max(w, h), 4096 / w, 4096 / h, Math.sqrt(8847360 / (w * h)));
6
+ return { width: Math.max(2, Math.floor(w * scale / 2) * 2), height: Math.max(2, Math.floor(h * scale / 2) * 2) };
7
+ });
8
+ return modes.filter((mode, index) => modes.findIndex(m => m.width === mode.width && m.height === mode.height) === index);
9
+ }
@@ -0,0 +1,9 @@
1
+ export function captureTargetLabel(label) {
2
+ let result = '';
3
+ for (const char of label.replace(/[\x00-\x1f\x7f]/g, ' ')) {
4
+ if (Buffer.byteLength(result + char) > 128)
5
+ break;
6
+ result += char;
7
+ }
8
+ return result.trim() || 'Window';
9
+ }
@@ -0,0 +1,246 @@
1
+ import WebSocket from 'ws';
2
+ import { randomBytes } from 'node:crypto';
3
+ import { openQuicMedia } from './quicMedia.js';
4
+ import { isTrustedPlaintextHost } from '../channels/url.js';
5
+ import { decodeDesktopControl, encodeDesktopControl, parseDesktopJson } from './protocol.js';
6
+ import { decodeDesktopTransport, encodeDesktopTransport } from './transport.js';
7
+ import { bounded, DesktopQueue, NativeCommandError } from './helperProcess.js';
8
+ import { mediaRequire } from './media.js';
9
+ export function desktopSocketUrl(server, role, allow = false) {
10
+ const url = new URL(server);
11
+ mediaRequire(!url.username && !url.password && !url.search && !url.hash && ['https:', 'http:'].includes(url.protocol));
12
+ mediaRequire(url.protocol === 'https:' || allow && isTrustedPlaintextHost(url.hostname));
13
+ url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:';
14
+ url.pathname = `${url.pathname.replace(/\/$/, '')}/workstation/ws/desktop/${role}`;
15
+ return url;
16
+ }
17
+ /** Fixed paths, certificate validation on both legs, and no redirect/URL credential propagation. */
18
+ export class DeviceSockets {
19
+ server;
20
+ allowPrivateWs;
21
+ quicRetryAt = 0;
22
+ constructor(server, allowPrivateWs = false) {
23
+ this.server = server;
24
+ this.allowPrivateWs = allowPrivateWs;
25
+ }
26
+ async uploadWallpaper(grant, wallpaper, unchanged = false) {
27
+ const url = desktopSocketUrl(this.server, 'media', this.allowPrivateWs);
28
+ url.protocol = url.protocol === 'wss:' ? 'https:' : 'http:';
29
+ url.pathname = url.pathname.replace(/\/ws\/desktop\/media$/, '/desktop/background');
30
+ const headers = { 'X-Axom-Background-Grant': grant, 'Content-Type': 'image/jpeg' };
31
+ if (wallpaper) {
32
+ headers['X-Axom-Background-Asset'] = Buffer.from(JSON.stringify(wallpaper.asset)).toString('base64url');
33
+ headers['X-Axom-Background-Unchanged'] = unchanged ? '1' : '0';
34
+ }
35
+ else
36
+ headers['X-Axom-Background-Error'] = 'unavailable';
37
+ const response = await fetch(url, { method: 'POST', redirect: 'error', signal: AbortSignal.timeout(10_000), headers,
38
+ body: wallpaper && !unchanged ? new Uint8Array(wallpaper.jpeg) : undefined });
39
+ if (!response.ok)
40
+ throw new NativeCommandError('transport_failed');
41
+ await response.body?.cancel();
42
+ }
43
+ async control(binding, signal, current) {
44
+ const queue = new DesktopQueue();
45
+ const socket = await this.open(binding, binding.controlGrant, 'control', signal, current, bytes => {
46
+ const raw = parseDesktopJson(bytes);
47
+ if (raw.type === 'media_grant') {
48
+ const grant = decodeDesktopTransport(bytes);
49
+ mediaRequire(grant.type === 'media_grant' && grant.controlGeneration === binding.controlGeneration && grant.sessionId === binding.sessionId);
50
+ queue.push(grant);
51
+ }
52
+ else {
53
+ const message = decodeDesktopControl(bytes);
54
+ mediaRequire(message.sessionId === binding.sessionId && !['clipboard_result', 'offer', 'configured', 'input_result', 'resize_result', 'media_open', 'appearance_result', 'motion_cut_barrier', 'motion_cut_prepared', 'motion_cut_request', 'motion_cut_ack', 'motion_cut_fence', 'audio_configured', 'audio_state'].includes(message.type));
55
+ queue.push(message);
56
+ }
57
+ }, () => queue.close());
58
+ let overview;
59
+ let audio;
60
+ let audioOpening;
61
+ let audioSerial = 0;
62
+ const audioCostId = randomBytes(16).toString('hex');
63
+ let audioBytes = 0;
64
+ const prepareAudio = async () => {
65
+ if (!socket.audioToken || !current() || signal.aborted)
66
+ throw new NativeCommandError('audio_unavailable');
67
+ const serial = audioSerial;
68
+ audioOpening ??= this.open(binding, socket.audioToken, 'audio', signal, current, () => { });
69
+ const connection = await audioOpening;
70
+ if (serial !== audioSerial || !current()) {
71
+ connection.close();
72
+ throw new NativeCommandError('audio_unavailable');
73
+ }
74
+ audio = connection;
75
+ void connection.closed.then(() => { if (audio === connection) {
76
+ audio = undefined;
77
+ audioOpening = undefined;
78
+ } });
79
+ };
80
+ let retryAt = 0;
81
+ return { messages: queue, send: message => socket.send(encodeDesktopControl(message)),
82
+ prepareAudio: socket.audioToken ? prepareAudio : undefined,
83
+ sendAudio: socket.audioToken ? async (bytes) => {
84
+ if (!current() || signal.aborted)
85
+ return false;
86
+ const serial = audioSerial;
87
+ try {
88
+ if (!audio)
89
+ await prepareAudio();
90
+ const connection = audio;
91
+ if (serial !== audioSerial || !current()) {
92
+ connection.close();
93
+ return false;
94
+ }
95
+ audio = connection;
96
+ await bounded(connection.send(bytes), 80, 'audio_stale');
97
+ audioBytes += bytes.length + 32;
98
+ return true;
99
+ }
100
+ catch {
101
+ audio?.close();
102
+ audio = undefined;
103
+ audioOpening = undefined;
104
+ return false;
105
+ }
106
+ } : undefined,
107
+ stopAudio: () => {
108
+ audioSerial++;
109
+ audio?.close();
110
+ void audioOpening?.then(connection => connection.close(), () => { });
111
+ audio = undefined;
112
+ audioOpening = undefined;
113
+ },
114
+ audioTransportCost: () => ({ id: audioCostId, totalBytes: audioBytes }),
115
+ sendOverview: socket.overviewToken ? async (bytes) => {
116
+ if (performance.now() < retryAt || !current() || signal.aborted)
117
+ return false;
118
+ try {
119
+ overview ??= await this.open(binding, socket.overviewToken, 'overview', signal, current, () => { });
120
+ await overview.send(bytes);
121
+ return true;
122
+ }
123
+ catch {
124
+ overview?.close();
125
+ overview = undefined;
126
+ retryAt = performance.now() + 5000;
127
+ return false;
128
+ }
129
+ } : undefined,
130
+ close: () => { audioSerial++; audio?.close(); overview?.close(); socket.close(); } };
131
+ }
132
+ async media(binding, grant, signal, current, quic) {
133
+ desktopSocketUrl(this.server, 'media', this.allowPrivateWs); // Apply the same origin/TLS policy before either transport.
134
+ if (quic && binding.mediaTransport !== 'wss' && (binding.mediaTransport === 'quic' || performance.now() >= this.quicRetryAt)) {
135
+ const native = await openQuicMedia(this.server, quic, signal, current);
136
+ if (native) {
137
+ // Once the single-use credential is sent, recovery needs a fresh epoch;
138
+ // never replay it over WSS after an ambiguous attachment acknowledgement.
139
+ try {
140
+ await native.send(2, Buffer.from(encodeDesktopTransport({ type: 'device_attach', sessionId: binding.sessionId, grant })));
141
+ const ack = await native.receive(5000);
142
+ mediaRequire(ack.kind === 2);
143
+ const attached = decodeDesktopTransport(ack.bytes);
144
+ mediaRequire(attached.type === 'attached' && attached.sessionId === binding.sessionId && attached.role === 'device_media' && attached.controlGeneration === binding.controlGeneration);
145
+ console.error(JSON.stringify({ event: 'desktop_media_attached', transport: 'quic', role: 'device_media', session: binding.sessionId }));
146
+ void native.receive().then(() => native.close(), () => native.close());
147
+ const id = randomBytes(16).toString('hex');
148
+ return { sendAudio: native.sendAudio, transportCost: () => ({ id, totalBytes: native.transportCost() }), networkSample: () => { const stats = native.pathStats(); return stats ? { ...stats, id } : undefined; }, send: bytes => native.send(3, bytes), close: () => native.close(), closed: native.closed, cutEvents: native.cutEvents, cutCommand: native.cutCommand, finishCut: native.finishCut };
149
+ }
150
+ catch (error) {
151
+ this.quicRetryAt = performance.now() + 60000;
152
+ native.close();
153
+ throw error;
154
+ }
155
+ }
156
+ else
157
+ this.quicRetryAt = performance.now() + 60000;
158
+ }
159
+ if (binding.mediaTransport === 'quic')
160
+ throw new NativeCommandError('transport_failed');
161
+ const socket = await this.open(binding, grant, 'media', signal, current, () => { throw new NativeCommandError('invalid_request'); });
162
+ console.error(JSON.stringify({ event: 'desktop_media_attached', transport: 'wss', role: 'device_media', fallback: !quic ? 'not_offered' : 'connect_failed_or_backoff', session: binding.sessionId }));
163
+ const id = randomBytes(16).toString('hex');
164
+ let framingBytes = 0;
165
+ return { transportCost: () => ({ id, totalBytes: framingBytes }), send: async (bytes) => {
166
+ await socket.send(bytes);
167
+ framingBytes += bytes.length < 126 ? 6 : bytes.length <= 65535 ? 8 : 14;
168
+ }, sendCutBarrier: message => socket.send(encodeDesktopControl(message)), close: socket.close, closed: socket.closed };
169
+ }
170
+ async open(binding, grant, role, signal, current, onMessage, onClose = () => { }) {
171
+ mediaRequire(current() && !signal.aborted);
172
+ const ws = new WebSocket(desktopSocketUrl(this.server, role, this.allowPrivateWs), {
173
+ headers: role === 'control' ? { 'X-Axom-Desktop-Quic': '1', 'X-Axom-Desktop-Overview': '1', 'X-Axom-Desktop-Audio': '1' } : {},
174
+ rejectUnauthorized: true, followRedirects: false, maxPayload: 49152, perMessageDeflate: false,
175
+ });
176
+ let overviewToken;
177
+ let audioToken;
178
+ let attached = false, settle, reject;
179
+ const opening = new Promise((resolve, fail) => { settle = resolve; reject = fail; });
180
+ const close = () => { if (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)
181
+ ws.terminate(); };
182
+ const closed = new Promise(resolve => ws.once('close', () => {
183
+ signal.removeEventListener('abort', close);
184
+ onClose();
185
+ resolve();
186
+ if (!attached)
187
+ reject(new NativeCommandError('authorization_lost'));
188
+ }));
189
+ signal.addEventListener('abort', close, { once: true });
190
+ ws.on('error', () => { reject(new NativeCommandError('transport_failed')); close(); });
191
+ ws.once('open', () => {
192
+ if (!current() || signal.aborted) {
193
+ close();
194
+ return;
195
+ }
196
+ ws.send(encodeDesktopTransport({ type: 'device_attach', sessionId: binding.sessionId, grant }));
197
+ });
198
+ ws.on('message', (data, binary) => {
199
+ try {
200
+ if (!current() || signal.aborted) {
201
+ close();
202
+ return;
203
+ }
204
+ mediaRequire(!binary);
205
+ const bytes = Buffer.isBuffer(data) ? data : Buffer.from(data);
206
+ if (!attached) {
207
+ const ack = decodeDesktopTransport(bytes);
208
+ mediaRequire(ack.type === 'attached' && ack.sessionId === binding.sessionId && ack.role === (role === 'overview' || role === 'audio' ? 'device_control' : `device_${role}`) && ack.controlGeneration === binding.controlGeneration);
209
+ overviewToken = ack.overviewToken;
210
+ audioToken = ack.audioToken;
211
+ attached = true;
212
+ settle();
213
+ }
214
+ else
215
+ onMessage(bytes);
216
+ }
217
+ catch {
218
+ reject(new NativeCommandError('invalid_request'));
219
+ close();
220
+ }
221
+ });
222
+ try {
223
+ await bounded(opening, 5000);
224
+ mediaRequire(current() && !signal.aborted);
225
+ }
226
+ catch (error) {
227
+ close();
228
+ throw error;
229
+ }
230
+ let sendTail = Promise.resolve();
231
+ let pending = 0;
232
+ return { close, closed, overviewToken, audioToken, send: bytes => {
233
+ if (pending >= (role === 'media' ? 3 : 2))
234
+ return Promise.reject(new NativeCommandError('transport_failed'));
235
+ pending++;
236
+ const perform = async () => {
237
+ // This is the last check after any queued await, immediately before ws.send.
238
+ mediaRequire(current() && !signal.aborted && ws.readyState === WebSocket.OPEN && ws.bufferedAmount <= (role === 'media' ? 524288 : 65600));
239
+ await bounded(new Promise((resolve, fail) => ws.send(bytes, error => error ? fail(new NativeCommandError('transport_failed')) : resolve())), 5000);
240
+ };
241
+ const sent = role === 'media' ? perform() : sendTail.then(perform);
242
+ sendTail = sent.catch(() => { close(); }).finally(() => { pending--; });
243
+ return sent;
244
+ } };
245
+ }
246
+ }