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.
- package/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# handmux 上报 hook. $1 = stop | notify | prompt | end | resume | permreq | compacting | compact | stopfail.
|
|
3
|
+
# (compacting/compact/stopfail 是版本门控事件:PreCompact/PostCompact/StopFailure,只在够新的 Claude 上注册。)
|
|
4
|
+
# stdin = Claude Code 原始 payload(JSON).
|
|
5
|
+
# (resume = PostToolUse on AskUserQuestion/ExitPlanMode:答完选项/批准计划 → 状态翻回进行中、带所选项。)
|
|
6
|
+
# (permreq = PermissionRequest:真实弹框一出现就发、带 tool_name → 比 permission_prompt 早亮「需要你」。)
|
|
7
|
+
# 只做本地落盘:更新 JSON latest state,并在启用时追加有界 Bridge event spool。不联网、
|
|
8
|
+
# 不依赖服务进程是否在跑 → 永不阻塞 Claude(始终 exit 0)。服务端/Connector 稍后消费这些文件。
|
|
9
|
+
# 真正的读-改-写交给同目录的 handmux-write.js(node:真 JSON 解析 + 文件锁,多 pane 并发 hook 不丢更新)。
|
|
10
|
+
CFG="$(dirname "$0")/handmux-notify.env"
|
|
11
|
+
[ -f "$CFG" ] && . "$CFG"
|
|
12
|
+
PANE="${CLAUDE_PANE:-$TMUX_PANE}"
|
|
13
|
+
[ -z "$PANE" ] && exit 0 # 不在 tmux 里(没有 pane 可定位)→ 暂不记录
|
|
14
|
+
FILE="${HANDMUX_STATE:-$HOME/.claude/handmux-state.json}"
|
|
15
|
+
# 毫秒时间戳:与旧实现的 Date.now() 同单位(客户端已阅水位线兼容)。优先 perl,退化到 秒×1000。
|
|
16
|
+
TS=$(perl -MTime::HiRes -e 'printf "%.0f", Time::HiRes::time()*1000' 2>/dev/null)
|
|
17
|
+
[ -z "$TS" ] && TS=$(( $(date +%s) * 1000 ))
|
|
18
|
+
HOST=$(hostname 2>/dev/null || printf '')
|
|
19
|
+
# Capture the owning Claude process rather than this short-lived Hook process. The fingerprint lets the
|
|
20
|
+
# Connector reject an offline event after tmux has reused the pane for a different Claude generation.
|
|
21
|
+
# Missing/incomplete identity is intentionally tolerated for older Claude/platform variants: the writer
|
|
22
|
+
# then omits the additive field and the legacy latest-state path keeps working.
|
|
23
|
+
CLAUDE_PID=''
|
|
24
|
+
CLAUDE_STARTED_AT=''
|
|
25
|
+
CLAUDE_TTY=''
|
|
26
|
+
# Claude runs configured hooks asynchronously and may reparent the short-lived shell before it starts, so
|
|
27
|
+
# PPID is not authoritative. Resolve the exact `claude` process on this tmux pane's TTY first;
|
|
28
|
+
# retain ancestor walking only as a fallback for platforms where tmux/targeted ps is unavailable.
|
|
29
|
+
PANE_TTY=$(tmux display-message -p -t "$PANE" '#{pane_tty}' 2>/dev/null || printf '')
|
|
30
|
+
if [ -n "$PANE_TTY" ]; then
|
|
31
|
+
# During an async Hook Claude may block waiting for the child and temporarily lose the foreground `+`.
|
|
32
|
+
# Prefer the sole exact process; if more than one exact Claude shares the TTY, require one foreground owner.
|
|
33
|
+
CLAUDE_PROCESSES=$(ps -t "${PANE_TTY#/dev/}" -o pid=,stat=,comm= 2>/dev/null || printf '')
|
|
34
|
+
CLAUDE_PID=$(printf '%s\n' "$CLAUDE_PROCESSES" \
|
|
35
|
+
| awk '$3 == "claude" { count += 1; only = $1 } END { if (count == 1) print only }')
|
|
36
|
+
if [ -z "$CLAUDE_PID" ]; then
|
|
37
|
+
CLAUDE_PID=$(printf '%s\n' "$CLAUDE_PROCESSES" \
|
|
38
|
+
| awk '$3 == "claude" && $2 ~ /\+/ { count += 1; only = $1 } END { if (count == 1) print only }')
|
|
39
|
+
fi
|
|
40
|
+
fi
|
|
41
|
+
if [ -z "$CLAUDE_PID" ]; then
|
|
42
|
+
ANCESTOR_PID=$PPID
|
|
43
|
+
ANCESTOR_DEPTH=0
|
|
44
|
+
while [ -n "$ANCESTOR_PID" ] && [ "$ANCESTOR_PID" -gt 1 ] 2>/dev/null && [ "$ANCESTOR_DEPTH" -lt 32 ]; do
|
|
45
|
+
ANCESTOR_COMMAND=$(ps -p "$ANCESTOR_PID" -o comm= 2>/dev/null || printf '')
|
|
46
|
+
ANCESTOR_COMMAND=${ANCESTOR_COMMAND##*/}
|
|
47
|
+
if [ "$ANCESTOR_COMMAND" = 'claude' ]; then
|
|
48
|
+
CLAUDE_PID=$ANCESTOR_PID
|
|
49
|
+
break
|
|
50
|
+
fi
|
|
51
|
+
ANCESTOR_PID=$(ps -p "$ANCESTOR_PID" -o ppid= 2>/dev/null | tr -d ' ')
|
|
52
|
+
ANCESTOR_DEPTH=$((ANCESTOR_DEPTH + 1))
|
|
53
|
+
done
|
|
54
|
+
fi
|
|
55
|
+
if [ -n "$CLAUDE_PID" ]; then
|
|
56
|
+
# `lstart` follows the process locale on macOS; force C so standalone Node can parse it consistently.
|
|
57
|
+
CLAUDE_STARTED_AT=$(LC_ALL=C ps -p "$CLAUDE_PID" -o lstart= 2>/dev/null || printf '')
|
|
58
|
+
CLAUDE_TTY=$(ps -p "$CLAUDE_PID" -o tty= 2>/dev/null || printf '')
|
|
59
|
+
fi
|
|
60
|
+
# payload 经 stdin 原样流给 node(不在 shell 里转义,避免坏数据);pane 含 '%' 直接进 JSON 字段,
|
|
61
|
+
# 不再进 URL → 彻底告别旧的 "%110 被 url-decode 丢弃" 那类坑。
|
|
62
|
+
node "$(dirname "$0")/handmux-write.cjs" "$FILE" "$PANE" "$1" "$TS" "$HOST" \
|
|
63
|
+
"${HANDMUX_CLAUDE_EVENTS:-}" "$CLAUDE_PID" "$CLAUDE_STARTED_AT" "$CLAUDE_TTY" 2>/dev/null || true
|
|
64
|
+
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// handmux hook writer. Updates one JSON latest-state file keyed by tmux pane and, when configured,
|
|
3
|
+
// appends the same effective lifecycle edge to a bounded durable Connector spool. Invoked by notify.sh:
|
|
4
|
+
// node handmux-write.cjs <file> <pane> <src> <ts> <host> <event-dir>
|
|
5
|
+
// [claude-pid] [claude-lstart] [claude-tty]
|
|
6
|
+
//
|
|
7
|
+
// .cjs (not .js): runs standalone via `node <file>`, including from ~/.claude/hooks (no package.json) —
|
|
8
|
+
// .cjs forces CommonJS everywhere; a bare .js under the server tree ("type":"module") would be ESM and
|
|
9
|
+
// break require().
|
|
10
|
+
//
|
|
11
|
+
// Why node (not pure shell): the file is a single JSON object, so each event is a read-modify-write that
|
|
12
|
+
// must parse JSON and not clobber other panes — shell can't do that safely. The user runs many Claude
|
|
13
|
+
// panes at once, so hooks fire concurrently: we take a short O_EXCL lock (stealing a stale one) around
|
|
14
|
+
// the read-modify-write and replace atomically (tmp + rename), so concurrent writers don't lose updates
|
|
15
|
+
// or corrupt the file. Best-effort throughout and silent — the hook is fire-and-forget and must never
|
|
16
|
+
// fail Claude (the shell wrapper swallows errors and always exits 0).
|
|
17
|
+
const fs = require('node:fs');
|
|
18
|
+
const path = require('node:path');
|
|
19
|
+
|
|
20
|
+
const crypto = require('node:crypto');
|
|
21
|
+
|
|
22
|
+
const [
|
|
23
|
+
, , file, pane, src, ts, host = '', eventDirectory = '',
|
|
24
|
+
claudePid = '', claudeStartedAt = '', claudeTty = '',
|
|
25
|
+
] = process.argv;
|
|
26
|
+
if (!file || !pane || !src) process.exit(0);
|
|
27
|
+
|
|
28
|
+
function processFingerprint() {
|
|
29
|
+
const pid = Number(String(claudePid).trim());
|
|
30
|
+
const startedAt = Date.parse(String(claudeStartedAt).trim());
|
|
31
|
+
const rawTty = String(claudeTty).trim();
|
|
32
|
+
if (!Number.isSafeInteger(pid) || pid <= 0 || !Number.isFinite(startedAt)
|
|
33
|
+
|| !rawTty || rawTty === '?' || rawTty === '??') return undefined;
|
|
34
|
+
return {
|
|
35
|
+
pid,
|
|
36
|
+
startedAt,
|
|
37
|
+
tty: rawTty.startsWith('/dev/') ? rawTty : `/dev/${rawTty}`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sourceProcess = processFingerprint();
|
|
42
|
+
|
|
43
|
+
function sameSourceProcess(previous) {
|
|
44
|
+
const prior = previous?.process;
|
|
45
|
+
if (!sourceProcess || !prior) return true; // retain compatibility with rows from older Hook versions
|
|
46
|
+
return prior.pid === sourceProcess.pid
|
|
47
|
+
&& prior.startedAt === sourceProcess.startedAt
|
|
48
|
+
&& prior.tty === sourceProcess.tty;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let payload = {};
|
|
52
|
+
try { payload = JSON.parse(fs.readFileSync(0, 'utf8') || '{}'); } catch { /* unreadable stdin → {} */ }
|
|
53
|
+
|
|
54
|
+
// idle_prompt ("been idle ~60s") is decided in update() against the pane's PRIOR state: it either trails
|
|
55
|
+
// a resting state (done/needs → drop, so it can't bump ts and re-surface an already-cleared 已完成) or it
|
|
56
|
+
// terminates an ESC-interrupted working turn (→ clear the stuck 进行中). Flag it here; the read-modify-
|
|
57
|
+
// write under the lock — the only place we can read the prior state safely — makes the call.
|
|
58
|
+
const isIdle = src === 'notify' && payload && payload.notification_type === 'idle_prompt';
|
|
59
|
+
|
|
60
|
+
// Synchronous nap without busy-spinning (the hook runs async, so a few ms is free). SharedArrayBuffer
|
|
61
|
+
// may be unavailable in odd runtimes — fall back to a tiny busy loop so the lock retry still paces.
|
|
62
|
+
const nap = (ms) => {
|
|
63
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); }
|
|
64
|
+
catch { const end = Date.now() + ms; while (Date.now() < end) { /* spin */ } }
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
function ensurePrivateDirectory(directory) {
|
|
68
|
+
const resolved = path.resolve(directory);
|
|
69
|
+
const parsed = path.parse(resolved);
|
|
70
|
+
const parts = resolved.slice(parsed.root.length).split(path.sep).filter(Boolean);
|
|
71
|
+
const privateRoot = parts.indexOf('.handmux');
|
|
72
|
+
if (privateRoot < 0) {
|
|
73
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (let i = privateRoot; i < parts.length; i++) {
|
|
77
|
+
const current = path.join(parsed.root, ...parts.slice(0, i + 1));
|
|
78
|
+
fs.mkdirSync(current, { recursive: true, mode: 0o700 });
|
|
79
|
+
fs.chmodSync(current, 0o700);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const MAX_EVENT_SPOOL_BYTES = 16 * 1024 * 1024;
|
|
84
|
+
|
|
85
|
+
function readEvent(fileName) {
|
|
86
|
+
try {
|
|
87
|
+
const value = JSON.parse(fs.readFileSync(fileName, 'utf8'));
|
|
88
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
89
|
+
} catch { return null; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function allocateSequence(minimum = 0) {
|
|
93
|
+
if (!eventDirectory) return null;
|
|
94
|
+
ensurePrivateDirectory(eventDirectory);
|
|
95
|
+
const sequenceFile = path.join(eventDirectory, '.sequence');
|
|
96
|
+
let current = Number.isSafeInteger(minimum) && minimum > 0 ? minimum : 0;
|
|
97
|
+
try {
|
|
98
|
+
const parsed = Number(fs.readFileSync(sequenceFile, 'utf8'));
|
|
99
|
+
if (Number.isSafeInteger(parsed) && parsed >= 0) current = Math.max(current, parsed);
|
|
100
|
+
} catch { /* first event */ }
|
|
101
|
+
if (current === 0) {
|
|
102
|
+
try {
|
|
103
|
+
for (const name of fs.readdirSync(eventDirectory)) {
|
|
104
|
+
const match = /^event-(\d+)-/.exec(name);
|
|
105
|
+
if (match) current = Math.max(current, Number(match[1]) || 0);
|
|
106
|
+
}
|
|
107
|
+
} catch { /* empty spool */ }
|
|
108
|
+
}
|
|
109
|
+
const next = current + 1;
|
|
110
|
+
const temporary = `${sequenceFile}.${process.pid}.tmp`;
|
|
111
|
+
fs.writeFileSync(temporary, String(next), { mode: 0o600 });
|
|
112
|
+
fs.renameSync(temporary, sequenceFile);
|
|
113
|
+
fs.chmodSync(sequenceFile, 0o600);
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function safeSequence(minimum = 0) {
|
|
118
|
+
try { return allocateSequence(minimum); } catch { return null; }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function writeGapMarker(value) {
|
|
122
|
+
if (!value || !value.paneId) return;
|
|
123
|
+
const paneKey = crypto.createHash('sha256').update(String(value.paneId)).digest('hex').slice(0, 16);
|
|
124
|
+
const marker = path.join(eventDirectory, `gap-${paneKey}.json`);
|
|
125
|
+
const temporary = `${marker}.${process.pid}.tmp`;
|
|
126
|
+
fs.writeFileSync(temporary, JSON.stringify({
|
|
127
|
+
version: 1,
|
|
128
|
+
type: 'gap',
|
|
129
|
+
agent: 'claude',
|
|
130
|
+
eventId: `claude-gap-${Date.now()}-${process.pid}`,
|
|
131
|
+
paneId: value.paneId,
|
|
132
|
+
sessionId: typeof value.sessionId === 'string' ? value.sessionId : undefined,
|
|
133
|
+
...(value.process ? { process: value.process } : {}),
|
|
134
|
+
}), { mode: 0o600 });
|
|
135
|
+
fs.renameSync(temporary, marker);
|
|
136
|
+
fs.chmodSync(marker, 0o600);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function pruneEventSpool() {
|
|
140
|
+
const entries = [];
|
|
141
|
+
let total = 0;
|
|
142
|
+
for (const name of fs.readdirSync(eventDirectory)) {
|
|
143
|
+
if (!/^event-\d+-\d+\.json$/.test(name)) continue;
|
|
144
|
+
const eventFile = path.join(eventDirectory, name);
|
|
145
|
+
try {
|
|
146
|
+
const size = fs.statSync(eventFile).size;
|
|
147
|
+
entries.push({ name, file: eventFile, size });
|
|
148
|
+
total += size;
|
|
149
|
+
} catch { /* concurrently consumed */ }
|
|
150
|
+
}
|
|
151
|
+
entries.sort((first, second) => first.name.localeCompare(second.name));
|
|
152
|
+
const gaps = new Map();
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
if (total <= MAX_EVENT_SPOOL_BYTES) break;
|
|
155
|
+
const dropped = readEvent(entry.file);
|
|
156
|
+
if (dropped?.paneId) gaps.set(dropped.paneId, {
|
|
157
|
+
paneId: dropped.paneId,
|
|
158
|
+
sessionId: dropped.sessionId,
|
|
159
|
+
process: dropped.process,
|
|
160
|
+
});
|
|
161
|
+
try { fs.unlinkSync(entry.file); total -= entry.size; } catch { /* already consumed */ }
|
|
162
|
+
}
|
|
163
|
+
for (const gap of gaps.values()) writeGapMarker(gap);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function writeBridgeEvent(sequence, rawTs, payload) {
|
|
167
|
+
if (!eventDirectory || !Number.isSafeInteger(sequence)) return;
|
|
168
|
+
const sessionId = typeof payload?.session_id === 'string' ? payload.session_id : undefined;
|
|
169
|
+
const event = {
|
|
170
|
+
version: 1,
|
|
171
|
+
type: 'event',
|
|
172
|
+
agent: 'claude',
|
|
173
|
+
eventId: `claude-hook-${sequence}`,
|
|
174
|
+
sequence,
|
|
175
|
+
paneId: pane,
|
|
176
|
+
src,
|
|
177
|
+
sourceOccurredAt: Number(rawTs) || 0,
|
|
178
|
+
...(sessionId ? { sessionId } : {}),
|
|
179
|
+
...(sourceProcess ? { process: sourceProcess } : {}),
|
|
180
|
+
payload,
|
|
181
|
+
};
|
|
182
|
+
const final = path.join(
|
|
183
|
+
eventDirectory,
|
|
184
|
+
`event-${String(sequence).padStart(16, '0')}-${process.pid}.json`,
|
|
185
|
+
);
|
|
186
|
+
const temporary = `${final}.tmp`;
|
|
187
|
+
fs.writeFileSync(temporary, JSON.stringify(event), { mode: 0o600 });
|
|
188
|
+
fs.renameSync(temporary, final);
|
|
189
|
+
fs.chmodSync(final, 0o600);
|
|
190
|
+
pruneEventSpool();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
ensurePrivateDirectory(path.dirname(file));
|
|
195
|
+
if (fs.existsSync(file)) fs.chmodSync(file, 0o600);
|
|
196
|
+
} catch { /* ignore */ }
|
|
197
|
+
|
|
198
|
+
function update() {
|
|
199
|
+
let obj = {};
|
|
200
|
+
try { const j = JSON.parse(fs.readFileSync(file, 'utf8')); if (j && typeof j === 'object' && !Array.isArray(j)) obj = j; }
|
|
201
|
+
catch { /* fresh / corrupt / half-written → start clean */ }
|
|
202
|
+
const previous = obj[pane];
|
|
203
|
+
const prevSrc = previous && previous.src;
|
|
204
|
+
const sequenceFloor = Object.values(obj).reduce((maximum, value) => (
|
|
205
|
+
Number.isSafeInteger(value?.sequence) ? Math.max(maximum, value.sequence) : maximum
|
|
206
|
+
), 0);
|
|
207
|
+
let applied = true;
|
|
208
|
+
if (isIdle) {
|
|
209
|
+
// idle after a resting state (done/needs/nothing) is just the "still waiting" reminder → drop and
|
|
210
|
+
// leave the file as it was (recording it would bump ts and re-surface an already-cleared 已完成).
|
|
211
|
+
// idle after a WORKING turn (prompt/resume) that never got a Stop = an ESC interrupt / walk-away —
|
|
212
|
+
// no Stop hook fires there, so idle is the only signal the turn ended. Without this the pane would
|
|
213
|
+
// stay latched at 进行中 forever; treat it as a soft end and clear the pane.
|
|
214
|
+
if (prevSrc === 'prompt' || prevSrc === 'resume') { delete obj[pane]; }
|
|
215
|
+
else applied = false; // resting → drop without writing
|
|
216
|
+
} else if (src === 'notify' && payload?.notification_type === 'permission_prompt'
|
|
217
|
+
&& prevSrc === 'permreq'
|
|
218
|
+
&& sameSourceProcess(previous)
|
|
219
|
+
&& (!previous?.payload?.session_id || !payload?.session_id
|
|
220
|
+
|| previous.payload.session_id === payload.session_id)) {
|
|
221
|
+
// PermissionRequest is the authoritative opening edge. Claude later emits a generic Notification for
|
|
222
|
+
// the same blocked gate; retaining the earlier detailed row also prevents a second canonical unread.
|
|
223
|
+
applied = false;
|
|
224
|
+
} else if (src === 'end') {
|
|
225
|
+
// SessionEnd drops the pane on a clean exit. But /clear (and /resume) END the old session AND START a
|
|
226
|
+
// new one — SessionEnd(old) + SessionStart(new) fire as two async hooks, in either order. A late end for
|
|
227
|
+
// the OLD session must NOT wipe the NEW session's binding: only drop when what we currently hold IS the
|
|
228
|
+
// ending session (or neither side carries a session_id to compare — preserve the plain clean-exit drop).
|
|
229
|
+
const curSid = obj[pane] && obj[pane].payload && obj[pane].payload.session_id;
|
|
230
|
+
const endSid = payload && payload.session_id;
|
|
231
|
+
if (!curSid || !endSid || curSid === endSid) delete obj[pane];
|
|
232
|
+
else applied = false;
|
|
233
|
+
} else {
|
|
234
|
+
// sequence is allocated under the same pane-state lock, so independently spawned async hooks share
|
|
235
|
+
// one durable source order. Existing readers ignore the additive field.
|
|
236
|
+
const sequence = safeSequence(sequenceFloor);
|
|
237
|
+
obj[pane] = {
|
|
238
|
+
ts: Number(ts) || 0, src, host, payload, agent: 'claude',
|
|
239
|
+
...(sourceProcess ? { process: sourceProcess } : {}),
|
|
240
|
+
...(sequence === null ? {} : { sequence }),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (!applied) return;
|
|
244
|
+
const sequence = src === 'end' || isIdle ? safeSequence(sequenceFloor) : obj[pane]?.sequence;
|
|
245
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
246
|
+
fs.writeFileSync(tmp, JSON.stringify(obj), { mode: 0o600 });
|
|
247
|
+
fs.renameSync(tmp, file); // atomic: a torn write can't corrupt the file
|
|
248
|
+
fs.chmodSync(file, 0o600);
|
|
249
|
+
try { writeBridgeEvent(sequence, ts, payload); } catch { /* latest snapshot still recovers */ }
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const lock = `${file}.lock`;
|
|
253
|
+
let held = false;
|
|
254
|
+
for (let i = 0; i < 60 && !held; i++) { // ~0.9s budget; never corrupt order locklessly
|
|
255
|
+
try { fs.closeSync(fs.openSync(lock, 'wx', 0o600)); held = true; } // O_EXCL → atomic "I hold it"
|
|
256
|
+
catch {
|
|
257
|
+
try { if (Date.now() - fs.statSync(lock).mtimeMs > 3000) fs.unlinkSync(lock); } catch { /* steal a stale lock */ }
|
|
258
|
+
nap(15);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (held) {
|
|
262
|
+
try { update(); } catch { /* best effort */ }
|
|
263
|
+
try { fs.unlinkSync(lock); } catch { /* ignore */ }
|
|
264
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "handmux",
|
|
3
|
+
"version": "0.25.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": ">=22.16.0"
|
|
26
|
+
},
|
|
27
|
+
"bin": {
|
|
28
|
+
"handmux": "bin/handmux.js"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"bin/handmux.js",
|
|
32
|
+
"bin/node-version.js",
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"README.zh-CN.md",
|
|
36
|
+
"LICENSE"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"start": "npm run build:server && node dist/bin/handmux.js start",
|
|
40
|
+
"dev": "npm run build:server && node dist/bin/handmux.js start --foreground",
|
|
41
|
+
"handmux": "npm run build:server && node dist/bin/handmux.js",
|
|
42
|
+
"build:server": "node scripts/build-server.mjs",
|
|
43
|
+
"bundle": "node scripts/bundle-web.mjs",
|
|
44
|
+
"prepack": "npm run bundle && npm run build:server",
|
|
45
|
+
"typecheck:source": "node scripts/check-typescript-product-source.mjs",
|
|
46
|
+
"typecheck": "tsc --project tsconfig.json",
|
|
47
|
+
"test": "npm run build:server && vitest run",
|
|
48
|
+
"test:pack": "node scripts/pack-smoke.mjs",
|
|
49
|
+
"test:workspace-host": "npm run build:server && HANDMUX_WORKSPACE_HOST=1 vitest run test/workspaceHost.test.js",
|
|
50
|
+
"test:workspace-docker": "npm run build:server && HANDMUX_WORKSPACE_DOCKER=1 vitest run test/workspaceDocker.test.js"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@clack/prompts": "0.11.0",
|
|
54
|
+
"busboy": "^1.6.0",
|
|
55
|
+
"compression": "^1.8.1",
|
|
56
|
+
"express": "^4.19.2",
|
|
57
|
+
"testcafe-hammerhead": "31.7.8",
|
|
58
|
+
"tunlite": "^0.10.0",
|
|
59
|
+
"web-push": "^3.6.7",
|
|
60
|
+
"ws": "^8.21.0"
|
|
61
|
+
},
|
|
62
|
+
"optionalDependencies": {
|
|
63
|
+
"qrcode-terminal": "^0.12.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/busboy": "^1.5.4",
|
|
67
|
+
"@types/compression": "^1.8.1",
|
|
68
|
+
"@types/express": "4.17.25",
|
|
69
|
+
"@types/node": "22.20.1",
|
|
70
|
+
"@types/supertest": "7.2.1",
|
|
71
|
+
"@types/web-push": "^3.6.4",
|
|
72
|
+
"@types/ws": "^8.18.1",
|
|
73
|
+
"supertest": "^7.0.0",
|
|
74
|
+
"typescript": "6.0.3",
|
|
75
|
+
"vitest": "^2.0.0"
|
|
76
|
+
},
|
|
77
|
+
"whatsNew": [
|
|
78
|
+
{
|
|
79
|
+
"version": "0.25.0",
|
|
80
|
+
"date": "2026-09-01",
|
|
81
|
+
"zh": "Pi 原生接入 · 多 Agent 对话更可靠",
|
|
82
|
+
"en": "Native Pi support · more reliable agent chat"
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
"version": "0.24.0",
|
|
86
|
+
"date": "2026-08-11",
|
|
87
|
+
"zh": "Codex 任务进度与 Goal 原生上线",
|
|
88
|
+
"en": "Native Codex task progress and Goals"
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
"version": "0.23.0",
|
|
92
|
+
"date": "2026-08-09",
|
|
93
|
+
"zh": "Codex 托管对话正式上线",
|
|
94
|
+
"en": "Managed Codex chat is here"
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
"version": "0.22.1",
|
|
98
|
+
"date": "2026-08-04",
|
|
99
|
+
"zh": "网页宽度与网站版本分开控制",
|
|
100
|
+
"en": "Separate page width and site-version controls"
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"version": "0.22.0",
|
|
104
|
+
"date": "2026-08-03",
|
|
105
|
+
"zh": "全屏设置 · 窗口与分屏管理更清晰",
|
|
106
|
+
"en": "Full-screen Settings · clearer window and pane management"
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
"version": "0.21.0",
|
|
110
|
+
"date": "2026-08-03",
|
|
111
|
+
"zh": "网页预览:手机直连、电脑代理与静态目录",
|
|
112
|
+
"en": "Web preview: direct, computer proxy, and static folders"
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
"version": "0.20.3",
|
|
116
|
+
"date": "2026-08-01",
|
|
117
|
+
"zh": "实时终端显示更稳定流畅",
|
|
118
|
+
"en": "More stable, smoother live terminals"
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"version": "0.20.2",
|
|
122
|
+
"date": "2026-07-27",
|
|
123
|
+
"zh": "实时终端长时间运行更流畅",
|
|
124
|
+
"en": "Smoother long-running live terminals"
|
|
125
|
+
}
|
|
126
|
+
]
|
|
127
|
+
}
|