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,158 @@
1
+ const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
2
+ const RESTORE_REQUEST_FIELDS = new Set(['checkpointId', 'sessions']);
3
+ const PROTECTION_STATUSES = ['protected', 'unprotected', 'degraded'];
4
+ const PLAN_ACTIONS = ['create', 'create-renamed', 'already-present', 'unsupported'];
5
+ const OPERATION_STATUSES = ['pending', 'running', 'succeeded', 'partial', 'failed', 'interrupted'];
6
+ const RESULT_STATUSES = ['restored', 'already-present', 'failed'];
7
+ const RESULT_STAGES = ['plan', 'topology', 'agent', 'restore', 'reconcile'];
8
+ const recordOf = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value)
9
+ ? value : null);
10
+ const memberOf = (values, value) => (typeof value === 'string' && values.includes(value) ? value : undefined);
11
+ const finiteNumber = (value) => (typeof value === 'number' && Number.isFinite(value) ? value : undefined);
12
+ const optionalString = (value) => (typeof value === 'string' ? value : value === null ? null : undefined);
13
+ const stringArray = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []);
14
+ const optionalField = (key, value) => (value === undefined ? {} : { [key]: value });
15
+ export function isWorkspaceSafeId(value) {
16
+ return typeof value === 'string' && SAFE_ID.test(value) && value !== '.' && value !== '..';
17
+ }
18
+ export function parseWorkspaceRestoreRequest(value, defaultCheckpointId = 'latest') {
19
+ const request = recordOf(value);
20
+ if (!request || Object.keys(request).some((key) => !RESTORE_REQUEST_FIELDS.has(key)))
21
+ return null;
22
+ const checkpointId = request.checkpointId === undefined
23
+ ? defaultCheckpointId
24
+ : request.checkpointId;
25
+ if (!isWorkspaceSafeId(checkpointId))
26
+ return null;
27
+ const parsed = { checkpointId };
28
+ if (request.sessions !== undefined) {
29
+ if (!Array.isArray(request.sessions)
30
+ || request.sessions.some((name) => typeof name !== 'string' || !name || name.length > 256
31
+ || /[\x00-\x1f\x7f]/.test(name)))
32
+ return null;
33
+ parsed.sessions = [...request.sessions];
34
+ }
35
+ return parsed;
36
+ }
37
+ export function parseWorkspaceProtectionStatus(value) {
38
+ const status = recordOf(value);
39
+ if (!status)
40
+ return null;
41
+ return {
42
+ ...optionalField('status', memberOf(PROTECTION_STATUSES, status.status)),
43
+ ...optionalField('lastSuccessfulCaptureAt', optionalString(status.lastSuccessfulCaptureAt)),
44
+ ...optionalField('errorCode', optionalString(status.errorCode)),
45
+ };
46
+ }
47
+ export function parseWorkspaceRestoreStart(value) {
48
+ const start = recordOf(value);
49
+ if (!start)
50
+ return null;
51
+ return {
52
+ ...optionalField('operationId', typeof start.operationId === 'string' ? start.operationId : undefined),
53
+ ...optionalField('status', memberOf(OPERATION_STATUSES, start.status)),
54
+ ...optionalField('reused', typeof start.reused === 'boolean' ? start.reused : undefined),
55
+ };
56
+ }
57
+ const countsOf = (value) => {
58
+ const counts = recordOf(value);
59
+ if (!counts)
60
+ return undefined;
61
+ return {
62
+ ...optionalField('sessions', finiteNumber(counts.sessions)),
63
+ ...optionalField('windows', finiteNumber(counts.windows)),
64
+ ...optionalField('panes', finiteNumber(counts.panes)),
65
+ ...optionalField('agents', finiteNumber(counts.agents)),
66
+ };
67
+ };
68
+ const planSessionOf = (value) => {
69
+ const session = recordOf(value);
70
+ return session ? {
71
+ ...optionalField('logicalId', optionalString(session.logicalId)),
72
+ ...optionalField('sourceName', optionalString(session.sourceName)),
73
+ ...optionalField('targetName', optionalString(session.targetName)),
74
+ ...optionalField('action', session.action === null ? null : memberOf(PLAN_ACTIONS, session.action)),
75
+ } : null;
76
+ };
77
+ export function parseWorkspaceRecoveryPlan(value) {
78
+ const plan = recordOf(value);
79
+ if (!plan)
80
+ return null;
81
+ const rawPlanSummary = recordOf(plan.planSummary);
82
+ const summary = countsOf(plan.summary);
83
+ const planSummary = rawPlanSummary ? {
84
+ ...countsOf(rawPlanSummary),
85
+ ...optionalField('create', finiteNumber(rawPlanSummary.create)),
86
+ ...optionalField('renamed', finiteNumber(rawPlanSummary.renamed)),
87
+ ...optionalField('alreadyPresent', finiteNumber(rawPlanSummary.alreadyPresent)),
88
+ ...optionalField('unsupported', finiteNumber(rawPlanSummary.unsupported)),
89
+ } : undefined;
90
+ const pendingCount = finiteNumber(plan.pendingCount)
91
+ ?? (plan.pendingCount === null ? null : undefined);
92
+ return {
93
+ ...optionalField('checkpointId', optionalString(plan.checkpointId)),
94
+ ...optionalField('capturedAt', optionalString(plan.capturedAt)),
95
+ ...optionalField('changeReason', optionalString(plan.changeReason)),
96
+ ...optionalField('serverNow', optionalString(plan.serverNow)),
97
+ ...optionalField('expiresAt', optionalString(plan.expiresAt)),
98
+ ...optionalField('promptEligible', typeof plan.promptEligible === 'boolean' ? plan.promptEligible : undefined),
99
+ ...optionalField('resolved', typeof plan.resolved === 'boolean' ? plan.resolved : undefined),
100
+ ...optionalField('pendingCount', pendingCount),
101
+ ...optionalField('summary', summary),
102
+ ...optionalField('planSummary', planSummary),
103
+ ...(Array.isArray(plan.sessions) ? { sessions: plan.sessions.flatMap((session) => {
104
+ const parsed = planSessionOf(session);
105
+ return parsed ? [parsed] : [];
106
+ }) } : {}),
107
+ ...(Array.isArray(plan.warningCodes) ? { warningCodes: stringArray(plan.warningCodes) } : {}),
108
+ ...(plan.mapping !== undefined ? { mapping: plan.mapping } : {}),
109
+ };
110
+ }
111
+ export function parseWorkspaceRestoreOperation(value) {
112
+ const operation = recordOf(value);
113
+ if (!operation)
114
+ return null;
115
+ const progress = recordOf(operation.progress);
116
+ const request = recordOf(operation.request);
117
+ const results = Array.isArray(operation.results)
118
+ ? operation.results.flatMap((candidate) => {
119
+ const result = recordOf(candidate);
120
+ const session = planSessionOf(candidate);
121
+ if (!result || !session)
122
+ return [];
123
+ return [{
124
+ ...session,
125
+ ...optionalField('status', result.status === null ? null : memberOf(RESULT_STATUSES, result.status)),
126
+ ...optionalField('stage', result.stage === null ? null : memberOf(RESULT_STAGES, result.stage)),
127
+ ...optionalField('errorCode', optionalString(result.errorCode)),
128
+ ...optionalField('errorMessage', optionalString(result.errorMessage)),
129
+ warningCodes: stringArray(result.warningCodes),
130
+ }];
131
+ }) : [];
132
+ const parsedProgress = progress ? {
133
+ ...optionalField('completed', finiteNumber(progress.completed)),
134
+ ...optionalField('total', finiteNumber(progress.total)),
135
+ } : undefined;
136
+ const checkpointId = typeof request?.checkpointId === 'string' ? request.checkpointId : undefined;
137
+ const parsedRequest = request && checkpointId ? {
138
+ checkpointId,
139
+ ...(Array.isArray(request.sessions) ? { sessions: stringArray(request.sessions) } : {}),
140
+ ...(typeof request.historical === 'boolean' ? { historical: request.historical } : {}),
141
+ } : undefined;
142
+ return {
143
+ ...(typeof operation.id === 'string' ? { id: operation.id } : {}),
144
+ ...optionalField('status', operation.status === null ? null : memberOf(OPERATION_STATUSES, operation.status)),
145
+ ...optionalField('request', parsedRequest),
146
+ ...optionalField('createdAt', optionalString(operation.createdAt)),
147
+ ...optionalField('updatedAt', optionalString(operation.updatedAt)),
148
+ ...optionalField('startedAt', optionalString(operation.startedAt)),
149
+ ...optionalField('completedAt', optionalString(operation.completedAt)),
150
+ ...optionalField('progress', parsedProgress),
151
+ ...optionalField('summary', countsOf(operation.summary)),
152
+ ...(Array.isArray(operation.results) ? { results } : {}),
153
+ ...optionalField('errorCode', optionalString(operation.errorCode)),
154
+ ...optionalField('errorMessage', optionalString(operation.errorMessage)),
155
+ ...(Array.isArray(operation.warningCodes) ? { warningCodes: stringArray(operation.warningCodes) } : {}),
156
+ ...(operation.mapping !== undefined ? { mapping: operation.mapping } : {}),
157
+ };
158
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "handmux",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "A mobile vibe-coding cockpit — built on tmux: drive your live session, Claude Code / Codex — anything a terminal can run — from your phone.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -22,27 +22,32 @@
22
22
  "pwa"
23
23
  ],
