@platless/axom-cli 0.3.13 → 0.5.9

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 (235) hide show
  1. package/dist/axom-patch/cli/README.md +30 -8
  2. package/dist/axom-patch/cli/account-limits/README.md +27 -0
  3. package/dist/axom-patch/cli/account-limits/antigravity-proxy.mjs +47 -0
  4. package/dist/axom-patch/cli/account-limits/antigravity-read.mjs +90 -0
  5. package/dist/axom-patch/cli/account-limits/antigravity.mjs +46 -0
  6. package/dist/axom-patch/cli/account-limits/generate.py +120 -0
  7. package/dist/axom-patch/cli/account-limits/kimi-proxy.mjs +64 -0
  8. package/dist/axom-patch/cli/account-limits/normalize.mjs +151 -0
  9. package/dist/axom-patch/cli/claude-account-limits.json +37 -0
  10. package/dist/axom-patch/cli/codex-account-limits.json +37 -0
  11. package/dist/axom-patch/cli/commandcode-account-limits.json +30 -0
  12. package/dist/axom-patch/cli/nightly-task.md +152 -0
  13. package/dist/axom-patch/cli/pi-subagent-subwork.json +23 -0
  14. package/dist/axom-patch/cli/probes/claude-late-async-terminal.mjs +5 -1
  15. package/dist/axom-patch/cli/probes/pi-subagent-subwork.mjs +211 -0
  16. package/dist/axom-patch/cli/probes/qoder-steering.mjs +1 -1
  17. package/dist/axom-patch/cli/probes/qoder-subagent-origin.mjs +20 -8
  18. package/dist/axom-patch/cli/probes/zcode-subagent-subwork.mjs +257 -0
  19. package/dist/axom-patch/cli/qoder-background-terminal.md +2 -1
  20. package/dist/axom-patch/cli/qoder-foreground-output.md +4 -3
  21. package/dist/axom-patch/cli/qoder-steering.json +15 -15
  22. package/dist/axom-patch/cli/qoder-subagent-origin.json +21 -21
  23. package/dist/axom-patch/cli/runtime.json +34 -8
  24. package/dist/axom-patch/cli/zcode-account-limits.json +31 -0
  25. package/dist/axom-patch/cli/zcode-models.json +38 -0
  26. package/dist/axom-patch/cli/zcode-subagent-subwork.json +73 -0
  27. package/dist/cli.js +33 -6
  28. package/dist/keepers/acp/keeper.mjs +3083 -0
  29. package/dist/keepers/acp/manifest.json +9 -0
  30. package/dist/keepers/terminal/keeper.mjs +8469 -0
  31. package/dist/keepers/terminal/manifest.json +9 -0
  32. package/dist/keepers/terminal/node_modules/node-pty/lib/conpty_console_list_agent.js +25 -0
  33. package/dist/keepers/terminal/node_modules/node-pty/lib/eventEmitter2.js +47 -0
  34. package/dist/keepers/terminal/node_modules/node-pty/lib/index.js +52 -0
  35. package/dist/keepers/terminal/node_modules/node-pty/lib/interfaces.js +7 -0
  36. package/dist/keepers/terminal/node_modules/node-pty/lib/shared/conout.js +10 -0
  37. package/dist/keepers/terminal/node_modules/node-pty/lib/terminal.js +190 -0
  38. package/dist/keepers/terminal/node_modules/node-pty/lib/types.js +7 -0
  39. package/dist/keepers/terminal/node_modules/node-pty/lib/unixTerminal.js +351 -0
  40. package/dist/keepers/terminal/node_modules/node-pty/lib/utils.js +38 -0
  41. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsConoutConnection.js +131 -0
  42. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsPtyAgent.js +344 -0
  43. package/dist/keepers/terminal/node_modules/node-pty/lib/windowsTerminal.js +209 -0
  44. package/dist/keepers/terminal/node_modules/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  45. package/dist/keepers/terminal/node_modules/node-pty/package.json +66 -0
  46. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/pty.node +0 -0
  47. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-arm64/spawn-helper +0 -0
  48. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/pty.node +0 -0
  49. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/darwin-x64/spawn-helper +0 -0
  50. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-arm64/pty.node +0 -0
  51. package/dist/keepers/terminal/node_modules/node-pty/prebuilds/linux-x64/pty.node +0 -0
  52. package/dist/runtime-deps/qoder-sdk/package-lock.json +1189 -0
  53. package/dist/runtime-deps/qoder-sdk/package.json +8 -0
  54. package/dist/test/keeperKit.js +67 -0
  55. package/dist/workstation/acp/acpAgents.js +2163 -611
  56. package/dist/workstation/acp/acpClient.js +60 -497
  57. package/dist/workstation/acp/adoption.js +10 -396
  58. package/dist/workstation/acp/binaryInstall.js +100 -27
  59. package/dist/workstation/acp/builds/gc.js +245 -0
  60. package/dist/workstation/acp/builds/install/binary.js +28 -0
  61. package/dist/workstation/acp/builds/install/npm.js +235 -0
  62. package/dist/workstation/acp/builds/install/uv.js +254 -0
  63. package/dist/workstation/acp/builds/migrate.js +195 -0
  64. package/dist/workstation/acp/builds/model.js +128 -0
  65. package/dist/workstation/acp/builds/nodeGate.js +100 -0
  66. package/dist/workstation/acp/builds/store.js +352 -0
  67. package/dist/workstation/acp/commandTableProbe.js +68 -0
  68. package/dist/workstation/acp/configOptions.js +103 -1
  69. package/dist/workstation/acp/envSecrets.js +27 -0
  70. package/dist/workstation/acp/initializeResponse.js +285 -0
  71. package/dist/workstation/acp/installErrors.js +12 -2
  72. package/dist/workstation/acp/installProbe.js +1 -1
  73. package/dist/workstation/acp/managedPatches.js +76 -160
  74. package/dist/workstation/acp/outbox.js +330 -0
  75. package/dist/workstation/acp/patchAssets.js +20 -3
  76. package/dist/workstation/acp/permission.js +42 -10
  77. package/dist/workstation/acp/processor/checkpoint.js +87 -0
  78. package/dist/workstation/acp/processor/commands.js +35 -0
  79. package/dist/workstation/acp/processor/control.js +355 -0
  80. package/dist/workstation/acp/processor/emit.js +26 -0
  81. package/dist/workstation/acp/processor/jsonrpc.js +172 -0
  82. package/dist/workstation/acp/processor/memoryLog.js +49 -0
  83. package/dist/workstation/acp/processor/origins.js +48 -0
  84. package/dist/workstation/acp/processor/output.js +67 -0
  85. package/dist/workstation/acp/processor/projection.js +42 -0
  86. package/dist/workstation/acp/processor/record.js +128 -0
  87. package/dist/workstation/acp/processor/runtime.js +181 -0
  88. package/dist/workstation/acp/processor/sdkSchemas.js +61 -0
  89. package/dist/workstation/acp/processor/session.js +525 -0
  90. package/dist/workstation/acp/processor/sideChannel.js +80 -0
  91. package/dist/workstation/acp/processor/state.js +74 -0
  92. package/dist/workstation/acp/processor/step.js +459 -0
  93. package/dist/workstation/acp/processor/test/fixtureLog.js +65 -0
  94. package/dist/workstation/acp/processor/test/harness.js +120 -0
  95. package/dist/workstation/acp/processor/test/largeIdEcho.js +50 -0
  96. package/dist/workstation/acp/processor/test/rawAgent.js +70 -0
  97. package/dist/workstation/acp/processor/transport/generations.js +76 -0
  98. package/dist/workstation/acp/processor/transport/keeper.js +130 -0
  99. package/dist/workstation/acp/processor/transport/local.js +269 -0
  100. package/dist/workstation/acp/processor/transport/types.js +78 -0
  101. package/dist/workstation/acp/processor/updates.js +364 -0
  102. package/dist/workstation/acp/processorSession.js +1512 -0
  103. package/dist/workstation/acp/qoderStartup.js +1 -1
  104. package/dist/workstation/acp/registry/catalog.js +154 -32
  105. package/dist/workstation/acp/registry/launch.js +61 -119
  106. package/dist/workstation/acp/sessionUpdateMapper.js +18 -7
  107. package/dist/workstation/acp/steering.js +58 -0
  108. package/dist/workstation/acp/switching.js +114 -0
  109. package/dist/workstation/acp/terminalLogin.js +16 -42
  110. package/dist/workstation/acp/terminalLoginSpec.js +41 -0
  111. package/dist/workstation/acp/test/adapterFixtures.js +256 -0
  112. package/dist/workstation/acp/test/daemonLife.js +106 -0
  113. package/dist/workstation/acp/test/keeperWorld.js +267 -0
  114. package/dist/workstation/acp/test/memoryOutbox.js +55 -0
  115. package/dist/workstation/acp/test/raw/fixtures.js +196 -0
  116. package/dist/workstation/acp/test/raw/qoderCaptures.js +125 -0
  117. package/dist/workstation/acp/test/raw/sanitize.js +344 -0
  118. package/dist/workstation/acp/test/raw/sources.js +249 -0
  119. package/dist/workstation/acp/test/realAdapter.js +377 -0
  120. package/dist/workstation/acp/test/scriptAgent.js +206 -0
  121. package/dist/workstation/acp/test/sessionFrames.js +42 -0
  122. package/dist/workstation/acp/timedCommand.js +4 -2
  123. package/dist/workstation/acp/uvxInstall.js +72 -122
  124. package/dist/workstation/agentFlow.js +103 -0
  125. package/dist/workstation/agentSessions.js +1324 -474
  126. package/dist/workstation/channels/agent.js +7 -5
  127. package/dist/workstation/channels/computer.js +69 -21
  128. package/dist/workstation/channels/computerExecs.js +200 -0
  129. package/dist/workstation/channels/management.js +84 -4
  130. package/dist/workstation/daemon.js +288 -7
  131. package/dist/workstation/daemonctl.js +28 -2
  132. package/dist/workstation/exec/attach.js +126 -0
  133. package/dist/workstation/exec/command.js +40 -0
  134. package/dist/workstation/exec/durable.js +517 -0
  135. package/dist/workstation/exec/fallback.js +106 -0
  136. package/dist/workstation/exec/policy.js +837 -0
  137. package/dist/workstation/exec/shaping.js +211 -0
  138. package/dist/workstation/exec.js +3 -112
  139. package/dist/workstation/files/adapters/computerFileFrames.js +51 -0
  140. package/dist/workstation/files/adapters/toolRequests.js +121 -0
  141. package/dist/workstation/files/client.js +455 -0
  142. package/dist/workstation/files/errors.js +28 -0
  143. package/dist/workstation/files/fence.js +350 -0
  144. package/dist/workstation/files/find/fileIndex.js +153 -0
  145. package/dist/workstation/files/find/fuzzy.js +155 -0
  146. package/dist/workstation/files/host.js +122 -0
  147. package/dist/workstation/files/hostProtocol.js +2 -0
  148. package/dist/workstation/files/media/cache.js +620 -0
  149. package/dist/workstation/files/media/encoding.js +308 -0
  150. package/dist/workstation/files/media/estimator.js +100 -0
  151. package/dist/workstation/files/media/media.js +325 -0
  152. package/dist/workstation/files/media/packedAudio.js +23 -0
  153. package/dist/workstation/files/media/scheduler.js +551 -0
  154. package/dist/workstation/files/media/types.js +5 -0
  155. package/dist/workstation/files/scope.js +45 -0
  156. package/dist/workstation/files/structured.js +284 -0
  157. package/dist/workstation/{fileOps.js → files/text.js} +117 -38
  158. package/dist/workstation/files/uploads.js +351 -0
  159. package/dist/workstation/journal.js +65 -236
  160. package/dist/workstation/keepers/acp/client.js +101 -0
  161. package/dist/workstation/keepers/acp/envelope.js +62 -0
  162. package/dist/workstation/keepers/acp/host.js +419 -0
  163. package/dist/workstation/keepers/acp/main.js +18 -0
  164. package/dist/workstation/keepers/acp/memoryStream.js +121 -0
  165. package/dist/workstation/keepers/acp/params.js +137 -0
  166. package/dist/workstation/keepers/acp/protocol.js +66 -0
  167. package/dist/workstation/keepers/acp/recover.js +120 -0
  168. package/dist/workstation/keepers/acp/session.js +584 -0
  169. package/dist/workstation/keepers/acp/stderr.js +154 -0
  170. package/dist/workstation/keepers/kit/activity.js +103 -0
  171. package/dist/workstation/keepers/kit/bootstrap.js +164 -0
  172. package/dist/workstation/keepers/kit/buildVersion.js +51 -0
  173. package/dist/workstation/keepers/kit/bundle.js +120 -0
  174. package/dist/workstation/keepers/kit/client.js +432 -0
  175. package/dist/workstation/keepers/kit/identity.js +21 -0
  176. package/dist/workstation/keepers/kit/ipc.js +112 -0
  177. package/dist/workstation/keepers/kit/launchConfig.js +8 -0
  178. package/dist/workstation/keepers/kit/launcher.js +249 -0
  179. package/dist/workstation/keepers/kit/lock.js +123 -0
  180. package/dist/workstation/keepers/kit/orphanSweep.js +86 -0
  181. package/dist/workstation/keepers/kit/paths.js +75 -0
  182. package/dist/workstation/keepers/kit/protocol.js +43 -0
  183. package/dist/workstation/keepers/kit/server.js +518 -0
  184. package/dist/workstation/keepers/kit/streamLog.js +697 -0
  185. package/dist/workstation/keepers/legacy.js +296 -0
  186. package/dist/workstation/keepers/policy.js +576 -0
  187. package/dist/workstation/keepers/stop.js +34 -0
  188. package/dist/workstation/keepers/terminal/client.js +213 -0
  189. package/dist/workstation/keepers/terminal/exec.js +981 -0
  190. package/dist/workstation/keepers/terminal/generations.js +129 -0
  191. package/dist/workstation/keepers/terminal/keeper.js +62 -0
  192. package/dist/workstation/keepers/terminal/main.js +43 -0
  193. package/dist/workstation/keepers/terminal/orphanSweep.js +3 -0
  194. package/dist/workstation/keepers/terminal/params.js +27 -0
  195. package/dist/workstation/keepers/terminal/protocol.js +56 -0
  196. package/dist/workstation/keepers/terminal/pty.js +330 -0
  197. package/dist/workstation/protocol/accountLimits.js +47 -0
  198. package/dist/workstation/protocol/agent.js +48 -4
  199. package/dist/workstation/protocol/common.js +30 -0
  200. package/dist/workstation/protocol/computer.js +15 -4
  201. package/dist/workstation/protocol/management.js +50 -2
  202. package/dist/workstation/qoderAdapter/backgroundProcess.js +18 -6
  203. package/dist/workstation/qoderAdapter/backgroundTerminal.js +78 -147
  204. package/dist/workstation/qoderAdapter/backgroundTerminalFold.js +372 -0
  205. package/dist/workstation/qoderAdapter/callbackContext.js +76 -0
  206. package/dist/workstation/qoderAdapter/foregroundOutput.js +15 -9
  207. package/dist/workstation/qoderAdapter/history.js +1 -3
  208. package/dist/workstation/qoderAdapter/main.js +4 -2
  209. package/dist/workstation/qoderAdapter/sdkDriver.js +1 -2
  210. package/dist/workstation/qoderAdapter/sdkRuntime.js +93 -0
  211. package/dist/workstation/qoderAdapter/sdkRuntimeInstall.js +145 -0
  212. package/dist/workstation/qoderAdapter/session.js +1 -1
  213. package/dist/workstation/qoderAdapter/subwork.js +15 -7
  214. package/dist/workstation/systemMetrics.js +110 -1
  215. package/dist/workstation/tools/client.js +9 -1
  216. package/dist/workstation/tools/info.js +50 -0
  217. package/dist/workstation/tools/keeperProcess.js +15 -5
  218. package/dist/workstation/tools/router.js +55 -0
  219. package/dist/workstation/tools/terminalRouter.js +222 -0
  220. package/dist/workstation/tools/validation.js +37 -1
  221. package/dist/workstation/update/coordinator.js +3 -0
  222. package/dist/workstation/update/ptyPreflight.js +21 -0
  223. package/dist/workstation/update/taskRegistry.js +105 -11
  224. package/package.json +10 -7
  225. package/dist/workstation/acp/acpSession.js +0 -1475
  226. package/dist/workstation/acp/contract.js +0 -499
  227. package/dist/workstation/acp/draftInterceptor.js +0 -163
  228. package/dist/workstation/acp/sessionScope.js +0 -19
  229. package/dist/workstation/filePaths.js +0 -174
  230. package/dist/workstation/fileWorker.js +0 -81
  231. package/dist/workstation/fileWorkerClient.js +0 -121
  232. package/dist/workstation/qoderAdapter/taskEvent.js +0 -26
  233. package/dist/workstation/tools/core.js +0 -50
  234. package/dist/workstation/tools/files.js +0 -525
  235. package/dist/workstation/tools/terminal.js +0 -214
