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,39 @@
1
+ // Shared IO scaffolding for the Claude Code hook installer and statusLine capturer.
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ export const HOOK_SCRIPTS = ['handmux-notify.sh', 'handmux-write.cjs'];
5
+ const LEGACY_HOOK_SCRIPTS = ['handmux-codex-usage.cjs'];
6
+ // Atomic write (tmp + rename) so a crash can't leave a half-written config file. Text in, text out — callers
7
+ // pass raw TOML for config.toml, or use writeJsonAtomic for pretty-printed settings.json.
8
+ export function writeFileAtomic(file, text) {
9
+ const tmp = `${file}.tmp`;
10
+ fs.writeFileSync(tmp, text);
11
+ fs.renameSync(tmp, file);
12
+ }
13
+ export const writeJsonAtomic = (file, value) => (writeFileAtomic(file, JSON.stringify(value, null, 2)));
14
+ // Deploy the Claude notify/write scripts into `hooksDir` and point their env at the shared state file.
15
+ export function deployHookScripts(hooksDir, srcDir, stateFile) {
16
+ fs.mkdirSync(hooksDir, { recursive: true });
17
+ for (const f of HOOK_SCRIPTS)
18
+ fs.copyFileSync(path.join(srcDir, f), path.join(hooksDir, f));
19
+ for (const f of LEGACY_HOOK_SCRIPTS) {
20
+ try {
21
+ fs.unlinkSync(path.join(hooksDir, f));
22
+ }
23
+ catch { /* absent */ }
24
+ }
25
+ fs.chmodSync(path.join(hooksDir, 'handmux-notify.sh'), 0o755);
26
+ fs.writeFileSync(path.join(hooksDir, 'handmux-notify.env'), [
27
+ `HANDMUX_STATE=${stateFile}`,
28
+ '',
29
+ ].join('\n'), { mode: 0o600 });
30
+ }
31
+ // Remove the deployed scripts + env (uninstall). Best-effort: a missing file is fine.
32
+ export function removeHookScripts(hooksDir) {
33
+ for (const f of [...HOOK_SCRIPTS, ...LEGACY_HOOK_SCRIPTS, 'handmux-notify.env']) {
34
+ try {
35
+ fs.unlinkSync(path.join(hooksDir, f));
36
+ }
37
+ catch { /* already gone */ }
38
+ }
39
+ }
@@ -0,0 +1,392 @@
1
+ // English catalog (the fallback locale). Keys are grouped by command/area. `{var}` placeholders are filled
2
+ // by translate(). Keep this in lockstep with zh.js — a missing zh key silently falls back to the line here.
3
+ export default {
4
+ // generic
5
+ 'err.generic': '✗ {msg}',
6
+ 'err.configNotFound': '✗ --config {path}: not found',
7
+ 'err.badConfig': '✗ bad config {path}: {msg}',
8
+ 'err.namedNotProvisioned': '✗ named tunnel not provisioned — run `handmux setup` first',
9
+ 'lifecycle.busy': '✗ another handmux lifecycle operation is running (pid {pid}); try again shortly.',
10
+ 'lifecycle.scanFailed': '✗ could not inspect handmux supervisor processes; refusing to continue because that could create a duplicate.',
11
+ // config line printed at the top of start / service install
12
+ 'config.loaded': 'config: {path}',
13
+ 'config.none': '(none — flags + defaults)',
14
+ // shortcut editor
15
+ 'shortcuts.title': 'Shared quick shortcuts',
16
+ 'shortcuts.command': 'Command mode',
17
+ 'shortcuts.chat': 'Chat mode',
18
+ 'shortcuts.count': '{n} items',
19
+ 'shortcuts.save': 'Save and exit',
20
+ 'shortcuts.exit': 'Exit without saving',
21
+ 'shortcuts.addKey': 'Add key',
22
+ 'shortcuts.addText': 'Add text',
23
+ 'shortcuts.back': 'Back',
24
+ 'shortcuts.edit': 'Edit',
25
+ 'shortcuts.move': 'Change position',
26
+ 'shortcuts.addPositionPrompt': 'Place at',
27
+ 'shortcuts.movePrompt': 'Move to (current: position {n})',
28
+ 'shortcuts.positionFirst': 'First (position 1)',
29
+ 'shortcuts.positionAfter': 'After {item} (position {n})',
30
+ 'shortcuts.positionLast': 'Last (position {n})',
31
+ 'shortcuts.delete': 'Delete',
32
+ 'shortcuts.type': 'Shortcut type',
33
+ 'shortcuts.key': 'Key',
34
+ 'shortcuts.text': 'Text',
35
+ 'shortcuts.textEnter': 'Text · sends with Enter',
36
+ 'shortcuts.textOnly': 'Text · type only',
37
+ 'shortcuts.textPrompt': 'Text to type',
38
+ 'shortcuts.badText': 'Enter one non-empty line',
39
+ 'shortcuts.enter': 'Press Enter after the text?',
40
+ 'shortcuts.modifier': 'Modifier',
41
+ 'shortcuts.noModifier': 'None',
42
+ 'shortcuts.base': 'Base key',
43
+ 'shortcuts.needTty': 'handmux shortcuts needs an interactive terminal.',
44
+ 'shortcuts.wrote': 'Saved {path}',
45
+ 'shortcuts.exited': 'No changes saved',
46
+ 'shortcuts.applied': 'Applied immediately to the running handmux server.',
47
+ 'shortcuts.applyFailed': 'Config was saved but could not be applied immediately: {msg}. Run `handmux restart`.',
48
+ // ssh preflight
49
+ 'ssh.confirmSetup': 'passwordless SSH to {host} is not set up. Configure it now?',
50
+ 'ssh.notSetup': 'passwordless SSH not set up — run: {bin} setup-key {host}',
51
+ // tmux presence / version
52
+ 'tmux.notFound': '✗ tmux not found.',
53
+ 'tmux.explain1': ' handmux runs on top of tmux (a terminal multiplexer) — it drives your real tmux',
54
+ 'tmux.explain2': ' panes from your phone, so you need tmux on this machine first.',
55
+ 'tmux.install': ' Install it: {hint}',
56
+ 'tmux.thenStart': ' Then run `handmux start` again.',
57
+ 'open.usage': 'usage: handmux open <session> attach the tmux session (creates it if missing)',
58
+ 'open.insideTmux': "you're already inside tmux — switch sessions with tmux itself (e.g. `tmux switch-client -t <session>`), don't nest.",
59
+ 'tmux.tooOld': '⚠ tmux {raw} is below the tested minimum {min}; terminal rendering may be off',
60
+ // start — already running
61
+ 'start.running.same': 'handmux is already running — open the address below.',
62
+ 'start.running.changedHead': 'handmux is already running (tunnel: {tunnel}). A running instance does NOT pick up config changes on its own:',
63
+ 'start.running.changedRow': ' • {key}: {from} → {to} (what you asked for now)',
64
+ 'start.running.switchQ': 'Switch to the new settings now? (restarts handmux)',
65
+ 'start.running.hint': "Leaving it as-is. Run 'handmux restart' whenever you want to apply the new settings.",
66
+ 'start.instancesFound': '✗ found an untracked or duplicate handmux supervisor (pid: {pids}).',
67
+ 'start.instancesHint': ' refusing to launch another copy. Run `handmux stop` first to reap every instance.',
68
+ // start — launching
69
+ 'start.overrides': ' ↳ --tunnel {flag} overrides config ({file}) for this run only',
70
+ 'start.foreground': 'starting handmux (tunnel: {tunnel}, port: {port}) — Ctrl-C to stop',
71
+ 'start.starting': 'starting handmux (tunnel: {tunnel}, port: {port}) …',
72
+ // stop / status
73
+ 'stop.notRunning': 'handmux not running',
74
+ 'stop.stopped': 'stopped handmux (pid {pid})',
75
+ 'stop.stoppedMany': 'stopped duplicate handmux instances (pids: {pids})',
76
+ 'stop.stoppedManaged': 'stopped the managed handmux service',
77
+ 'stop.timeout': '✗ handmux (pids: {pids}) did not fully stop; refusing to start a second instance. Check `handmux status` / `handmux logs`.',
78
+ 'status.stopped': '● handmux {version} stopped',
79
+ 'status.running': '● handmux {version} running',
80
+ 'status.installed': ' installed version {version} (takes effect after restart)',
81
+ 'status.untracked': '⚠ handmux state is stale, but supervisor processes still exist (pids: {pids}); running version unknown, installed version {version}.',
82
+ 'status.duplicates': '⚠ multiple handmux supervisors detected (pids: {pids}); the access details below only belong to the last state writer.',
83
+ 'status.cleanupHint': ' run `handmux stop` to reap every copy.',
84
+ 'status.scanFailed': '⚠ could not inspect supervisor processes; the status below comes from state.json only.',
85
+ // logs
86
+ 'logs.none': '(no log yet — start handmux first)',
87
+ // update
88
+ 'update.available': ' ⬆ handmux {latest} is available (you have {current})',
89
+ 'update.how': ' upgrade: handmux update (or npm i -g handmux@latest)',
90
+ 'update.howBrew': ' upgrade: brew upgrade handmux',
91
+ 'update.brew': 'This handmux was installed with Homebrew — upgrade it with: brew upgrade handmux',
92
+ 'update.running': 'Upgrading handmux (npm i -g handmux@latest)…',
93
+ 'update.done': '✓ handmux updated.',
94
+ 'update.restartHint': ' run `handmux restart` to run the new version.',
95
+ 'update.failed': '✗ Upgrade failed. Try manually: npm i -g {pkg}@latest (may need sudo).',
96
+ // cloudflared auto-download
97
+ 'cf.downloading': ' ↓ downloading cloudflared ({file}) …',
98
+ // natapp / cpolar client binaries
99
+ 'client.downloading': ' ↓ downloading {name} ({file}) …',
100
+ 'client.cpolarManual': '✗ cpolar not available — install it from https://www.cpolar.com/download (unzip the binary into ~/.handmux/bin/), then re-run.',
101
+ 'client.cpolarAuthFail': '✗ cpolar rejected the authtoken — double-check it at https://dashboard.cpolar.com',
102
+ 'client.natappManual': '✗ natapp not found — download it from https://natapp.cn (login first, it is not a public download), then put the `natapp` binary in {dir} (or anywhere on your PATH) and re-run.',
103
+ // access block (printAccess)
104
+ 'access.noState': ' (no state)',
105
+ 'access.error': ' ✗ {msg}',
106
+ 'access.tunnel': ' tunnel {tunnel} · pid {pid}',
107
+ 'access.open': ' 🌐 open {url}',
108
+ 'access.pending': '(pending…)',
109
+ 'access.lan': ' 📶 lan {url}',
110
+ 'access.local': ' 💻 local {url}',
111
+ 'access.token': ' 🔑 token {token}',
112
+ 'access.reachable': ' ✓ reachable',
113
+ 'access.unreachable': ' ⚠ tunnel up but {url} did not answer — check the server-side reverse proxy / DNS',
114
+ 'access.hint': ' handmux status | stop',
115
+ // hooks
116
+ 'hooks.confirmEnable': 'Enable Claude Code states and notifications (inbox)?',
117
+ 'hooks.noClaude': 'Claude Code not detected (~/.claude missing) — nothing to install.',
118
+ 'hooks.installedClaude': '✓ Claude integration enabled.',
119
+ 'hooks.installedHint': ' Restart or open a new Claude Code session to load it.',
120
+ 'hooks.removed': '✓ Claude integration disabled.',
121
+ 'hooks.usage': 'usage: handmux hooks install|uninstall',
122
+ // Agent launchers and Handmux integration management
123
+ 'agent.state.ready': 'ready',
124
+ 'agent.state.not-installed': 'not installed',
125
+ 'agent.state.not-enabled': 'not enabled',
126
+ 'agent.state.needs-repair': 'needs repair',
127
+ 'agent.state.conflict': 'conflict',
128
+ 'agent.builtIn': ' (built-in)',
129
+ 'agent.status': '{name}: {status}{detail}',
130
+ 'agent.hint.install': ' Install {name} first, then run `handmux agent enable {name}`.',
131
+ 'agent.hint.initializeClaude': ' Run Claude Code once to initialize ~/.claude, then run `handmux agent enable claude`.',
132
+ 'agent.hint.enable': ' Run `handmux agent enable {name}`.',
133
+ 'agent.hint.conflict': ' The integration target is owned by another extension; move it aside, then run `handmux agent enable {name}`.',
134
+ 'agent.enabled': '✓ {name} integration enabled.',
135
+ 'agent.alreadyReady': '= {name} integration is already ready.',
136
+ 'agent.disabled': '✓ {name} integration disabled.',
137
+ 'agent.alreadyDisabled': '= {name} integration is already disabled.',
138
+ 'agent.codexBuiltIn': '= Codex integration is built in and already ready.',
139
+ 'agent.codexDisable': 'Codex integration is built in and cannot be disabled. Run native `codex` directly to bypass Handmux.',
140
+ 'agent.unknown': 'unknown Agent: {name}',
141
+ 'agent.usage': 'usage: handmux agent [list] | handmux agent status|enable|disable <codex|pi|claude>',
142
+ // Pi Extension
143
+ 'piExtension.confirmEnable': 'Enable the Pi integration with Handmux?',
144
+ 'piExtension.installed': '✓ Pi integration enabled.',
145
+ 'piExtension.reload': ' Open a new Pi session, or run /reload in Pi, to apply the change.',
146
+ // Claude statusLine usage capturer (powers the phone Usage page's 5h/weekly bars)
147
+ 'statusline.confirmEnable': "Show Claude's 5h/weekly usage on the phone? (installs a Claude statusLine)",
148
+ 'statusline.installed': '✓ Claude statusLine installed → ~/.claude/settings.json',
149
+ 'statusline.reload': ' Open a new Claude session to load it; the Usage page fills in as it reports.',
150
+ 'statusline.foreignHint': 'You already have a Claude statusLine — leaving it untouched. To also feed the phone Usage page, pipe it through our capturer:',
151
+ // service
152
+ 'service.usage': 'usage: handmux service install [start-flags] | handmux service uninstall',
153
+ 'service.installed': "handmux will now start at login. 'handmux service uninstall' to remove.",
154
+ // config command
155
+ 'configcmd.file': 'config file: {path}',
156
+ 'configcmd.fileNone': '(none — using defaults; run `handmux setup` to create one)',
157
+ 'configcmd.legend': ' origin: flag (this run only) · file · env · default',
158
+ // setup wizard
159
+ 'setup.confirmStart': 'Start handmux now?',
160
+ 'setup.later': "run 'handmux start' when you're ready.",
161
+ 'setup.laterRestart': "already running — run 'handmux restart' to apply the changes.",
162
+ 'setup.needTty': 'handmux setup needs an interactive terminal',
163
+ 'setup.langQ': 'Command-line language (handmux terminal output) / 命令行语言',
164
+ 'setup.lang1': ' 1) English',
165
+ 'setup.lang2': ' 2) 中文',
166
+ 'setup.askName': 'app name (shown in the browser tab / home-screen icon; blank = default)',
167
+ 'setup.tunnelQ': 'How should your phone reach this machine?',
168
+ 'setup.tunnel1': ' 1) none — same Wi-Fi / LAN only',
169
+ 'setup.tunnel2': ' 2) cloudflare — instant, random temporary https URL',
170
+ 'setup.tunnel3': ' 3) cloudflare-named — your domain, stable HTTPS (most hands-off)',
171
+ 'setup.tunnel4': ' 4) ssh (tunlite) — your own server / edge',
172
+ 'setup.tunnel5': ' 5) natapp — China-usable tunnel (needs a natapp authtoken)',
173
+ 'setup.tunnel6': ' 6) cpolar — China-usable tunnel (auto-installs; needs a cpolar authtoken)',
174
+ 'setup.choose': 'choose 1-6',
175
+ 'setup.invalid': 'invalid choice',
176
+ 'setup.askPort': 'server port',
177
+ 'setup.askHostname': 'public hostname (e.g. handmux.example.com)',
178
+ 'setup.askTunnelName': 'tunnel name',
179
+ 'setup.askSshHost': 'ssh host (user@host[:port])',
180
+ 'setup.askRemotePort': 'remote port on the ssh host',
181
+ 'setup.askPublicUrl': 'public url — http(s):// as appropriate (blank = http://host:remotePort)',
182
+ 'setup.natappGuide': 'Where to get the authtoken: register free at https://natapp.cn → create a tunnel → copy its authtoken (the free tier is enough to start).',
183
+ 'setup.cpolarGuide': 'Where to get the authtoken: register free at https://cpolar.com → open the dashboard → Verify → copy your authtoken.',
184
+ 'setup.askAuthtoken': 'authtoken',
185
+ 'setup.askFixed': 'use a fixed domain? (No = a free temporary domain, changes each restart)',
186
+ 'setup.askNatappDomain': 'your reserved domain bound to this authtoken (e.g. myapp.natapp1.cc)',
187
+ 'setup.askCpolarDomain': 'your reserved second-level subdomain or bound domain (e.g. myapp.cpolar.top)',
188
+ 'setup.askCpolarRegion': 'cpolar region (cn = mainland China, faster domestically; blank = cpolar default)',
189
+ 'setup.natappReady': '✓ natapp client ready',
190
+ 'setup.cpolarReady': '✓ cpolar client ready',
191
+ // setup hub (menu model)
192
+ 'setup.welcome': "handmux is open-source and runs no relay of its own — your phone always connects to your own computer, which keeps it private and secure; on the same Wi-Fi it's a pure direct link and works out of the box (pick 'Direct'). To reach it from outside too, the options below are built-in ways to do that with your own free account. Not sure? Press Enter on the highlighted option — you can change it anytime.",
193
+ 'setup.hubTitle': 'Anything to change? (or just Save & start)',
194
+ 'setup.secConnection': 'Connection',
195
+ 'setup.secName': 'Name',
196
+ 'setup.secPort': 'Port',
197
+ 'setup.secToken': 'Token',
198
+ 'setup.secBrowser': 'Web preview',
199
+ 'setup.browserOff': 'not configured · Direct mode only',
200
+ 'setup.askBrowserDomain': 'Web preview proxy domain (blank = Direct mode only)',
201
+ 'setup.browserAbout': 'Enter a proxy domain (for example, preview.example.com) and route its wildcard subdomains to Handmux over HTTPS; leave it blank for Direct mode only.',
202
+ 'setup.tokenAuto': 'auto · new each start',
203
+ 'setup.tokenCustom': 'Set a custom token…',
204
+ 'setup.tokenRandom': 'Generate a random one',
205
+ 'setup.tokenReset': 'Reset to auto (new each start)',
206
+ 'setup.askToken': 'Access token — it appears in the URL you open on the phone',
207
+ 'setup.tokenGenerated': 'New token: {token}',
208
+ 'setup.valToken': 'enter a token',
209
+ 'setup.valTokenSpace': 'no spaces — the token goes in a URL',
210
+ 'setup.secLanguage': 'CLI language / 命令行语言',
211
+ 'setup.secPush': 'Push',
212
+ 'setup.secVoice': 'Voice',
213
+ 'setup.default': '(default)',
214
+ 'setup.on': 'on',
215
+ 'setup.off': 'off',
216
+ 'setup.yes': 'Yes',
217
+ 'setup.no': 'No',
218
+ 'setup.escBack': '(Esc to go back)',
219
+ 'setup.actStart': 'Save & start',
220
+ 'setup.actRestart': 'Save & restart (apply now)',
221
+ 'setup.actSave': 'Save',
222
+ 'setup.actExit': 'Exit — discard changes',
223
+ 'setup.exited': 'setup cancelled — no changes saved',
224
+ 'setup.tunnelNone': 'Direct',
225
+ 'setup.hintNone': 'no tunnel — reach this machine directly (same Wi-Fi, or its own public IP); simplest',
226
+ 'setup.hintCf': 'reach it from anywhere, no signup (temporary URL; can be flaky in China)',
227
+ 'setup.hintCfNamed': 'your domain, stable HTTPS (most hands-off)',
228
+ 'setup.hintSsh': 'you already have a server to route through',
229
+ 'setup.hintNatapp': 'reach it from anywhere, works in China (needs a free natapp account)',
230
+ 'setup.hintCpolar': 'reach it from anywhere, works in China, auto-installs (needs a free cpolar account)',
231
+ 'setup.domainQ': 'temporary or fixed domain?',
232
+ 'setup.domainTemp': 'Free temporary domain',
233
+ 'setup.domainTempHint': 'changes each restart',
234
+ 'setup.domainFixed': 'Fixed / reserved domain',
235
+ 'setup.valPort': 'port must be an integer 1–65535',
236
+ 'setup.valRequired': '{label} is required',
237
+ 'setup.valHost': 'enter a valid domain (e.g. myapp.example.com)',
238
+ 'setup.valPreviewDomain': 'enter only a domain, e.g. preview.example.com; do not include http://, https://, a port, or *.',
239
+ 'setup.valContact': 'use mailto:you@example.com or https://your.site (a real domain — Apple rejects fake ones)',
240
+ 'setup.sumTemp': 'temporary',
241
+ 'setup.sumFixed': 'fixed',
242
+ 'setup.sumNoRelay': 'no relay',
243
+ // connection mini-hub (per-tunnel field rows)
244
+ 'setup.connTunnel': 'Tunnel',
245
+ 'setup.connMode': 'Mode',
246
+ 'setup.connHostname': 'Hostname',
247
+ 'setup.connTunnelName': 'Tunnel name',
248
+ 'setup.connSshHost': 'SSH host',
249
+ 'setup.connRemotePort': 'Remote port',
250
+ 'setup.connPublicUrl': 'Public URL',
251
+ 'setup.connJump': 'Jump host',
252
+ 'setup.connDomain': 'Domain',
253
+ 'setup.connRegion': 'Region',
254
+ 'setup.connNone': '(not set)',
255
+ 'setup.connAuto': '(auto)',
256
+ 'setup.askSshJump': 'ssh jump host (user@host, blank = none)',
257
+ 'setup.cfModeQ': 'cloudflare address',
258
+ 'setup.cfTemp': 'Temporary (no login)',
259
+ 'setup.cfTempHint': 'instant random https URL, changes each restart',
260
+ 'setup.cfNamed': 'Named (your domain)',
261
+ 'setup.cfNamedHint': 'stable HTTPS, needs a Cloudflare login',
262
+ 'setup.wrote': '✓ wrote {path}',
263
+ 'setup.pushKeep': 'keep push notifications configured?',
264
+ 'setup.pushSetup': 'Turn on phone notifications? (get pinged when a coding agent finishes)',
265
+ 'setup.pushAbout': 'handmux generates a private signing key for you and stores it in ~/.handmux/config.json — it never leaves this machine. The contact below is only an address the push service can reach you at; nobody else sees it.',
266
+ 'setup.pushContact': 'contact for the push service — mailto: or https:// (the default is fine; keep the mailto: prefix)',
267
+ 'setup.pushGenerated': '✓ generated a signing keypair (kept in your local config)',
268
+ 'setup.pushContactLabel': 'Contact',
269
+ 'setup.pushRegen': 'Regenerate keys',
270
+ 'setup.pushRegenHint': 'resets every phone subscription',
271
+ 'setup.pushRegenConfirm': 'Regenerate the keypair? Every phone that already subscribed stops getting notifications until it re-opens the web app and subscribes again. Only needed if the key leaked.',
272
+ 'setup.pushRegenerated': '✓ regenerated keys — existing phones must re-subscribe',
273
+ 'setup.pushOff': 'Turn off push',
274
+ 'setup.voiceOff': 'Turn off voice',
275
+ 'setup.voiceKeep': 'keep voice input configured?',
276
+ 'setup.voiceSetup': 'Turn on voice input? (talk to your phone → text; needs an iFlytek/xfyun account)',
277
+ 'setup.voiceAppId': 'xfyun appId',
278
+ 'setup.voiceApiKey': 'xfyun apiKey',
279
+ 'setup.voiceApiSecret': 'xfyun apiSecret',
280
+ 'setup.voiceSkipped': ' (skipped — missing fields)',
281
+ 'setup.cfLogin': '→ logging in to Cloudflare (a browser will open) …',
282
+ 'setup.cfReuse': '✓ reusing existing tunnel {name} ({id})',
283
+ 'setup.cfCredMissing1': '⚠ credentials file {file} not found on this machine — the tunnel was likely',
284
+ 'setup.cfCredMissing2': ' created elsewhere. Run `{bin} tunnel delete {name}` and re-run setup to recreate it here.',
285
+ 'setup.cfCreate': '→ creating tunnel {name} …',
286
+ 'setup.cfRoute': '→ routing {host} → tunnel …',
287
+ 'setup.cfRouteFail': "⚠ route dns failed — is {domain}'s DNS hosted on Cloudflare?",
288
+ 'setup.cfRouteFail2': ' Add the domain on Cloudflare (free) and point its nameservers there, then re-run setup.',
289
+ 'setup.sshReady': '✓ passwordless SSH already set up',
290
+ 'setup.sshSetup': "→ setting up passwordless SSH to {host} (you'll enter the password once) …",
291
+ 'setup.sshHelp1': 'Server side (one-time): point a reverse proxy at the forwarded loopback port.',
292
+ 'setup.sshHelpNginx': ' nginx: proxy_pass http://127.0.0.1:{port}; also set proxy_http_version 1.1, the Upgrade/Connection WebSocket headers, client_max_body_size 60m, and proxy_read_timeout 90s;',
293
+ 'setup.sshHelpCaddy': ' caddy: {url} { reverse_proxy 127.0.0.1:{port} }',
294
+ // workspace restore
295
+ 'restore.usage': 'Usage: handmux restore [--dry-run] [--checkpoint <id>] [--session <name>] | handmux restore --list',
296
+ 'restore.badFlag': 'Unknown restore option {flag}.',
297
+ 'restore.unexpectedArgument': 'Unexpected restore argument: {value}.',
298
+ 'restore.unknownShortFlag': 'Unknown restore option {flag}.',
299
+ 'restore.flagBoolean': '{flag} does not take a value.',
300
+ 'restore.checkpointValue': '--checkpoint needs an id value (or latest).',
301
+ 'restore.checkpointId': '--checkpoint must be a valid id containing only letters, numbers, dots, underscores, or hyphens.',
302
+ 'restore.sessionValue': '--session needs a non-empty session name.',
303
+ 'restore.listExclusive': '--list must be used alone; it cannot be combined with --dry-run, --checkpoint, or --session.',
304
+ 'restore.listRow': '{id} {time} {sessions} session(s) · {windows} window(s) · {panes} pane(s) · {agents} agent(s)',
305
+ 'restore.listUnavailable': '{id} unavailable: {error}',
306
+ 'restore.listEmpty': 'No workspace checkpoints have been archived yet.',
307
+ 'restore.selectCheckpoint': 'Choose a workspace checkpoint to restore',
308
+ 'restore.selectHint': '{time} · {sessions} session(s) · {windows} window(s) · {panes} pane(s)',
309
+ 'restore.noCheckpoint': 'No usable checkpoint exists yet. handmux is silently protecting the current workspace; retry after the next computer or tmux restart.',
310
+ 'restore.selectionCancelled': 'No checkpoint was selected. Run `{command}` again when you are ready.',
311
+ 'restore.planCheckpoint': 'Checkpoint: {time} ({id}; {sessions} sessions, {windows} windows, {panes} panes)',
312
+ 'restore.planCurrent': 'Current: {sessions} live sessions',
313
+ 'restore.planCreate': '+ {session}',
314
+ 'restore.planRenamed': '+ {session} -> {target} (name already in use)',
315
+ 'restore.planAlready': '= {session} (already restored)',
316
+ 'restore.planUnavailable': '! {session} (unavailable: {reason})',
317
+ 'restore.reason.linkedWindows': 'this checkpoint contains linked windows that cannot be restored safely by this version',
318
+ 'restore.reason.invalidTopology': 'this checkpoint contains incomplete or invalid tmux topology',
319
+ 'restore.reason.unknown': 'unsupported checkpoint topology ({reason})',
320
+ 'restore.manualRecovery': 'Restore this session manually instead: {command}',
321
+ 'restore.warning': 'Warning: {warning}',
322
+ 'restore.sessionWarning': ' Warning for {session}: {warning}',
323
+ 'restore.nonDestructive': 'No existing session or process will be stopped or modified.',
324
+ 'restore.nonDestructivePast': 'Existing sessions were not modified.',
325
+ 'restore.dryRunHint': 'Run `{command}` to continue.',
326
+ 'restore.resultRestored': '✓ {session}',
327
+ 'restore.resultRenamed': '✓ {session} restored as {target}',
328
+ 'restore.resultAlready': '= {session} (already restored)',
329
+ 'restore.sessionFailed': 'Checkpoint {checkpoint}; session {session}; stage {stage}: {error}',
330
+ 'restore.operationFailed': 'Checkpoint {checkpoint}; stage {stage}: {error}',
331
+ 'restore.retrySession': 'Retry only this session: {command}',
332
+ 'restore.resultSummary': 'Restored {restored} session(s); {already} already present; {failed} failed.',
333
+ 'restore.error': 'Checkpoint {checkpoint} failed during the restore command: {error}',
334
+ 'restore.retry': 'Retry with `{command}` after fixing the reported cause.',
335
+ // help
336
+ 'help.body': `handmux — drive your tmux from your phone
337
+
338
+ handmux start run it (defaults to LAN-only; no config needed)
339
+ handmux open <session> attach a tmux session, creating it if missing — incl. ones made on the phone
340
+ handmux setup configure tunnel / name / notifications (writes config; re-run to change)
341
+ handmux shortcuts configure shared command/chat quick shortcuts
342
+ handmux stop | restart | status
343
+ handmux logs [--follow] [--lines N]
344
+ handmux push <title> <body> notify your phone from a script (--session X · --device K · --tag T · --url U)
345
+ handmux codex [args...] launch a Codex TUI synchronized with chat view
346
+ handmux pi [args...] launch Pi with its arguments unchanged
347
+ handmux agent [list] show supported Agent integrations
348
+ handmux agent status|enable|disable <name>
349
+ handmux restore [--dry-run] [--checkpoint <id>] [--session <name>]
350
+ handmux restore --list
351
+ handmux update upgrade to the latest published version (npm i -g handmux@latest)
352
+
353
+ New here? run 'handmux setup' — it walks you through remote access + phone notifications.
354
+ The model: 'start' runs · 'setup' configures (writes ~/.handmux/config.json) · re-run setup to change.
355
+
356
+ more:
357
+ handmux config show the effective config + where each value came from
358
+ handmux service install|uninstall start at login (launchd/systemd)
359
+ handmux help flags one-run flags + env vars (scripting / headless / Docker)
360
+ --config PATH · --lang en|zh · --version, -v
361
+ `,
362
+ 'help.flags': `handmux flags — one-run overrides & headless config
363
+
364
+ Precedence: flag > file (~/.handmux/config.json) > env (HANDMUX_*) > default.
365
+ A flag overrides one value for THIS run only and never persists — for persistence use 'handmux setup'.
366
+ env vars are the headless interface (Docker / systemd / CI) and DO carry across runs.
367
+
368
+ start flags (matching env var in parens):
369
+ --tunnel none|cloudflare|cloudflare-named|ssh|natapp|cpolar expose method (default: none)
370
+ --port N server port (HANDMUX_PORT, default: 19999)
371
+ --host H bind host (HANDMUX_HOST, default: 0.0.0.0)
372
+ --token S auth token (HANDMUX_TOKEN, default: generated each start)
373
+ --name "My Box" app name in the browser tab + home-screen icon (HANDMUX_APP_NAME)
374
+ --public-url URL public url to advertise (HANDMUX_PUBLIC_URL; any tunnel, incl. none if
375
+ you run your own; ssh defaults to http://host:remotePort; for
376
+ natapp/cpolar this is your fixed/reserved domain — omit for a temporary one)
377
+ --ssh-host user@host[:port] ssh tunnel target (HANDMUX_SSH_HOST)
378
+ --remote-port N port bound on the ssh host (HANDMUX_REMOTE_PORT, default: --port)
379
+ --ssh-jump u@h[,…] optional bastion for ssh (HANDMUX_SSH_JUMP)
380
+ --cf-hostname H public hostname for cloudflare-named (HANDMUX_CF_HOSTNAME)
381
+ --cf-tunnel-name N tunnel name for cloudflare-named (HANDMUX_CF_TUNNEL_NAME, default: handmux)
382
+ --authtoken T authtoken for natapp / cpolar (HANDMUX_AUTHTOKEN)
383
+ --cpolar-region R cpolar edge region, e.g. cn (HANDMUX_CPOLAR_REGION)
384
+ --foreground, -f run in the foreground (don't daemonize)
385
+ --no-qr don't render the QR code
386
+
387
+ rarely needed (env or flag): --static-dir / --upload-exts
388
+ (HANDMUX_STATIC_DIR / HANDMUX_UPLOAD_EXTS)
389
+
390
+ 'handmux service install' accepts these same start flags — they're baked into the autostart entry.
391
+ `,
392
+ };
@@ -5,49 +5,51 @@
5
5
  // key falls back to English, so a partial translation degrades gracefully rather than printing blanks.