24
24
  "engines": {
25
- "node": ">=20"
25
+ "node": ">=22.16.0"
26
26
  },
27
27
  "bin": {
28
28
  "handmux": "bin/handmux.js"
29
29
  },
30
30
  "files": [
31
- "bin",
32
- "src",
33
- "hooks",
34
- "public",
35
- "README.zh-CN.md"
31
+ "bin/handmux.js",
32
+ "bin/node-version.js",
33
+ "dist",
34
+ "README.md",
35
+ "README.zh-CN.md",
36
+ "LICENSE"
36
37
  ],
37
38
  "scripts": {
38
- "start": "node bin/handmux.js start",
39
- "dev": "node bin/handmux.js start --foreground",
40
- "handmux": "node bin/handmux.js",
39
+ "start": "npm run build:server && node dist/bin/handmux.js start",
40
+ "dev": "npm run build:server && node dist/bin/handmux.js start --foreground",
41
+ "handmux": "npm run build:server && node dist/bin/handmux.js",
42
+ "build:server": "node scripts/build-server.mjs",
41
43
  "bundle": "node scripts/bundle-web.mjs",
42
- "prepack": "node scripts/bundle-web.mjs",
43
- "test": "vitest run",
44
- "test:workspace-host": "HANDMUX_WORKSPACE_HOST=1 vitest run test/workspaceHost.test.js",
45
- "test:workspace-docker": "HANDMUX_WORKSPACE_DOCKER=1 vitest run test/workspaceDocker.test.js"
44
+ "prepack": "npm run bundle && npm run build:server",
45
+ "typecheck:source": "node scripts/check-typescript-product-source.mjs",
46
+ "typecheck": "tsc --project tsconfig.json",
47
+ "test": "npm run build:server && vitest run",
48
+ "test:pack": "node scripts/pack-smoke.mjs",
49
+ "test:workspace-host": "npm run build:server && HANDMUX_WORKSPACE_HOST=1 vitest run test/workspaceHost.test.js",
50
+ "test:workspace-docker": "npm run build:server && HANDMUX_WORKSPACE_DOCKER=1 vitest run test/workspaceDocker.test.js"
46
51
  },
