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,164 @@
1
+ import { spawn } from "node:child_process";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ function printUsage() {
6
+ console.log(
7
+ "Usage: node src/bin/mobile-proof.mjs [--base-url http://127.0.0.1:4317] [--surface remote|host] [--transport-probe-send] [--no-native-probe-send] [--network-profile weak-mobile|weak-mobile-reconnect] [--proxy-port 4417]"
8
+ );
9
+ }
10
+
11
+ function parseArgs(argv) {
12
+ const options = {
13
+ baseUrl: "http://127.0.0.1:4317",
14
+ nativeProbeSend: true,
15
+ networkProfile: "",
16
+ proxyPort: 4417,
17
+ surface: "remote"
18
+ };
19
+
20
+ for (let index = 0; index < argv.length; index += 1) {
21
+ const arg = argv[index];
22
+
23
+ if (arg === "--help" || arg === "-h") {
24
+ printUsage();
25
+ process.exit(0);
26
+ }
27
+
28
+ if (arg === "--base-url") {
29
+ options.baseUrl = argv[index + 1] || options.baseUrl;
30
+ index += 1;
31
+ continue;
32
+ }
33
+
34
+ if (arg === "--surface") {
35
+ options.surface = argv[index + 1] || options.surface;
36
+ index += 1;
37
+ continue;
38
+ }
39
+
40
+ if (arg === "--network-profile") {
41
+ options.networkProfile = argv[index + 1] || options.networkProfile;
42
+ index += 1;
43
+ continue;
44
+ }
45
+
46
+ if (arg === "--proxy-port") {
47
+ options.proxyPort = Number(argv[index + 1]) || options.proxyPort;
48
+ index += 1;
49
+ continue;
50
+ }
51
+
52
+ if (arg === "--no-probe-send" || arg === "--no-native-probe-send") {
53
+ options.nativeProbeSend = false;
54
+ continue;
55
+ }
56
+
57
+ if (arg === "--transport-probe-send") {
58
+ options.transportProbeSend = true;
59
+ continue;
60
+ }
61
+
62
+ throw new Error(`Unknown argument: ${arg}`);
63
+ }
64
+
65
+ return options;
66
+ }
67
+
68
+ function runCommand(command, args, options = {}) {
69
+ return new Promise((resolve, reject) => {
70
+ const child = spawn(command, args, {
71
+ cwd: options.cwd || process.cwd(),
72
+ env: process.env,
73
+ stdio: "inherit"
74
+ });
75
+
76
+ child.on("error", reject);
77
+ child.on("exit", (code, signal) => {
78
+ if (signal) {
79
+ reject(new Error(`${command} exited via signal ${signal}`));
80
+ return;
81
+ }
82
+
83
+ if (code !== 0) {
84
+ reject(new Error(`${command} exited with code ${code}`));
85
+ return;
86
+ }
87
+
88
+ resolve();
89
+ });
90
+ });
91
+ }
92
+
93
+ async function main() {
94
+ const options = parseArgs(process.argv.slice(2));
95
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
96
+ const repoRoot = path.resolve(__dirname, "..", "..");
97
+ const nativeDir = path.join(repoRoot, "native", "apple");
98
+ let effectiveBaseUrl = options.baseUrl;
99
+ let proxyChild = null;
100
+
101
+ if (options.networkProfile) {
102
+ const proxyScript = path.join(repoRoot, "src", "bin", "mobile-link-proxy.mjs");
103
+ proxyChild = spawn(process.execPath, [
104
+ proxyScript,
105
+ "--target-base-url",
106
+ options.baseUrl,
107
+ "--listen-port",
108
+ String(options.proxyPort),
109
+ "--profile",
110
+ options.networkProfile
111
+ ], {
112
+ cwd: repoRoot,
113
+ env: process.env,
114
+ stdio: "inherit"
115
+ });
116
+
117
+ proxyChild.on("error", (error) => {
118
+ console.error(`[mobile-proof] proxy failed: ${error.message}`);
119
+ });
120
+
121
+ await new Promise((resolve) => setTimeout(resolve, 250));
122
+ effectiveBaseUrl = `http://127.0.0.1:${options.proxyPort}`;
123
+ }
124
+
125
+ const transportArgs = [
126
+ path.join(repoRoot, "src", "bin", "mobile-transport-smoke.mjs"),
127
+ "--base-url",
128
+ effectiveBaseUrl,
129
+ "--surface",
130
+ options.surface
131
+ ];
132
+ if (options.transportProbeSend) {
133
+ transportArgs.push("--probe-send");
134
+ }
135
+
136
+ const nativeArgs = [
137
+ "run",
138
+ "DextunnelNativeBridgeSmoke",
139
+ "--base-url",
140
+ effectiveBaseUrl,
141
+ "--surface",
142
+ options.surface
143
+ ];
144
+ if (options.nativeProbeSend) {
145
+ nativeArgs.push("--probe-send");
146
+ }
147
+
148
+ try {
149
+ console.log(`[mobile-proof] transport smoke -> ${effectiveBaseUrl} (${options.surface})`);
150
+ await runCommand(process.execPath, transportArgs, { cwd: repoRoot });
151
+ console.log("[mobile-proof] native bridge smoke");
152
+ await runCommand("swift", nativeArgs, { cwd: nativeDir });
153
+ console.log("[mobile-proof] complete");
154
+ } finally {
155
+ if (proxyChild && proxyChild.exitCode == null) {
156
+ proxyChild.kill("SIGTERM");
157
+ }
158
+ }
159
+ }
160
+
161
+ main().catch((error) => {
162
+ console.error(`[mobile-proof] failed: ${error.message}`);
163
+ process.exit(1);
164
+ });
@@ -0,0 +1,200 @@
1
+ import { performance } from "node:perf_hooks";
2
+
3
+ function parseArgs(argv) {
4
+ const options = {
5
+ baseUrl: "http://127.0.0.1:4317",
6
+ json: false,
7
+ probeSend: false,
8
+ surface: "remote"
9
+ };
10
+
11
+ for (let index = 0; index < argv.length; index += 1) {
12
+ const argument = argv[index];
13
+ if (argument === "--base-url" && argv[index + 1]) {
14
+ options.baseUrl = argv[index + 1];
15
+ index += 1;
16
+ continue;
17
+ }
18
+ if (argument === "--surface" && argv[index + 1]) {
19
+ options.surface = argv[index + 1];
20
+ index += 1;
21
+ continue;
22
+ }
23
+ if (argument === "--probe-send") {
24
+ options.probeSend = true;
25
+ continue;
26
+ }
27
+ if (argument === "--json") {
28
+ options.json = true;
29
+ continue;
30
+ }
31
+ if (argument === "--help" || argument === "-h") {
32
+ printHelpAndExit(0);
33
+ }
34
+ }
35
+
36
+ return options;
37
+ }
38
+
39
+ function printHelpAndExit(code = 0) {
40
+ console.log("Usage: node src/bin/mobile-transport-smoke.mjs [--base-url http://127.0.0.1:4317] [--surface remote|host] [--probe-send] [--json]");
41
+ process.exit(code);
42
+ }
43
+
44
+ async function measuredJson(url, init = {}) {
45
+ const startedAt = performance.now();
46
+ const response = await fetch(url, init);
47
+ const rawText = await response.text();
48
+ const elapsedMs = Math.round(performance.now() - startedAt);
49
+ const bytes = Buffer.byteLength(rawText || "", "utf8");
50
+ const payload = rawText ? JSON.parse(rawText) : null;
51
+ if (!response.ok) {
52
+ const error = new Error(payload?.error || payload?.message || response.statusText || "Request failed.");
53
+ error.status = response.status;
54
+ error.payload = payload;
55
+ throw error;
56
+ }
57
+ return {
58
+ bytes,
59
+ elapsedMs,
60
+ payload,
61
+ status: response.status,
62
+ url
63
+ };
64
+ }
65
+
66
+ function surfaceHeaders(token) {
67
+ return {
68
+ "x-dextunnel-surface-token": token
69
+ };
70
+ }
71
+
72
+ function summarizeMetric(name, result) {
73
+ return {
74
+ bytes: result.bytes,
75
+ elapsedMs: result.elapsedMs,
76
+ name,
77
+ status: result.status
78
+ };
79
+ }
80
+
81
+ async function main() {
82
+ const options = parseArgs(process.argv.slice(2));
83
+ const baseUrl = new URL(options.baseUrl);
84
+ const bootstrapUrl = new URL("/api/codex-app-server/bootstrap", baseUrl);
85
+ bootstrapUrl.searchParams.set("surface", options.surface);
86
+
87
+ const bootstrap = await measuredJson(bootstrapUrl);
88
+ const accessToken = bootstrap.payload?.accessToken;
89
+ if (!accessToken) {
90
+ throw new Error("Bootstrap did not return a surface token.");
91
+ }
92
+
93
+ const metrics = [summarizeMetric("bootstrap", bootstrap)];
94
+
95
+ const liveState = await measuredJson(new URL("/api/codex-app-server/live-state", baseUrl), {
96
+ headers: surfaceHeaders(accessToken)
97
+ });
98
+ metrics.push(summarizeMetric("live-state", liveState));
99
+
100
+ const threads = await measuredJson(new URL("/api/codex-app-server/threads", baseUrl), {
101
+ headers: surfaceHeaders(accessToken)
102
+ });
103
+ metrics.push(summarizeMetric("threads", threads));
104
+
105
+ const refresh = await measuredJson(new URL("/api/codex-app-server/refresh", baseUrl), {
106
+ headers: surfaceHeaders(accessToken),
107
+ method: "POST"
108
+ });
109
+ metrics.push(summarizeMetric("refresh", refresh));
110
+
111
+ const selectedThreadId = refresh.payload?.state?.selectedThreadId || liveState.payload?.selectedThreadId || null;
112
+ const selectedProjectCwd = refresh.payload?.state?.selectedProjectCwd || liveState.payload?.selectedProjectCwd || null;
113
+
114
+ if (selectedThreadId || selectedProjectCwd) {
115
+ const selection = await measuredJson(new URL("/api/codex-app-server/selection", baseUrl), {
116
+ body: JSON.stringify({
117
+ cwd: selectedProjectCwd,
118
+ threadId: selectedThreadId
119
+ }),
120
+ headers: {
121
+ ...surfaceHeaders(accessToken),
122
+ "Content-Type": "application/json"
123
+ },
124
+ method: "POST"
125
+ });
126
+ metrics.push(summarizeMetric("selection", selection));
127
+ }
128
+
129
+ if (options.probeSend) {
130
+ if (!selectedThreadId) {
131
+ throw new Error("Probe send requires a selected thread.");
132
+ }
133
+ const stamp = new Date().toISOString().replaceAll(":", "").replaceAll(".", "");
134
+ const claim = await measuredJson(new URL("/api/codex-app-server/control", baseUrl), {
135
+ body: JSON.stringify({
136
+ action: "claim",
137
+ reason: "transport_smoke",
138
+ threadId: selectedThreadId
139
+ }),
140
+ headers: {
141
+ ...surfaceHeaders(accessToken),
142
+ "Content-Type": "application/json"
143
+ },
144
+ method: "POST"
145
+ });
146
+ metrics.push(summarizeMetric("claim", claim));
147
+
148
+ const send = await measuredJson(new URL("/api/codex-app-server/turn", baseUrl), {
149
+ body: JSON.stringify({
150
+ attachments: [],
151
+ text: `TRANSPORT_SMOKE_PROBE_${stamp}. Reply with exactly TRANSPORT_SMOKE_ACK_${stamp}.`,
152
+ threadId: selectedThreadId
153
+ }),
154
+ headers: {
155
+ ...surfaceHeaders(accessToken),
156
+ "Content-Type": "application/json"
157
+ },
158
+ method: "POST"
159
+ });
160
+ metrics.push(summarizeMetric("send", send));
161
+
162
+ const release = await measuredJson(new URL("/api/codex-app-server/control", baseUrl), {
163
+ body: JSON.stringify({
164
+ action: "release",
165
+ reason: "transport_smoke_cleanup",
166
+ threadId: selectedThreadId
167
+ }),
168
+ headers: {
169
+ ...surfaceHeaders(accessToken),
170
+ "Content-Type": "application/json"
171
+ },
172
+ method: "POST"
173
+ });
174
+ metrics.push(summarizeMetric("release", release));
175
+ }
176
+
177
+ const summary = {
178
+ metrics,
179
+ selectedProjectCwd,
180
+ selectedThreadId,
181
+ surface: options.surface
182
+ };
183
+
184
+ if (options.json) {
185
+ console.log(JSON.stringify(summary, null, 2));
186
+ return;
187
+ }
188
+
189
+ for (const metric of metrics) {
190
+ console.log(`[${options.surface}] ${metric.name} elapsedMs=${metric.elapsedMs} bytes=${metric.bytes} status=${metric.status}`);
191
+ }
192
+ if (selectedThreadId) {
193
+ console.log(`[${options.surface}] selectedThreadId=${selectedThreadId}`);
194
+ }
195
+ }
196
+
197
+ main().catch((error) => {
198
+ console.error(error?.message || String(error));
199
+ process.exit(1);
200
+ });
@@ -0,0 +1,36 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import path from "node:path";
4
+
5
+ import { createCodexAppServerBridge } from "../lib/codex-app-server-client.mjs";
6
+
7
+ const cwd = process.argv[2] || path.join(tmpdir(), "dextunnel-app-server-write-probe");
8
+ const bridge = createCodexAppServerBridge();
9
+
10
+ try {
11
+ await mkdir(cwd, { recursive: true });
12
+ const result = await bridge.sendText({
13
+ cwd,
14
+ text: "Reply with REMOTE_WRITE_OK only.",
15
+ createThreadIfMissing: true,
16
+ timeoutMs: 45000
17
+ });
18
+
19
+ console.log(
20
+ JSON.stringify(
21
+ {
22
+ ok: true,
23
+ cwd,
24
+ mode: result.mode,
25
+ threadId: result.thread.id,
26
+ turnId: result.turn.id,
27
+ turnStatus: result.turn.status,
28
+ preview: result.snapshot.transcript.slice(-6)
29
+ },
30
+ null,
31
+ 2
32
+ )
33
+ );
34
+ } finally {
35
+ await bridge.dispose();
36
+ }
@@ -0,0 +1,30 @@
1
+ import { createCodexAppServerBridge, mapThreadToCompanionSnapshot } from "../lib/codex-app-server-client.mjs";
2
+
3
+ const cwd = process.argv[2] || process.cwd();
4
+ const bridge = createCodexAppServerBridge();
5
+
6
+ try {
7
+ const thread = await bridge.getLatestThreadForCwd(cwd);
8
+
9
+ if (!thread) {
10
+ console.log(JSON.stringify({ cwd, found: false }, null, 2));
11
+ process.exit(0);
12
+ }
13
+
14
+ const snapshot = mapThreadToCompanionSnapshot(thread);
15
+ console.log(
16
+ JSON.stringify(
17
+ {
18
+ found: true,
19
+ cwd,
20
+ thread: snapshot.thread,
21
+ transcriptCount: snapshot.transcript.length,
22
+ preview: snapshot.transcript.slice(-8)
23
+ },
24
+ null,
25
+ 2
26
+ )
27
+ );
28
+ } finally {
29
+ await bridge.dispose();
30
+ }
@@ -0,0 +1,54 @@
1
+ function formatTimestampLabel(value) {
2
+ const date = new Date(value || 0);
3
+ if (Number.isNaN(date.getTime())) {
4
+ return "";
5
+ }
6
+ return date.toISOString();
7
+ }
8
+
9
+ function formatContextEntries(entries = [], limit = 10, { trimTopicText }) {
10
+ return entries
11
+ .slice(-limit)
12
+ .map((entry) => {
13
+ const label = entry?.participant?.label || entry?.participantId || entry?.origin || entry?.lane || entry?.role || "voice";
14
+ const text = trimTopicText(entry?.text || "", 280);
15
+ const timestamp = entry?.timestamp ? formatTimestampLabel(entry.timestamp) : "";
16
+ return `- ${timestamp ? `[${timestamp}] ` : ""}${label}: ${text}`;
17
+ })
18
+ .join("\n");
19
+ }
20
+
21
+ export function createAgentRoomContextBuilder({
22
+ buildSelectedChannel,
23
+ decorateSnapshot,
24
+ nowIso,
25
+ trimTopicText
26
+ }) {
27
+ function buildAgentRoomContextMarkdown({ roomState, snapshot, threadId }) {
28
+ const selectedSnapshot = snapshot ? decorateSnapshot(snapshot) : null;
29
+ const channel = selectedSnapshot?.channel || buildSelectedChannel(selectedSnapshot);
30
+ const mainEntries = selectedSnapshot?.transcript || [];
31
+ const roomEntries = roomState?.messages || [];
32
+
33
+ return [
34
+ "# Dextunnel Council Room Context",
35
+ "",
36
+ `generated_at: ${nowIso()}`,
37
+ `thread_id: ${threadId || channel.channelId || ""}`,
38
+ `server: ${channel.serverLabel || ""}`,
39
+ `channel: ${channel.channelSlug || ""}`,
40
+ `topic: ${channel.topic || ""}`,
41
+ "",
42
+ "## Main Codex thread excerpt",
43
+ formatContextEntries(mainEntries, 10, { trimTopicText }) || "- No main-thread transcript available.",
44
+ "",
45
+ "## Council room transcript",
46
+ formatContextEntries(roomEntries, 20, { trimTopicText }) || "- No prior council-room messages.",
47
+ ""
48
+ ].join("\n");
49
+ }
50
+
51
+ return {
52
+ buildAgentRoomContextMarkdown
53
+ };
54
+ }