handmux 0.22.1 → 0.24.0

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 (247) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/bin/handmux.js +3 -688
  4. package/dist/bin/handmux.js +850 -0
  5. package/{hooks → dist/hooks}/handmux-notify.sh +2 -4
  6. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  7. package/{hooks → dist/hooks}/handmux-write.cjs +25 -26
  8. package/dist/package.json +126 -0
  9. package/dist/public/assets/index-CicdY9E9.css +32 -0
  10. package/dist/public/assets/index-D4VP5t8h.js +337 -0
  11. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  12. package/{public → dist/public}/index.html +5 -4
  13. package/{src → dist/src}/agents/claude.js +106 -82
  14. package/dist/src/agents/codex.js +217 -0
  15. package/{src → dist/src}/agents/index.js +9 -10
  16. package/dist/src/agents/processIdentity.js +80 -0
  17. package/dist/src/agents/scanUtils.js +262 -0
  18. package/dist/src/apiErrors.js +77 -0
  19. package/{src → dist/src}/appName.js +12 -13
  20. package/dist/src/asr/iflyConfig.js +7 -0
  21. package/dist/src/asr/iflySign.js +14 -0
  22. package/dist/src/auth.js +27 -0
  23. package/dist/src/browser/bootstrap.js +72 -0
  24. package/dist/src/browser/cookieProfiles.js +413 -0
  25. package/dist/src/browser/coordinator.js +51 -0
  26. package/dist/src/browser/hammerheadCompat.js +104 -0
  27. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  28. package/dist/src/browser/manager.js +560 -0
  29. package/dist/src/browser/originLabel.js +15 -0
  30. package/dist/src/browser/profilePersistence.js +235 -0
  31. package/dist/src/browser/publicProxy.js +176 -0
  32. package/dist/src/browser/routes.js +159 -0
  33. package/dist/src/browser/sessionStore.js +72 -0
  34. package/dist/src/browser/siteVersion.js +121 -0
  35. package/dist/src/browser/targetPolicy.js +163 -0
  36. package/{src → dist/src}/browser/worker.js +14 -18
  37. package/dist/src/browser/workerClient.js +286 -0
  38. package/dist/src/browser/workerServer.js +91 -0
  39. package/dist/src/captureBackground.js +95 -0
  40. package/dist/src/claudeEvents.js +499 -0
  41. package/{src → dist/src}/cli/claudeHooks.js +146 -108
  42. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  43. package/dist/src/cli/cloudflared.js +106 -0
  44. package/dist/src/cli/codexManaged.js +77 -0
  45. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  46. package/dist/src/cli/drivers.js +98 -0
  47. package/dist/src/cli/hookScaffold.js +36 -0
  48. package/dist/src/cli/i18n/en.js +366 -0
  49. package/{src → dist/src}/cli/i18n/index.js +27 -25
  50. package/dist/src/cli/i18n/zh.js +365 -0
  51. package/dist/src/cli/legacyCodexHooks.js +52 -0
  52. package/{src → dist/src}/cli/natappUrl.js +2 -3
  53. package/dist/src/cli/options.js +245 -0
  54. package/dist/src/cli/probe.js +16 -0
  55. package/{src → dist/src}/cli/prompt.js +4 -6
  56. package/dist/src/cli/pushCmd.js +102 -0
  57. package/{src → dist/src}/cli/qr.js +19 -19
  58. package/dist/src/cli/service.js +144 -0
  59. package/dist/src/cli/setupModel.js +194 -0
  60. package/dist/src/cli/setupWizard.js +647 -0
  61. package/dist/src/cli/shortcutEditor.js +351 -0
  62. package/dist/src/cli/sshTunnel.js +18 -0
  63. package/dist/src/cli/state.js +114 -0
  64. package/{src → dist/src}/cli/statusLine.js +63 -46
  65. package/dist/src/cli/supervisor.js +278 -0
  66. package/dist/src/cli/supervisorHealth.js +21 -0
  67. package/dist/src/cli/supervisorLaunch.js +148 -0
  68. package/dist/src/cli/supervisorProcesses.js +62 -0
  69. package/dist/src/cli/supervisorState.js +62 -0
  70. package/dist/src/cli/tmuxVersion.js +53 -0
  71. package/{src → dist/src}/cli/tunlite.js +7 -10
  72. package/dist/src/cli/tunnelClients.js +72 -0
  73. package/{src → dist/src}/cli/updateCheck.js +98 -69
  74. package/{src → dist/src}/cli/urlHost.js +8 -4
  75. package/dist/src/cli/workspaceCmd.js +347 -0
  76. package/dist/src/codexAppServer.js +2299 -0
  77. package/dist/src/codexConversationProjection.js +199 -0
  78. package/dist/src/codexDiff.js +118 -0
  79. package/dist/src/codexMessageIdentity.js +18 -0
  80. package/dist/src/codexPlan.js +30 -0
  81. package/dist/src/codexQueueProtocol.js +96 -0
  82. package/dist/src/codexStreamProtocol.js +207 -0
  83. package/dist/src/codexToolProtocol.js +68 -0
  84. package/dist/src/codexTranscriptParse.js +724 -0
  85. package/dist/src/codexUsageSnapshot.js +205 -0
  86. package/dist/src/config.js +8 -0
  87. package/{src → dist/src}/docPath.js +22 -24
  88. package/dist/src/docs.js +337 -0
  89. package/dist/src/git.js +272 -0
  90. package/dist/src/healthProtocol.js +97 -0
  91. package/dist/src/httpApi.js +68 -0
  92. package/dist/src/jsonStore.js +15 -0
  93. package/{src → dist/src}/keyNames.js +3 -3
  94. package/dist/src/notifications.js +111 -0
  95. package/dist/src/orphans.js +157 -0
  96. package/dist/src/pendingPrompt.js +194 -0
  97. package/dist/src/previewServer.js +202 -0
  98. package/dist/src/previews.js +179 -0
  99. package/dist/src/privateStateStore.js +89 -0
  100. package/dist/src/push.js +224 -0
  101. package/dist/src/routes/codex.js +606 -0
  102. package/dist/src/routes/files.js +230 -0
  103. package/dist/src/routes/git.js +78 -0
  104. package/dist/src/routes/health.js +16 -0
  105. package/dist/src/routes/notifications.js +16 -0
  106. package/dist/src/routes/previews.js +43 -0
  107. package/dist/src/routes/push.js +145 -0
  108. package/dist/src/routes/sessions.js +232 -0
  109. package/dist/src/routes/system.js +156 -0
  110. package/dist/src/routes/terminal.js +238 -0
  111. package/dist/src/routes/transcript.js +197 -0
  112. package/dist/src/routes/workspace.js +267 -0
  113. package/{src → dist/src}/server.js +110 -76
  114. package/dist/src/shortcutConfig.js +83 -0
  115. package/{src → dist/src}/staticCache.js +5 -3
  116. package/{src → dist/src}/staticCompression.js +5 -6
  117. package/dist/src/terminalStream.js +441 -0
  118. package/{src → dist/src}/tmux/commands.js +160 -167
  119. package/dist/src/tmux/format.js +47 -0
  120. package/dist/src/transcriptParse.js +240 -0
  121. package/dist/src/transcriptReader.js +113 -0
  122. package/{src → dist/src}/trimCapture.js +11 -11
  123. package/dist/src/uploadTypes.js +29 -0
  124. package/dist/src/urlPolicy.js +18 -0
  125. package/dist/src/usage.js +260 -0
  126. package/dist/src/workspace/agentRunner.js +204 -0
  127. package/dist/src/workspace/atomicJson.js +31 -0
  128. package/dist/src/workspace/capture.js +139 -0
  129. package/dist/src/workspace/checkpointer.js +228 -0
  130. package/dist/src/workspace/environment.js +71 -0
  131. package/dist/src/workspace/lock.js +137 -0
  132. package/dist/src/workspace/mapping.js +128 -0
  133. package/dist/src/workspace/operations.js +271 -0
  134. package/dist/src/workspace/paths.js +15 -0
  135. package/dist/src/workspace/planner.js +292 -0
  136. package/dist/src/workspace/restore.js +314 -0
  137. package/dist/src/workspace/runtime.js +318 -0
  138. package/dist/src/workspace/schema.js +156 -0
  139. package/dist/src/workspace/store.js +464 -0
  140. package/dist/src/workspace/tmuxAdapter.js +694 -0
  141. package/dist/src/workspaceProtocol.js +158 -0
  142. package/package.json +28 -16
  143. package/hooks/handmux-codex-usage.cjs +0 -141
  144. package/public/assets/index-ClOByNCn.js +0 -335
  145. package/public/assets/index-rjrtUwoJ.css +0 -32
  146. package/src/.gitkeep +0 -0
  147. package/src/agents/codex.js +0 -121
  148. package/src/agents/processIdentity.js +0 -72
  149. package/src/agents/scanUtils.js +0 -225
  150. package/src/asr/iflyConfig.js +0 -10
  151. package/src/asr/iflySign.js +0 -16
  152. package/src/auth.js +0 -30
  153. package/src/browser/bootstrap.js +0 -68
  154. package/src/browser/cookieProfiles.js +0 -403
  155. package/src/browser/coordinator.js +0 -47
  156. package/src/browser/hammerheadCompat.js +0 -86
  157. package/src/browser/hammerheadRedirectCompat.js +0 -38
  158. package/src/browser/manager.js +0 -556
  159. package/src/browser/originLabel.js +0 -17
  160. package/src/browser/profilePersistence.js +0 -241
  161. package/src/browser/publicProxy.js +0 -175
  162. package/src/browser/routes.js +0 -154
  163. package/src/browser/sessionStore.js +0 -78
  164. package/src/browser/siteVersion.js +0 -124
  165. package/src/browser/targetPolicy.js +0 -137
  166. package/src/browser/workerClient.js +0 -253
  167. package/src/browser/workerServer.js +0 -98
  168. package/src/captureBackground.js +0 -93
  169. package/src/claudeEvents.js +0 -356
  170. package/src/cli/cloudflared.js +0 -99
  171. package/src/cli/codexHooks.js +0 -123
  172. package/src/cli/drivers.js +0 -97
  173. package/src/cli/hookScaffold.js +0 -35
  174. package/src/cli/i18n/en.js +0 -388
  175. package/src/cli/i18n/zh.js +0 -387
  176. package/src/cli/options.js +0 -211
  177. package/src/cli/probe.js +0 -16
  178. package/src/cli/pushCmd.js +0 -60
  179. package/src/cli/service.js +0 -126
  180. package/src/cli/setupModel.js +0 -154
  181. package/src/cli/setupWizard.js +0 -506
  182. package/src/cli/shortcutEditor.js +0 -288
  183. package/src/cli/sshTunnel.js +0 -12
  184. package/src/cli/state.js +0 -90
  185. package/src/cli/supervisor.js +0 -183
  186. package/src/cli/supervisorProcesses.js +0 -57
  187. package/src/cli/tmuxVersion.js +0 -49
  188. package/src/cli/tunnelClients.js +0 -62
  189. package/src/cli/workspaceCmd.js +0 -300
  190. package/src/config.js +0 -9
  191. package/src/docs.js +0 -228
  192. package/src/git.js +0 -208
  193. package/src/httpApi.js +0 -69
  194. package/src/jsonStore.js +0 -24
  195. package/src/notifications.js +0 -68
  196. package/src/orphans.js +0 -150
  197. package/src/pendingPrompt.js +0 -163
  198. package/src/previewServer.js +0 -168
  199. package/src/previews.js +0 -116
  200. package/src/push.js +0 -183
  201. package/src/routes/files.js +0 -172
  202. package/src/routes/git.js +0 -57
  203. package/src/routes/notifications.js +0 -19
  204. package/src/routes/previews.js +0 -35
  205. package/src/routes/push.js +0 -119
  206. package/src/routes/sessions.js +0 -163
  207. package/src/routes/system.js +0 -145
  208. package/src/routes/terminal.js +0 -210
  209. package/src/routes/transcript.js +0 -115
  210. package/src/routes/workspace.js +0 -267
  211. package/src/shortcutConfig.js +0 -75
  212. package/src/terminalStream.js +0 -391
  213. package/src/tmux/format.js +0 -45
  214. package/src/transcriptParse.js +0 -180
  215. package/src/transcriptReader.js +0 -101
  216. package/src/uploadTypes.js +0 -30
  217. package/src/urlPolicy.js +0 -14
  218. package/src/usage.js +0 -261
  219. package/src/workspace/agentRunner.js +0 -185
  220. package/src/workspace/atomicJson.js +0 -32
  221. package/src/workspace/capture.js +0 -87
  222. package/src/workspace/checkpointer.js +0 -183
  223. package/src/workspace/environment.js +0 -75
  224. package/src/workspace/lock.js +0 -128
  225. package/src/workspace/mapping.js +0 -124
  226. package/src/workspace/operations.js +0 -252
  227. package/src/workspace/paths.js +0 -16
  228. package/src/workspace/planner.js +0 -247
  229. package/src/workspace/restore.js +0 -279
  230. package/src/workspace/runtime.js +0 -314
  231. package/src/workspace/schema.js +0 -136
  232. package/src/workspace/store.js +0 -382
  233. package/src/workspace/tmuxAdapter.js +0 -554
  234. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  235. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  236. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  237. /package/{public → dist/public}/icons/badge-96.png +0 -0
  238. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  239. /package/{public → dist/public}/icons/icon-192.png +0 -0
  240. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  241. /package/{public → dist/public}/icons/icon-512.png +0 -0
  242. /package/{public → dist/public}/icons/logo.svg +0 -0
  243. /package/{public → dist/public}/manifest.webmanifest +0 -0
  244. /package/{public → dist/public}/offline.html +0 -0
  245. /package/{public → dist/public}/sw.js +0 -0
  246. /package/{src → dist/src}/browser/protocol.js +0 -0
  247. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -1,554 +0,0 @@
