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.
@@ -0,0 +1,164 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { runTurn } from "./codexRun.js";
5
+ function fail(message) {
6
+ process.stderr.write(`${message}\n`);
7
+ process.exit(1);
8
+ }
9
+ function parseArgs(argv) {
10
+ let goal = "";
11
+ let maxIterations = 0;
12
+ let workdir = process.cwd();
13
+ let stateDir = "";
14
+ for (let i = 0; i < argv.length; i += 1) {
15
+ const arg = argv[i];
16
+ switch (arg) {
17
+ case "--goal": {
18
+ const value = argv[i + 1];
19
+ if (!value)
20
+ fail("Missing value for --goal");
21
+ goal = value;
22
+ i += 1;
23
+ break;
24
+ }
25
+ case "--max-iterations": {
26
+ const value = argv[i + 1];
27
+ if (!value)
28
+ fail("Missing value for --max-iterations");
29
+ maxIterations = Number(value) || 0;
30
+ i += 1;
31
+ break;
32
+ }
33
+ case "--workdir": {
34
+ const value = argv[i + 1];
35
+ if (!value)
36
+ fail("Missing value for --workdir");
37
+ workdir = path.resolve(value);
38
+ i += 1;
39
+ break;
40
+ }
41
+ case "--state-dir": {
42
+ const value = argv[i + 1];
43
+ if (!value)
44
+ fail("Missing value for --state-dir");
45
+ stateDir = path.resolve(value);
46
+ i += 1;
47
+ break;
48
+ }
49
+ default:
50
+ fail(`Unknown arg: ${arg}`);
51
+ }
52
+ }
53
+ if (!goal.trim())
54
+ fail("--goal is required");
55
+ if (!stateDir)
56
+ fail("--state-dir is required");
57
+ return { goal, maxIterations, workdir, stateDir };
58
+ }
59
+ function replaceAll(template, replacements) {
60
+ let next = template;
61
+ for (const [key, value] of Object.entries(replacements)) {
62
+ next = next.replaceAll(key, value);
63
+ }
64
+ return next;
65
+ }
66
+ function renderPrompt(templatePath, outputPath, replacements) {
67
+ const template = fs.readFileSync(templatePath, "utf8");
68
+ const rendered = replaceAll(template, replacements);
69
+ fs.writeFileSync(outputPath, rendered, "utf8");
70
+ }
71
+ function readTextIfExists(p) {
72
+ try {
73
+ return fs.readFileSync(p, "utf8");
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ function findPackageRoot(fromDir) {
80
+ const candidates = [
81
+ // When running from `src/`
82
+ path.resolve(fromDir, ".."),
83
+ // When running from `dist/src/`
84
+ path.resolve(fromDir, "..", ".."),
85
+ ];
86
+ for (const candidate of candidates) {
87
+ const plan = path.join(candidate, "prompts", "plan.md");
88
+ const build = path.join(candidate, "prompts", "build.md");
89
+ if (fs.existsSync(plan) && fs.existsSync(build)) {
90
+ return candidate;
91
+ }
92
+ }
93
+ return path.resolve(fromDir, "..", "..");
94
+ }
95
+ async function main() {
96
+ const args = parseArgs(process.argv.slice(2));
97
+ fs.mkdirSync(args.stateDir, { recursive: true });
98
+ const threadFile = path.join(args.stateDir, "thread_id");
99
+ const planRendered = path.join(args.stateDir, "plan_prompt.md");
100
+ const buildRendered = path.join(args.stateDir, "build_prompt.md");
101
+ const planOut = path.join(args.stateDir, "plan_out.log");
102
+ const buildOut = path.join(args.stateDir, "build_out.log");
103
+ const planPath = path.join(args.stateDir, "IMPLEMENTATION_PLAN.md");
104
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
105
+ const packageRoot = findPackageRoot(__dirname);
106
+ const planTemplate = path.join(packageRoot, "prompts", "plan.md");
107
+ const buildTemplate = path.join(packageRoot, "prompts", "build.md");
108
+ const replacements = {
109
+ "{{GOAL}}": args.goal,
110
+ "{{PLAN_PATH}}": planPath,
111
+ };
112
+ renderPrompt(planTemplate, planRendered, replacements);
113
+ const planPromptText = fs.readFileSync(planRendered, "utf8");
114
+ const planResult = await runTurn({
115
+ promptText: planPromptText,
116
+ workdir: args.workdir,
117
+ threadId: undefined,
118
+ additionalDirectories: [args.stateDir],
119
+ sandboxMode: "workspace-write",
120
+ approvalPolicy: "never",
121
+ skipGitRepoCheck: true,
122
+ });
123
+ fs.writeFileSync(planOut, JSON.stringify(planResult, null, 2), "utf8");
124
+ if (planResult.threadId) {
125
+ fs.writeFileSync(threadFile, planResult.threadId, "utf8");
126
+ process.stdout.write(`thread: ${planResult.threadId}\n`);
127
+ }
128
+ renderPrompt(buildTemplate, buildRendered, replacements);
129
+ const buildPromptText = fs.readFileSync(buildRendered, "utf8");
130
+ let iter = 0;
131
+ while (true) {
132
+ if (args.maxIterations > 0 && iter >= args.maxIterations) {
133
+ process.stdout.write(`Reached max iterations: ${args.maxIterations}\n`);
134
+ break;
135
+ }
136
+ const tid = readTextIfExists(threadFile)?.trim();
137
+ const buildResult = await runTurn({
138
+ promptText: buildPromptText,
139
+ workdir: args.workdir,
140
+ threadId: tid || undefined,
141
+ additionalDirectories: [args.stateDir],
142
+ sandboxMode: "workspace-write",
143
+ approvalPolicy: "never",
144
+ skipGitRepoCheck: true,
145
+ });
146
+ fs.writeFileSync(buildOut, JSON.stringify(buildResult, null, 2), "utf8");
147
+ if (buildResult.threadId) {
148
+ fs.writeFileSync(threadFile, buildResult.threadId, "utf8");
149
+ process.stdout.write(`thread: ${buildResult.threadId}\n`);
150
+ }
151
+ const planText = readTextIfExists(planPath) ?? "";
152
+ if (/STATUS:\s*DONE\b/.test(planText)) {
153
+ process.stdout.write("Plan marked DONE. Exiting.\n");
154
+ break;
155
+ }
156
+ iter += 1;
157
+ process.stdout.write(`================ LOOP ${iter} ================\n`);
158
+ }
159
+ }
160
+ main().catch((err) => {
161
+ const message = err instanceof Error ? err.stack ?? err.message : String(err);
162
+ process.stderr.write(`${message}\n`);
163
+ process.exit(1);
164
+ });
@@ -0,0 +1,19 @@
1
+ import envPaths from "env-paths";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ export function resolveStateBaseDir(env = process.env) {
5
+ const override = env.MILHOUSE_STATE_DIR?.trim() || env.MILLHOUSE_STATE_DIR?.trim();
6
+ if (override)
7
+ return path.resolve(override);
8
+ const newDir = envPaths("milhouse").data;
9
+ const oldDir = envPaths("millhouse").data;
10
+ if (fs.existsSync(oldDir) && !fs.existsSync(newDir))
11
+ return oldDir;
12
+ return newDir;
13
+ }
14
+ export function resolveDefaultWorkdir(env = process.env) {
15
+ const override = env.MILHOUSE_DEFAULT_WORKDIR?.trim() || env.MILLHOUSE_DEFAULT_WORKDIR?.trim();
16
+ if (override)
17
+ return path.resolve(override);
18
+ return process.cwd();
19
+ }