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,118 @@
|
|
|
1
|
+
const record = (value) => (value !== null && typeof value === 'object' && !Array.isArray(value) ? value : null);
|
|
2
|
+
const text = (value) => (typeof value === 'string' && value.trim() ? value.trim() : undefined);
|
|
3
|
+
function effortOptions(value) {
|
|
4
|
+
if (!Array.isArray(value))
|
|
5
|
+
return [];
|
|
6
|
+
return value.flatMap((candidate) => {
|
|
7
|
+
const item = record(candidate);
|
|
8
|
+
const id = text(item?.reasoningEffort);
|
|
9
|
+
if (!id)
|
|
10
|
+
return [];
|
|
11
|
+
const description = text(item?.description);
|
|
12
|
+
return [{ id, label: id, ...(description === undefined ? {} : { description }) }];
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function tierOptions(model) {
|
|
16
|
+
const explicit = Array.isArray(model.serviceTiers)
|
|
17
|
+
? model.serviceTiers.flatMap((candidate) => {
|
|
18
|
+
const item = record(candidate);
|
|
19
|
+
const id = text(item?.id);
|
|
20
|
+
if (!id)
|
|
21
|
+
return [];
|
|
22
|
+
const label = text(item?.name);
|
|
23
|
+
const description = text(item?.description);
|
|
24
|
+
return [{
|
|
25
|
+
id,
|
|
26
|
+
...(label === undefined ? {} : { label }),
|
|
27
|
+
...(description === undefined ? {} : { description }),
|
|
28
|
+
}];
|
|
29
|
+
}) : [];
|
|
30
|
+
const fallback = explicit.length ? explicit : Array.isArray(model.additionalSpeedTiers)
|
|
31
|
+
? model.additionalSpeedTiers.flatMap((candidate) => {
|
|
32
|
+
const id = text(candidate);
|
|
33
|
+
return id ? [{ id, label: id }] : [];
|
|
34
|
+
}) : [];
|
|
35
|
+
return fallback.length ? fallback : undefined;
|
|
36
|
+
}
|
|
37
|
+
function modelOptions(value) {
|
|
38
|
+
if (!Array.isArray(value))
|
|
39
|
+
return [];
|
|
40
|
+
return value.flatMap((candidate) => {
|
|
41
|
+
const item = record(candidate);
|
|
42
|
+
if (!item)
|
|
43
|
+
return [];
|
|
44
|
+
const id = text(item.model) ?? text(item.id);
|
|
45
|
+
if (!id)
|
|
46
|
+
return [];
|
|
47
|
+
const label = text(item.displayName) ?? id;
|
|
48
|
+
const description = text(item.description);
|
|
49
|
+
const efforts = effortOptions(item.supportedReasoningEfforts);
|
|
50
|
+
const defaultEffort = text(item.defaultReasoningEffort);
|
|
51
|
+
const serviceTiers = tierOptions(item);
|
|
52
|
+
return [{
|
|
53
|
+
id,
|
|
54
|
+
label,
|
|
55
|
+
...(description === undefined ? {} : { description }),
|
|
56
|
+
efforts,
|
|
57
|
+
...(defaultEffort === undefined || !efforts.some((effort) => effort.id === defaultEffort)
|
|
58
|
+
? {} : { defaultEffort }),
|
|
59
|
+
...(serviceTiers === undefined ? {} : { serviceTiers }),
|
|
60
|
+
}];
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
function settingsFrom(value) {
|
|
64
|
+
const root = record(value);
|
|
65
|
+
return record(root?.settings) ?? root ?? {};
|
|
66
|
+
}
|
|
67
|
+
function snapshot(modelsValue, settingsValue) {
|
|
68
|
+
const models = modelOptions(modelsValue);
|
|
69
|
+
const settings = settingsFrom(settingsValue);
|
|
70
|
+
const selectedModelId = text(settings.model) ?? null;
|
|
71
|
+
const selectedModel = models.find((model) => model.id === selectedModelId);
|
|
72
|
+
const effort = text(settings.effort);
|
|
73
|
+
const serviceTier = settings.serviceTier === null ? null : text(settings.serviceTier) ?? null;
|
|
74
|
+
return {
|
|
75
|
+
models,
|
|
76
|
+
selected: {
|
|
77
|
+
model: selectedModel?.id ?? null,
|
|
78
|
+
effort: effort && selectedModel?.efforts.some((candidate) => candidate.id === effort)
|
|
79
|
+
? effort : null,
|
|
80
|
+
serviceTier: serviceTier && selectedModel?.serviceTiers?.some((tier) => tier.id === serviceTier)
|
|
81
|
+
? serviceTier : null,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
export function createCodexSessionControlAdapter(app) {
|
|
86
|
+
if (!app || typeof app.models !== 'function' || typeof app.status !== 'function'
|
|
87
|
+
|| typeof app.updateSettings !== 'function') {
|
|
88
|
+
throw new TypeError('Codex Session Control adapter requires App Server model/settings APIs');
|
|
89
|
+
}
|
|
90
|
+
const read = async (pane, threadId) => {
|
|
91
|
+
const [models, status] = await Promise.all([
|
|
92
|
+
app.models(pane, threadId),
|
|
93
|
+
app.status(pane, threadId),
|
|
94
|
+
]);
|
|
95
|
+
return snapshot(models, status);
|
|
96
|
+
};
|
|
97
|
+
return {
|
|
98
|
+
apiVersion: 1,
|
|
99
|
+
async readModelControl(run, options) {
|
|
100
|
+
const threadId = run.ref.sessionId;
|
|
101
|
+
if (!threadId || run.signal.aborted)
|
|
102
|
+
return null;
|
|
103
|
+
// Codex App Server's model/list is already a live native read and exposes no stronger refresh
|
|
104
|
+
// operation. `refresh` therefore has the same semantics instead of being backed by a Web cache.
|
|
105
|
+
void options?.refresh;
|
|
106
|
+
return read(run.ref.paneId, threadId);
|
|
107
|
+
},
|
|
108
|
+
async updateModelControl(run, patch) {
|
|
109
|
+
const threadId = run.ref.sessionId;
|
|
110
|
+
if (!threadId || run.signal.aborted)
|
|
111
|
+
throw new Error('Codex run is unavailable');
|
|
112
|
+
await app.updateSettings(run.ref.paneId, threadId, { ...patch });
|
|
113
|
+
if (run.signal.aborted)
|
|
114
|
+
throw new Error('Codex run is unavailable');
|
|
115
|
+
return read(run.ref.paneId, threadId);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
import { readLatestUsage, readSnapshot, writeSnapshot } from '../codexUsageSnapshot.js';
|
|
6
|
+
import { codexSessionsDir, codexUsagePath } from '../usagePaths.js';
|
|
7
|
+
const isRecord = (value) => (typeof value === 'object' && value !== null && !Array.isArray(value));
|
|
8
|
+
const finite = (value) => typeof value === 'number' && Number.isFinite(value);
|
|
9
|
+
const optionalString = (value) => (typeof value === 'string' && value.trim() ? value.trim() : null);
|
|
10
|
+
function rolloutFilesByMtime(dir) {
|
|
11
|
+
const files = [];
|
|
12
|
+
const visit = (current) => {
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const entry of entries) {
|
|
21
|
+
if (entry.name.startsWith('.'))
|
|
22
|
+
continue;
|
|
23
|
+
const file = path.join(current, entry.name);
|
|
24
|
+
if (entry.isDirectory())
|
|
25
|
+
visit(file);
|
|
26
|
+
else if (entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
|
|
27
|
+
try {
|
|
28
|
+
files.push({ file, mtimeMs: fs.statSync(file).mtimeMs });
|
|
29
|
+
}
|
|
30
|
+
catch { /* raced away */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
visit(dir);
|
|
35
|
+
return files.sort((first, second) => second.mtimeMs - first.mtimeMs);
|
|
36
|
+
}
|
|
37
|
+
function scanCodexUsage(home) {
|
|
38
|
+
let latest = null;
|
|
39
|
+
for (const { file, mtimeMs } of rolloutFilesByMtime(codexSessionsDir(home))) {
|
|
40
|
+
if (latest?.updatedAt && mtimeMs < latest.updatedAt)
|
|
41
|
+
break;
|
|
42
|
+
const usage = readLatestUsage(file);
|
|
43
|
+
if (usage && (!latest || usage.updatedAt > latest.updatedAt))
|
|
44
|
+
latest = usage;
|
|
45
|
+
}
|
|
46
|
+
return latest;
|
|
47
|
+
}
|
|
48
|
+
export function readCodexSubscriptionUsage(home = homedir(), { now = Date.now(), calibrationMs = 60_000 } = {}) {
|
|
49
|
+
const file = codexUsagePath(home);
|
|
50
|
+
const snapshot = readSnapshot(file);
|
|
51
|
+
if (snapshot && now - snapshot.checkedAt < calibrationMs)
|
|
52
|
+
return snapshot.usage;
|
|
53
|
+
writeSnapshot(file, scanCodexUsage(home), { checkedAt: now });
|
|
54
|
+
return readSnapshot(file)?.usage ?? null;
|
|
55
|
+
}
|
|
56
|
+
function normalizeWindow(value) {
|
|
57
|
+
if (!isRecord(value) || !finite(value.usedPercent))
|
|
58
|
+
return null;
|
|
59
|
+
return {
|
|
60
|
+
usedPercent: value.usedPercent,
|
|
61
|
+
windowMinutes: finite(value.windowDurationMins) ? value.windowDurationMins : null,
|
|
62
|
+
resetsAt: finite(value.resetsAt) ? value.resetsAt : null,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function normalizeAccount(value) {
|
|
66
|
+
if (!isRecord(value))
|
|
67
|
+
return null;
|
|
68
|
+
const type = optionalString(value.type);
|
|
69
|
+
return type ? { type, email: optionalString(value.email), planType: optionalString(value.planType) } : null;
|
|
70
|
+
}
|
|
71
|
+
function normalizeModels(value, mainLimitId) {
|
|
72
|
+
if (!isRecord(value))
|
|
73
|
+
return [];
|
|
74
|
+
const models = [];
|
|
75
|
+
const seen = new Set();
|
|
76
|
+
for (const [key, raw] of Object.entries(value)) {
|
|
77
|
+
if (!isRecord(raw))
|
|
78
|
+
continue;
|
|
79
|
+
const limitId = optionalString(raw.limitId) ?? key;
|
|
80
|
+
if (limitId === (mainLimitId ?? 'codex') || seen.has(limitId))
|
|
81
|
+
continue;
|
|
82
|
+
const primary = normalizeWindow(raw.primary);
|
|
83
|
+
const secondary = normalizeWindow(raw.secondary);
|
|
84
|
+
if (!primary && !secondary)
|
|
85
|
+
continue;
|
|
86
|
+
models.push({ limitId, limitName: optionalString(raw.limitName), primary, secondary });
|
|
87
|
+
seen.add(limitId);
|
|
88
|
+
}
|
|
89
|
+
return models;
|
|
90
|
+
}
|
|
91
|
+
function normalizeCredits(value) {
|
|
92
|
+
if (!isRecord(value) || !Number.isSafeInteger(value.availableCount))
|
|
93
|
+
return null;
|
|
94
|
+
const availableCount = value.availableCount;
|
|
95
|
+
if (availableCount < 0)
|
|
96
|
+
return null;
|
|
97
|
+
const expiryTimes = (Array.isArray(value.credits) ? value.credits : []).flatMap((credit) => {
|
|
98
|
+
if (!isRecord(credit) || credit.status !== 'available' || !finite(credit.expiresAt)
|
|
99
|
+
|| credit.expiresAt <= 0 || !Number.isFinite(new Date(credit.expiresAt * 1000).getTime()))
|
|
100
|
+
return [];
|
|
101
|
+
return [credit.expiresAt];
|
|
102
|
+
}).sort((first, second) => first - second).slice(0, availableCount);
|
|
103
|
+
return { availableCount, expiryTimes };
|
|
104
|
+
}
|
|
105
|
+
export function fetchCodexSubscriptionUsage({ home = homedir(), command = 'codex', args = ['app-server', '--stdio'], timeoutMs = 15_000, } = {}) {
|
|
106
|
+
return new Promise((resolve) => {
|
|
107
|
+
let child;
|
|
108
|
+
let settled = false;
|
|
109
|
+
let stdout = '';
|
|
110
|
+
let account = null;
|
|
111
|
+
let accountRead = false;
|
|
112
|
+
let limits = null;
|
|
113
|
+
const finish = (value) => {
|
|
114
|
+
if (settled)
|
|
115
|
+
return;
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
try {
|
|
119
|
+
child?.kill();
|
|
120
|
+
}
|
|
121
|
+
catch { /* best effort */ }
|
|
122
|
+
resolve(value);
|
|
123
|
+
};
|
|
124
|
+
const finishWhenReady = () => { if (accountRead && limits)
|
|
125
|
+
finish({ account, ...limits }); };
|
|
126
|
+
const timer = setTimeout(() => finish(null), timeoutMs);
|
|
127
|
+
try {
|
|
128
|
+
child = spawn(command, args, {
|
|
129
|
+
env: { ...process.env, CODEX_HOME: path.join(home, '.codex') },
|
|
130
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
finish(null);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
child.on('error', () => finish(null));
|
|
138
|
+
child.on('exit', () => finish(null));
|
|
139
|
+
child.stdout?.on('data', (chunk) => {
|
|
140
|
+
stdout += chunk;
|
|
141
|
+
if (stdout.length > 1024 * 1024) {
|
|
142
|
+
finish(null);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
let newline;
|
|
146
|
+
while ((newline = stdout.indexOf('\n')) >= 0) {
|
|
147
|
+
const line = stdout.slice(0, newline);
|
|
148
|
+
stdout = stdout.slice(newline + 1);
|
|
149
|
+
if (!line)
|
|
150
|
+
continue;
|
|
151
|
+
let message;
|
|
152
|
+
try {
|
|
153
|
+
message = JSON.parse(line);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!isRecord(message))
|
|
159
|
+
continue;
|
|
160
|
+
if (message.id === 1 && message.result) {
|
|
161
|
+
child?.stdin?.write(`${JSON.stringify({ method: 'initialized', params: {} })}\n`);
|
|
162
|
+
child?.stdin?.write(`${JSON.stringify({ method: 'account/read', id: 2, params: { refreshToken: false } })}\n`);
|
|
163
|
+
child?.stdin?.write(`${JSON.stringify({ method: 'account/rateLimits/read', id: 3 })}\n`);
|
|
164
|
+
}
|
|
165
|
+
else if (message.id === 2) {
|
|
166
|
+
const result = isRecord(message.result) ? message.result : null;
|
|
167
|
+
account = normalizeAccount(result?.account);
|
|
168
|
+
accountRead = true;
|
|
169
|
+
finishWhenReady();
|
|
170
|
+
}
|
|
171
|
+
else if (message.id === 3) {
|
|
172
|
+
const result = isRecord(message.result) ? message.result : null;
|
|
173
|
+
const source = result && isRecord(result.rateLimits) ? result.rateLimits : null;
|
|
174
|
+
if (!source) {
|
|
175
|
+
finish(null);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
limits = {
|
|
179
|
+
rateLimits: {
|
|
180
|
+
primary: normalizeWindow(source.primary), secondary: normalizeWindow(source.secondary),
|
|
181
|
+
},
|
|
182
|
+
modelRateLimits: normalizeModels(result?.rateLimitsByLimitId, optionalString(source.limitId)),
|
|
183
|
+
resetCredits: normalizeCredits(result?.rateLimitResetCredits),
|
|
184
|
+
};
|
|
185
|
+
finishWhenReady();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
child.stdin?.on('error', () => finish(null));
|
|
190
|
+
child.stdin?.write(`${JSON.stringify({
|
|
191
|
+
method: 'initialize', id: 1,
|
|
192
|
+
params: { clientInfo: { name: 'handmux', title: 'handmux', version: '0.0.0' } },
|
|
193
|
+
})}\n`);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function adapterWindow(id, value) {
|
|
197
|
+
if (!value)
|
|
198
|
+
return null;
|
|
199
|
+
return {
|
|
200
|
+
id, usedPercent: value.usedPercent,
|
|
201
|
+
...(value.windowMinutes === null ? {} : { windowMinutes: value.windowMinutes }),
|
|
202
|
+
...(value.resetsAt === null ? {} : { resetsAt: value.resetsAt }),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function group(kind, id, label, limits) {
|
|
206
|
+
const windows = [adapterWindow('primary', limits.primary), adapterWindow('secondary', limits.secondary)]
|
|
207
|
+
.filter((window) => window !== null);
|
|
208
|
+
return windows.length ? { kind, id, ...(label ? { label } : {}), windows } : null;
|
|
209
|
+
}
|
|
210
|
+
function titleCase(value) {
|
|
211
|
+
return value.replace(/[_-]+/g, ' ').replace(/\b\w/g, (letter) => letter.toUpperCase());
|
|
212
|
+
}
|
|
213
|
+
function accountLabel(account) {
|
|
214
|
+
if (account.email)
|
|
215
|
+
return account.email;
|
|
216
|
+
if (account.type === 'apiKey')
|
|
217
|
+
return 'API Key';
|
|
218
|
+
if (account.type === 'chatgpt')
|
|
219
|
+
return 'ChatGPT';
|
|
220
|
+
return titleCase(account.type);
|
|
221
|
+
}
|
|
222
|
+
export function createCodexSubscriptionUsageAdapter(options = {}) {
|
|
223
|
+
const home = options.home ?? homedir();
|
|
224
|
+
const now = options.now ?? Date.now;
|
|
225
|
+
let lastAccount = null;
|
|
226
|
+
let accountCheckedAt = Number.NEGATIVE_INFINITY;
|
|
227
|
+
let accountPromise = null;
|
|
228
|
+
let accountPromiseForced = false;
|
|
229
|
+
let legacy = null;
|
|
230
|
+
const refreshAccount = async (force) => {
|
|
231
|
+
const current = now();
|
|
232
|
+
if (!force && current - accountCheckedAt < (options.accountTtlMs ?? 60_000))
|
|
233
|
+
return lastAccount;
|
|
234
|
+
if (accountPromise) {
|
|
235
|
+
if (!force || accountPromiseForced)
|
|
236
|
+
return accountPromise;
|
|
237
|
+
try {
|
|
238
|
+
await accountPromise;
|
|
239
|
+
}
|
|
240
|
+
catch { /* the explicit refresh below gets its own authoritative result */ }
|
|
241
|
+
return refreshAccount(true);
|
|
242
|
+
}
|
|
243
|
+
let pending;
|
|
244
|
+
pending = (options.fetchAccount
|
|
245
|
+
? options.fetchAccount()
|
|
246
|
+
: fetchCodexSubscriptionUsage({ ...options, home })).then((fetched) => {
|
|
247
|
+
if (!fetched && force)
|
|
248
|
+
throw new Error('Codex subscription usage refresh failed');
|
|
249
|
+
if (fetched) {
|
|
250
|
+
accountCheckedAt = now();
|
|
251
|
+
lastAccount = { ...fetched, updatedAt: accountCheckedAt };
|
|
252
|
+
}
|
|
253
|
+
return lastAccount;
|
|
254
|
+
}).finally(() => {
|
|
255
|
+
if (accountPromise === pending) {
|
|
256
|
+
accountPromise = null;
|
|
257
|
+
accountPromiseForced = false;
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
accountPromise = pending;
|
|
261
|
+
accountPromiseForced = force;
|
|
262
|
+
return pending;
|
|
263
|
+
};
|
|
264
|
+
return {
|
|
265
|
+
apiVersion: 1,
|
|
266
|
+
async snapshot({ refresh = false } = {}) {
|
|
267
|
+
const rollout = readCodexSubscriptionUsage(home, {
|
|
268
|
+
now: now(), calibrationMs: refresh ? 0 : options.calibrationMs ?? 60_000,
|
|
269
|
+
});
|
|
270
|
+
const account = await refreshAccount(refresh);
|
|
271
|
+
const effectiveLimits = account?.rateLimits ?? rollout?.rateLimits ?? { primary: null, secondary: null };
|
|
272
|
+
const coreResetCredits = account?.resetCredits ? {
|
|
273
|
+
availableCount: account.resetCredits.availableCount,
|
|
274
|
+
expiryTimes: account.resetCredits.expiryTimes.slice(0, 256),
|
|
275
|
+
} : null;
|
|
276
|
+
const groups = [];
|
|
277
|
+
const accountGroup = group('account', 'account', null, effectiveLimits);
|
|
278
|
+
if (accountGroup)
|
|
279
|
+
groups.push(accountGroup);
|
|
280
|
+
for (const model of (account?.modelRateLimits ?? []).slice(0, 63)) {
|
|
281
|
+
const modelGroup = group('model', model.limitId, model.limitName, model);
|
|
282
|
+
if (modelGroup)
|
|
283
|
+
groups.push(modelGroup);
|
|
284
|
+
}
|
|
285
|
+
legacy = !account && !rollout ? null : {
|
|
286
|
+
updatedAt: account?.updatedAt ?? rollout?.updatedAt ?? null,
|
|
287
|
+
account: account?.account ? structuredClone(account.account) : null,
|
|
288
|
+
rateLimits: structuredClone(effectiveLimits),
|
|
289
|
+
modelRateLimits: structuredClone(account?.modelRateLimits ?? []),
|
|
290
|
+
rateLimitResetCredits: account?.resetCredits
|
|
291
|
+
? structuredClone(account.resetCredits) : null,
|
|
292
|
+
tokens: structuredClone(rollout?.tokens ?? {
|
|
293
|
+
total: null, input: null, cachedInput: null, output: null, reasoning: null,
|
|
294
|
+
}),
|
|
295
|
+
contextWindow: rollout?.contextWindow ?? null,
|
|
296
|
+
};
|
|
297
|
+
return {
|
|
298
|
+
...(account?.account ? {
|
|
299
|
+
account: {
|
|
300
|
+
label: accountLabel(account.account),
|
|
301
|
+
...(account.account.planType ? { plan: titleCase(account.account.planType) } : {}),
|
|
302
|
+
},
|
|
303
|
+
} : {}),
|
|
304
|
+
groups,
|
|
305
|
+
...(coreResetCredits ? { resetCredits: coreResetCredits } : {}),
|
|
306
|
+
updatedAt: account?.updatedAt ?? rollout?.updatedAt ?? null,
|
|
307
|
+
status: !account && !rollout ? 'unavailable'
|
|
308
|
+
: groups.length || account?.account || account?.resetCredits ? 'ready' : 'pending',
|
|
309
|
+
};
|
|
310
|
+
},
|
|
311
|
+
legacySnapshot: () => legacy ? structuredClone(legacy) : null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const INPUT_SETTLE_MS = 100;
|
|
2
|
+
const CLEAR_SWITCH_ATTEMPTS = 60;
|
|
3
|
+
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
|
+
// Codex's /clear replaces the active TUI chat widget as well as the App Server thread. Driving that
|
|
5
|
+
// native action keeps terminal and Conversation on one session; a direct thread/start would split them.
|
|
6
|
+
export async function clearCodexConversationThroughTui(codexApp, commands, pane, previousThreadId, wait = pause) {
|
|
7
|
+
await commands.exitCopyModeIfActive(pane);
|
|
8
|
+
await commands.sendKey(pane, 'C-u');
|
|
9
|
+
await wait(INPUT_SETTLE_MS);
|
|
10
|
+
await commands.sendText(pane, '/clear');
|
|
11
|
+
await wait(INPUT_SETTLE_MS);
|
|
12
|
+
await commands.sendEnter(pane);
|
|
13
|
+
for (let attempt = 0; attempt < CLEAR_SWITCH_ATTEMPTS; attempt += 1) {
|
|
14
|
+
const discovered = await codexApp.discover(pane);
|
|
15
|
+
if (!discovered?.managed)
|
|
16
|
+
throw new Error('Codex session is no longer managed by Handmux');
|
|
17
|
+
if (discovered.threadId && discovered.threadId !== previousThreadId) {
|
|
18
|
+
return { threadId: discovered.threadId };
|
|
19
|
+
}
|
|
20
|
+
await wait(INPUT_SETTLE_MS);
|
|
21
|
+
}
|
|
22
|
+
throw new Error('Codex terminal did not accept /clear; switch to the terminal, close any open panel, and try again');
|
|
23
|
+
}
|