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,315 @@
1
+ import express from 'express';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { loadConfig } from './config.js';
6
+ import { expressAuth, loadToken } from './auth.js';
7
+ import { createApiRouter } from './httpApi.js';
8
+ import { loadUploadExts } from './uploadTypes.js';
9
+ import { createClaudeEvents } from './claudeEvents.js';
10
+ import { syncHooks } from './cli/claudeHooks.js';
11
+ import { syncPiExtension } from './cli/piExtension.js';
12
+ import { removeLegacyCodexHooks } from './cli/legacyCodexHooks.js';
13
+ import { agentRuntimeDirectoryPath, claudeStatePath, codexOutboxPath, } from './cli/state.js';
14
+ import * as commands from './tmux/commands.js';
15
+ import * as push from './push.js';
16
+ import { cacheControlFor } from './staticCache.js';
17
+ import { compressStaticAssets } from './staticCompression.js';
18
+ import { applyAppName, applyManifestName } from './appName.js';
19
+ import { homedir } from 'node:os';
20
+ import { createPreviews } from './previews.js';
21
+ import { createPreview } from './previewServer.js';
22
+ import { createWorkspaceStore } from './workspace/store.js';
23
+ import { createWorkspaceTmux } from './workspace/tmuxAdapter.js';
24
+ import { createEnvironmentProvider } from './workspace/environment.js';
25
+ import { createWorkspaceLock } from './workspace/lock.js';
26
+ import { createGracefulShutdown, createWorkspaceBackground } from './workspace/checkpointer.js';
27
+ import { createWorkspaceRuntime } from './workspace/runtime.js';
28
+ import { createBrowserWorkerClient } from './browser/workerClient.js';
29
+ import { createTerminalStream } from './terminalStream.js';
30
+ import { createCodexAppServer } from './codexAppServer.js';
31
+ import { migrateLegacyCodexOutbox } from './agent-runtime/migrateLegacyCodexOutbox.js';
32
+ import { apiErrorBoundary, apiRequestContext } from './apiErrors.js';
33
+ import { RuntimeHealth } from './healthProtocol.js';
34
+ import { healthRoutes } from './routes/health.js';
35
+ import { createBuiltinAgentRuntime } from './agent-runtime/builtinRuntime.js';
36
+ import { interruptPane, sendPaneChoice, sendPanePrompt } from './paneInput.js';
37
+ import { createLocalAgentProcessContext, TmuxAgentPaneSource, } from './agent-runtime/tmuxRuntime.js';
38
+ import { InboxPushProjection } from './agent-runtime/inboxPushProjection.js';
39
+ import { defaultGit } from './git.js';
40
+ import { createProjectTaskRuntime } from './projectTask/runtime.js';
41
+ import { clearCodexConversationThroughTui } from './agents/codexTerminalControl.js';
42
+ import { ApiAccountService, apiAccountsPath } from './apiAccounts.js';
43
+ import { ClaudeHookBridgeConnector } from '../connectors/claude/index.js';
44
+ const here = path.dirname(fileURLToPath(import.meta.url));
45
+ // There is ONE config source — ~/.handmux/config.json — and ONE way to run handmux: `handmux start`
46
+ // (the CLI; `node bin/handmux.js start` from a source checkout is the same thing). The CLI resolves
47
+ // the config and spawns this server with everything it needs in the environment, so the server reads
48
+ // only process.env here (no .env files, no NODE_ENV branching). Running this file directly is not a
49
+ // supported entry point — go through the CLI.
50
+ const cfg = loadConfig();
51
+ const token = loadToken();
52
+ const uploadExts = loadUploadExts();
53
+ const home = homedir();
54
+ const apiAccounts = new ApiAccountService({ file: apiAccountsPath(home) });
55
+ const previewDomain = process.env.HANDMUX_PREVIEW_DOMAIN || null;
56
+ const health = new RuntimeHealth({ browserRequired: Boolean(previewDomain) });
57
+ const projectTask = await createProjectTaskRuntime({
58
+ home,
59
+ storeOptions: {
60
+ resolveRepositoryRoot: async (rootPath) => {
61
+ const result = await defaultGit.worktree(rootPath);
62
+ return 'error' in result ? null : result.worktree?.path ?? null;
63
+ },
64
+ },
65
+ });
66
+ if (projectTask.status().status === 'unavailable') {
67
+ console.warn(`[handmux] Project Task unavailable: ${projectTask.status().error?.message ?? 'unknown error'}`);
68
+ }
69
+ // One writer set is shared by background capture today and the restore runtime added on top of it. The
70
+ // lock is filesystem-backed because the CLI may restore while this daemon is alive in another process.
71
+ const workspaceStore = createWorkspaceStore({ home });
72
+ const workspaceTmux = createWorkspaceTmux({ run: commands.runTmux });
73
+ const workspaceLock = createWorkspaceLock({ dir: workspaceStore.paths.lockDir });
74
+ const observeEnvironment = createEnvironmentProvider({
75
+ tmuxServerIdProvider: async () => {
76
+ const observed = await workspaceTmux.observeEnvironment();
77
+ if (observed.status === 'unknown')
78
+ throw new Error('tmux environment unavailable');
79
+ return observed.tmuxServerId;
80
+ },
81
+ });
82
+ const stateFile = process.env.CLAUDE_STATE_FILE || claudeStatePath(home);
83
+ let codexApp = null;
84
+ const agentRuntimeDirectory = agentRuntimeDirectoryPath(home);
85
+ const legacyCodexOutbox = codexOutboxPath(home);
86
+ let conversationStartupBlockReason;
87
+ try {
88
+ migrateLegacyCodexOutbox(legacyCodexOutbox, path.join(agentRuntimeDirectory, 'conversation-state.json'));
89
+ }
90
+ catch (error) {
91
+ const detail = error instanceof Error ? error.message : String(error);
92
+ conversationStartupBlockReason = `Codex queue migration requires attention: ${detail}`;
93
+ console.warn(`[handmux] Conversation dispatch disabled: ${conversationStartupBlockReason}`);
94
+ }
95
+ const workspaceBackground = createWorkspaceBackground({
96
+ store: workspaceStore,
97
+ tmux: workspaceTmux,
98
+ observeEnvironment,
99
+ lock: workspaceLock,
100
+ stateFile,
101
+ getCodexApp: () => codexApp,
102
+ });
103
+ const workspace = createWorkspaceRuntime({
104
+ store: workspaceStore,
105
+ tmux: workspaceTmux,
106
+ lock: workspaceLock,
107
+ checkpointer: workspaceBackground,
108
+ home,
109
+ });
110
+ // Claude states come from Hooks; Codex states come only from App Server. Both feed the same inbox and
111
+ // workspace checkpointer, but no Codex Hook state is accepted as a fallback.
112
+ let events = null;
113
+ codexApp = createCodexAppServer({
114
+ home,
115
+ // Conversation Core is the only queue owner. The legacy file is migrated above and retained as a backup.
116
+ outboxStore: null,
117
+ onStateChange: () => {
118
+ if (!events)
119
+ return;
120
+ events.getStates().then(() => workspace.requestReconcile()).catch(() => { });
121
+ },
122
+ });
123
+ // Legacy Hook watching still drives workspace reconciliation and compatibility reads. Canonical Agent
124
+ // notifications are delivered below from Inbox Core, so this reader no longer owns Web Push side effects.
125
+ events = createClaudeEvents({ commands, codexApp, file: stateFile, onStateChange: workspace.requestReconcile });
126
+ events.start();
127
+ try {
128
+ codexApp.start();
129
+ health.set('codex', 'ready');
130
+ }
131
+ catch {
132
+ health.set('codex', 'degraded', 'codex-start-failed');
133
+ }
134
+ const agentPanes = new TmuxAgentPaneSource({ commands });
135
+ const agentProcess = createLocalAgentProcessContext();
136
+ const agentRuntime = createBuiltinAgentRuntime({
137
+ home,
138
+ panes: agentPanes,
139
+ process: agentProcess,
140
+ stateDirectory: agentRuntimeDirectory,
141
+ ...(conversationStartupBlockReason === undefined ? {} : { conversationStartupBlockReason }),
142
+ claudeEvents: events,
143
+ claudeConversationControl: {
144
+ sendPrompt: (paneId, text, guard) => sendPanePrompt(commands, paneId, text, guard),
145
+ interrupt: (paneId) => interruptPane(commands, paneId),
146
+ },
147
+ claudeInteractionControl: {
148
+ capturePlain: (paneId) => commands.capturePlain(paneId),
149
+ sendChoice: (paneId, choice) => sendPaneChoice(commands, paneId, choice),
150
+ pendingKind: (paneId) => events?.paneKind(paneId) ?? null,
151
+ },
152
+ codexApp,
153
+ codexActivationCommands: {
154
+ sendKey: (pane, key) => commands.sendKey(pane, key),
155
+ capturePlain: (pane) => commands.capturePlain(pane),
156
+ runPaneCommand: (pane, command) => commands.runPaneCommand(pane, command),
157
+ },
158
+ codexClear: async (pane, threadId) => {
159
+ await clearCodexConversationThroughTui(codexApp, commands, pane, threadId);
160
+ },
161
+ });
162
+ const claudeInboxBridge = new ClaudeHookBridgeConnector({
163
+ socketPath: agentRuntime.socketPath,
164
+ credentialFile: path.join(agentRuntimeDirectory, 'bridge-credential.json'),
165
+ stateDirectory: path.join(agentRuntimeDirectory, 'connectors', 'claude'),
166
+ hookStateFile: stateFile,
167
+ eventDirectory: `${stateFile}.events`,
168
+ panes: agentPanes,
169
+ process: agentProcess,
170
+ logger: (message, error) => {
171
+ const detail = error === undefined
172
+ ? '' : `: ${error instanceof Error ? error.message : String(error)}`;
173
+ console.warn(`[handmux] ${message}${detail}`);
174
+ },
175
+ });
176
+ const inboxPush = new InboxPushProjection({
177
+ inbox: agentRuntime.inbox,
178
+ runs: agentRuntime.runs,
179
+ panes: agentPanes,
180
+ push,
181
+ });
182
+ inboxPush.start();
183
+ agentRuntime.start().catch((error) => {
184
+ console.warn(`[handmux] Agent Runtime unavailable: ${error instanceof Error ? error.message : String(error)}`);
185
+ });
186
+ claudeInboxBridge.start();
187
+ workspace.start().catch(() => { });
188
+ // Keep an already-opted-in user's Claude hooks in step with this handmux version on restart: newly-added
189
+ // lifecycle events (e.g. SessionStart, which rebinds the 对话 lens after /clear) and a refreshed
190
+ // handmux-write.cjs land via `./deploy.sh` alone — no phone re-enable. A strict no-op unless our hooks are
191
+ // already installed; best-effort and must never block or crash startup (pure fs, no subprocess).
192
+ try {
193
+ syncHooks(home, {
194
+ srcDir: path.resolve(here, '../hooks'),
195
+ stateFile,
196
+ });
197
+ }
198
+ catch { /* best effort — hook sync never fails startup */ }
199
+ // Keep an explicitly installed Pi wrapper pointed at this package version (notably across Homebrew
200
+ // Cellar upgrades). Absent or foreign extensions are untouched; this never opts a user in silently.
201
+ try {
202
+ syncPiExtension(home, { entryFile: path.resolve(here, '../connectors/pi/index.js') });
203
+ }
204
+ catch { /* best effort — Pi integration never fails startup */ }
205
+ // Versions before App Server support installed a marked Handmux block under ~/.codex. Remove only that
206
+ // exact legacy block and Handmux-owned files; all user Codex configuration and third-party hooks remain.
207
+ try {
208
+ removeLegacyCodexHooks(home);
209
+ }
210
+ catch { /* best effort — migration never fails startup */ }
211
+ // Static directory preview remains for folders without a web server. Arbitrary sites and local ports
212
+ // use the built-in browser below; previewDomain may provide its dedicated public origin.
213
+ const previews = createPreviews({ home });
214
+ const preview = createPreview({ previews });
215
+ const handmuxOrigin = (() => {
216
+ try {
217
+ return new URL(process.env.HANDMUX_PUBLIC_URL || `http://127.0.0.1:${cfg.port}`).origin;
218
+ }
219
+ catch {
220
+ return `http://127.0.0.1:${cfg.port}`;
221
+ }
222
+ })();
223
+ const browserWorker = createBrowserWorkerClient({ appToken: token, previewDomain, handmuxOrigin });
224
+ const app = express();
225
+ app.use(healthRoutes({
226
+ health,
227
+ refresh: async () => {
228
+ const browser = browserWorker.health();
229
+ health.set('browser', browser.status, browser.detail);
230
+ const workspaceStatus = workspace.health();
231
+ health.set('workspace', workspaceStatus.status, workspaceStatus.detail);
232
+ },
233
+ }));
234
+ // Browser proxy leases stay behind normal Handmux auth. Client-owned direct tabs never enter
235
+ // server state; proxy operations and all claimed Hammerhead paths use the isolated worker.
236
+ app.use(browserWorker.publicHandler);
237
+ app.use('/api/browser-proxy', apiRequestContext(), expressAuth(token), express.json(), browserWorker.apiHandler);
238
+ app.use('/api', createApiRouter({
239
+ token, events, uploadExts, previews, shortcuts: cfg.shortcuts, workspace, previewDomain,
240
+ agentRuntime, projectTask, apiAccounts,
241
+ }));
242
+ app.use('/preview', preview.router);
243
+ app.use(preview.refererFallback);
244
+ // Serve the built web client so one process hosts both the API and the frontend (single origin, no dev
245
+ // proxy). Prefer the bundled copy inside the package (server/public — what `npm publish` ships); fall back
246
+ // to the sibling web/dist of a source checkout. Override either with HANDMUX_STATIC_DIR.
247
+ const bundledDir = path.resolve(here, '../public');
248
+ const sourceDir = path.resolve(here, '../../web/dist');
249
+ const staticDir = process.env.HANDMUX_STATIC_DIR || (fs.existsSync(bundledDir) ? bundledDir : sourceDir);
250
+ const indexPath = path.join(staticDir, 'index.html');
251
+ // Optional custom instance name (handmux start --name). When set, the prebuilt shell + manifest are
252
+ // rewritten on the way out so the browser-tab title and "Add to Home Screen" label match the user's
253
+ // name — the bundle ships generic and is renamed at serve time, never rebuilt. Unset → serve as-is.
254
+ const appName = process.env.HANDMUX_APP_NAME || null;
255
+ let renamedIndex = null; // computed once; the name is fixed for the process lifetime
256
+ if (appName) {
257
+ app.get('/manifest.webmanifest', (req, res, next) => {
258
+ try {
259
+ const raw = fs.readFileSync(path.join(staticDir, 'manifest.webmanifest'), 'utf8');
260
+ res.type('application/manifest+json').send(JSON.stringify(applyManifestName(JSON.parse(raw), appName)));
261
+ }
262
+ catch {
263
+ next();
264
+ }
265
+ });
266
+ }
267
+ // index:false so the renamed shell below owns "/" too (otherwise static would serve the generic one).
268
+ app.use(compressStaticAssets);
269
+ app.use(express.static(staticDir, {
270
+ index: false,
271
+ // Cache-Control policy lives in staticCache.js (unit-tested): index.html + sw.js are never cached
272
+ // (stale-shell / stale-SW trap), content-hashed assets cache forever.
273
+ setHeaders: (res, filePath) => res.setHeader('Cache-Control', cacheControlFor(filePath)),
274
+ }));
275
+ // SPA fallback: serve index.html for any non-API GET (client routing lives in the URL hash, so
276
+ // the server only ever needs to hand back the one HTML shell). API 404s pass through untouched.
277
+ app.get('*', (req, res, next) => {
278
+ if (req.path.startsWith('/api/'))
279
+ return next();
280
+ res.setHeader('Cache-Control', 'no-store');
281
+ if (appName) {
282
+ try {
283
+ if (renamedIndex == null)
284
+ renamedIndex = applyAppName(fs.readFileSync(indexPath, 'utf8'), appName);
285
+ return res.type('html').send(renamedIndex);
286
+ }
287
+ catch { /* fall through to sendFile */ }
288
+ }
289
+ res.sendFile(indexPath);
290
+ });
291
+ app.use(apiErrorBoundary());
292
+ const server = app.listen(cfg.port, cfg.host, () => {
293
+ console.log(`[handmux] listening on http://${cfg.host}:${cfg.port} (serving ${staticDir})`);
294
+ });
295
+ const terminalStream = createTerminalStream({ token, commands });
296
+ server.on('upgrade', (req, socket, head) => {
297
+ if (terminalStream.onUpgrade(req, socket, head))
298
+ return;
299
+ if (!browserWorker.onUpgrade(req, socket, head))
300
+ socket.destroy();
301
+ });
302
+ const shutdown = createGracefulShutdown({ events, workspace, browser: browserWorker, server });
303
+ const handleSignal = () => {
304
+ terminalStream.close();
305
+ codexApp.close();
306
+ inboxPush.close();
307
+ Promise.all([
308
+ claudeInboxBridge.close(),
309
+ projectTask.close(),
310
+ agentRuntime.close(),
311
+ shutdown(),
312
+ ]).catch(() => { });
313
+ };
314
+ process.on('SIGINT', handleSignal);
315
+ process.on('SIGTERM', handleSignal);
@@ -0,0 +1,83 @@
1
+ import { isAllowedKey } from './keyNames.js';
2
+ const CTRL_C = Object.freeze({ type: 'key', key: 'C-c', label: 'Ctrl+C' });
3
+ export const DEFAULT_SHORTCUTS = Object.freeze({
4
+ command: Object.freeze([CTRL_C]),
5
+ chat: Object.freeze([
6
+ CTRL_C,
7
+ Object.freeze({ type: 'key', key: 'Escape', label: 'Esc' }),
8
+ Object.freeze({ type: 'key', key: 'Tab', label: 'Tab' }),
9
+ Object.freeze({ type: 'key', key: 'BSpace', label: '⌫' }),
10
+ Object.freeze({ type: 'text', text: 'ok', enter: true }),
11
+ Object.freeze({ type: 'text', text: 'go on', enter: true }),
12
+ Object.freeze({ type: 'text', text: '1', enter: true }),
13
+ Object.freeze({ type: 'text', text: '2', enter: true }),
14
+ Object.freeze({ type: 'text', text: '3', enter: true }),
15
+ Object.freeze({ type: 'text', text: '/compact', enter: true }),
16
+ Object.freeze({ type: 'text', text: '/clear', enter: true }),
17
+ Object.freeze({ type: 'text', text: '/model', enter: true }),
18
+ ]),
19
+ });
20
+ const cloneItems = (items) => items.map((item) => ({ ...item }));
21
+ const fail = (path, message) => { throw new Error(`${path}: ${message}`); };
22
+ const isSingleLine = (value, max) => (typeof value === 'string' && value.length > 0 && value.length <= max
23
+ && [...value].every((char) => {
24
+ const code = char.charCodeAt(0);
25
+ return code >= 0x20 && code !== 0x7f;
26
+ }));
27
+ export function shortcutIdentity(item) {
28
+ return item.type === 'key' ? `key:${item.key}` : `text:${item.enter ? 1 : 0}:${item.text}`;
29
+ }
30
+ function normalizeMode(raw, mode) {
31
+ const path = `shortcuts.${mode}`;
32
+ if (!Array.isArray(raw))
33
+ return fail(path, 'expected an array');
34
+ const seen = new Set();
35
+ return raw.map((candidate, index) => {
36
+ const itemPath = `${path}[${index}]`;
37
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
38
+ return fail(itemPath, 'expected an object');
39
+ const item = candidate;
40
+ let normalized;
41
+ if (item.type === 'key') {
42
+ const key = item.key;
43
+ const label = item.label;
44
+ if (!isAllowedKey(key))
45
+ return fail(`${itemPath}.key`, `unsupported key ${JSON.stringify(key)}`);
46
+ if (label !== undefined && !isSingleLine(label, 80)) {
47
+ return fail(`${itemPath}.label`, 'expected non-empty single-line text');
48
+ }
49
+ if (item.enter !== undefined)
50
+ return fail(`${itemPath}.enter`, 'not valid for a key shortcut');
51
+ normalized = { type: 'key', key, label: label ?? key };
52
+ }
53
+ else if (item.type === 'text') {
54
+ const text = item.text;
55
+ const enter = item.enter;
56
+ if (!isSingleLine(text, 500))
57
+ return fail(`${itemPath}.text`, 'expected non-empty single-line text');
58
+ if (typeof enter !== 'boolean')
59
+ return fail(`${itemPath}.enter`, 'expected a boolean');
60
+ normalized = { type: 'text', text, enter };
61
+ }
62
+ else {
63
+ return fail(`${itemPath}.type`, 'expected "key" or "text"');
64
+ }
65
+ const identity = shortcutIdentity(normalized);
66
+ if (seen.has(identity))
67
+ fail(itemPath, 'duplicate shortcut');
68
+ seen.add(identity);
69
+ return normalized;
70
+ });
71
+ }
72
+ export function normalizeShortcuts(raw) {
73
+ if (raw === undefined) {
74
+ return { command: cloneItems(DEFAULT_SHORTCUTS.command), chat: cloneItems(DEFAULT_SHORTCUTS.chat) };
75
+ }
76
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw))
77
+ fail('shortcuts', 'expected an object');
78
+ const config = raw;
79
+ return {
80
+ command: config.command === undefined ? cloneItems(DEFAULT_SHORTCUTS.command) : normalizeMode(config.command, 'command'),
81
+ chat: config.chat === undefined ? cloneItems(DEFAULT_SHORTCUTS.chat) : normalizeMode(config.chat, 'chat'),
82
+ };
83
+ }
@@ -11,7 +11,9 @@
11
11
  // make the recheck a cheap 304 when nothing changed, and an actual change is picked up at once.
