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.
Files changed (76) hide show
  1. package/LICENSE +211 -0
  2. package/README.md +112 -0
  3. package/SECURITY.md +27 -0
  4. package/SUPPORT.md +43 -0
  5. package/package.json +44 -0
  6. package/public/client-shared.js +1831 -0
  7. package/public/favicon.svg +11 -0
  8. package/public/host.html +29 -0
  9. package/public/host.js +2079 -0
  10. package/public/index.html +28 -0
  11. package/public/index.js +98 -0
  12. package/public/live-bridge-lifecycle.js +258 -0
  13. package/public/live-bridge-retry-state.js +61 -0
  14. package/public/live-selection-intent.js +79 -0
  15. package/public/remote-operator-state.js +316 -0
  16. package/public/remote.html +167 -0
  17. package/public/remote.js +3967 -0
  18. package/public/styles.css +2793 -0
  19. package/public/surface-view-state.js +89 -0
  20. package/public/voice-dictation.js +45 -0
  21. package/src/bin/desktop-rehydration-smoke.mjs +111 -0
  22. package/src/bin/dextunnel.mjs +41 -0
  23. package/src/bin/doctor.mjs +48 -0
  24. package/src/bin/launch-attest.mjs +39 -0
  25. package/src/bin/launch-status.mjs +49 -0
  26. package/src/bin/mobile-link-proxy.mjs +221 -0
  27. package/src/bin/mobile-proof.mjs +164 -0
  28. package/src/bin/mobile-transport-smoke.mjs +200 -0
  29. package/src/bin/probe-codex-app-server-write.mjs +36 -0
  30. package/src/bin/probe-codex-app-server.mjs +30 -0
  31. package/src/lib/agent-room-context.mjs +54 -0
  32. package/src/lib/agent-room-runtime.mjs +355 -0
  33. package/src/lib/agent-room-service.mjs +335 -0
  34. package/src/lib/agent-room-state.mjs +406 -0
  35. package/src/lib/agent-room-store.mjs +71 -0
  36. package/src/lib/agent-room-text.mjs +48 -0
  37. package/src/lib/app-server-contract.mjs +66 -0
  38. package/src/lib/app-server-runtime.mjs +60 -0
  39. package/src/lib/attachment-service.mjs +119 -0
  40. package/src/lib/bridge-api-handler.mjs +719 -0
  41. package/src/lib/bridge-runtime-lifecycle.mjs +51 -0
  42. package/src/lib/bridge-status-builder.mjs +60 -0
  43. package/src/lib/codex-app-server-client.mjs +1511 -0
  44. package/src/lib/companion-state.mjs +453 -0
  45. package/src/lib/control-lease-service.mjs +180 -0
  46. package/src/lib/debug-harness-service.mjs +173 -0
  47. package/src/lib/desktop-integration.mjs +146 -0
  48. package/src/lib/desktop-rehydration-smoke.mjs +269 -0
  49. package/src/lib/dextunnel-cli.mjs +122 -0
  50. package/src/lib/discovery-docs.mjs +1321 -0
  51. package/src/lib/fake-codex-app-server-bridge.mjs +340 -0
  52. package/src/lib/install-preflight.mjs +373 -0
  53. package/src/lib/interaction-resolution-service.mjs +185 -0
  54. package/src/lib/interaction-state.mjs +360 -0
  55. package/src/lib/launch-release-bar.mjs +158 -0
  56. package/src/lib/live-control-state.mjs +107 -0
  57. package/src/lib/live-payload-builder.mjs +298 -0
  58. package/src/lib/live-selection-transition-state.mjs +49 -0
  59. package/src/lib/live-transcript-state.mjs +549 -0
  60. package/src/lib/mobile-network-profile.mjs +39 -0
  61. package/src/lib/mock-codex-adapter.mjs +62 -0
  62. package/src/lib/operator-diagnostics.mjs +82 -0
  63. package/src/lib/repo-changes-service.mjs +527 -0
  64. package/src/lib/runtime-config.mjs +106 -0
  65. package/src/lib/selection-state-service.mjs +214 -0
  66. package/src/lib/session-store.mjs +355 -0
  67. package/src/lib/shared-room-state.mjs +473 -0
  68. package/src/lib/shared-selection-state.mjs +40 -0
  69. package/src/lib/sse-hub.mjs +35 -0
  70. package/src/lib/static-surface-service.mjs +71 -0
  71. package/src/lib/surface-access.mjs +189 -0
  72. package/src/lib/surface-presence-service.mjs +118 -0
  73. package/src/lib/surface-request-guard.mjs +52 -0
  74. package/src/lib/thread-sync-state.mjs +536 -0
  75. package/src/lib/watcher-lifecycle.mjs +287 -0
  76. package/src/server.mjs +1446 -0