47
52
  "dependencies": {
48
53
  "@clack/prompts": "0.11.0",
@@ -58,17 +63,25 @@
58
63
  "qrcode-terminal": "^0.12.0"
59
64
  },
60
65
  "devDependencies": {
66
+ "@types/busboy": "^1.5.4",
67
+ "@types/compression": "^1.8.1",
68
+ "@types/express": "4.17.25",
69
+ "@types/node": "22.20.1",
70
+ "@types/supertest": "7.2.1",
71
+ "@types/web-push": "^3.6.4",
72
+ "@types/ws": "^8.18.1",
61
73
  "supertest": "^7.0.0",
74
+ "typescript": "6.0.3",
62
75
  "vitest": "^2.0.0"
63
76
  },
64
77
  "whatsNew": [
78
+ {"version":"0.25.0","date":"2026-09-01","zh":"Pi 原生接入 · 多 Agent 对话更可靠","en":"Native Pi support · more reliable agent chat"},
79
+ {"version":"0.24.0","date":"2026-08-11","zh":"Codex 任务进度与 Goal 原生上线","en":"Native Codex task progress and Goals"},
65
80
  {"version":"0.23.0","date":"2026-08-09","zh":"Codex 托管对话正式上线","en":"Managed Codex chat is here"},
66
81
  {"version":"0.22.1","date":"2026-08-04","zh":"网页宽度与网站版本分开控制","en":"Separate page width and site-version controls"},
67
82
  {"version":"0.22.0","date":"2026-08-03","zh":"全屏设置 · 窗口与分屏管理更清晰","en":"Full-screen Settings · clearer window and pane management"},
68
83
  {"version":"0.21.0","date":"2026-08-03","zh":"网页预览:手机直连、电脑代理与静态目录","en":"Web preview: direct, computer proxy, and static folders"},
69
84
  {"version":"0.20.3","date":"2026-08-01","zh":"实时终端显示更稳定流畅","en":"More stable, smoother live terminals"},
70
- {"version":"0.20.2","date":"2026-07-27","zh":"实时终端长时间运行更流畅","en":"Smoother long-running live terminals"},
71
- {"version":"0.20.1","date":"2026-07-26","zh":"聊天发送修复 · 电脑键盘不中断","en":"Reliable chat sending · uninterrupted desktop input"},
72
- {"version":"0.20.0","date":"2026-07-26","zh":"终端实时推送 · 弱网自动回退","en":"Live terminal pushing · automatic network fallback"}
85
+ {"version":"0.20.2","date":"2026-07-27","zh":"实时终端长时间运行更流畅","en":"Smoother long-running live terminals"}
73
86
  ]
