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