@@ -0,0 +1,145 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';
4
+ import { delimiter, dirname, join, resolve } from 'node:path';
5
+ import { pathToFileURL } from 'node:url';
6
+ import { assertSdkApi, QODER_SDK_VERSION, readPreparedSdkRuntime, sdkRecipe, sha256, validateSdkInstance } from './sdkRuntime.js';
7
+ /** No shell, bounded output, and no surviving npm subprocesses after timeout or cancellation. */
8
+ export function runSdkCommand(command, args, options) {
9
+ return new Promise((resolvePromise, reject) => {
10
+ if (options.signal?.aborted) {
11
+ reject(new Error('Qoder SDK preparation cancelled'));
12
+ return;
13
+ }
14
+ if (options.timeoutMs <= 0) {
15
+ reject(new Error('Qoder SDK preparation timed out'));
16
+ return;
17
+ }
18
+ let output = '', failure;
19
+ const child = spawn(command, args, { cwd: options.cwd, stdio: ['ignore', 'pipe', 'pipe'],
20
+ detached: process.platform !== 'win32', windowsHide: true });
21
+ const capture = (chunk) => { output = (output + chunk.toString()).slice(-8192); };
22
+ child.stdout.on('data', capture);
23
+ child.stderr.on('data', capture);
24
+ const stop = (reason) => {
25
+ failure ??= new Error(reason);
26
+ if (!child.pid)
27
+ return;
28
+ if (process.platform === 'win32') {
29
+ const killer = spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' });
30
+ killer.on('error', () => { child.kill('SIGKILL'); });
31
+ }
32
+ else {
33
+ try {
34
+ process.kill(-child.pid, 'SIGKILL');
35
+ }
36
+ catch {
37
+ child.kill('SIGKILL');
38
+ }
39
+ }
40
+ };
41
+ const abort = () => stop('Qoder SDK preparation cancelled');
42
+ const timer = setTimeout(() => stop('Qoder SDK preparation timed out'), options.timeoutMs);
43
+ options.signal?.addEventListener('abort', abort, { once: true });
44
+ const cleanup = () => { clearTimeout(timer); options.signal?.removeEventListener('abort', abort); };
45
+ child.once('error', error => { cleanup(); reject(error); });
46
+ child.once('close', code => {
47
+ cleanup();
48
+ if (failure)
49
+ reject(failure);
50
+ else if (code !== 0)
51
+ reject(new Error(`Qoder SDK preparation failed (${code}): ${output}`));
52
+ else
53
+ resolvePromise();
54
+ });
55
+ });
56
+ }
57
+ /** Resolve the npm JavaScript entry so standard Windows npm.cmd never needs shell execution. */
58
+ export async function sdkNpmCli() {
59
+ const configured = process.env.AXOM_WORKSTATION_NPM_PATH;
60
+ const candidates = configured ? [configured] : [join(dirname(process.execPath), process.platform === 'win32' ? 'npm.cmd' : 'npm'),
61
+ ...(process.env.PATH ?? '').split(delimiter).filter(Boolean).map(dir => join(dir, process.platform === 'win32' ? 'npm.cmd' : 'npm'))];
62
+ for (const candidate of candidates) {
63
+ try {
64
+ const actual = await realpath(candidate);
65
+ if (actual.endsWith(`${join('bin', 'npm-cli.js')}`))
66
+ return actual;
67
+ for (const script of [join(dirname(actual), 'node_modules/npm/bin/npm-cli.js'), join(dirname(actual), '../lib/node_modules/npm/bin/npm-cli.js')]) {
68
+ if ((await stat(script).catch(() => undefined))?.isFile())
69
+ return resolve(script);
70
+ }
71
+ }
72
+ catch { /* try the next PATH entry */ }
73
+ }
74
+ throw new Error('Cannot resolve npm CLI. Set AXOM_WORKSTATION_NPM_PATH to the standard npm executable.');
75
+ }
76
+ const preparing = new Map();
77
+ export async function prepareSdkRuntime(options = {}) {
78
+ const recipe = await sdkRecipe(options);
79
+ // Calls with independent cancellation signals keep their own lifecycle; immutable publication
80
+ // also handles separate processes without a cross-process lock.
81
+ if (!options.signal && preparing.has(recipe.root))
82
+ return preparing.get(recipe.root);
83
+ const pending = prepare(options, recipe);
84
+ if (!options.signal)
85
+ preparing.set(recipe.root, pending);
86
+ try {
87
+ return await pending;
88
+ }
89
+ finally {
90
+ if (preparing.get(recipe.root) === pending)
91
+ preparing.delete(recipe.root);
92
+ }
93
+ }
94
+ async function prepare(options, recipe) {
95
+ const budget = options.timeoutMs ?? 300_000;
96
+ if (!Number.isFinite(budget) || budget <= 0)
97
+ throw new Error('SDK preparation timeout must be positive');
98
+ const deadline = Date.now() + budget;
99
+ const commandOptions = (cwd) => ({ cwd, timeoutMs: deadline - Date.now(), signal: options.signal });
100
+ const probe = async (entry, cwd) => {
101
+ const code = `const sdk=await import(${JSON.stringify(pathToFileURL(entry).href)}); (${assertSdkApi.toString()})(sdk);`;
102
+ await runSdkCommand(process.execPath, ['--input-type=module', '-e', code], commandOptions(cwd));
103
+ };
104
+ try {
105
+ const existing = await readPreparedSdkRuntime(options);
106
+ await probe(existing.entry, existing.instance);
107
+ return { instance: existing.instance, alreadyInstalled: true };
108
+ }
109
+ catch { /* Missing/damaged runtime: publish a new instance; leave old instances untouched. */ }
110
+ const installationId = randomUUID();
111
+ const staging = join(recipe.root, 'staging', installationId);
112
+ const instance = join(recipe.root, 'instances', installationId);
113
+ const pointerTemp = join(recipe.root, `current-${installationId}.json`);
114
+ await mkdir(staging, { recursive: true, mode: 0o700 });
115
+ try {
116
+ await writeFile(join(staging, 'package.json'), recipe.manifest);
117
+ await writeFile(join(staging, 'package-lock.json'), recipe.lock);
118
+ if (options.install)
119
+ await options.install(staging, commandOptions(staging));
120
+ else
121
+ await runSdkCommand(process.execPath, [await sdkNpmCli(), 'ci', '--ignore-scripts', '--omit=dev', '--include=optional',
122
+ '--no-audit', '--no-fund'], commandOptions(staging));
123
+ const validated = await validateSdkInstance(staging, recipe);
124
+ await probe(validated.entry, staging);
125
+ if (options.signal?.aborted)
126
+ throw new Error('Qoder SDK preparation cancelled');
127
+ if (Date.now() >= deadline)
128
+ throw new Error('Qoder SDK preparation timed out');
129
+ await writeFile(join(staging, 'receipt.json'), JSON.stringify({ schema: 1, digest: recipe.digest,
130
+ version: QODER_SDK_VERSION, installationId, lockSha256: sha256(recipe.lock), entrySha256: validated.entrySha256 }));
131
+ await mkdir(dirname(instance), { recursive: true, mode: 0o700 });
132
+ await rename(staging, instance);
133
+ await writeFile(pointerTemp, JSON.stringify({ digest: recipe.digest, installationId }), { flag: 'wx', mode: 0o600 });
134
+ if (options.signal?.aborted)
135
+ throw new Error('Qoder SDK preparation cancelled');
136
+ if (Date.now() >= deadline)
137
+ throw new Error('Qoder SDK preparation timed out');
138
+ await rename(pointerTemp, join(recipe.root, 'current.json'));
139
+ return { instance, alreadyInstalled: false };
140
+ }
141
+ finally {
142
+ await rm(staging, { recursive: true, force: true });
143
+ await rm(pointerTemp, { force: true });
144
+ }
145
+ }
@@ -46,7 +46,7 @@ export class QoderAdapterSession {
46
46
  offset: page?.offset ?? 0, limit: page?.limit ?? 50, hasMore: false, snapshotStable: false,
47
47
  reason: 'invalid-page' };