74
87
  }
@@ -1,22 +0,0 @@
1
- #!/bin/sh
2
- # handmux 上报 hook. $1 = stop | notify | prompt | end | resume | permreq | compacting | compact | stopfail.
3
- # (compacting/compact/stopfail 是版本门控事件:PreCompact/PostCompact/StopFailure,只在够新的 Claude 上注册。)
4
- # stdin = Claude Code 原始 payload(JSON).
5
- # (resume = PostToolUse on AskUserQuestion/ExitPlanMode:答完选项/批准计划 → 状态翻回进行中、带所选项。)
6
- # (permreq = PermissionRequest:真实弹框一出现就发、带 tool_name → 比 permission_prompt 早亮「需要你」。)
7
- # 只做一件事:把本次事件写进一个本地 JSON 状态文件(键=tmux pane,值=该 pane 最新事件)。不联网、
8
- # 不依赖服务进程是否在跑 → 永不阻塞 Claude(始终 exit 0)。服务端按需读这个文件(见 claudeEvents.js)。
9
- # 真正的读-改-写交给同目录的 handmux-write.js(node:真 JSON 解析 + 文件锁,多 pane 并发 hook 不丢更新)。
10
- CFG="$(dirname "$0")/handmux-notify.env"
11
- [ -f "$CFG" ] && . "$CFG"
12
- PANE="${CLAUDE_PANE:-$TMUX_PANE}"
13
- [ -z "$PANE" ] && exit 0 # 不在 tmux 里(没有 pane 可定位)→ 暂不记录
14
- FILE="${HANDMUX_STATE:-$HOME/.claude/handmux-state.json}"
15
- # 毫秒时间戳:与旧实现的 Date.now() 同单位(客户端已阅水位线兼容)。优先 perl,退化到 秒×1000。
16
- TS=$(perl -MTime::HiRes -e 'printf "%.0f", Time::HiRes::time()*1000' 2>/dev/null)
17
- [ -z "$TS" ] && TS=$(( $(date +%s) * 1000 ))
18
- HOST=$(hostname 2>/dev/null || printf '')
19
- # payload 经 stdin 原样流给 node(不在 shell 里转义,避免坏数据);pane 含 '%' 直接进 JSON 字段,
20
- # 不再进 URL → 彻底告别旧的 "%110 被 url-decode 丢弃" 那类坑。
21
- node "$(dirname "$0")/handmux-write.cjs" "$FILE" "$PANE" "$1" "$TS" "$HOST" 2>/dev/null || true
22
- exit 0
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env node
2
- // handmux hook writer. Updates ONE JSON state file keyed by tmux pane id with this pane's latest
3
- // Claude event: { "%pane": { ts, src, host, payload }, ... }. Invoked by handmux-notify.sh:
4
- // node handmux-write.cjs <file> <pane> <src> <ts> <host> (stdin = Claude's raw hook payload JSON)
5
- //
6
- // .cjs (not .js): runs standalone via `node <file>`, including from ~/.claude/hooks (no package.json) —
7
- // .cjs forces CommonJS everywhere; a bare .js under the server tree ("type":"module") would be ESM and
8
- // break require().
9
- //
10
- // Why node (not pure shell): the file is a single JSON object, so each event is a read-modify-write that
11
- // must parse JSON and not clobber other panes — shell can't do that safely. The user runs many Claude
12
- // panes at once, so hooks fire concurrently: we take a short O_EXCL lock (stealing a stale one) around
13
- // the read-modify-write and replace atomically (tmp + rename), so concurrent writers don't lose updates
14
- // or corrupt the file. Best-effort throughout and silent — the hook is fire-and-forget and must never
15
- // fail Claude (the shell wrapper swallows errors and always exits 0).
16
- const fs = require('node:fs');
17
-
18
- const [, , file, pane, src, ts, host = ''] = process.argv;
19
- if (!file || !pane || !src) process.exit(0);
20
-
21
- let payload = {};
22
- try { payload = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch { /* unreadable stdin → {} */ }
23
-
24
- // idle_prompt ("been idle ~60s") is decided in update() against the pane's PRIOR state: it either trails
25
- // a resting state (done/needs → drop, so it can't bump ts and re-surface an already-cleared 已完成) or it
26
- // terminates an ESC-interrupted working turn (→ clear the stuck 进行中). Flag it here; the read-modify-
27
- // write under the lock — the only place we can read the prior state safely — makes the call.
28
- const isIdle = src === 'notify' && payload && payload.notification_type === 'idle_prompt';
29
-
30
- // Synchronous nap without busy-spinning (the hook runs async, so a few ms is free). SharedArrayBuffer
31
- // may be unavailable in odd runtimes — fall back to a tiny busy loop so the lock retry still paces.
32
- const nap = (ms) => {
33
- try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
34
- catch { const end = Date.now() + ms; while (Date.now() < end) { /* spin */ } }
35
- };
36
-
37
- try { fs.mkdirSync(path.dirname(file), { recursive: true }); } catch { /* ignore */ }
38
-
39
- function update() {
40
- let obj = {};
41
- try { const j = JSON.parse(fs.readFileSync(file, 'utf8')); if (j && typeof j === 'object' && !Array.isArray(j)) obj = j; }
42
- catch { /* fresh / corrupt / half-written → start clean */ }
43
- const prevSrc = obj[pane] && obj[pane].src;
44
- if (isIdle) {
45
- // idle after a resting state (done/needs/nothing) is just the "still waiting" reminder → drop and
46
- // leave the file as it was (recording it would bump ts and re-surface an already-cleared 已完成).
47
- // idle after a WORKING turn (prompt/resume) that never got a Stop = an ESC interrupt / walk-away —
48
- // no Stop hook fires there, so idle is the only signal the turn ended. Without this the pane would
49
- // stay latched at 进行中 forever; treat it as a soft end and clear the pane.
50
- if (prevSrc === 'prompt' || prevSrc === 'resume') { delete obj[pane]; }
51
- else return; // resting → drop without writing
52
- } else if (src === 'end') {
53
- // SessionEnd drops the pane on a clean exit. But /clear (and /resume) END the old session AND START a
54
- // new one — SessionEnd(old) + SessionStart(new) fire as two async hooks, in either order. A late end for
55
- // the OLD session must NOT wipe the NEW session's binding: only drop when what we currently hold IS the
56
- // ending session (or neither side carries a session_id to compare — preserve the plain clean-exit drop).
57
- const curSid = obj[pane] && obj[pane].payload && obj[pane].payload.session_id;
58
- const endSid = payload && payload.session_id;
59
- if (!curSid || !endSid || curSid === endSid) delete obj[pane];
60
- } else {
61
- obj[pane] = { ts: Number(ts) || 0, src, host, payload };
62
- }
63
- const tmp = `${file}.${process.pid}.tmp`;
64
- fs.writeFileSync(tmp, JSON.stringify(obj));
65
- fs.renameSync(tmp, file); // atomic: a torn write can't corrupt the file
66
- }
67
-
68
- const lock = `${file}.lock`;
69
- let held = false;
70
- for (let i = 0; i < 60 && !held; i++) { // ~0.9s budget, then write lockless (best-effort)
71
- try { fs.closeSync(fs.openSync(lock, 'wx')); held = true; } // O_EXCL → atomic "I hold it"
72
- catch {
73
- try { if (Date.now() - fs.statSync(lock).mtimeMs > 3000) fs.unlinkSync(lock); } catch { /* steal a stale lock */ }
74
- nap(15);
75
- }
76
- }
77
- try { update(); } catch { /* best effort */ }
78
- if (held) { try { fs.unlinkSync(lock); } catch { /* ignore */ } }