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,301 @@
1
+ import { spawn as spawnChild } from 'node:child_process';
2
+ import { constants as fsConstants } from 'node:fs';
3
+ import fsp from 'node:fs/promises';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { ensurePrivateDir, writeJsonAtomic } from './atomicJson.js';
8
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
9
+ const PANE_RE = /^%\d+$/;
10
+ const RUNNER_FILE = fileURLToPath(import.meta.url);
11
+ const HANDMUX_BIN = fileURLToPath(new URL('../../bin/handmux.js', import.meta.url));
12
+ const MAX_PATH_LENGTH = 32_768;
13
+ const MAX_PATH_ENTRIES = 256;
14
+ const MAX_PATH_ENTRY_LENGTH = 4_096;
15
+ function errorText(error) {
16
+ return error instanceof Error ? error.message : String(error);
17
+ }
18
+ const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value)
19
+ ? value : null);
20
+ function errorCode(error) {
21
+ return error && typeof error === 'object' ? error.code : undefined;
22
+ }
23
+ export function validateAgentRequest(input) {
24
+ const request = recordOf(input);
25
+ const args = request?.args;
26
+ const cmd = request?.cmd;
27
+ const valid = cmd === 'claude'
28
+ ? Array.isArray(args) && args.length === 2 && args[0] === '--resume' && typeof args[1] === 'string' && UUID_RE.test(args[1])
29
+ : cmd === 'codex'
30
+ && Array.isArray(args) && args.length === 2 && args[0] === 'resume' && typeof args[1] === 'string' && UUID_RE.test(args[1]);
31
+ if (!valid)
32
+ throw new Error('unsafe agent request');
33
+ const validArgs = args;
34
+ return { cmd: cmd, args: [validArgs[0], validArgs[1]] };
35
+ }
36
+ function validatePathSnapshot(value, platform) {
37
+ if (typeof value !== 'string' || !value || value.length > MAX_PATH_LENGTH || /[\0\r\n]/.test(value)) {
38
+ throw new Error('unsafe agent PATH snapshot');
39
+ }
40
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
41
+ const entries = value.split(platform === 'win32' ? ';' : ':');
42
+ if (!entries.length || entries.length > MAX_PATH_ENTRIES)
43
+ throw new Error('unsafe agent PATH snapshot');
44
+ for (const entry of entries) {
45
+ if (!entry || entry.length > MAX_PATH_ENTRY_LENGTH || !pathApi.isAbsolute(entry)) {
46
+ throw new Error('unsafe agent PATH snapshot');
47
+ }
48
+ }
49
+ return value;
50
+ }
51
+ export function validatePreparedAgentRequest(input, platform = process.platform) {
52
+ const record = recordOf(input);
53
+ const request = validateAgentRequest(input);
54
+ const paneId = record?.paneId;
55
+ if (typeof paneId !== 'string' || !PANE_RE.test(paneId))
56
+ throw new Error('unsafe prepared agent request');
57
+ return { ...request, paneId, pathEnv: validatePathSnapshot(record?.pathEnv, platform) };
58
+ }
59
+ function earlyFailure(request, error, code) {
60
+ if (errorCode(error) === 'ENOENT')
61
+ return `${request.cmd} binary not found`;
62
+ if (error)
63
+ return `${request.cmd} failed to start: ${errorText(error)}`;
64
+ return `${request.cmd} exited ${code ?? 'unknown'} before ready`;
65
+ }
66
+ export function launchAgentRequest(requestInput, { spawn = spawnChild, readinessMs = 500, setTimeout = globalThis.setTimeout, clearTimeout = globalThis.clearTimeout, env, } = {}) {
67
+ const request = validateAgentRequest(requestInput);
68
+ let child;
69
+ let readyDone = false;
70
+ let timer = null;
71
+ let completionDone = false;
72
+ let settleReady;
73
+ let settleCompletion;
74
+ const ready = new Promise((resolve) => { settleReady = resolve; });
75
+ const completion = new Promise((resolve) => { settleCompletion = resolve; });
76
+ const finishCompletion = (value) => {
77
+ if (completionDone)
78
+ return;
79
+ completionDone = true;
80
+ settleCompletion(value);
81
+ };
82
+ const failReady = (error, code) => {
83
+ if (readyDone)
84
+ return;
85
+ readyDone = true;
86
+ if (timer)
87
+ clearTimeout(timer);
88
+ settleReady({ status: 'failed', error: earlyFailure(request, error, code) });
89
+ };
90
+ try {
91
+ const command = request.cmd === 'codex' ? process.execPath : request.cmd;
92
+ const args = request.cmd === 'codex' ? [HANDMUX_BIN, 'codex', ...request.args] : request.args;
93
+ child = spawn(command, args, {
94
+ stdio: 'inherit',
95
+ shell: false,
96
+ ...(env ? { env } : {}),
97
+ });
98
+ }
99
+ catch (error) {
100
+ failReady(error);
101
+ finishCompletion({ code: null, signal: null, error: errorText(error) });
102
+ return { child: null, ready, completion, terminate: async () => completion };
103
+ }
104
+ child.once('spawn', () => {
105
+ timer = setTimeout(() => {
106
+ if (readyDone)
107
+ return;
108
+ readyDone = true;
109
+ settleReady({ status: 'ready' });
110
+ }, readinessMs);
111
+ });
112
+ child.once('error', (error) => {
113
+ failReady(error);
114
+ finishCompletion({ code: null, signal: null, error: errorText(error) });
115
+ });
116
+ child.once('exit', (code, signal) => {
117
+ failReady(null, code);
118
+ finishCompletion({ code, signal });
119
+ });
120
+ return {
121
+ child,
122
+ ready,
123
+ completion,
124
+ async terminate() {
125
+ if (!completionDone)
126
+ child.kill('SIGTERM');
127
+ return completion;
128
+ },
129
+ };
130
+ }
131
+ export function launchPreparedAgentRequest(requestInput, { env = process.env, platform = process.platform, ...options } = {}) {
132
+ const request = validatePreparedAgentRequest(requestInput, platform);
133
+ return launchAgentRequest(request, { ...options, env: { ...env, PATH: request.pathEnv } });
134
+ }
135
+ export async function publishAgentReadiness(launched, writeStatus) {
136
+ const ready = await launched.ready;
137
+ try {
138
+ await writeStatus(ready);
139
+ }
140
+ catch (error) {
141
+ let cleanupError;
142
+ try {
143
+ await launched.terminate();
144
+ }
145
+ catch (failure) {
146
+ cleanupError = failure;
147
+ }
148
+ if (cleanupError)
149
+ throw new Error(`${errorText(error)}; cleanup failed: ${errorText(cleanupError)}`);
150
+ throw error;
151
+ }
152
+ return ready;
153
+ }
154
+ function shellQuote(value) {
155
+ return `'${String(value).replaceAll("'", `'"'"'`)}'`;
156
+ }
157
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
158
+ async function executableOnPath(command, { fs = fsp, env = process.env, platform = process.platform, } = {}) {
159
+ const pathValue = env.PATH;
160
+ if (!pathValue)
161
+ return false;
162
+ const pathApi = platform === 'win32' ? path.win32 : path.posix;
163
+ const delimiter = platform === 'win32' ? ';' : ':';
164
+ const extensions = platform === 'win32'
165
+ ? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
166
+ : [''];
167
+ for (const directory of pathValue.split(delimiter)) {
168
+ if (!directory)
169
+ continue;
170
+ for (const extension of extensions) {
171
+ const candidate = pathApi.join(directory, platform === 'win32' ? `${command}${extension}` : command);
172
+ try {
173
+ const stat = await fs.stat(candidate);
174
+ if (!stat.isFile())
175
+ continue;
176
+ await fs.access(candidate, platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
177
+ return true;
178
+ }
179
+ catch { /* keep searching while an install may be replacing the executable */ }
180
+ }
181
+ }
182
+ return false;
183
+ }
184
+ export async function waitForAgentExecutable(command, { available = executableOnPath, now = Date.now, wait = delay, timeoutMs = 8_000, pollMs = 100, } = {}) {
185
+ if (command !== 'claude' && command !== 'codex')
186
+ throw new Error('unsafe agent executable');
187
+ const started = now();
188
+ while (true) {
189
+ if (await available(command))
190
+ return { status: 'ready' };
191
+ const remaining = timeoutMs - (now() - started);
192
+ if (remaining <= 0) {
193
+ return { status: 'failed', error: `${command} executable unavailable after startup wait` };
194
+ }
195
+ await wait(Math.min(pollMs, remaining));
196
+ }
197
+ }
198
+ async function unlinkIfPresent(fs, file) {
199
+ try {
200
+ await fs.unlink(file);
201
+ }
202
+ catch (error) {
203
+ if (errorCode(error) !== 'ENOENT')
204
+ throw error;
205
+ }
206
+ }
207
+ export function createAgentRunner({ home = os.homedir(), fs = fsp, now = Date.now, wait = delay, timeoutMs = 3_000, pollMs = 25, executableAvailable, executableWaitMs = 8_000, executablePollMs = 100, pathEnv = process.env.PATH, platform = process.platform, } = {}) {
208
+ const dir = path.join(home, '.handmux', 'workspaces', 'agent-runs');
209
+ const command = `${shellQuote(process.execPath)} ${shellQuote(RUNNER_FILE)}`;
210
+ const files = (paneId) => {
211
+ if (!PANE_RE.test(paneId))
212
+ throw new Error('invalid agent runner pane id');
213
+ const base = paneId.slice(1);
214
+ return { request: path.join(dir, `${base}.request.json`), status: path.join(dir, `${base}.status.json`) };
215
+ };
216
+ return {
217
+ command,
218
+ waitForExecutable(cmd) {
219
+ let safePath;
220
+ try {
221
+ safePath = validatePathSnapshot(pathEnv, platform);
222
+ }
223
+ catch (error) {
224
+ return Promise.resolve({ status: 'failed', error: errorText(error) });
225
+ }
226
+ return waitForAgentExecutable(cmd, {
227
+ available: executableAvailable ?? ((command) => executableOnPath(command, {
228
+ fs,
229
+ env: { PATH: safePath, PATHEXT: process.env.PATHEXT },
230
+ platform,
231
+ })),
232
+ now,
233
+ wait,
234
+ timeoutMs: executableWaitMs,
235
+ pollMs: executablePollMs,
236
+ });
237
+ },
238
+ async prepare({ paneId, cmd, args }) {
239
+ const request = validateAgentRequest({ cmd, args });
240
+ const safePath = validatePathSnapshot(pathEnv, platform);
241
+ const target = files(paneId);
242
+ await ensurePrivateDir(dir, { fs });
243
+ await unlinkIfPresent(fs, target.status);
244
+ await writeJsonAtomic(target.request, { paneId, ...request, pathEnv: safePath }, { fs });
245
+ },
246
+ async waitReady(paneId) {
247
+ const target = files(paneId);
248
+ const started = now();
249
+ while (now() - started <= timeoutMs) {
250
+ try {
251
+ const status = recordOf(JSON.parse(await fs.readFile(target.status, 'utf8')));
252
+ if (status?.status === 'ready')
253
+ return { status: 'ready' };
254
+ if (status?.status === 'failed' && typeof status.error === 'string')
255
+ return { status: 'failed', error: status.error };
256
+ }
257
+ catch (error) {
258
+ if (errorCode(error) !== 'ENOENT' && !(error instanceof SyntaxError))
259
+ throw error;
260
+ }
261
+ await wait(pollMs);
262
+ }
263
+ return { status: 'failed', error: 'agent readiness timed out' };
264
+ },
265
+ async cancel(paneId) {
266
+ const target = files(paneId);
267
+ await Promise.all([target.request, target.status].map((file) => unlinkIfPresent(fs, file)));
268
+ },
269
+ };
270
+ }
271
+ async function runPreparedAgent() {
272
+ const paneId = process.env.TMUX_PANE;
273
+ const target = (() => {
274
+ if (typeof paneId !== 'string' || !PANE_RE.test(paneId))
275
+ throw new Error('agent runner requires TMUX_PANE');
276
+ const dir = path.join(os.homedir(), '.handmux', 'workspaces', 'agent-runs');
277
+ const base = paneId.slice(1);
278
+ return { request: path.join(dir, `${base}.request.json`), status: path.join(dir, `${base}.status.json`) };
279
+ })();
280
+ const input = JSON.parse(await fsp.readFile(target.request, 'utf8'));
281
+ const request = validatePreparedAgentRequest(input);
282
+ if (request.paneId !== paneId)
283
+ throw new Error('agent runner pane mismatch');
284
+ const launched = launchPreparedAgentRequest(request);
285
+ try {
286
+ const ready = await publishAgentReadiness(launched, (status) => writeJsonAtomic(target.status, status));
287
+ if (ready.status === 'ready')
288
+ await launched.completion;
289
+ if (ready.status !== 'ready')
290
+ throw new Error(ready.error);
291
+ }
292
+ finally {
293
+ await unlinkIfPresent(fsp, target.request);
294
+ }
295
+ }
296
+ if (process.argv[1] && path.resolve(process.argv[1]) === RUNNER_FILE) {
297
+ runPreparedAgent().catch((error) => {
298
+ process.stderr.write(`handmux agent resume: ${errorText(error)}\n`);
299
+ process.exitCode = 1;
300
+ });
301
+ }
@@ -0,0 +1,31 @@
1
+ import crypto from 'node:crypto';
2
+ import fsp from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ export async function ensurePrivateDir(dir, { fs = fsp } = {}) {
5
+ const resolved = path.resolve(dir);
6
+ const { root } = path.parse(resolved);
7
+ const parts = resolved.slice(root.length).split(path.sep).filter(Boolean);
8
+ const privateStart = parts.indexOf('.handmux');
9
+ const first = privateStart === -1 ? parts.length - 1 : privateStart;
10
+ for (let index = first; index < parts.length; index += 1) {
11
+ const current = path.join(root, ...parts.slice(0, index + 1));
12
+ await fs.mkdir(current, { recursive: true, mode: 0o700 });
13
+ await fs.chmod(current, 0o700);
14
+ }
15
+ }
16
+ export async function writeJsonAtomic(file, value, { fs = fsp } = {}) {
17
+ const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
18
+ const handle = await fs.open(temp, 'wx', 0o600);
19
+ try {
20
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
21
+ await handle.sync();
22
+ await handle.close();
23
+ await fs.rename(temp, file);
24
+ await fs.chmod(file, 0o600);
25
+ }
26
+ catch (error) {
27
+ await handle.close().catch(() => { });
28
+ await fs.unlink(temp).catch(() => { });
29
+ throw error;
30
+ }
31
+ }
@@ -0,0 +1,139 @@
1
+ import fsp from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { AGENTS } from '../agents/index.js';
4
+ import { resolveCodexRollout, sessionsDir as codexSessionsDir } from '../agents/codex.js';
5
+ import { canonicalizeSnapshot, WORKSPACE_SCHEMA_VERSION } from './schema.js';
6
+ function agentMap(agents) {
7
+ return new Map(agents.map((agent) => [agent.id, agent]));
8
+ }
9
+ export async function readAgentBindings(stateFile, agents = AGENTS, readFile = fsp.readFile, access = fsp.access, stat = fsp.stat) {
10
+ let state;
11
+ try {
12
+ state = JSON.parse(String(await readFile(stateFile, 'utf8')));
13
+ if (!state || typeof state !== 'object' || Array.isArray(state))
14
+ return new Map();
15
+ }
16
+ catch {
17
+ return new Map();
18
+ }
19
+ const known = agentMap(agents);
20
+ const bindings = new Map();
21
+ for (const [paneRuntimeId, record] of Object.entries(state)) {
22
+ if (!record || typeof record !== 'object' || Array.isArray(record))
23
+ continue;
24
+ // This is the Claude Hook state file. Legacy Codex-tagged rows are deliberately ignored; managed
25
+ // Codex identity belongs to App Server and must not be reconstructed from the removed Hook path.
26
+ if (record.agent !== undefined && record.agent !== 'claude')
27
+ continue;
28
+ const id = 'claude';
29
+ if (!known.has(id))
30
+ continue;
31
+ const payload = record.payload;
32
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload))
33
+ continue;
34
+ const sessionId = payload.session_id;
35
+ const transcriptPath = payload.transcript_path;
36
+ const driver = known.get(id);
37
+ if (typeof sessionId !== 'string' || !driver?.sessions?.isId(sessionId)
38
+ || typeof transcriptPath !== 'string' || !transcriptPath)
39
+ continue;
40
+ try {
41
+ await access(transcriptPath, constants.R_OK);
42
+ if (!(await stat(transcriptPath)).isFile())
43
+ continue;
44
+ }
45
+ catch {
46
+ continue;
47
+ }
48
+ bindings.set(paneRuntimeId, { id, sessionId, transcriptPath });
49
+ }
50
+ return bindings;
51
+ }
52
+ function topologyPanes(topology) {
53
+ return topology.windows.flatMap((window) => window.panes);
54
+ }
55
+ export async function readManagedCodexBindings(topology, { codexApp, codexSessions = codexSessionsDir(), agents = AGENTS, findRollout = resolveCodexRollout, access = fsp.access, stat = fsp.stat, } = {}) {
56
+ if (typeof codexApp?.discover !== 'function')
57
+ return new Map();
58
+ const panes = topologyPanes(topology);
59
+ const discovered = new Map(await Promise.all(panes.map(async (pane) => {
60
+ try {
61
+ const state = await codexApp.discover(pane.runtimeId);
62
+ return [pane.runtimeId, state?.managed && typeof state.threadId === 'string' ? state.threadId : null];
63
+ }
64
+ catch {
65
+ return [pane.runtimeId, null];
66
+ }
67
+ })));
68
+ const driver = agents.find((agent) => agent.id === 'codex');
69
+ if (!driver)
70
+ return new Map();
71
+ const bindings = new Map();
72
+ for (const pane of panes) {
73
+ const sessionId = discovered.get(pane.runtimeId);
74
+ if (typeof sessionId !== 'string' || !driver.sessions?.isId(sessionId))
75
+ continue;
76
+ let transcriptPath;
77
+ try {
78
+ transcriptPath = await findRollout(codexSessions, sessionId);
79
+ if (typeof transcriptPath !== 'string' || !transcriptPath)
80
+ continue;
81
+ await access(transcriptPath, constants.R_OK);
82
+ if (!(await stat(transcriptPath)).isFile())
83
+ continue;
84
+ }
85
+ catch {
86
+ continue;
87
+ }
88
+ bindings.set(pane.runtimeId, { id: 'codex', sessionId, transcriptPath });
89
+ }
90
+ return bindings;
91
+ }
92
+ export function attachBindings(topology, bindings) {
93
+ return {
94
+ ...topology,
95
+ windows: topology.windows.map((window) => ({
96
+ ...window,
97
+ panes: window.panes.map((pane) => ({ ...pane, agent: bindings.get(pane.runtimeId) || null })),
98
+ })),
99
+ };
100
+ }
101
+ function isUnknownFingerprint(value) {
102
+ return value === null || value === undefined
103
+ || (typeof value === 'object' && 'status' in value && value.status === 'unknown');
104
+ }
105
+ export async function captureWorkspace({ tmux, stateFile, environment, agents = AGENTS, readFile = fsp.readFile, access = fsp.access, stat = fsp.stat, codexApp, codexSessions = codexSessionsDir(), findCodexRollout = resolveCodexRollout, now = Date.now, }) {
106
+ try {
107
+ const before = await tmux.topologyFingerprint();
108
+ if (isUnknownFingerprint(before))
109
+ return { status: 'unknown' };
110
+ const topology = await tmux.captureTopology();
111
+ if (!topology || topology.status === 'unknown')
112
+ return { status: 'unknown' };
113
+ const bindings = await readAgentBindings(stateFile, agents, readFile, access, stat);
114
+ const managedCodex = await readManagedCodexBindings(topology, {
115
+ ...(codexApp ? { codexApp } : {}), codexSessions, agents,
116
+ findRollout: findCodexRollout, access, stat,
117
+ });
118
+ for (const [pane, binding] of managedCodex)
119
+ bindings.set(pane, binding);
120
+ const after = await tmux.topologyFingerprint();
121
+ if (isUnknownFingerprint(after))
122
+ return { status: 'unknown' };
123
+ if (before !== after)
124
+ return { status: 'changed-during-capture' };
125
+ const snapshot = canonicalizeSnapshot(attachBindings({
126
+ schemaVersion: WORKSPACE_SCHEMA_VERSION,
127
+ capturedAt: new Date(now()).toISOString(),
128
+ environment: { ...environment },
129
+ tmuxVersion: topology.tmuxVersion,
130
+ active: topology.active,
131
+ sessions: topology.sessions,
132
+ windows: topology.windows,
133
+ }, bindings));
134
+ return { status: topology.status === 'empty' ? 'empty' : 'ok', snapshot };
135
+ }
136
+ catch {
137
+ return { status: 'unknown' };
138
+ }
139
+ }