codex-potter 0.1.31 → 0.2.0-next.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/README.md +4 -18
- package/bin/codex-potter.js +478 -188
- package/package.json +7 -11
- package/resources/potter_worker.toml +122 -0
package/README.md
CHANGED
|
@@ -1,24 +1,10 @@
|
|
|
1
1
|
# CodexPotter
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Set up CodexPotter for use from Codex:
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
|
-
|
|
6
|
+
npx codex-potter@next setup
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- macOS: Apple Silicon + Intel
|
|
12
|
-
- Linux: x86_64 + aarch64
|
|
13
|
-
- Windows: x86_64 + aarch64 (ARM64)
|
|
14
|
-
|
|
15
|
-
Packaging note:
|
|
16
|
-
|
|
17
|
-
- The `codex-potter` npm package ships a small cross-platform launcher.
|
|
18
|
-
- The native binary payload is delivered via platform-specific optional dependencies.
|
|
19
|
-
If you see an error like `Missing optional dependency codex-potter-<platform>`, reinstall
|
|
20
|
-
CodexPotter so your package manager can fetch the correct platform package.
|
|
21
|
-
|
|
22
|
-
---
|
|
23
|
-
|
|
24
|
-
See https://github.com/breezewish/CodexPotter for full usage.
|
|
9
|
+
The setup command configures the global gitignore, installs the CodexPotter
|
|
10
|
+
subagent profile, and installs the `$loop` skill.
|
package/bin/codex-potter.js
CHANGED
|
@@ -1,233 +1,523 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// Unified entry point for CodexPotter.
|
|
3
2
|
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import { fileURLToPath } from "url";
|
|
3
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import fs from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import readline from "node:readline";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
9
8
|
|
|
10
|
-
// __dirname equivalent in ESM
|
|
11
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
12
10
|
const __dirname = path.dirname(__filename);
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
11
|
+
|
|
12
|
+
const CODEXPOTTER_GITIGNORE_ENTRY = "/.codexpotter";
|
|
13
|
+
const LOOP_SKILL_COMMAND = [
|
|
14
|
+
"skills",
|
|
15
|
+
"add",
|
|
16
|
+
"--yes",
|
|
17
|
+
"-g",
|
|
18
|
+
"https://github.com/breezewish/CodexPotter/tree/v2",
|
|
19
|
+
"-a",
|
|
20
|
+
"codex",
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const colorEnabled = shouldUseColor();
|
|
24
|
+
const colors = {
|
|
25
|
+
bold: "\x1b[1m",
|
|
26
|
+
cyan: "\x1b[36m",
|
|
27
|
+
dim: "\x1b[2m",
|
|
28
|
+
green: "\x1b[32m",
|
|
29
|
+
red: "\x1b[31m",
|
|
30
|
+
reset: "\x1b[0m",
|
|
31
|
+
yellow: "\x1b[33m",
|
|
22
32
|
};
|
|
23
33
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
targetTriple = "aarch64-unknown-linux-musl";
|
|
36
|
-
break;
|
|
37
|
-
default:
|
|
38
|
-
break;
|
|
39
|
-
}
|
|
40
|
-
break;
|
|
41
|
-
case "darwin":
|
|
42
|
-
switch (arch) {
|
|
43
|
-
case "x64":
|
|
44
|
-
targetTriple = "x86_64-apple-darwin";
|
|
45
|
-
break;
|
|
46
|
-
case "arm64":
|
|
47
|
-
targetTriple = "aarch64-apple-darwin";
|
|
48
|
-
break;
|
|
49
|
-
default:
|
|
50
|
-
break;
|
|
34
|
+
async function main(args) {
|
|
35
|
+
const { command, yes, errors } = parseArgs(args);
|
|
36
|
+
if (errors.length > 0) {
|
|
37
|
+
printErrors(errors);
|
|
38
|
+
printAvailableCommands();
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (command !== "setup") {
|
|
43
|
+
if (command) {
|
|
44
|
+
printErrors([`Unknown command: ${command}`]);
|
|
51
45
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
46
|
+
printAvailableCommands();
|
|
47
|
+
return command ? 1 : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await runSetup({ yes });
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(args) {
|
|
55
|
+
const remaining = [];
|
|
56
|
+
const errors = [];
|
|
57
|
+
let yes = false;
|
|
58
|
+
|
|
59
|
+
for (const arg of args) {
|
|
60
|
+
if (arg === "--yes" || arg === "-y") {
|
|
61
|
+
yes = true;
|
|
62
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
63
|
+
return { command: null, yes, errors };
|
|
64
|
+
} else if (arg.startsWith("-")) {
|
|
65
|
+
errors.push(`Unknown option: ${arg}`);
|
|
66
|
+
} else {
|
|
67
|
+
remaining.push(arg);
|
|
63
68
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (remaining.length > 1) {
|
|
72
|
+
errors.push(`Unexpected argument: ${remaining[1]}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { command: remaining[0] ?? null, yes, errors };
|
|
67
76
|
}
|
|
68
77
|
|
|
69
|
-
|
|
70
|
-
|
|
78
|
+
function printAvailableCommands() {
|
|
79
|
+
console.log(format("CodexPotter", "bold"));
|
|
80
|
+
console.log("");
|
|
81
|
+
console.log("Usage:");
|
|
82
|
+
console.log(" codex-potter setup [--yes]");
|
|
83
|
+
console.log("");
|
|
84
|
+
console.log("Available commands:");
|
|
85
|
+
console.log(" setup Configure CodexPotter for $loop.");
|
|
71
86
|
}
|
|
72
87
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
function printErrors(errors) {
|
|
89
|
+
for (const error of errors) {
|
|
90
|
+
console.error(`${format("Error:", "red")} ${error}`);
|
|
91
|
+
}
|
|
76
92
|
}
|
|
77
93
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
);
|
|
87
|
-
const
|
|
94
|
+
async function runSetup({ yes }) {
|
|
95
|
+
const home = getHomeDir();
|
|
96
|
+
const resourcePath = path.join(
|
|
97
|
+
__dirname,
|
|
98
|
+
"..",
|
|
99
|
+
"resources",
|
|
100
|
+
"potter_worker.toml",
|
|
101
|
+
);
|
|
102
|
+
const profilePath = path.join(home, ".codex", "agents", "potter_worker.toml");
|
|
103
|
+
const profileContent = await fs.promises.readFile(resourcePath, "utf8");
|
|
88
104
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
const globalGitignore = resolveGlobalGitignore(home);
|
|
106
|
+
const gitignoreContent = await readTextIfExists(globalGitignore.path);
|
|
107
|
+
const gitignoreNeedsWrite = !gitignoreIgnoresCodexPotter(gitignoreContent);
|
|
108
|
+
|
|
109
|
+
const currentProfileContent = await readTextIfExists(profilePath);
|
|
110
|
+
const profileNeedsWrite = currentProfileContent !== profileContent;
|
|
111
|
+
const skillInstaller = resolveSkillInstaller();
|
|
112
|
+
|
|
113
|
+
printSetupPlan({
|
|
114
|
+
gitignoreNeedsWrite,
|
|
115
|
+
profileNeedsWrite,
|
|
116
|
+
profilePath,
|
|
117
|
+
skillInstaller,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
if (!yes && !(await confirm())) {
|
|
121
|
+
console.log("Setup cancelled.");
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (gitignoreNeedsWrite) {
|
|
126
|
+
const gitignoreUpdated = await ensureCodexPotterIgnored(globalGitignore.path);
|
|
127
|
+
if (gitignoreUpdated) {
|
|
128
|
+
console.log(
|
|
129
|
+
`${format("✓ Added", "green")} ${format(
|
|
130
|
+
CODEXPOTTER_GITIGNORE_ENTRY,
|
|
131
|
+
"dim",
|
|
132
|
+
)} to ${format(displayPath(globalGitignore.path), "dim")}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (profileNeedsWrite) {
|
|
138
|
+
await fs.promises.mkdir(path.dirname(profilePath), { recursive: true });
|
|
139
|
+
await fs.promises.writeFile(profilePath, profileContent, "utf8");
|
|
140
|
+
console.log(
|
|
141
|
+
`${format("✓ Added", "green")} subagent profile ${format(
|
|
142
|
+
displayPath(profilePath),
|
|
143
|
+
"dim",
|
|
144
|
+
)}`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await runLoopSkillInstaller(skillInstaller);
|
|
149
|
+
|
|
150
|
+
console.log("");
|
|
151
|
+
console.log(format("✨ CodexPotter setup complete!", "green"));
|
|
152
|
+
console.log(
|
|
153
|
+
`${format("Usage in Codex:", "bold")} ${format(
|
|
154
|
+
"$loop",
|
|
155
|
+
"cyan",
|
|
156
|
+
)} <your_instruction>`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function printSetupPlan({
|
|
161
|
+
gitignoreNeedsWrite,
|
|
162
|
+
profileNeedsWrite,
|
|
163
|
+
profilePath,
|
|
164
|
+
skillInstaller,
|
|
165
|
+
}) {
|
|
166
|
+
console.log(format("CodexPotter setup", "bold"));
|
|
167
|
+
console.log("");
|
|
168
|
+
console.log(
|
|
169
|
+
`${statusLabel(gitignoreNeedsWrite)} Ignore ${format(
|
|
170
|
+
CODEXPOTTER_GITIGNORE_ENTRY,
|
|
171
|
+
"dim",
|
|
172
|
+
)} in global gitignore`,
|
|
173
|
+
);
|
|
174
|
+
console.log(
|
|
175
|
+
`${statusLabel(profileNeedsWrite)} Add subagent profile ${format(
|
|
176
|
+
displayPath(profilePath),
|
|
177
|
+
"dim",
|
|
178
|
+
)}`,
|
|
179
|
+
);
|
|
180
|
+
console.log(
|
|
181
|
+
`${statusLabel(true)} Install / update skill: ${format(
|
|
182
|
+
skillInstaller.displayCommand,
|
|
183
|
+
"cyan",
|
|
184
|
+
)}`,
|
|
185
|
+
);
|
|
186
|
+
console.log("");
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function statusLabel(needsAction) {
|
|
190
|
+
return needsAction ? format("□ Todo:", "yellow") : format("✓ Skip:", "green");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function confirm() {
|
|
194
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
195
|
+
return confirmInteractively();
|
|
96
196
|
}
|
|
197
|
+
|
|
198
|
+
return confirmFromLineInput();
|
|
97
199
|
}
|
|
98
200
|
|
|
99
|
-
|
|
100
|
-
|
|
201
|
+
async function confirmInteractively() {
|
|
202
|
+
return await new Promise((resolve) => {
|
|
203
|
+
let selected = true;
|
|
204
|
+
let settled = false;
|
|
205
|
+
const input = process.stdin;
|
|
206
|
+
|
|
207
|
+
const render = () => {
|
|
208
|
+
process.stdout.write(`\r\x1b[2K${confirmPrompt(selected)}`);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const settle = (value) => {
|
|
212
|
+
if (settled) {
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
settled = true;
|
|
216
|
+
input.off("keypress", onKeypress);
|
|
217
|
+
input.off("close", onClose);
|
|
218
|
+
input.off("end", onClose);
|
|
219
|
+
if (input.isTTY) {
|
|
220
|
+
input.setRawMode(false);
|
|
221
|
+
}
|
|
222
|
+
process.stdout.write("\n");
|
|
223
|
+
resolve(value);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const onClose = () => {
|
|
227
|
+
settle(false);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const onKeypress = (text, key = {}) => {
|
|
231
|
+
if (key.ctrl && key.name === "c") {
|
|
232
|
+
settle(false);
|
|
233
|
+
} else if (key.name === "return") {
|
|
234
|
+
settle(selected);
|
|
235
|
+
} else if (key.name === "escape") {
|
|
236
|
+
settle(false);
|
|
237
|
+
} else if (key.name === "left" || key.name === "up") {
|
|
238
|
+
selected = true;
|
|
239
|
+
render();
|
|
240
|
+
} else if (key.name === "right" || key.name === "down") {
|
|
241
|
+
selected = false;
|
|
242
|
+
render();
|
|
243
|
+
} else if (key.name === "tab" || text === " ") {
|
|
244
|
+
selected = !selected;
|
|
245
|
+
render();
|
|
246
|
+
} else if (text && text.toLowerCase() === "y") {
|
|
247
|
+
settle(true);
|
|
248
|
+
} else if (text && text.toLowerCase() === "n") {
|
|
249
|
+
settle(false);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
readline.emitKeypressEvents(input);
|
|
254
|
+
input.on("keypress", onKeypress);
|
|
255
|
+
input.on("close", onClose);
|
|
256
|
+
input.on("end", onClose);
|
|
257
|
+
input.setRawMode(true);
|
|
258
|
+
input.resume();
|
|
259
|
+
render();
|
|
260
|
+
});
|
|
101
261
|
}
|
|
102
262
|
|
|
103
|
-
|
|
104
|
-
const
|
|
263
|
+
async function confirmFromLineInput() {
|
|
264
|
+
const answer = await new Promise((resolve) => {
|
|
265
|
+
let settled = false;
|
|
266
|
+
const rl = readline.createInterface({
|
|
267
|
+
input: process.stdin,
|
|
268
|
+
output: process.stdout,
|
|
269
|
+
terminal: false,
|
|
270
|
+
});
|
|
105
271
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
272
|
+
const settle = (value) => {
|
|
273
|
+
if (settled) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
settled = true;
|
|
277
|
+
rl.close();
|
|
278
|
+
resolve(value);
|
|
279
|
+
};
|
|
111
280
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
return updatedPath;
|
|
281
|
+
rl.on("close", () => {
|
|
282
|
+
settle("");
|
|
283
|
+
});
|
|
284
|
+
rl.question(confirmPrompt(true), settle);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
return /^(y|yes)$/i.test(String(answer).trim());
|
|
120
288
|
}
|
|
121
289
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
290
|
+
function confirmPrompt(yesSelected) {
|
|
291
|
+
const yes = yesSelected
|
|
292
|
+
? `${format("●", "green")} Yes`
|
|
293
|
+
: format("○ Yes", "dim");
|
|
294
|
+
const no = yesSelected
|
|
295
|
+
? format(" / ○ No", "dim")
|
|
296
|
+
: ` ${format("/", "dim")} ${format("●", "green")} No`;
|
|
297
|
+
return `Continue? ${yes}${no} `;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function resolveGlobalGitignore(home) {
|
|
301
|
+
const configured = spawnSync(
|
|
302
|
+
"git",
|
|
303
|
+
["config", "--global", "--path", "--get", "core.excludesfile"],
|
|
304
|
+
{ encoding: "utf8" },
|
|
305
|
+
);
|
|
306
|
+
|
|
307
|
+
if (configured.status === 0) {
|
|
308
|
+
const configuredPath = configured.stdout.trim();
|
|
309
|
+
if (configuredPath) {
|
|
310
|
+
const resolvedPath = expandHome(configuredPath, home);
|
|
311
|
+
return {
|
|
312
|
+
path: resolvedPath,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
130
315
|
}
|
|
131
316
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
317
|
+
const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
|
|
318
|
+
const resolvedPath = path.join(configHome, "git", "ignore");
|
|
319
|
+
return {
|
|
320
|
+
path: resolvedPath,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function readTextIfExists(filePath) {
|
|
325
|
+
try {
|
|
326
|
+
return await fs.promises.readFile(filePath, "utf8");
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error && error.code === "ENOENT") {
|
|
329
|
+
return "";
|
|
330
|
+
}
|
|
331
|
+
throw error;
|
|
135
332
|
}
|
|
333
|
+
}
|
|
136
334
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return "bun";
|
|
335
|
+
async function ensureCodexPotterIgnored(filePath) {
|
|
336
|
+
const currentContent = await readTextIfExists(filePath);
|
|
337
|
+
if (gitignoreIgnoresCodexPotter(currentContent)) {
|
|
338
|
+
return false;
|
|
142
339
|
}
|
|
143
340
|
|
|
144
|
-
|
|
341
|
+
let updated = currentContent;
|
|
342
|
+
if (updated && !updated.endsWith("\n")) {
|
|
343
|
+
updated += "\n";
|
|
344
|
+
}
|
|
345
|
+
updated += `${CODEXPOTTER_GITIGNORE_ENTRY}\n`;
|
|
346
|
+
await writeTextAtomic(filePath, updated);
|
|
347
|
+
return true;
|
|
145
348
|
}
|
|
146
349
|
|
|
147
|
-
function
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
350
|
+
async function writeTextAtomic(filePath, content) {
|
|
351
|
+
const dir = path.dirname(filePath);
|
|
352
|
+
await fs.promises.mkdir(dir, { recursive: true });
|
|
353
|
+
const tempPath = path.join(
|
|
354
|
+
dir,
|
|
355
|
+
`.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await fs.promises.writeFile(tempPath, content, "utf8");
|
|
360
|
+
await fs.promises.rename(tempPath, filePath);
|
|
361
|
+
} catch (error) {
|
|
362
|
+
await fs.promises.rm(tempPath, { force: true }).catch(() => {});
|
|
363
|
+
throw error;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function gitignoreIgnoresCodexPotter(contents) {
|
|
368
|
+
if (!contents) {
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return simpleGitignoreIgnoresCodexPotter(contents);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function simpleGitignoreIgnoresCodexPotter(contents) {
|
|
376
|
+
let ignored = false;
|
|
377
|
+
|
|
378
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
379
|
+
const parsed = parseGitignoreLine(rawLine);
|
|
380
|
+
if (!parsed) {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
if (patternMatchesCodexPotter(parsed.pattern)) {
|
|
385
|
+
ignored = !parsed.negated;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return ignored;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function parseGitignoreLine(rawLine) {
|
|
393
|
+
const line = rawLine.trim();
|
|
394
|
+
if (!line || line.startsWith("#")) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (line.startsWith("!")) {
|
|
399
|
+
return { negated: true, pattern: line.slice(1) };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
return { negated: false, pattern: line };
|
|
151
403
|
}
|
|
152
404
|
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
405
|
+
function patternMatchesCodexPotter(pattern) {
|
|
406
|
+
const normalized = pattern.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
407
|
+
const withoutTrailingSlash = normalized.replace(/\/+$/, "");
|
|
408
|
+
|
|
409
|
+
return (
|
|
410
|
+
withoutTrailingSlash === ".codexpotter" ||
|
|
411
|
+
withoutTrailingSlash === "**/.codexpotter" ||
|
|
412
|
+
normalized === ".codexpotter/**" ||
|
|
413
|
+
normalized === "**/.codexpotter/**"
|
|
156
414
|
);
|
|
157
415
|
}
|
|
158
416
|
|
|
159
|
-
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
stdio: "inherit",
|
|
177
|
-
env,
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
child.on("error", (err) => {
|
|
181
|
-
// Typically triggered when the binary is missing or not executable.
|
|
182
|
-
// Re-throwing here will terminate the parent with a non-zero exit code
|
|
183
|
-
// while still printing a helpful stack trace.
|
|
184
|
-
// eslint-disable-next-line no-console
|
|
185
|
-
console.error(err);
|
|
186
|
-
process.exit(1);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Forward common termination signals to the child so that it shuts down
|
|
190
|
-
// gracefully. In the handler we temporarily disable the default behavior of
|
|
191
|
-
// exiting immediately; once the child has been signaled we simply wait for
|
|
192
|
-
// its exit event which will in turn terminate the parent (see below).
|
|
193
|
-
const forwardSignal = (signal) => {
|
|
194
|
-
if (child.killed) {
|
|
195
|
-
return;
|
|
417
|
+
function resolveSkillInstaller() {
|
|
418
|
+
const runner = detectPackageRunner();
|
|
419
|
+
const args =
|
|
420
|
+
runner === "npx" ? ["--yes", ...LOOP_SKILL_COMMAND] : LOOP_SKILL_COMMAND;
|
|
421
|
+
return {
|
|
422
|
+
command: executableForRunner(runner),
|
|
423
|
+
args,
|
|
424
|
+
displayCommand: `${runner} ${args.join(" ")}`,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function detectPackageRunner() {
|
|
429
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
|
430
|
+
const execPath = process.env.npm_execpath || "";
|
|
431
|
+
const execName = path.basename(execPath).toLowerCase();
|
|
432
|
+
if (/\bbun\//.test(userAgent) || execName.startsWith("bun")) {
|
|
433
|
+
return "bunx";
|
|
196
434
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
435
|
+
return "npx";
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function executableForRunner(runner) {
|
|
439
|
+
if (process.platform === "win32") {
|
|
440
|
+
return `${runner}.cmd`;
|
|
201
441
|
}
|
|
202
|
-
|
|
442
|
+
return runner;
|
|
443
|
+
}
|
|
203
444
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
// When the child exits, mirror its termination reason in the parent so that
|
|
210
|
-
// shell scripts and other tooling observe the correct exit status.
|
|
211
|
-
// Wrap the lifetime of the child process in a Promise so that we can await
|
|
212
|
-
// its termination in a structured way. The Promise resolves with an object
|
|
213
|
-
// describing how the child exited: either via exit code or due to a signal.
|
|
214
|
-
const childResult = await new Promise((resolve) => {
|
|
215
|
-
child.on("exit", (code, signal) => {
|
|
216
|
-
if (signal) {
|
|
217
|
-
resolve({ type: "signal", signal });
|
|
218
|
-
} else {
|
|
219
|
-
resolve({ type: "code", exitCode: code ?? 1 });
|
|
220
|
-
}
|
|
445
|
+
async function runLoopSkillInstaller(skillInstaller) {
|
|
446
|
+
const child = spawn(skillInstaller.command, skillInstaller.args, {
|
|
447
|
+
stdio: "inherit",
|
|
221
448
|
});
|
|
222
|
-
});
|
|
223
449
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
450
|
+
const result = await new Promise((resolve, reject) => {
|
|
451
|
+
child.on("error", reject);
|
|
452
|
+
child.on("exit", (code, signal) => {
|
|
453
|
+
resolve({ code, signal });
|
|
454
|
+
});
|
|
227
455
|
});
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
456
|
+
|
|
457
|
+
if (result.signal) {
|
|
458
|
+
throw new Error(`Loop skill installer terminated by ${result.signal}.`);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (result.code !== 0) {
|
|
462
|
+
throw new Error(`Loop skill installer failed with exit code ${result.code}.`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function getHomeDir() {
|
|
467
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
468
|
+
if (!home) {
|
|
469
|
+
throw new Error("Cannot determine the home directory.");
|
|
470
|
+
}
|
|
471
|
+
return home;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function expandHome(value, home) {
|
|
475
|
+
if (value === "~") {
|
|
476
|
+
return home;
|
|
477
|
+
}
|
|
478
|
+
if (value.startsWith("~/") || value.startsWith("~\\")) {
|
|
479
|
+
return path.join(home, value.slice(2));
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function displayPath(filePath) {
|
|
485
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
486
|
+
if (!home) {
|
|
487
|
+
return filePath;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const relative = path.relative(home, filePath);
|
|
491
|
+
if (relative === "") {
|
|
492
|
+
return "~";
|
|
493
|
+
}
|
|
494
|
+
if (!relative.startsWith("..") && !path.isAbsolute(relative)) {
|
|
495
|
+
return path.join("~", relative);
|
|
496
|
+
}
|
|
497
|
+
return filePath;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function shouldUseColor() {
|
|
501
|
+
if (process.env.NO_COLOR) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
if (process.env.FORCE_COLOR && process.env.FORCE_COLOR !== "0") {
|
|
505
|
+
return true;
|
|
506
|
+
}
|
|
507
|
+
return Boolean(process.stdout.isTTY);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function format(text, color) {
|
|
511
|
+
if (!colorEnabled) {
|
|
512
|
+
return text;
|
|
513
|
+
}
|
|
514
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
try {
|
|
518
|
+
const exitCode = await main(process.argv.slice(2));
|
|
519
|
+
process.exit(exitCode);
|
|
520
|
+
} catch (error) {
|
|
521
|
+
console.error(`${format("Error:", "red")} ${error.message}`);
|
|
522
|
+
process.exit(1);
|
|
233
523
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codex-potter",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.0-next.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codex-potter": "bin/codex-potter.js"
|
|
@@ -10,20 +10,16 @@
|
|
|
10
10
|
"node": ">=16"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"bin"
|
|
13
|
+
"bin",
|
|
14
|
+
"resources"
|
|
14
15
|
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "node --test"
|
|
18
|
+
},
|
|
15
19
|
"repository": {
|
|
16
20
|
"type": "git",
|
|
17
21
|
"url": "https://github.com/breezewish/CodexPotter",
|
|
18
22
|
"directory": "npm"
|
|
19
23
|
},
|
|
20
|
-
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
|
|
21
|
-
"optionalDependencies": {
|
|
22
|
-
"codex-potter-linux-x64": "npm:codex-potter@0.1.31-linux-x64",
|
|
23
|
-
"codex-potter-linux-arm64": "npm:codex-potter@0.1.31-linux-arm64",
|
|
24
|
-
"codex-potter-darwin-x64": "npm:codex-potter@0.1.31-darwin-x64",
|
|
25
|
-
"codex-potter-darwin-arm64": "npm:codex-potter@0.1.31-darwin-arm64",
|
|
26
|
-
"codex-potter-win32-x64": "npm:codex-potter@0.1.31-win32-x64",
|
|
27
|
-
"codex-potter-win32-arm64": "npm:codex-potter@0.1.31-win32-arm64"
|
|
28
|
-
}
|
|
24
|
+
"packageManager": "pnpm@10.29.3+sha512.498e1fb4cca5aa06c1dcf2611e6fafc50972ffe7189998c409e90de74566444298ffe43e6cd2acdc775ba1aa7cc5e092a8b7054c811ba8c5770f84693d33d2dc"
|
|
29
25
|
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
name = "potter_worker"
|
|
2
|
+
description = "General purpose worker, only used in CodexPotter loop"
|
|
3
|
+
developer_instructions = """
|
|
4
|
+
|
|
5
|
+
<WORKFLOW_INSTRUCTIONS>
|
|
6
|
+
|
|
7
|
+
Run the workflow below to implement the overall goal recorded in the handoff file.
|
|
8
|
+
Keep handoff file updated until all listed tasks are complete or handoff file's `status == skip`.
|
|
9
|
+
|
|
10
|
+
- `.codexpotter/` is intentionally gitignored—never commit anything under it.
|
|
11
|
+
- Sections in handoff file: Overall Goal, In Progress, Todo, Done
|
|
12
|
+
- handoff file's status in front matter: initial / open / skip
|
|
13
|
+
|
|
14
|
+
# Phase: `status == initial`
|
|
15
|
+
|
|
16
|
+
1. Resolve and fully understand user's request in `Overall Goal`.
|
|
17
|
+
|
|
18
|
+
2. For user request that:
|
|
19
|
+
- requires broken down into smaller tasks:
|
|
20
|
+
set status to `open` and create smaller tasks in `Todo`.
|
|
21
|
+
- can be done / answered immediately:
|
|
22
|
+
do so and record in `Done`, set status to `skip`. No need to create other tasks.
|
|
23
|
+
|
|
24
|
+
# Phase: `status == open`
|
|
25
|
+
|
|
26
|
+
1. Always continue tasks in `In Progress` first (if any).
|
|
27
|
+
- If none are in progress, pick from `Todo` (not necessarily first, choose wisely).
|
|
28
|
+
- You may start multiple related tasks, but don't start multiple large/complex ones at once.
|
|
29
|
+
|
|
30
|
+
2. When start tasks, move them from `Todo` -> `In Progress` (keep text unchanged).
|
|
31
|
+
|
|
32
|
+
3. When complete a task:
|
|
33
|
+
|
|
34
|
+
3.1. Append an entry to `Done` including:
|
|
35
|
+
- what you completed (concise, derived from the original task, keep necessary details)
|
|
36
|
+
- key decisions + rationale
|
|
37
|
+
- files changed (if any)
|
|
38
|
+
- learnings for future iterations (optional)
|
|
39
|
+
|
|
40
|
+
Keep it concise (brevity > grammar).
|
|
41
|
+
|
|
42
|
+
3.2. Remove task from `Todo`/`In Progress`.
|
|
43
|
+
|
|
44
|
+
3.3. Create a git commit for your changes (if any) with a succinct message. No need to commit the handoff file.
|
|
45
|
+
|
|
46
|
+
4. You may add/remove `Todo` tasks as needed.
|
|
47
|
+
- Break large tasks into small, concrete steps; adjust tasks as your understanding improves.
|
|
48
|
+
|
|
49
|
+
5. If all tasks are complete, do strict review and try to enhance:
|
|
50
|
+
|
|
51
|
+
5.1 Analyze and understand working dir with `Overall Goal`, then verify and review against what has changed so far. Utilize review skills if available.
|
|
52
|
+
|
|
53
|
+
handoff file's front matter recorded git commit before change; use it to learn diffs.
|
|
54
|
+
|
|
55
|
+
5.2 Identify issues, missing parts, unaligned areas, or possible improvements, and add them to `Todo`.
|
|
56
|
+
|
|
57
|
+
IMPORTANT PRINCIPLE: `Done` TASKS COULD BE MISLEADING, always be critical and skeptical about `Done` tasks,
|
|
58
|
+
as they could be written by previous low-quality agents, claimed to be done but actually incomplete,
|
|
59
|
+
incorrect, not well-designed, not respecting project's standard, or not aligned with the overall goal at all.
|
|
60
|
+
You must find the good path to achieve the overall goal based on the first principle, without being biased by the existing "Done" tasks.
|
|
61
|
+
|
|
62
|
+
5.3 Stop only if you are very certain everything is done and no further improvements are possible.
|
|
63
|
+
|
|
64
|
+
If the user request was fulfilled by replying directly without any artifact files or code changes, you can stop once all tasks are done — no further improvements are needed.
|
|
65
|
+
|
|
66
|
+
# Do Improvements
|
|
67
|
+
|
|
68
|
+
When all tasks are complete AND overall goal is to make changes, consider improvements of various kinds, for example but not limited to:
|
|
69
|
+
|
|
70
|
+
**Coding kind**:
|
|
71
|
+
|
|
72
|
+
- polish, simplify, quality, performance, edge cases, error handling, UX, docs, etc.
|
|
73
|
+
|
|
74
|
+
When polishing codes, follow the first principle, try to simplify the solution, instead of bloating the code with extra checks, fallbacks, or safety nets that may hide potential issues.
|
|
75
|
+
The goal of polishing is to find real missing pieces, make the code more elegant, simple and efficient, not to add more layers of complexity.
|
|
76
|
+
|
|
77
|
+
**Docs / research / reports kind:**
|
|
78
|
+
|
|
79
|
+
- correctness, completeness, readability, logical clarity, accuracy
|
|
80
|
+
- remove irrelevant and redundant content
|
|
81
|
+
|
|
82
|
+
# Requirements
|
|
83
|
+
|
|
84
|
+
- Don't ask the user questions. Decide and act autonomously.
|
|
85
|
+
- Keep working until all tasks in the handoff file are complete.
|
|
86
|
+
- Follow engineering rules in `AGENTS.md` (if present).
|
|
87
|
+
- NEVER mention this workflow / "developer instruction" or what workflow steps you have followed in your response. This workflow should be transparent to the user.
|
|
88
|
+
- You must NOT change handoff file status from `open` to `skip`.
|
|
89
|
+
- To avoid regression, read full handoff file.
|
|
90
|
+
- NEVER change any text in `Overall Goal`.
|
|
91
|
+
|
|
92
|
+
# Knowledge capture (`.codexpotter/kb/`)
|
|
93
|
+
|
|
94
|
+
- Before starting, read `.codexpotter/kb/README.md` (if present).
|
|
95
|
+
- After deep research/exploration of a module, write intermediate facts + code locations to `.codexpotter/kb/xxx.md` and update the README index.
|
|
96
|
+
- KB files may be stale; **code is the source of truth**—update KB promptly when conflicts are found.
|
|
97
|
+
- No need to commit KB files.
|
|
98
|
+
|
|
99
|
+
# When all tasks are done or the project is skipped
|
|
100
|
+
|
|
101
|
+
Mark handoff file's `finite_incantatem` to true ONLY IF you have not changed any file or code since you received this workflow instruction.
|
|
102
|
+
|
|
103
|
+
Updating handoff files or files under `.codexpotter/kb` doesn't matter, but any other file changes indicate you have done some work,
|
|
104
|
+
so `finite_incantatem` should be kept false.
|
|
105
|
+
|
|
106
|
+
# Review guidelines
|
|
107
|
+
|
|
108
|
+
When you are acting as a reviewer for the code change made so far, here are the general guidelines
|
|
109
|
+
for determining whether something is a bug and should be fixed:
|
|
110
|
+
|
|
111
|
+
- It meaningfully impacts the accuracy, performance, security, or maintainability of the code.
|
|
112
|
+
- The bug is discrete and actionable (i.e. not a general issue with the codebase or a combination of multiple issues).
|
|
113
|
+
- Fixing the bug does not demand a level of rigor that is not present in the rest of the codebase (e.g. one doesn't need very detailed comments and input validation in a repository of one-off scripts in personal projects)
|
|
114
|
+
- The bug was introduced by this project's change.
|
|
115
|
+
- The author of the original PR would likely fix the issue if they were made aware of it.
|
|
116
|
+
- The bug does not rely on unstated assumptions about the codebase or author's intent.
|
|
117
|
+
- It is not enough to speculate that a change may disrupt another part of the codebase, to be considered a bug, one must identify the other parts of the code that are provably affected.
|
|
118
|
+
- The bug is clearly not just an intentional change by the original author.
|
|
119
|
+
|
|
120
|
+
</WORKFLOW_INSTRUCTIONS>
|
|
121
|
+
|
|
122
|
+
"""
|