handmux 0.22.1 → 0.24.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 (247) hide show
  1. package/README.md +3 -3
  2. package/README.zh-CN.md +3 -3
  3. package/bin/handmux.js +3 -688
  4. package/dist/bin/handmux.js +850 -0
  5. package/{hooks → dist/hooks}/handmux-notify.sh +2 -4
  6. package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
  7. package/{hooks → dist/hooks}/handmux-write.cjs +25 -26
  8. package/dist/package.json +126 -0
  9. package/dist/public/assets/index-CicdY9E9.css +32 -0
  10. package/dist/public/assets/index-D4VP5t8h.js +337 -0
  11. package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
  12. package/{public → dist/public}/index.html +5 -4
  13. package/{src → dist/src}/agents/claude.js +106 -82
  14. package/dist/src/agents/codex.js +217 -0
  15. package/{src → dist/src}/agents/index.js +9 -10
  16. package/dist/src/agents/processIdentity.js +80 -0
  17. package/dist/src/agents/scanUtils.js +262 -0
  18. package/dist/src/apiErrors.js +77 -0
  19. package/{src → dist/src}/appName.js +12 -13
  20. package/dist/src/asr/iflyConfig.js +7 -0
  21. package/dist/src/asr/iflySign.js +14 -0
  22. package/dist/src/auth.js +27 -0
  23. package/dist/src/browser/bootstrap.js +72 -0
  24. package/dist/src/browser/cookieProfiles.js +413 -0
  25. package/dist/src/browser/coordinator.js +51 -0
  26. package/dist/src/browser/hammerheadCompat.js +104 -0
  27. package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
  28. package/dist/src/browser/manager.js +560 -0
  29. package/dist/src/browser/originLabel.js +15 -0
  30. package/dist/src/browser/profilePersistence.js +235 -0
  31. package/dist/src/browser/publicProxy.js +176 -0
  32. package/dist/src/browser/routes.js +159 -0
  33. package/dist/src/browser/sessionStore.js +72 -0
  34. package/dist/src/browser/siteVersion.js +121 -0
  35. package/dist/src/browser/targetPolicy.js +163 -0
  36. package/{src → dist/src}/browser/worker.js +14 -18
  37. package/dist/src/browser/workerClient.js +286 -0
  38. package/dist/src/browser/workerServer.js +91 -0
  39. package/dist/src/captureBackground.js +95 -0
  40. package/dist/src/claudeEvents.js +499 -0
  41. package/{src → dist/src}/cli/claudeHooks.js +146 -108
  42. package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
  43. package/dist/src/cli/cloudflared.js +106 -0
  44. package/dist/src/cli/codexManaged.js +77 -0
  45. package/{src → dist/src}/cli/cpolarUrl.js +8 -9
  46. package/dist/src/cli/drivers.js +98 -0
  47. package/dist/src/cli/hookScaffold.js +36 -0
  48. package/dist/src/cli/i18n/en.js +366 -0
  49. package/{src → dist/src}/cli/i18n/index.js +27 -25
  50. package/dist/src/cli/i18n/zh.js +365 -0
  51. package/dist/src/cli/legacyCodexHooks.js +52 -0
  52. package/{src → dist/src}/cli/natappUrl.js +2 -3
  53. package/dist/src/cli/options.js +245 -0
  54. package/dist/src/cli/probe.js +16 -0
  55. package/{src → dist/src}/cli/prompt.js +4 -6
  56. package/dist/src/cli/pushCmd.js +102 -0
  57. package/{src → dist/src}/cli/qr.js +19 -19
  58. package/dist/src/cli/service.js +144 -0
  59. package/dist/src/cli/setupModel.js +194 -0
  60. package/dist/src/cli/setupWizard.js +647 -0
  61. package/dist/src/cli/shortcutEditor.js +351 -0
  62. package/dist/src/cli/sshTunnel.js +18 -0
  63. package/dist/src/cli/state.js +114 -0
  64. package/{src → dist/src}/cli/statusLine.js +63 -46
  65. package/dist/src/cli/supervisor.js +278 -0
  66. package/dist/src/cli/supervisorHealth.js +21 -0
  67. package/dist/src/cli/supervisorLaunch.js +148 -0
  68. package/dist/src/cli/supervisorProcesses.js +62 -0
  69. package/dist/src/cli/supervisorState.js +62 -0
  70. package/dist/src/cli/tmuxVersion.js +53 -0
  71. package/{src → dist/src}/cli/tunlite.js +7 -10
  72. package/dist/src/cli/tunnelClients.js +72 -0
  73. package/{src → dist/src}/cli/updateCheck.js +98 -69
  74. package/{src → dist/src}/cli/urlHost.js +8 -4
  75. package/dist/src/cli/workspaceCmd.js +347 -0
  76. package/dist/src/codexAppServer.js +2299 -0
  77. package/dist/src/codexConversationProjection.js +199 -0
  78. package/dist/src/codexDiff.js +118 -0
  79. package/dist/src/codexMessageIdentity.js +18 -0
  80. package/dist/src/codexPlan.js +30 -0
  81. package/dist/src/codexQueueProtocol.js +96 -0
  82. package/dist/src/codexStreamProtocol.js +207 -0
  83. package/dist/src/codexToolProtocol.js +68 -0
  84. package/dist/src/codexTranscriptParse.js +724 -0
  85. package/dist/src/codexUsageSnapshot.js +205 -0
  86. package/dist/src/config.js +8 -0
  87. package/{src → dist/src}/docPath.js +22 -24
  88. package/dist/src/docs.js +337 -0
  89. package/dist/src/git.js +272 -0
  90. package/dist/src/healthProtocol.js +97 -0
  91. package/dist/src/httpApi.js +68 -0
  92. package/dist/src/jsonStore.js +15 -0
  93. package/{src → dist/src}/keyNames.js +3 -3
  94. package/dist/src/notifications.js +111 -0
  95. package/dist/src/orphans.js +157 -0
  96. package/dist/src/pendingPrompt.js +194 -0
  97. package/dist/src/previewServer.js +202 -0
  98. package/dist/src/previews.js +179 -0
  99. package/dist/src/privateStateStore.js +89 -0
  100. package/dist/src/push.js +224 -0
  101. package/dist/src/routes/codex.js +606 -0
  102. package/dist/src/routes/files.js +230 -0
  103. package/dist/src/routes/git.js +78 -0
  104. package/dist/src/routes/health.js +16 -0
  105. package/dist/src/routes/notifications.js +16 -0
  106. package/dist/src/routes/previews.js +43 -0
  107. package/dist/src/routes/push.js +145 -0
  108. package/dist/src/routes/sessions.js +232 -0
  109. package/dist/src/routes/system.js +156 -0
  110. package/dist/src/routes/terminal.js +238 -0
  111. package/dist/src/routes/transcript.js +197 -0
  112. package/dist/src/routes/workspace.js +267 -0
  113. package/{src → dist/src}/server.js +110 -76
  114. package/dist/src/shortcutConfig.js +83 -0
  115. package/{src → dist/src}/staticCache.js +5 -3
  116. package/{src → dist/src}/staticCompression.js +5 -6
  117. package/dist/src/terminalStream.js +441 -0
  118. package/{src → dist/src}/tmux/commands.js +160 -167
  119. package/dist/src/tmux/format.js +47 -0
  120. package/dist/src/transcriptParse.js +240 -0
  121. package/dist/src/transcriptReader.js +113 -0
  122. package/{src → dist/src}/trimCapture.js +11 -11
  123. package/dist/src/uploadTypes.js +29 -0
  124. package/dist/src/urlPolicy.js +18 -0
  125. package/dist/src/usage.js +260 -0
  126. package/dist/src/workspace/agentRunner.js +204 -0
  127. package/dist/src/workspace/atomicJson.js +31 -0
  128. package/dist/src/workspace/capture.js +139 -0
  129. package/dist/src/workspace/checkpointer.js +228 -0
  130. package/dist/src/workspace/environment.js +71 -0
  131. package/dist/src/workspace/lock.js +137 -0
  132. package/dist/src/workspace/mapping.js +128 -0
  133. package/dist/src/workspace/operations.js +271 -0
  134. package/dist/src/workspace/paths.js +15 -0
  135. package/dist/src/workspace/planner.js +292 -0
  136. package/dist/src/workspace/restore.js +314 -0
  137. package/dist/src/workspace/runtime.js +318 -0
  138. package/dist/src/workspace/schema.js +156 -0
  139. package/dist/src/workspace/store.js +464 -0
  140. package/dist/src/workspace/tmuxAdapter.js +694 -0
  141. package/dist/src/workspaceProtocol.js +158 -0
  142. package/package.json +28 -16
  143. package/hooks/handmux-codex-usage.cjs +0 -141
  144. package/public/assets/index-ClOByNCn.js +0 -335
  145. package/public/assets/index-rjrtUwoJ.css +0 -32
  146. package/src/.gitkeep +0 -0
  147. package/src/agents/codex.js +0 -121
  148. package/src/agents/processIdentity.js +0 -72
  149. package/src/agents/scanUtils.js +0 -225
  150. package/src/asr/iflyConfig.js +0 -10
  151. package/src/asr/iflySign.js +0 -16
  152. package/src/auth.js +0 -30
  153. package/src/browser/bootstrap.js +0 -68
  154. package/src/browser/cookieProfiles.js +0 -403
  155. package/src/browser/coordinator.js +0 -47
  156. package/src/browser/hammerheadCompat.js +0 -86
  157. package/src/browser/hammerheadRedirectCompat.js +0 -38
  158. package/src/browser/manager.js +0 -556
  159. package/src/browser/originLabel.js +0 -17
  160. package/src/browser/profilePersistence.js +0 -241
  161. package/src/browser/publicProxy.js +0 -175
  162. package/src/browser/routes.js +0 -154
  163. package/src/browser/sessionStore.js +0 -78
  164. package/src/browser/siteVersion.js +0 -124
  165. package/src/browser/targetPolicy.js +0 -137
  166. package/src/browser/workerClient.js +0 -253
  167. package/src/browser/workerServer.js +0 -98
  168. package/src/captureBackground.js +0 -93
  169. package/src/claudeEvents.js +0 -356
  170. package/src/cli/cloudflared.js +0 -99
  171. package/src/cli/codexHooks.js +0 -123
  172. package/src/cli/drivers.js +0 -97
  173. package/src/cli/hookScaffold.js +0 -35
  174. package/src/cli/i18n/en.js +0 -388
  175. package/src/cli/i18n/zh.js +0 -387
  176. package/src/cli/options.js +0 -211
  177. package/src/cli/probe.js +0 -16
  178. package/src/cli/pushCmd.js +0 -60
  179. package/src/cli/service.js +0 -126
  180. package/src/cli/setupModel.js +0 -154
  181. package/src/cli/setupWizard.js +0 -506
  182. package/src/cli/shortcutEditor.js +0 -288
  183. package/src/cli/sshTunnel.js +0 -12
  184. package/src/cli/state.js +0 -90
  185. package/src/cli/supervisor.js +0 -183
  186. package/src/cli/supervisorProcesses.js +0 -57
  187. package/src/cli/tmuxVersion.js +0 -49
  188. package/src/cli/tunnelClients.js +0 -62
  189. package/src/cli/workspaceCmd.js +0 -300
  190. package/src/config.js +0 -9
  191. package/src/docs.js +0 -228
  192. package/src/git.js +0 -208
  193. package/src/httpApi.js +0 -69
  194. package/src/jsonStore.js +0 -24
  195. package/src/notifications.js +0 -68
  196. package/src/orphans.js +0 -150
  197. package/src/pendingPrompt.js +0 -163
  198. package/src/previewServer.js +0 -168
  199. package/src/previews.js +0 -116
  200. package/src/push.js +0 -183
  201. package/src/routes/files.js +0 -172
  202. package/src/routes/git.js +0 -57
  203. package/src/routes/notifications.js +0 -19
  204. package/src/routes/previews.js +0 -35
  205. package/src/routes/push.js +0 -119
  206. package/src/routes/sessions.js +0 -163
  207. package/src/routes/system.js +0 -145
  208. package/src/routes/terminal.js +0 -210
  209. package/src/routes/transcript.js +0 -115
  210. package/src/routes/workspace.js +0 -267
  211. package/src/shortcutConfig.js +0 -75
  212. package/src/terminalStream.js +0 -391
  213. package/src/tmux/format.js +0 -45
  214. package/src/transcriptParse.js +0 -180
  215. package/src/transcriptReader.js +0 -101
  216. package/src/uploadTypes.js +0 -30
  217. package/src/urlPolicy.js +0 -14
  218. package/src/usage.js +0 -261
  219. package/src/workspace/agentRunner.js +0 -185
  220. package/src/workspace/atomicJson.js +0 -32
  221. package/src/workspace/capture.js +0 -87
  222. package/src/workspace/checkpointer.js +0 -183
  223. package/src/workspace/environment.js +0 -75
  224. package/src/workspace/lock.js +0 -128
  225. package/src/workspace/mapping.js +0 -124
  226. package/src/workspace/operations.js +0 -252
  227. package/src/workspace/paths.js +0 -16
  228. package/src/workspace/planner.js +0 -247
  229. package/src/workspace/restore.js +0 -279
  230. package/src/workspace/runtime.js +0 -314
  231. package/src/workspace/schema.js +0 -136
  232. package/src/workspace/store.js +0 -382
  233. package/src/workspace/tmuxAdapter.js +0 -554
  234. /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
  235. /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
  236. /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
  237. /package/{public → dist/public}/icons/badge-96.png +0 -0
  238. /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
  239. /package/{public → dist/public}/icons/icon-192.png +0 -0
  240. /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
  241. /package/{public → dist/public}/icons/icon-512.png +0 -0
  242. /package/{public → dist/public}/icons/logo.svg +0 -0
  243. /package/{public → dist/public}/manifest.webmanifest +0 -0
  244. /package/{public → dist/public}/offline.html +0 -0
  245. /package/{public → dist/public}/sw.js +0 -0
  246. /package/{src → dist/src}/browser/protocol.js +0 -0
  247. /package/{src → dist/src}/cli/cfNamed.js +0 -0
