handmux 0.23.0 → 0.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -8
- package/README.zh-CN.md +31 -8
- package/bin/handmux.js +14 -688
- package/bin/node-version.js +20 -0
- package/dist/bin/handmux.js +1015 -0
- package/dist/connectors/bridgeClient.js +385 -0
- package/dist/connectors/claude/index.js +476 -0
- package/dist/connectors/pi/bridgeClient.js +8 -0
- package/dist/connectors/pi/index.js +851 -0
- package/dist/hooks/handmux-notify.sh +64 -0
- package/{hooks → dist/hooks}/handmux-statusline.cjs +28 -6
- package/dist/hooks/handmux-write.cjs +264 -0
- package/dist/package.json +127 -0
- package/dist/public/assets/index-CTrqTqtT.css +32 -0
- package/dist/public/assets/index-DmjeMSQA.js +346 -0
- package/dist/public/assets/pcm-worklet-CxcDy7PB.js +1 -0
- package/{public → dist/public}/index.html +5 -4
- package/dist/src/agent-runtime/adapter.js +204 -0
- package/dist/src/agent-runtime/bridge.js +1006 -0
- package/dist/src/agent-runtime/bridgeStore.js +25 -0
- package/dist/src/agent-runtime/bridgeTransport.js +619 -0
- package/dist/src/agent-runtime/bridgeTypes.js +13 -0
- package/dist/src/agent-runtime/builtinRuntime.js +193 -0
- package/dist/src/agent-runtime/conversation.js +2179 -0
- package/dist/src/agent-runtime/conversationActivation.js +75 -0
- package/dist/src/agent-runtime/conversationActivity.js +69 -0
- package/dist/src/agent-runtime/conversationControls.js +206 -0
- package/dist/src/agent-runtime/conversationLiveHub.js +276 -0
- package/dist/src/agent-runtime/conversationStore.js +352 -0
- package/dist/src/agent-runtime/conversationTypes.js +1 -0
- package/dist/src/agent-runtime/conversationValidation.js +339 -0
- package/dist/src/agent-runtime/inbox.js +973 -0
- package/dist/src/agent-runtime/inboxPushProjection.js +69 -0
- package/dist/src/agent-runtime/inboxStore.js +25 -0
- package/dist/src/agent-runtime/inboxTypes.js +1 -0
- package/dist/src/agent-runtime/interaction.js +829 -0
- package/dist/src/agent-runtime/interactionLiveHub.js +225 -0
- package/dist/src/agent-runtime/interactionStore.js +18 -0
- package/dist/src/agent-runtime/interactionTypes.js +1 -0
- package/dist/src/agent-runtime/legacyInboxProjection.js +70 -0
- package/dist/src/agent-runtime/migrateLegacyCodexOutbox.js +336 -0
- package/dist/src/agent-runtime/resourceTypes.js +1 -0
- package/dist/src/agent-runtime/resources.js +245 -0
- package/dist/src/agent-runtime/run.js +328 -0
- package/dist/src/agent-runtime/runtime.js +793 -0
- package/dist/src/agent-runtime/sessionControl.js +164 -0
- package/dist/src/agent-runtime/subscriptionUsage.js +190 -0
- package/dist/src/agent-runtime/subscriptionUsageLegacy.js +18 -0
- package/dist/src/agent-runtime/tmuxRuntime.js +156 -0
- package/dist/src/agents/claude.js +203 -0
- package/dist/src/agents/claudeConversation.js +362 -0
- package/dist/src/agents/claudeInteraction.js +134 -0
- package/dist/src/agents/claudeSubscriptionUsage.js +61 -0
- package/dist/src/agents/codex.js +262 -0
- package/dist/src/agents/codexConversation.js +1103 -0
- package/dist/src/agents/codexConversationActivation.js +121 -0
- package/dist/src/agents/codexConversationControls.js +256 -0
- package/dist/src/agents/codexInteraction.js +306 -0
- package/dist/src/agents/codexSessionControl.js +118 -0
- package/dist/src/agents/codexSubscriptionUsage.js +313 -0
- package/dist/src/agents/codexTerminalControl.js +23 -0
- package/dist/src/agents/conversationProjectionSafety.js +416 -0
- package/dist/src/agents/index.js +39 -0
- package/dist/src/agents/nativeInbox.js +264 -0
- package/dist/src/agents/nativeInboxSources.js +115 -0
- package/dist/src/agents/pi.js +28 -0
- package/dist/src/agents/piConversation.js +523 -0
- package/dist/src/agents/piConversationHistory.js +502 -0
- package/dist/src/agents/piInboxBridge.js +368 -0
- package/dist/src/agents/piSessionControl.js +32 -0
- package/dist/src/agents/processIdentity.js +94 -0
- package/dist/src/agents/scanUtils.js +262 -0
- package/dist/src/apiAccountProviders.js +194 -0
- package/dist/src/apiAccounts.js +592 -0
- package/dist/src/apiErrors.js +86 -0
- package/{src → dist/src}/appName.js +12 -13
- package/dist/src/asr/iflyConfig.js +7 -0
- package/dist/src/asr/iflySign.js +14 -0
- package/dist/src/auth.js +27 -0
- package/dist/src/browser/bootstrap.js +72 -0
- package/dist/src/browser/cookieProfiles.js +413 -0
- package/dist/src/browser/coordinator.js +51 -0
- package/dist/src/browser/hammerheadCompat.js +104 -0
- package/dist/src/browser/hammerheadRedirectCompat.js +37 -0
- package/dist/src/browser/manager.js +560 -0
- package/dist/src/browser/originLabel.js +15 -0
- package/dist/src/browser/profilePersistence.js +235 -0
- package/dist/src/browser/publicProxy.js +176 -0
- package/dist/src/browser/routes.js +159 -0
- package/dist/src/browser/sessionStore.js +72 -0
- package/dist/src/browser/siteVersion.js +121 -0
- package/dist/src/browser/targetPolicy.js +163 -0
- package/{src → dist/src}/browser/worker.js +14 -18
- package/dist/src/browser/workerClient.js +286 -0
- package/dist/src/browser/workerServer.js +91 -0
- package/dist/src/captureBackground.js +95 -0
- package/dist/src/claudeEvents.js +525 -0
- package/dist/src/cli/agentIntegration.js +131 -0
- package/dist/src/cli/agentLauncher.js +26 -0
- package/dist/src/cli/claudeHooks.js +290 -0
- package/{src → dist/src}/cli/cloudflareUrl.js +2 -3
- package/dist/src/cli/cloudflared.js +106 -0
- package/dist/src/cli/codexManaged.js +80 -0
- package/{src → dist/src}/cli/cpolarUrl.js +8 -9
- package/dist/src/cli/drivers.js +98 -0
- package/dist/src/cli/hookScaffold.js +39 -0
- package/dist/src/cli/i18n/en.js +392 -0
- package/{src → dist/src}/cli/i18n/index.js +27 -25
- package/dist/src/cli/i18n/zh.js +391 -0
- package/dist/src/cli/legacyCodexHooks.js +52 -0
- package/{src → dist/src}/cli/natappUrl.js +2 -3
- package/dist/src/cli/options.js +245 -0
- package/dist/src/cli/piExtension.js +155 -0
- package/dist/src/cli/probe.js +16 -0
- package/{src → dist/src}/cli/prompt.js +4 -6
- package/dist/src/cli/pushCmd.js +102 -0
- package/{src → dist/src}/cli/qr.js +19 -19
- package/dist/src/cli/service.js +154 -0
- package/dist/src/cli/setupModel.js +194 -0
- package/dist/src/cli/setupWizard.js +647 -0
- package/dist/src/cli/shortcutEditor.js +351 -0
- package/dist/src/cli/sshTunnel.js +18 -0
- package/dist/src/cli/state.js +117 -0
- package/dist/src/cli/statusLine.js +115 -0
- package/dist/src/cli/supervisor.js +278 -0
- package/dist/src/cli/supervisorHealth.js +21 -0
- package/dist/src/cli/supervisorLaunch.js +148 -0
- package/dist/src/cli/supervisorProcesses.js +62 -0
- package/dist/src/cli/supervisorState.js +62 -0
- package/dist/src/cli/tmuxVersion.js +53 -0
- package/{src → dist/src}/cli/tunlite.js +7 -10
- package/dist/src/cli/tunnelClients.js +72 -0
- package/{src → dist/src}/cli/updateCheck.js +98 -69
- package/{src → dist/src}/cli/urlHost.js +8 -4
- package/dist/src/cli/workspaceCmd.js +347 -0
- package/dist/src/codexAppServer.js +3436 -0
- package/dist/src/codexConversationProjection.js +199 -0
- package/dist/src/codexDiff.js +118 -0
- package/dist/src/codexMessageIdentity.js +18 -0
- package/dist/src/codexPlan.js +30 -0
- package/dist/src/codexQueueProtocol.js +137 -0
- package/dist/src/codexStreamProtocol.js +207 -0
- package/dist/src/codexToolProtocol.js +68 -0
- package/dist/src/codexTranscriptParse.js +835 -0
- package/dist/src/codexUsageSnapshot.js +228 -0
- package/dist/src/config.js +8 -0
- package/{src → dist/src}/docPath.js +22 -24
- package/dist/src/docs.js +337 -0
- package/dist/src/git.js +284 -0
- package/dist/src/healthProtocol.js +97 -0
- package/dist/src/httpApi.js +96 -0
- package/dist/src/jsonStore.js +15 -0
- package/{src → dist/src}/keyNames.js +3 -3
- package/dist/src/notifications.js +111 -0
- package/dist/src/orphans.js +167 -0
- package/dist/src/paneInput.js +42 -0
- package/dist/src/pendingPrompt.js +194 -0
- package/dist/src/previewServer.js +202 -0
- package/dist/src/previews.js +179 -0
- package/dist/src/privateStateStore.js +124 -0
- package/dist/src/projectTask/backup.js +224 -0
- package/dist/src/projectTask/lock.js +108 -0
- package/dist/src/projectTask/migrations.js +90 -0
- package/dist/src/projectTask/routes.js +155 -0
- package/dist/src/projectTask/runtime.js +182 -0
- package/dist/src/projectTask/schema.js +56 -0
- package/dist/src/projectTask/store.js +457 -0
- package/dist/src/push.js +224 -0
- package/dist/src/routes/agents.js +631 -0
- package/dist/src/routes/apiAccounts.js +82 -0
- package/dist/src/routes/files.js +230 -0
- package/dist/src/routes/git.js +89 -0
- package/dist/src/routes/health.js +16 -0
- package/dist/src/routes/notifications.js +16 -0
- package/dist/src/routes/previews.js +43 -0
- package/dist/src/routes/push.js +145 -0
- package/dist/src/routes/sessions.js +244 -0
- package/dist/src/routes/system.js +220 -0
- package/dist/src/routes/terminal.js +244 -0
- package/dist/src/routes/workspace.js +267 -0
- package/dist/src/server.js +315 -0
- package/dist/src/shortcutConfig.js +83 -0
- package/{src → dist/src}/staticCache.js +5 -3
- package/{src → dist/src}/staticCompression.js +5 -6
- package/dist/src/terminalStream.js +441 -0
- package/{src → dist/src}/tmux/commands.js +185 -172
- package/dist/src/tmux/format.js +47 -0
- package/dist/src/transcriptParse.js +261 -0
- package/dist/src/transcriptReader.js +209 -0
- package/{src → dist/src}/trimCapture.js +11 -11
- package/dist/src/uploadTypes.js +31 -0
- package/dist/src/urlPolicy.js +18 -0
- package/dist/src/usagePaths.js +15 -0
- package/dist/src/workspace/agentRunner.js +301 -0
- package/dist/src/workspace/atomicJson.js +31 -0
- package/dist/src/workspace/capture.js +139 -0
- package/dist/src/workspace/checkpointer.js +228 -0
- package/dist/src/workspace/environment.js +71 -0
- package/dist/src/workspace/lock.js +137 -0
- package/dist/src/workspace/mapping.js +128 -0
- package/dist/src/workspace/operations.js +271 -0
- package/dist/src/workspace/paths.js +15 -0
- package/dist/src/workspace/planner.js +292 -0
- package/dist/src/workspace/restore.js +363 -0
- package/dist/src/workspace/runtime.js +318 -0
- package/dist/src/workspace/schema.js +156 -0
- package/dist/src/workspace/store.js +464 -0
- package/dist/src/workspace/tmuxAdapter.js +704 -0
- package/dist/src/workspaceProtocol.js +158 -0
- package/package.json +30 -17
- package/hooks/handmux-notify.sh +0 -22
- package/hooks/handmux-write.cjs +0 -78
- package/public/assets/index-C3N7OkyP.css +0 -32
- package/public/assets/index-D1DWcC5u.js +0 -337
- package/src/.gitkeep +0 -0
- package/src/agents/claude.js +0 -159
- package/src/agents/codex.js +0 -168
- package/src/agents/index.js +0 -21
- package/src/agents/processIdentity.js +0 -72
- package/src/agents/scanUtils.js +0 -225
- package/src/asr/iflyConfig.js +0 -10
- package/src/asr/iflySign.js +0 -16
- package/src/auth.js +0 -30
- package/src/browser/bootstrap.js +0 -68
- package/src/browser/cookieProfiles.js +0 -403
- package/src/browser/coordinator.js +0 -47
- package/src/browser/hammerheadCompat.js +0 -86
- package/src/browser/hammerheadRedirectCompat.js +0 -38
- package/src/browser/manager.js +0 -556
- package/src/browser/originLabel.js +0 -17
- package/src/browser/profilePersistence.js +0 -241
- package/src/browser/publicProxy.js +0 -175
- package/src/browser/routes.js +0 -154
- package/src/browser/sessionStore.js +0 -78
- package/src/browser/siteVersion.js +0 -124
- package/src/browser/targetPolicy.js +0 -137
- package/src/browser/workerClient.js +0 -253
- package/src/browser/workerServer.js +0 -98
- package/src/captureBackground.js +0 -93
- package/src/claudeEvents.js +0 -399
- package/src/cli/claudeHooks.js +0 -206
- package/src/cli/cloudflared.js +0 -99
- package/src/cli/codexManaged.js +0 -77
- package/src/cli/drivers.js +0 -97
- package/src/cli/hookScaffold.js +0 -33
- package/src/cli/i18n/en.js +0 -385
- package/src/cli/i18n/zh.js +0 -384
- package/src/cli/legacyCodexHooks.js +0 -49
- package/src/cli/options.js +0 -211
- package/src/cli/probe.js +0 -16
- package/src/cli/pushCmd.js +0 -60
- package/src/cli/service.js +0 -126
- package/src/cli/setupModel.js +0 -154
- package/src/cli/setupWizard.js +0 -506
- package/src/cli/shortcutEditor.js +0 -288
- package/src/cli/sshTunnel.js +0 -12
- package/src/cli/state.js +0 -90
- package/src/cli/statusLine.js +0 -90
- package/src/cli/supervisor.js +0 -183
- package/src/cli/supervisorProcesses.js +0 -57
- package/src/cli/tmuxVersion.js +0 -49
- package/src/cli/tunnelClients.js +0 -62
- package/src/cli/workspaceCmd.js +0 -300
- package/src/codexAppServer.js +0 -1620
- package/src/codexDiff.js +0 -108
- package/src/codexTranscriptParse.js +0 -413
- package/src/codexUsageSnapshot.cjs +0 -129
- package/src/config.js +0 -9
- package/src/docs.js +0 -228
- package/src/git.js +0 -208
- package/src/httpApi.js +0 -72
- package/src/jsonStore.js +0 -24
- package/src/notifications.js +0 -68
- package/src/orphans.js +0 -152
- package/src/pendingPrompt.js +0 -163
- package/src/previewServer.js +0 -168
- package/src/previews.js +0 -116
- package/src/push.js +0 -183
- package/src/routes/codex.js +0 -473
- package/src/routes/files.js +0 -172
- package/src/routes/git.js +0 -57
- package/src/routes/notifications.js +0 -19
- package/src/routes/previews.js +0 -35
- package/src/routes/push.js +0 -119
- package/src/routes/sessions.js +0 -168
- package/src/routes/system.js +0 -132
- package/src/routes/terminal.js +0 -210
- package/src/routes/transcript.js +0 -171
- package/src/routes/workspace.js +0 -267
- package/src/server.js +0 -186
- package/src/shortcutConfig.js +0 -75
- package/src/terminalStream.js +0 -391
- package/src/tmux/format.js +0 -45
- package/src/transcriptParse.js +0 -180
- package/src/transcriptReader.js +0 -101
- package/src/uploadTypes.js +0 -30
- package/src/urlPolicy.js +0 -14
- package/src/usage.js +0 -260
- package/src/workspace/agentRunner.js +0 -188
- package/src/workspace/atomicJson.js +0 -32
- package/src/workspace/capture.js +0 -139
- package/src/workspace/checkpointer.js +0 -187
- package/src/workspace/environment.js +0 -75
- package/src/workspace/lock.js +0 -128
- package/src/workspace/mapping.js +0 -124
- package/src/workspace/operations.js +0 -252
- package/src/workspace/paths.js +0 -16
- package/src/workspace/planner.js +0 -247
- package/src/workspace/restore.js +0 -279
- package/src/workspace/runtime.js +0 -314
- package/src/workspace/schema.js +0 -136
- package/src/workspace/store.js +0 -382
- package/src/workspace/tmuxAdapter.js +0 -554
- /package/{public → dist/public}/fonts/JetBrainsMonoNerdFontMono-Regular.woff2 +0 -0
- /package/{public → dist/public}/fonts/TWUnifont.woff2 +0 -0
- /package/{public → dist/public}/icons/apple-touch-icon.png +0 -0
- /package/{public → dist/public}/icons/badge-96.png +0 -0
- /package/{public → dist/public}/icons/icon-192-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-192.png +0 -0
- /package/{public → dist/public}/icons/icon-512-maskable.png +0 -0
- /package/{public → dist/public}/icons/icon-512.png +0 -0
- /package/{public → dist/public}/icons/logo.svg +0 -0
- /package/{public → dist/public}/manifest.webmanifest +0 -0
- /package/{public → dist/public}/offline.html +0 -0
- /package/{public → dist/public}/sw.js +0 -0
- /package/{src → dist/src}/browser/protocol.js +0 -0
- /package/{src → dist/src}/cli/cfNamed.js +0 -0
package/src/push.js
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
// Web Push (VAPID) — minimal delivery layer. Sends notifications to subscribed devices via the
|
|
2
|
-
// browser/OS push services (Android=FCM, iOS=APNs); the server only talks to those services, never
|
|
3
|
-
// to the phone directly, so device reachability/queueing is their problem (see TTL/topic below).
|
|
4
|
-
//
|
|
5
|
-
// Subscriptions are stored as records: { subscription, boundSessions }. The subscription field is
|
|
6
|
-
// the raw PushSubscription the browser hands us ({endpoint, keys:{p256dh, auth}}); boundSessions is
|
|
7
|
-
// the list of session names this device cares about (used by sendToSession for targeted delivery). A
|
|
8
|
-
// dead subscription (404/410 from the push service) is pruned on the next send.
|
|
9
|
-
import webpush from 'web-push';
|
|
10
|
-
import fs from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
-
import crypto from 'node:crypto';
|
|
14
|
-
import { writeJsonAtomic } from './jsonStore.js';
|
|
15
|
-
|
|
16
|
-
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
-
const STORE = process.env.PUSH_STORE || path.resolve(here, '../data/push-subs.json');
|
|
18
|
-
|
|
19
|
-
// VAPID is optional: without keys every send is a no-op, so the server still boots in an
|
|
20
|
-
// environment that hasn't generated keys (configured=false surfaces as a 503 on /vapid). Push delivers
|
|
21
|
-
// whenever VAPID is configured — there is no separate "dev vs prod server" anymore (one config file, one
|
|
22
|
-
// `handmux start`). If you run a second instance on the same host and don't want it to deliver, leave
|
|
23
|
-
// `vapid` out of that instance's config. Init is LAZY (first use) so the module is import-safe.
|
|
24
|
-
let inited = false;
|
|
25
|
-
let configured = false;
|
|
26
|
-
function ensureInit() {
|
|
27
|
-
if (inited) return;
|
|
28
|
-
inited = true;
|
|
29
|
-
configured = !!(process.env.VAPID_PUBLIC && process.env.VAPID_PRIVATE);
|
|
30
|
-
if (configured) {
|
|
31
|
-
webpush.setVapidDetails(
|
|
32
|
-
// Apple (APNs) rejects a VAPID subject on a fake/.local domain with BadJwtToken — it must be a
|
|
33
|
-
// valid mailto:/https: with a real-looking domain. example.com is reserved and accepted by both.
|
|
34
|
-
process.env.VAPID_SUBJECT || 'mailto:admin@example.com',
|
|
35
|
-
process.env.VAPID_PUBLIC,
|
|
36
|
-
process.env.VAPID_PRIVATE,
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const genKey = () => crypto.randomBytes(18).toString('base64url');
|
|
42
|
-
const VALID_PUSH_KEY = /^[A-Za-z0-9_-]{16,128}$/;
|
|
43
|
-
|
|
44
|
-
let subs = load();
|
|
45
|
-
|
|
46
|
-
function load() {
|
|
47
|
-
try {
|
|
48
|
-
const raw = JSON.parse(fs.readFileSync(STORE, 'utf8')) || [];
|
|
49
|
-
return raw.map((e) => (e && e.subscription)
|
|
50
|
-
? { subscription: e.subscription, boundSessions: e.boundSessions || [], pushKey: e.pushKey || undefined }
|
|
51
|
-
: { subscription: e, boundSessions: [] }); // migrate old bare-subscription entries
|
|
52
|
-
} catch { return []; }
|
|
53
|
-
}
|
|
54
|
-
function persist() { writeJsonAtomic(STORE, subs); }
|
|
55
|
-
|
|
56
|
-
export function isConfigured() { ensureInit(); return configured; }
|
|
57
|
-
export function publicKey() { ensureInit(); return process.env.VAPID_PUBLIC || null; }
|
|
58
|
-
export function count() { return subs.length; }
|
|
59
|
-
|
|
60
|
-
export function addSubscription(sub, boundSessions = [], preferredPushKey = null) {
|
|
61
|
-
if (!sub || typeof sub.endpoint !== 'string') return false;
|
|
62
|
-
const endpointRecord = subs.find((s) => s.subscription.endpoint === sub.endpoint);
|
|
63
|
-
const requestedKey = VALID_PUSH_KEY.test(preferredPushKey || '') ? preferredPushKey : null;
|
|
64
|
-
const keyRecord = requestedKey ? subs.find((s) => s.pushKey === requestedKey) : null;
|
|
65
|
-
const pushKey = keyRecord?.pushKey || endpointRecord?.pushKey || requestedKey || genKey();
|
|
66
|
-
|
|
67
|
-
// A browser PushSubscription is transport state, not device identity. Re-enabling push can produce a
|
|
68
|
-
// different endpoint; replace the old endpoint that owns this stable key instead of creating a new
|
|
69
|
-
// script-push target. Also collapse any legacy duplicate for the new endpoint.
|
|
70
|
-
subs = subs.filter((s) => s.subscription.endpoint !== sub.endpoint && s.pushKey !== pushKey);
|
|
71
|
-
subs.push({ subscription: sub, boundSessions, pushKey });
|
|
72
|
-
persist();
|
|
73
|
-
return pushKey;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function updateBound(endpoint, boundSessions = []) {
|
|
77
|
-
const rec = subs.find((s) => s.subscription.endpoint === endpoint);
|
|
78
|
-
if (rec) { rec.boundSessions = boundSessions; persist(); }
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function removeSubscription(endpoint) {
|
|
82
|
-
const removed = subs.find((s) => s.subscription.endpoint === endpoint);
|
|
83
|
-
subs = subs.filter((s) => s.subscription.endpoint !== endpoint);
|
|
84
|
-
if (removed) persist();
|
|
85
|
-
return removed?.pushKey || null;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// The device-addressing id (NOT an auth credential — see /api/push/send-local). Lazy-generate for
|
|
89
|
-
// records stored before the feature existed so an already-subscribed device still has one.
|
|
90
|
-
export function getPushKey(endpoint) {
|
|
91
|
-
const rec = subs.find((s) => s.subscription.endpoint === endpoint);
|
|
92
|
-
if (!rec) return null;
|
|
93
|
-
if (!rec.pushKey) { rec.pushKey = genKey(); persist(); }
|
|
94
|
-
return rec.pushKey;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// The push-service Topic header (RFC 8030) must be ≤32 URL/filename-safe base64 chars [A-Za-z0-9_-].
|
|
98
|
-
// An invalid topic makes the service reject the ENTIRE send with a non-404/410 error that deliver()
|
|
99
|
-
// swallows — a silent zero-delivery, no log, no prune. tmux pane ids carry a '%' (e.g. %4), so a
|
|
100
|
-
// pane-derived topic ("pane-%4") breaks every per-pane push (需要你 / 已完成). Sanitize here, the layer
|
|
101
|
-
// that owns the web-push contract, so no caller has to know the rule.
|
|
102
|
-
function safeTopic(t) {
|
|
103
|
-
if (!t) return undefined;
|
|
104
|
-
const s = t.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 32);
|
|
105
|
-
return s || undefined;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// TTL bounds staleness (a phone offline longer than this drops the push instead of getting it
|
|
109
|
-
// hours later); topic collapses older undelivered messages with the same key so a device coming
|
|
110
|
-
// back online sees only the latest per topic. urgency hints the OS how aggressively to wake.
|
|
111
|
-
function options(opts = {}) {
|
|
112
|
-
return { TTL: opts.ttl ?? 90, urgency: opts.urgency || 'normal', topic: safeTopic(opts.topic) };
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export function classifyDeliveryFailure(error) {
|
|
116
|
-
const status = Number(error?.statusCode) || 0;
|
|
117
|
-
if (status === 404 || status === 410) return 'expired';
|
|
118
|
-
if (status === 429) return 'rate_limited';
|
|
119
|
-
if (status >= 500) return 'service_unavailable';
|
|
120
|
-
if (status >= 400) return 'rejected';
|
|
121
|
-
return 'network_error';
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function deliver(records, payload, opts = {}) {
|
|
125
|
-
ensureInit();
|
|
126
|
-
if (!configured) return {
|
|
127
|
-
sent: 0, failed: 0, gone: 0, configured: false,
|
|
128
|
-
deliveries: records.map((rec) => ({ pushKey: rec.pushKey, status: 'failed', reason: 'not_configured' })),
|
|
129
|
-
};
|
|
130
|
-
const data = JSON.stringify(payload);
|
|
131
|
-
const dead = [];
|
|
132
|
-
const deliveries = [];
|
|
133
|
-
let sent = 0;
|
|
134
|
-
let failed = 0;
|
|
135
|
-
await Promise.all(records.map(async (rec) => {
|
|
136
|
-
try {
|
|
137
|
-
await webpush.sendNotification(rec.subscription, data, options(opts));
|
|
138
|
-
sent += 1;
|
|
139
|
-
deliveries.push({ pushKey: rec.pushKey, status: 'success' });
|
|
140
|
-
}
|
|
141
|
-
catch (e) {
|
|
142
|
-
failed += 1;
|
|
143
|
-
if (e?.statusCode === 404 || e?.statusCode === 410) dead.push(rec.subscription.endpoint);
|
|
144
|
-
deliveries.push({ pushKey: rec.pushKey, status: 'failed', reason: classifyDeliveryFailure(e) });
|
|
145
|
-
let host = 'unknown push service';
|
|
146
|
-
try { host = new URL(rec.subscription.endpoint).host; } catch { /* malformed endpoints fail below */ }
|
|
147
|
-
const detail = String(e?.body || e?.message || 'unknown error').replace(/\s+/g, ' ').slice(0, 200);
|
|
148
|
-
console.warn(`[handmux] push delivery failed (${host}, HTTP ${e?.statusCode || 'unknown'}): ${detail}`);
|
|
149
|
-
}
|
|
150
|
-
}));
|
|
151
|
-
if (dead.length) { subs = subs.filter((s) => !dead.includes(s.subscription.endpoint)); persist(); }
|
|
152
|
-
return { sent, failed, gone: dead.length, configured: true, deliveries };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export const sendToAll = (payload, opts = {}) => deliver(subs, payload, opts);
|
|
156
|
-
export const sendToSession = (session, payload, opts = {}) =>
|
|
157
|
-
deliver(subs.filter((s) => s.boundSessions.includes(session)), payload, opts);
|
|
158
|
-
|
|
159
|
-
export const sendToDevices = (keys, payload, opts = {}) =>
|
|
160
|
-
deliver(subs.filter((s) => keys.includes(s.pushKey)), payload, opts);
|
|
161
|
-
|
|
162
|
-
// Union of the given sessions, deduped: a device bound to several of them is still delivered once
|
|
163
|
-
// (deliver() iterates the record list, so each record appears at most once here).
|
|
164
|
-
export const sendToSessions = (sessions, payload, opts = {}) =>
|
|
165
|
-
deliver(subs.filter((s) => s.boundSessions.some((x) => sessions.includes(x))), payload, opts);
|
|
166
|
-
|
|
167
|
-
// The pushKeys a given scope resolves to (mirrors sendToDevices/sendToSessions/sendToAll targeting) — used
|
|
168
|
-
// by the inbox to write a record into exactly the devices a manual push is delivered to.
|
|
169
|
-
export const resolveTargetKeys = ({ devices, sessions } = {}) => {
|
|
170
|
-
const pick = (devices && devices.length)
|
|
171
|
-
? subs.filter((s) => devices.includes(s.pushKey))
|
|
172
|
-
: (sessions && sessions.length)
|
|
173
|
-
? subs.filter((s) => s.boundSessions.some((x) => sessions.includes(x)))
|
|
174
|
-
: subs;
|
|
175
|
-
return [...new Set(pick.map((s) => s.pushKey).filter(Boolean))];
|
|
176
|
-
};
|
|
177
|
-
|
|
178
|
-
// Back-compat: the /push/subscribe welcome still pushes to a single just-added subscription.
|
|
179
|
-
export async function sendToOne(sub, payload, opts = {}) {
|
|
180
|
-
const rec = subs.find((s) => s.subscription.endpoint === sub.endpoint)
|
|
181
|
-
?? { subscription: sub, boundSessions: [] };
|
|
182
|
-
return deliver([rec], payload, opts);
|
|
183
|
-
}
|
package/src/routes/codex.js
DELETED
|
@@ -1,473 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import { isPaneId } from '../tmux/commands.js';
|
|
3
|
-
import { getAgent } from '../agents/index.js';
|
|
4
|
-
import { codexExitSessionId } from '../agents/codex.js';
|
|
5
|
-
|
|
6
|
-
const TAKEOVER_TERMINAL_HINT_MS = 10_000;
|
|
7
|
-
const TAKEOVER_TIMEOUT_MS = 30_000;
|
|
8
|
-
const INPUT_SETTLE_MS = 100;
|
|
9
|
-
const EXIT_POLL_MS = 500;
|
|
10
|
-
const EXIT_ATTEMPTS = 10;
|
|
11
|
-
const RECOVERY_OUTPUT_ATTEMPTS = 20;
|
|
12
|
-
const CLEAR_SWITCH_ATTEMPTS = 60;
|
|
13
|
-
const STREAM_BATCH_MS = 60;
|
|
14
|
-
const STREAM_HEARTBEAT_MS = 15_000;
|
|
15
|
-
const PERMISSION_MODES = {
|
|
16
|
-
default: {
|
|
17
|
-
approvalPolicy: 'on-request', approvalsReviewer: 'user',
|
|
18
|
-
sandboxPolicy: { type: 'workspaceWrite' },
|
|
19
|
-
},
|
|
20
|
-
'auto-review': {
|
|
21
|
-
approvalPolicy: 'on-request', approvalsReviewer: 'auto_review',
|
|
22
|
-
sandboxPolicy: { type: 'workspaceWrite' },
|
|
23
|
-
},
|
|
24
|
-
'full-access': {
|
|
25
|
-
approvalPolicy: 'never', approvalsReviewer: 'user',
|
|
26
|
-
sandboxPolicy: { type: 'dangerFullAccess' },
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
31
|
-
|
|
32
|
-
async function paneAgent(commands, claudeEvents, pane) {
|
|
33
|
-
const panes = await commands?.listLivePanes?.();
|
|
34
|
-
const live = Array.isArray(panes) ? panes.find((candidate) => candidate.id === pane) : null;
|
|
35
|
-
if (!live) return { live: null, agent: null };
|
|
36
|
-
const agents = await claudeEvents?.identifyPaneAgents?.(panes) || {};
|
|
37
|
-
return { live, agent: agents[pane] || null };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function exitCurrentCodex(commands, claudeEvents, pane) {
|
|
41
|
-
// During an active turn Codex can spend several seconds writing interruption details, token usage and
|
|
42
|
-
// its recovery command. Give each Ctrl+C a full five-second window; only send a second one if the pane
|
|
43
|
-
// still proves Codex is the foreground process after that wait.
|
|
44
|
-
for (let press = 0; press < 2; press += 1) {
|
|
45
|
-
await commands.sendKey(pane, 'C-c');
|
|
46
|
-
for (let attempt = 0; attempt < EXIT_ATTEMPTS; attempt += 1) {
|
|
47
|
-
await pause(EXIT_POLL_MS);
|
|
48
|
-
const current = await paneAgent(commands, claudeEvents, pane);
|
|
49
|
-
if (!current.live || current.agent !== 'codex') return current.live;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function waitForRecoverySession(commands, pane, isId) {
|
|
56
|
-
// tmux can report pane_current_command=the shell before the last buffered Codex output has been painted.
|
|
57
|
-
// Poll the visible pane briefly instead of treating the first incomplete frame as a permanent failure.
|
|
58
|
-
for (let attempt = 0; attempt < RECOVERY_OUTPUT_ATTEMPTS; attempt += 1) {
|
|
59
|
-
try {
|
|
60
|
-
const sessionId = codexExitSessionId(await commands.capturePlain(pane));
|
|
61
|
-
if (isId(sessionId)) return sessionId;
|
|
62
|
-
} catch { return null; } // a direct-command pane may have closed with Codex
|
|
63
|
-
await pause(INPUT_SETTLE_MS);
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
async function clearCurrentCodexThroughTui(codexApp, commands, pane, previousThreadId, wait) {
|
|
69
|
-
if (typeof commands?.exitCopyModeIfActive !== 'function'
|
|
70
|
-
|| typeof commands?.sendKey !== 'function'
|
|
71
|
-
|| typeof commands?.sendText !== 'function'
|
|
72
|
-
|| typeof commands?.sendEnter !== 'function') {
|
|
73
|
-
throw new Error('Codex terminal control is unavailable');
|
|
74
|
-
}
|
|
75
|
-
// /clear is not just thread/start: the remote TUI also replaces its active ChatWidget and redraws the
|
|
76
|
-
// terminal. Drive that native action so terminal and chat remain one session. C-u makes the command exact
|
|
77
|
-
// even if the hidden terminal composer retained a draft; if a modal owns input, confirmation below times
|
|
78
|
-
// out instead of letting Handmux split onto a different thread.
|
|
79
|
-
await commands.exitCopyModeIfActive(pane);
|
|
80
|
-
await commands.sendKey(pane, 'C-u');
|
|
81
|
-
await wait(INPUT_SETTLE_MS);
|
|
82
|
-
await commands.sendText(pane, '/clear');
|
|
83
|
-
await wait(INPUT_SETTLE_MS);
|
|
84
|
-
await commands.sendEnter(pane);
|
|
85
|
-
|
|
86
|
-
for (let attempt = 0; attempt < CLEAR_SWITCH_ATTEMPTS; attempt += 1) {
|
|
87
|
-
const discovered = await codexApp.discover(pane);
|
|
88
|
-
if (!discovered?.managed) throw new Error('Codex session is no longer managed by Handmux');
|
|
89
|
-
if (discovered.threadId && discovered.threadId !== previousThreadId) {
|
|
90
|
-
return { threadId: discovered.threadId };
|
|
91
|
-
}
|
|
92
|
-
await wait(INPUT_SETTLE_MS);
|
|
93
|
-
}
|
|
94
|
-
throw new Error('Codex terminal did not accept /clear; switch to the terminal, close any open panel, and try again');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async function binding(codexApp, pane) {
|
|
98
|
-
if (!isPaneId(pane)) return { error: 'bad pane id', status: 400 };
|
|
99
|
-
try {
|
|
100
|
-
const discovered = await codexApp?.discover?.(pane);
|
|
101
|
-
if (!discovered?.managed) return { error: 'Codex session is not managed by Handmux', status: 409 };
|
|
102
|
-
if (discovered.threadId) return { pane, threadId: discovered.threadId };
|
|
103
|
-
} catch (error) {
|
|
104
|
-
return { error: error?.message || String(error), status: 503 };
|
|
105
|
-
}
|
|
106
|
-
return { error: 'Codex session is not bound yet', status: 409 };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function routeError(res, result) {
|
|
110
|
-
if (!result.error) return false;
|
|
111
|
-
res.status(result.status).json({ error: result.error });
|
|
112
|
-
return true;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function codexError(res, error) {
|
|
116
|
-
const message = error?.message || String(error);
|
|
117
|
-
const conflict = /not managed|session changed|no longer pending|already being sent|being edited|edit is no longer active|queue is full|decision is unavailable|bad user input response/i.test(message);
|
|
118
|
-
return res.status(conflict ? 409 : 503).json({ error: message });
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// App Server can emit many tiny token deltas. Keep their order, but fold adjacent chunks for the same
|
|
122
|
-
// message before crossing the tunnel to a phone; lifecycle events always flush immediately.
|
|
123
|
-
export function appendCodexStreamEvent(queue, event) {
|
|
124
|
-
const last = queue.at(-1);
|
|
125
|
-
if (event?.type === 'delta' && last?.type === 'delta'
|
|
126
|
-
&& last.threadId === event.threadId && last.turnId === event.turnId && last.itemId === event.itemId) {
|
|
127
|
-
last.delta += event.delta || '';
|
|
128
|
-
} else if (event) queue.push({ ...event });
|
|
129
|
-
return queue;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function codexRoutes({ codexApp, commands, claudeEvents, wait = pause }) {
|
|
133
|
-
const r = express.Router();
|
|
134
|
-
if (!codexApp) return r;
|
|
135
|
-
// A takeover is pane-scoped and deliberately kept in server memory. Besides making repeated taps
|
|
136
|
-
// idempotent, this gates discovery while the replacement process starts: App Server may briefly list
|
|
137
|
-
// another historical thread, but the UI must not enter chat until the exact pre-takeover thread appears.
|
|
138
|
-
const takeovers = new Map(); // pane -> { threadId, startedAt }
|
|
139
|
-
const takeoverView = (takeover) => {
|
|
140
|
-
const elapsed = Date.now() - takeover.startedAt;
|
|
141
|
-
return {
|
|
142
|
-
state: elapsed >= TAKEOVER_TIMEOUT_MS ? 'timed-out' : 'starting',
|
|
143
|
-
needsTerminal: elapsed >= TAKEOVER_TERMINAL_HINT_MS,
|
|
144
|
-
};
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
r.get('/codex/session', async (req, res) => {
|
|
148
|
-
const pane = req.query.pane;
|
|
149
|
-
if (!isPaneId(pane)) return res.status(400).json({ error: 'bad pane id' });
|
|
150
|
-
const takeover = takeovers.get(pane);
|
|
151
|
-
try {
|
|
152
|
-
const discovered = await codexApp.discover(pane);
|
|
153
|
-
if (takeover && (!takeover.threadId || discovered?.threadId !== takeover.threadId)) {
|
|
154
|
-
return res.json({ managed: false, threadId: null, takeover: takeoverView(takeover) });
|
|
155
|
-
}
|
|
156
|
-
if (takeover) takeovers.delete(pane);
|
|
157
|
-
if (!discovered?.managed || !discovered.threadId) {
|
|
158
|
-
return res.json({ managed: !!discovered?.managed, threadId: discovered?.threadId || null });
|
|
159
|
-
}
|
|
160
|
-
return res.json(await codexApp.status(pane, discovered.threadId));
|
|
161
|
-
} catch (error) {
|
|
162
|
-
// A killed plain Codex and its replacement App Server have a normal short disconnect window. Keep
|
|
163
|
-
// the takeover page stable through it instead of flashing a generic connection failure.
|
|
164
|
-
if (takeover) return res.json({ managed: false, threadId: null, takeover: takeoverView(takeover) });
|
|
165
|
-
return codexError(res, error);
|
|
166
|
-
}
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
r.get('/codex/stream', async (req, res) => {
|
|
170
|
-
const target = await binding(codexApp, req.query.pane);
|
|
171
|
-
if (routeError(res, target)) return;
|
|
172
|
-
|
|
173
|
-
res.status(200);
|
|
174
|
-
res.set({
|
|
175
|
-
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
176
|
-
'Cache-Control': 'no-cache, no-transform',
|
|
177
|
-
Connection: 'keep-alive',
|
|
178
|
-
'X-Accel-Buffering': 'no',
|
|
179
|
-
});
|
|
180
|
-
res.flushHeaders?.();
|
|
181
|
-
res.write(`data: ${JSON.stringify({ type: 'ready', threadId: target.threadId })}\n\n`);
|
|
182
|
-
|
|
183
|
-
let closed = false;
|
|
184
|
-
let unsubscribe = null;
|
|
185
|
-
let flushTimer = null;
|
|
186
|
-
let heartbeat = null;
|
|
187
|
-
const pending = [];
|
|
188
|
-
const flush = () => {
|
|
189
|
-
if (flushTimer) clearTimeout(flushTimer);
|
|
190
|
-
flushTimer = null;
|
|
191
|
-
if (closed || !pending.length) return;
|
|
192
|
-
const events = pending.splice(0);
|
|
193
|
-
res.write(`data: ${JSON.stringify({ type: 'events', events })}\n\n`);
|
|
194
|
-
res.flush?.();
|
|
195
|
-
};
|
|
196
|
-
const cleanup = () => {
|
|
197
|
-
if (closed) return;
|
|
198
|
-
closed = true;
|
|
199
|
-
if (flushTimer) clearTimeout(flushTimer);
|
|
200
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
201
|
-
flushTimer = null;
|
|
202
|
-
heartbeat = null;
|
|
203
|
-
unsubscribe?.();
|
|
204
|
-
unsubscribe = null;
|
|
205
|
-
};
|
|
206
|
-
const enqueue = (event) => {
|
|
207
|
-
if (closed) return;
|
|
208
|
-
if (event?.type === 'disconnected') {
|
|
209
|
-
appendCodexStreamEvent(pending, event);
|
|
210
|
-
flush();
|
|
211
|
-
cleanup();
|
|
212
|
-
res.end();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
appendCodexStreamEvent(pending, event);
|
|
216
|
-
if (event?.type === 'completed' || event?.type === 'turnCompleted') flush();
|
|
217
|
-
else if (!flushTimer) {
|
|
218
|
-
flushTimer = setTimeout(flush, STREAM_BATCH_MS);
|
|
219
|
-
flushTimer.unref?.();
|
|
220
|
-
}
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
req.once('aborted', cleanup);
|
|
224
|
-
res.once('close', cleanup);
|
|
225
|
-
try {
|
|
226
|
-
unsubscribe = await codexApp.subscribe(target.pane, target.threadId, enqueue);
|
|
227
|
-
if (closed) { unsubscribe(); unsubscribe = null; return; }
|
|
228
|
-
heartbeat = setInterval(() => {
|
|
229
|
-
if (!closed) res.write(`: keepalive ${Date.now()}\n\n`);
|
|
230
|
-
}, STREAM_HEARTBEAT_MS);
|
|
231
|
-
heartbeat.unref?.();
|
|
232
|
-
} catch (error) {
|
|
233
|
-
if (!closed) {
|
|
234
|
-
res.write(`data: ${JSON.stringify({ type: 'error', message: error?.message || String(error) })}\n\n`);
|
|
235
|
-
res.end();
|
|
236
|
-
}
|
|
237
|
-
cleanup();
|
|
238
|
-
}
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
r.post('/codex/takeover', async (req, res) => {
|
|
242
|
-
const pane = req.body?.pane;
|
|
243
|
-
if (!isPaneId(pane)) return res.status(400).json({ error: 'bad pane id' });
|
|
244
|
-
const pending = takeovers.get(pane);
|
|
245
|
-
if (pending) return res.json({ started: true, takeover: takeoverView(pending) });
|
|
246
|
-
try {
|
|
247
|
-
const discovered = await codexApp.discover(pane);
|
|
248
|
-
if (discovered?.managed && discovered.threadId) {
|
|
249
|
-
return res.json({ started: false, managed: true, threadId: discovered.threadId });
|
|
250
|
-
}
|
|
251
|
-
if (discovered?.managed) return res.status(409).json({ error: 'codex-session-starting' });
|
|
252
|
-
|
|
253
|
-
// Re-check live process identity immediately before interacting with the pane. A stale client must
|
|
254
|
-
// never send slash commands to a pane whose Codex has already exited or changed.
|
|
255
|
-
const current = await paneAgent(commands, claudeEvents, pane);
|
|
256
|
-
if (!current.live) return res.status(404).json({ error: 'codex-pane-gone' });
|
|
257
|
-
if (current.agent !== 'codex') return res.status(409).json({ error: 'codex-pane-changed' });
|
|
258
|
-
|
|
259
|
-
const driver = getAgent('codex');
|
|
260
|
-
const takeover = { threadId: null, startedAt: Date.now() };
|
|
261
|
-
takeovers.set(pane, takeover);
|
|
262
|
-
try {
|
|
263
|
-
const exited = await exitCurrentCodex(commands, claudeEvents, pane);
|
|
264
|
-
if (!exited) {
|
|
265
|
-
takeovers.delete(pane);
|
|
266
|
-
return res.status(409).json({ error: 'codex-exit-blocked' });
|
|
267
|
-
}
|
|
268
|
-
const sessionId = await waitForRecoverySession(commands, pane, driver.sessions.isId);
|
|
269
|
-
if (!sessionId) {
|
|
270
|
-
takeovers.delete(pane);
|
|
271
|
-
return res.status(409).json({ error: 'codex-session-unconfirmed' });
|
|
272
|
-
}
|
|
273
|
-
takeover.threadId = sessionId;
|
|
274
|
-
await pause(INPUT_SETTLE_MS); // let the restored shell finish drawing its prompt
|
|
275
|
-
await commands.runPaneCommand(pane, driver.sessions.managedResumeCmd(sessionId));
|
|
276
|
-
} catch (error) {
|
|
277
|
-
takeovers.delete(pane);
|
|
278
|
-
throw error;
|
|
279
|
-
}
|
|
280
|
-
return res.json({ started: true, takeover: takeoverView(takeover) });
|
|
281
|
-
} catch (error) { return codexError(res, error); }
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
r.post('/codex/send', async (req, res) => {
|
|
285
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
286
|
-
if (routeError(res, target)) return;
|
|
287
|
-
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
|
|
288
|
-
if (!text) return res.status(400).json({ error: 'message is empty' });
|
|
289
|
-
const requestId = typeof req.body?.requestId === 'string' ? req.body.requestId.trim() : '';
|
|
290
|
-
if (requestId && (requestId.length > 128 || !/^[a-zA-Z0-9._:-]+$/.test(requestId))) {
|
|
291
|
-
return res.status(400).json({ error: 'bad Codex request id' });
|
|
292
|
-
}
|
|
293
|
-
try { res.json(await codexApp.send(target.pane, target.threadId, text, requestId || null)); }
|
|
294
|
-
catch (error) { codexError(res, error); }
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
for (const [path, method] of [['/codex/queue/steer', 'steerQueued'], ['/codex/queue/remove', 'removeQueued']]) {
|
|
298
|
-
r.post(path, async (req, res) => {
|
|
299
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
300
|
-
if (routeError(res, target)) return;
|
|
301
|
-
const id = typeof req.body?.id === 'string' ? req.body.id.trim() : '';
|
|
302
|
-
if (!id || id.length > 128) return res.status(400).json({ error: 'bad queued message id' });
|
|
303
|
-
try { res.json(await codexApp[method](target.pane, target.threadId, id)); }
|
|
304
|
-
catch (error) { codexError(res, error); }
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
r.post('/codex/queue/edit/begin', async (req, res) => {
|
|
309
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
310
|
-
if (routeError(res, target)) return;
|
|
311
|
-
const id = typeof req.body?.id === 'string' ? req.body.id.trim() : '';
|
|
312
|
-
if (!id || id.length > 128) return res.status(400).json({ error: 'bad queued message id' });
|
|
313
|
-
try { res.json(await codexApp.beginQueuedEdit(target.pane, target.threadId, id)); }
|
|
314
|
-
catch (error) { codexError(res, error); }
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
for (const [action, method] of [
|
|
318
|
-
['renew', 'renewQueuedEdit'], ['commit', 'commitQueuedEdit'], ['cancel', 'cancelQueuedEdit'],
|
|
319
|
-
]) {
|
|
320
|
-
r.post(`/codex/queue/edit/${action}`, async (req, res) => {
|
|
321
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
322
|
-
if (routeError(res, target)) return;
|
|
323
|
-
const id = typeof req.body?.id === 'string' ? req.body.id.trim() : '';
|
|
324
|
-
const token = typeof req.body?.token === 'string' ? req.body.token.trim() : '';
|
|
325
|
-
if (!id || id.length > 128) return res.status(400).json({ error: 'bad queued message id' });
|
|
326
|
-
if (!token || token.length > 128) return res.status(400).json({ error: 'bad queued message edit token' });
|
|
327
|
-
if (action === 'commit') {
|
|
328
|
-
const text = typeof req.body?.text === 'string' ? req.body.text.trim() : '';
|
|
329
|
-
if (!text) return res.status(400).json({ error: 'message is empty' });
|
|
330
|
-
try { return res.json(await codexApp[method](target.pane, target.threadId, id, token, text)); }
|
|
331
|
-
catch (error) { return codexError(res, error); }
|
|
332
|
-
}
|
|
333
|
-
try { return res.json(await codexApp[method](target.pane, target.threadId, id, token)); }
|
|
334
|
-
catch (error) { return codexError(res, error); }
|
|
335
|
-
});
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
r.post('/codex/compact', async (req, res) => {
|
|
339
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
340
|
-
if (routeError(res, target)) return;
|
|
341
|
-
try { res.json(await codexApp.compact(target.pane, target.threadId)); }
|
|
342
|
-
catch (error) { codexError(res, error); }
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
r.post('/codex/clear', async (req, res) => {
|
|
346
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
347
|
-
if (routeError(res, target)) return;
|
|
348
|
-
try {
|
|
349
|
-
res.json(await clearCurrentCodexThroughTui(
|
|
350
|
-
codexApp, commands, target.pane, target.threadId, wait,
|
|
351
|
-
));
|
|
352
|
-
}
|
|
353
|
-
catch (error) { codexError(res, error); }
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
r.get('/codex/models', async (req, res) => {
|
|
357
|
-
const target = await binding(codexApp, req.query.pane);
|
|
358
|
-
if (routeError(res, target)) return;
|
|
359
|
-
try { res.json({ models: await codexApp.models(target.pane, target.threadId) }); }
|
|
360
|
-
catch (error) { codexError(res, error); }
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
r.get('/codex/goal', async (req, res) => {
|
|
364
|
-
const target = await binding(codexApp, req.query.pane);
|
|
365
|
-
if (routeError(res, target)) return;
|
|
366
|
-
try { res.json({ goal: await codexApp.getGoal(target.pane, target.threadId) }); }
|
|
367
|
-
catch (error) { codexError(res, error); }
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
r.post('/codex/goal', async (req, res) => {
|
|
371
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
372
|
-
if (routeError(res, target)) return;
|
|
373
|
-
const updates = {};
|
|
374
|
-
if (Object.hasOwn(req.body || {}, 'objective')) {
|
|
375
|
-
const objective = req.body.objective;
|
|
376
|
-
const trimmed = typeof objective === 'string' ? objective.trim() : '';
|
|
377
|
-
if (!trimmed || trimmed.length > 4_000) {
|
|
378
|
-
return res.status(400).json({ error: 'bad goal objective' });
|
|
379
|
-
}
|
|
380
|
-
updates.objective = trimmed;
|
|
381
|
-
}
|
|
382
|
-
if (Object.hasOwn(req.body || {}, 'status')) {
|
|
383
|
-
const status = req.body.status;
|
|
384
|
-
if (!['active', 'paused'].includes(status)) return res.status(400).json({ error: 'bad goal status' });
|
|
385
|
-
updates.status = status;
|
|
386
|
-
}
|
|
387
|
-
if (!Object.keys(updates).length) return res.status(400).json({ error: 'no goal update supplied' });
|
|
388
|
-
try { res.json({ goal: await codexApp.updateGoal(target.pane, target.threadId, updates) }); }
|
|
389
|
-
catch (error) { codexError(res, error); }
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
r.post('/codex/goal/clear', async (req, res) => {
|
|
393
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
394
|
-
if (routeError(res, target)) return;
|
|
395
|
-
try { res.json(await codexApp.clearGoal(target.pane, target.threadId)); }
|
|
396
|
-
catch (error) { codexError(res, error); }
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
r.post('/codex/settings', async (req, res) => {
|
|
400
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
401
|
-
if (routeError(res, target)) return;
|
|
402
|
-
const updates = {};
|
|
403
|
-
for (const key of ['model', 'effort']) {
|
|
404
|
-
const value = req.body?.[key];
|
|
405
|
-
if (value == null) continue;
|
|
406
|
-
if (typeof value !== 'string' || !value.trim() || value.length > 128) {
|
|
407
|
-
return res.status(400).json({ error: `bad ${key}` });
|
|
408
|
-
}
|
|
409
|
-
updates[key] = value.trim();
|
|
410
|
-
}
|
|
411
|
-
if (Object.hasOwn(req.body || {}, 'serviceTier')) {
|
|
412
|
-
const value = req.body.serviceTier;
|
|
413
|
-
if (value !== null && (typeof value !== 'string' || !value.trim() || value.length > 128)) {
|
|
414
|
-
return res.status(400).json({ error: 'bad serviceTier' });
|
|
415
|
-
}
|
|
416
|
-
updates.serviceTier = value === null ? null : value.trim();
|
|
417
|
-
}
|
|
418
|
-
const approvalPolicy = req.body?.approvalPolicy;
|
|
419
|
-
if (approvalPolicy != null) {
|
|
420
|
-
if (!['untrusted', 'on-request', 'never'].includes(approvalPolicy)) {
|
|
421
|
-
return res.status(400).json({ error: 'bad approvalPolicy' });
|
|
422
|
-
}
|
|
423
|
-
updates.approvalPolicy = approvalPolicy;
|
|
424
|
-
}
|
|
425
|
-
const permissionMode = req.body?.permissionMode;
|
|
426
|
-
if (permissionMode != null) {
|
|
427
|
-
if (typeof permissionMode !== 'string' || !PERMISSION_MODES[permissionMode]) {
|
|
428
|
-
return res.status(400).json({ error: 'bad permissionMode' });
|
|
429
|
-
}
|
|
430
|
-
Object.assign(updates, PERMISSION_MODES[permissionMode]);
|
|
431
|
-
}
|
|
432
|
-
if (!Object.keys(updates).length) return res.status(400).json({ error: 'no settings supplied' });
|
|
433
|
-
try { res.json({ settings: await codexApp.updateSettings(target.pane, target.threadId, updates) }); }
|
|
434
|
-
catch (error) { codexError(res, error); }
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
r.post('/codex/interrupt', async (req, res) => {
|
|
438
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
439
|
-
if (routeError(res, target)) return;
|
|
440
|
-
try { res.json(await codexApp.interrupt(target.pane, target.threadId)); }
|
|
441
|
-
catch (error) { codexError(res, error); }
|
|
442
|
-
});
|
|
443
|
-
|
|
444
|
-
r.post('/codex/approval', async (req, res) => {
|
|
445
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
446
|
-
if (routeError(res, target)) return;
|
|
447
|
-
const requestId = req.body?.requestId;
|
|
448
|
-
const decision = req.body?.decision;
|
|
449
|
-
if ((typeof requestId !== 'string' && typeof requestId !== 'number') || typeof decision !== 'string') {
|
|
450
|
-
return res.status(400).json({ error: 'bad approval response' });
|
|
451
|
-
}
|
|
452
|
-
try { res.json(await codexApp.decide(target.pane, target.threadId, requestId, decision)); }
|
|
453
|
-
catch (error) { codexError(res, error); }
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
r.post('/codex/input', async (req, res) => {
|
|
457
|
-
const target = await binding(codexApp, req.body?.pane);
|
|
458
|
-
if (routeError(res, target)) return;
|
|
459
|
-
const requestId = req.body?.requestId;
|
|
460
|
-
const answers = req.body?.answers;
|
|
461
|
-
const entries = answers && typeof answers === 'object' && !Array.isArray(answers)
|
|
462
|
-
? Object.entries(answers) : [];
|
|
463
|
-
if ((typeof requestId !== 'string' && typeof requestId !== 'number') || !entries.length
|
|
464
|
-
|| entries.some(([questionId, value]) => !questionId || !Array.isArray(value) || !value.length
|
|
465
|
-
|| value.some((answer) => typeof answer !== 'string' || !answer.trim()))) {
|
|
466
|
-
return res.status(400).json({ error: 'bad user input response' });
|
|
467
|
-
}
|
|
468
|
-
try { res.json(await codexApp.answerInput(target.pane, target.threadId, requestId, answers)); }
|
|
469
|
-
catch (error) { codexError(res, error); }
|
|
470
|
-
});
|
|
471
|
-
|
|
472
|
-
return r;
|
|
473
|
-
}
|