6
6
  import en from './en.js';
7
7
  import zh from './zh.js';
8
-
9
8
  const LOCALES = { en, zh };
10
9
  export const AVAILABLE = [
11
- { code: 'en', label: 'English' },
12
- { code: 'zh', label: '中文' },
10
+ { code: 'en', label: 'English' },
11
+ { code: 'zh', label: '中文' },
13
12
  ];
14
-
15
13
  // pure + testable: current-dict → English-fallback → key, then {var} interpolation (same as web).
16
14
  export function translate(dict, fallback, key, vars) {
17
- let s = dict && dict[key] != null ? dict[key]
15
+ let s = dict && dict[key] != null ? dict[key]
18
16
  : fallback && fallback[key] != null ? fallback[key]
19
- : key;
20
- if (vars) s = s.replace(/\{(\w+)\}/g, (_, k) => (vars[k] != null ? vars[k] : `{${k}}`));
21
- return s;
17
+ : key;
18
+ if (vars)
19
+ s = s.replace(/\{(\w+)\}/g, (_match, name) => (vars[name] != null ? String(vars[name]) : `{${name}}`));
20
+ return s;
22
21
  }
23
-
24
22
  // A POSIX locale string → the base language we might have a dict for: `zh_CN.UTF-8` → `zh`, `en` → `en`.