@@ -1,9 +1,7 @@
1
1
  #!/bin/sh
2
2
  # handmux 上报 hook. $1 = stop | notify | prompt | end | resume | permreq | compacting | compact | stopfail.
3
3
  # (compacting/compact/stopfail 是版本门控事件:PreCompact/PostCompact/StopFailure,只在够新的 Claude 上注册。)
4
- # $2 = agent id(留空=claude;
5
- # Codex 传 'codex')—— Claude 与 Codex 的 hook payload 字段一致(stdin JSON),所以共用这一个脚本。
6
- # stdin = agent 原始 payload(JSON).
4
+ # stdin = Claude Code 原始 payload(JSON).
7
5
  # (resume = PostToolUse on AskUserQuestion/ExitPlanMode:答完选项/批准计划 → 状态翻回进行中、带所选项。)
8
6
  # (permreq = PermissionRequest:真实弹框一出现就发、带 tool_name → 比 permission_prompt 早亮「需要你」。)
9
7
  # 只做一件事:把本次事件写进一个本地 JSON 状态文件(键=tmux pane,值=该 pane 最新事件)。不联网、
@@ -20,5 +18,5 @@ TS=$(perl -MTime::HiRes -e 'printf "%.0f", Time::HiRes::time()*1000' 2>/dev/null
20
18
  HOST=$(hostname 2>/dev/null || printf '')
21
19
  # payload 经 stdin 原样流给 node(不在 shell 里转义,避免坏数据);pane 含 '%' 直接进 JSON 字段,
22
20
  # 不再进 URL → 彻底告别旧的 "%110 被 url-decode 丢弃" 那类坑。
23
- node "$(dirname "$0")/handmux-write.cjs" "$FILE" "$PANE" "$1" "$TS" "$HOST" "$2" 2>/dev/null || true
21
+ node "$(dirname "$0")/handmux-write.cjs" "$FILE" "$PANE" "$1" "$TS" "$HOST" 2>/dev/null || true
24
22
  exit 0
@@ -21,6 +21,22 @@ const raw = (() => { try { return fs.readFileSync(0, 'utf8'); } catch { return '
21
21
  let j = {};
22
22
  try { j = JSON.parse(raw || '{}'); } catch { /* not JSON → leave j = {} */ }
23
23
 
24
+ function ensurePrivateDirectory(directory) {
25
+ const resolved = path.resolve(directory);
26
+ const parsed = path.parse(resolved);
27
+ const parts = resolved.slice(parsed.root.length).split(path.sep).filter(Boolean);
28
+ const privateRoot = parts.indexOf('.handmux');
29
+ if (privateRoot < 0) {
30
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
31
+ return;
32
+ }
33
+ for (let i = privateRoot; i < parts.length; i++) {
34
+ const current = path.join(parsed.root, ...parts.slice(0, i + 1));
35
+ fs.mkdirSync(current, { recursive: true, mode: 0o700 });
36
+ fs.chmodSync(current, 0o700);
37
+ }
38
+ }
39
+
24
40
  // One rate-limit window → our shape, or undefined if the field is absent (rate_limits only appears for
25
41
  // Pro/Max plans, and only after a session's first API response).
26
42
  function win(o) {
@@ -48,7 +64,8 @@ if (file) {
48
64
  rateLimits,
49
65
  };
50
66
  if (snap.context === undefined) delete snap.context;
51
- fs.mkdirSync(path.dirname(file), { recursive: true });
67
+ ensurePrivateDirectory(path.dirname(file));
68
+ if (fs.existsSync(file)) fs.chmodSync(file, 0o600);
52
69
  let previousHasQuota = false;
53
70
  try {
54
71
  const previous = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -58,12 +75,13 @@ if (file) {
58
75
  // previous machine-wide quota until a real rate-limit payload refreshes it instead of flashing empty.
59
76
  if (Object.keys(rateLimits).length > 0) {
60
77
  const tmp = `${file}.${process.pid}.tmp`;
61
- fs.writeFileSync(tmp, JSON.stringify(snap));
78
+ fs.writeFileSync(tmp, JSON.stringify(snap), { mode: 0o600 });
62
79
  fs.renameSync(tmp, file); // atomic: concurrent statuslines (multiple sessions) can't tear the snapshot
80
+ fs.chmodSync(file, 0o600);
63
81
  } else if (!previousHasQuota) {
64
82
  // Create the initial "capturer active, awaiting data" marker only if no concurrent session has
65
83
  // already published a real quota snapshot. An empty session can therefore never win that race.
66
- try { fs.writeFileSync(file, JSON.stringify(snap), { flag: 'wx' }); } catch { /* file now exists */ }
84
+ try { fs.writeFileSync(file, JSON.stringify(snap), { flag: 'wx', mode: 0o600 }); } catch { /* file now exists */ }
67
85
  }
68
86
 
69
87
  // Per-session context snapshot. The global file above is last-writer-wins across ALL sessions, so it
@@ -73,12 +91,13 @@ if (file) {
73
91
  const sid = typeof j.session_id === 'string' ? j.session_id : null;
74
92
  if (sid && /^[\w-]+$/.test(sid) && snap.context) {
75
93
  const cdir = path.join(path.dirname(file), 'context');
76
- fs.mkdirSync(cdir, { recursive: true });
94
+ ensurePrivateDirectory(cdir);
77
95
  const cfile = path.join(cdir, `${sid}.json`);
78
96
  const csnap = { sessionId: sid, model: snap.model, usedPercent: snap.context.usedPercent, updatedAt: snap.updatedAt };
79
97
  const ctmp = `${cfile}.${process.pid}.tmp`;
80
- fs.writeFileSync(ctmp, JSON.stringify(csnap));
98
+ fs.writeFileSync(ctmp, JSON.stringify(csnap), { mode: 0o600 });
81
99
  fs.renameSync(ctmp, cfile);
100
+ fs.chmodSync(cfile, 0o600);
82
101
  // Best-effort prune of stale session files (ended sessions never clean up after themselves) so the
83
102
  // dir can't grow without bound. A day is well past any live session's last statusLine render.
84
103
  try {
@@ -86,7 +105,10 @@ if (file) {
86
105
  for (const nm of fs.readdirSync(cdir)) {
87
106
  if (!nm.endsWith('.json')) continue;
88
107
  const fp = path.join(cdir, nm);
89
- try { if (fs.statSync(fp).mtimeMs < cutoff) fs.unlinkSync(fp); } catch { /* skip */ }
108
+ try {
109
+ fs.chmodSync(fp, 0o600);
110
+ if (fs.statSync(fp).mtimeMs < cutoff) fs.unlinkSync(fp);
111
+ } catch { /* skip */ }
90
112
  }
91
113
  } catch { /* prune is best-effort */ }
92
114
  }
@@ -16,7 +16,7 @@
16
16
  const fs = require('node:fs');
17
17
  const path = require('node:path');
18
18
 
19
- const [, , file, pane, src, ts, host = '', agent = ''] = process.argv;
19
+ const [, , file, pane, src, ts, host = ''] = process.argv;
20
20
  if (!file || !pane || !src) process.exit(0);
21
21
 
22
22
  let payload = {};
@@ -35,7 +35,26 @@ const nap = (ms) => {
35
35
  catch { const end = Date.now() + ms; while (Date.now() < end) { /* spin */ } }
36
36
  };
37
37
 
38
- try { fs.mkdirSync(path.dirname(file), { recursive: true }); } catch { /* ignore */ }
38
+ function ensurePrivateDirectory(directory) {
39
+ const resolved = path.resolve(directory);
40
+ const parsed = path.parse(resolved);
41
+ const parts = resolved.slice(parsed.root.length).split(path.sep).filter(Boolean);
42
+ const privateRoot = parts.indexOf('.handmux');
43
+ if (privateRoot < 0) {
44
+ fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
45
+ return;
46
+ }
47
+ for (let i = privateRoot; i < parts.length; i++) {
48
+ const current = path.join(parsed.root, ...parts.slice(0, i + 1));
49
+ fs.mkdirSync(current, { recursive: true, mode: 0o700 });
50
+ fs.chmodSync(current, 0o700);
51
+ }
52
+ }
53
+
54
+ try {
55
+ ensurePrivateDirectory(path.dirname(file));
56
+ if (fs.existsSync(file)) fs.chmodSync(file, 0o600);
57
+ } catch { /* ignore */ }
39
58
 
40
59
  function update() {
41
60
  let obj = {};
@@ -58,30 +77,19 @@ function update() {
58
77
  const curSid = obj[pane] && obj[pane].payload && obj[pane].payload.session_id;
59
78
  const endSid = payload && payload.session_id;
60
79
  if (!curSid || !endSid || curSid === endSid) delete obj[pane];
61
- } else if (src === 'resume' && agent === 'codex') {
62
- // Codex fires PostToolUse on EVERY tool call, so its resume exists purely to un-stick a pane from 需要你
63
- // back to 进行中 after the user approved a PermissionRequest. Apply it ONLY as that transition — a mid-
64
- // turn tool call (pane already 进行中 / 已完成) is a no-op, so we don't rewrite the entry on every
65
- // command (the load Claude's matcher avoids). Claude's resume — no agent arg — is unaffected.
66
- const prev = obj[pane];
67
- const prevPerm = prev && (prev.src === 'permreq'
68
- || (prev.src === 'notify' && (prev.payload || {}).notification_type === 'permission_prompt'));
69
- if (!prevPerm) { return; }
70
- obj[pane] = { ts: Number(ts) || 0, src, host, payload, agent };
71
80
  } else {
72
- // agent tag lets the server dispatch classify + liveness per agent (Codex passes 'codex'); omitted for
73
- // Claude so legacy entries stay byte-identical and default to claude server-side.
74
- obj[pane] = { ts: Number(ts) || 0, src, host, payload, ...(agent ? { agent } : {}) };
81
+ obj[pane] = { ts: Number(ts) || 0, src, host, payload };
75
82
  }
76
83
  const tmp = `${file}.${process.pid}.tmp`;
77
- fs.writeFileSync(tmp, JSON.stringify(obj));
84
+ fs.writeFileSync(tmp, JSON.stringify(obj), { mode: 0o600 });
78
85
  fs.renameSync(tmp, file); // atomic: a torn write can't corrupt the file
86
+ fs.chmodSync(file, 0o600);
79
87
  }
80
88
 
81
89
  const lock = `${file}.lock`;
82
90
  let held = false;
83
91
  for (let i = 0; i < 60 && !held; i++) { // ~0.9s budget, then write lockless (best-effort)
84
- try { fs.closeSync(fs.openSync(lock, 'wx')); held = true; } // O_EXCL → atomic "I hold it"
92
+ try { fs.closeSync(fs.openSync(lock, 'wx', 0o600)); held = true; } // O_EXCL → atomic "I hold it"
85
93
  catch {
86
94
  try { if (Date.now() - fs.statSync(lock).mtimeMs > 3000) fs.unlinkSync(lock); } catch { /* steal a stale lock */ }
87
95
  nap(15);
@@ -89,12 +97,3 @@ for (let i = 0; i < 60 && !held; i++) { // ~0.9s budget, t
89
97
  }
90
98
  try { update(); } catch { /* best effort */ }
91
99
  if (held) { try { fs.unlinkSync(lock); } catch { /* ignore */ } }
92
-
93
- // Codex hook payloads include the exact rollout transcript_path. Capture its latest token_count into a
94
- // machine-wide snapshot after the inbox update; failures are isolated so usage can never break the hook.
95
- if (agent === 'codex' && typeof payload.transcript_path === 'string') {
96
- try {
97
- const { captureTranscript } = require('./handmux-codex-usage.cjs');
98
- captureTranscript(payload.transcript_path, path.join(path.dirname(file), 'codex-usage.json'));
99
- } catch { /* best effort */ }
100
- }
@@ -0,0 +1,126 @@
1
+ {
2
+ "name": "handmux",
3
+ "version": "0.24.0",
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
+ "type": "module",
6
+ "license": "AGPL-3.0-only",
7
+ "author": "yuanyuanzijin",
8
+ "homepage": "https://handmux.com",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/handmux/handmux.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/handmux/handmux/issues"
15
+ },
16
+ "keywords": [
17
+ "tmux",
18
+ "mobile",
19
+ "terminal",
20
+ "claude-code",
21
+ "remote",
22
+ "pwa"
23
+ ],
24
+ "engines": {
25
+ "node": ">=20"
26
+ },
27
+ "bin": {
28
+ "handmux": "bin/handmux.js"
29
+ },
30
+ "files": [
31
+ "bin/handmux.js",
32
+ "dist",
33
+ "README.md",
34
+ "README.zh-CN.md",
35
+ "LICENSE"
36
+ ],
37
+ "scripts": {
38
+ "start": "npm run build:server && node dist/bin/handmux.js start",
39
+ "dev": "npm run build:server && node dist/bin/handmux.js start --foreground",
40
+ "handmux": "npm run build:server && node dist/bin/handmux.js",
41
+ "build:server": "node scripts/build-server.mjs",
42
+ "bundle": "node scripts/bundle-web.mjs",
43
+ "prepack": "npm run bundle && npm run build:server",
44
+ "typecheck:source": "node scripts/check-typescript-product-source.mjs",
45
+ "typecheck": "tsc --project tsconfig.json",
46
+ "test": "npm run build:server && vitest run",
47
+ "test:pack": "node scripts/pack-smoke.mjs",
48
+ "test:workspace-host": "npm run build:server && HANDMUX_WORKSPACE_HOST=1 vitest run test/workspaceHost.test.js",
49
+ "test:workspace-docker": "npm run build:server && HANDMUX_WORKSPACE_DOCKER=1 vitest run test/workspaceDocker.test.js"
50
+ },
51
+ "dependencies": {
52
+ "@clack/prompts": "0.11.0",
53
+ "busboy": "^1.6.0",
54
+ "compression": "^1.8.1",
55
+ "express": "^4.19.2",
56
+ "testcafe-hammerhead": "31.7.8",
57
+ "tunlite": "^0.10.0",
58
+ "web-push": "^3.6.7",
59
+ "ws": "^8.21.0"
60
+ },
61
+ "optionalDependencies": {
62
+ "qrcode-terminal": "^0.12.0"
63
+ },
64
+ "devDependencies": {
65
+ "@types/busboy": "^1.5.4",
66
+ "@types/compression": "^1.8.1",
67
+ "@types/express": "4.17.25",
68
+ "@types/node": "20.19.43",
69
+ "@types/supertest": "7.2.1",
70
+ "@types/web-push": "^3.6.4",
71
+ "@types/ws": "^8.18.1",
72
+ "supertest": "^7.0.0",
73
+ "typescript": "6.0.3",
74
+ "vitest": "^2.0.0"
75
+ },
76
+ "whatsNew": [
77
+ {
78
+ "version": "0.24.0",
79
+ "date": "2026-08-11",
80
+ "zh": "Codex 任务进度与 Goal 原生上线",
81
+ "en": "Native Codex task progress and Goals"
82
+ },
83
+ {
84
+ "version": "0.23.0",
85
+ "date": "2026-08-09",
86
+ "zh": "Codex 托管对话正式上线",
87
+ "en": "Managed Codex chat is here"
88
+ },
89
+ {
90
+ "version": "0.22.1",
91
+ "date": "2026-08-04",
92
+ "zh": "网页宽度与网站版本分开控制",
93
+ "en": "Separate page width and site-version controls"
94
+ },
95
+ {
96
+ "version": "0.22.0",
97
+ "date": "2026-08-03",
98
+ "zh": "全屏设置 · 窗口与分屏管理更清晰",
99
+ "en": "Full-screen Settings · clearer window and pane management"
100
+ },
101
+ {
102
+ "version": "0.21.0",
103
+ "date": "2026-08-03",
104
+ "zh": "网页预览:手机直连、电脑代理与静态目录",
105
+ "en": "Web preview: direct, computer proxy, and static folders"
106
+ },
107
+ {
108
+ "version": "0.20.3",
109
+ "date": "2026-08-01",
110
+ "zh": "实时终端显示更稳定流畅",
111
+ "en": "More stable, smoother live terminals"
112
+ },
113
+ {
114
+ "version": "0.20.2",
115
+ "date": "2026-07-27",
116
+ "zh": "实时终端长时间运行更流畅",
117
+ "en": "Smoother long-running live terminals"
118
+ },
119
+ {
120
+ "version": "0.20.1",
121
+ "date": "2026-07-26",
122
+ "zh": "聊天发送修复 · 电脑键盘不中断",
123
+ "en": "Reliable chat sending · uninterrupted desktop input"
124
+ }
125
+ ]
126
+ }