ralph-codex 0.1.0
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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/bin/ralph-codex.js +56 -0
- package/package.json +49 -0
- package/src/commands/docker.js +174 -0
- package/src/commands/init.js +413 -0
- package/src/commands/plan.js +1129 -0
- package/src/commands/refine.js +1 -0
- package/src/commands/reset.js +105 -0
- package/src/commands/revise.js +571 -0
- package/src/commands/run.js +1225 -0
- package/src/commands/view.js +695 -0
- package/src/ui/terminal.js +137 -0
- package/templates/ralph.config.yml +53 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import enquirer from "enquirer";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
|
|
6
|
+
const { AutoComplete, Confirm, Input, Toggle } = enquirer;
|
|
7
|
+
|
|
8
|
+
const root = process.cwd();
|
|
9
|
+
const argv = process.argv.slice(2);
|
|
10
|
+
|
|
11
|
+
let force = false;
|
|
12
|
+
let configPath = null;
|
|
13
|
+
let updateGitignore = true;
|
|
14
|
+
|
|
15
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
16
|
+
const arg = argv[i];
|
|
17
|
+
if (arg === "--force") {
|
|
18
|
+
force = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (arg === "--config") {
|
|
22
|
+
configPath = argv[i + 1];
|
|
23
|
+
i += 1;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (arg === "--no-gitignore") {
|
|
27
|
+
updateGitignore = false;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
const templatePath = path.join(
|
|
34
|
+
__dirname,
|
|
35
|
+
"..",
|
|
36
|
+
"..",
|
|
37
|
+
"templates",
|
|
38
|
+
"ralph.config.yml",
|
|
39
|
+
);
|
|
40
|
+
const targetPath = configPath
|
|
41
|
+
? path.resolve(root, configPath)
|
|
42
|
+
: path.join(root, "ralph.config.yml");
|
|
43
|
+
|
|
44
|
+
async function confirmOverwrite() {
|
|
45
|
+
const confirm = new Confirm({
|
|
46
|
+
name: "overwrite",
|
|
47
|
+
message: `Overwrite existing ${path.relative(root, targetPath)}?`,
|
|
48
|
+
initial: false,
|
|
49
|
+
});
|
|
50
|
+
return confirm.run();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function formatYamlValue(value) {
|
|
54
|
+
if (value === null || value === undefined || value === "") return "null";
|
|
55
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
56
|
+
if (typeof value === "number") return String(value);
|
|
57
|
+
return JSON.stringify(String(value));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function setYamlValue(content, key, value, indent = " ") {
|
|
61
|
+
const pattern = new RegExp(`^${indent}${key}:\\s*[^#]*?(\\s*#.*)?$`, "m");
|
|
62
|
+
if (!pattern.test(content)) return content;
|
|
63
|
+
return content.replace(pattern, (match, comment = "") => {
|
|
64
|
+
return `${indent}${key}: ${formatYamlValue(value)}${comment || ""}`;
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function removeTopLevelSection(content, section) {
|
|
69
|
+
const lines = content.split(/\r?\n/);
|
|
70
|
+
const topLevel = (line) => /^[A-Za-z_][A-Za-z0-9_-]*:\s*$/.test(line);
|
|
71
|
+
let start = -1;
|
|
72
|
+
let end = lines.length;
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
75
|
+
if (lines[i].trim() === `${section}:`) {
|
|
76
|
+
start = i;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (start === -1) return content;
|
|
82
|
+
|
|
83
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
84
|
+
if (topLevel(lines[i]) && !lines[i].startsWith(" ")) {
|
|
85
|
+
end = i;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const before = lines.slice(0, start);
|
|
91
|
+
const after = lines.slice(end);
|
|
92
|
+
while (
|
|
93
|
+
before.length > 0 &&
|
|
94
|
+
before[before.length - 1].trim() === "" &&
|
|
95
|
+
after.length > 0 &&
|
|
96
|
+
after[0].trim() === ""
|
|
97
|
+
) {
|
|
98
|
+
before.pop();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return [...before, ...after].join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function promptOptionalInput(message) {
|
|
105
|
+
const input = new Input({
|
|
106
|
+
name: "value",
|
|
107
|
+
message,
|
|
108
|
+
});
|
|
109
|
+
const value = await input.run();
|
|
110
|
+
const trimmed = String(value || "").trim();
|
|
111
|
+
return trimmed.length > 0 ? trimmed : null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function promptAutoComplete(message, choices, initial = 0, options = {}) {
|
|
115
|
+
const prompt = new AutoComplete({
|
|
116
|
+
name: "choice",
|
|
117
|
+
message,
|
|
118
|
+
choices,
|
|
119
|
+
initial,
|
|
120
|
+
limit: Math.min(choices.length, 12),
|
|
121
|
+
...options,
|
|
122
|
+
});
|
|
123
|
+
return prompt.run();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function promptModelChoice() {
|
|
127
|
+
const choices = [
|
|
128
|
+
{
|
|
129
|
+
name: "unset",
|
|
130
|
+
message: "unset (null)",
|
|
131
|
+
value: null,
|
|
132
|
+
hint: "Use the Codex default",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: "gpt-5.2-codex",
|
|
136
|
+
message: "gpt-5.2-codex",
|
|
137
|
+
value: "gpt-5.2-codex",
|
|
138
|
+
hint: "Recommended: most advanced agentic coding model.",
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: "gpt-5.1-codex-mini",
|
|
142
|
+
message: "gpt-5.1-codex-mini",
|
|
143
|
+
value: "gpt-5.1-codex-mini",
|
|
144
|
+
hint: "Recommended: smaller, cost-effective GPT-5.1-Codex.",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "gpt-5.1-codex-max",
|
|
148
|
+
message: "gpt-5.1-codex-max",
|
|
149
|
+
value: "gpt-5.1-codex-max",
|
|
150
|
+
hint: "Optimized for long-horizon, agentic coding tasks.",
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: "gpt-5.2",
|
|
154
|
+
message: "gpt-5.2",
|
|
155
|
+
value: "gpt-5.2",
|
|
156
|
+
hint: "Best general agentic model across domains.",
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
name: "gpt-5.1",
|
|
160
|
+
message: "gpt-5.1",
|
|
161
|
+
value: "gpt-5.1",
|
|
162
|
+
hint: "Strong general coding model (succeeded by GPT-5.2).",
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
name: "gpt-5.1-codex",
|
|
166
|
+
message: "gpt-5.1-codex",
|
|
167
|
+
value: "gpt-5.1-codex",
|
|
168
|
+
hint: "Long-running agentic coding (succeeded by GPT-5.1-Codex-Max).",
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
name: "gpt-5-codex",
|
|
172
|
+
message: "gpt-5-codex",
|
|
173
|
+
value: "gpt-5-codex",
|
|
174
|
+
hint: "Tuned for long-running agentic coding (succeeded by GPT-5.1-Codex).",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "gpt-5-codex-mini",
|
|
178
|
+
message: "gpt-5-codex-mini",
|
|
179
|
+
value: "gpt-5-codex-mini",
|
|
180
|
+
hint: "Smaller GPT-5-Codex (succeeded by GPT-5.1-Codex-Mini).",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
name: "gpt-5",
|
|
184
|
+
message: "gpt-5",
|
|
185
|
+
value: "gpt-5",
|
|
186
|
+
hint: "Reasoning model for coding (succeeded by GPT-5.1).",
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: "custom",
|
|
190
|
+
message: "custom (enter manually)",
|
|
191
|
+
value: "__custom__",
|
|
192
|
+
hint: "Enter any other model string.",
|
|
193
|
+
},
|
|
194
|
+
];
|
|
195
|
+
const choice = await promptAutoComplete(
|
|
196
|
+
"Select a Codex model (type to filter; choose custom for other models):",
|
|
197
|
+
choices,
|
|
198
|
+
0,
|
|
199
|
+
{
|
|
200
|
+
suggest: (input, list) => {
|
|
201
|
+
const term = String(input || "").toLowerCase();
|
|
202
|
+
if (!term) return list;
|
|
203
|
+
const matches = list.filter((item) => {
|
|
204
|
+
const label = String(
|
|
205
|
+
item?.message || item?.name || item?.value || ""
|
|
206
|
+
).toLowerCase();
|
|
207
|
+
return label.includes(term);
|
|
208
|
+
});
|
|
209
|
+
for (const item of list) {
|
|
210
|
+
if (item?.name === "unset" || item?.name === "custom") {
|
|
211
|
+
if (!matches.includes(item)) matches.push(item);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return matches;
|
|
215
|
+
},
|
|
216
|
+
}
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
if (choice === "__custom__") {
|
|
220
|
+
return promptOptionalInput("Custom model name (leave blank for null)");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return choice;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function collectCodexConfig() {
|
|
227
|
+
const model = await promptModelChoice();
|
|
228
|
+
const profile = await promptOptionalInput(
|
|
229
|
+
"Codex CLI profile (optional; leave blank to use Codex default)",
|
|
230
|
+
);
|
|
231
|
+
const fullAuto = await new Toggle({
|
|
232
|
+
name: "full_auto",
|
|
233
|
+
message: "Enable full_auto? (workspace-write + on-request)",
|
|
234
|
+
enabled: "Yes",
|
|
235
|
+
disabled: "No",
|
|
236
|
+
initial: false,
|
|
237
|
+
}).run();
|
|
238
|
+
|
|
239
|
+
const sandbox = await promptAutoComplete("Sandbox mode:", [
|
|
240
|
+
{
|
|
241
|
+
name: "unset",
|
|
242
|
+
message: "unset (null)",
|
|
243
|
+
value: null,
|
|
244
|
+
hint: "Use the Codex default",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
name: "read-only",
|
|
248
|
+
message: "read-only",
|
|
249
|
+
value: "read-only",
|
|
250
|
+
hint: "Safest; no writes allowed.",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
name: "workspace-write",
|
|
254
|
+
message: "workspace-write",
|
|
255
|
+
value: "workspace-write",
|
|
256
|
+
hint: "Recommended; allow repo writes only.",
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "danger-full-access",
|
|
260
|
+
message: "danger-full-access",
|
|
261
|
+
value: "danger-full-access",
|
|
262
|
+
hint: "No guardrails; full system access.",
|
|
263
|
+
},
|
|
264
|
+
]);
|
|
265
|
+
|
|
266
|
+
const askForApproval = await promptAutoComplete("Approval policy:", [
|
|
267
|
+
{
|
|
268
|
+
name: "unset",
|
|
269
|
+
message: "unset (null)",
|
|
270
|
+
value: null,
|
|
271
|
+
hint: "Use the Codex default",
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
name: "untrusted",
|
|
275
|
+
message: "untrusted",
|
|
276
|
+
value: "untrusted",
|
|
277
|
+
hint: "Prompt often for approvals.",
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
name: "on-failure",
|
|
281
|
+
message: "on-failure",
|
|
282
|
+
value: "on-failure",
|
|
283
|
+
hint: "Prompt only on errors.",
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
name: "on-request",
|
|
287
|
+
message: "on-request",
|
|
288
|
+
value: "on-request",
|
|
289
|
+
hint: "Prompt for risky operations.",
|
|
290
|
+
},
|
|
291
|
+
{
|
|
292
|
+
name: "never",
|
|
293
|
+
message: "never",
|
|
294
|
+
value: "never",
|
|
295
|
+
hint: "Never prompt for approvals.",
|
|
296
|
+
},
|
|
297
|
+
]);
|
|
298
|
+
|
|
299
|
+
const modelReasoningEffort = await promptAutoComplete(
|
|
300
|
+
"Model reasoning effort:",
|
|
301
|
+
[
|
|
302
|
+
{
|
|
303
|
+
name: "unset",
|
|
304
|
+
message: "unset (null)",
|
|
305
|
+
value: null,
|
|
306
|
+
hint: "Use the Codex default",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "low",
|
|
310
|
+
message: "low",
|
|
311
|
+
value: "low",
|
|
312
|
+
hint: "Faster, less thorough reasoning.",
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
name: "medium",
|
|
316
|
+
message: "medium",
|
|
317
|
+
value: "medium",
|
|
318
|
+
hint: "Default balance of speed + depth.",
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
name: "high",
|
|
322
|
+
message: "high",
|
|
323
|
+
value: "high",
|
|
324
|
+
hint: "Deeper reasoning, slower.",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "xhigh",
|
|
328
|
+
message: "xhigh",
|
|
329
|
+
value: "xhigh",
|
|
330
|
+
hint: "Maximum depth, slowest.",
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
2,
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
model,
|
|
338
|
+
profile,
|
|
339
|
+
sandbox,
|
|
340
|
+
ask_for_approval: askForApproval,
|
|
341
|
+
full_auto: fullAuto,
|
|
342
|
+
model_reasoning_effort: modelReasoningEffort,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function main() {
|
|
347
|
+
if (!fs.existsSync(templatePath)) {
|
|
348
|
+
console.error("Missing template ralph.config.yml in package.");
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (fs.existsSync(targetPath) && !force) {
|
|
353
|
+
const ok = await confirmOverwrite();
|
|
354
|
+
if (!ok) {
|
|
355
|
+
process.stdout.write("Aborted.\n");
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const content = fs.readFileSync(templatePath, "utf8");
|
|
361
|
+
const codexConfig = await collectCodexConfig();
|
|
362
|
+
const useDocker = await new Toggle({
|
|
363
|
+
name: "use_docker",
|
|
364
|
+
message: "Use Docker for the loop? (adds a docker section)",
|
|
365
|
+
enabled: "Yes",
|
|
366
|
+
disabled: "No",
|
|
367
|
+
initial: false,
|
|
368
|
+
}).run();
|
|
369
|
+
|
|
370
|
+
let updated = content;
|
|
371
|
+
updated = setYamlValue(updated, "model", codexConfig.model);
|
|
372
|
+
updated = setYamlValue(updated, "profile", codexConfig.profile);
|
|
373
|
+
updated = setYamlValue(updated, "sandbox", codexConfig.sandbox);
|
|
374
|
+
updated = setYamlValue(
|
|
375
|
+
updated,
|
|
376
|
+
"ask_for_approval",
|
|
377
|
+
codexConfig.ask_for_approval,
|
|
378
|
+
);
|
|
379
|
+
updated = setYamlValue(updated, "full_auto", codexConfig.full_auto);
|
|
380
|
+
updated = setYamlValue(
|
|
381
|
+
updated,
|
|
382
|
+
"model_reasoning_effort",
|
|
383
|
+
codexConfig.model_reasoning_effort,
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
if (useDocker) {
|
|
387
|
+
updated = setYamlValue(updated, "enabled", true);
|
|
388
|
+
} else {
|
|
389
|
+
updated = removeTopLevelSection(updated, "docker");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
fs.writeFileSync(targetPath, `${updated.trimEnd()}\n`, "utf8");
|
|
393
|
+
process.stdout.write(
|
|
394
|
+
`Success: configured ${path.relative(root, targetPath)}\n`,
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
if (updateGitignore) {
|
|
398
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
399
|
+
const entry = ".ralph";
|
|
400
|
+
if (fs.existsSync(gitignorePath)) {
|
|
401
|
+
const gitignore = fs.readFileSync(gitignorePath, "utf8");
|
|
402
|
+
if (!gitignore.split(/\r?\n/).includes(entry)) {
|
|
403
|
+
fs.appendFileSync(gitignorePath, `\n${entry}\n`, "utf8");
|
|
404
|
+
process.stdout.write("Updated .gitignore with .ralph\n");
|
|
405
|
+
}
|
|
406
|
+
} else {
|
|
407
|
+
fs.writeFileSync(gitignorePath, `${entry}\n`, "utf8");
|
|
408
|
+
process.stdout.write("Created .gitignore with .ralph\n");
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
void main();
|