@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
@@ -1,330 +1 @@
1
- // Persistent PTY sessions (tmux-like): each session keeps a headless xterm screen and a bounded ring of rendered output;
2
- // clients `attach` (snapshot) and `poll(after)` (delta, or a snapshot reset when they fell behind). Control is explicit
3
- // (controller / takeover / detach), at most 8 sessions, exited sessions stay listed until closed. Spec §4.1.
4
- //
5
- // This is the one implementation: keeper-terminal serves it as `pty.*` (main.ts); the daemon's `terminal.*` reaches it through
6
- // tools/terminalRouter.ts.
7
- //
8
- // Keeper-side module: node builtins, kit siblings and the bundled @xterm packages only (the import guard enforces it).
9
- // Sessions are independent: there is no queue across sessions (a slow operation on one never delays another).
10
- import { execFile } from 'node:child_process';
11
- import { createRequire } from 'node:module';
12
- import path from 'node:path';
13
- import xtermHeadless from '@xterm/headless';
14
- import xtermSerialize from '@xterm/addon-serialize';
15
- import { KeeperMethodError } from '../kit/protocol.js';
16
- import { PTY_HISTORY_BYTES, PTY_INPUT_BYTES, PTY_LABEL_CHARS, PTY_MAX_POLL_BYTES, PTY_RESULT_BYTES, PTY_SCROLLBACK, PTY_SESSION_LIMIT, PTY_UNAVAILABLE_ERROR, PtyErrorCode, } from './protocol.js';
17
- // CommonJS packages: their default export is `module.exports` both under Node and once esbuild bundles them.
18
- const { Terminal } = xtermHeadless;
19
- const { SerializeAddon } = xtermSerialize;
20
- const darwinShellName = (pid) => new Promise((resolve) => {
21
- execFile('ps', ['-o', 'ucomm=', '-p', String(pid)], { timeout: 1000 }, (error, stdout) => resolve(error ? null : stdout.trim() || null));
22
- });
23
- const defaultShellProcessName = (pid) => (process.platform === 'darwin' ? darwinShellName(pid) : Promise.resolve(null));
24
- // Parameter checks (today's tools/validation.ts rules; kept here so the keeper bundle depends on no daemon module).
25
- const bad = (message) => { throw new KeeperMethodError(PtyErrorCode.badRequest, message); };
26
- function keys(params, allowed) {
27
- if (Object.keys(params).some((key) => !allowed.includes(key)))
28
- bad('Unexpected tool parameter');
29
- }
30
- function identifier(params, name) {
31
- const value = params[name];
32
- if (typeof value !== 'string' || !value.length || value.includes('\0') || value.length > 128 || !/^[A-Za-z0-9_.:-]+$/.test(value))
33
- bad(`Invalid ${name}`);
34
- return value;
35
- }
36
- function integer(params, name, min, max, fallback) {
37
- const value = params[name] === undefined ? fallback : params[name];
38
- if (!Number.isSafeInteger(value) || value < min || value > max)
39
- bad(`Invalid ${name}`);
40
- return value;
41
- }
42
- /** The basename of a foreground process name, without the leading '-' a login shell may carry. */
43
- export const processBasename = (name) => path.basename(name.trim()).replace(/^-/, '');
44
- export const CREATE_KEYS = ['id', 'clientId', 'name', 'cwd', 'cols', 'rows'];
45
- export const PTY_OPERATIONS = new Set(['attach', 'poll', 'write', 'resize', 'takeover', 'detach', 'close']);
46
- export class PtySessions {
47
- options;
48
- sessions = new Map();
49
- ptyModule;
50
- error;
51
- now;
52
- constructor(options = {}) {
53
- this.options = options;
54
- this.now = options.now ?? Date.now;
55
- try {
56
- this.ptyModule = (options.loadPty ?? (() => createRequire(import.meta.url)('node-pty')))();
57
- }
58
- catch {
59
- this.error = PTY_UNAVAILABLE_ERROR;
60
- }
61
- }
62
- get available() { return !!this.ptyModule; }
63
- descriptor(s) {
64
- return { id: s.id, name: s.name, cwd: s.cwd, cols: s.cols, rows: s.rows, exited: s.exited, exitCode: s.exitCode, controllerId: s.controllerId };
65
- }
66
- control(s, clientId) {
67
- if (s.controllerId !== clientId)
68
- throw new KeeperMethodError(PtyErrorCode.notController, 'Terminal is controlled by another client; take control explicitly');
69
- }
70
- resize(s, p) {
71
- const cols = integer(p, 'cols', 20, 300, s.cols);
72
- const rows = integer(p, 'rows', 5, 100, s.rows);
73
- if (cols !== s.cols || rows !== s.rows) {
74
- if (!s.exited)
75
- s.pty.resize(cols, rows);
76
- s.terminal.resize(cols, rows);
77
- s.cols = cols;
78
- s.rows = rows;
79
- }
80
- }
81
- snapshot(s) {
82
- // Scroll position belongs to the viewer, never the keeper. Serialization includes normal and
83
- // alternate buffers, SGR attributes, cursor and modes; it starts from a clean emulator.
84
- for (let scrollback = PTY_SCROLLBACK;; scrollback = Math.floor(scrollback / 2)) {
85
- const snapshot = '\x1bc' + s.serializer.serialize({ scrollback });
86
- if (Buffer.byteLength(JSON.stringify({ snapshot })) < PTY_RESULT_BYTES - 8192)
87
- return snapshot;
88
- if (!scrollback)
89
- throw new KeeperMethodError(PtyErrorCode.snapshotTooLarge, 'Terminal screen exceeds snapshot limit; resize to a smaller viewport');
90
- }
91
- }
92
- output(s, data) {
93
- if (!data || s.closed)
94
- return;
95
- const bytes = Buffer.byteLength(data);
96
- s.pendingBytes += bytes;
97
- if (s.pendingBytes > 256 * 1024)
98
- s.pty.pause();
99
- // Written to xterm at once, not chained behind the previous write's callback: xterm parses writes and runs their
100
- // callbacks strictly in order, so sequence numbers stay per event and `rendered` (the newest write's promise) still
101
- // means "everything received so far is rendered". Chaining cost one timer tick per event, and node-pty on macOS
102
- // delivers line-by-line output as one ~10-byte event per line (~9 KB/s rendered, a paused shell behind it).
103
- s.rendered = new Promise((resolve) => {
104
- s.terminal.write(data, () => {
105
- // Each PTY event is a sequence boundary. A single large event can exceed the history budget;
106
- // dropping it is safe because poll then returns a fully rendered snapshot.
107
- s.seq += 1;
108
- s.ring.push({ seq: s.seq, data, bytes });
109
- s.ringBytes += bytes;
110
- const limit = this.options.historyBytes ?? PTY_HISTORY_BYTES;
111
- while (s.ringBytes > limit && s.ring.length)
112
- s.ringBytes -= s.ring.shift().bytes;
113
- s.pendingBytes -= bytes;
114
- if (s.pendingBytes < 64 * 1024 && !s.exited)
115
- s.pty.resume();
116
- resolve();
117
- });
118
- });
119
- }
120
- unavailable() { throw new KeeperMethodError(PtyErrorCode.unavailable, this.error ?? 'Terminal unavailable'); }
121
- list(p) {
122
- keys(p, []);
123
- return { sessions: [...this.sessions.values()].map((s) => this.descriptor(s)) };
124
- }
125
- /**
126
- * Creates session `id`, or returns the existing one (idempotent). `allowedKeys` is the caller's parameter set; `launch`
127
- * supplies cwd, shell and env and runs only when a session is really started (after the duplicate and limit checks,
128
- * which are re-checked afterwards because `launch` may be asynchronous).
129
- */
130
- async create(p, allowedKeys, launch) {
131
- if (!this.ptyModule)
132
- this.unavailable();
133
- const id = identifier(p, 'id');
134
- const clientId = identifier(p, 'clientId');
135
- keys(p, allowedKeys);
136
- const cols = integer(p, 'cols', 20, 300, 100);
137
- const rows = integer(p, 'rows', 5, 100, 30);
138
- if (p.name !== undefined && (typeof p.name !== 'string' || !p.name.length || p.name.length > 128 || /[\x00-\x1f]/.test(p.name)))
139
- bad('Invalid terminal name');
140
- const existing = () => { const old = this.sessions.get(id); return old && this.descriptor(old); };
141
- const limit = () => { if (this.sessions.size >= PTY_SESSION_LIMIT)
142
- throw new KeeperMethodError(PtyErrorCode.limit, 'Terminal session limit exceeded'); };
143
- let old = existing();
144
- if (old)
145
- return old;
146
- limit();
147
- const { cwd, shell, env } = await launch();
148
- old = existing();
149
- if (old)
150
- return old; // a concurrent create of the same id won
151
- limit();
152
- const terminal = new Terminal({ cols, rows, scrollback: PTY_SCROLLBACK, allowProposedApi: true });
153
- const serializer = new SerializeAddon();
154
- // Headless and browser xterm share the addon contract, but their declaration packages differ.
155
- terminal.loadAddon(serializer);
156
- let pty;
157
- try {
158
- pty = this.ptyModule.spawn(shell, ['-l'], { name: 'xterm-256color', cwd, cols, rows, env: { ...env, TERM: 'xterm-256color', COLORTERM: 'truecolor' } });
159
- }
160
- catch (error) {
161
- terminal.dispose();
162
- throw error;
163
- }
164
- const created = this.now();
165
- const shellName = processBasename(shell);
166
- const session = {
167
- id, name: p.name ?? 'Terminal', cwd, cols, rows, exited: false, exitCode: null, controllerId: clientId,
168
- pty, closed: false, terminal, serializer, seq: 0, ring: [], ringBytes: 0, pendingBytes: 0, rendered: Promise.resolve(),
169
- shellName, exitedAt: 0, shellNames: new Set([shellName]), observed: { label: shellName, state: 'idle', since: created },
170
- };
171
- pty.onData((data) => this.output(session, data));
172
- pty.onExit((event) => { session.exited = true; session.exitCode = event.exitCode; session.exitedAt = this.now(); });
173
- // Answering terminal queries (DA/DSR, etc.) is keeper-owned and does not require a web client.
174
- terminal.onData((data) => { if (!session.exited)
175
- pty.write(data); });
176
- this.sessions.set(id, session);
177
- const learn = setTimeout(() => { void this.learnShellName(session); }, this.options.shellNameDelayMs ?? 250);
178
- learn.unref();
179
- return this.descriptor(session);
180
- }
181
- /** attach / poll / write / resize / takeover / detach / close on an existing session. */
182
- async operate(op, p) {
183
- if (!this.ptyModule)
184
- this.unavailable();
185
- const id = identifier(p, 'id');
186
- const clientId = identifier(p, 'clientId');
187
- const s = this.sessions.get(id);
188
- if (!s)
189
- throw new KeeperMethodError(PtyErrorCode.notFound, 'Terminal session not found; keeper restart loses live PTYs');
190
- switch (op) {
191
- case 'attach': {
192
- keys(p, ['id', 'clientId', 'cols', 'rows']);
193
- // Validate dimensions even for a viewer, but only the controller's dimensions affect PTY.
194
- integer(p, 'cols', 20, 300, s.cols);
195
- integer(p, 'rows', 5, 100, s.rows);
196
- await s.rendered;
197
- if (s.controllerId === null)
198
- s.controllerId = clientId;
199
- if (s.controllerId === clientId)
200
- this.resize(s, p);
201
- return { session: this.descriptor(s), seq: s.seq, snapshot: this.snapshot(s), control: s.controllerId === clientId };
202
- }
203
- case 'poll': {
204
- keys(p, ['id', 'clientId', 'after']);
205
- const after = integer(p, 'after', 0, Number.MAX_SAFE_INTEGER);
206
- await s.rendered;
207
- const common = { exited: s.exited, exitCode: s.exitCode, control: s.controllerId === clientId };
208
- if (after > s.seq || after < (s.ring[0]?.seq ?? s.seq + 1) - 1)
209
- return { ...common, seq: s.seq, data: '', reset: true, snapshot: this.snapshot(s) };
210
- let data = '';
211
- let bytes = 0;
212
- let seq = after;
213
- for (const output of s.ring) {
214
- if (output.seq <= after)
215
- continue;
216
- if (bytes + output.bytes > PTY_MAX_POLL_BYTES) {
217
- if (!bytes)
218
- return { ...common, seq: s.seq, data: '', reset: true, snapshot: this.snapshot(s) };
219
- break;
220
- }
221
- data += output.data;
222
- bytes += output.bytes;
223
- seq = output.seq;
224
- }
225
- return { ...common, seq, data, reset: false };
226
- }
227
- case 'takeover':
228
- keys(p, ['id', 'clientId']);
229
- s.controllerId = clientId;
230
- return { ok: true };
231
- case 'detach':
232
- keys(p, ['id', 'clientId']);
233
- if (s.controllerId === clientId)
234
- s.controllerId = null;
235
- return { ok: true };
236
- case 'write':
237
- keys(p, ['id', 'clientId', 'data']);
238
- this.control(s, clientId);
239
- if (s.exited)
240
- throw new KeeperMethodError(PtyErrorCode.exited, 'Terminal has exited');
241
- if (typeof p.data !== 'string' || Buffer.byteLength(p.data) > PTY_INPUT_BYTES)
242
- bad('Invalid terminal input');
243
- s.pty.write(p.data);
244
- return { ok: true };
245
- case 'resize':
246
- keys(p, ['id', 'clientId', 'cols', 'rows']);
247
- this.control(s, clientId);
248
- integer(p, 'cols', 20, 300);
249
- integer(p, 'rows', 5, 100);
250
- await s.rendered;
251
- this.resize(s, p);
252
- return { ok: true };
253
- case 'close':
254
- keys(p, ['id', 'clientId']);
255
- this.control(s, clientId);
256
- this.sessions.delete(id);
257
- s.closed = true;
258
- if (!s.exited)
259
- s.pty.kill();
260
- await s.rendered;
261
- s.terminal.dispose();
262
- return { ok: true };
263
- default: return bad('Unsupported terminal tool');
264
- }
265
- }
266
- /** The tty's foreground process basename, '' when it cannot be read (node-pty falls back to the shell path itself). */
267
- foreground(s) {
268
- try {
269
- return processBasename(String(s.pty.process ?? ''));
270
- }
271
- catch {
272
- return '';
273
- }
274
- }
275
- /**
276
- * Learns once per session, in the background shortly after spawn, what the tty calls the shell itself (macOS `p_comm`).
277
- * Until it resolves (or when it fails) the configured basename is the only shell name: activity never waits on a subprocess.
278
- */
279
- async learnShellName(s) {
280
- if (s.closed || s.exited)
281
- return;
282
- try {
283
- const name = await (this.options.shellProcessName ?? defaultShellProcessName)(s.pty.pid);
284
- if (name)
285
- s.shellNames.add(processBasename(name));
286
- }
287
- catch { /* keep the configured basename */ }
288
- }
289
- /**
290
- * A live sample for `keeper.activity` (spec §3.5/§4.1): `ptyOpen` live sessions, `ptyActive` live sessions whose foreground
291
- * process is not their shell, `ptyExited` exited sessions still listed. One item per session, labelled with the foreground
292
- * process basename (never its arguments), `sinceEpochMs` = when this label/state was first observed.
293
- */
294
- activity() {
295
- const now = this.now();
296
- let open = 0;
297
- let active = 0;
298
- let exited = 0;
299
- const items = [];
300
- for (const s of this.sessions.values()) {
301
- if (s.exited) {
302
- exited += 1;
303
- items.push({ type: 'pty', id: s.id, label: s.shellName.slice(0, PTY_LABEL_CHARS), state: 'exited', sinceEpochMs: s.exitedAt });
304
- continue;
305
- }
306
- open += 1;
307
- const fg = this.foreground(s);
308
- const busy = fg !== '' && !s.shellNames.has(fg);
309
- const label = (busy ? fg : s.shellName).slice(0, PTY_LABEL_CHARS);
310
- const state = busy ? 'active' : 'idle';
311
- if (s.observed.label !== label || s.observed.state !== state)
312
- s.observed = { label, state, since: now };
313
- if (busy)
314
- active += 1;
315
- items.push({ type: 'pty', id: s.id, label, state, sinceEpochMs: s.observed.since });
316
- }
317
- return { metrics: { ptyOpen: open, ptyActive: active, ptyExited: exited }, items };
318
- }
319
- /** Kills every session (the keeper is stopping). */
320
- async dispose() {
321
- for (const session of this.sessions.values()) {
322
- session.closed = true;
323
- if (!session.exited)
324
- session.pty.kill();
325
- await session.rendered;
326
- session.terminal.dispose();
327
- }
328
- this.sessions.clear();
329
- }
330
- }
1
+ export {};
@@ -0,0 +1,246 @@
1
+ import xtermHeadless from '@xterm/headless';
2
+ import { TERMINAL_GRID_LIMITS, TERMINAL_RGB_COLOR_BASE, TERMINAL_STYLE_FLAGS, } from './gridProtocol.js';
3
+ const { Terminal } = xtermHeadless;
4
+ const mouseEncodings = { 1005: 'utf8', 1006: 'sgr', 1015: 'urxvt', 1016: 'sgr-pixels' };
5
+ /** Keeper-owned emulator and its single shared, native xterm viewport. */
6
+ export class TerminalScreen {
7
+ terminal;
8
+ settled = Promise.resolve();
9
+ resizeBarrier;
10
+ pending = new Set();
11
+ closed = false;
12
+ normalRestoreTop;
13
+ alternateScroll = false;
14
+ mouseEncoding = 'default';
15
+ constructor(cols, rows) {
16
+ this.terminal = new Terminal({ cols, rows, scrollback: TERMINAL_GRID_LIMITS.scrollbackLines, allowProposedApi: true });
17
+ // Observe only the input modes absent from xterm's public API. Native parsing
18
+ // still owns all escape processing; these handlers fall through to xterm.
19
+ for (const final of ['h', 'l'])
20
+ this.terminal.parser.registerCsiHandler({ prefix: '?', final }, params => {
21
+ for (const param of params) {
22
+ if (param === 1007)
23
+ this.alternateScroll = final === 'h';
24
+ const encoding = typeof param === 'number' ? mouseEncodings[param] : undefined;
25
+ if (encoding)
26
+ this.mouseEncoding = final === 'h' ? encoding : 'default';
27
+ }
28
+ return false;
29
+ });
30
+ this.terminal.parser.registerEscHandler({ final: 'c' }, () => {
31
+ this.alternateScroll = false;
32
+ this.mouseEncoding = 'default';
33
+ return false;
34
+ });
35
+ this.terminal.buffer.onBufferChange(buffer => {
36
+ // xterm's isUserScrolling flag is shared by both buffers. Alternate-buffer
37
+ // scrolling must not change whether normal-buffer output follows the cursor.
38
+ const top = buffer.type === 'normal' ? this.normalRestoreTop ?? buffer.viewportY : buffer.viewportY;
39
+ if (buffer.type === 'normal')
40
+ this.normalRestoreTop = undefined;
41
+ this.terminal.scrollToBottom();
42
+ this.terminal.scrollToLine(top);
43
+ });
44
+ }
45
+ write(data) {
46
+ if (this.closed)
47
+ return Promise.reject(new Error('Terminal screen is disposed'));
48
+ const barrier = this.resizeBarrier;
49
+ const operation = new Promise((resolve, reject) => {
50
+ this.pending.add(reject);
51
+ const write = () => {
52
+ if (this.closed)
53
+ return;
54
+ try {
55
+ this.terminal.write(data, () => { this.pending.delete(reject); resolve(); });
56
+ }
57
+ catch (error) {
58
+ this.pending.delete(reject);
59
+ reject(error);
60
+ }
61
+ };
62
+ // Batch ordinary PTY writes directly into xterm; do not add a timer per chunk.
63
+ if (barrier)
64
+ void barrier.then(write, error => { this.pending.delete(reject); reject(error); });
65
+ else
66
+ write();
67
+ });
68
+ this.settled = operation;
69
+ return operation;
70
+ }
71
+ resize(cols, rows) {
72
+ if (this.closed)
73
+ return Promise.reject(new Error('Terminal screen is disposed'));
74
+ const previous = this.settled;
75
+ const operation = previous.then(() => {
76
+ if (this.closed)
77
+ throw new Error('Terminal screen is disposed');
78
+ const normal = this.terminal.buffer.normal;
79
+ const anchor = this.anchor(normal);
80
+ const live = (this.normalRestoreTop ?? normal.viewportY) === normal.baseY;
81
+ this.terminal.resize(cols, rows);
82
+ // The native resize reflows both buffers. Restore a content anchor, never
83
+ // the old numerical line index, which now refers to different content.
84
+ const top = live ? normal.baseY : anchor ? this.anchorLine(anchor, normal) : 0;
85
+ if (this.terminal.buffer.active.type === 'normal') {
86
+ this.terminal.scrollToBottom();
87
+ this.terminal.scrollToLine(top);
88
+ }
89
+ else {
90
+ this.normalRestoreTop = Math.min(top, normal.baseY);
91
+ }
92
+ anchor?.marker.dispose();
93
+ });
94
+ this.resizeBarrier = operation;
95
+ this.settled = operation;
96
+ void operation.then(() => { if (this.resizeBarrier === operation)
97
+ this.resizeBarrier = undefined; }, () => {
98
+ if (this.resizeBarrier === operation)
99
+ this.resizeBarrier = undefined;
100
+ });
101
+ return operation;
102
+ }
103
+ scroll(lines) {
104
+ this.assertOpen();
105
+ this.terminal.scrollLines(lines);
106
+ }
107
+ /** Only settled state may be published; callers await write/resize before capture. */
108
+ capture() {
109
+ this.assertOpen();
110
+ if (this.pending.size || this.resizeBarrier)
111
+ throw new Error('Terminal screen has pending writes or resize');
112
+ const terminal = this.terminal;
113
+ const buffer = terminal.buffer.active;
114
+ const core = terminal._core;
115
+ const styles = [];
116
+ const styleIndices = new Map();
117
+ const changedRows = Array.from({ length: terminal.rows }, (_, index) => {
118
+ const line = buffer.getLine(buffer.viewportY + index);
119
+ const runs = [];
120
+ for (let x = 0; x < terminal.cols; x++) {
121
+ const cell = line.getCell(x);
122
+ const style = cellStyle(cell);
123
+ const key = `${style.fg},${style.bg},${style.flags}`;
124
+ let styleIndex = styleIndices.get(key);
125
+ if (styleIndex === undefined) {
126
+ styleIndex = styles.length;
127
+ styles.push(style);
128
+ styleIndices.set(key, styleIndex);
129
+ }
130
+ let run = runs[runs.length - 1];
131
+ if (!run || run.style !== styleIndex) {
132
+ run = { style: styleIndex, cells: [] };
133
+ runs.push(run);
134
+ }
135
+ // A width-zero continuation still occupies one grid column. Never drop it.
136
+ run.cells.push({ text: cell.getChars(), width: cell.getWidth() });
137
+ }
138
+ return { index, runs, wrapped: line.isWrapped };
139
+ });
140
+ const y = buffer.baseY + buffer.cursorY - buffer.viewportY;
141
+ return {
142
+ cols: terminal.cols, rows: terminal.rows, buffer: buffer.type,
143
+ viewport: { top: buffer.viewportY, totalLines: buffer.length, live: buffer.viewportY === buffer.baseY },
144
+ cursor: {
145
+ x: Math.min(buffer.cursorX, terminal.cols - 1), y,
146
+ visible: !core.coreService.isCursorHidden && y >= 0 && y < terminal.rows,
147
+ shape: core.coreService.decPrivateModes.cursorStyle ?? terminal.options.cursorStyle ?? 'block',
148
+ blink: core.coreService.decPrivateModes.cursorBlink ?? terminal.options.cursorBlink ?? false,
149
+ },
150
+ modes: this.state().modes,
151
+ styles, changedRows,
152
+ };
153
+ }
154
+ /** Cheap parsed state for input routing; never extracts viewport cells. */
155
+ state() {
156
+ this.assertOpen();
157
+ if (this.pending.size || this.resizeBarrier)
158
+ throw new Error('Terminal screen has pending writes or resize');
159
+ const terminal = this.terminal;
160
+ const modes = terminal.modes;
161
+ return {
162
+ buffer: terminal.buffer.active.type,
163
+ modes: {
164
+ applicationCursor: modes.applicationCursorKeysMode, applicationKeypad: modes.applicationKeypadMode,
165
+ bracketedPaste: modes.bracketedPasteMode, alternateScroll: this.alternateScroll, focusReporting: modes.sendFocusMode,
166
+ mouseTracking: modes.mouseTrackingMode,
167
+ mouseEncoding: this.mouseEncoding,
168
+ },
169
+ };
170
+ }
171
+ onTitleChange(listener) { return this.terminal.onTitleChange(listener); }
172
+ onCwdChange(listener) {
173
+ return this.terminal.parser.registerOscHandler(7, uri => { listener(uri); return true; });
174
+ }
175
+ onReply(listener) { return this.terminal.onData(listener); }
176
+ dispose() {
177
+ if (this.closed)
178
+ return;
179
+ this.closed = true;
180
+ for (const reject of this.pending)
181
+ reject(new Error('Terminal screen is disposed'));
182
+ this.pending.clear();
183
+ this.terminal.dispose();
184
+ }
185
+ assertOpen() { if (this.closed)
186
+ throw new Error('Terminal screen is disposed'); }
187
+ anchor(buffer) {
188
+ const top = this.normalRestoreTop ?? buffer.viewportY;
189
+ if (top === buffer.baseY)
190
+ return undefined;
191
+ let end = top + 1;
192
+ while (buffer.getLine(end)?.isWrapped)
193
+ end++;
194
+ let cellsFromEnd = this.trimmedLength(buffer, end - 1);
195
+ for (let line = top; line < end - 1; line++)
196
+ cellsFromEnd += this.wrappedLength(buffer, line);
197
+ // Anchor AFTER the logical line. Its beginning can be evicted during reflow
198
+ // while the segment being read survives. A marker at that beginning would
199
+ // incorrectly lose the surviving segment. The end marker follows native
200
+ // insert/delete/trim events, then we walk backwards over the reflowed cells.
201
+ // Public registerMarker cannot target inactive normal history.
202
+ const marker = this.terminal.buffer.active.type === 'normal'
203
+ ? this.terminal.registerMarker(end - buffer.baseY - buffer.cursorY)
204
+ : this.terminal._core.buffers.normal.addMarker(end);
205
+ return marker ? { marker, cellsFromEnd } : undefined;
206
+ }
207
+ anchorLine(anchor, buffer) {
208
+ if (anchor.marker.isDisposed)
209
+ return 0;
210
+ let line = Math.min(anchor.marker.line, buffer.length) - 1;
211
+ if (line < 0)
212
+ return 0;
213
+ let remaining = anchor.cellsFromEnd - this.trimmedLength(buffer, line);
214
+ while (remaining > 0 && line > 0 && buffer.getLine(line)?.isWrapped) {
215
+ line--;
216
+ remaining -= this.wrappedLength(buffer, line);
217
+ }
218
+ return line;
219
+ }
220
+ trimmedLength(buffer, line) {
221
+ const row = buffer.getLine(line);
222
+ for (let x = this.terminal.cols - 1; x >= 0; x--) {
223
+ const cell = row.getCell(x);
224
+ if (cell.getChars())
225
+ return x + cell.getWidth();
226
+ }
227
+ return 0;
228
+ }
229
+ wrappedLength(buffer, line) {
230
+ const last = buffer.getLine(line).getCell(this.terminal.cols - 1);
231
+ // xterm leaves one empty padding cell when a wide glyph wraps at the edge.
232
+ return this.terminal.cols - (last.getChars() === '' && last.getWidth() === 1
233
+ && buffer.getLine(line + 1)?.getCell(0)?.getWidth() === 2 ? 1 : 0);
234
+ }
235
+ }
236
+ function cellStyle(cell) {
237
+ const flags = TERMINAL_STYLE_FLAGS;
238
+ return {
239
+ fg: cell.isFgDefault() ? -1 : cell.getFgColor() + (cell.isFgRGB() ? TERMINAL_RGB_COLOR_BASE : 0),
240
+ bg: cell.isBgDefault() ? -1 : cell.getBgColor() + (cell.isBgRGB() ? TERMINAL_RGB_COLOR_BASE : 0),
241
+ flags: (cell.isBold() ? flags.bold : 0) | (cell.isDim() ? flags.dim : 0)
242
+ | (cell.isItalic() ? flags.italic : 0) | (cell.isUnderline() ? flags.underline : 0)
243
+ | (cell.isBlink() ? flags.blink : 0) | (cell.isInverse() ? flags.inverse : 0)
244
+ | (cell.isInvisible() ? flags.invisible : 0) | (cell.isStrikethrough() ? flags.strike : 0),
245
+ };
246
+ }