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 +49 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/src/banner.js +47 -0
- package/dist/src/cli.js +113 -0
- package/dist/src/codexRun.js +24 -0
- package/dist/src/index.js +190 -0
- package/dist/src/loop-runner.js +164 -0
- package/dist/src/paths.js +19 -0
- package/dist/ui/public/index.html +691 -0
- package/dist/ui/public/milhouse.gif +0 -0
- package/dist/ui/public/millhouse.gif +0 -0
- package/dist/ui/public/millhouse.png +0 -0
- package/dist/ui/server.js +369 -0
- package/package.json +63 -0
- package/prompts/build.md +11 -0
- package/prompts/plan.md +11 -0
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
|
+

|
|
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
|
+
}
|
package/dist/src/cli.js
ADDED
|
@@ -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
|
+
});
|