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
package/src/cli/claudeHooks.js
DELETED
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
// Install/uninstall the Claude Code lifecycle hooks that feed the handmux inbox. This ports the idempotent
|
|
2
|
-
// merge from scripts/install-hooks.sh into testable JS so the OSS CLI (and the phone, via the server) can
|
|
3
|
-
// turn the inbox on — opt-in, since writing ~/.claude/settings.json edits another tool's config.
|
|
4
|
-
//
|
|
5
|
-
// Iron rule: only ever touch ~/.handmux/ and — after explicit opt-in — ~/.claude/. If ~/.claude is absent
|
|
6
|
-
// (no Claude Code), skip and report 'no-claude'; never create it.
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import { homedir } from 'node:os';
|
|
10
|
-
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { writeJsonAtomic, deployHookScripts, removeHookScripts } from './hookScaffold.js';
|
|
12
|
-
|
|
13
|
-
// The seven events the inbox reads. src is the arg passed to handmux-notify.sh; only PostToolUse is scoped to
|
|
14
|
-
// a matcher (the two "需要你" interaction tools) — every other Read/Bash/Edit must NOT wake the hook, or
|
|
15
|
-
// many concurrent Claude panes would each spawn the hook on every tool call. Keep this table in sync with
|
|
16
|
-
// the hook scripts in ../../hooks.
|
|
17
|
-
// SessionStart binds the pane→session mapping the instant a session begins — critically after /clear, which
|
|
18
|
-
// starts a NEW transcript file. Without it, /clear's SessionEnd(old) drops the pane and nothing rebinds it
|
|
19
|
-
// until the next prompt, so the 对话 lens goes blank / falls back to the ambiguous cwd→newest-jsonl guess.
|
|
20
|
-
// Fires only at session boundaries (startup/clear/resume), never per tool call, so it adds no hot-path load;
|
|
21
|
-
// classified neutral (no push, no roster entry — see agents/claude.js). Base, not version-gated: SessionStart
|
|
22
|
-
// is a long-standing lifecycle hook every supported Claude recognises.
|
|
23
|
-
export const HOOK_EVENTS = [
|
|
24
|
-
{ event: 'Stop', src: 'stop' },
|
|
25
|
-
{ event: 'Notification', src: 'notify' },
|
|
26
|
-
{ event: 'UserPromptSubmit', src: 'prompt' },
|
|
27
|
-
{ event: 'SessionStart', src: 'start' },
|
|
28
|
-
{ event: 'SessionEnd', src: 'end' },
|
|
29
|
-
{ event: 'PostToolUse', src: 'resume', matcher: 'AskUserQuestion|ExitPlanMode' },
|
|
30
|
-
{ event: 'PermissionRequest', src: 'permreq' },
|
|
31
|
-
];
|
|
32
|
-
|
|
33
|
-
// Version-gated events, only registered on a Claude Code new enough to EMIT them. Older Claude does not
|
|
34
|
-
// recognise these event names — and the docs give NO guarantee it ignores unknown ones (it "likely" does,
|
|
35
|
-
// but might reject the settings file) — so we NEVER write an event a version can't handle. Each carries a
|
|
36
|
-
// minVersion; below it (or when the version can't be detected) the event isn't written at all → pure
|
|
37
|
-
// downgrade, never an error. The pane instead self-heals via the existing idle_prompt(~60s) fallback.
|
|
38
|
-
// PreCompact → 压缩中 shown while compaction runs (honest progress for a slow op).
|
|
39
|
-
// PostCompact → clears the 压缩中/进行中 state the instant compaction finishes.
|
|
40
|
-
// StopFailure → a turn that died on an API error (rate limit / overload / …) fires NO Stop, so without
|
|
41
|
-
// this the pane sticks at 进行中 forever; maps to an 'error' state.
|
|
42
|
-
// `pairWith` welds an invariant (learned the hard way — never light a state you can't turn off): PreCompact
|
|
43
|
-
// is installed ONLY when its clearer PostCompact is also installed. minVersion is set conservatively to the
|
|
44
|
-
// version we've actually verified emits these (2.1.207); lower it only after test-firing an older build.
|
|
45
|
-
const COMPACT_MIN = '2.1.207';
|
|
46
|
-
export const HOOK_EVENTS_EXT = [
|
|
47
|
-
{ event: 'PostCompact', src: 'compact', minVersion: COMPACT_MIN },
|
|
48
|
-
{ event: 'PreCompact', src: 'compacting', minVersion: COMPACT_MIN, pairWith: 'PostCompact' },
|
|
49
|
-
{ event: 'StopFailure', src: 'stopfail', minVersion: COMPACT_MIN },
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const HOOK_MARK = 'handmux-notify.sh'; // identifies our hooks among the user's own
|
|
53
|
-
|
|
54
|
-
// Parse `claude --version` output ("2.1.207 (Claude Code)") → { major, minor, patch } | null.
|
|
55
|
-
export function parseClaudeVersion(out) {
|
|
56
|
-
const m = /(\d+)\.(\d+)\.(\d+)/.exec(String(out || ''));
|
|
57
|
-
return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// v >= min ("X.Y.Z"), full major.minor.patch compare. A null/undefined version is always below → fail-closed.
|
|
61
|
-
export function claudeVersionAtLeast(v, minStr) {
|
|
62
|
-
if (!v) return false;
|
|
63
|
-
const [a, b, c] = String(minStr).split('.').map(Number);
|
|
64
|
-
if (v.major !== a) return v.major > a;
|
|
65
|
-
if (v.minor !== b) return v.minor > b;
|
|
66
|
-
return v.patch >= c;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Detect the installed Claude Code version, or null if `claude` can't be run/parsed (→ ext hooks skipped).
|
|
70
|
-
export function detectClaudeVersion(exec = spawnSync) {
|
|
71
|
-
try {
|
|
72
|
-
const r = exec('claude', ['--version'], { encoding: 'utf8', timeout: 4000 });
|
|
73
|
-
if (!r || r.status !== 0 || !r.stdout) return null;
|
|
74
|
-
return parseClaudeVersion(r.stdout);
|
|
75
|
-
} catch { return null; }
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Drop OUR hook from settings.hooks[event] (used to prune an ext event that no longer passes the version
|
|
79
|
-
// gate — e.g. Claude was downgraded after a newer install). Mutates the passed hooks object.
|
|
80
|
-
function dropOurHook(hooks, event) {
|
|
81
|
-
if (!hooks[event]) return;
|
|
82
|
-
const kept = hooks[event]
|
|
83
|
-
.map((g) => ({ ...g, hooks: (g.hooks || []).filter((h) => !(typeof h.command === 'string' && h.command.includes(HOOK_MARK))) }))
|
|
84
|
-
.filter((g) => (g.hooks || []).length > 0);
|
|
85
|
-
if (kept.length) hooks[event] = kept; else delete hooks[event];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// True if `settings.hooks[event]` already has one of our hooks (command references the dest script).
|
|
89
|
-
function alreadyHas(hooks, event) {
|
|
90
|
-
return (hooks[event] || []).some((g) => (g.hooks || []).some(
|
|
91
|
-
(h) => typeof h.command === 'string' && h.command.includes(HOOK_MARK)));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Merge one event's hook group into `hooks` (mutates), idempotently — a no-op if one of ours is already
|
|
95
|
-
// registered for that event, so it's safe to call on every install/sync. Shared by mergeHooks and syncHooks.
|
|
96
|
-
function addHook(hooks, e, dest) {
|
|
97
|
-
if (alreadyHas(hooks, e.event)) return;
|
|
98
|
-
const groups = hooks[e.event] = [...(hooks[e.event] || [])];
|
|
99
|
-
groups.push({ matcher: e.matcher || '', hooks: [{ type: 'command', command: `${dest} ${e.src}`, async: true, timeout: 5 }] });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
// Pure: return a NEW settings object with our six hooks merged into settings.hooks, idempotently, leaving
|
|
103
|
-
// the user's own hooks and other keys untouched. `dest` is the absolute path to the copied notify script.
|
|
104
|
-
export function mergeHooks(settings, dest, claudeVersion = null) {
|
|
105
|
-
const s = { ...(settings || {}) };
|
|
106
|
-
const hooks = (s.hooks && typeof s.hooks === 'object' && !Array.isArray(s.hooks)) ? { ...s.hooks } : {};
|
|
107
|
-
for (const e of HOOK_EVENTS) addHook(hooks, e, dest);
|
|
108
|
-
// Version-gated ext events: install only when the detected Claude is new enough AND (for a paired event)
|
|
109
|
-
// its clearer is also being installed. Anything that fails the gate is actively PRUNED, so a Claude
|
|
110
|
-
// downgrade after a newer install can't leave an unrecognised event name lingering in settings.json.
|
|
111
|
-
const enabled = new Set();
|
|
112
|
-
for (const e of HOOK_EVENTS_EXT) {
|
|
113
|
-
const ok = claudeVersionAtLeast(claudeVersion, e.minVersion) && (!e.pairWith || enabled.has(e.pairWith));
|
|
114
|
-
if (ok) { enabled.add(e.event); addHook(hooks, e, dest); } else dropOurHook(hooks, e.event);
|
|
115
|
-
}
|
|
116
|
-
s.hooks = hooks;
|
|
117
|
-
return s;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// Pure: return a NEW settings object with all of OUR hooks removed (uninstall). An event group that ends up
|
|
121
|
-
// empty is dropped; the user's own hooks and other keys are untouched.
|
|
122
|
-
export function stripHooks(settings) {
|
|
123
|
-
const s = { ...(settings || {}) };
|
|
124
|
-
if (!s.hooks || typeof s.hooks !== 'object' || Array.isArray(s.hooks)) return s;
|
|
125
|
-
const hooks = {};
|
|
126
|
-
for (const [event, groups] of Object.entries(s.hooks)) {
|
|
127
|
-
const kept = (groups || [])
|
|
128
|
-
.map((g) => ({ ...g, hooks: (g.hooks || []).filter((h) => !(typeof h.command === 'string' && h.command.includes(HOOK_MARK))) }))
|
|
129
|
-
.filter((g) => (g.hooks || []).length > 0);
|
|
130
|
-
if (kept.length) hooks[event] = kept;
|
|
131
|
-
}
|
|
132
|
-
s.hooks = hooks;
|
|
133
|
-
return s;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// Path helpers (also used by the IO shell in the next task).
|
|
137
|
-
function claudeDir(home = homedir()) { return path.join(home, '.claude'); }
|
|
138
|
-
function settingsPath(home = homedir()) { return path.join(claudeDir(home), 'settings.json'); }
|
|
139
|
-
|
|
140
|
-
// 'no-claude' → ~/.claude absent (don't prompt to enable). 'installed' → our hooks present. 'absent' →
|
|
141
|
-
// Claude Code is here but our hooks aren't (offer to enable).
|
|
142
|
-
export function hooksStatus(home = homedir()) {
|
|
143
|
-
if (!fs.existsSync(claudeDir(home))) return 'no-claude';
|
|
144
|
-
let settings = {};
|
|
145
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath(home), 'utf8')); } catch { /* missing/corrupt → {} */ }
|
|
146
|
-
const hooks = (settings && settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)) ? settings.hooks : {};
|
|
147
|
-
return Object.keys(hooks).some((ev) => alreadyHas(hooks, ev)) ? 'installed' : 'absent';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// Install (opt-in): copy the bundled hook scripts to ~/.claude/hooks/, write the env pointing at the state
|
|
151
|
-
// file, and merge our six hooks into settings.json. Returns { status }. NEVER creates ~/.claude — if it's
|
|
152
|
-
// absent the user doesn't run Claude Code, so we report 'no-claude' and do nothing.
|
|
153
|
-
// srcDir = the bundled hooks dir (server/hooks)
|
|
154
|
-
// stateFile = the unified ~/.handmux/claude-state.json path the hook writes and the server reads
|
|
155
|
-
export function installHooks(home = homedir(), { srcDir, stateFile, claudeVersion } = {}) {
|
|
156
|
-
if (!fs.existsSync(claudeDir(home))) return { status: 'no-claude' };
|
|
157
|
-
const hooksDir = path.join(claudeDir(home), 'hooks');
|
|
158
|
-
deployHookScripts(hooksDir, srcDir, stateFile);
|
|
159
|
-
|
|
160
|
-
const dest = path.join(hooksDir, 'handmux-notify.sh');
|
|
161
|
-
let settings = {};
|
|
162
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath(home), 'utf8')); } catch { /* missing/corrupt → {} */ }
|
|
163
|
-
// Gate the version-specific events (compact pair, StopFailure) on the installed Claude. Detect when the
|
|
164
|
-
// caller didn't inject a version; a null result (no `claude` / unparseable) is fail-closed = base 6 only.
|
|
165
|
-
const version = claudeVersion !== undefined ? claudeVersion : detectClaudeVersion();
|
|
166
|
-
writeJsonAtomic(settingsPath(home), mergeHooks(settings, dest, version));
|
|
167
|
-
return { status: 'installed' };
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Keep an ALREADY-installed user's hooks in step with this handmux version on every server start, so a plain
|
|
171
|
-
// `./deploy.sh` (restart) rolls out newly-added lifecycle events (e.g. SessionStart) and refreshed hook
|
|
172
|
-
// scripts — no phone re-enable needed. Two moves: (1) re-deploy the bundled hook scripts (idempotent — picks
|
|
173
|
-
// up a fixed handmux-write.cjs), and (2) add any of our BASE events a prior install predates.
|
|
174
|
-
//
|
|
175
|
-
// Strictly opt-in-preserving: a NO-OP unless our hooks are already present ('installed'). It never enables
|
|
176
|
-
// hooks for a user who hasn't opted in ('absent') and never creates ~/.claude ('no-claude'). BASE events
|
|
177
|
-
// only — it deliberately does NOT touch the version-gated ext events (compact pair / StopFailure): reconciling
|
|
178
|
-
// those needs the Claude version (a null read would PRUNE the ext hooks a user already has), and detecting it
|
|
179
|
-
// means spawning `claude --version` on the hot startup path. So sync stays pure-fs and non-pruning; ext-event
|
|
180
|
-
// rollout stays with the explicit installHooks (phone re-enable) path. settings.json is rewritten only when
|
|
181
|
-
// the merge actually changes it, so a steady state writes nothing.
|
|
182
|
-
export function syncHooks(home = homedir(), { srcDir, stateFile } = {}) {
|
|
183
|
-
const status = hooksStatus(home);
|
|
184
|
-
if (status !== 'installed') return { status, changed: false };
|
|
185
|
-
const hooksDir = path.join(claudeDir(home), 'hooks');
|
|
186
|
-
deployHookScripts(hooksDir, srcDir, stateFile); // refresh the deployed scripts (idempotent)
|
|
187
|
-
const dest = path.join(hooksDir, 'handmux-notify.sh');
|
|
188
|
-
let settings = {};
|
|
189
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath(home), 'utf8')); } catch { /* missing/corrupt → {} */ }
|
|
190
|
-
const hooks = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks)) ? { ...settings.hooks } : {};
|
|
191
|
-
const before = JSON.stringify(hooks);
|
|
192
|
-
for (const e of HOOK_EVENTS) addHook(hooks, e, dest); // add only MISSING base events; never add/prune ext
|
|
193
|
-
const changed = JSON.stringify(hooks) !== before;
|
|
194
|
-
if (changed) writeJsonAtomic(settingsPath(home), { ...settings, hooks });
|
|
195
|
-
return { status: 'installed', changed };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Uninstall: strip our hooks from settings.json and remove the copied scripts/env. Best-effort on the file
|
|
199
|
-
// deletes (a missing file is fine). Leaves ~/.claude and the user's own hooks intact.
|
|
200
|
-
export function uninstallHooks(home = homedir()) {
|
|
201
|
-
let settings = {};
|
|
202
|
-
try { settings = JSON.parse(fs.readFileSync(settingsPath(home), 'utf8')); } catch { /* nothing to strip */ }
|
|
203
|
-
if (fs.existsSync(settingsPath(home))) writeJsonAtomic(settingsPath(home), stripHooks(settings));
|
|
204
|
-
removeHookScripts(path.join(claudeDir(home), 'hooks'));
|
|
205
|
-
return { status: 'absent' };
|
|
206
|
-
}
|
package/src/cli/cloudflared.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// Resolve a usable `cloudflared` binary so `--tunnel cloudflare` is truly one-click (no brew/manual
|
|
2
|
-
// install). Order: $PATH → ~/.handmux/bin/ → download the latest release for this OS/arch from GitHub.
|
|
3
|
-
// `which`/`fetchImpl` are injectable so the pure mapping (assetFor) unit-tests offline.
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { spawnSync } from 'node:child_process';
|
|
7
|
-
import { pocketHome } from './state.js';
|
|
8
|
-
import { t } from './i18n/index.js';
|
|
9
|
-
|
|
10
|
-
const fmtMB = (n) => (n / 1048576).toFixed(1);
|
|
11
|
-
|
|
12
|
-
// Stream a fetch response into a Buffer, reporting progress per chunk as onProgress(received, total).
|
|
13
|
-
// `total` is the content-length (0 if the server didn't send one). Falls back to arrayBuffer() when the
|
|
14
|
-
// response isn't a readable stream (older/mock fetch), so callers still get the bytes.
|
|
15
|
-
export async function drain(res, onProgress) {
|
|
16
|
-
const total = Number(res.headers?.get?.('content-length')) || 0;
|
|
17
|
-
if (!res.body?.getReader) {
|
|
18
|
-
const b = Buffer.from(await res.arrayBuffer());
|
|
19
|
-
onProgress?.(b.length, total || b.length);
|
|
20
|
-
return b;
|
|
21
|
-
}
|
|
22
|
-
const reader = res.body.getReader();
|
|
23
|
-
const chunks = [];
|
|
24
|
-
let received = 0;
|
|
25
|
-
for (;;) {
|
|
26
|
-
const { done, value } = await reader.read();
|
|
27
|
-
if (done) break;
|
|
28
|
-
const c = Buffer.from(value);
|
|
29
|
-
chunks.push(c);
|
|
30
|
-
received += c.length;
|
|
31
|
-
onProgress?.(received, total);
|
|
32
|
-
}
|
|
33
|
-
return Buffer.concat(chunks);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// Default progress renderer: a single \r-updated line on a TTY (throttled to ~10/s), e.g.
|
|
37
|
-
// ` cloudflared 45% (9.2/20.4 MB)`. Non-TTY (piped/logged) gets nothing — the start line already
|
|
38
|
-
// announced the download and a spammy animation would just fill the log with control chars.
|
|
39
|
-
export function defaultProgress(out = process.stdout, name = 'cloudflared') {
|
|
40
|
-
if (!out.isTTY) return () => {};
|
|
41
|
-
let last = 0;
|
|
42
|
-
return (received, total) => {
|
|
43
|
-
const done = total && received >= total;
|
|
44
|
-
const now = Date.now();
|
|
45
|
-
if (!done && now - last < 100) return;
|
|
46
|
-
last = now;
|
|
47
|
-
const body = total
|
|
48
|
-
? `${Math.floor((received / total) * 100)}% (${fmtMB(received)}/${fmtMB(total)} MB)`
|
|
49
|
-
: `${fmtMB(received)} MB`;
|
|
50
|
-
out.write(`\r ${name} ${body} `);
|
|
51
|
-
if (done) out.write('\n');
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Map Node's platform/arch to cloudflared's release asset. Linux/Windows ship a bare binary; macOS
|
|
56
|
-
// ships a .tgz that contains a `cloudflared` executable.
|
|
57
|
-
export function assetFor(platform = process.platform, arch = process.arch) {
|
|
58
|
-
const a = { x64: 'amd64', arm64: 'arm64', arm: 'arm', ia32: '386' }[arch] || arch;
|
|
59
|
-
if (platform === 'darwin') return { file: `cloudflared-darwin-${a}.tgz`, archive: 'tgz', bin: 'cloudflared' };
|
|
60
|
-
if (platform === 'win32') return { file: `cloudflared-windows-${a}.exe`, archive: null, bin: 'cloudflared.exe' };
|
|
61
|
-
return { file: `cloudflared-linux-${a}`, archive: null, bin: 'cloudflared' };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function onPath(exec = 'cloudflared') {
|
|
65
|
-
const finder = process.platform === 'win32' ? 'where' : 'which';
|
|
66
|
-
const r = spawnSync(finder, [exec], { encoding: 'utf8' });
|
|
67
|
-
return r.status === 0 ? String(r.stdout).trim().split(/\r?\n/)[0] : null;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function resolveCloudflared(home, { which = onPath, fetchImpl, log = console, progress } = {}) {
|
|
71
|
-
const found = which('cloudflared');
|
|
72
|
-
if (found) return found;
|
|
73
|
-
|
|
74
|
-
const dir = path.join(pocketHome(home), 'bin');
|
|
75
|
-
const asset = assetFor();
|
|
76
|
-
const dest = path.join(dir, asset.bin);
|
|
77
|
-
if (fs.existsSync(dest)) return dest;
|
|
78
|
-
|
|
79
|
-
const doFetch = fetchImpl || globalThis.fetch;
|
|
80
|
-
if (!doFetch) throw new Error('no fetch available to download cloudflared (Node 18+ required)');
|
|
81
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
82
|
-
const url = `https://github.com/cloudflare/cloudflared/releases/latest/download/${asset.file}`;
|
|
83
|
-
log.log?.(t('cf.downloading', { file: asset.file }));
|
|
84
|
-
const res = await doFetch(url, { redirect: 'follow' });
|
|
85
|
-
if (!res.ok) throw new Error(`cloudflared download failed: HTTP ${res.status} (${url})`);
|
|
86
|
-
const buf = await drain(res, progress || defaultProgress(process.stdout));
|
|
87
|
-
|
|
88
|
-
if (asset.archive === 'tgz') {
|
|
89
|
-
const tmp = path.join(dir, asset.file);
|
|
90
|
-
fs.writeFileSync(tmp, buf);
|
|
91
|
-
const r = spawnSync('tar', ['xzf', tmp, '-C', dir], { encoding: 'utf8' });
|
|
92
|
-
fs.unlinkSync(tmp);
|
|
93
|
-
if (r.status !== 0) throw new Error(`failed to extract cloudflared: ${r.stderr || r.status}`);
|
|
94
|
-
} else {
|
|
95
|
-
fs.writeFileSync(dest, buf);
|
|
96
|
-
}
|
|
97
|
-
fs.chmodSync(dest, 0o755);
|
|
98
|
-
return dest;
|
|
99
|
-
}
|
package/src/cli/codexManaged.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import os from 'node:os';
|
|
4
|
-
import { spawn as spawnChild } from 'node:child_process';
|
|
5
|
-
|
|
6
|
-
const PANE_RE = /^%\d+$/;
|
|
7
|
-
|
|
8
|
-
export function codexAppSocketPath(pane, home = os.homedir()) {
|
|
9
|
-
if (!PANE_RE.test(pane || '')) throw new Error('managed Codex requires a tmux pane');
|
|
10
|
-
return path.join(home, '.handmux', 'codex-app', `${pane.slice(1)}.sock`);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function waitForSocket(socketPath, child, {
|
|
14
|
-
exists = fs.existsSync,
|
|
15
|
-
wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
16
|
-
timeoutMs = 5_000,
|
|
17
|
-
now = Date.now,
|
|
18
|
-
} = {}) {
|
|
19
|
-
return new Promise((resolve, reject) => {
|
|
20
|
-
let exited = false;
|
|
21
|
-
let exitCode = null;
|
|
22
|
-
child.once('exit', (code) => { exited = true; exitCode = code; });
|
|
23
|
-
child.once('error', reject);
|
|
24
|
-
(async () => {
|
|
25
|
-
const started = now();
|
|
26
|
-
while (!exists(socketPath)) {
|
|
27
|
-
if (exited) throw new Error(`Codex App Server exited before startup (${exitCode ?? 'unknown'})`);
|
|
28
|
-
if (now() - started >= timeoutMs) throw new Error('Codex App Server startup timed out');
|
|
29
|
-
await wait(25);
|
|
30
|
-
}
|
|
31
|
-
resolve();
|
|
32
|
-
})().catch(reject);
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function finish(child) {
|
|
37
|
-
return new Promise((resolve, reject) => {
|
|
38
|
-
child.once('error', reject);
|
|
39
|
-
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// One App Server belongs to one tmux pane. The TUI and Handmux connect to that pane-owned Unix socket,
|
|
44
|
-
// so thread identity, messages, approvals, settings, interrupts and inbox state share one runtime.
|
|
45
|
-
export async function runManagedCodex(args = [], {
|
|
46
|
-
home = os.homedir(),
|
|
47
|
-
env = process.env,
|
|
48
|
-
spawn = spawnChild,
|
|
49
|
-
mkdir = (dir) => fs.mkdirSync(dir, { recursive: true, mode: 0o700 }),
|
|
50
|
-
unlink = (file) => { try { fs.unlinkSync(file); } catch (error) { if (error?.code !== 'ENOENT') throw error; } },
|
|
51
|
-
waitOptions,
|
|
52
|
-
} = {}) {
|
|
53
|
-
const pane = env.TMUX_PANE;
|
|
54
|
-
const socketPath = codexAppSocketPath(pane, home);
|
|
55
|
-
mkdir(path.dirname(socketPath));
|
|
56
|
-
unlink(socketPath);
|
|
57
|
-
|
|
58
|
-
const managedEnv = { ...env, HANDMUX_CODEX_MANAGED: '1' };
|
|
59
|
-
const appServer = spawn('codex', ['app-server', '--listen', `unix://${socketPath}`], {
|
|
60
|
-
env: managedEnv,
|
|
61
|
-
stdio: ['ignore', 'ignore', 'ignore'],
|
|
62
|
-
});
|
|
63
|
-
let tui = null;
|
|
64
|
-
try {
|
|
65
|
-
await waitForSocket(socketPath, appServer, waitOptions);
|
|
66
|
-
tui = spawn('codex', ['--remote', `unix://${socketPath}`, ...args], {
|
|
67
|
-
env: managedEnv,
|
|
68
|
-
stdio: 'inherit',
|
|
69
|
-
});
|
|
70
|
-
const result = await finish(tui);
|
|
71
|
-
return Number.isInteger(result.code) ? result.code : 1;
|
|
72
|
-
} finally {
|
|
73
|
-
if (tui && tui.exitCode == null && tui.signalCode == null) tui.kill('SIGTERM');
|
|
74
|
-
if (appServer.exitCode == null && appServer.signalCode == null) appServer.kill('SIGTERM');
|
|
75
|
-
unlink(socketPath);
|
|
76
|
-
}
|
|
77
|
-
}
|
package/src/cli/drivers.js
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
// Tunnel driver registry. A driver only DESCRIBES how to expose the local server (what process to spawn,
|
|
2
|
-
// how to read the public URL out of its output); the supervisor owns process lifecycle (spawn, restart,
|
|
3
|
-
// kill). Declarative → unit-testable without spawning. matchUrl receives (chunk, cfg): cloudflare scrapes
|
|
4
|
-
// a random URL from logs; ssh / cloudflare-named already KNOW the URL (cfg.publicUrl) and only gate it on
|
|
5
|
-
// a readiness signal so the QR isn't shown before the tunnel is live.
|
|
6
|
-
import { extractCloudflareUrl } from './cloudflareUrl.js';
|
|
7
|
-
import { isTunnelConnected } from './sshTunnel.js';
|
|
8
|
-
import { cfNamedReady } from './cfNamed.js';
|
|
9
|
-
import { extractNatappUrl } from './natappUrl.js';
|
|
10
|
-
import { extractCpolarUrl, cpolarNamedArgs } from './cpolarUrl.js';
|
|
11
|
-
import { hostIn } from './urlHost.js';
|
|
12
|
-
|
|
13
|
-
export const DRIVERS = {
|
|
14
|
-
none: {
|
|
15
|
-
name: 'none',
|
|
16
|
-
needsProcess: false,
|
|
17
|
-
proc: () => null,
|
|
18
|
-
matchUrl: () => null,
|
|
19
|
-
},
|
|
20
|
-
cloudflare: {
|
|
21
|
-
name: 'cloudflare',
|
|
22
|
-
needsProcess: true,
|
|
23
|
-
notFoundHint: 'cloudflared not found — install it (brew install cloudflared)',
|
|
24
|
-
proc: (cfg) => ({
|
|
25
|
-
cmd: cfg.cloudflaredBin || 'cloudflared',
|
|
26
|
-
// --grace-period 0s: drop the edge connection immediately on SIGTERM instead of draining for the 30s
|
|
27
|
-
// default, so `stop`/`restart` don't leave the tunnel lingering on Cloudflare's side (it would look
|
|
28
|
-
// "still running" remotely and overlap a restart). See supervisor.shutdown for the local-process side.
|
|
29
|
-
args: ['tunnel', '--grace-period', '0s', '--url', `http://localhost:${cfg.port}`],
|
|
30
|
-
}),
|
|
31
|
-
matchUrl: (chunk) => extractCloudflareUrl(chunk),
|
|
32
|
-
},
|
|
33
|
-
'cloudflare-named': {
|
|
34
|
-
name: 'cloudflare-named',
|
|
35
|
-
needsProcess: true,
|
|
36
|
-
notFoundHint: 'cloudflared not found — run `handmux setup` to provision the named tunnel',
|
|
37
|
-
proc: (cfg) => ({
|
|
38
|
-
cmd: cfg.cloudflaredBin || 'cloudflared',
|
|
39
|
-
// --grace-period 0s goes BEFORE the `run` subcommand (it's a `cloudflared tunnel` flag). Same reason as
|
|
40
|
-
// the quick tunnel: disconnect immediately on stop so the named tunnel doesn't linger on the edge.
|
|
41
|
-
args: ['tunnel', '--grace-period', '0s', 'run', cfg.cfTunnelName],
|
|
42
|
-
}),
|
|
43
|
-
matchUrl: (chunk, cfg) => (cfNamedReady(chunk) ? cfg.publicUrl : null),
|
|
44
|
-
},
|
|
45
|
-
ssh: {
|
|
46
|
-
name: 'ssh',
|
|
47
|
-
needsProcess: true,
|
|
48
|
-
notFoundHint: 'tunlite not found — install it (npm i -g tunlite / npx tunlite install)',
|
|
49
|
-
proc: (cfg) => ({
|
|
50
|
-
cmd: cfg.tunliteBin || 'tunlite',
|
|
51
|
-
args: ['run', '--to', cfg.sshHost, '-R', `${cfg.remotePort}:localhost:${cfg.port}`,
|
|
52
|
-
'--name', 'handmux', '--json', ...(cfg.sshJump ? ['--jump', cfg.sshJump] : [])],
|
|
53
|
-
}),
|
|
54
|
-
matchUrl: (chunk, cfg) => (isTunnelConnected(chunk) ? cfg.publicUrl : null),
|
|
55
|
-
},
|
|
56
|
-
// natapp / cpolar — ngrok-derived domestic tunnels (China-usable when cloudflare's edge isn't). Both take a
|
|
57
|
-
// stable `authtoken`; both default to a full-screen TUI, so we force `-log=stdout` to get scrapeable text.
|
|
58
|
-
// FREE tier = random domain we scrape; a fixed/reserved domain arrives via publicUrl (see cfg.publicUrl).
|
|
59
|
-
natapp: {
|
|
60
|
-
name: 'natapp',
|
|
61
|
-
needsProcess: true,
|
|
62
|
-
notFoundHint: 'natapp not found — download it from https://natapp.cn (login required) into ~/.handmux/bin/',
|
|
63
|
-
proc: (cfg) => ({
|
|
64
|
-
cmd: cfg.natappBin || 'natapp',
|
|
65
|
-
args: [`-authtoken=${cfg.authtoken}`, '-log=stdout'],
|
|
66
|
-
}),
|
|
67
|
-
// The reserved/paid zone is arbitrary (natapp1.cc / your own) so we can't scrape it — gate on the known
|
|
68
|
-
// host echoing in the log. Free tier has no publicUrl → scrape the natappfree.cc URL.
|
|
69
|
-
matchUrl: (chunk, cfg) => (cfg.publicUrl
|
|
70
|
-
? (hostIn(chunk, cfg.publicUrl) ? cfg.publicUrl : null)
|
|
71
|
-
: extractNatappUrl(chunk)),
|
|
72
|
-
},
|
|
73
|
-
cpolar: {
|
|
74
|
-
name: 'cpolar',
|
|
75
|
-
needsProcess: true,
|
|
76
|
-
notFoundHint: 'cpolar not found — install it (or run `handmux setup`, which auto-downloads it)',
|
|
77
|
-
proc: (cfg) => ({
|
|
78
|
-
cmd: cfg.cpolarBin || 'cpolar',
|
|
79
|
-
// authtoken is seeded into cpolar's own config beforehand (handmux.js / setup); a reserved subdomain or
|
|
80
|
-
// bound domain becomes -subdomain/-hostname; -region selects the edge (cn = mainland China).
|
|
81
|
-
args: ['http', '-log=stdout',
|
|
82
|
-
...cpolarNamedArgs(cfg.publicUrl),
|
|
83
|
-
...(cfg.cpolarRegion ? [`-region=${cfg.cpolarRegion}`] : []),
|
|
84
|
-
String(cfg.port)],
|
|
85
|
-
}),
|
|
86
|
-
// A reserved subdomain is still on the cpolar zone, so scrape it (learns the region-qualified host cpolar
|
|
87
|
-
// serves); a bound custom domain is off-zone → gate on its known host from publicUrl.
|
|
88
|
-
matchUrl: (chunk, cfg) => extractCpolarUrl(chunk)
|
|
89
|
-
|| (cfg.publicUrl && hostIn(chunk, cfg.publicUrl) ? cfg.publicUrl : null),
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export function getDriver(name) {
|
|
94
|
-
const d = DRIVERS[name];
|
|
95
|
-
if (!d) throw new Error(`unknown tunnel: ${name}`);
|
|
96
|
-
return d;
|
|
97
|
-
}
|
package/src/cli/hookScaffold.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
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
|
-
|
|
5
|
-
export const HOOK_SCRIPTS = ['handmux-notify.sh', 'handmux-write.cjs'];
|
|
6
|
-
const LEGACY_HOOK_SCRIPTS = ['handmux-codex-usage.cjs'];
|
|
7
|
-
|
|
8
|
-
// Atomic write (tmp + rename) so a crash can't leave a half-written config file. Text in, text out — callers
|
|
9
|
-
// pass raw TOML for config.toml, or use writeJsonAtomic for pretty-printed settings.json.
|
|
10
|
-
export function writeFileAtomic(file, text) {
|
|
11
|
-
const tmp = `${file}.tmp`;
|
|
12
|
-
fs.writeFileSync(tmp, text);
|
|
13
|
-
fs.renameSync(tmp, file);
|
|
14
|
-
}
|
|
15
|
-
export const writeJsonAtomic = (file, obj) => writeFileAtomic(file, JSON.stringify(obj, null, 2));
|
|
16
|
-
|
|
17
|
-
// Deploy the Claude notify/write scripts into `hooksDir` and point their env at the shared state file.
|
|
18
|
-
export function deployHookScripts(hooksDir, srcDir, stateFile) {
|
|
19
|
-
fs.mkdirSync(hooksDir, { recursive: true });
|
|
20
|
-
for (const f of HOOK_SCRIPTS) fs.copyFileSync(path.join(srcDir, f), path.join(hooksDir, f));
|
|
21
|
-
for (const f of LEGACY_HOOK_SCRIPTS) {
|
|
22
|
-
try { fs.unlinkSync(path.join(hooksDir, f)); } catch { /* absent */ }
|
|
23
|
-
}
|
|
24
|
-
fs.chmodSync(path.join(hooksDir, 'handmux-notify.sh'), 0o755);
|
|
25
|
-
fs.writeFileSync(path.join(hooksDir, 'handmux-notify.env'), `HANDMUX_STATE=${stateFile}\n`, { mode: 0o600 });
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Remove the deployed scripts + env (uninstall). Best-effort: a missing file is fine.
|
|
29
|
-
export function removeHookScripts(hooksDir) {
|
|
30
|
-
for (const f of [...HOOK_SCRIPTS, ...LEGACY_HOOK_SCRIPTS, 'handmux-notify.env']) {
|
|
31
|
-
try { fs.unlinkSync(path.join(hooksDir, f)); } catch { /* already gone */ }
|
|
32
|
-
}
|
|
33
|
-
}
|