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,28 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="refresh" content="0; url=/">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1">
7
+ <title>Dextunnel</title>
8
+ <link rel="icon" href="/favicon.svg" type="image/svg+xml">
9
+ <link rel="stylesheet" href="/styles.css">
10
+ </head>
11
+ <body class="landing-shell">
12
+ <main class="landing-grid">
13
+ <section class="panel hero-panel">
14
+ <p class="eyebrow">Dextunnel / remote</p>
15
+ <h1>Opening the Dextunnel remote.</h1>
16
+ <p class="lede">
17
+ The browser landing page has been retired. Dextunnel now opens the main remote surface at the root URL.
18
+ </p>
19
+ <div class="hero-actions">
20
+ <a class="cta" href="/">Open remote</a>
21
+ </div>
22
+ </section>
23
+ </main>
24
+ <script>
25
+ window.location.replace("/");
26
+ </script>
27
+ </body>
28
+ </html>
@@ -0,0 +1,98 @@
1
+ const nodes = {
2
+ badge: document.querySelector("#preflight-badge"),
3
+ checks: document.querySelector("#preflight-checks"),
4
+ nextSteps: document.querySelector("#preflight-next-steps"),
5
+ refresh: document.querySelector("#preflight-refresh"),
6
+ summary: document.querySelector("#preflight-summary")
7
+ };
8
+
9
+ function setBadge(status, label) {
10
+ if (!nodes.badge) {
11
+ return;
12
+ }
13
+
14
+ nodes.badge.textContent = label;
15
+ nodes.badge.className = "landing-status-badge";
16
+ nodes.badge.classList.add(`is-${status}`);
17
+ }
18
+
19
+ function setSummary(message) {
20
+ if (nodes.summary) {
21
+ nodes.summary.textContent = message;
22
+ }
23
+ }
24
+
25
+ function renderChecks(checks) {
26
+ if (!nodes.checks) {
27
+ return;
28
+ }
29
+
30
+ nodes.checks.textContent = "";
31
+ for (const check of checks || []) {
32
+ const article = document.createElement("article");
33
+ article.className = `preflight-card is-${check.severity || "warning"}`;
34
+
35
+ const label = document.createElement("p");
36
+ label.className = "preflight-card-label";
37
+ label.textContent = check.label || "Check";
38
+
39
+ const detail = document.createElement("p");
40
+ detail.className = "preflight-card-detail";
41
+ detail.textContent = check.detail || "";
42
+
43
+ article.append(label, detail);
44
+ nodes.checks.append(article);
45
+ }
46
+ }
47
+
48
+ function renderNextSteps(steps) {
49
+ if (!nodes.nextSteps) {
50
+ return;
51
+ }
52
+
53
+ nodes.nextSteps.textContent = "";
54
+ for (const step of steps || []) {
55
+ const item = document.createElement("li");
56
+ item.textContent = step;
57
+ nodes.nextSteps.append(item);
58
+ }
59
+ }
60
+
61
+ async function loadPreflight() {
62
+ setBadge("loading", "Checking");
63
+ setSummary("Running a local Dextunnel preflight...");
64
+
65
+ try {
66
+ const response = await fetch("/api/preflight?warmup=1", {
67
+ headers: {
68
+ Accept: "application/json"
69
+ }
70
+ });
71
+ if (!response.ok) {
72
+ throw new Error(`Preflight returned HTTP ${response.status}.`);
73
+ }
74
+
75
+ const payload = await response.json();
76
+ const status = payload.status || "warning";
77
+ setBadge(status, status === "ready" ? "Ready" : status === "error" ? "Needs attention" : "Almost there");
78
+ setSummary(payload.summary || "Preflight complete.");
79
+ renderChecks(payload.checks || []);
80
+ renderNextSteps(payload.nextSteps || []);
81
+ } catch (error) {
82
+ setBadge("error", "Unavailable");
83
+ setSummary(String(error?.message || error || "Failed to load the Dextunnel preflight."));
84
+ renderChecks([
85
+ {
86
+ detail: "Try reloading this page or run npm run doctor in the repo root.",
87
+ label: "Preflight",
88
+ severity: "error"
89
+ }
90
+ ]);
91
+ }
92
+ }
93
+
94
+ nodes.refresh?.addEventListener("click", () => {
95
+ void loadPreflight();
96
+ });
97
+
98
+ void loadPreflight();
@@ -0,0 +1,258 @@
1
+ import {
2
+ planBootstrapRetry,
3
+ planStreamRecovery,
4
+ reconnectStreamState
5
+ } from "./live-bridge-retry-state.js";
6
+
7
+ export function createLiveBridgeLifecycleState({
8
+ bootstrapRetryBaseMs,
9
+ streamRecoveryBaseMs
10
+ } = {}) {
11
+ return {
12
+ bootstrapPromise: null,
13
+ bootstrapRetryBackoffMs: bootstrapRetryBaseMs,
14
+ bootstrapRetryTimer: null,
15
+ refreshAfterStreamReconnect: false,
16
+ refreshPromise: null,
17
+ stream: null,
18
+ streamRecoveryBackoffMs: streamRecoveryBaseMs,
19
+ streamRecoveryTimer: null,
20
+ streamState: "connecting"
21
+ };
22
+ }
23
+
24
+ export function createLiveBridgeLifecycle({
25
+ state,
26
+ bootstrapRetry,
27
+ streamRecovery,
28
+ createEventSource,
29
+ createTimeout = (callback, delay) => window.setTimeout(callback, delay),
30
+ clearCreatedTimeout = (timer) => window.clearTimeout(timer),
31
+ getHasLiveState,
32
+ getVisible,
33
+ onBootstrapError,
34
+ onBootstrapStart,
35
+ onBootstrapSuccess,
36
+ onLive,
37
+ onRender = () => {},
38
+ onSnapshot,
39
+ onStreamError,
40
+ onStreamOpen,
41
+ requestBootstrap,
42
+ requestRefresh,
43
+ streamUrl
44
+ }) {
45
+ if (!state) {
46
+ throw new Error("createLiveBridgeLifecycle requires a mutable state object.");
47
+ }
48
+
49
+ const lifecycle = {
50
+ bootstrap,
51
+ closeStream,
52
+ ensureStream,
53
+ resetBootstrapRetry,
54
+ resetStreamRecovery,
55
+ resumeVisible,
56
+ refresh
57
+ };
58
+
59
+ function resetStreamRecovery() {
60
+ if (state.streamRecoveryTimer) {
61
+ clearCreatedTimeout(state.streamRecoveryTimer);
62
+ state.streamRecoveryTimer = null;
63
+ }
64
+ state.streamRecoveryBackoffMs = streamRecovery.baseMs;
65
+ }
66
+
67
+ function resetBootstrapRetry() {
68
+ if (state.bootstrapRetryTimer) {
69
+ clearCreatedTimeout(state.bootstrapRetryTimer);
70
+ state.bootstrapRetryTimer = null;
71
+ }
72
+ state.bootstrapRetryBackoffMs = bootstrapRetry.baseMs;
73
+ }
74
+
75
+ function closeStream() {
76
+ if (!state.stream) {
77
+ return;
78
+ }
79
+
80
+ state.stream.close();
81
+ state.stream = null;
82
+ }
83
+
84
+ function scheduleBootstrapRetry() {
85
+ const plan = planBootstrapRetry({
86
+ backoffMs: state.bootstrapRetryBackoffMs,
87
+ baseMs: bootstrapRetry.baseMs,
88
+ hasTimer: Boolean(state.bootstrapRetryTimer),
89
+ isVisible: getVisible(),
90
+ maxMs: bootstrapRetry.maxMs
91
+ });
92
+ if (!plan.schedule) {
93
+ return;
94
+ }
95
+
96
+ state.bootstrapRetryBackoffMs = plan.nextBackoffMs;
97
+ state.bootstrapRetryTimer = createTimeout(() => {
98
+ state.bootstrapRetryTimer = null;
99
+ if (getVisible()) {
100
+ void lifecycle.bootstrap({ retrying: true });
101
+ lifecycle.ensureStream();
102
+ }
103
+ }, plan.delay);
104
+ }
105
+
106
+ function ensureStream({ force = false } = {}) {
107
+ if (state.stream && !force) {
108
+ return;
109
+ }
110
+
111
+ if (force) {
112
+ closeStream();
113
+ }
114
+
115
+ state.streamState = reconnectStreamState({ hasLiveState: getHasLiveState() });
116
+
117
+ const nextStream = createEventSource(streamUrl);
118
+ state.stream = nextStream;
119
+
120
+ nextStream.addEventListener("snapshot", (event) => {
121
+ if (state.stream !== nextStream) {
122
+ return;
123
+ }
124
+
125
+ onSnapshot(JSON.parse(event.data));
126
+ onRender();
127
+ });
128
+
129
+ nextStream.addEventListener("open", () => {
130
+ if (state.stream !== nextStream) {
131
+ return;
132
+ }
133
+
134
+ state.streamState = "live";
135
+ resetStreamRecovery();
136
+ onStreamOpen?.();
137
+ if (state.refreshAfterStreamReconnect) {
138
+ state.refreshAfterStreamReconnect = false;
139
+ void lifecycle.refresh({ background: true });
140
+ }
141
+ onRender();
142
+ });
143
+
144
+ nextStream.addEventListener("live", (event) => {
145
+ if (state.stream !== nextStream) {
146
+ return;
147
+ }
148
+
149
+ onLive(JSON.parse(event.data));
150
+ onRender();
151
+ });
152
+
153
+ nextStream.addEventListener("error", () => {
154
+ if (state.stream !== nextStream) {
155
+ return;
156
+ }
157
+
158
+ state.streamState = reconnectStreamState({ hasLiveState: getHasLiveState() });
159
+ closeStream();
160
+ onStreamError?.();
161
+ scheduleStreamRecovery();
162
+ onRender();
163
+ });
164
+ }
165
+
166
+ function scheduleStreamRecovery() {
167
+ const plan = planStreamRecovery({
168
+ backoffMs: state.streamRecoveryBackoffMs,
169
+ baseMs: streamRecovery.baseMs,
170
+ hasLiveState: getHasLiveState(),
171
+ hasTimer: Boolean(state.streamRecoveryTimer),
172
+ isVisible: getVisible(),
173
+ maxMs: streamRecovery.maxMs
174
+ });
175
+ if (!plan.schedule) {
176
+ state.streamState = plan.streamState;
177
+ return;
178
+ }
179
+
180
+ state.streamState = plan.streamState;
181
+ state.streamRecoveryBackoffMs = plan.nextBackoffMs;
182
+ state.streamRecoveryTimer = createTimeout(() => {
183
+ state.streamRecoveryTimer = null;
184
+ if (!getVisible()) {
185
+ return;
186
+ }
187
+
188
+ state.refreshAfterStreamReconnect = plan.followupAction === "refresh";
189
+ lifecycle.ensureStream({ force: true });
190
+ if (plan.followupAction === "bootstrap") {
191
+ void lifecycle.bootstrap({ retrying: true });
192
+ }
193
+ }, plan.delay);
194
+ }
195
+
196
+ async function bootstrap({ retrying = false } = {}) {
197
+ if (state.bootstrapPromise) {
198
+ return state.bootstrapPromise;
199
+ }
200
+
201
+ onBootstrapStart?.({ retrying });
202
+ onRender();
203
+
204
+ state.bootstrapPromise = requestBootstrap()
205
+ .then(({ snapshot, live }) => {
206
+ onBootstrapSuccess({ live, retrying, snapshot });
207
+ state.streamState = "live";
208
+ resetBootstrapRetry();
209
+ onRender();
210
+ return live;
211
+ })
212
+ .catch((error) => {
213
+ state.streamState = reconnectStreamState({ hasLiveState: getHasLiveState() });
214
+ onBootstrapError?.({ error, retrying });
215
+ onRender();
216
+ scheduleBootstrapRetry();
217
+ return null;
218
+ })
219
+ .finally(() => {
220
+ state.bootstrapPromise = null;
221
+ });
222
+
223
+ return state.bootstrapPromise;
224
+ }
225
+
226
+ async function refresh({ background = false } = {}) {
227
+ if (state.refreshPromise) {
228
+ return state.refreshPromise;
229
+ }
230
+
231
+ state.refreshPromise = Promise.resolve()
232
+ .then(() => requestRefresh({ background }))
233
+ .catch((error) => {
234
+ if (background) {
235
+ return null;
236
+ }
237
+ throw error;
238
+ })
239
+ .finally(() => {
240
+ state.refreshPromise = null;
241
+ });
242
+
243
+ return state.refreshPromise;
244
+ }
245
+
246
+ function resumeVisible() {
247
+ if (!getVisible()) {
248
+ return;
249
+ }
250
+
251
+ lifecycle.ensureStream();
252
+ if (!getHasLiveState()) {
253
+ void lifecycle.bootstrap({ retrying: true });
254
+ }
255
+ }
256
+
257
+ return lifecycle;
258
+ }
@@ -0,0 +1,61 @@
1
+ export function nextBackoffDelay(currentMs, { baseMs, maxMs }) {
2
+ const delay = Number.isFinite(currentMs) && currentMs > 0 ? currentMs : baseMs;
3
+ return {
4
+ delay,
5
+ nextMs: Math.min(delay * 2, maxMs)
6
+ };
7
+ }
8
+
9
+ export function shouldScheduleRetry({ hasTimer = false, isVisible = true } = {}) {
10
+ return !hasTimer && Boolean(isVisible);
11
+ }
12
+
13
+ export function reconnectStreamState({ hasLiveState = false } = {}) {
14
+ return hasLiveState ? "recovering" : "connecting";
15
+ }
16
+
17
+ export function planBootstrapRetry({
18
+ backoffMs,
19
+ baseMs,
20
+ maxMs,
21
+ hasTimer = false,
22
+ isVisible = true
23
+ } = {}) {
24
+ if (!shouldScheduleRetry({ hasTimer, isVisible })) {
25
+ return {
26
+ schedule: false
27
+ };
28
+ }
29
+
30
+ const next = nextBackoffDelay(backoffMs, { baseMs, maxMs });
31
+ return {
32
+ delay: next.delay,
33
+ nextBackoffMs: next.nextMs,
34
+ schedule: true
35
+ };
36
+ }
37
+
38
+ export function planStreamRecovery({
39
+ backoffMs,
40
+ baseMs,
41
+ maxMs,
42
+ hasLiveState = false,
43
+ hasTimer = false,
44
+ isVisible = true
45
+ } = {}) {
46
+ if (!shouldScheduleRetry({ hasTimer, isVisible })) {
47
+ return {
48
+ schedule: false,
49
+ streamState: reconnectStreamState({ hasLiveState })
50
+ };
51
+ }
52
+
53
+ const next = nextBackoffDelay(backoffMs, { baseMs, maxMs });
54
+ return {
55
+ delay: next.delay,
56
+ followupAction: hasLiveState ? "refresh" : "bootstrap",
57
+ nextBackoffMs: next.nextMs,
58
+ schedule: true,
59
+ streamState: reconnectStreamState({ hasLiveState })
60
+ };
61
+ }
@@ -0,0 +1,79 @@
1
+ export function createSelectionIntent({
2
+ cwd = null,
3
+ projectLabel = "",
4
+ source = "remote",
5
+ threadId = null,
6
+ threadLabel = ""
7
+ } = {}) {
8
+ return {
9
+ projectLabel: String(projectLabel || "").trim(),
10
+ requestedCwd: String(cwd || "").trim(),
11
+ requestedThreadId: String(threadId || "").trim(),
12
+ source: String(source || "remote").trim() || "remote",
13
+ threadLabel: String(threadLabel || "").trim()
14
+ };
15
+ }
16
+
17
+ export function selectionIntentSatisfied(intent = null, liveState = null) {
18
+ if (!intent) {
19
+ return true;
20
+ }
21
+
22
+ const requestedThreadId = String(intent.requestedThreadId || "").trim();
23
+ const requestedCwd = String(intent.requestedCwd || "").trim();
24
+ const selectedThreadId = String(liveState?.selectedThreadId || "").trim();
25
+ const selectedCwd = String(liveState?.selectedProjectCwd || "").trim();
26
+
27
+ if (requestedThreadId) {
28
+ return selectedThreadId === requestedThreadId;
29
+ }
30
+
31
+ if (requestedCwd) {
32
+ return selectedCwd === requestedCwd;
33
+ }
34
+
35
+ return true;
36
+ }
37
+
38
+ export function selectionIntentMessage(intent = null, fallback = "Switching shared room...") {
39
+ if (!intent) {
40
+ return fallback;
41
+ }
42
+
43
+ if (intent.threadLabel) {
44
+ return `Switching to ${intent.threadLabel}...`;
45
+ }
46
+
47
+ if (intent.projectLabel) {
48
+ return `Switching to ${intent.projectLabel}...`;
49
+ }
50
+
51
+ return fallback;
52
+ }
53
+
54
+ export function selectionIntentTitle(intent = null) {
55
+ if (!intent) {
56
+ return "";
57
+ }
58
+
59
+ const threadLabel = String(intent.threadLabel || "").trim();
60
+ if (!threadLabel) {
61
+ return "";
62
+ }
63
+
64
+ const normalized = threadLabel
65
+ .toLowerCase()
66
+ .replace(/[^a-z0-9]+/g, "-")
67
+ .replace(/^-+|-+$/g, "");
68
+
69
+ return normalized ? `#${normalized}` : "";
70
+ }
71
+
72
+ export function reconcileSelectionIntent(intent = null, liveState = null) {
73
+ const settled = selectionIntentSatisfied(intent, liveState);
74
+
75
+ return {
76
+ intent: settled ? null : intent,
77
+ settled
78
+ };
79
+ }