codex-webstrapper 0.2.0 → 0.2.5
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/package.json +2 -1
- package/src/bun-pty-bridge.mjs +161 -0
- package/src/message-router.mjs +783 -41
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-webstrapper",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Web wrapper for Codex desktop assets with bridge + token auth",
|
|
6
6
|
"license": "MIT",
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
25
|
"@electron/asar": "^4.0.1",
|
|
26
|
+
"bun-pty": "^0.4.8",
|
|
26
27
|
"ws": "^8.18.0"
|
|
27
28
|
}
|
|
28
29
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { spawn } from "bun-pty";
|
|
2
|
+
|
|
3
|
+
function toErrorMessage(error) {
|
|
4
|
+
if (!error) {
|
|
5
|
+
return "unknown_error";
|
|
6
|
+
}
|
|
7
|
+
if (typeof error === "string") {
|
|
8
|
+
return error;
|
|
9
|
+
}
|
|
10
|
+
if (typeof error.message === "string" && error.message.length > 0) {
|
|
11
|
+
return error.message;
|
|
12
|
+
}
|
|
13
|
+
return String(error);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function send(payload) {
|
|
17
|
+
try {
|
|
18
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
19
|
+
} catch {
|
|
20
|
+
// Ignore JSON serialization/output failures.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseConfig(raw) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(raw || "{}");
|
|
27
|
+
if (!parsed || typeof parsed !== "object") {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toDimension(value, fallback) {
|
|
37
|
+
const parsed = Number(value);
|
|
38
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
39
|
+
return fallback;
|
|
40
|
+
}
|
|
41
|
+
return Math.max(1, Math.floor(parsed));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const config = parseConfig(process.env.CODEX_WEBSTRAP_BUN_PTY_CONFIG || "");
|
|
45
|
+
if (!config || typeof config.file !== "string" || config.file.length === 0) {
|
|
46
|
+
send({ type: "error", message: "Invalid bun-pty config." });
|
|
47
|
+
process.exit(2);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let terminal;
|
|
51
|
+
try {
|
|
52
|
+
terminal = spawn(
|
|
53
|
+
config.file,
|
|
54
|
+
Array.isArray(config.args) ? config.args.filter((entry) => typeof entry === "string") : [],
|
|
55
|
+
{
|
|
56
|
+
name: typeof config.term === "string" && config.term.length > 0 ? config.term : "xterm-256color",
|
|
57
|
+
cols: toDimension(config.cols, 120),
|
|
58
|
+
rows: toDimension(config.rows, 30),
|
|
59
|
+
cwd: typeof config.cwd === "string" && config.cwd.length > 0 ? config.cwd : process.cwd(),
|
|
60
|
+
env: config.env && typeof config.env === "object" ? config.env : process.env
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
send({
|
|
65
|
+
type: "error",
|
|
66
|
+
message: `Failed to start bun-pty terminal: ${toErrorMessage(error)}`
|
|
67
|
+
});
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let exiting = false;
|
|
72
|
+
const requestExit = (code = 0) => {
|
|
73
|
+
if (exiting) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
exiting = true;
|
|
77
|
+
process.exit(code);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
terminal.onData((data) => {
|
|
81
|
+
send({
|
|
82
|
+
type: "data",
|
|
83
|
+
data
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
terminal.onExit((exit) => {
|
|
88
|
+
send({
|
|
89
|
+
type: "exit",
|
|
90
|
+
exitCode: typeof exit?.exitCode === "number" ? exit.exitCode : null,
|
|
91
|
+
signal: exit?.signal ?? null
|
|
92
|
+
});
|
|
93
|
+
requestExit(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
let inputBuffer = "";
|
|
97
|
+
process.stdin.setEncoding("utf8");
|
|
98
|
+
process.stdin.on("data", (chunk) => {
|
|
99
|
+
inputBuffer += chunk;
|
|
100
|
+
|
|
101
|
+
for (;;) {
|
|
102
|
+
const newlineAt = inputBuffer.indexOf("\n");
|
|
103
|
+
if (newlineAt < 0) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const line = inputBuffer.slice(0, newlineAt);
|
|
108
|
+
inputBuffer = inputBuffer.slice(newlineAt + 1);
|
|
109
|
+
|
|
110
|
+
if (line.trim().length === 0) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let command;
|
|
115
|
+
try {
|
|
116
|
+
command = JSON.parse(line);
|
|
117
|
+
} catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
try {
|
|
122
|
+
if (command?.type === "write") {
|
|
123
|
+
terminal.write(typeof command.data === "string" ? command.data : "");
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (command?.type === "resize") {
|
|
128
|
+
terminal.resize(toDimension(command.cols, 120), toDimension(command.rows, 30));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (command?.type === "close") {
|
|
133
|
+
terminal.kill("SIGTERM");
|
|
134
|
+
requestExit(0);
|
|
135
|
+
}
|
|
136
|
+
} catch (error) {
|
|
137
|
+
send({
|
|
138
|
+
type: "error",
|
|
139
|
+
message: toErrorMessage(error)
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
process.on("SIGTERM", () => {
|
|
146
|
+
try {
|
|
147
|
+
terminal.kill("SIGTERM");
|
|
148
|
+
} catch {
|
|
149
|
+
// Ignore.
|
|
150
|
+
}
|
|
151
|
+
requestExit(0);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
process.on("SIGINT", () => {
|
|
155
|
+
try {
|
|
156
|
+
terminal.kill("SIGTERM");
|
|
157
|
+
} catch {
|
|
158
|
+
// Ignore.
|
|
159
|
+
}
|
|
160
|
+
requestExit(0);
|
|
161
|
+
});
|
package/src/message-router.mjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn as childSpawn, spawnSync as childSpawnSync } from "node:child_process";
|
|
2
2
|
import { Worker } from "node:worker_threads";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { readFileSync, statSync } from "node:fs";
|
|
6
6
|
import fs from "node:fs/promises";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
|
|
8
9
|
import { createLogger, randomId, safeJsonParse, toErrorMessage } from "./util.mjs";
|
|
9
10
|
|
|
@@ -76,6 +77,11 @@ class TerminalRegistry {
|
|
|
76
77
|
this.sendToWs = sendToWs;
|
|
77
78
|
this.logger = logger;
|
|
78
79
|
this.sessions = new Map();
|
|
80
|
+
this.bunPtyAvailable = this._detectBunPtyAvailability();
|
|
81
|
+
this.loggedBunPtyFailure = false;
|
|
82
|
+
this.pythonPtyAvailable = this._detectPythonPtyAvailability();
|
|
83
|
+
this.loggedPythonPtyFailure = false;
|
|
84
|
+
this.hasPtyLikeRuntime = this.bunPtyAvailable || this.pythonPtyAvailable;
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
createOrAttach(ws, message) {
|
|
@@ -83,41 +89,80 @@ class TerminalRegistry {
|
|
|
83
89
|
const existing = this.sessions.get(sessionId);
|
|
84
90
|
if (existing) {
|
|
85
91
|
existing.listeners.add(ws);
|
|
86
|
-
this.sendToWs(ws, {
|
|
92
|
+
this.sendToWs(ws, {
|
|
93
|
+
type: "terminal-attached",
|
|
94
|
+
sessionId,
|
|
95
|
+
cwd: existing.cwd,
|
|
96
|
+
shell: existing.shell
|
|
97
|
+
});
|
|
98
|
+
this.sendToWs(ws, {
|
|
99
|
+
type: "terminal-init-log",
|
|
100
|
+
sessionId,
|
|
101
|
+
log: existing.attachLog
|
|
102
|
+
});
|
|
103
|
+
if (this._hasExplicitDimensions(message)) {
|
|
104
|
+
this.resize(sessionId, message.cols, message.rows);
|
|
105
|
+
}
|
|
87
106
|
return;
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
109
|
+
const launchConfig = this._resolveLaunchConfig(message);
|
|
110
|
+
let runtime;
|
|
111
|
+
try {
|
|
112
|
+
runtime = this._spawnRuntime(launchConfig, message);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
this.sendToWs(ws, {
|
|
115
|
+
type: "terminal-error",
|
|
116
|
+
sessionId,
|
|
117
|
+
message: toErrorMessage(error)
|
|
118
|
+
});
|
|
119
|
+
this.logger.warn("Terminal spawn failed", {
|
|
120
|
+
sessionId,
|
|
121
|
+
error: toErrorMessage(error)
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
94
125
|
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
});
|
|
126
|
+
const attachLogLines = [
|
|
127
|
+
`Terminal attached via codex-webstrapper (${runtime.mode})`
|
|
128
|
+
];
|
|
129
|
+
if (launchConfig.cwdWasFallback && launchConfig.requestedCwd) {
|
|
130
|
+
attachLogLines.push(`[webstrap] Requested cwd unavailable: ${launchConfig.requestedCwd}`);
|
|
131
|
+
attachLogLines.push(`[webstrap] Using cwd: ${launchConfig.cwd}`);
|
|
132
|
+
}
|
|
133
|
+
const attachLog = `${attachLogLines.join("\r\n")}\r\n`;
|
|
104
134
|
|
|
105
135
|
const session = {
|
|
106
136
|
sessionId,
|
|
107
|
-
|
|
108
|
-
|
|
137
|
+
listeners: new Set([ws]),
|
|
138
|
+
cwd: launchConfig.cwd,
|
|
139
|
+
shell: launchConfig.shell,
|
|
140
|
+
attachLog,
|
|
141
|
+
cols: runtime.cols ?? this._coerceDimension(message?.cols, 120),
|
|
142
|
+
rows: runtime.rows ?? this._coerceDimension(message?.rows, 30),
|
|
143
|
+
...runtime
|
|
109
144
|
};
|
|
110
145
|
|
|
111
146
|
this.sessions.set(sessionId, session);
|
|
112
147
|
|
|
113
|
-
this.sendToWs(ws, {
|
|
148
|
+
this.sendToWs(ws, {
|
|
149
|
+
type: "terminal-attached",
|
|
150
|
+
sessionId,
|
|
151
|
+
cwd: session.cwd,
|
|
152
|
+
shell: session.shell
|
|
153
|
+
});
|
|
114
154
|
this.sendToWs(ws, {
|
|
115
155
|
type: "terminal-init-log",
|
|
116
156
|
sessionId,
|
|
117
|
-
log:
|
|
157
|
+
log: session.attachLog
|
|
118
158
|
});
|
|
119
159
|
|
|
120
|
-
|
|
160
|
+
if (session.mode === "bun-pty") {
|
|
161
|
+
this._attachBunPtyProcess(sessionId, session);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
session.proc.stdout?.on("data", (chunk) => {
|
|
121
166
|
this._broadcast(sessionId, {
|
|
122
167
|
type: "terminal-data",
|
|
123
168
|
sessionId,
|
|
@@ -125,7 +170,7 @@ class TerminalRegistry {
|
|
|
125
170
|
});
|
|
126
171
|
});
|
|
127
172
|
|
|
128
|
-
proc.stderr?.on("data", (chunk) => {
|
|
173
|
+
session.proc.stderr?.on("data", (chunk) => {
|
|
129
174
|
this._broadcast(sessionId, {
|
|
130
175
|
type: "terminal-data",
|
|
131
176
|
sessionId,
|
|
@@ -133,7 +178,7 @@ class TerminalRegistry {
|
|
|
133
178
|
});
|
|
134
179
|
});
|
|
135
180
|
|
|
136
|
-
proc.on("error", (error) => {
|
|
181
|
+
session.proc.on("error", (error) => {
|
|
137
182
|
this._broadcast(sessionId, {
|
|
138
183
|
type: "terminal-error",
|
|
139
184
|
sessionId,
|
|
@@ -141,7 +186,7 @@ class TerminalRegistry {
|
|
|
141
186
|
});
|
|
142
187
|
});
|
|
143
188
|
|
|
144
|
-
proc.on("exit", (code, signal) => {
|
|
189
|
+
session.proc.on("exit", (code, signal) => {
|
|
145
190
|
this._broadcast(sessionId, {
|
|
146
191
|
type: "terminal-exit",
|
|
147
192
|
sessionId,
|
|
@@ -154,17 +199,72 @@ class TerminalRegistry {
|
|
|
154
199
|
|
|
155
200
|
write(sessionId, data) {
|
|
156
201
|
const session = this.sessions.get(sessionId);
|
|
157
|
-
if (!session
|
|
202
|
+
if (!session) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (session.mode === "bun-pty") {
|
|
207
|
+
try {
|
|
208
|
+
this._writeToBunPty(session, {
|
|
209
|
+
type: "write",
|
|
210
|
+
data
|
|
211
|
+
});
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this._broadcast(sessionId, {
|
|
214
|
+
type: "terminal-error",
|
|
215
|
+
sessionId,
|
|
216
|
+
message: toErrorMessage(error)
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (!session.proc.stdin || session.proc.stdin.destroyed) {
|
|
158
223
|
return;
|
|
159
224
|
}
|
|
160
|
-
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
session.proc.stdin.write(data);
|
|
228
|
+
} catch (error) {
|
|
229
|
+
this._broadcast(sessionId, {
|
|
230
|
+
type: "terminal-error",
|
|
231
|
+
sessionId,
|
|
232
|
+
message: toErrorMessage(error)
|
|
233
|
+
});
|
|
234
|
+
}
|
|
161
235
|
}
|
|
162
236
|
|
|
163
|
-
resize(sessionId) {
|
|
164
|
-
|
|
237
|
+
resize(sessionId, cols, rows) {
|
|
238
|
+
const session = this.sessions.get(sessionId);
|
|
239
|
+
if (!session) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (session.mode === "bun-pty") {
|
|
244
|
+
try {
|
|
245
|
+
const nextCols = this._coerceDimension(cols, session.cols || 120);
|
|
246
|
+
const nextRows = this._coerceDimension(rows, session.rows || 30);
|
|
247
|
+
this._writeToBunPty(session, {
|
|
248
|
+
type: "resize",
|
|
249
|
+
cols: nextCols,
|
|
250
|
+
rows: nextRows
|
|
251
|
+
});
|
|
252
|
+
session.cols = nextCols;
|
|
253
|
+
session.rows = nextRows;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
this._broadcast(sessionId, {
|
|
256
|
+
type: "terminal-error",
|
|
257
|
+
sessionId,
|
|
258
|
+
message: toErrorMessage(error)
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (session.mode !== "python-pty") {
|
|
265
|
+
this.logger.debug("Terminal resize ignored (non-PTY mode)", { sessionId });
|
|
165
266
|
return;
|
|
166
267
|
}
|
|
167
|
-
this.logger.debug("Terminal resize ignored (non-PTY mode)", { sessionId });
|
|
168
268
|
}
|
|
169
269
|
|
|
170
270
|
close(sessionId) {
|
|
@@ -173,6 +273,24 @@ class TerminalRegistry {
|
|
|
173
273
|
return;
|
|
174
274
|
}
|
|
175
275
|
|
|
276
|
+
if (session.mode === "bun-pty") {
|
|
277
|
+
try {
|
|
278
|
+
this._writeToBunPty(session, {
|
|
279
|
+
type: "close"
|
|
280
|
+
});
|
|
281
|
+
} catch (error) {
|
|
282
|
+
this.logger.debug("Bun PTY close command failed", {
|
|
283
|
+
sessionId,
|
|
284
|
+
error: toErrorMessage(error)
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
if (!session.proc.killed) {
|
|
288
|
+
session.proc.kill();
|
|
289
|
+
}
|
|
290
|
+
this.sessions.delete(sessionId);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
176
294
|
if (!session.proc.killed) {
|
|
177
295
|
session.proc.kill();
|
|
178
296
|
}
|
|
@@ -204,6 +322,353 @@ class TerminalRegistry {
|
|
|
204
322
|
this.sendToWs(listener, message);
|
|
205
323
|
}
|
|
206
324
|
}
|
|
325
|
+
|
|
326
|
+
_resolveLaunchConfig(message) {
|
|
327
|
+
const requestedCwd = typeof message?.cwd === "string" ? message.cwd.trim() : "";
|
|
328
|
+
const cwdResult = this._resolveCwd(requestedCwd);
|
|
329
|
+
|
|
330
|
+
const commandFromMessage = Array.isArray(message?.command)
|
|
331
|
+
? message.command.filter((entry) => typeof entry === "string" && entry.length > 0)
|
|
332
|
+
: [];
|
|
333
|
+
|
|
334
|
+
let command = commandFromMessage;
|
|
335
|
+
let shell = null;
|
|
336
|
+
|
|
337
|
+
if (command.length === 0) {
|
|
338
|
+
shell = this._resolveShellPath(message?.shell);
|
|
339
|
+
command = this._defaultShellCommand(shell);
|
|
340
|
+
} else {
|
|
341
|
+
shell = command[0];
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
command,
|
|
346
|
+
shell,
|
|
347
|
+
cwd: cwdResult.cwd,
|
|
348
|
+
requestedCwd: cwdResult.requestedCwd,
|
|
349
|
+
cwdWasFallback: cwdResult.cwdWasFallback,
|
|
350
|
+
env: this._buildEnv(message?.env)
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
_spawnRuntime(launchConfig, message) {
|
|
355
|
+
const bunPtyRuntime = this._spawnBunPtyRuntime(launchConfig, message);
|
|
356
|
+
if (bunPtyRuntime) {
|
|
357
|
+
return bunPtyRuntime;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const pythonPtyRuntime = this._spawnPythonPtyRuntime(launchConfig);
|
|
361
|
+
if (pythonPtyRuntime) {
|
|
362
|
+
return pythonPtyRuntime;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const [bin, ...args] = launchConfig.command;
|
|
366
|
+
const proc = childSpawn(bin, args, {
|
|
367
|
+
cwd: launchConfig.cwd,
|
|
368
|
+
env: launchConfig.env,
|
|
369
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
return {
|
|
373
|
+
mode: "pipe",
|
|
374
|
+
proc,
|
|
375
|
+
cols: this._coerceDimension(message?.cols, 120),
|
|
376
|
+
rows: this._coerceDimension(message?.rows, 30)
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
_spawnBunPtyRuntime(launchConfig, message) {
|
|
381
|
+
if (!this.bunPtyAvailable) {
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const [bin, ...args] = launchConfig.command;
|
|
386
|
+
try {
|
|
387
|
+
const bridgePath = fileURLToPath(new URL("./bun-pty-bridge.mjs", import.meta.url));
|
|
388
|
+
const initialCols = this._coerceDimension(message?.cols, 120);
|
|
389
|
+
const initialRows = this._coerceDimension(message?.rows, 30);
|
|
390
|
+
const config = JSON.stringify({
|
|
391
|
+
file: bin,
|
|
392
|
+
args,
|
|
393
|
+
cwd: launchConfig.cwd,
|
|
394
|
+
env: launchConfig.env,
|
|
395
|
+
cols: initialCols,
|
|
396
|
+
rows: initialRows,
|
|
397
|
+
term: launchConfig.env.TERM || "xterm-256color"
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const proc = childSpawn("bun", [bridgePath], {
|
|
401
|
+
cwd: launchConfig.cwd,
|
|
402
|
+
env: {
|
|
403
|
+
...launchConfig.env,
|
|
404
|
+
CODEX_WEBSTRAP_BUN_PTY_CONFIG: config
|
|
405
|
+
},
|
|
406
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
return {
|
|
410
|
+
mode: "bun-pty",
|
|
411
|
+
proc,
|
|
412
|
+
bunStdoutBuffer: "",
|
|
413
|
+
cols: initialCols,
|
|
414
|
+
rows: initialRows
|
|
415
|
+
};
|
|
416
|
+
} catch (error) {
|
|
417
|
+
this.bunPtyAvailable = false;
|
|
418
|
+
if (!this.loggedBunPtyFailure) {
|
|
419
|
+
this.loggedBunPtyFailure = true;
|
|
420
|
+
this.logger.warn("Bun PTY unavailable; falling back", {
|
|
421
|
+
error: toErrorMessage(error)
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
_spawnPythonPtyRuntime(launchConfig) {
|
|
429
|
+
if (!this.pythonPtyAvailable) {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const [bin, ...args] = launchConfig.command;
|
|
434
|
+
try {
|
|
435
|
+
const proc = childSpawn(
|
|
436
|
+
"python3",
|
|
437
|
+
[
|
|
438
|
+
"-c",
|
|
439
|
+
"import pty, sys; pty.spawn(sys.argv[1:])",
|
|
440
|
+
bin,
|
|
441
|
+
...args
|
|
442
|
+
],
|
|
443
|
+
{
|
|
444
|
+
cwd: launchConfig.cwd,
|
|
445
|
+
env: launchConfig.env,
|
|
446
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
447
|
+
}
|
|
448
|
+
);
|
|
449
|
+
return {
|
|
450
|
+
mode: "python-pty",
|
|
451
|
+
proc,
|
|
452
|
+
cols: 120,
|
|
453
|
+
rows: 30
|
|
454
|
+
};
|
|
455
|
+
} catch (error) {
|
|
456
|
+
this.pythonPtyAvailable = false;
|
|
457
|
+
if (!this.loggedPythonPtyFailure) {
|
|
458
|
+
this.loggedPythonPtyFailure = true;
|
|
459
|
+
this.logger.warn("python3 PTY fallback unavailable; using pipe terminal", {
|
|
460
|
+
error: toErrorMessage(error)
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
_attachBunPtyProcess(sessionId, session) {
|
|
468
|
+
const handleBridgeLine = (line) => {
|
|
469
|
+
const parsed = safeJsonParse(line);
|
|
470
|
+
if (!parsed || typeof parsed !== "object") {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (parsed.type === "data") {
|
|
475
|
+
this._broadcast(sessionId, {
|
|
476
|
+
type: "terminal-data",
|
|
477
|
+
sessionId,
|
|
478
|
+
data: typeof parsed.data === "string" ? parsed.data : ""
|
|
479
|
+
});
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (parsed.type === "error") {
|
|
484
|
+
this._broadcast(sessionId, {
|
|
485
|
+
type: "terminal-error",
|
|
486
|
+
sessionId,
|
|
487
|
+
message: typeof parsed.message === "string" ? parsed.message : "bun-pty bridge error"
|
|
488
|
+
});
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (parsed.type === "exit") {
|
|
493
|
+
this._broadcast(sessionId, {
|
|
494
|
+
type: "terminal-exit",
|
|
495
|
+
sessionId,
|
|
496
|
+
code: typeof parsed.exitCode === "number" ? parsed.exitCode : null,
|
|
497
|
+
signal: parsed.signal ?? null
|
|
498
|
+
});
|
|
499
|
+
this.sessions.delete(sessionId);
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
session.proc.stdout?.on("data", (chunk) => {
|
|
504
|
+
session.bunStdoutBuffer += chunk.toString("utf8");
|
|
505
|
+
for (;;) {
|
|
506
|
+
const newlineAt = session.bunStdoutBuffer.indexOf("\n");
|
|
507
|
+
if (newlineAt < 0) {
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
const line = session.bunStdoutBuffer.slice(0, newlineAt);
|
|
511
|
+
session.bunStdoutBuffer = session.bunStdoutBuffer.slice(newlineAt + 1);
|
|
512
|
+
if (line.trim().length === 0) {
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
handleBridgeLine(line);
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
session.proc.stderr?.on("data", (chunk) => {
|
|
520
|
+
this._broadcast(sessionId, {
|
|
521
|
+
type: "terminal-error",
|
|
522
|
+
sessionId,
|
|
523
|
+
message: chunk.toString("utf8")
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
session.proc.on("error", (error) => {
|
|
528
|
+
this._broadcast(sessionId, {
|
|
529
|
+
type: "terminal-error",
|
|
530
|
+
sessionId,
|
|
531
|
+
message: toErrorMessage(error)
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
session.proc.on("exit", (code, signal) => {
|
|
536
|
+
if (!this.sessions.has(sessionId)) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
this._broadcast(sessionId, {
|
|
540
|
+
type: "terminal-exit",
|
|
541
|
+
sessionId,
|
|
542
|
+
code,
|
|
543
|
+
signal
|
|
544
|
+
});
|
|
545
|
+
this.sessions.delete(sessionId);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
_writeToBunPty(session, payload) {
|
|
550
|
+
if (!session?.proc?.stdin || session.proc.stdin.destroyed) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
session.proc.stdin.write(`${JSON.stringify(payload)}\n`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
_detectBunPtyAvailability() {
|
|
557
|
+
const hasBun = childSpawnSync("bun", ["--version"], { stdio: "ignore" });
|
|
558
|
+
if (hasBun.status !== 0 || hasBun.error) {
|
|
559
|
+
return false;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const probe = childSpawnSync(
|
|
563
|
+
"bun",
|
|
564
|
+
["-e", "import 'bun-pty';"],
|
|
565
|
+
{ stdio: "ignore" }
|
|
566
|
+
);
|
|
567
|
+
return probe.status === 0 && !probe.error;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_detectPythonPtyAvailability() {
|
|
571
|
+
const probe = childSpawnSync(
|
|
572
|
+
"python3",
|
|
573
|
+
["-c", "import pty"],
|
|
574
|
+
{ stdio: "ignore" }
|
|
575
|
+
);
|
|
576
|
+
return probe.status === 0 && !probe.error;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
_buildEnv(messageEnv) {
|
|
580
|
+
const env = {
|
|
581
|
+
...process.env,
|
|
582
|
+
...(messageEnv && typeof messageEnv === "object" ? messageEnv : {})
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
if (!env.TERM || env.TERM === "dumb") {
|
|
586
|
+
env.TERM = "xterm-256color";
|
|
587
|
+
}
|
|
588
|
+
if (!env.COLORTERM) {
|
|
589
|
+
env.COLORTERM = "truecolor";
|
|
590
|
+
}
|
|
591
|
+
if (!env.TERM_PROGRAM) {
|
|
592
|
+
env.TERM_PROGRAM = "codex-webstrapper";
|
|
593
|
+
}
|
|
594
|
+
return env;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
_resolveShellPath(messageShell) {
|
|
598
|
+
if (typeof messageShell === "string" && messageShell.trim().length > 0) {
|
|
599
|
+
return messageShell.trim();
|
|
600
|
+
}
|
|
601
|
+
return process.env.SHELL || "/bin/zsh";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
_defaultShellCommand(shellPath) {
|
|
605
|
+
const shellName = path.basename(shellPath).toLowerCase();
|
|
606
|
+
const disableProfileLoad = process.env.CODEX_WEBSTRAP_TERMINAL_NO_PROFILE === "1";
|
|
607
|
+
const preferLoginProfile = this.hasPtyLikeRuntime && !disableProfileLoad;
|
|
608
|
+
|
|
609
|
+
if (shellName === "zsh") {
|
|
610
|
+
return preferLoginProfile
|
|
611
|
+
? [shellPath, "-il"]
|
|
612
|
+
: [shellPath, "-fi"];
|
|
613
|
+
}
|
|
614
|
+
if (shellName === "bash") {
|
|
615
|
+
return preferLoginProfile
|
|
616
|
+
? [shellPath, "-il"]
|
|
617
|
+
: [shellPath, "--noprofile", "--norc", "-i"];
|
|
618
|
+
}
|
|
619
|
+
if (shellName === "fish") {
|
|
620
|
+
return preferLoginProfile
|
|
621
|
+
? [shellPath, "-il"]
|
|
622
|
+
: [shellPath, "-i"];
|
|
623
|
+
}
|
|
624
|
+
return [shellPath, "-i"];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
_resolveCwd(requestedCwd) {
|
|
628
|
+
const requested = requestedCwd && requestedCwd.length > 0
|
|
629
|
+
? path.resolve(requestedCwd)
|
|
630
|
+
: null;
|
|
631
|
+
if (requested && this._isDirectory(requested)) {
|
|
632
|
+
return {
|
|
633
|
+
cwd: requested,
|
|
634
|
+
requestedCwd: requested,
|
|
635
|
+
cwdWasFallback: false
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const fallbackCandidates = [process.cwd(), os.homedir(), "/"];
|
|
640
|
+
const fallback = fallbackCandidates.find((candidate) => this._isDirectory(candidate)) || process.cwd();
|
|
641
|
+
return {
|
|
642
|
+
cwd: fallback,
|
|
643
|
+
requestedCwd: requested,
|
|
644
|
+
cwdWasFallback: Boolean(requested)
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
_isDirectory(candidatePath) {
|
|
649
|
+
if (!candidatePath || typeof candidatePath !== "string") {
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
try {
|
|
653
|
+
return statSync(candidatePath).isDirectory();
|
|
654
|
+
} catch {
|
|
655
|
+
return false;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
_coerceDimension(value, fallback) {
|
|
660
|
+
const parsed = Number(value);
|
|
661
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
662
|
+
return fallback;
|
|
663
|
+
}
|
|
664
|
+
return Math.max(1, Math.floor(parsed));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
_hasExplicitDimensions(message) {
|
|
668
|
+
const cols = Number(message?.cols);
|
|
669
|
+
const rows = Number(message?.rows);
|
|
670
|
+
return Number.isFinite(cols) && cols > 0 && Number.isFinite(rows) && rows > 0;
|
|
671
|
+
}
|
|
207
672
|
}
|
|
208
673
|
|
|
209
674
|
class GitWorkerBridge {
|
|
@@ -543,7 +1008,7 @@ export class MessageRouter {
|
|
|
543
1008
|
this.terminals.write(message.sessionId, message.data || "");
|
|
544
1009
|
return;
|
|
545
1010
|
case "terminal-resize":
|
|
546
|
-
this.terminals.resize(message.sessionId);
|
|
1011
|
+
this.terminals.resize(message.sessionId, message.cols, message.rows);
|
|
547
1012
|
return;
|
|
548
1013
|
case "terminal-close":
|
|
549
1014
|
this.terminals.close(message.sessionId);
|
|
@@ -753,14 +1218,25 @@ export class MessageRouter {
|
|
|
753
1218
|
this.logger.debug("renderer-fetch", {
|
|
754
1219
|
requestId,
|
|
755
1220
|
method: message.method || "GET",
|
|
756
|
-
url: message.url
|
|
757
|
-
|
|
1221
|
+
url: this._sanitizeUrlForLogs(message.url),
|
|
1222
|
+
hasBody: message.body != null
|
|
758
1223
|
});
|
|
759
1224
|
|
|
760
1225
|
if (await this._handleVirtualFetch(ws, requestId, message)) {
|
|
761
1226
|
return;
|
|
762
1227
|
}
|
|
763
1228
|
|
|
1229
|
+
if (typeof message.url === "string" && message.url === "/transcribe") {
|
|
1230
|
+
const controller = new AbortController();
|
|
1231
|
+
this.fetchControllers.set(requestId, controller);
|
|
1232
|
+
try {
|
|
1233
|
+
await this._handleTranscribeFetch(ws, requestId, message, controller.signal);
|
|
1234
|
+
} finally {
|
|
1235
|
+
this.fetchControllers.delete(requestId);
|
|
1236
|
+
}
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
764
1240
|
const resolvedUrl = this._resolveFetchUrl(message.url);
|
|
765
1241
|
if (!resolvedUrl) {
|
|
766
1242
|
this.sendMainMessage(ws, {
|
|
@@ -772,7 +1248,7 @@ export class MessageRouter {
|
|
|
772
1248
|
});
|
|
773
1249
|
this.logger.warn("renderer-fetch-failed", {
|
|
774
1250
|
requestId,
|
|
775
|
-
url: message.url
|
|
1251
|
+
url: this._sanitizeUrlForLogs(message.url),
|
|
776
1252
|
error: "unsupported_fetch_url"
|
|
777
1253
|
});
|
|
778
1254
|
return;
|
|
@@ -782,10 +1258,11 @@ export class MessageRouter {
|
|
|
782
1258
|
this.fetchControllers.set(requestId, controller);
|
|
783
1259
|
|
|
784
1260
|
try {
|
|
1261
|
+
const outbound = this._prepareOutgoingFetchRequest(message);
|
|
785
1262
|
const response = await fetch(resolvedUrl, {
|
|
786
|
-
method:
|
|
787
|
-
headers:
|
|
788
|
-
body:
|
|
1263
|
+
method: outbound.method,
|
|
1264
|
+
headers: outbound.headers,
|
|
1265
|
+
body: outbound.body,
|
|
789
1266
|
signal: controller.signal
|
|
790
1267
|
});
|
|
791
1268
|
|
|
@@ -814,7 +1291,7 @@ export class MessageRouter {
|
|
|
814
1291
|
requestId,
|
|
815
1292
|
status: response.status,
|
|
816
1293
|
ok: response.ok,
|
|
817
|
-
url: response.url || resolvedUrl
|
|
1294
|
+
url: this._sanitizeUrlForLogs(response.url || resolvedUrl)
|
|
818
1295
|
});
|
|
819
1296
|
} catch (error) {
|
|
820
1297
|
this.sendMainMessage(ws, {
|
|
@@ -826,7 +1303,7 @@ export class MessageRouter {
|
|
|
826
1303
|
});
|
|
827
1304
|
this.logger.warn("renderer-fetch-failed", {
|
|
828
1305
|
requestId,
|
|
829
|
-
url: resolvedUrl,
|
|
1306
|
+
url: this._sanitizeUrlForLogs(resolvedUrl),
|
|
830
1307
|
error: toErrorMessage(error)
|
|
831
1308
|
});
|
|
832
1309
|
} finally {
|
|
@@ -834,6 +1311,271 @@ export class MessageRouter {
|
|
|
834
1311
|
}
|
|
835
1312
|
}
|
|
836
1313
|
|
|
1314
|
+
async _handleTranscribeFetch(ws, requestId, message, signal) {
|
|
1315
|
+
try {
|
|
1316
|
+
const outbound = this._prepareOutgoingFetchRequest(message);
|
|
1317
|
+
const bodyBuffer = this._asBuffer(outbound.body);
|
|
1318
|
+
const contentType = this._readHeader(outbound.headers, "content-type");
|
|
1319
|
+
const boundary = this._extractMultipartBoundary(contentType);
|
|
1320
|
+
if (!boundary) {
|
|
1321
|
+
throw new Error("Missing multipart boundary for /transcribe request.");
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
const { fields, files } = this._parseMultipartBody(bodyBuffer, boundary);
|
|
1325
|
+
const file = files.find((part) => part.name === "file") || files[0];
|
|
1326
|
+
if (!file || !file.data || file.data.length === 0) {
|
|
1327
|
+
throw new Error("Missing audio file in /transcribe request.");
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const authToken = await this._resolveTranscriptionAuthToken();
|
|
1331
|
+
if (!authToken) {
|
|
1332
|
+
throw new Error("Dictation requires ChatGPT authentication in Codex.");
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
const model = process.env.CODEX_WEBSTRAP_TRANSCRIBE_MODEL || "gpt-4o-mini-transcribe";
|
|
1336
|
+
const form = new FormData();
|
|
1337
|
+
form.append("model", model);
|
|
1338
|
+
if (typeof fields.language === "string" && fields.language.trim().length > 0) {
|
|
1339
|
+
form.append("language", fields.language.trim());
|
|
1340
|
+
}
|
|
1341
|
+
form.append(
|
|
1342
|
+
"file",
|
|
1343
|
+
new Blob([file.data], { type: file.contentType || "audio/webm" }),
|
|
1344
|
+
file.filename || "codex.webm"
|
|
1345
|
+
);
|
|
1346
|
+
|
|
1347
|
+
const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
1348
|
+
method: "POST",
|
|
1349
|
+
headers: {
|
|
1350
|
+
Authorization: `Bearer ${authToken}`
|
|
1351
|
+
},
|
|
1352
|
+
body: form,
|
|
1353
|
+
signal
|
|
1354
|
+
});
|
|
1355
|
+
const responseText = await response.text();
|
|
1356
|
+
|
|
1357
|
+
let bodyJsonString = responseText;
|
|
1358
|
+
if (response.ok) {
|
|
1359
|
+
const parsed = safeJsonParse(responseText);
|
|
1360
|
+
if (parsed && typeof parsed === "object" && typeof parsed.text === "string") {
|
|
1361
|
+
bodyJsonString = JSON.stringify({ text: parsed.text });
|
|
1362
|
+
} else {
|
|
1363
|
+
bodyJsonString = JSON.stringify({ text: "" });
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
this.sendMainMessage(ws, {
|
|
1368
|
+
type: "fetch-response",
|
|
1369
|
+
requestId,
|
|
1370
|
+
responseType: "success",
|
|
1371
|
+
status: response.status,
|
|
1372
|
+
headers: {
|
|
1373
|
+
"content-type": response.headers.get("content-type") || "application/json"
|
|
1374
|
+
},
|
|
1375
|
+
bodyJsonString
|
|
1376
|
+
});
|
|
1377
|
+
} catch (error) {
|
|
1378
|
+
this.sendMainMessage(ws, {
|
|
1379
|
+
type: "fetch-response",
|
|
1380
|
+
requestId,
|
|
1381
|
+
responseType: "error",
|
|
1382
|
+
status: 0,
|
|
1383
|
+
error: toErrorMessage(error)
|
|
1384
|
+
});
|
|
1385
|
+
this.logger.warn("transcribe-fetch-failed", {
|
|
1386
|
+
requestId,
|
|
1387
|
+
error: toErrorMessage(error)
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
_prepareOutgoingFetchRequest(message) {
|
|
1393
|
+
const method = message?.method || "GET";
|
|
1394
|
+
const headers = message?.headers && typeof message.headers === "object"
|
|
1395
|
+
? { ...message.headers }
|
|
1396
|
+
: {};
|
|
1397
|
+
let body = message?.body;
|
|
1398
|
+
|
|
1399
|
+
const base64Marker = this._readHeader(headers, "x-codex-base64");
|
|
1400
|
+
if (base64Marker === "1") {
|
|
1401
|
+
this._deleteHeader(headers, "x-codex-base64");
|
|
1402
|
+
if (typeof body !== "string") {
|
|
1403
|
+
throw new Error("X-Codex-Base64 fetch body must be a base64 string.");
|
|
1404
|
+
}
|
|
1405
|
+
body = Buffer.from(body, "base64");
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
return {
|
|
1409
|
+
method,
|
|
1410
|
+
headers,
|
|
1411
|
+
body
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
_asBuffer(body) {
|
|
1416
|
+
if (Buffer.isBuffer(body)) {
|
|
1417
|
+
return body;
|
|
1418
|
+
}
|
|
1419
|
+
if (body instanceof Uint8Array) {
|
|
1420
|
+
return Buffer.from(body);
|
|
1421
|
+
}
|
|
1422
|
+
if (typeof body === "string") {
|
|
1423
|
+
return Buffer.from(body, "utf8");
|
|
1424
|
+
}
|
|
1425
|
+
if (body == null) {
|
|
1426
|
+
return Buffer.alloc(0);
|
|
1427
|
+
}
|
|
1428
|
+
return Buffer.from(String(body), "utf8");
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
_extractMultipartBoundary(contentType) {
|
|
1432
|
+
if (typeof contentType !== "string") {
|
|
1433
|
+
return null;
|
|
1434
|
+
}
|
|
1435
|
+
const match = contentType.match(/boundary=([^;]+)/i);
|
|
1436
|
+
if (!match || !match[1]) {
|
|
1437
|
+
return null;
|
|
1438
|
+
}
|
|
1439
|
+
return match[1].trim().replace(/^"|"$/g, "");
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
_parseMultipartBody(body, boundary) {
|
|
1443
|
+
const files = [];
|
|
1444
|
+
const fields = {};
|
|
1445
|
+
const delimiter = Buffer.from(`--${boundary}`);
|
|
1446
|
+
const partSeparator = Buffer.from("\r\n\r\n");
|
|
1447
|
+
|
|
1448
|
+
let cursor = 0;
|
|
1449
|
+
for (;;) {
|
|
1450
|
+
const start = body.indexOf(delimiter, cursor);
|
|
1451
|
+
if (start < 0) {
|
|
1452
|
+
break;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
cursor = start + delimiter.length;
|
|
1456
|
+
if (body[cursor] === 45 && body[cursor + 1] === 45) {
|
|
1457
|
+
break;
|
|
1458
|
+
}
|
|
1459
|
+
if (body[cursor] === 13 && body[cursor + 1] === 10) {
|
|
1460
|
+
cursor += 2;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
const headerEnd = body.indexOf(partSeparator, cursor);
|
|
1464
|
+
if (headerEnd < 0) {
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
const headersText = body.slice(cursor, headerEnd).toString("utf8");
|
|
1469
|
+
const contentStart = headerEnd + partSeparator.length;
|
|
1470
|
+
const nextBoundary = body.indexOf(Buffer.from(`\r\n--${boundary}`), contentStart);
|
|
1471
|
+
const contentEnd = nextBoundary >= 0 ? nextBoundary : body.length;
|
|
1472
|
+
const content = body.slice(contentStart, contentEnd);
|
|
1473
|
+
|
|
1474
|
+
const headers = {};
|
|
1475
|
+
for (const line of headersText.split("\r\n")) {
|
|
1476
|
+
const splitAt = line.indexOf(":");
|
|
1477
|
+
if (splitAt <= 0) {
|
|
1478
|
+
continue;
|
|
1479
|
+
}
|
|
1480
|
+
const key = line.slice(0, splitAt).trim().toLowerCase();
|
|
1481
|
+
const value = line.slice(splitAt + 1).trim();
|
|
1482
|
+
headers[key] = value;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
const disposition = headers["content-disposition"] || "";
|
|
1486
|
+
const nameMatch = disposition.match(/\bname="([^"]+)"/i);
|
|
1487
|
+
const filenameMatch = disposition.match(/\bfilename="([^"]+)"/i);
|
|
1488
|
+
const name = nameMatch ? nameMatch[1] : null;
|
|
1489
|
+
const filename = filenameMatch ? filenameMatch[1] : null;
|
|
1490
|
+
const part = {
|
|
1491
|
+
name,
|
|
1492
|
+
filename,
|
|
1493
|
+
contentType: headers["content-type"] || null,
|
|
1494
|
+
data: content
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
if (filename) {
|
|
1498
|
+
files.push(part);
|
|
1499
|
+
} else if (name) {
|
|
1500
|
+
fields[name] = content.toString("utf8");
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
cursor = contentEnd + 2;
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
return { fields, files };
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
async _resolveTranscriptionAuthToken() {
|
|
1510
|
+
if (!this.appServer) {
|
|
1511
|
+
return null;
|
|
1512
|
+
}
|
|
1513
|
+
try {
|
|
1514
|
+
const response = await this.appServer.sendRequest("getAuthStatus", {
|
|
1515
|
+
includeToken: true,
|
|
1516
|
+
refreshToken: true
|
|
1517
|
+
}, {
|
|
1518
|
+
timeoutMs: 10_000
|
|
1519
|
+
});
|
|
1520
|
+
const token = response?.result?.authToken;
|
|
1521
|
+
return typeof token === "string" && token.trim().length > 0 ? token.trim() : null;
|
|
1522
|
+
} catch (error) {
|
|
1523
|
+
this.logger.warn("Failed to resolve transcription auth token", {
|
|
1524
|
+
error: toErrorMessage(error)
|
|
1525
|
+
});
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
_readHeader(headers, name) {
|
|
1531
|
+
if (!headers || typeof headers !== "object") {
|
|
1532
|
+
return null;
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const target = String(name).toLowerCase();
|
|
1536
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
1537
|
+
if (String(key).toLowerCase() !== target) {
|
|
1538
|
+
continue;
|
|
1539
|
+
}
|
|
1540
|
+
if (Array.isArray(value)) {
|
|
1541
|
+
return value.length > 0 ? String(value[0]) : "";
|
|
1542
|
+
}
|
|
1543
|
+
return value == null ? null : String(value);
|
|
1544
|
+
}
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
_deleteHeader(headers, name) {
|
|
1549
|
+
if (!headers || typeof headers !== "object") {
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1553
|
+
const target = String(name).toLowerCase();
|
|
1554
|
+
for (const key of Object.keys(headers)) {
|
|
1555
|
+
if (String(key).toLowerCase() === target) {
|
|
1556
|
+
delete headers[key];
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
_sanitizeUrlForLogs(url) {
|
|
1562
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
1563
|
+
return null;
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
// Drop query params and fragments from logs to avoid leaking tokens/user data.
|
|
1567
|
+
const withoutQuery = url.split("?")[0]?.split("#")[0] ?? "";
|
|
1568
|
+
if (withoutQuery.startsWith("http://") || withoutQuery.startsWith("https://")) {
|
|
1569
|
+
try {
|
|
1570
|
+
const parsed = new URL(withoutQuery);
|
|
1571
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
1572
|
+
} catch {
|
|
1573
|
+
return withoutQuery;
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
return withoutQuery;
|
|
1577
|
+
}
|
|
1578
|
+
|
|
837
1579
|
_resolveFetchUrl(url) {
|
|
838
1580
|
if (typeof url !== "string" || url.length === 0) {
|
|
839
1581
|
return null;
|
|
@@ -1883,7 +2625,7 @@ export class MessageRouter {
|
|
|
1883
2625
|
|
|
1884
2626
|
async _runCommand(command, args, { timeoutMs = 5_000, allowNonZero = false, cwd = process.cwd() } = {}) {
|
|
1885
2627
|
return new Promise((resolve) => {
|
|
1886
|
-
const child =
|
|
2628
|
+
const child = childSpawn(command, args, {
|
|
1887
2629
|
cwd,
|
|
1888
2630
|
env: process.env,
|
|
1889
2631
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -1958,7 +2700,7 @@ export class MessageRouter {
|
|
|
1958
2700
|
requestId,
|
|
1959
2701
|
status,
|
|
1960
2702
|
ok: status >= 200 && status < 300,
|
|
1961
|
-
url
|
|
2703
|
+
url: this._sanitizeUrlForLogs(url)
|
|
1962
2704
|
});
|
|
1963
2705
|
}
|
|
1964
2706
|
|
|
@@ -2471,7 +3213,7 @@ export class MessageRouter {
|
|
|
2471
3213
|
return;
|
|
2472
3214
|
}
|
|
2473
3215
|
|
|
2474
|
-
const child =
|
|
3216
|
+
const child = childSpawn("open", [url], {
|
|
2475
3217
|
stdio: ["ignore", "ignore", "ignore"],
|
|
2476
3218
|
detached: true
|
|
2477
3219
|
});
|