@@ -0,0 +1,107 @@
1
+ import {
2
+ clearControlLease,
3
+ ensureControlActionAllowed,
4
+ renewControlLease,
5
+ setControlLease
6
+ } from "./shared-room-state.mjs";
7
+
8
+ export function applyLiveControlAction({
9
+ action = "claim",
10
+ clientId = null,
11
+ existingLease = null,
12
+ owner = null,
13
+ reason = null,
14
+ source = "remote",
15
+ threadId = null,
16
+ ttlMs,
17
+ now = Date.now()
18
+ } = {}) {
19
+ const nextThreadId = String(threadId || "").trim();
20
+ const nextClientId = String(clientId || "").trim() || null;
21
+ const nextSource = source || "remote";
22
+
23
+ if (!nextThreadId) {
24
+ throw new Error("No live session selected.");
25
+ }
26
+
27
+ if (nextSource === "remote" || nextSource === "agent") {
28
+ ensureControlActionAllowed({
29
+ action,
30
+ clientId: nextClientId,
31
+ lease: existingLease,
32
+ source: nextSource,
33
+ threadId: nextThreadId,
34
+ now
35
+ });
36
+ }
37
+
38
+ if (action === "release") {
39
+ const previousLease = existingLease || null;
40
+ return {
41
+ lease: clearControlLease(existingLease, { threadId: nextThreadId, now }),
42
+ recordEvent: Boolean(previousLease),
43
+ event: previousLease
44
+ ? {
45
+ action: "release",
46
+ actor: nextSource,
47
+ actorClientId: nextClientId,
48
+ cause: "released",
49
+ owner: previousLease.owner || null,
50
+ ownerClientId: previousLease.ownerClientId || null,
51
+ ownerLabel: previousLease.ownerLabel || null,
52
+ reason: previousLease.reason || null,
53
+ source: previousLease.source || null,
54
+ threadId: previousLease.threadId || nextThreadId
55
+ }
56
+ : null
57
+ };
58
+ }
59
+
60
+ if (action === "renew") {
61
+ return {
62
+ lease: renewControlLease({
63
+ clientId: nextClientId,
64
+ lease: existingLease,
65
+ now,
66
+ owner: owner || nextSource,
67
+ reason,
68
+ source: nextSource,
69
+ threadId: nextThreadId,
70
+ ttlMs
71
+ }),
72
+ recordEvent: false,
73
+ event: null
74
+ };
75
+ }
76
+
77
+ if (action === "claim") {
78
+ const lease = setControlLease({
79
+ clientId: nextClientId,
80
+ now,
81
+ owner: owner || nextSource,
82
+ reason: reason || "compose",
83
+ source: nextSource,
84
+ threadId: nextThreadId,
85
+ ttlMs
86
+ });
87
+
88
+ return {
89
+ lease,
90
+ recordEvent: true,
91
+ event: {
92
+ action: "claim",
93
+ actor: nextSource,
94
+ actorClientId: nextClientId,
95
+ cause: "claimed",
96
+ owner: lease.owner || null,
97
+ ownerClientId: lease.ownerClientId || null,
98
+ ownerLabel: lease.ownerLabel || null,
99
+ reason: lease.reason || null,
100
+ source: lease.source || null,
101
+ threadId: lease.threadId || nextThreadId
102
+ }
103
+ };
104
+ }
105
+
106
+ throw new Error(`Unsupported control action: ${action}`);
107
+ }
@@ -0,0 +1,298 @@
1
+ function isOriginEligibleEntry(entry) {
2
+ if (!entry) {
3
+ return false;
4
+ }
5
+
6
+ if (entry.role === "user") {
7
+ return true;
8
+ }
9
+
10
+ return entry.role === "assistant" && entry.kind !== "commentary";
11
+ }
12
+
13
+ export function createLivePayloadBuilder(deps) {
14
+ const {
15
+ advisoryParticipantForThread,
16
+ ADVISORY_PARTICIPANT_IDS = [],
17
+ bestConversationLabel,
18
+ bestThreadLabel,
19
+ buildBridgeStatus,
20
+ buildParticipant,
21
+ buildSelectedAgentRoomState,
22
+ buildSelectedAttachments,
23
+ buildSelectedCompanionState,
24
+ getPendingInteractionForSelectedThread,
25
+ liveState,
26
+ looksLikeTopicNoise,
27
+ normalizeLane,
28
+ projectLabel,
29
+ pruneAllCompanionWakeups,
30
+ pruneStaleSurfacePresence,
31
+ repoObjective,
32
+ selectedThreadSummary,
33
+ slugifyChannelName,
34
+ summarizeThread,
35
+ trimTopicText
36
+ } = deps;
37
+
38
+ function inferEntryLane(entry, thread) {
39
+ const explicitLane = normalizeLane(entry?.origin);
40
+ if (explicitLane) {
41
+ return explicitLane;
42
+ }
43
+
44
+ if (entry?.role !== "user") {
45
+ return "";
46
+ }
47
+
48
+ if (thread?.source === "vscode") {
49
+ return "desktop";
50
+ }
51
+
52
+ if (thread?.source === "cli") {
53
+ return "external";
54
+ }
55
+
56
+ return "";
57
+ }
58
+
59
+ function participantForEntry(entry, thread) {
60
+ const lane = inferEntryLane(entry, thread);
61
+
62
+ if (entry?.kind === "pending" || entry?.kind === "queued") {
63
+ return buildParticipant("remote");
64
+ }
65
+
66
+ if (entry?.role === "assistant" && entry?.kind === "commentary") {
67
+ return buildParticipant("updates");
68
+ }
69
+
70
+ if (entry?.role === "assistant") {
71
+ return buildParticipant("codex");
72
+ }
73
+
74
+ if (entry?.role === "user") {
75
+ if (lane === "remote") {
76
+ return buildParticipant("remote");
77
+ }
78
+
79
+ if (lane === "desktop") {
80
+ return buildParticipant("desktop");
81
+ }
82
+
83
+ if (lane === "oracle") {
84
+ return buildParticipant("oracle");
85
+ }
86
+
87
+ if (lane === "gemini") {
88
+ return buildParticipant("gemini");
89
+ }
90
+
91
+ if (lane === "external") {
92
+ return buildParticipant("user", {
93
+ label: "external",
94
+ lane: "external",
95
+ token: "user"
96
+ });
97
+ }
98
+
99
+ return buildParticipant("user");
100
+ }
101
+
102
+ if (entry?.role === "tool") {
103
+ return buildParticipant("tools");
104
+ }
105
+
106
+ return buildParticipant("system");
107
+ }
108
+
109
+ function buildChannelTopic(snapshot) {
110
+ const thread = snapshot?.thread || null;
111
+ const objective = repoObjective(thread?.cwd || "");
112
+ if (objective && !looksLikeTopicNoise(objective)) {
113
+ return objective;
114
+ }
115
+
116
+ if (thread?.preview && !looksLikeTopicNoise(thread.preview)) {
117
+ return trimTopicText(thread.preview);
118
+ }
119
+
120
+ const latestConversation = bestConversationLabel(snapshot, 120);
121
+ if (!latestConversation) {
122
+ return "";
123
+ }
124
+
125
+ return trimTopicText(latestConversation, 120);
126
+ }
127
+
128
+ function buildSelectedChannel(snapshot) {
129
+ const thread = snapshot?.thread || selectedThreadSummary() || null;
130
+ const cwd = thread?.cwd || liveState.selectedProjectCwd || process.cwd();
131
+ const label = bestThreadLabel(
132
+ {
133
+ ...thread,
134
+ cwd
135
+ },
136
+ snapshot,
137
+ { selected: true }
138
+ );
139
+
140
+ return {
141
+ channelId: thread?.id || liveState.selectedThreadId || null,
142
+ channelLabel: label,
143
+ channelSlug: `#${slugifyChannelName(label)}`,
144
+ source: thread?.source || null,
145
+ serverId: cwd,
146
+ serverLabel: projectLabel(cwd),
147
+ topic: buildChannelTopic(snapshot)
148
+ };
149
+ }
150
+
151
+ function buildParticipants(snapshot) {
152
+ const participants = new Map();
153
+ const threadId = snapshot?.thread?.id || null;
154
+
155
+ for (const entry of snapshot?.transcript || []) {
156
+ const participant = participantForEntry(entry, snapshot?.thread);
157
+ participants.set(participant.id, participant);
158
+ }
159
+
160
+ if (snapshot?.thread?.source === "vscode" && !participants.has("desktop")) {
161
+ participants.set("desktop", buildParticipant("desktop"));
162
+ }
163
+
164
+ if (!participants.has("remote")) {
165
+ participants.set("remote", buildParticipant("remote"));
166
+ }
167
+
168
+ if (!participants.has("codex")) {
169
+ participants.set("codex", buildParticipant("codex"));
170
+ }
171
+
172
+ for (const advisorId of ADVISORY_PARTICIPANT_IDS) {
173
+ participants.set(advisorId, advisoryParticipantForThread(advisorId, threadId));
174
+ }
175
+
176
+ return [...participants.values()].sort((a, b) => {
177
+ const orderDelta = (a.sortOrder || 999) - (b.sortOrder || 999);
178
+ if (orderDelta !== 0) {
179
+ return orderDelta;
180
+ }
181
+
182
+ return String(a.label || a.id || "").localeCompare(String(b.label || b.id || ""));
183
+ });
184
+ }
185
+
186
+ function applyTurnOrigins(snapshot) {
187
+ const threadId = snapshot?.thread?.id || null;
188
+ if (!threadId || !Array.isArray(snapshot?.transcript) || snapshot.transcript.length === 0) {
189
+ return snapshot;
190
+ }
191
+
192
+ const turnOrigins = liveState.turnOriginsByThreadId?.[threadId];
193
+ if (!turnOrigins) {
194
+ return snapshot;
195
+ }
196
+
197
+ let changed = false;
198
+ const transcript = snapshot.transcript.map((entry) => {
199
+ const origin =
200
+ isOriginEligibleEntry(entry) && entry.turnId
201
+ ? turnOrigins[entry.turnId] || null
202
+ : null;
203
+ const currentOrigin = entry.origin || null;
204
+
205
+ if (currentOrigin === origin) {
206
+ return entry;
207
+ }
208
+
209
+ changed = true;
210
+ if (!origin) {
211
+ const { origin: _origin, ...rest } = entry;
212
+ return rest;
213
+ }
214
+
215
+ return {
216
+ ...entry,
217
+ origin
218
+ };
219
+ });
220
+
221
+ if (!changed) {
222
+ return snapshot;
223
+ }
224
+
225
+ return {
226
+ ...snapshot,
227
+ transcript
228
+ };
229
+ }
230
+
231
+ function decorateSnapshot(snapshot) {
232
+ const withOrigins = applyTurnOrigins(snapshot);
233
+ const thread = withOrigins?.thread || null;
234
+
235
+ if (!thread || !Array.isArray(withOrigins?.transcript)) {
236
+ return withOrigins;
237
+ }
238
+
239
+ const transcript = withOrigins.transcript.map((entry) => {
240
+ const lane = inferEntryLane(entry, thread);
241
+ const participant = participantForEntry(entry, thread);
242
+
243
+ return {
244
+ ...entry,
245
+ lane,
246
+ participant
247
+ };
248
+ });
249
+
250
+ const nextSnapshot = {
251
+ ...withOrigins,
252
+ transcript
253
+ };
254
+
255
+ return {
256
+ ...nextSnapshot,
257
+ channel: buildSelectedChannel(nextSnapshot),
258
+ participants: buildParticipants(nextSnapshot)
259
+ };
260
+ }
261
+
262
+ function buildLivePayload() {
263
+ pruneStaleSurfacePresence();
264
+ pruneAllCompanionWakeups();
265
+ const selectedThreadSnapshot = decorateSnapshot(liveState.selectedThreadSnapshot);
266
+ const selectedThreadId = selectedThreadSnapshot?.thread?.id || liveState.selectedThreadId || null;
267
+ const selectedAttachments = buildSelectedAttachments(selectedThreadId);
268
+ const selectedCompanion = buildSelectedCompanionState(selectedThreadId);
269
+ const selectedAgentRoom = buildSelectedAgentRoomState(selectedThreadId);
270
+
271
+ return {
272
+ pendingInteraction: getPendingInteractionForSelectedThread(),
273
+ participants: selectedThreadSnapshot?.participants || [],
274
+ selectedAgentRoom,
275
+ selectedCompanion,
276
+ selectedAttachments,
277
+ selectedChannel: selectedThreadSnapshot?.channel || buildSelectedChannel(null),
278
+ selectedProjectCwd: liveState.selectedProjectCwd,
279
+ selectionSource: liveState.selectionSource,
280
+ selectedThreadId: liveState.selectedThreadId,
281
+ selectedThreadSnapshot,
282
+ status: buildBridgeStatus(),
283
+ turnDiff: liveState.turnDiff,
284
+ threads: liveState.threads.map(summarizeThread)
285
+ };
286
+ }
287
+
288
+ return {
289
+ applyTurnOrigins,
290
+ buildChannelTopic,
291
+ buildLivePayload,
292
+ buildParticipants,
293
+ buildSelectedChannel,
294
+ decorateSnapshot,
295
+ inferEntryLane,
296
+ participantForEntry
297
+ };
298
+ }
@@ -0,0 +1,49 @@
1
+ import { clearControlLease } from "./shared-room-state.mjs";
2
+ import { applySharedSelectionState } from "./shared-selection-state.mjs";
3
+
4
+ export function applyLiveSelectionTransition(
5
+ state = {},
6
+ {
7
+ cwd = null,
8
+ source = "remote",
9
+ threadId = null,
10
+ threads = []
11
+ } = {},
12
+ { now = Date.now() } = {}
13
+ ) {
14
+ const previousThreadId = state.selectedThreadId || null;
15
+ const selection = applySharedSelectionState(
16
+ {
17
+ selectedProjectCwd: state.selectedProjectCwd || "",
18
+ selectedThreadId: previousThreadId,
19
+ selectedThreadSnapshot: state.selectedThreadSnapshot || null,
20
+ selectionSource: state.selectionSource || "remote",
21
+ turnDiff: state.turnDiff || null,
22
+ writeLock: state.writeLock || null
23
+ },
24
+ {
25
+ cwd,
26
+ source,
27
+ threadId,
28
+ threads
29
+ }
30
+ );
31
+
32
+ const interactionFlow = selection.threadChanged ? null : state.interactionFlow || null;
33
+ const controlLease = selection.threadChanged
34
+ ? clearControlLease(state.controlLease || null, { now, threadId: previousThreadId || null })
35
+ : state.controlLease || null;
36
+
37
+ return {
38
+ cleared: {
39
+ controlLease: selection.threadChanged && Boolean(state.controlLease),
40
+ interactionFlow: selection.threadChanged && Boolean(state.interactionFlow)
41
+ },
42
+ nextState: {
43
+ ...selection.nextState,
44
+ controlLease,
45
+ interactionFlow
46
+ },
47
+ threadChanged: selection.threadChanged
48
+ };
49
+ }