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