milhouse 1.0.1 → 1.0.4
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/README.md +40 -18
- package/dist/ui/public/index.html +34 -3
- package/dist/ui/server.js +152 -54
- package/package.json +1 -1
- package/dist/ui/public/millhouse.gif +0 -0
- package/dist/ui/public/millhouse.png +0 -0
package/README.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
# Milhouse Van Houten
|
|
2
|
-
|
|
3
|
-

|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
# Milhouse Van Houten
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[milhouse on npm](https://www.npmjs.com/package/milhouse)
|
|
6
|
+
|
|
7
|
+
Milhouse Van Houten is a chad pair programmer that provides a lightweight web UI for running Codex threads, starting with a quick planning phase and iterating through builds until the job is done. Inspired by a Ralph Wiggum-style autonomous loop, Milhouse focuses on simplicity, approachability, and making experimentation fun and easy to modify.
|
|
6
8
|
|
|
7
9
|
This repository contains the **bare-bones** version of Milhouse. It represents the core foundation, with many new features, experiments, and feedback loops currently in the pipeline.
|
|
8
10
|
|
|
@@ -19,14 +21,32 @@ This repository contains the **bare-bones** version of Milhouse. It represents t
|
|
|
19
21
|
* Node.js 18+
|
|
20
22
|
* `CODEX_API_KEY` set in your environment **(optional if your Codex CLI is already authenticated)**
|
|
21
23
|
|
|
22
|
-
## Install
|
|
23
|
-
|
|
24
|
+
## Install
|
|
25
|
+
|
|
24
26
|
```bash
|
|
25
27
|
npm install -g milhouse
|
|
26
28
|
```
|
|
27
|
-
|
|
28
|
-
##
|
|
29
|
-
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
1. Authenticate Codex (either works):
|
|
33
|
+
- Set `CODEX_API_KEY`, or
|
|
34
|
+
- Use local Codex auth if your Codex CLI is already authenticated.
|
|
35
|
+
|
|
36
|
+
Example (PowerShell):
|
|
37
|
+
|
|
38
|
+
```powershell
|
|
39
|
+
$env:CODEX_API_KEY="..."
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. Launch the UI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
milhouse ui
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
30
50
|
Start the Web UI:
|
|
31
51
|
|
|
32
52
|
```bash
|
|
@@ -40,14 +60,16 @@ This opens a local web panel at `http://127.0.0.1:4173` (falls back to a free po
|
|
|
40
60
|
```text
|
|
41
61
|
milhouse ui [OPTIONS]
|
|
42
62
|
|
|
43
|
-
Options:
|
|
44
|
-
--host <ip> Server host (default: 127.0.0.1)
|
|
45
|
-
--port <n>, -p <n> Server port (default: 4173)
|
|
46
|
-
--workdir <path>, -w Working directory for Codex (default: current directory)
|
|
47
|
-
--state-dir <path> State/logs directory (default: OS user data directory)
|
|
48
|
-
--no-open Don't auto-open browser
|
|
49
|
-
--help, -h Show help
|
|
50
|
-
```
|
|
63
|
+
Options:
|
|
64
|
+
--host <ip> Server host (default: 127.0.0.1)
|
|
65
|
+
--port <n>, -p <n> Server port (default: 4173)
|
|
66
|
+
--workdir <path>, -w Working directory for Codex (default: current directory)
|
|
67
|
+
--state-dir <path> State/logs directory (default: OS user data directory)
|
|
68
|
+
--no-open Don't auto-open browser
|
|
69
|
+
--help, -h Show help
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Note: the UI's folder picker is best-effort. If it fails, you can always paste a path into the Workdir field.
|
|
51
73
|
|
|
52
74
|
### Examples
|
|
53
75
|
|
|
@@ -466,6 +466,7 @@
|
|
|
466
466
|
|
|
467
467
|
let es;
|
|
468
468
|
let running = false;
|
|
469
|
+
let streamConnected = false;
|
|
469
470
|
|
|
470
471
|
function appendLog(line) {
|
|
471
472
|
logsEl.textContent += line + "\n";
|
|
@@ -550,7 +551,11 @@
|
|
|
550
551
|
);
|
|
551
552
|
});
|
|
552
553
|
|
|
553
|
-
|
|
554
|
+
if (parts.length === 0) {
|
|
555
|
+
artifactsEl.innerHTML = running ? "<em>Waiting for artifacts...</em>" : "<em>No artifacts yet.</em>";
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
artifactsEl.innerHTML = parts.join("");
|
|
554
559
|
}
|
|
555
560
|
|
|
556
561
|
function escapeHtml(str) {
|
|
@@ -569,7 +574,20 @@
|
|
|
569
574
|
|
|
570
575
|
function startStream() {
|
|
571
576
|
if (es) es.close();
|
|
577
|
+
streamConnected = false;
|
|
578
|
+
appendLog("[ui] Connecting to live logs...");
|
|
572
579
|
es = new EventSource("/api/events");
|
|
580
|
+
es.onopen = () => {
|
|
581
|
+
if (streamConnected) return;
|
|
582
|
+
streamConnected = true;
|
|
583
|
+
appendLog("[ui] Live logs connected.");
|
|
584
|
+
};
|
|
585
|
+
es.onerror = () => {
|
|
586
|
+
if (streamConnected) {
|
|
587
|
+
streamConnected = false;
|
|
588
|
+
appendLog("[ui] Live logs disconnected (will retry)...");
|
|
589
|
+
}
|
|
590
|
+
};
|
|
573
591
|
es.onmessage = (ev) => {
|
|
574
592
|
appendLog(ev.data);
|
|
575
593
|
const threadMatch = ev.data.match(/thread:\s*([0-9a-zA-Z-]+)/);
|
|
@@ -594,6 +612,8 @@
|
|
|
594
612
|
workdir: workdirEl.value.trim() || undefined,
|
|
595
613
|
};
|
|
596
614
|
setRunning(true);
|
|
615
|
+
setStatus("Starting...", "running");
|
|
616
|
+
appendLog("[ui] Starting run...");
|
|
597
617
|
const res = await fetch("/api/start", {
|
|
598
618
|
method: "POST",
|
|
599
619
|
headers: { "Content-Type": "application/json" },
|
|
@@ -607,6 +627,7 @@
|
|
|
607
627
|
return;
|
|
608
628
|
}
|
|
609
629
|
setStatus("Running", "running");
|
|
630
|
+
appendLog("[ui] Run started. Waiting for first logs/artifacts...");
|
|
610
631
|
fetchStatus();
|
|
611
632
|
fetchSessions();
|
|
612
633
|
};
|
|
@@ -631,10 +652,14 @@
|
|
|
631
652
|
})
|
|
632
653
|
.then((r) => r.text().then((t) => ({ ok: r.ok, text: t })))
|
|
633
654
|
.then(({ ok, text }) => {
|
|
655
|
+
if (!text.trim()) return;
|
|
634
656
|
try {
|
|
635
657
|
const data = JSON.parse(text);
|
|
636
658
|
if (data.path) workdirEl.value = data.path;
|
|
637
|
-
else if (data.error)
|
|
659
|
+
else if (data.error) {
|
|
660
|
+
if (String(data.error).toLowerCase().includes("cancel")) return;
|
|
661
|
+
appendLog("Browse error: " + data.error);
|
|
662
|
+
}
|
|
638
663
|
} catch {
|
|
639
664
|
if (!ok) appendLog("Browse error: " + text.slice(0, 200));
|
|
640
665
|
}
|
|
@@ -643,13 +668,19 @@
|
|
|
643
668
|
};
|
|
644
669
|
|
|
645
670
|
newProjectBtn.onclick = () => {
|
|
646
|
-
fetch("/api/browse", {
|
|
671
|
+
fetch("/api/browse", {
|
|
672
|
+
method: "POST",
|
|
673
|
+
headers: { "Content-Type": "application/json" },
|
|
674
|
+
body: "{}"
|
|
675
|
+
})
|
|
647
676
|
.then((r) => r.text().then((t) => ({ ok: r.ok, text: t })))
|
|
648
677
|
.then(({ ok, text }) => {
|
|
678
|
+
if (!text.trim()) return;
|
|
649
679
|
let base = "";
|
|
650
680
|
try {
|
|
651
681
|
const data = JSON.parse(text);
|
|
652
682
|
if (!data.path && data.error) {
|
|
683
|
+
if (String(data.error).toLowerCase().includes("cancel")) return;
|
|
653
684
|
appendLog("Browse error: " + data.error);
|
|
654
685
|
return;
|
|
655
686
|
}
|
package/dist/ui/server.js
CHANGED
|
@@ -45,7 +45,8 @@ function createServerContext(options) {
|
|
|
45
45
|
const sessionsFile = path.join(stateBaseDir, "sessions.json");
|
|
46
46
|
const loopRunner = resolveLoopRunner(runtimeRoot);
|
|
47
47
|
const app = express();
|
|
48
|
-
|
|
48
|
+
const jsonParser = express.json({ limit: "1mb" });
|
|
49
|
+
app.get("/favicon.ico", (_req, res) => res.status(204).end());
|
|
49
50
|
app.use(express.static(path.join(__dirname, "public")));
|
|
50
51
|
const clients = [];
|
|
51
52
|
let child = null;
|
|
@@ -102,6 +103,9 @@ function createServerContext(options) {
|
|
|
102
103
|
status: "running",
|
|
103
104
|
};
|
|
104
105
|
saveSessions((sessions) => [...sessions, currentSession]);
|
|
106
|
+
broadcast(`[milhouse] Starting run…`);
|
|
107
|
+
broadcast(`[milhouse] workdir: ${workdir}`);
|
|
108
|
+
broadcast(`[milhouse] state dir: ${stateDir}`);
|
|
105
109
|
const args = [
|
|
106
110
|
...loopRunner.args,
|
|
107
111
|
"--goal",
|
|
@@ -148,6 +152,28 @@ function createServerContext(options) {
|
|
|
148
152
|
child.kill();
|
|
149
153
|
}
|
|
150
154
|
}
|
|
155
|
+
function parseBrowseDefaultPath(req) {
|
|
156
|
+
const body = req.body;
|
|
157
|
+
if (!body)
|
|
158
|
+
return undefined;
|
|
159
|
+
if (typeof body === "string") {
|
|
160
|
+
const trimmed = body.trim();
|
|
161
|
+
if (!trimmed)
|
|
162
|
+
return undefined;
|
|
163
|
+
try {
|
|
164
|
+
const parsed = JSON.parse(trimmed);
|
|
165
|
+
return typeof parsed.defaultPath === "string" ? parsed.defaultPath : undefined;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (typeof body === "object") {
|
|
172
|
+
const maybe = body;
|
|
173
|
+
return typeof maybe.defaultPath === "string" ? maybe.defaultPath : undefined;
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
151
177
|
async function browseFolder(defaultPath) {
|
|
152
178
|
return await new Promise((resolve, reject) => {
|
|
153
179
|
const isWindows = process.platform === "win32";
|
|
@@ -156,9 +182,18 @@ function createServerContext(options) {
|
|
|
156
182
|
if (isWindows) {
|
|
157
183
|
const initialDirectory = defaultPath?.trim() ? defaultPath.replace(/'/g, "''") : "";
|
|
158
184
|
const script = `
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
185
|
+
$ErrorActionPreference = 'Stop'
|
|
186
|
+
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
|
|
187
|
+
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
|
|
188
|
+
|
|
189
|
+
function Write-Err([string]$msg) {
|
|
190
|
+
try { [Console]::Error.WriteLine($msg) } catch {}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function Try-FolderBrowserDialog([string]$initial) {
|
|
194
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
195
|
+
Add-Type -AssemblyName System.Drawing
|
|
196
|
+
Add-Type -TypeDefinition @"
|
|
162
197
|
using System;
|
|
163
198
|
using System.Runtime.InteropServices;
|
|
164
199
|
public static class User32 {
|
|
@@ -167,49 +202,83 @@ public static class User32 {
|
|
|
167
202
|
}
|
|
168
203
|
"@
|
|
169
204
|
|
|
170
|
-
$topForm = New-Object System.Windows.Forms.Form
|
|
171
|
-
$topForm.TopMost = $true
|
|
172
|
-
$topForm.ShowInTaskbar = $false
|
|
173
|
-
$topForm.FormBorderStyle = 'FixedToolWindow'
|
|
174
|
-
$topForm.StartPosition = 'Manual'
|
|
175
|
-
$topForm.Location = New-Object System.Drawing.Point(-32000, -32000)
|
|
176
|
-
$topForm.Size = New-Object System.Drawing.Size(1, 1)
|
|
177
|
-
$topForm.Opacity = 0
|
|
178
|
-
$topForm.Show()
|
|
179
|
-
$topForm.Activate()
|
|
180
|
-
$topForm.BringToFront()
|
|
181
|
-
$topForm.Focus()
|
|
182
|
-
[User32]::SetForegroundWindow($topForm.Handle) | Out-Null
|
|
183
|
-
[System.Windows.Forms.Application]::DoEvents()
|
|
205
|
+
$topForm = New-Object System.Windows.Forms.Form
|
|
206
|
+
$topForm.TopMost = $true
|
|
207
|
+
$topForm.ShowInTaskbar = $false
|
|
208
|
+
$topForm.FormBorderStyle = 'FixedToolWindow'
|
|
209
|
+
$topForm.StartPosition = 'Manual'
|
|
210
|
+
$topForm.Location = New-Object System.Drawing.Point(-32000, -32000)
|
|
211
|
+
$topForm.Size = New-Object System.Drawing.Size(1, 1)
|
|
212
|
+
$topForm.Opacity = 0
|
|
213
|
+
$topForm.Show()
|
|
214
|
+
$topForm.Activate()
|
|
215
|
+
$topForm.BringToFront()
|
|
216
|
+
$topForm.Focus()
|
|
217
|
+
[User32]::SetForegroundWindow($topForm.Handle) | Out-Null
|
|
218
|
+
[System.Windows.Forms.Application]::DoEvents()
|
|
219
|
+
|
|
220
|
+
$dlg = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
221
|
+
$dlg.Description = 'Select project folder'
|
|
222
|
+
if ($initial) { $dlg.SelectedPath = $initial }
|
|
184
223
|
|
|
185
|
-
$
|
|
186
|
-
$
|
|
187
|
-
$dlg.ValidateNames = $false
|
|
188
|
-
$dlg.CheckFileExists = $false
|
|
189
|
-
$dlg.CheckPathExists = $true
|
|
190
|
-
$dlg.FileName = 'Select Folder'
|
|
191
|
-
${initialDirectory ? `$dlg.InitialDirectory = '${initialDirectory}'` : ""}
|
|
224
|
+
$result = $dlg.ShowDialog($topForm)
|
|
225
|
+
$topForm.Close()
|
|
192
226
|
|
|
193
|
-
$result
|
|
194
|
-
|
|
195
|
-
|
|
227
|
+
if ($result -eq [System.Windows.Forms.DialogResult]::OK -and $dlg.SelectedPath) {
|
|
228
|
+
return $dlg.SelectedPath
|
|
229
|
+
}
|
|
230
|
+
return $null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function Try-ShellBrowseForFolder([string]$initial) {
|
|
234
|
+
$shell = New-Object -ComObject Shell.Application
|
|
235
|
+
$root = 0
|
|
236
|
+
if ($initial) { $root = $initial }
|
|
237
|
+
$folder = $shell.BrowseForFolder(0, 'Select project folder', 0, $root)
|
|
238
|
+
if ($folder -and $folder.Self -and $folder.Self.Path) {
|
|
239
|
+
return $folder.Self.Path
|
|
240
|
+
}
|
|
241
|
+
return $null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
$initial = ${initialDirectory ? `'${initialDirectory}'` : "''"}
|
|
246
|
+
$path = $null
|
|
247
|
+
try { $path = Try-FolderBrowserDialog $initial } catch {}
|
|
248
|
+
if (-not $path) {
|
|
249
|
+
try { $path = Try-ShellBrowseForFolder $initial } catch {}
|
|
250
|
+
}
|
|
251
|
+
if ($path) { Write-Output $path }
|
|
252
|
+
exit 0
|
|
253
|
+
} catch {
|
|
254
|
+
Write-Err ($_.Exception.Message)
|
|
255
|
+
exit 1
|
|
196
256
|
}
|
|
197
|
-
$topForm.Close()
|
|
198
257
|
`;
|
|
199
258
|
const encoded = Buffer.from(script, "utf16le").toString("base64");
|
|
200
|
-
const ps = spawn("powershell", ["-NoProfile", "-STA", "-EncodedCommand", encoded]);
|
|
259
|
+
const ps = spawn("powershell.exe", ["-NoProfile", "-STA", "-EncodedCommand", encoded]);
|
|
201
260
|
let out = "";
|
|
261
|
+
let errOut = "";
|
|
202
262
|
ps.stdout.on("data", (d) => (out += d.toString()));
|
|
203
|
-
ps.stderr.on("data", (d) =>
|
|
263
|
+
ps.stderr.on("data", (d) => {
|
|
264
|
+
const line = d.toString().trim();
|
|
265
|
+
if (line)
|
|
266
|
+
broadcast(`[browse stderr] ${line}`);
|
|
267
|
+
errOut += `${line}\n`;
|
|
268
|
+
});
|
|
204
269
|
ps.on("error", (err) => reject(err));
|
|
205
270
|
ps.on("exit", (code) => {
|
|
206
271
|
const trimmed = out.trim();
|
|
207
|
-
if (code === 0
|
|
208
|
-
resolve(trimmed);
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
272
|
+
if (code === 0)
|
|
273
|
+
return resolve(trimmed || null);
|
|
274
|
+
const errTrimmed = errOut.trim();
|
|
275
|
+
if (!trimmed && !errTrimmed)
|
|
276
|
+
return resolve(null);
|
|
277
|
+
if (!trimmed && errTrimmed.toLowerCase().includes("cancel"))
|
|
278
|
+
return resolve(null);
|
|
279
|
+
if (trimmed && fs.existsSync(trimmed))
|
|
280
|
+
return resolve(trimmed);
|
|
281
|
+
reject(new Error(errTrimmed || "Folder selection cancelled or failed"));
|
|
213
282
|
});
|
|
214
283
|
return;
|
|
215
284
|
}
|
|
@@ -219,17 +288,23 @@ $topForm.Close()
|
|
|
219
288
|
'set p to POSIX path of (choose folder with prompt "Select project folder")',
|
|
220
289
|
]);
|
|
221
290
|
let out = "";
|
|
291
|
+
let errOut = "";
|
|
222
292
|
osa.stdout.on("data", (d) => (out += d.toString()));
|
|
223
|
-
osa.stderr.on("data", (d) =>
|
|
293
|
+
osa.stderr.on("data", (d) => {
|
|
294
|
+
const line = d.toString().trim();
|
|
295
|
+
if (line)
|
|
296
|
+
broadcast(`[browse stderr] ${line}`);
|
|
297
|
+
errOut += `${line}\n`;
|
|
298
|
+
});
|
|
224
299
|
osa.on("error", (err) => reject(err));
|
|
225
300
|
osa.on("exit", (code) => {
|
|
226
301
|
const trimmed = out.trim();
|
|
227
|
-
if (code === 0
|
|
228
|
-
resolve(trimmed);
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
302
|
+
if (code === 0)
|
|
303
|
+
return resolve(trimmed || null);
|
|
304
|
+
const errTrimmed = errOut.trim();
|
|
305
|
+
if (!trimmed && errTrimmed.toLowerCase().includes("canceled"))
|
|
306
|
+
return resolve(null);
|
|
307
|
+
reject(new Error(errTrimmed || "Folder selection cancelled or failed"));
|
|
233
308
|
});
|
|
234
309
|
return;
|
|
235
310
|
}
|
|
@@ -237,8 +312,14 @@ $topForm.Close()
|
|
|
237
312
|
const run = (command, args, onMissing) => {
|
|
238
313
|
const proc = spawn(command, args);
|
|
239
314
|
let out = "";
|
|
315
|
+
let errOut = "";
|
|
240
316
|
proc.stdout.on("data", (d) => (out += d.toString()));
|
|
241
|
-
proc.stderr.on("data", (d) =>
|
|
317
|
+
proc.stderr.on("data", (d) => {
|
|
318
|
+
const line = d.toString().trim();
|
|
319
|
+
if (line)
|
|
320
|
+
broadcast(`[browse stderr] ${line}`);
|
|
321
|
+
errOut += `${line}\n`;
|
|
322
|
+
});
|
|
242
323
|
proc.on("error", (err) => {
|
|
243
324
|
if (err.code === "ENOENT")
|
|
244
325
|
onMissing();
|
|
@@ -247,10 +328,13 @@ $topForm.Close()
|
|
|
247
328
|
});
|
|
248
329
|
proc.on("exit", (code) => {
|
|
249
330
|
const trimmed = out.trim();
|
|
250
|
-
if (code === 0
|
|
251
|
-
resolve(trimmed);
|
|
252
|
-
|
|
253
|
-
|
|
331
|
+
if (code === 0)
|
|
332
|
+
return resolve(trimmed || null);
|
|
333
|
+
const errTrimmed = errOut.trim();
|
|
334
|
+
if (!trimmed && (code === 1 || code == null) && (!errTrimmed || errTrimmed.toLowerCase().includes("canceled"))) {
|
|
335
|
+
return resolve(null);
|
|
336
|
+
}
|
|
337
|
+
reject(new Error(errTrimmed || "Folder selection cancelled or failed"));
|
|
254
338
|
});
|
|
255
339
|
};
|
|
256
340
|
run("zenity", [
|
|
@@ -266,11 +350,16 @@ $topForm.Close()
|
|
|
266
350
|
}
|
|
267
351
|
async function handleBrowse(req, res) {
|
|
268
352
|
try {
|
|
269
|
-
const path = await browseFolder(req
|
|
353
|
+
const path = await browseFolder(parseBrowseDefaultPath(req));
|
|
354
|
+
if (!path)
|
|
355
|
+
return res.status(204).end();
|
|
270
356
|
res.json({ path });
|
|
271
357
|
}
|
|
272
358
|
catch (err) {
|
|
273
|
-
|
|
359
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
360
|
+
if (message.toLowerCase().includes("cancel"))
|
|
361
|
+
return res.status(204).end();
|
|
362
|
+
res.status(400).json({ error: message });
|
|
274
363
|
}
|
|
275
364
|
}
|
|
276
365
|
app.get("/api/events", (req, res) => {
|
|
@@ -307,7 +396,8 @@ $topForm.Close()
|
|
|
307
396
|
}
|
|
308
397
|
res.json({ sessions });
|
|
309
398
|
});
|
|
310
|
-
app.post("/api/
|
|
399
|
+
app.post("/api/browse", express.text({ type: "*/*", limit: "64kb" }), handleBrowse);
|
|
400
|
+
app.post("/api/start", jsonParser, (req, res) => {
|
|
311
401
|
const { goal, maxIterations = 0, workdir = defaultWorkdir, createIfMissing = true } = req.body || {};
|
|
312
402
|
if (!goal || typeof goal !== "string") {
|
|
313
403
|
return res.status(400).json({ error: "goal is required" });
|
|
@@ -320,6 +410,16 @@ $topForm.Close()
|
|
|
320
410
|
res.status(400).json({ error: err.message });
|
|
321
411
|
}
|
|
322
412
|
});
|
|
413
|
+
app.use((err, req, res, next) => {
|
|
414
|
+
if (req.method === "POST" && req.path === "/api/browse") {
|
|
415
|
+
const status = typeof err?.status === "number" ? err.status : undefined;
|
|
416
|
+
const type = typeof err?.type === "string" ? err.type : undefined;
|
|
417
|
+
if (status === 400 && type === "entity.parse.failed") {
|
|
418
|
+
return res.status(204).end();
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
next(err);
|
|
422
|
+
});
|
|
323
423
|
app.post("/api/stop", (_req, res) => {
|
|
324
424
|
stopSession();
|
|
325
425
|
res.json({ ok: true });
|
|
@@ -329,8 +429,6 @@ $topForm.Close()
|
|
|
329
429
|
return res.json({});
|
|
330
430
|
res.json(readArtifacts(currentSession.stateDir));
|
|
331
431
|
});
|
|
332
|
-
app.post("/api/browse", handleBrowse);
|
|
333
|
-
app.get("/api/browse", handleBrowse);
|
|
334
432
|
return { app, stop: stopSession };
|
|
335
433
|
}
|
|
336
434
|
function listenOnce(app, host, port) {
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|