12
12
  // Pure so it unit-tests on its own.
13
13
  export function cacheControlFor(filePath) {
14
- if (filePath.endsWith('.html') || filePath.endsWith('/sw.js')) return 'no-store';
15
- if (/[\\/]assets[\\/]/.test(filePath)) return 'public, max-age=31536000, immutable';
16
- return 'no-cache';
14
+ if (filePath.endsWith('.html') || filePath.endsWith('/sw.js'))
15
+ return 'no-store';
16
+ if (/[\\/]assets[\\/]/.test(filePath))
17
+ return 'public, max-age=31536000, immutable';
18
+ return 'no-cache';
17
19
  }
@@ -1,10 +1,9 @@
1
1
  import compression from 'compression';
2
-
3
2
  const compress = compression({ threshold: 1024 });
4
-
5
3
  // Only compress Vite's content-hashed frontend assets. API responses keep their existing
6
4
  // latency/streaming behavior, while the large JS/CSS needed for first paint becomes much smaller.
7
- export function compressStaticAssets(req, res, next) {
8
- if (!req.path.startsWith('/assets/')) return next();
9
- return compress(req, res, next);
10
- }
5
+ export const compressStaticAssets = (req, res, next) => {
6
+ if (!req.path.startsWith('/assets/'))
7
+ return next();
8
+ return compress(req, res, next);
9
+ };