48
48
  }
49
- return readChildHistory(root, agentId, { ...page, dir });
49
+ return readChildHistory(root, agentId, { ...page, dir }, this.dependencies.historyReader);
50
50
  }
51
51
  async publishProfile() {
52
52
  const value = this.currentProfile;
@@ -1,5 +1,8 @@
1
1
  const object = (v) => v && typeof v === 'object' && !Array.isArray(v) ? v : {};
2
2
  const id = (v) => typeof v === 'string' && v.length > 0 && v.length <= 256 && !/[\x00-\x1f\x7f]/.test(v);
3
+ export function newSubworkData() {
4
+ return { calls: {}, count: 0 };
5
+ }
3
6
  export function supportsSubworkMeta(info) {
4
7
  return (info?.name === 'qoder-cli' && info.version === '1.1.61') ||
5
8
  (info?.name === 'axom-qoder-sdk' && info.version === '1.0.50');
@@ -18,10 +21,12 @@ function resultText(value) {
18
21
  }
19
22
  export class SubworkBoundary {
20
23
  root;
21
- calls = new Map();
22
- constructor(root) {
24
+ data;
25
+ constructor(root, data = newSubworkData()) {
23
26
  this.root = root;
27
+ this.data = data;
24
28
  }
29
+ get(toolId) { return Object.hasOwn(this.data.calls, toolId) ? this.data.calls[toolId] : undefined; }
25
30
  child(toolId) { return `qoder-agent:${this.root}:${toolId}`; }
26
31
  map(event) {
27
32
  if (event.acpSessionId && event.acpSessionId !== this.root)
@@ -29,16 +34,17 @@ export class SubworkBoundary {
29
34
  const p = event.payload, q = object(object(p._meta).qoder), input = object(p.rawInput);
30
35
  const marked = q.version === 1 && ['native-acp-patch', 'sdk', 'correction'].includes(String(q.source));
31
36
  const toolId = p.toolCallId;
32
- let call = id(toolId) ? this.calls.get(toolId) : undefined;
37
+ let call = id(toolId) ? this.get(toolId) : undefined;
33
38
  const parent = marked && q.route !== 'unknown' && id(q.parentToolUseId) ? q.parentToolUseId : undefined;
34
39
  const events = [];
35
40
  if (!call && marked && p.kind === 'think' && q.toolName === 'Agent' && id(toolId) &&
36
41
  typeof input.prompt === 'string' && (input.subagent_type == null || typeof input.subagent_type === 'string') &&
37
42
  (input.run_in_background === undefined || typeof input.run_in_background === 'boolean') &&
38
- ['tool_call', 'tool_call_update'].includes(event.eventName) && this.calls.size < 4096) {
43
+ ['tool_call', 'tool_call_update'].includes(event.eventName) && this.data.count < 4096) {
39
44
  call = { child: this.child(toolId), ...(parent ? { parent: this.child(parent) } : {}),
40
45
  background: input.run_in_background === true, ended: false, streamedText: '' };
41
- this.calls.set(toolId, call);
46
+ Object.defineProperty(this.data.calls, toolId, { value: call, enumerable: true, writable: true, configurable: true });
47
+ this.data.count += 1;
42
48
  events.push({ eventName: 'subagent_spawned', payload: { subagentSessionId: call.child,
43
49
  name: typeof input.description === 'string' && input.description.trim() ? input.description : input.subagent_type ?? 'Agent',
44
50
  task: input.prompt, capabilities: {} }, ...(call.parent ? { acpSessionId: call.parent } : {}) });
@@ -60,14 +66,16 @@ export class SubworkBoundary {
60
66
  events.push({ eventName: 'subagent_state_update', payload: { subagentSessionId: call.child, state: terminal },
61
67
  ...(call.parent ? { acpSessionId: call.parent } : {}) });
62
68
  call.ended = true;
69
+ // Read only by the check above, once: an ended call keeps no stream (checkpointed state).
70
+ call.streamedText = '';
63
71
  }
64
72
  // The Agent invocation becomes a dispatch/result in subwork. Keeping the vendor tool here
65
73
  // would also create the old delegation card for the very same task.
66
74
  return events;
67
75
  }
68
76
  if (parent) {
69
- const owner = this.calls.get(parent), content = object(p.content);
70
- if (owner && event.eventName === 'agent_message_chunk' && content.type === 'text' && typeof content.text === 'string') {
77
+ const owner = this.get(parent), content = object(p.content);
78
+ if (owner && !owner.ended && event.eventName === 'agent_message_chunk' && content.type === 'text' && typeof content.text === 'string') {
71
79
  owner.streamedText += content.text;
72
80
  }
73
81
  return [{ ...event, acpSessionId: this.child(parent) }];
@@ -100,6 +100,97 @@ async function defaultDarwinMemoryPressure() {
100
100
  return null;
101
101
  }
102
102
  }
103
+ /**
104
+ * Parses `ioreg -r -d 1 -w 0 -c AGXAccelerator` output. Apple Silicon publishes live GPU
105
+ * utilization (`Device Utilization %`, a driver-side running average) and GPU-resident unified
106
+ * memory (`In use system memory`) in each accelerator's `PerformanceStatistics`, without any
107
+ * privileges. Intel Macs expose no `AGXAccelerator` class, so they parse to null.
108
+ */
109
+ export function parseAppleAgxStats(output) {
110
+ const gpus = output.split('+-o ').slice(1).map((block, index) => {
111
+ const stats = /"PerformanceStatistics"\s*=\s*\{([^}]*)\}/.exec(block)?.[1];
112
+ if (stats === undefined)
113
+ return null;
114
+ const utilizationPercent = Number(/"Device Utilization %"\s*=\s*(\d+(?:\.\d+)?)/.exec(stats)?.[1]);
115
+ const memoryUsedBytes = Number(/"In use system memory"\s*=\s*(\d+)/.exec(stats)?.[1]);
116
+ return {
117
+ id: `apple:agx-${index}`,
118
+ name: /"model"\s*=\s*"([^"]+)"/.exec(block)?.[1] ?? 'Apple GPU',
119
+ utilization: Number.isFinite(utilizationPercent)
120
+ ? Math.max(0, Math.min(1, utilizationPercent / 100))
121
+ : null,
122
+ memoryTotalBytes: null,
123
+ memoryUsedBytes: Number.isFinite(memoryUsedBytes) && memoryUsedBytes >= 0 ? memoryUsedBytes : null,
124
+ coreCount: (() => {
125
+ const cores = Number(/"gpu-core-count"\s*=\s*(\d+)/.exec(block)?.[1]);
126
+ return Number.isFinite(cores) && cores > 0 ? cores : null;
127
+ })(),
128
+ };
129
+ }).filter((gpu) => gpu !== null);
130
+ return gpus.length > 0 ? gpus : null;
131
+ }
132
+ async function appleAgxGpuProbe() {
133
+ try {
134
+ const { stdout } = await execFileAsync('/usr/sbin/ioreg', ['-r', '-d', '1', '-w', '0', '-c', 'AGXAccelerator'], {
135
+ timeout: 2_000,
136
+ maxBuffer: 4 * 1024 * 1024,
137
+ });
138
+ return parseAppleAgxStats(stdout);
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ /** Parses `nvidia-smi --query-gpu=... --format=csv,noheader,nounits` output; `[N/A]` reads as null. */
145
+ export function parseNvidiaSmiGpus(output) {
146
+ const mib = (field) => {
147
+ if (field === undefined)
148
+ return null;
149
+ const value = Number(field);
150
+ return Number.isFinite(value) && value >= 0 ? Math.round(value * 1024 * 1024) : null;
151
+ };
152
+ const gpus = output.split(/\r?\n/).map((line) => {
153
+ const fields = line.split(',').map((field) => field.trim());
154
+ if (fields.length < 6)
155
+ return null;
156
+ const [, uuid, name, utilizationField, totalField, usedField] = fields;
157
+ if (!/^GPU-/.test(uuid) || name.length === 0)
158
+ return null;
159
+ const utilizationPercent = Number(utilizationField);
160
+ return {
161
+ id: `nvidia:${uuid}`,
162
+ name,
163
+ utilization: Number.isFinite(utilizationPercent)
164
+ ? Math.max(0, Math.min(1, utilizationPercent / 100))
165
+ : null,
166
+ memoryTotalBytes: mib(totalField),
167
+ memoryUsedBytes: mib(usedField),
168
+ coreCount: null,
169
+ };
170
+ }).filter((gpu) => gpu !== null);
171
+ return gpus.length > 0 ? gpus : null;
172
+ }
173
+ async function nvidiaSmiGpuProbe() {
174
+ try {
175
+ const { stdout } = await execFileAsync('nvidia-smi', [
176
+ '--query-gpu=index,uuid,name,utilization.gpu,memory.total,memory.used',
177
+ '--format=csv,noheader,nounits',
178
+ ], { timeout: 3_000 });
179
+ return parseNvidiaSmiGpus(stdout);
180
+ }
181
+ catch {
182
+ // Absent binary, driver hiccup, or unsupported GPU -- all read as "no reading", never as failure.
183
+ return null;
184
+ }
185
+ }
186
+ async function defaultProbeGpus(platform) {
187
+ const [apple, nvidia] = await Promise.all([
188
+ platform === 'darwin' ? appleAgxGpuProbe() : Promise.resolve(null),
189
+ nvidiaSmiGpuProbe(),
190
+ ]);
191
+ const gpus = [...(apple ?? []), ...(nvidia ?? [])];
192
+ return gpus.length > 0 ? gpus : null;
193
+ }
103
194
  /**
104
195
  * Low-overhead host sampler. CPU and memory are read only while a heartbeat is being built; storage
105
196
  * topology is discovered out of band and capacity reads use the cached representative paths.
@@ -114,6 +205,8 @@ export class SystemMetricsSampler {
114
205
  darwinMemoryRefreshInFlight = null;
115
206
  darwinMemoryAvailable = null;
116
207
  darwinMemoryPressure = null;
208
+ gpuRefreshInFlight = null;
209
+ gpus = null;
117
210
  constructor(workspaces, deps = {
118
211
  platform: process.platform,
119
212
  cpus: os.cpus,
@@ -126,12 +219,13 @@ export class SystemMetricsSampler {
126
219
  identifyStorage: (cwd, device) => defaultIdentifyStorage(process.platform, cwd, device),
127
220
  darwinAvailableMemory: defaultDarwinAvailableMemory,
128
221
  darwinMemoryPressure: defaultDarwinMemoryPressure,
222
+ probeGpus: () => defaultProbeGpus(process.platform),
129
223
  }) {
130
224
  this.workspaces = workspaces;
131
225
  this.deps = deps;
132
226
  }
133
227
  async start() {
134
- await Promise.all([this.refreshTopology(), this.refreshDarwinMemory()]);
228
+ await Promise.all([this.refreshTopology(), this.refreshDarwinMemory(), this.refreshGpus()]);
135
229
  this.refreshTimer = setInterval(() => this.refreshTopologyInBackground(), STORAGE_TOPOLOGY_REFRESH_MS);
136
230
  this.refreshTimer.unref();
137
231
  }
@@ -143,6 +237,7 @@ export class SystemMetricsSampler {
143
237
  snapshot() {
144
238
  try {
145
239
  this.refreshDarwinMemoryInBackground();
240
+ this.refreshGpusInBackground();
146
241
  const cpus = this.deps.cpus();
147
242
  const currentCpu = cpuTotals(cpus);
148
243
  const previousCpu = this.previousCpu;
@@ -192,6 +287,7 @@ export class SystemMetricsSampler {
192
287
  memoryTotalBytes,
193
288
  memoryAvailableBytes,
194
289
  storagePools: [...pools.values()],
290
+ gpus: this.gpus,
195
291
  };
196
292
  }
197
293
  catch {
@@ -218,6 +314,19 @@ export class SystemMetricsSampler {
218
314
  this.darwinMemoryRefreshInFlight = null;
219
315
  });
220
316
  }
317
+ refreshGpusInBackground() {
318
+ if (this.gpuRefreshInFlight)
319
+ return;
320
+ const refresh = this.refreshGpus();
321
+ this.gpuRefreshInFlight = refresh;
322
+ void refresh.finally(() => {
323
+ if (this.gpuRefreshInFlight === refresh)
324
+ this.gpuRefreshInFlight = null;
325
+ });
326
+ }
327
+ async refreshGpus() {
328
+ this.gpus = await this.deps.probeGpus().catch(() => null);
329
+ }
221
330
  async refreshDarwinMemory() {
222
331
  if (this.deps.platform !== 'darwin')
223
332
  return;
@@ -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
+ }