milhouse 1.0.1

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/AGENTS.md ADDED
@@ -0,0 +1,49 @@
1
+ # Repository Guidelines
2
+
3
+ ## Project Structure
4
+
5
+ - `src/cli.ts`: npm-installed CLI entrypoint (`milhouse`) with the yellow header.
6
+ - `ui/server.ts`: Express server for the local web UI; static assets live in `ui/public/` (copied to `dist/ui/public/` on build).
7
+ - `src/loop-runner.ts`: Node-based loop engine used by the UI (plan once, then iterate until `STATUS: DONE`).
8
+ - `prompts/*.md`: Prompt templates used by the loop runner (interpolate `{{GOAL}}` and `{{PLAN_PATH}}`).
9
+ - `dist/`: Compiled output (generated; not committed).
10
+
11
+ ## Build, Test, and Development Commands
12
+
13
+ Prereqs: Node.js 18+.
14
+
15
+ ```bash
16
+ npm install # install deps
17
+ npm run typecheck # tsc --noEmit
18
+ npm run build # compile to dist/ and copy UI assets
19
+ npm run ui # build + start web UI (local-only)
20
+ npm run dev -- ui # run UI from TS via tsx
21
+ ```
22
+
23
+ Installed usage (after publish):
24
+ ```bash
25
+ npm install -g milhouse
26
+ milhouse ui
27
+ ```
28
+
29
+ ## Coding Style & Naming Conventions
30
+
31
+ - TypeScript ESM (NodeNext) with `strict` enabled.
32
+ - Match existing formatting: 2-space indentation, semicolons, double quotes.
33
+ - CLI flags use kebab-case (`--state-dir`, `--workdir`).
34
+
35
+ ## Testing Guidelines
36
+
37
+ - No dedicated test suite yet. Validate with:
38
+ - `npm run typecheck`
39
+ - `npm run ui` and verify start/stop + live logs/artifacts
40
+
41
+ ## Commit & Pull Request Guidelines
42
+
43
+ - Use Conventional Commits (`feat:`, `fix:`, `docs:`), e.g. `feat(ui): ...`.
44
+ - PRs should include: summary, local verification steps, and screenshots for UI changes.
45
+
46
+ ## Security & Configuration Tips
47
+
48
+ - Never commit secrets. `CODEX_API_KEY` is optional (can fall back to local Codex auth if configured).
49
+ - State/logs default to the OS user data directory; override with `MILHOUSE_STATE_DIR` or `milhouse ui --state-dir <path>`.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Milhouse Van Houten
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # Milhouse Van Houten
2
+
3
+ ![Milhouse](milhouse.png)
4
+
5
+ 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
+
7
+ 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
+
9
+ ## Features
10
+
11
+ * Web UI for managing Codex runs
12
+ * Ralph Wiggum-style autonomous loop system: plan once, then iterate builds until done
13
+ * Live logs via Server-Sent Events (SSE)
14
+ * Session history with status tracking
15
+ * Cross-platform folder picker (Windows, macOS, Linux)
16
+
17
+ ## Prerequisites
18
+
19
+ * Node.js 18+
20
+ * `CODEX_API_KEY` set in your environment **(optional if your Codex CLI is already authenticated)**
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ npm install -g milhouse
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Start the Web UI:
31
+
32
+ ```bash
33
+ milhouse ui
34
+ ```
35
+
36
+ This opens a local web panel at `http://127.0.0.1:4173` (falls back to a free port if busy).
37
+
38
+ ### CLI Options
39
+
40
+ ```text
41
+ milhouse ui [OPTIONS]
42
+
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
+ ```
51
+
52
+ ### Examples
53
+
54
+ ```bash
55
+ # Start with default settings
56
+ milhouse ui
57
+
58
+ # Specify a project directory
59
+ milhouse ui --workdir /path/to/project
60
+
61
+ # Use a custom port
62
+ milhouse ui --port 8080
63
+
64
+ # Don't auto-open browser
65
+ milhouse ui --no-open
66
+ ```
67
+
68
+ ## How It Works
69
+
70
+ 1. **Plan Phase**: Enter a goal in the Web UI. Milhouse generates an `IMPLEMENTATION_PLAN.md` with prioritized tasks.
71
+ 2. **Build Loop**: Milhouse iteratively executes tasks from the plan, updating progress after each iteration.
72
+ 3. **Completion**: When all tasks are done, the plan is marked `STATUS: DONE` and the loop exits.
73
+
74
+ ## Web UI Features
75
+
76
+ * **Hero Status**: Shows current run status and thread ID
77
+ * **Controls**: Start/stop runs, set goals and max iterations
78
+ * **Live Logs**: Real-time log streaming with auto-scroll
79
+ * **Artifacts**: View plan output, build logs, and implementation plan
80
+ * **Sessions**: History of all runs with status, timestamps, and durations
81
+
82
+ ## Development
83
+
84
+ ```bash
85
+ # Clone the repository
86
+ git clone https://github.com/ordinalOS/Milhouse-Van-Houten.git
87
+ cd Milhouse-Van-Houten
88
+
89
+ # Install dependencies
90
+ npm install
91
+
92
+ # Run in development mode
93
+ npm run dev -- ui
94
+
95
+ # Build for production
96
+ npm run build
97
+
98
+ # Run built version
99
+ npm run ui
100
+ ```
101
+
102
+ ## Environment Variables
103
+
104
+ * `CODEX_API_KEY`: OpenAI Codex API key (optional if using local Codex auth)
105
+ * `MILHOUSE_STATE_DIR`: Override default state directory
106
+ * `MILHOUSE_DEFAULT_WORKDIR`: Override default working directory
107
+
108
+ (Legacy env vars `MILLHOUSE_STATE_DIR` / `MILLHOUSE_DEFAULT_WORKDIR` are still supported.)
109
+
110
+ ## How to Contribute
111
+
112
+ Thank you for considering contributing to Milhouse! This repository is intentionally open-ended, and we welcome contributions of all kinds — including bug fixes, enhancements, documentation improvements, new loops, unconventional experiments, and wild ideas.
113
+
114
+ If you have something you want to try, this is the place to do it. Fork the repository, explore freely, and open a pull request when you’re ready. No idea is a bad idea. Creativity encouraged.
115
+
116
+ MIT License — see [LICENSE](LICENSE) for details.
@@ -0,0 +1,47 @@
1
+ import figlet from "figlet";
2
+ const ANSI_BRIGHT_YELLOW = "\u001B[93m";
3
+ const TRUECOLOR_YELLOW = "\u001B[38;2;255;255;0m";
4
+ const RESET = "\u001B[0m";
5
+ const BANNER_TEXT = "MILHOUSE";
6
+ const CONTACT_LINE = "github.com/ordinalOS/Milhouse-Van-Houten";
7
+ const SEPARATOR_CHAR = "─";
8
+ function useColor() {
9
+ return Boolean(process.stdout.isTTY) && process.env.NO_COLOR == null;
10
+ }
11
+ function supportsTruecolor() {
12
+ const colorterm = process.env.COLORTERM?.toLowerCase();
13
+ return colorterm === "truecolor" || colorterm === "24bit" || colorterm?.includes("truecolor") === true;
14
+ }
15
+ function rtrim(value) {
16
+ return value.replace(/\s+$/u, "");
17
+ }
18
+ function maxWidth(lines) {
19
+ return lines.reduce((acc, line) => Math.max(acc, line.length), 0);
20
+ }
21
+ function makeSeparator(width) {
22
+ if (width <= 0)
23
+ return "";
24
+ return SEPARATOR_CHAR.repeat(width);
25
+ }
26
+ export function getMilhouseHeader() {
27
+ const bannerLines = figlet
28
+ .textSync(BANNER_TEXT, { font: "ANSI Shadow" })
29
+ .trimEnd()
30
+ .split("\n")
31
+ .map(rtrim);
32
+ const allLines = [...bannerLines];
33
+ const width = maxWidth([...allLines, CONTACT_LINE]);
34
+ const sep = makeSeparator(width);
35
+ if (!useColor()) {
36
+ const combined = allLines.join("\n");
37
+ return `${combined}\n${sep}\n${CONTACT_LINE}\n${sep}\n`;
38
+ }
39
+ const truecolor = supportsTruecolor();
40
+ const color = truecolor ? TRUECOLOR_YELLOW : ANSI_BRIGHT_YELLOW;
41
+ const lineColor = color;
42
+ const coloredLines = allLines.map((line) => `${color}${line}${RESET}`);
43
+ return `${coloredLines.join("\n")}\n${lineColor}${sep}${RESET}\n${lineColor}${CONTACT_LINE}${RESET}\n${lineColor}${sep}${RESET}\n`;
44
+ }
45
+ export function printMilhouseHeader() {
46
+ process.stdout.write(getMilhouseHeader());
47
+ }
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import open from "open";
4
+ import { printMilhouseHeader } from "./banner.js";
5
+ import { startServer } from "../ui/server.js";
6
+ function printHelp() {
7
+ const help = [
8
+ "Usage:",
9
+ " milhouse ui [--workdir <path>] [--port <n>] [--host <ip>] [--state-dir <path>] [--no-open]",
10
+ "",
11
+ "Examples:",
12
+ " milhouse ui --workdir .",
13
+ " milhouse ui --port 4173",
14
+ ];
15
+ process.stdout.write(`${help.join("\n")}\n`);
16
+ }
17
+ function parseUiOptions(argv) {
18
+ let host = "127.0.0.1";
19
+ let port = 4173;
20
+ let workdir = process.cwd();
21
+ let stateDir;
22
+ let openBrowser = true;
23
+ for (let i = 0; i < argv.length; i += 1) {
24
+ const arg = argv[i];
25
+ switch (arg) {
26
+ case "--help":
27
+ case "-h":
28
+ printHelp();
29
+ process.exit(0);
30
+ case "--host": {
31
+ const value = argv[i + 1];
32
+ if (!value)
33
+ throw new Error("Missing value for --host");
34
+ host = value;
35
+ i += 1;
36
+ break;
37
+ }
38
+ case "--port":
39
+ case "-p": {
40
+ const value = argv[i + 1];
41
+ if (!value)
42
+ throw new Error("Missing value for --port");
43
+ port = Number(value);
44
+ if (!Number.isFinite(port) || port < 0 || port > 65535)
45
+ throw new Error(`Invalid port: ${value}`);
46
+ i += 1;
47
+ break;
48
+ }
49
+ case "--workdir":
50
+ case "-w": {
51
+ const value = argv[i + 1];
52
+ if (!value)
53
+ throw new Error("Missing value for --workdir");
54
+ workdir = path.resolve(value);
55
+ i += 1;
56
+ break;
57
+ }
58
+ case "--state-dir": {
59
+ const value = argv[i + 1];
60
+ if (!value)
61
+ throw new Error("Missing value for --state-dir");
62
+ stateDir = path.resolve(value);
63
+ i += 1;
64
+ break;
65
+ }
66
+ case "--open":
67
+ openBrowser = true;
68
+ break;
69
+ case "--no-open":
70
+ openBrowser = false;
71
+ break;
72
+ default:
73
+ throw new Error(`Unknown arg: ${arg}`);
74
+ }
75
+ }
76
+ return { host, port, workdir, stateDir, openBrowser };
77
+ }
78
+ async function main() {
79
+ const argv = process.argv.slice(2);
80
+ const cmd = argv[0];
81
+ if (!cmd || cmd === "--help" || cmd === "-h") {
82
+ printMilhouseHeader();
83
+ printHelp();
84
+ return;
85
+ }
86
+ if (cmd !== "ui") {
87
+ printMilhouseHeader();
88
+ throw new Error(`Unknown command: ${cmd}`);
89
+ }
90
+ const options = parseUiOptions(argv.slice(1));
91
+ printMilhouseHeader();
92
+ const { url } = await startServer({
93
+ host: options.host,
94
+ port: options.port,
95
+ defaultWorkdir: options.workdir,
96
+ stateBaseDir: options.stateDir,
97
+ });
98
+ process.stdout.write(`Milhouse panel running at ${url}\n`);
99
+ if (options.openBrowser) {
100
+ try {
101
+ await open(url);
102
+ }
103
+ catch (err) {
104
+ const message = err instanceof Error ? err.message : String(err);
105
+ process.stderr.write(`Failed to open browser: ${message}\n`);
106
+ }
107
+ }
108
+ }
109
+ main().catch((err) => {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ process.stderr.write(`${message}\n`);
112
+ process.exit(1);
113
+ });
@@ -0,0 +1,24 @@
1
+ import { Codex } from "@openai/codex-sdk";
2
+ export async function runTurn(options) {
3
+ const apiKey = process.env.CODEX_API_KEY;
4
+ const codex = apiKey ? new Codex({ apiKey }) : new Codex();
5
+ const threadOptions = {
6
+ workingDirectory: options.workdir,
7
+ sandboxMode: options.sandboxMode,
8
+ approvalPolicy: options.approvalPolicy,
9
+ skipGitRepoCheck: options.skipGitRepoCheck ?? true,
10
+ additionalDirectories: options.additionalDirectories,
11
+ networkAccessEnabled: options.networkAccessEnabled,
12
+ webSearchEnabled: options.webSearchEnabled,
13
+ };
14
+ const thread = options.threadId
15
+ ? codex.resumeThread(options.threadId, threadOptions)
16
+ : codex.startThread(threadOptions);
17
+ const turn = await thread.run(options.promptText);
18
+ return {
19
+ threadId: thread.id,
20
+ finalResponse: turn.finalResponse,
21
+ items: turn.items,
22
+ usage: turn.usage,
23
+ };
24
+ }
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { Codex } from "@openai/codex-sdk";
4
+ function printUsage() {
5
+ const usage = [
6
+ "Usage: npm run codex -- \"<prompt>\" [--thread <id>] [--workdir <path>] [--json] [--stream] [--log-file <path>]",
7
+ " or: npm run codex -- --prompt-file <path> [--thread <id>] [--workdir <path>] [--json] [--stream] [--log-file <path>]",
8
+ "",
9
+ "Options:",
10
+ " --prompt-file <path> Read prompt from file",
11
+ " -t, --thread <id> Resume an existing Codex thread",
12
+ " -w, --workdir <path> Working directory for the agent (defaults to current)",
13
+ " --stream Stream Codex events to stderr",
14
+ " --log-file <path> Write final JSON to a file",
15
+ " --json Print JSON (response, items, usage, threadId)",
16
+ " -h, --help Show this help text",
17
+ ];
18
+ console.log(usage.join("\n"));
19
+ }
20
+ function parseArgs(argv) {
21
+ if (argv.length === 0) {
22
+ printUsage();
23
+ process.exit(1);
24
+ }
25
+ let threadId;
26
+ let promptFile;
27
+ let workdir;
28
+ let stream = false;
29
+ let logFile;
30
+ let json = false;
31
+ const promptParts = [];
32
+ for (let i = 0; i < argv.length; i += 1) {
33
+ const arg = argv[i];
34
+ switch (arg) {
35
+ case "--prompt-file": {
36
+ const value = argv[i + 1];
37
+ if (!value) {
38
+ console.error("Missing value for --prompt-file");
39
+ process.exit(1);
40
+ }
41
+ promptFile = value;
42
+ i += 1;
43
+ break;
44
+ }
45
+ case "--log-file": {
46
+ const value = argv[i + 1];
47
+ if (!value) {
48
+ console.error("Missing value for --log-file");
49
+ process.exit(1);
50
+ }
51
+ logFile = value;
52
+ i += 1;
53
+ break;
54
+ }
55
+ case "--stream":
56
+ stream = true;
57
+ break;
58
+ case "-t":
59
+ case "--thread": {
60
+ const value = argv[i + 1];
61
+ if (!value) {
62
+ console.error("Missing value for --thread");
63
+ process.exit(1);
64
+ }
65
+ threadId = value;
66
+ i += 1;
67
+ break;
68
+ }
69
+ case "-w":
70
+ case "--workdir": {
71
+ const value = argv[i + 1];
72
+ if (!value) {
73
+ console.error("Missing value for --workdir");
74
+ process.exit(1);
75
+ }
76
+ workdir = value;
77
+ i += 1;
78
+ break;
79
+ }
80
+ case "--json":
81
+ json = true;
82
+ break;
83
+ case "-h":
84
+ case "--help":
85
+ printUsage();
86
+ process.exit(0);
87
+ default:
88
+ promptParts.push(arg);
89
+ }
90
+ }
91
+ const prompt = promptParts.join(" ").trim();
92
+ return { prompt, promptFile, threadId, workdir, stream, logFile, json };
93
+ }
94
+ async function main() {
95
+ const { prompt, promptFile, threadId, workdir, stream, logFile, json } = parseArgs(process.argv.slice(2));
96
+ const promptText = promptFile != null
97
+ ? fs.readFileSync(promptFile, "utf8")
98
+ : prompt;
99
+ if (!promptText.trim()) {
100
+ console.error("Prompt is required (inline or via --prompt-file).");
101
+ printUsage();
102
+ process.exit(1);
103
+ }
104
+ const apiKey = process.env.CODEX_API_KEY;
105
+ const threadOptions = {
106
+ workingDirectory: workdir ?? process.cwd(),
107
+ approvalPolicy: "never",
108
+ sandboxMode: "danger-full-access",
109
+ skipGitRepoCheck: true,
110
+ };
111
+ const codex = apiKey ? new Codex({ apiKey }) : new Codex();
112
+ const thread = threadId
113
+ ? codex.resumeThread(threadId, threadOptions)
114
+ : codex.startThread(threadOptions);
115
+ if (stream) {
116
+ const streamed = await thread.runStreamed(promptText);
117
+ let lastAgent = "";
118
+ let usage = null;
119
+ const items = [];
120
+ for await (const event of streamed.events) {
121
+ // Log event summaries to stderr
122
+ if ("type" in event) {
123
+ if (event.type === "item.completed" && "item" in event && event.item?.type === "agent_message") {
124
+ const text = event.item.text ?? "";
125
+ lastAgent = text || lastAgent;
126
+ console.error(`[agent] ${text}`);
127
+ }
128
+ else if (event.type === "item.completed" && "item" in event) {
129
+ console.error(`[item.completed] ${event.item.type ?? "unknown"}`);
130
+ }
131
+ else if (event.type === "turn.completed") {
132
+ usage = event.usage ?? usage;
133
+ console.error(`[turn.completed] usage recorded`);
134
+ }
135
+ else {
136
+ console.error(`[${event.type}]`);
137
+ }
138
+ // Collect items if present
139
+ if (event.item) {
140
+ items.push(event.item);
141
+ }
142
+ }
143
+ }
144
+ const result = {
145
+ threadId: thread.id,
146
+ finalResponse: lastAgent,
147
+ items,
148
+ usage,
149
+ };
150
+ const jsonOut = JSON.stringify(result, null, 2);
151
+ if (logFile) {
152
+ fs.writeFileSync(logFile, jsonOut, "utf8");
153
+ }
154
+ if (json) {
155
+ console.log(jsonOut);
156
+ }
157
+ else {
158
+ console.log(lastAgent);
159
+ if (thread.id) {
160
+ console.error(`thread: ${thread.id}`);
161
+ }
162
+ }
163
+ return;
164
+ }
165
+ const turn = await thread.run(promptText);
166
+ const currentThreadId = thread.id;
167
+ const output = {
168
+ threadId: currentThreadId,
169
+ finalResponse: turn.finalResponse,
170
+ items: turn.items,
171
+ usage: turn.usage,
172
+ };
173
+ if (logFile) {
174
+ fs.writeFileSync(logFile, JSON.stringify(output, null, 2), "utf8");
175
+ }
176
+ if (json) {
177
+ console.log(JSON.stringify(output, null, 2));
178
+ }
179
+ else {
180
+ console.log(turn.finalResponse);
181
+ if (currentThreadId) {
182
+ console.error(`thread: ${currentThreadId}`);
183
+ }
184
+ }
185
+ }
186
+ main().catch((err) => {
187
+ const message = err instanceof Error ? err.message : String(err);
188
+ console.error(`Codex run failed: ${message}`);
189
+ process.exit(1);
190
+ });