1
- import crypto from 'node:crypto';
2
- import { createAgentRunner } from './agentRunner.js';
3
- import { parseTmuxRows, tmuxFormat } from '../tmux/format.js';
4
-
5
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
- const NO_SERVER_RE = /^(?:no server running on(?: .+)?|no sessions|error connecting to .+ \(no such file or directory\))$/i;
7
- const MISSING_OPTION_RE = /invalid option|unknown option|not set/i;
8
- const SESSION_FORMAT = tmuxFormat(['session_id', 'session_name', 'session_last_attached', '@handmux_session_id']);
9
- const WINDOW_FORMAT = tmuxFormat(['session_id', 'window_id', 'window_index', 'window_name', 'window_active', 'window_layout', '@handmux_window_id']);
10
- const PANE_FORMAT = tmuxFormat(['window_id', 'pane_id', 'pane_index', 'pane_active', 'pane_current_path', '@handmux_pane_id']);
11
- const ACTIVE_FORMAT = tmuxFormat(['session_id', 'window_id', 'pane_id']);
12
-
13
- const text = (value) => String(value && typeof value === 'object' && 'stdout' in value ? value.stdout : value ?? '');
14
- const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
15
- const byId = (a, b) => compare(a.id, b.id);
16
- const byRuntime = (a, b) => compare(a.runtimeId, b.runtimeId);
17
-
18
- function isUuid(value) { return typeof value === 'string' && UUID_RE.test(value); }
19
- function isNoServer(error) {
20
- if (error?.code === 'ENOENT') return true;
21
- return NO_SERVER_RE.test(String(error?.stderr || error?.message || error || '').trim());
22
- }
23
- function isMissingOption(error) { return MISSING_OPTION_RE.test(String(error?.stderr || error?.message || error || '')); }
24
-
25
- function rows(output, columns, label) {
26
- return parseTmuxRows(text(output), columns, label);
27
- }
28
-
29
- function index(value, label) {
30
- if (!/^\d+$/.test(value)) throw new Error(`invalid ${label}`);
31
- return Number(value);
32
- }
33
-
34
- function active(value, label) {
35
- if (value !== '0' && value !== '1') throw new Error(`invalid ${label}`);
36
- return value === '1';
37
- }
38
-
39
- function runtime(value, prefix, label) {
40
- if (!new RegExp(`^\\${prefix}\\d+$`).test(value)) throw new Error(`invalid ${label}`);
41
- return value;
42
- }
43
-
44
- function logicalAllocator(randomUUID) {
45
- const used = new Set();
46
- return {
47
- accept(candidate) {
48
- const id = candidate;
49
- if (!isUuid(id) || used.has(id)) return null;
50
- used.add(id);
51
- return id;
52
- },
53
- fresh() {
54
- for (let tries = 0; tries < 100; tries++) {
55
- const id = randomUUID();
56
- if (isUuid(id) && !used.has(id)) { used.add(id); return id; }
57
- }
58
- throw new Error('could not allocate a unique workspace logical id');
59
- },
60
- };
61
- }
62
-
63
- function requireLogicalId(value, label) {
64
- if (!isUuid(value)) throw new Error(`${label} must be a UUID`);
65
- return value;
66
- }
67
-
68
- function ephemeralLogicalId(option, runtimeId) {
69
- const hash = crypto.createHash('sha256').update(`${option}\0${runtimeId}`).digest('hex');
70
- return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
71
- }
72
-
73
- function requireCreatedRuntime(value, prefix, label) {
74
- return runtime(value, prefix, label);
75
- }
76
-
77
- function isTmuxLayout(value) {
78
- if (typeof value !== 'string' || value.length === 0 || value.length > 8192) return false;
79
- if (!/^[0-9a-f]{4},\d+x\d+,\d+,\d+(?:,\d+|[\[{])/i.test(value)) return false;
80
- if (!/^[0-9a-fx,{}\[\]]+$/i.test(value)) return false;
81
- const stack = [];
82
- for (const char of value) {
83
- if (char === '{' || char === '[') stack.push(char);
84
- else if (char === '}' || char === ']') {
85
- const expected = char === '}' ? '{' : '[';
86
- if (stack.pop() !== expected) return false;
87
- }
88
- }
89
- return stack.length === 0;
90
- }
91
-
92
- function errorText(error) {
93
- return error instanceof Error ? error.message : String(error);
94
- }
95
-
96
- function withCleanupError(error, cleanupError) {
97
- if (!cleanupError) return error;
98
- const combined = new Error(`${errorText(error)}; cleanup failed: ${errorText(cleanupError)}`);
99
- combined.cause = error;
100
- return combined;
101
- }
102
-
103
- export function createdTargetGuard(created) {
104
- return (target) => {
105
- if (!created.has(target)) throw new Error(`workspace target was not created by this restore: ${target}`);
106
- return target;
107
- };
108
- }
109
-
110
- export function createWorkspaceTmux({ run, randomUUID = crypto.randomUUID, agentRunner = createAgentRunner(), readOnly = false } = {}) {
111
- if (typeof run !== 'function') throw new Error('workspace tmux run is required');
112
- const created = new Set();
113
- const guard = createdTargetGuard(created);
114
- const sessionWindows = new Map();
115
- const windowSession = new Map();
116
- const windowPanes = new Map();
117
- const paneWindow = new Map();
118
-
119
- function ensureWritable() {
120
- if (readOnly) throw new Error('workspace tmux adapter is read-only');
121
- }
122
-
123
- function trackWindow(sessionId, windowId, paneId) {
124
- const windows = sessionWindows.get(sessionId) || new Set();
125
- windows.add(windowId);
126
- sessionWindows.set(sessionId, windows);
127
- windowSession.set(windowId, sessionId);
128
- windowPanes.set(windowId, new Set([paneId]));
129
- paneWindow.set(paneId, windowId);
130
- }
131
-
132
- function trackPane(targetPaneId, paneId) {
133
- const windowId = paneWindow.get(targetPaneId);
134
- if (!windowId) throw new Error(`workspace pane owner is unavailable: ${targetPaneId}`);
135
- windowPanes.get(windowId).add(paneId);
136
- paneWindow.set(paneId, windowId);
137
- }
138
-
139
- function forgetWindow(windowId) {
140
- for (const paneId of windowPanes.get(windowId) || []) {
141
- created.delete(paneId);
142
- paneWindow.delete(paneId);
143
- }
144
- windowPanes.delete(windowId);
145
- const sessionId = windowSession.get(windowId);
146
- if (sessionId) sessionWindows.get(sessionId)?.delete(windowId);
147
- windowSession.delete(windowId);
148
- created.delete(windowId);
149
- }
150
-
151
- function forgetSession(sessionId) {
152
- for (const windowId of sessionWindows.get(sessionId) || []) forgetWindow(windowId);
153
- sessionWindows.delete(sessionId);
154
- created.delete(sessionId);
155
- }
156
-
157
- function revokeCreatedTargets() {
158
- created.clear();
159
- sessionWindows.clear();
160
- windowSession.clear();
161
- windowPanes.clear();
162
- paneWindow.clear();
163
- }
164
-
165
- async function cleanupSteps(steps) {
166
- const failures = [];
167
- for (const step of steps) {
168
- try { await step(); } catch (error) { failures.push(errorText(error)); }
169
- }
170
- if (failures.length) throw new Error(failures.join('; '));
171
- }
172
-
173
- async function cleanupSession(sessionId, windowId, paneId) {
174
- let killed = false;
175
- try {
176
- await cleanupSteps([
177
- () => run(['set-option', '-u', '-p', '-t', paneId, '@handmux_pane_id']),
178
- () => run(['set-option', '-u', '-w', '-t', windowId, '@handmux_window_id']),
179
- () => run(['set-option', '-u', '-t', sessionId, '@handmux_session_id']),
180
- async () => { await run(['kill-session', '-t', sessionId]); killed = true; },
181
- ]);
182
- } finally {
183
- if (killed) forgetSession(sessionId);
184
- }
185
- }
186
-
187
- async function cleanupWindow(windowId, paneId) {
188
- let killed = false;
189
- try {
190
- await cleanupSteps([
191
- () => run(['set-option', '-u', '-p', '-t', paneId, '@handmux_pane_id']),
192
- () => run(['set-option', '-u', '-w', '-t', windowId, '@handmux_window_id']),
193
- async () => { await run(['kill-window', '-t', windowId]); killed = true; },
194
- ]);
195
- } finally {
196
- if (killed) forgetWindow(windowId);
197
- }
198
- }
199
-
200
- async function cleanupPane(paneId) {
201
- let killed = false;
202
- try {
203
- await cleanupSteps([
204
- () => run(['set-option', '-u', '-p', '-t', paneId, '@handmux_pane_id']),
205
- async () => { await run(['kill-pane', '-t', paneId]); killed = true; },
206
- ]);
207
- } finally {
208
- if (killed) {
209
- created.delete(paneId);
210
- windowPanes.get(paneWindow.get(paneId))?.delete(paneId);
211
- paneWindow.delete(paneId);
212
- }
213
- }
214
- }
215
-
216
- async function observeEnvironment({ readOnly: observeReadOnly = readOnly } = {}) {
217
- let current;
218
- try {
219
- current = text(await run(['show-options', '-gv', '@handmux_server_id'])).replace(/\r?\n$/, '');
220
- } catch (error) {
221
- if (isNoServer(error)) return { status: 'absent', tmuxServerId: null };
222
- if (!isMissingOption(error)) return { status: 'unknown' };
223
- current = '';
224
- }
225
- if (isUuid(current)) return { status: 'present', tmuxServerId: current };
226
- const tmuxServerId = randomUUID();
227
- if (!isUuid(tmuxServerId)) return { status: 'unknown' };
228
- if (observeReadOnly) return { status: 'present', tmuxServerId };
229
- try {
230
- await run(['set-option', '-g', '@handmux_server_id', tmuxServerId]);
231
- return { status: 'present', tmuxServerId };
232
- } catch (error) {
233
- return isNoServer(error) ? { status: 'absent', tmuxServerId: null } : { status: 'unknown' };
234
- }
235
- }
236
-
237
- async function assignLogicalIds(items, option, scopeArgs, { readOnly: assignReadOnly = readOnly } = {}) {
238
- const allocator = logicalAllocator(randomUUID);
239
- for (const item of [...items].sort(byRuntime)) {
240
- const accepted = allocator.accept(item.optionId);
241
- const generated = !accepted && (assignReadOnly
242
- ? allocator.accept(ephemeralLogicalId(option, item.runtimeId))
243
- : allocator.fresh());
244
- item.id = accepted || generated;
245
- if (!item.id) throw new Error(`could not allocate a read-only logical id for ${item.runtimeId}`);
246
- if (!accepted && !assignReadOnly) await run(['set-option', ...scopeArgs, '-t', item.runtimeId, option, item.id]);
247
- }
248
- }
249
-
250
- async function captureTopology({ readOnly: captureReadOnly = readOnly } = {}) {
251
- try {
252
- const environment = await observeEnvironment({ readOnly: captureReadOnly });
253
- if (environment.status === 'absent') return { status: 'empty', tmuxVersion: 'unknown', active: null, sessions: [], windows: [] };
254
- if (environment.status !== 'present') return { status: 'unknown', error: 'tmux environment unavailable' };
255
-
256
- const tmuxVersion = text(await run(['-V'])).trim().replace(/^tmux\s+/, '');
257
- if (!tmuxVersion) throw new Error('invalid tmux version');
258
- let sessionFields;
259
- try { sessionFields = rows(await run(['list-sessions', '-F', SESSION_FORMAT]), 4, 'session'); }
260
- catch (error) {
261
- if (isNoServer(error)) return { status: 'empty', tmuxVersion: 'unknown', active: null, sessions: [], windows: [] };
262
- throw error;
263
- }
264
- if (sessionFields.length === 0) return { status: 'empty', tmuxVersion: 'unknown', active: null, sessions: [], windows: [] };
265
-
266
- const sessions = sessionFields.map(([runtimeId, name, lastAttached, optionId]) => ({
267
- runtimeId: runtime(runtimeId, '$', 'session runtime id'), name,
268
- lastAttached: index(lastAttached === '' ? '0' : lastAttached, 'session last attached'), optionId,
269
- windowLinks: [], activeWindowId: null,
270
- }));
271
- if (new Set(sessions.map((item) => item.runtimeId)).size !== sessions.length) throw new Error('duplicate session runtime id');
272
- await assignLogicalIds(sessions, '@handmux_session_id', [], { readOnly: captureReadOnly });
273
- const sessionByRuntime = new Map(sessions.map((item) => [item.runtimeId, item]));
274
-
275
- const windowFields = rows(await run(['list-windows', '-a', '-F', WINDOW_FORMAT]), 7, 'window');
276
- const windowLinks = windowFields.map(([sessionRuntimeId, runtimeId, windowIndex, name, isActive, layout, optionId]) => {
277
- if (!sessionByRuntime.has(sessionRuntimeId)) throw new Error('window references unknown session');
278
- return {
279
- sessionRuntimeId, runtimeId: runtime(runtimeId, '@', 'window runtime id'), index: index(windowIndex, 'window index'),
280
- name, active: active(isActive, 'window active'), layout, optionId,
281
- };
282
- });
283
- const groupedWindows = new Map();
284
- for (const link of windowLinks) {
285
- const group = groupedWindows.get(link.runtimeId) || [];
286
- group.push(link);
287
- groupedWindows.set(link.runtimeId, group);
288
- }
289
- const windows = [...groupedWindows].map(([runtimeId, links]) => {
290
- const optionIds = new Set(links.map((item) => item.optionId).filter(Boolean));
291
- if (optionIds.size > 1) throw new Error('linked window has conflicting logical ids');
292
- return { runtimeId, optionId: optionIds.values().next().value || '', links };
293
- });
294
- await assignLogicalIds(windows, '@handmux_window_id', ['-w'], { readOnly: captureReadOnly });
295
- const windowByRuntime = new Map(windows.map((item) => [item.runtimeId, item]));
296
-
297
- for (const window of windows) {
298
- for (const link of window.links) {
299
- const session = sessionByRuntime.get(link.sessionRuntimeId);
300
- session.windowLinks.push({ windowId: window.id, index: link.index });
301
- if (link.active) session.activeWindowId = window.id;
302
- }
303
- }
304
- for (const session of sessions) {
305
- session.windowLinks.sort((a, b) => a.index - b.index || compare(a.windowId, b.windowId));
306
- if (!session.windowLinks.length || !session.activeWindowId) throw new Error('session has no active linked window');
307
- }
308
-
309
- const paneFields = rows(await run(['list-panes', '-a', '-F', PANE_FORMAT]), 6, 'pane');
310
- const paneByRuntime = new Map();
311
- for (const [windowRuntimeId, runtimeId, paneIndex, isActive, cwd, optionId] of paneFields) {
312
- if (!windowByRuntime.has(windowRuntimeId)) throw new Error('pane references unknown window');
313
- const pane = {
314
- windowRuntimeId, runtimeId: runtime(runtimeId, '%', 'pane runtime id'), index: index(paneIndex, 'pane index'),
315
- active: active(isActive, 'pane active'), cwd, optionId, agent: null,
316
- };
317
- const existing = paneByRuntime.get(pane.runtimeId);
318
- if (existing) {
319
- const fields = (item) => [item.windowRuntimeId, item.index, item.active, item.cwd, item.optionId];
320
- if (JSON.stringify(fields(existing)) !== JSON.stringify(fields(pane))) {
321
- throw new Error('duplicate pane runtime id has conflicting fields');
322
- }
323
- } else {
324
- paneByRuntime.set(pane.runtimeId, pane);
325
- }
326
- }
327
- const panes = [...paneByRuntime.values()];
328
- await assignLogicalIds(panes, '@handmux_pane_id', ['-p'], { readOnly: captureReadOnly });
329
-
330
- const canonicalSessions = sessions.sort(byId);
331
- const canonicalWindows = windows.map((window) => {
332
- const owner = [...window.links].sort((a, b) => compare(sessionByRuntime.get(a.sessionRuntimeId).id, sessionByRuntime.get(b.sessionRuntimeId).id))[0];
333
- const windowPanes = panes.filter((pane) => pane.windowRuntimeId === window.runtimeId).sort(byId);
334
- const activePane = windowPanes.find((pane) => pane.active);
335
- if (!activePane) throw new Error('window has no active pane');
336
- return {
337
- id: window.id, runtimeId: window.runtimeId, name: owner.name, index: owner.index, layout: owner.layout,
338
- activePaneId: activePane.id,
339
- panes: windowPanes.map(({ id, runtimeId, index: paneIndex, cwd, agent }) => ({ id, runtimeId, index: paneIndex, cwd, agent })),
340
- };
341
- }).sort(byId);
342
-
343
- const maxAttached = Math.max(...canonicalSessions.map((session) => session.lastAttached));
344
- const selected = canonicalSessions.find((session) => session.lastAttached === maxAttached);
345
- const [activeSessionRuntime, activeWindowRuntime, activePaneRuntime] = rows(
346
- await run(['display-message', '-p', '-t', selected.runtimeId, ACTIVE_FORMAT]), 3, 'active path',
347
- )[0] || [];
348
- const activeSession = sessionByRuntime.get(activeSessionRuntime);
349
- const activeWindow = windowByRuntime.get(activeWindowRuntime);
350
- const activePane = panes.find((pane) => pane.runtimeId === activePaneRuntime);
351
- if (!activeSession || !activeWindow || !activePane || activePane.windowRuntimeId !== activeWindowRuntime) throw new Error('invalid active path');
352
-
353
- return {
354
- status: 'ok', tmuxVersion,
355
- active: { sessionId: activeSession.id, windowId: activeWindow.id, paneId: activePane.id },
356
- sessions: canonicalSessions.map(({ id, runtimeId, name, windowLinks, activeWindowId }) => ({ id, runtimeId, name, windowLinks, activeWindowId })),
357
- windows: canonicalWindows,
358
- };
359
- } catch (error) {
360
- return { status: 'unknown', error: error?.message || String(error) };
361
- }
362
- }
363
-
364
- async function createTemporarySession({ cwd, sessionLogicalId, windowLogicalId, paneLogicalId, windowName, windowIndex }) {
365
- ensureWritable();
366
- requireLogicalId(sessionLogicalId, 'sessionLogicalId');
367
- const hasSeed = [windowLogicalId, paneLogicalId, windowName, windowIndex].some((value) => value !== undefined);
368
- if (hasSeed) {
369
- requireLogicalId(windowLogicalId, 'windowLogicalId');
370
- requireLogicalId(paneLogicalId, 'paneLogicalId');
371
- if (typeof windowName !== 'string' || !windowName) throw new Error('windowName must be a non-empty string');
372
- if (!Number.isInteger(windowIndex) || windowIndex < 0) throw new Error('windowIndex must be a non-negative integer');
373
- }
374
- const name = `hm-r-${randomUUID().replaceAll('-', '').slice(0, 8)}`;
375
- if (!/^hm-r-[0-9a-f]{8}$/i.test(name)) throw new Error('could not allocate temporary session name');
376
- const args = ['new-session', '-d', '-P', '-F', tmuxFormat(['session_id', 'window_id', 'pane_id', 'window_index']), '-s', name];
377
- if (hasSeed) args.push('-n', windowName);
378
- args.push('-c', cwd);
379
- const output = await run(args);
380
- let sessionId;
381
- let windowId;
382
- let paneId;
383
- let seedIndex;
384
- try {
385
- const parsed = rows(output, 4, 'created session')[0];
386
- if (!parsed) throw new Error('tmux did not return created session ids');
387
- [sessionId, windowId, paneId] = parsed;
388
- requireCreatedRuntime(sessionId, '$', 'created session id');
389
- requireCreatedRuntime(windowId, '@', 'created window id');
390
- requireCreatedRuntime(paneId, '%', 'created pane id');
391
- seedIndex = index(parsed[3], 'created window index');
392
- } catch (error) {
393
- let cleanupError;
394
- try { await run(['kill-session', '-t', `=${name}`]); } catch (failure) { cleanupError = failure; }
395
- throw withCleanupError(error, cleanupError);
396
- }
397
- created.add(sessionId); created.add(windowId); created.add(paneId);
398
- trackWindow(sessionId, windowId, paneId);
399
- try {
400
- const targetIndex = hasSeed ? windowIndex : 9999;
401
- if (seedIndex !== targetIndex) await run(['move-window', '-s', guard(windowId), '-t', `${guard(sessionId)}:${targetIndex}`]);
402
- if (hasSeed) {
403
- await run(['set-option', '-p', '-t', guard(paneId), '@handmux_pane_id', paneLogicalId]);
404
- await run(['set-option', '-w', '-t', guard(windowId), '@handmux_window_id', windowLogicalId]);
405
- }
406
- // Set the session id last: after this succeeds the helper has no remaining fallible setup step.
407
- await run(['set-option', '-t', guard(sessionId), '@handmux_session_id', sessionLogicalId]);
408
- } catch (error) {
409
- let cleanupError;
410
- try { await cleanupSession(sessionId, windowId, paneId); } catch (failure) { cleanupError = failure; }
411
- throw withCleanupError(error, cleanupError);
412
- }
413
- return { sessionId, windowId, paneId, name };
414
- }
415
-
416
- async function createWindow(sessionId, { name, index: windowIndex, cwd, windowLogicalId, paneLogicalId }) {
417
- ensureWritable();
418
- guard(sessionId);
419
- if (!Number.isInteger(windowIndex) || windowIndex < 0) throw new Error('window index must be a non-negative integer');
420
- requireLogicalId(windowLogicalId, 'windowLogicalId');
421
- requireLogicalId(paneLogicalId, 'paneLogicalId');
422
- const parsed = rows(await run(['new-window', '-d', '-P', '-F', tmuxFormat(['window_id', 'pane_id']), '-t', `${sessionId}:${windowIndex}`, '-n', name, '-c', cwd]), 2, 'created window')[0];
423
- if (!parsed) throw new Error('tmux did not return created window ids');
424
- const [windowId, paneId] = parsed;
425
- requireCreatedRuntime(windowId, '@', 'created window id');
426
- requireCreatedRuntime(paneId, '%', 'created pane id');
427
- created.add(windowId); created.add(paneId);
428
- trackWindow(sessionId, windowId, paneId);
429
- try {
430
- await run(['set-option', '-p', '-t', guard(paneId), '@handmux_pane_id', paneLogicalId]);
431
- await run(['set-option', '-w', '-t', guard(windowId), '@handmux_window_id', windowLogicalId]);
432
- } catch (error) {
433
- let cleanupError;
434
- try { await cleanupWindow(windowId, paneId); } catch (failure) { cleanupError = failure; }
435
- throw withCleanupError(error, cleanupError);
436
- }
437
- return { windowId, paneId };
438
- }
439
-
440
- async function splitPane(targetPaneId, { cwd, paneLogicalId }) {
441
- ensureWritable();
442
- guard(targetPaneId);
443
- requireLogicalId(paneLogicalId, 'paneLogicalId');
444
- const paneId = text(await run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', targetPaneId, '-c', cwd])).trim();
445
- requireCreatedRuntime(paneId, '%', 'created pane id');
446
- created.add(paneId);
447
- trackPane(targetPaneId, paneId);
448
- try {
449
- await run(['set-option', '-p', '-t', guard(paneId), '@handmux_pane_id', paneLogicalId]);
450
- } catch (error) {
451
- let cleanupError;
452
- try { await cleanupPane(paneId); } catch (failure) { cleanupError = failure; }
453
- throw withCleanupError(error, cleanupError);
454
- }
455
- return paneId;
456
- }
457
-
458
- async function linkWindow(windowId, sessionId, windowIndex, { existing = false } = {}) {
459
- ensureWritable();
460
- if (existing) runtime(windowId, '@', 'existing window id');
461
- else guard(windowId);
462
- guard(sessionId);
463
- if (!Number.isInteger(windowIndex) || windowIndex < 0) throw new Error('window index must be a non-negative integer');
464
- await run(['link-window', '-s', windowId, '-t', `${sessionId}:${windowIndex}`]);
465
- }
466
- async function applyLayout(windowId, layout) {
467
- ensureWritable();
468
- const target = guard(windowId);
469
- if (!isTmuxLayout(layout)) throw new Error('invalid workspace layout');
470
- await run(['select-layout', '-t', target, layout]);
471
- }
472
- async function selectPane(paneId) { ensureWritable(); await run(['select-pane', '-t', guard(paneId)]); }
473
- async function selectWindow(windowId) { ensureWritable(); await run(['select-window', '-t', guard(windowId)]); }
474
- async function selectWindowInSession(sessionId, windowIndex) {
475
- ensureWritable();
476
- guard(sessionId);
477
- if (!Number.isInteger(windowIndex) || windowIndex < 0) throw new Error('window index must be a non-negative integer');
478
- await run(['select-window', '-t', `${sessionId}:${windowIndex}`]);
479
- }
480
- async function renameCreatedSession(sessionId, name) { ensureWritable(); await run(['rename-session', '-t', guard(sessionId), name]); }
481
- async function killCreatedSession(sessionId) {
482
- ensureWritable();
483
- guard(sessionId);
484
- let killed = false;
485
- try {
486
- await cleanupSteps([
487
- () => run(['set-option', '-u', '-t', sessionId, '@handmux_session_id']),
488
- async () => { await run(['kill-session', '-t', sessionId]); killed = true; },
489
- ]);
490
- } finally {
491
- if (killed) forgetSession(sessionId);
492
- }
493
- }
494
- async function killCreatedWindow(windowId) {
495
- ensureWritable();
496
- await run(['kill-window', '-t', guard(windowId)]);
497
- forgetWindow(windowId);
498
- }
499
-
500
- async function startAgent(paneId, cmd, args = []) {
501
- ensureWritable();
502
- guard(paneId);
503
- const valid = cmd === 'claude'
504
- ? args.length === 2 && args[0] === '--resume' && isUuid(args[1])
505
- : cmd === 'codex' && args.length === 2 && args[0] === 'resume' && isUuid(args[1]);
506
- if (!valid) throw new Error('unsafe agent command token');
507
- await agentRunner.prepare({ paneId, cmd, args });
508
- let foregroundStarted = false;
509
- try {
510
- await run(['send-keys', '-t', paneId, '-l', '--', agentRunner.command]);
511
- await run(['send-keys', '-t', paneId, 'Enter']);
512
- foregroundStarted = true;
513
- const ready = await agentRunner.waitReady(paneId);
514
- if (ready?.status !== 'ready') throw new Error(ready?.error || 'agent failed before readiness');
515
- } catch (error) {
516
- const cleanupFailures = [];
517
- if (foregroundStarted) {
518
- try { await run(['send-keys', '-t', paneId, 'C-c']); } catch (failure) { cleanupFailures.push(failure); }
519
- }
520
- if (typeof agentRunner.cancel === 'function') {
521
- try { await agentRunner.cancel(paneId); } catch (failure) { cleanupFailures.push(failure); }
522
- }
523
- const cleanupError = cleanupFailures.length
524
- ? new Error(cleanupFailures.map(errorText).join('; '))
525
- : null;
526
- throw withCleanupError(error, cleanupError);
527
- }
528
- }
529
-
530
- async function topologyFingerprint() {
531
- const topology = await captureTopology();
532
- if (topology.status === 'unknown') return topology;
533
- return crypto.createHash('sha256').update(JSON.stringify(topology)).digest('hex');
534
- }
535
-
536
- return {
537
- observeEnvironment,
538
- captureTopology,
539
- createTemporarySession,
540
- createWindow,
541
- splitPane,
542
- linkWindow,
543
- applyLayout,
544
- selectPane,
545
- selectWindow,
546
- selectWindowInSession,
547
- renameCreatedSession,
548
- killCreatedSession,
549
- killCreatedWindow,
550
- startAgent,
551
- topologyFingerprint,
552
- revokeCreatedTargets,
553
- };
554
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes