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,301 @@
|
|
|
1
|
+
import { spawn as spawnChild } from 'node:child_process';
|
|
2
|
+
import { constants as fsConstants } from 'node:fs';
|
|
3
|
+
import fsp from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { ensurePrivateDir, writeJsonAtomic } from './atomicJson.js';
|
|
8
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
const PANE_RE = /^%\d+$/;
|
|
10
|
+
const RUNNER_FILE = fileURLToPath(import.meta.url);
|
|
11
|
+
const HANDMUX_BIN = fileURLToPath(new URL('../../bin/handmux.js', import.meta.url));
|
|
12
|
+
const MAX_PATH_LENGTH = 32_768;
|
|
13
|
+
const MAX_PATH_ENTRIES = 256;
|
|
14
|
+
const MAX_PATH_ENTRY_LENGTH = 4_096;
|
|
15
|
+
function errorText(error) {
|
|
16
|
+
return error instanceof Error ? error.message : String(error);
|
|
17
|
+
}
|
|
18
|
+
const recordOf = (value) => (value && typeof value === 'object' && !Array.isArray(value)
|
|
19
|
+
? value : null);
|
|
20
|
+
function errorCode(error) {
|
|
21
|
+
return error && typeof error === 'object' ? error.code : undefined;
|
|
22
|
+
}
|
|
23
|
+
export function validateAgentRequest(input) {
|
|
24
|
+
const request = recordOf(input);
|
|
25
|
+
const args = request?.args;
|
|
26
|
+
const cmd = request?.cmd;
|
|
27
|
+
const valid = cmd === 'claude'
|
|
28
|
+
? Array.isArray(args) && args.length === 2 && args[0] === '--resume' && typeof args[1] === 'string' && UUID_RE.test(args[1])
|
|
29
|
+
: cmd === 'codex'
|
|
30
|
+
&& Array.isArray(args) && args.length === 2 && args[0] === 'resume' && typeof args[1] === 'string' && UUID_RE.test(args[1]);
|
|
31
|
+
if (!valid)
|
|
32
|
+
throw new Error('unsafe agent request');
|
|
33
|
+
const validArgs = args;
|
|
34
|
+
return { cmd: cmd, args: [validArgs[0], validArgs[1]] };
|
|
35
|
+
}
|
|
36
|
+
function validatePathSnapshot(value, platform) {
|
|
37
|
+
if (typeof value !== 'string' || !value || value.length > MAX_PATH_LENGTH || /[\0\r\n]/.test(value)) {
|
|
38
|
+
throw new Error('unsafe agent PATH snapshot');
|
|
39
|
+
}
|
|
40
|
+
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
41
|
+
const entries = value.split(platform === 'win32' ? ';' : ':');
|
|
42
|
+
if (!entries.length || entries.length > MAX_PATH_ENTRIES)
|
|
43
|
+
throw new Error('unsafe agent PATH snapshot');
|
|
44
|
+
for (const entry of entries) {
|
|
45
|
+
if (!entry || entry.length > MAX_PATH_ENTRY_LENGTH || !pathApi.isAbsolute(entry)) {
|
|
46
|
+
throw new Error('unsafe agent PATH snapshot');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
export function validatePreparedAgentRequest(input, platform = process.platform) {
|
|
52
|
+
const record = recordOf(input);
|
|
53
|
+
const request = validateAgentRequest(input);
|
|
54
|
+
const paneId = record?.paneId;
|
|
55
|
+
if (typeof paneId !== 'string' || !PANE_RE.test(paneId))
|
|
56
|
+
throw new Error('unsafe prepared agent request');
|
|
57
|
+
return { ...request, paneId, pathEnv: validatePathSnapshot(record?.pathEnv, platform) };
|
|
58
|
+
}
|
|
59
|
+
function earlyFailure(request, error, code) {
|
|
60
|
+
if (errorCode(error) === 'ENOENT')
|
|
61
|
+
return `${request.cmd} binary not found`;
|
|
62
|
+
if (error)
|
|
63
|
+
return `${request.cmd} failed to start: ${errorText(error)}`;
|
|
64
|
+
return `${request.cmd} exited ${code ?? 'unknown'} before ready`;
|
|
65
|
+
}
|
|
66
|
+
export function launchAgentRequest(requestInput, { spawn = spawnChild, readinessMs = 500, setTimeout = globalThis.setTimeout, clearTimeout = globalThis.clearTimeout, env, } = {}) {
|
|
67
|
+
const request = validateAgentRequest(requestInput);
|
|
68
|
+
let child;
|
|
69
|
+
let readyDone = false;
|
|
70
|
+
let timer = null;
|
|
71
|
+
let completionDone = false;
|
|
72
|
+
let settleReady;
|
|
73
|
+
let settleCompletion;
|
|
74
|
+
const ready = new Promise((resolve) => { settleReady = resolve; });
|
|
75
|
+
const completion = new Promise((resolve) => { settleCompletion = resolve; });
|
|
76
|
+
const finishCompletion = (value) => {
|
|
77
|
+
if (completionDone)
|
|
78
|
+
return;
|
|
79
|
+
completionDone = true;
|
|
80
|
+
settleCompletion(value);
|
|
81
|
+
};
|
|
82
|
+
const failReady = (error, code) => {
|
|
83
|
+
if (readyDone)
|
|
84
|
+
return;
|
|
85
|
+
readyDone = true;
|
|
86
|
+
if (timer)
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
settleReady({ status: 'failed', error: earlyFailure(request, error, code) });
|
|
89
|
+
};
|
|
90
|
+
try {
|
|
91
|
+
const command = request.cmd === 'codex' ? process.execPath : request.cmd;
|
|
92
|
+
const args = request.cmd === 'codex' ? [HANDMUX_BIN, 'codex', ...request.args] : request.args;
|
|
93
|
+
child = spawn(command, args, {
|
|
94
|
+
stdio: 'inherit',
|
|
95
|
+
shell: false,
|
|
96
|
+
...(env ? { env } : {}),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
failReady(error);
|
|
101
|
+
finishCompletion({ code: null, signal: null, error: errorText(error) });
|
|
102
|
+
return { child: null, ready, completion, terminate: async () => completion };
|
|
103
|
+
}
|
|
104
|
+
child.once('spawn', () => {
|
|
105
|
+
timer = setTimeout(() => {
|
|
106
|
+
if (readyDone)
|
|
107
|
+
return;
|
|
108
|
+
readyDone = true;
|
|
109
|
+
settleReady({ status: 'ready' });
|
|
110
|
+
}, readinessMs);
|
|
111
|
+
});
|
|
112
|
+
child.once('error', (error) => {
|
|
113
|
+
failReady(error);
|
|
114
|
+
finishCompletion({ code: null, signal: null, error: errorText(error) });
|
|
115
|
+
});
|
|
116
|
+
child.once('exit', (code, signal) => {
|
|
117
|
+
failReady(null, code);
|
|
118
|
+
finishCompletion({ code, signal });
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
child,
|
|
122
|
+
ready,
|
|
123
|
+
completion,
|
|
124
|
+
async terminate() {
|
|
125
|
+
if (!completionDone)
|
|
126
|
+
child.kill('SIGTERM');
|
|
127
|
+
return completion;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
export function launchPreparedAgentRequest(requestInput, { env = process.env, platform = process.platform, ...options } = {}) {
|
|
132
|
+
const request = validatePreparedAgentRequest(requestInput, platform);
|
|
133
|
+
return launchAgentRequest(request, { ...options, env: { ...env, PATH: request.pathEnv } });
|
|
134
|
+
}
|
|
135
|
+
export async function publishAgentReadiness(launched, writeStatus) {
|
|
136
|
+
const ready = await launched.ready;
|
|
137
|
+
try {
|
|
138
|
+
await writeStatus(ready);
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
let cleanupError;
|
|
142
|
+
try {
|
|
143
|
+
await launched.terminate();
|
|
144
|
+
}
|
|
145
|
+
catch (failure) {
|
|
146
|
+
cleanupError = failure;
|
|
147
|
+
}
|
|
148
|
+
if (cleanupError)
|
|
149
|
+
throw new Error(`${errorText(error)}; cleanup failed: ${errorText(cleanupError)}`);
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
return ready;
|
|
153
|
+
}
|
|
154
|
+
function shellQuote(value) {
|
|
155
|
+
return `'${String(value).replaceAll("'", `'"'"'`)}'`;
|
|
156
|
+
}
|
|
157
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
158
|
+
async function executableOnPath(command, { fs = fsp, env = process.env, platform = process.platform, } = {}) {
|
|
159
|
+
const pathValue = env.PATH;
|
|
160
|
+
if (!pathValue)
|
|
161
|
+
return false;
|
|
162
|
+
const pathApi = platform === 'win32' ? path.win32 : path.posix;
|
|
163
|
+
const delimiter = platform === 'win32' ? ';' : ':';
|
|
164
|
+
const extensions = platform === 'win32'
|
|
165
|
+
? String(env.PATHEXT || '.EXE;.CMD;.BAT;.COM').split(';').filter(Boolean)
|
|
166
|
+
: [''];
|
|
167
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
168
|
+
if (!directory)
|
|
169
|
+
continue;
|
|
170
|
+
for (const extension of extensions) {
|
|
171
|
+
const candidate = pathApi.join(directory, platform === 'win32' ? `${command}${extension}` : command);
|
|
172
|
+
try {
|
|
173
|
+
const stat = await fs.stat(candidate);
|
|
174
|
+
if (!stat.isFile())
|
|
175
|
+
continue;
|
|
176
|
+
await fs.access(candidate, platform === 'win32' ? fsConstants.F_OK : fsConstants.X_OK);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
catch { /* keep searching while an install may be replacing the executable */ }
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
export async function waitForAgentExecutable(command, { available = executableOnPath, now = Date.now, wait = delay, timeoutMs = 8_000, pollMs = 100, } = {}) {
|
|
185
|
+
if (command !== 'claude' && command !== 'codex')
|
|
186
|
+
throw new Error('unsafe agent executable');
|
|
187
|
+
const started = now();
|
|
188
|
+
while (true) {
|
|
189
|
+
if (await available(command))
|
|
190
|
+
return { status: 'ready' };
|
|
191
|
+
const remaining = timeoutMs - (now() - started);
|
|
192
|
+
if (remaining <= 0) {
|
|
193
|
+
return { status: 'failed', error: `${command} executable unavailable after startup wait` };
|
|
194
|
+
}
|
|
195
|
+
await wait(Math.min(pollMs, remaining));
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async function unlinkIfPresent(fs, file) {
|
|
199
|
+
try {
|
|
200
|
+
await fs.unlink(file);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (errorCode(error) !== 'ENOENT')
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function createAgentRunner({ home = os.homedir(), fs = fsp, now = Date.now, wait = delay, timeoutMs = 3_000, pollMs = 25, executableAvailable, executableWaitMs = 8_000, executablePollMs = 100, pathEnv = process.env.PATH, platform = process.platform, } = {}) {
|
|
208
|
+
const dir = path.join(home, '.handmux', 'workspaces', 'agent-runs');
|
|
209
|
+
const command = `${shellQuote(process.execPath)} ${shellQuote(RUNNER_FILE)}`;
|
|
210
|
+
const files = (paneId) => {
|
|
211
|
+
if (!PANE_RE.test(paneId))
|
|
212
|
+
throw new Error('invalid agent runner pane id');
|
|
213
|
+
const base = paneId.slice(1);
|
|
214
|
+
return { request: path.join(dir, `${base}.request.json`), status: path.join(dir, `${base}.status.json`) };
|
|
215
|
+
};
|
|
216
|
+
return {
|
|
217
|
+
command,
|
|
218
|
+
waitForExecutable(cmd) {
|
|
219
|
+
let safePath;
|
|
220
|
+
try {
|
|
221
|
+
safePath = validatePathSnapshot(pathEnv, platform);
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
return Promise.resolve({ status: 'failed', error: errorText(error) });
|
|
225
|
+
}
|
|
226
|
+
return waitForAgentExecutable(cmd, {
|
|
227
|
+
available: executableAvailable ?? ((command) => executableOnPath(command, {
|
|
228
|
+
fs,
|
|
229
|
+
env: { PATH: safePath, PATHEXT: process.env.PATHEXT },
|
|
230
|
+
platform,
|
|
231
|
+
})),
|
|
232
|
+
now,
|
|
233
|
+
wait,
|
|
234
|
+
timeoutMs: executableWaitMs,
|
|
235
|
+
pollMs: executablePollMs,
|
|
236
|
+
});
|
|
237
|
+
},
|
|
238
|
+
async prepare({ paneId, cmd, args }) {
|
|
239
|
+
const request = validateAgentRequest({ cmd, args });
|
|
240
|
+
const safePath = validatePathSnapshot(pathEnv, platform);
|
|
241
|
+
const target = files(paneId);
|
|
242
|
+
await ensurePrivateDir(dir, { fs });
|
|
243
|
+
await unlinkIfPresent(fs, target.status);
|
|
244
|
+
await writeJsonAtomic(target.request, { paneId, ...request, pathEnv: safePath }, { fs });
|
|
245
|
+
},
|
|
246
|
+
async waitReady(paneId) {
|
|
247
|
+
const target = files(paneId);
|
|
248
|
+
const started = now();
|
|
249
|
+
while (now() - started <= timeoutMs) {
|
|
250
|
+
try {
|
|
251
|
+
const status = recordOf(JSON.parse(await fs.readFile(target.status, 'utf8')));
|
|
252
|
+
if (status?.status === 'ready')
|
|
253
|
+
return { status: 'ready' };
|
|
254
|
+
if (status?.status === 'failed' && typeof status.error === 'string')
|
|
255
|
+
return { status: 'failed', error: status.error };
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
if (errorCode(error) !== 'ENOENT' && !(error instanceof SyntaxError))
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
await wait(pollMs);
|
|
262
|
+
}
|
|
263
|
+
return { status: 'failed', error: 'agent readiness timed out' };
|
|
264
|
+
},
|
|
265
|
+
async cancel(paneId) {
|
|
266
|
+
const target = files(paneId);
|
|
267
|
+
await Promise.all([target.request, target.status].map((file) => unlinkIfPresent(fs, file)));
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
async function runPreparedAgent() {
|
|
272
|
+
const paneId = process.env.TMUX_PANE;
|
|
273
|
+
const target = (() => {
|
|
274
|
+
if (typeof paneId !== 'string' || !PANE_RE.test(paneId))
|
|
275
|
+
throw new Error('agent runner requires TMUX_PANE');
|
|
276
|
+
const dir = path.join(os.homedir(), '.handmux', 'workspaces', 'agent-runs');
|
|
277
|
+
const base = paneId.slice(1);
|
|
278
|
+
return { request: path.join(dir, `${base}.request.json`), status: path.join(dir, `${base}.status.json`) };
|
|
279
|
+
})();
|
|
280
|
+
const input = JSON.parse(await fsp.readFile(target.request, 'utf8'));
|
|
281
|
+
const request = validatePreparedAgentRequest(input);
|
|
282
|
+
if (request.paneId !== paneId)
|
|
283
|
+
throw new Error('agent runner pane mismatch');
|
|
284
|
+
const launched = launchPreparedAgentRequest(request);
|
|
285
|
+
try {
|
|
286
|
+
const ready = await publishAgentReadiness(launched, (status) => writeJsonAtomic(target.status, status));
|
|
287
|
+
if (ready.status === 'ready')
|
|
288
|
+
await launched.completion;
|
|
289
|
+
if (ready.status !== 'ready')
|
|
290
|
+
throw new Error(ready.error);
|
|
291
|
+
}
|
|
292
|
+
finally {
|
|
293
|
+
await unlinkIfPresent(fsp, target.request);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === RUNNER_FILE) {
|
|
297
|
+
runPreparedAgent().catch((error) => {
|
|
298
|
+
process.stderr.write(`handmux agent resume: ${errorText(error)}\n`);
|
|
299
|
+
process.exitCode = 1;
|
|
300
|
+
});
|
|
301
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fsp from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export async function ensurePrivateDir(dir, { fs = fsp } = {}) {
|
|
5
|
+
const resolved = path.resolve(dir);
|
|
6
|
+
const { root } = path.parse(resolved);
|
|
7
|
+
const parts = resolved.slice(root.length).split(path.sep).filter(Boolean);
|
|
8
|
+
const privateStart = parts.indexOf('.handmux');
|
|
9
|
+
const first = privateStart === -1 ? parts.length - 1 : privateStart;
|
|
10
|
+
for (let index = first; index < parts.length; index += 1) {
|
|
11
|
+
const current = path.join(root, ...parts.slice(0, index + 1));
|
|
12
|
+
await fs.mkdir(current, { recursive: true, mode: 0o700 });
|
|
13
|
+
await fs.chmod(current, 0o700);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export async function writeJsonAtomic(file, value, { fs = fsp } = {}) {
|
|
17
|
+
const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
18
|
+
const handle = await fs.open(temp, 'wx', 0o600);
|
|
19
|
+
try {
|
|
20
|
+
await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
|
|
21
|
+
await handle.sync();
|
|
22
|
+
await handle.close();
|
|
23
|
+
await fs.rename(temp, file);
|
|
24
|
+
await fs.chmod(file, 0o600);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
await handle.close().catch(() => { });
|
|
28
|
+
await fs.unlink(temp).catch(() => { });
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { AGENTS } from '../agents/index.js';
|
|
4
|
+
import { resolveCodexRollout, sessionsDir as codexSessionsDir } from '../agents/codex.js';
|
|
5
|
+
import { canonicalizeSnapshot, WORKSPACE_SCHEMA_VERSION } from './schema.js';
|
|
6
|
+
function agentMap(agents) {
|
|
7
|
+
return new Map(agents.map((agent) => [agent.id, agent]));
|
|
8
|
+
}
|
|
9
|
+
export async function readAgentBindings(stateFile, agents = AGENTS, readFile = fsp.readFile, access = fsp.access, stat = fsp.stat) {
|
|
10
|
+
let state;
|
|
11
|
+
try {
|
|
12
|
+
state = JSON.parse(String(await readFile(stateFile, 'utf8')));
|
|
13
|
+
if (!state || typeof state !== 'object' || Array.isArray(state))
|
|
14
|
+
return new Map();
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return new Map();
|
|
18
|
+
}
|
|
19
|
+
const known = agentMap(agents);
|
|
20
|
+
const bindings = new Map();
|
|
21
|
+
for (const [paneRuntimeId, record] of Object.entries(state)) {
|
|
22
|
+
if (!record || typeof record !== 'object' || Array.isArray(record))
|
|
23
|
+
continue;
|
|
24
|
+
// This is the Claude Hook state file. Legacy Codex-tagged rows are deliberately ignored; managed
|
|
25
|
+
// Codex identity belongs to App Server and must not be reconstructed from the removed Hook path.
|
|
26
|
+
if (record.agent !== undefined && record.agent !== 'claude')
|
|
27
|
+
continue;
|
|
28
|
+
const id = 'claude';
|
|
29
|
+
if (!known.has(id))
|
|
30
|
+
continue;
|
|
31
|
+
const payload = record.payload;
|
|
32
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload))
|
|
33
|
+
continue;
|
|
34
|
+
const sessionId = payload.session_id;
|
|
35
|
+
const transcriptPath = payload.transcript_path;
|
|
36
|
+
const driver = known.get(id);
|
|
37
|
+
if (typeof sessionId !== 'string' || !driver?.sessions?.isId(sessionId)
|
|
38
|
+
|| typeof transcriptPath !== 'string' || !transcriptPath)
|
|
39
|
+
continue;
|
|
40
|
+
try {
|
|
41
|
+
await access(transcriptPath, constants.R_OK);
|
|
42
|
+
if (!(await stat(transcriptPath)).isFile())
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
bindings.set(paneRuntimeId, { id, sessionId, transcriptPath });
|
|
49
|
+
}
|
|
50
|
+
return bindings;
|
|
51
|
+
}
|
|
52
|
+
function topologyPanes(topology) {
|
|
53
|
+
return topology.windows.flatMap((window) => window.panes);
|
|
54
|
+
}
|
|
55
|
+
export async function readManagedCodexBindings(topology, { codexApp, codexSessions = codexSessionsDir(), agents = AGENTS, findRollout = resolveCodexRollout, access = fsp.access, stat = fsp.stat, } = {}) {
|
|
56
|
+
if (typeof codexApp?.discover !== 'function')
|
|
57
|
+
return new Map();
|
|
58
|
+
const panes = topologyPanes(topology);
|
|
59
|
+
const discovered = new Map(await Promise.all(panes.map(async (pane) => {
|
|
60
|
+
try {
|
|
61
|
+
const state = await codexApp.discover(pane.runtimeId);
|
|
62
|
+
return [pane.runtimeId, state?.managed && typeof state.threadId === 'string' ? state.threadId : null];
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return [pane.runtimeId, null];
|
|
66
|
+
}
|
|
67
|
+
})));
|
|
68
|
+
const driver = agents.find((agent) => agent.id === 'codex');
|
|
69
|
+
if (!driver)
|
|
70
|
+
return new Map();
|
|
71
|
+
const bindings = new Map();
|
|
72
|
+
for (const pane of panes) {
|
|
73
|
+
const sessionId = discovered.get(pane.runtimeId);
|
|
74
|
+
if (typeof sessionId !== 'string' || !driver.sessions?.isId(sessionId))
|
|
75
|
+
continue;
|
|
76
|
+
let transcriptPath;
|
|
77
|
+
try {
|
|
78
|
+
transcriptPath = await findRollout(codexSessions, sessionId);
|
|
79
|
+
if (typeof transcriptPath !== 'string' || !transcriptPath)
|
|
80
|
+
continue;
|
|
81
|
+
await access(transcriptPath, constants.R_OK);
|
|
82
|
+
if (!(await stat(transcriptPath)).isFile())
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
bindings.set(pane.runtimeId, { id: 'codex', sessionId, transcriptPath });
|
|
89
|
+
}
|
|
90
|
+
return bindings;
|
|
91
|
+
}
|
|
92
|
+
export function attachBindings(topology, bindings) {
|
|
93
|
+
return {
|
|
94
|
+
...topology,
|
|
95
|
+
windows: topology.windows.map((window) => ({
|
|
96
|
+
...window,
|
|
97
|
+
panes: window.panes.map((pane) => ({ ...pane, agent: bindings.get(pane.runtimeId) || null })),
|
|
98
|
+
})),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function isUnknownFingerprint(value) {
|
|
102
|
+
return value === null || value === undefined
|
|
103
|
+
|| (typeof value === 'object' && 'status' in value && value.status === 'unknown');
|
|
104
|
+
}
|
|
105
|
+
export async function captureWorkspace({ tmux, stateFile, environment, agents = AGENTS, readFile = fsp.readFile, access = fsp.access, stat = fsp.stat, codexApp, codexSessions = codexSessionsDir(), findCodexRollout = resolveCodexRollout, now = Date.now, }) {
|
|
106
|
+
try {
|
|
107
|
+
const before = await tmux.topologyFingerprint();
|
|
108
|
+
if (isUnknownFingerprint(before))
|
|
109
|
+
return { status: 'unknown' };
|
|
110
|
+
const topology = await tmux.captureTopology();
|
|
111
|
+
if (!topology || topology.status === 'unknown')
|
|
112
|
+
return { status: 'unknown' };
|
|
113
|
+
const bindings = await readAgentBindings(stateFile, agents, readFile, access, stat);
|
|
114
|
+
const managedCodex = await readManagedCodexBindings(topology, {
|
|
115
|
+
...(codexApp ? { codexApp } : {}), codexSessions, agents,
|
|
116
|
+
findRollout: findCodexRollout, access, stat,
|
|
117
|
+
});
|
|
118
|
+
for (const [pane, binding] of managedCodex)
|
|
119
|
+
bindings.set(pane, binding);
|
|
120
|
+
const after = await tmux.topologyFingerprint();
|
|
121
|
+
if (isUnknownFingerprint(after))
|
|
122
|
+
return { status: 'unknown' };
|
|
123
|
+
if (before !== after)
|
|
124
|
+
return { status: 'changed-during-capture' };
|
|
125
|
+
const snapshot = canonicalizeSnapshot(attachBindings({
|
|
126
|
+
schemaVersion: WORKSPACE_SCHEMA_VERSION,
|
|
127
|
+
capturedAt: new Date(now()).toISOString(),
|
|
128
|
+
environment: { ...environment },
|
|
129
|
+
tmuxVersion: topology.tmuxVersion,
|
|
130
|
+
active: topology.active,
|
|
131
|
+
sessions: topology.sessions,
|
|
132
|
+
windows: topology.windows,
|
|
133
|
+
}, bindings));
|
|
134
|
+
return { status: topology.status === 'empty' ? 'empty' : 'ok', snapshot };
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
return { status: 'unknown' };
|
|
138
|
+
}
|
|
139
|
+
}
|