25
- const baseOf = (v) => String(v || '').toLowerCase().split(/[._@-]/)[0];
26
-
23
+ const baseOf = (value) => String(value || '').toLowerCase().split(/[._@-]/)[0] ?? '';
27
24
  // pure + testable: explicit choice (flag/config) wins, else the first shell locale var that names a known
28
25
  // language, else English. `C`/`POSIX`/`C.UTF-8` carry no language → fall through to English.
29
26
  export function detectLang(flags = {}, fileCfg = {}, env = {}, available = LOCALES) {
30
- const explicit = flags.lang ?? fileCfg.lang;
31
- if (explicit != null) { const b = baseOf(explicit); if (available[b]) return b; }
32
- for (const k of ['HANDMUX_LANG', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
33
- const b = baseOf(env[k]);
34
- if (b && available[b]) return b;
35
- }
36
- return 'en';
27
+ const explicit = flags.lang ?? fileCfg.lang;
28
+ if (explicit != null) {
29
+ const b = baseOf(explicit);
30
+ if (available[b])
31
+ return b;
32
+ }
33
+ for (const k of ['HANDMUX_LANG', 'LC_ALL', 'LC_MESSAGES', 'LANG']) {
34
+ const b = baseOf(env[k]);
35
+ if (b && available[b])
36
+ return b;
37
+ }
38
+ return 'en';
37
39
  }
38
-
39
40
  let current = 'en';
40
-
41
41
  // Resolve + install the active locale for the whole CLI process (called once at startup). Returns the code.
42
42
  export function initLocale(flags, fileCfg, env = process.env) {
43
- current = detectLang(flags, fileCfg, env);
44
- return current;
43
+ const detected = detectLang(flags, fileCfg, env);
44
+ current = detected === 'zh' ? 'zh' : 'en';
45
+ return current;
45
46
  }
46
-
47
47
  // Force a specific locale (e.g. right after the setup wizard's language question, so the rest of the wizard
48
48
  // speaks the chosen language). No-op for an unknown code.
49
- export function setLocale(code) { if (LOCALES[code]) current = code; }
49
+ export function setLocale(code) {
50
+ if (code === 'en' || code === 'zh')
51
+ current = code;
52
+ }
50
53
  export function getLocale() { return current; }
51
-
52
54
  // The one call sites use: translate `key` in the active locale, falling back to English then the key itself.
53
55
  export function t(key, vars) { return translate(LOCALES[current], en, key, vars); }