dextunnel 0.1.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/LICENSE +211 -0
- package/README.md +112 -0
- package/SECURITY.md +27 -0
- package/SUPPORT.md +43 -0
- package/package.json +44 -0
- package/public/client-shared.js +1831 -0
- package/public/favicon.svg +11 -0
- package/public/host.html +29 -0
- package/public/host.js +2079 -0
- package/public/index.html +28 -0
- package/public/index.js +98 -0
- package/public/live-bridge-lifecycle.js +258 -0
- package/public/live-bridge-retry-state.js +61 -0
- package/public/live-selection-intent.js +79 -0
- package/public/remote-operator-state.js +316 -0
- package/public/remote.html +167 -0
- package/public/remote.js +3967 -0
- package/public/styles.css +2793 -0
- package/public/surface-view-state.js +89 -0
- package/public/voice-dictation.js +45 -0
- package/src/bin/desktop-rehydration-smoke.mjs +111 -0
- package/src/bin/dextunnel.mjs +41 -0
- package/src/bin/doctor.mjs +48 -0
- package/src/bin/launch-attest.mjs +39 -0
- package/src/bin/launch-status.mjs +49 -0
- package/src/bin/mobile-link-proxy.mjs +221 -0
- package/src/bin/mobile-proof.mjs +164 -0
- package/src/bin/mobile-transport-smoke.mjs +200 -0
- package/src/bin/probe-codex-app-server-write.mjs +36 -0
- package/src/bin/probe-codex-app-server.mjs +30 -0
- package/src/lib/agent-room-context.mjs +54 -0
- package/src/lib/agent-room-runtime.mjs +355 -0
- package/src/lib/agent-room-service.mjs +335 -0
- package/src/lib/agent-room-state.mjs +406 -0
- package/src/lib/agent-room-store.mjs +71 -0
- package/src/lib/agent-room-text.mjs +48 -0
- package/src/lib/app-server-contract.mjs +66 -0
- package/src/lib/app-server-runtime.mjs +60 -0
- package/src/lib/attachment-service.mjs +119 -0
- package/src/lib/bridge-api-handler.mjs +719 -0
- package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
- package/src/lib/bridge-status-builder.mjs +60 -0
- package/src/lib/codex-app-server-client.mjs +1511 -0
- package/src/lib/companion-state.mjs +453 -0
- package/src/lib/control-lease-service.mjs +180 -0
- package/src/lib/debug-harness-service.mjs +173 -0
- package/src/lib/desktop-integration.mjs +146 -0
- package/src/lib/desktop-rehydration-smoke.mjs +269 -0
- package/src/lib/dextunnel-cli.mjs +122 -0
- package/src/lib/discovery-docs.mjs +1321 -0
- package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
- package/src/lib/install-preflight.mjs +373 -0
- package/src/lib/interaction-resolution-service.mjs +185 -0
- package/src/lib/interaction-state.mjs +360 -0
- package/src/lib/launch-release-bar.mjs +158 -0
- package/src/lib/live-control-state.mjs +107 -0
- package/src/lib/live-payload-builder.mjs +298 -0
- package/src/lib/live-selection-transition-state.mjs +49 -0
- package/src/lib/live-transcript-state.mjs +549 -0
- package/src/lib/mobile-network-profile.mjs +39 -0
- package/src/lib/mock-codex-adapter.mjs +62 -0
- package/src/lib/operator-diagnostics.mjs +82 -0
- package/src/lib/repo-changes-service.mjs +527 -0
- package/src/lib/runtime-config.mjs +106 -0
- package/src/lib/selection-state-service.mjs +214 -0
- package/src/lib/session-store.mjs +355 -0
- package/src/lib/shared-room-state.mjs +473 -0
- package/src/lib/shared-selection-state.mjs +40 -0
- package/src/lib/sse-hub.mjs +35 -0
- package/src/lib/static-surface-service.mjs +71 -0
- package/src/lib/surface-access.mjs +189 -0
- package/src/lib/surface-presence-service.mjs +118 -0
- package/src/lib/surface-request-guard.mjs +52 -0
- package/src/lib/thread-sync-state.mjs +536 -0
- package/src/lib/watcher-lifecycle.mjs +287 -0
- package/src/server.mjs +1446 -0
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
export async function handleBridgeApiRequest({ req, res, url, deps }) {
|
|
2
|
+
const {
|
|
3
|
+
CONTROL_LEASE_TTL_MS,
|
|
4
|
+
accessClientId,
|
|
5
|
+
appServerState,
|
|
6
|
+
applyCompanionWakeupAction,
|
|
7
|
+
applyLiveControlAction,
|
|
8
|
+
applySurfacePresenceUpdate,
|
|
9
|
+
broadcast,
|
|
10
|
+
buildBridgeStatus,
|
|
11
|
+
buildInstallPreflight,
|
|
12
|
+
buildLivePayload,
|
|
13
|
+
buildLiveTurnChanges,
|
|
14
|
+
buildSelectedThreadSnapshot,
|
|
15
|
+
canServeSurfaceBootstrap,
|
|
16
|
+
clearDebugPendingInteraction,
|
|
17
|
+
codexAppServer,
|
|
18
|
+
createThreadSelection,
|
|
19
|
+
decorateSnapshot,
|
|
20
|
+
devToolsEnabled,
|
|
21
|
+
exposeHostSurface,
|
|
22
|
+
ensureRemoteControlLease,
|
|
23
|
+
errorStatusCode,
|
|
24
|
+
getCachedRepoChanges,
|
|
25
|
+
getControlLeaseForThread,
|
|
26
|
+
hasWatcherController = () => false,
|
|
27
|
+
interruptSelectedThread,
|
|
28
|
+
issueSurfaceBootstrap,
|
|
29
|
+
liveState,
|
|
30
|
+
mergeSelectedThreadSnapshot = (_previousSnapshot, nextSnapshot) => nextSnapshot,
|
|
31
|
+
cwd,
|
|
32
|
+
mapThreadToCompanionSnapshot,
|
|
33
|
+
mockAdapter,
|
|
34
|
+
nowIso,
|
|
35
|
+
openThreadInCodex,
|
|
36
|
+
persistImageAttachments,
|
|
37
|
+
readJsonBody,
|
|
38
|
+
recordControlEvent,
|
|
39
|
+
refreshLiveState,
|
|
40
|
+
refreshThreads,
|
|
41
|
+
rememberTurnOrigin,
|
|
42
|
+
requireSurfaceCapability,
|
|
43
|
+
resolvePendingInteraction,
|
|
44
|
+
resolveSurfaceAccess,
|
|
45
|
+
restartWatcher,
|
|
46
|
+
scheduleSnapshotRefresh = () => {},
|
|
47
|
+
scheduleControlLeaseExpiry,
|
|
48
|
+
sendJson,
|
|
49
|
+
setDebugCompanionWakeup,
|
|
50
|
+
setDebugPendingInteraction,
|
|
51
|
+
setSelection,
|
|
52
|
+
store,
|
|
53
|
+
streamState,
|
|
54
|
+
summonCompanionWakeup,
|
|
55
|
+
updateAgentRoom,
|
|
56
|
+
invalidateRepoChangesCache,
|
|
57
|
+
loadTranscriptHistoryPage
|
|
58
|
+
} = deps;
|
|
59
|
+
|
|
60
|
+
function summarizeResponseThread(thread) {
|
|
61
|
+
if (!thread) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
activeTurnId: thread.activeTurnId || null,
|
|
67
|
+
activeTurnStatus: thread.activeTurnStatus || null,
|
|
68
|
+
cwd: thread.cwd || null,
|
|
69
|
+
id: thread.id || null,
|
|
70
|
+
lastTurnId: thread.lastTurnId || null,
|
|
71
|
+
lastTurnStatus: thread.lastTurnStatus || null,
|
|
72
|
+
name: thread.name || null,
|
|
73
|
+
path: thread.path || null,
|
|
74
|
+
preview: thread.preview || null,
|
|
75
|
+
source: thread.source || null,
|
|
76
|
+
status: thread.status || null,
|
|
77
|
+
tokenUsage: thread.tokenUsage || null,
|
|
78
|
+
updatedAt: thread.updatedAt || null
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function summarizeResponseTurn(turn) {
|
|
83
|
+
if (!turn) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
id: turn.id || null,
|
|
89
|
+
startedAt: turn.startedAt || null,
|
|
90
|
+
status: turn.status || null,
|
|
91
|
+
updatedAt: turn.updatedAt || null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/bootstrap") {
|
|
97
|
+
const surface = String(url.searchParams.get("surface") || "remote").trim().toLowerCase();
|
|
98
|
+
const pathname = surface === "host" ? "/host.html" : "/remote.html";
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
if (
|
|
102
|
+
!canServeSurfaceBootstrap({
|
|
103
|
+
exposeHostSurface,
|
|
104
|
+
localAddress: req.socket?.localAddress || "",
|
|
105
|
+
pathname,
|
|
106
|
+
remoteAddress: req.socket?.remoteAddress || ""
|
|
107
|
+
})
|
|
108
|
+
) {
|
|
109
|
+
sendJson(res, 403, {
|
|
110
|
+
error: "Host surface bootstrap is restricted to loopback unless DEXTUNNEL_EXPOSE_HOST_SURFACE is enabled."
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
sendJson(res, 200, issueSurfaceBootstrap(surface));
|
|
116
|
+
} catch (error) {
|
|
117
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message });
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (req.method === "GET" && url.pathname === "/api/state") {
|
|
123
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
124
|
+
sendJson(res, 200, store.getState());
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (req.method === "GET" && url.pathname === "/api/preflight") {
|
|
129
|
+
try {
|
|
130
|
+
const payload = await buildInstallPreflight({
|
|
131
|
+
codexAppServer,
|
|
132
|
+
cwd,
|
|
133
|
+
runtimeConfig: deps.runtimeConfig,
|
|
134
|
+
warmup: url.searchParams.get("warmup") !== "0"
|
|
135
|
+
});
|
|
136
|
+
sendJson(res, 200, payload);
|
|
137
|
+
} catch (error) {
|
|
138
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message });
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/live-state") {
|
|
144
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
145
|
+
sendJson(res, 200, buildLivePayload());
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/transcript-history") {
|
|
150
|
+
try {
|
|
151
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
152
|
+
const threadId = url.searchParams.get("threadId") || liveState.selectedThreadId || null;
|
|
153
|
+
if (!threadId) {
|
|
154
|
+
sendJson(res, 400, { error: "threadId is required" });
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const page = await loadTranscriptHistoryPage({
|
|
159
|
+
beforeIndex: url.searchParams.get("beforeIndex"),
|
|
160
|
+
limit: url.searchParams.get("limit"),
|
|
161
|
+
threadId,
|
|
162
|
+
visibleCount: url.searchParams.get("visibleCount")
|
|
163
|
+
});
|
|
164
|
+
sendJson(res, 200, page);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/status") {
|
|
172
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
173
|
+
sendJson(res, 200, buildBridgeStatus());
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/changes") {
|
|
178
|
+
try {
|
|
179
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
180
|
+
const targetCwd = url.searchParams.get("cwd") || liveState.selectedProjectCwd || cwd;
|
|
181
|
+
const threadId = url.searchParams.get("threadId") || liveState.selectedThreadId || null;
|
|
182
|
+
const selectedThread = liveState.selectedThreadSnapshot?.thread || null;
|
|
183
|
+
const liveTurnPayload =
|
|
184
|
+
threadId &&
|
|
185
|
+
liveState.turnDiff?.threadId === threadId &&
|
|
186
|
+
liveState.turnDiff?.diff
|
|
187
|
+
? buildLiveTurnChanges({
|
|
188
|
+
cwd: targetCwd,
|
|
189
|
+
diff: liveState.turnDiff.diff,
|
|
190
|
+
threadId,
|
|
191
|
+
turnId: liveState.turnDiff.turnId || null
|
|
192
|
+
})
|
|
193
|
+
: null;
|
|
194
|
+
const threadPath =
|
|
195
|
+
threadId && selectedThread?.id === threadId
|
|
196
|
+
? selectedThread.path || null
|
|
197
|
+
: null;
|
|
198
|
+
const payload =
|
|
199
|
+
liveTurnPayload && liveTurnPayload.items.length
|
|
200
|
+
? liveTurnPayload
|
|
201
|
+
: await getCachedRepoChanges(targetCwd, { threadPath });
|
|
202
|
+
sendJson(res, 200, payload);
|
|
203
|
+
} catch (error) {
|
|
204
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message });
|
|
205
|
+
}
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/refresh") {
|
|
210
|
+
try {
|
|
211
|
+
requireSurfaceCapability(req, url, "refresh_room");
|
|
212
|
+
const includeThreads = url.searchParams.get("threads") !== "0";
|
|
213
|
+
const payload = await refreshLiveState({ includeThreads });
|
|
214
|
+
sendJson(res, 200, { ok: true, state: payload });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, status: buildBridgeStatus() });
|
|
217
|
+
}
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/reconnect") {
|
|
222
|
+
try {
|
|
223
|
+
requireSurfaceCapability(req, url, "refresh_room");
|
|
224
|
+
const includeThreads = url.searchParams.get("threads") !== "0";
|
|
225
|
+
await restartWatcher();
|
|
226
|
+
const payload = await refreshLiveState({ includeThreads });
|
|
227
|
+
sendJson(res, 200, { ok: true, state: payload });
|
|
228
|
+
} catch (error) {
|
|
229
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, status: buildBridgeStatus() });
|
|
230
|
+
}
|
|
231
|
+
return true;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/open-in-codex") {
|
|
235
|
+
try {
|
|
236
|
+
requireSurfaceCapability(req, url, "open_in_codex");
|
|
237
|
+
const body = await readJsonBody(req);
|
|
238
|
+
const threadId = body.threadId || liveState.selectedThreadId || null;
|
|
239
|
+
if (!threadId) {
|
|
240
|
+
sendJson(res, 400, { error: "threadId is required" });
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const payload = await openThreadInCodex(threadId);
|
|
245
|
+
sendJson(res, 200, {
|
|
246
|
+
ok: true,
|
|
247
|
+
...payload,
|
|
248
|
+
message:
|
|
249
|
+
"Revealed this thread in Codex. Quit and reopen the Codex app manually to see new messages generated here."
|
|
250
|
+
});
|
|
251
|
+
} catch (error) {
|
|
252
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, status: buildBridgeStatus() });
|
|
253
|
+
}
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/selection") {
|
|
258
|
+
try {
|
|
259
|
+
const access = requireSurfaceCapability(req, url, "select_room");
|
|
260
|
+
const body = await readJsonBody(req);
|
|
261
|
+
const payload = await setSelection({
|
|
262
|
+
clientId: accessClientId(access),
|
|
263
|
+
cwd: body.cwd || null,
|
|
264
|
+
source: access.surface,
|
|
265
|
+
threadId: body.threadId || null
|
|
266
|
+
});
|
|
267
|
+
sendJson(res, 200, payload);
|
|
268
|
+
} catch (error) {
|
|
269
|
+
sendJson(res, errorStatusCode(error, 409), { error: error.message, state: buildLivePayload() });
|
|
270
|
+
}
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/presence") {
|
|
275
|
+
try {
|
|
276
|
+
const access = requireSurfaceCapability(req, url, "sync_presence");
|
|
277
|
+
const body = await readJsonBody(req);
|
|
278
|
+
const changed = applySurfacePresenceUpdate(
|
|
279
|
+
{
|
|
280
|
+
...body,
|
|
281
|
+
clientId: accessClientId(access),
|
|
282
|
+
surface: access.surface
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
now: Date.now(),
|
|
286
|
+
selectedThreadId: liveState.selectedThreadId || ""
|
|
287
|
+
}
|
|
288
|
+
);
|
|
289
|
+
|
|
290
|
+
if (changed) {
|
|
291
|
+
broadcast("live", buildLivePayload());
|
|
292
|
+
}
|
|
293
|
+
sendJson(res, 200, { ok: true });
|
|
294
|
+
} catch (error) {
|
|
295
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/control") {
|
|
301
|
+
try {
|
|
302
|
+
const body = await readJsonBody(req);
|
|
303
|
+
const threadId = body.threadId || liveState.selectedThreadId || null;
|
|
304
|
+
const action = body.action || "claim";
|
|
305
|
+
const access = resolveSurfaceAccess(req, url);
|
|
306
|
+
if (!access) {
|
|
307
|
+
throw Object.assign(new Error("Dextunnel surface access is missing or expired."), {
|
|
308
|
+
statusCode: 403
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
const clientId = accessClientId(access);
|
|
312
|
+
const existingLease = getControlLeaseForThread(threadId);
|
|
313
|
+
const requiredCapability =
|
|
314
|
+
action === "release" && access.surface === "host"
|
|
315
|
+
? "release_remote_control"
|
|
316
|
+
: "control_remote";
|
|
317
|
+
if (!access.capabilities.includes(requiredCapability)) {
|
|
318
|
+
throw Object.assign(
|
|
319
|
+
new Error(`${access.surface} surface is not allowed to ${requiredCapability.replaceAll("_", " ")}.`),
|
|
320
|
+
{ statusCode: 403 }
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const result = applyLiveControlAction({
|
|
325
|
+
action,
|
|
326
|
+
clientId,
|
|
327
|
+
existingLease,
|
|
328
|
+
now: Date.now(),
|
|
329
|
+
owner: access.surface,
|
|
330
|
+
reason: action === "claim" ? body.reason || "compose" : body.reason || null,
|
|
331
|
+
source: access.surface,
|
|
332
|
+
threadId,
|
|
333
|
+
ttlMs: CONTROL_LEASE_TTL_MS
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
liveState.controlLease = result.lease;
|
|
337
|
+
scheduleControlLeaseExpiry();
|
|
338
|
+
if (result.recordEvent && result.event) {
|
|
339
|
+
recordControlEvent(result.event);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
liveState.lastError = null;
|
|
343
|
+
broadcast("live", buildLivePayload());
|
|
344
|
+
sendJson(res, 200, {
|
|
345
|
+
ok: true,
|
|
346
|
+
action,
|
|
347
|
+
state: buildLivePayload()
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/interaction") {
|
|
356
|
+
try {
|
|
357
|
+
const body = await readJsonBody(req);
|
|
358
|
+
const access = requireSurfaceCapability(req, url, "respond_interaction");
|
|
359
|
+
await resolvePendingInteraction({
|
|
360
|
+
...body,
|
|
361
|
+
authorityClientId: accessClientId(access),
|
|
362
|
+
source: access.surface
|
|
363
|
+
});
|
|
364
|
+
sendJson(res, 200, { ok: true, state: buildLivePayload() });
|
|
365
|
+
} catch (error) {
|
|
366
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
367
|
+
}
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/companion") {
|
|
372
|
+
try {
|
|
373
|
+
requireSurfaceCapability(req, url, "use_companion");
|
|
374
|
+
const body = await readJsonBody(req);
|
|
375
|
+
const threadId = body.threadId || liveState.selectedThreadId || null;
|
|
376
|
+
const result =
|
|
377
|
+
body.action === "summon"
|
|
378
|
+
? summonCompanionWakeup({
|
|
379
|
+
advisorId: body.advisorId || "",
|
|
380
|
+
threadId
|
|
381
|
+
})
|
|
382
|
+
: applyCompanionWakeupAction({
|
|
383
|
+
action: body.action,
|
|
384
|
+
threadId,
|
|
385
|
+
wakeKey: body.wakeKey
|
|
386
|
+
});
|
|
387
|
+
broadcast("live", buildLivePayload());
|
|
388
|
+
sendJson(res, 200, { ok: true, message: result.message, state: buildLivePayload() });
|
|
389
|
+
} catch (error) {
|
|
390
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/agent-room") {
|
|
396
|
+
try {
|
|
397
|
+
requireSurfaceCapability(req, url, "use_agent_room");
|
|
398
|
+
const body = await readJsonBody(req);
|
|
399
|
+
const threadId = body.threadId || liveState.selectedThreadId || null;
|
|
400
|
+
const result = await updateAgentRoom({
|
|
401
|
+
action: body.action || "",
|
|
402
|
+
memberIds: Array.isArray(body.memberIds) ? body.memberIds : null,
|
|
403
|
+
text: body.text || "",
|
|
404
|
+
threadId
|
|
405
|
+
});
|
|
406
|
+
broadcast("live", buildLivePayload());
|
|
407
|
+
sendJson(res, 200, { ok: true, message: result.message, state: buildLivePayload() });
|
|
408
|
+
} catch (error) {
|
|
409
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
410
|
+
}
|
|
411
|
+
return true;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (req.method === "POST" && url.pathname === "/api/debug/live-interaction") {
|
|
415
|
+
if (!devToolsEnabled) {
|
|
416
|
+
sendJson(res, 404, { error: "Not found" });
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
requireSurfaceCapability(req, url, "debug_tools");
|
|
421
|
+
const body = await readJsonBody(req);
|
|
422
|
+
const payload =
|
|
423
|
+
body.action === "clear"
|
|
424
|
+
? clearDebugPendingInteraction()
|
|
425
|
+
: setDebugPendingInteraction(body.kind || "");
|
|
426
|
+
sendJson(res, 200, { ok: true, state: payload });
|
|
427
|
+
} catch (error) {
|
|
428
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
429
|
+
}
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (req.method === "POST" && url.pathname === "/api/debug/companion-wakeup") {
|
|
434
|
+
if (!devToolsEnabled) {
|
|
435
|
+
sendJson(res, 404, { error: "Not found" });
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
requireSurfaceCapability(req, url, "debug_tools");
|
|
440
|
+
const body = await readJsonBody(req);
|
|
441
|
+
const payload = setDebugCompanionWakeup({
|
|
442
|
+
advisorId: body.advisorId || "",
|
|
443
|
+
threadId: body.threadId || liveState.selectedThreadId || null,
|
|
444
|
+
wakeKind: body.wakeKind || "summary"
|
|
445
|
+
});
|
|
446
|
+
broadcast("live", payload);
|
|
447
|
+
sendJson(res, 200, { ok: true, state: payload });
|
|
448
|
+
} catch (error) {
|
|
449
|
+
sendJson(res, errorStatusCode(error, 400), { error: error.message, state: buildLivePayload() });
|
|
450
|
+
}
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/interrupt") {
|
|
455
|
+
try {
|
|
456
|
+
requireSurfaceCapability(req, url, "control_remote");
|
|
457
|
+
const payload = await interruptSelectedThread();
|
|
458
|
+
sendJson(res, 200, payload);
|
|
459
|
+
} catch (error) {
|
|
460
|
+
sendJson(res, 400, { error: error.message, state: buildLivePayload() });
|
|
461
|
+
}
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/threads") {
|
|
466
|
+
try {
|
|
467
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
468
|
+
await refreshThreads({ broadcastUpdate: false });
|
|
469
|
+
sendJson(res, 200, {
|
|
470
|
+
cwd: null,
|
|
471
|
+
data: liveState.threads
|
|
472
|
+
});
|
|
473
|
+
} catch (error) {
|
|
474
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, status: buildBridgeStatus() });
|
|
475
|
+
}
|
|
476
|
+
return true;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/thread") {
|
|
480
|
+
try {
|
|
481
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
482
|
+
const threadId = url.searchParams.get("threadId");
|
|
483
|
+
const limit = Number(url.searchParams.get("limit") || "40");
|
|
484
|
+
|
|
485
|
+
if (!threadId) {
|
|
486
|
+
sendJson(res, 400, { error: "threadId is required" });
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const normalizedLimit = Number.isFinite(limit) && limit > 0 ? limit : null;
|
|
491
|
+
const thread = await codexAppServer.readThread(threadId, false);
|
|
492
|
+
const snapshot = thread
|
|
493
|
+
? decorateSnapshot(
|
|
494
|
+
await buildSelectedThreadSnapshot(thread, {
|
|
495
|
+
limit: normalizedLimit
|
|
496
|
+
})
|
|
497
|
+
)
|
|
498
|
+
: null;
|
|
499
|
+
|
|
500
|
+
sendJson(res, 200, {
|
|
501
|
+
threadId,
|
|
502
|
+
found: Boolean(thread),
|
|
503
|
+
snapshot
|
|
504
|
+
});
|
|
505
|
+
} catch (error) {
|
|
506
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, status: buildBridgeStatus() });
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/thread") {
|
|
512
|
+
try {
|
|
513
|
+
const access = requireSurfaceCapability(req, url, "select_room");
|
|
514
|
+
const body = await readJsonBody(req);
|
|
515
|
+
const payload = await createThreadSelection({
|
|
516
|
+
clientId: accessClientId(access),
|
|
517
|
+
cwd: body.cwd || null,
|
|
518
|
+
source: access.surface
|
|
519
|
+
});
|
|
520
|
+
sendJson(res, 200, payload);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
sendJson(res, errorStatusCode(error, 409), { error: error.message, state: buildLivePayload() });
|
|
523
|
+
}
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
if (req.method === "GET" && url.pathname === "/api/codex-app-server/latest-thread") {
|
|
528
|
+
try {
|
|
529
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
530
|
+
const targetCwd = url.searchParams.get("cwd") || cwd;
|
|
531
|
+
const limit = Number(url.searchParams.get("limit") || "40");
|
|
532
|
+
const thread = await codexAppServer.getLatestThreadForCwd(targetCwd);
|
|
533
|
+
sendJson(res, 200, {
|
|
534
|
+
cwd: targetCwd,
|
|
535
|
+
found: Boolean(thread),
|
|
536
|
+
snapshot: thread
|
|
537
|
+
? decorateSnapshot(
|
|
538
|
+
mapThreadToCompanionSnapshot(thread, {
|
|
539
|
+
limit: Number.isFinite(limit) && limit > 0 ? limit : null
|
|
540
|
+
})
|
|
541
|
+
)
|
|
542
|
+
: null
|
|
543
|
+
});
|
|
544
|
+
} catch (error) {
|
|
545
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, status: buildBridgeStatus() });
|
|
546
|
+
}
|
|
547
|
+
return true;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (req.method === "POST" && url.pathname === "/api/codex-app-server/turn") {
|
|
551
|
+
let body = {};
|
|
552
|
+
let targetThreadId = null;
|
|
553
|
+
let selectedThreadIdBeforeSend = liveState.selectedThreadId || null;
|
|
554
|
+
try {
|
|
555
|
+
body = await readJsonBody(req);
|
|
556
|
+
const access = requireSurfaceCapability(req, url, "send_turn");
|
|
557
|
+
targetThreadId = body.threadId || liveState.selectedThreadId || null;
|
|
558
|
+
const targetCwd = body.cwd || liveState.selectedProjectCwd || cwd;
|
|
559
|
+
const attachments = await persistImageAttachments(body.attachments);
|
|
560
|
+
|
|
561
|
+
if (!targetThreadId && body.createThreadIfMissing === false) {
|
|
562
|
+
throw new Error("No selected live Codex thread is available.");
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (
|
|
566
|
+
liveState.writeLock &&
|
|
567
|
+
liveState.writeLock.threadId === targetThreadId &&
|
|
568
|
+
liveState.writeLock.status === "pending"
|
|
569
|
+
) {
|
|
570
|
+
throw new Error("A live send is already in progress for this session.");
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (liveState.pendingInteraction) {
|
|
574
|
+
throw new Error("Resolve the pending interaction before sending another message.");
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (targetThreadId && access.capabilities.includes("control_remote")) {
|
|
578
|
+
ensureRemoteControlLease(targetThreadId, access.surface, accessClientId(access));
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
liveState.writeLock = {
|
|
582
|
+
at: nowIso(),
|
|
583
|
+
preview:
|
|
584
|
+
String(body.text || "").slice(0, 140) ||
|
|
585
|
+
(attachments.length
|
|
586
|
+
? `[${attachments.length} image attachment${attachments.length === 1 ? "" : "s"}]`
|
|
587
|
+
: ""),
|
|
588
|
+
source: access.surface,
|
|
589
|
+
status: "pending",
|
|
590
|
+
threadId: targetThreadId
|
|
591
|
+
};
|
|
592
|
+
broadcast("live", buildLivePayload());
|
|
593
|
+
|
|
594
|
+
const result = await codexAppServer.sendText({
|
|
595
|
+
threadId: targetThreadId,
|
|
596
|
+
cwd: targetCwd,
|
|
597
|
+
text: body.text || "",
|
|
598
|
+
attachments,
|
|
599
|
+
createThreadIfMissing: body.createThreadIfMissing !== false,
|
|
600
|
+
waitForCompletion: false,
|
|
601
|
+
timeoutMs: Number(body.timeoutMs || 45000)
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
rememberTurnOrigin(result.thread.id, result.turn.id, access.surface);
|
|
605
|
+
invalidateRepoChangesCache({
|
|
606
|
+
cwd: result.thread.cwd || targetCwd,
|
|
607
|
+
threadPath: result.thread.path || ""
|
|
608
|
+
});
|
|
609
|
+
const selectionStillTargetsSendThread =
|
|
610
|
+
!liveState.selectedThreadId ||
|
|
611
|
+
liveState.selectedThreadId === targetThreadId ||
|
|
612
|
+
liveState.selectedThreadId === result.thread.id;
|
|
613
|
+
|
|
614
|
+
if (selectionStillTargetsSendThread) {
|
|
615
|
+
liveState.selectedThreadId = result.thread.id;
|
|
616
|
+
liveState.selectedProjectCwd = result.thread.cwd || targetCwd;
|
|
617
|
+
liveState.selectedThreadSnapshot = mergeSelectedThreadSnapshot(
|
|
618
|
+
liveState.selectedThreadSnapshot,
|
|
619
|
+
result.snapshot
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
liveState.lastSyncAt = nowIso();
|
|
623
|
+
liveState.writeLock = null;
|
|
624
|
+
appServerState.lastWrite = {
|
|
625
|
+
at: nowIso(),
|
|
626
|
+
mode: result.mode,
|
|
627
|
+
source: access.surface,
|
|
628
|
+
threadId: result.thread.id,
|
|
629
|
+
turnId: result.turn.id,
|
|
630
|
+
turnStatus: result.turn.status
|
|
631
|
+
};
|
|
632
|
+
const responseSnapshot =
|
|
633
|
+
selectionStillTargetsSendThread && liveState.selectedThreadSnapshot
|
|
634
|
+
? liveState.selectedThreadSnapshot
|
|
635
|
+
: result.snapshot;
|
|
636
|
+
const decoratedSnapshot = decorateSnapshot(responseSnapshot);
|
|
637
|
+
broadcast("live", buildLivePayload());
|
|
638
|
+
sendJson(res, 200, {
|
|
639
|
+
ok: true,
|
|
640
|
+
mode: result.mode,
|
|
641
|
+
snapshot: decoratedSnapshot,
|
|
642
|
+
thread: summarizeResponseThread(decoratedSnapshot?.thread || result.thread),
|
|
643
|
+
turn: summarizeResponseTurn(result.turn)
|
|
644
|
+
});
|
|
645
|
+
void (async () => {
|
|
646
|
+
try {
|
|
647
|
+
await refreshThreads({ broadcastUpdate: false });
|
|
648
|
+
const shouldRestartWatcher =
|
|
649
|
+
liveState.selectedThreadId !== selectedThreadIdBeforeSend ||
|
|
650
|
+
(selectionStillTargetsSendThread && liveState.selectedThreadId !== result.thread.id) ||
|
|
651
|
+
!liveState.watcherConnected ||
|
|
652
|
+
!hasWatcherController();
|
|
653
|
+
if (shouldRestartWatcher) {
|
|
654
|
+
await restartWatcher();
|
|
655
|
+
} else {
|
|
656
|
+
scheduleSnapshotRefresh(120);
|
|
657
|
+
}
|
|
658
|
+
broadcast("live", buildLivePayload());
|
|
659
|
+
} catch (error) {
|
|
660
|
+
liveState.lastError = error.message;
|
|
661
|
+
broadcast("live", buildLivePayload());
|
|
662
|
+
}
|
|
663
|
+
})();
|
|
664
|
+
} catch (error) {
|
|
665
|
+
liveState.writeLock = null;
|
|
666
|
+
appServerState.lastWrite = {
|
|
667
|
+
at: nowIso(),
|
|
668
|
+
error: error.message,
|
|
669
|
+
source: "remote",
|
|
670
|
+
threadId: targetThreadId || null
|
|
671
|
+
};
|
|
672
|
+
const state = buildLivePayload();
|
|
673
|
+
const statusCode = errorStatusCode(
|
|
674
|
+
error,
|
|
675
|
+
/already in progress|pending interaction|holds control|take control/i.test(String(error.message || ""))
|
|
676
|
+
? 409
|
|
677
|
+
: 500
|
|
678
|
+
);
|
|
679
|
+
broadcast("live", state);
|
|
680
|
+
sendJson(res, statusCode, {
|
|
681
|
+
error: error.message,
|
|
682
|
+
state,
|
|
683
|
+
status: state.status
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
return true;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (req.method === "GET" && url.pathname === "/api/stream") {
|
|
690
|
+
requireSurfaceCapability(req, url, "read_room");
|
|
691
|
+
streamState(req, res);
|
|
692
|
+
return true;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (req.method === "POST" && url.pathname === "/api/commands") {
|
|
696
|
+
if (!devToolsEnabled || !mockAdapter) {
|
|
697
|
+
sendJson(res, 404, { error: "Not found" });
|
|
698
|
+
return true;
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
requireSurfaceCapability(req, url, "debug_tools");
|
|
702
|
+
const command = await readJsonBody(req);
|
|
703
|
+
const snapshot = store.applyCommand(command);
|
|
704
|
+
mockAdapter.scheduleFollowUp(command);
|
|
705
|
+
sendJson(res, 200, snapshot);
|
|
706
|
+
} catch (error) {
|
|
707
|
+
sendJson(res, errorStatusCode(error, 400), {
|
|
708
|
+
error: error.message
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return true;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return false;
|
|
715
|
+
} catch (error) {
|
|
716
|
+
sendJson(res, errorStatusCode(error, 500), { error: error.message, state: buildLivePayload() });
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
}
|