skill-codex 0.2.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/LICENSE +21 -0
- package/README.md +202 -0
- package/commands/codex-consult.md +36 -0
- package/commands/codex-do.md +44 -0
- package/commands/codex-review.md +35 -0
- package/dist/bin/skill-codex.js +467 -0
- package/dist/bin/skill-codex.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +722 -0
- package/dist/index.js.map +1 -0
- package/hooks/post-tool-use-review.ps1 +50 -0
- package/hooks/post-tool-use-review.sh +49 -0
- package/package.json +81 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import {
|
|
5
|
+
CallToolRequestSchema,
|
|
6
|
+
ListToolsRequestSchema
|
|
7
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
|
|
9
|
+
// src/tools/codex-exec.ts
|
|
10
|
+
import fs2 from "fs";
|
|
11
|
+
import path3 from "path";
|
|
12
|
+
import { z } from "zod";
|
|
13
|
+
|
|
14
|
+
// src/errors/errors.ts
|
|
15
|
+
var BridgeError = class extends Error {
|
|
16
|
+
code;
|
|
17
|
+
retryable;
|
|
18
|
+
constructor(message, code, retryable) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "BridgeError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
this.retryable = retryable;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var CliNotFoundError = class extends BridgeError {
|
|
26
|
+
constructor(binary = "codex") {
|
|
27
|
+
super(
|
|
28
|
+
`${binary} CLI not found on PATH. Install it with: npm i -g @openai/codex`,
|
|
29
|
+
"CLI_NOT_FOUND",
|
|
30
|
+
false
|
|
31
|
+
);
|
|
32
|
+
this.name = "CliNotFoundError";
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var AuthExpiredError = class extends BridgeError {
|
|
36
|
+
constructor() {
|
|
37
|
+
super(
|
|
38
|
+
"Codex authentication expired or not found. Run `codex login` to re-authenticate.",
|
|
39
|
+
"AUTH_EXPIRED",
|
|
40
|
+
false
|
|
41
|
+
);
|
|
42
|
+
this.name = "AuthExpiredError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var RecursionLimitError = class extends BridgeError {
|
|
46
|
+
constructor(depth, max) {
|
|
47
|
+
super(
|
|
48
|
+
`Maximum bridge nesting depth reached (${depth} >= ${max}). This prevents infinite recursion between Claude and Codex.`,
|
|
49
|
+
"RECURSION_LIMIT",
|
|
50
|
+
false
|
|
51
|
+
);
|
|
52
|
+
this.name = "RecursionLimitError";
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var LockConflictError = class extends BridgeError {
|
|
56
|
+
constructor(pid) {
|
|
57
|
+
super(
|
|
58
|
+
`Another skill-codex instance is running (PID ${pid}). Wait for it to finish or delete the lock file.`,
|
|
59
|
+
"LOCK_CONFLICT",
|
|
60
|
+
false
|
|
61
|
+
);
|
|
62
|
+
this.name = "LockConflictError";
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
var TimeoutError = class extends BridgeError {
|
|
66
|
+
constructor(timeoutMs) {
|
|
67
|
+
super(
|
|
68
|
+
`Codex timed out after ${Math.round(timeoutMs / 1e3)}s. Increase SKILL_CODEX_TIMEOUT_MS if needed.`,
|
|
69
|
+
"TIMEOUT",
|
|
70
|
+
true
|
|
71
|
+
);
|
|
72
|
+
this.name = "TimeoutError";
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var RateLimitError = class extends BridgeError {
|
|
76
|
+
constructor() {
|
|
77
|
+
super(
|
|
78
|
+
"Codex rate limited (429). Will retry with backoff.",
|
|
79
|
+
"RATE_LIMIT",
|
|
80
|
+
true
|
|
81
|
+
);
|
|
82
|
+
this.name = "RateLimitError";
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var ServerError = class extends BridgeError {
|
|
86
|
+
constructor(detail = "") {
|
|
87
|
+
super(
|
|
88
|
+
`Codex server error${detail ? `: ${detail}` : ""}. Will retry.`,
|
|
89
|
+
"SERVER_ERROR",
|
|
90
|
+
true
|
|
91
|
+
);
|
|
92
|
+
this.name = "ServerError";
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
var NetworkError = class extends BridgeError {
|
|
96
|
+
constructor(detail = "") {
|
|
97
|
+
super(
|
|
98
|
+
`Network error connecting to Codex${detail ? `: ${detail}` : ""}. Check your connection.`,
|
|
99
|
+
"NETWORK_ERROR",
|
|
100
|
+
true
|
|
101
|
+
);
|
|
102
|
+
this.name = "NetworkError";
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
var EmptyOutputError = class extends BridgeError {
|
|
106
|
+
constructor() {
|
|
107
|
+
super(
|
|
108
|
+
"Codex returned empty output. This may be a transient issue.",
|
|
109
|
+
"EMPTY_OUTPUT",
|
|
110
|
+
true
|
|
111
|
+
);
|
|
112
|
+
this.name = "EmptyOutputError";
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
var NotGitRepoError = class extends BridgeError {
|
|
116
|
+
constructor(cwd) {
|
|
117
|
+
super(
|
|
118
|
+
`Not a git repository: ${cwd}. This operation requires a git repo.`,
|
|
119
|
+
"NOT_GIT_REPO",
|
|
120
|
+
false
|
|
121
|
+
);
|
|
122
|
+
this.name = "NotGitRepoError";
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// src/config/constants.ts
|
|
127
|
+
var MAX_BRIDGE_DEPTH = 2;
|
|
128
|
+
var BRIDGE_DEPTH_ENV = "SKILL_CODEX_DEPTH";
|
|
129
|
+
var DEFAULT_TIMEOUT_MS = 6e5;
|
|
130
|
+
var TIMEOUT_ENV = "SKILL_CODEX_TIMEOUT_MS";
|
|
131
|
+
var KILL_GRACE_MS = 5e3;
|
|
132
|
+
var MAX_RETRIES = 3;
|
|
133
|
+
var MAX_RETRIES_ENV = "SKILL_CODEX_MAX_RETRIES";
|
|
134
|
+
var RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
|
|
135
|
+
var RETRY_CAP_MS = 1e4;
|
|
136
|
+
var MAX_RESPONSE_CHARS = 8e4;
|
|
137
|
+
var LOCK_STALE_MS = 9e5;
|
|
138
|
+
var LOCK_FILENAME = ".skill-codex.lock";
|
|
139
|
+
|
|
140
|
+
// src/guards/check-recursion.ts
|
|
141
|
+
function getCurrentDepth() {
|
|
142
|
+
return parseInt(process.env[BRIDGE_DEPTH_ENV] ?? "0", 10);
|
|
143
|
+
}
|
|
144
|
+
function getNextDepth() {
|
|
145
|
+
return getCurrentDepth() + 1;
|
|
146
|
+
}
|
|
147
|
+
function checkRecursion() {
|
|
148
|
+
const depth = getCurrentDepth();
|
|
149
|
+
if (depth >= MAX_BRIDGE_DEPTH) {
|
|
150
|
+
throw new RecursionLimitError(depth, MAX_BRIDGE_DEPTH);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/guards/check-binary.ts
|
|
155
|
+
import which from "which";
|
|
156
|
+
var cachedBinaryPath = null;
|
|
157
|
+
function getCachedBinaryPath() {
|
|
158
|
+
return cachedBinaryPath;
|
|
159
|
+
}
|
|
160
|
+
async function checkBinary(binary = "codex") {
|
|
161
|
+
if (cachedBinaryPath !== null) {
|
|
162
|
+
return { found: true, path: cachedBinaryPath };
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
const resolved = await which(binary);
|
|
166
|
+
cachedBinaryPath = resolved;
|
|
167
|
+
return { found: true, path: resolved };
|
|
168
|
+
} catch {
|
|
169
|
+
throw new CliNotFoundError(binary);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// src/guards/check-auth.ts
|
|
174
|
+
import { execFile } from "child_process";
|
|
175
|
+
var AUTH_CACHE_TTL_MS = 6e4;
|
|
176
|
+
var authCachedAt = null;
|
|
177
|
+
async function checkAuth() {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
if (authCachedAt !== null && now - authCachedAt < AUTH_CACHE_TTL_MS) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const binary = getCachedBinaryPath() ?? "codex";
|
|
183
|
+
return new Promise((resolve, reject) => {
|
|
184
|
+
const child = execFile(
|
|
185
|
+
binary,
|
|
186
|
+
["exec", "--sandbox", "read-only", "--skip-git-repo-check", "--ephemeral", "echo ok"],
|
|
187
|
+
{ timeout: 15e3 },
|
|
188
|
+
(error, _stdout, stderr) => {
|
|
189
|
+
if (!error) {
|
|
190
|
+
authCachedAt = Date.now();
|
|
191
|
+
resolve();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const lower = (stderr ?? error.message ?? "").toLowerCase();
|
|
195
|
+
if (error.code === "ENOENT" || error.code === "ENOENT") {
|
|
196
|
+
reject(new CliNotFoundError());
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (error.killed) {
|
|
200
|
+
reject(new NetworkError("Auth check timed out \u2014 check your network connection"));
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (["econnrefused", "econnreset", "etimedout", "network error", "fetch failed"].some((p) => lower.includes(p))) {
|
|
204
|
+
reject(new NetworkError("Network error during auth check"));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
reject(new AuthExpiredError());
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
child.stdin?.end();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// src/lock/lock-file.ts
|
|
215
|
+
import fs from "fs";
|
|
216
|
+
import path from "path";
|
|
217
|
+
import os from "os";
|
|
218
|
+
function getLockPath(cwd) {
|
|
219
|
+
return path.join(cwd, LOCK_FILENAME);
|
|
220
|
+
}
|
|
221
|
+
function isProcessAlive(pid) {
|
|
222
|
+
try {
|
|
223
|
+
process.kill(pid, 0);
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function isLockStale(data) {
|
|
230
|
+
const age = Date.now() - data.timestamp;
|
|
231
|
+
if (age > LOCK_STALE_MS) return true;
|
|
232
|
+
if (!isProcessAlive(data.pid)) return true;
|
|
233
|
+
return false;
|
|
234
|
+
}
|
|
235
|
+
function tryRemoveStaleLock(lockPath) {
|
|
236
|
+
try {
|
|
237
|
+
const raw = fs.readFileSync(lockPath, "utf-8");
|
|
238
|
+
const data = JSON.parse(raw);
|
|
239
|
+
if (isLockStale(data)) {
|
|
240
|
+
fs.unlinkSync(lockPath);
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
throw new LockConflictError(data.pid);
|
|
244
|
+
} catch (err) {
|
|
245
|
+
if (err instanceof LockConflictError) throw err;
|
|
246
|
+
try {
|
|
247
|
+
fs.unlinkSync(lockPath);
|
|
248
|
+
} catch {
|
|
249
|
+
}
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function acquireLock(cwd) {
|
|
254
|
+
const lockPath = getLockPath(cwd);
|
|
255
|
+
const lockData = {
|
|
256
|
+
pid: process.pid,
|
|
257
|
+
timestamp: Date.now(),
|
|
258
|
+
hostname: os.hostname()
|
|
259
|
+
};
|
|
260
|
+
const content = JSON.stringify(lockData, null, 2);
|
|
261
|
+
try {
|
|
262
|
+
fs.writeFileSync(lockPath, content, { flag: "wx" });
|
|
263
|
+
} catch (err) {
|
|
264
|
+
const fsErr = err;
|
|
265
|
+
if (fsErr.code === "EEXIST") {
|
|
266
|
+
tryRemoveStaleLock(lockPath);
|
|
267
|
+
try {
|
|
268
|
+
fs.writeFileSync(lockPath, content, { flag: "wx" });
|
|
269
|
+
} catch (retryErr) {
|
|
270
|
+
const retryFsErr = retryErr;
|
|
271
|
+
if (retryFsErr.code === "EEXIST") {
|
|
272
|
+
throw new LockConflictError(0);
|
|
273
|
+
}
|
|
274
|
+
throw retryErr;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
throw err;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const onExit = () => {
|
|
281
|
+
try {
|
|
282
|
+
fs.unlinkSync(lockPath);
|
|
283
|
+
} catch {
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
process.on("exit", onExit);
|
|
287
|
+
process.on("SIGINT", onExit);
|
|
288
|
+
process.on("SIGTERM", onExit);
|
|
289
|
+
const release = () => {
|
|
290
|
+
process.removeListener("exit", onExit);
|
|
291
|
+
process.removeListener("SIGINT", onExit);
|
|
292
|
+
process.removeListener("SIGTERM", onExit);
|
|
293
|
+
try {
|
|
294
|
+
fs.unlinkSync(lockPath);
|
|
295
|
+
} catch {
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
return { release };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// src/guards/check-lock.ts
|
|
302
|
+
function checkLock(cwd) {
|
|
303
|
+
return acquireLock(cwd);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/guards/check-git.ts
|
|
307
|
+
import { execFileSync } from "child_process";
|
|
308
|
+
function checkGit(cwd) {
|
|
309
|
+
try {
|
|
310
|
+
execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
311
|
+
cwd,
|
|
312
|
+
stdio: "pipe",
|
|
313
|
+
timeout: 5e3
|
|
314
|
+
});
|
|
315
|
+
return { isGitRepo: true };
|
|
316
|
+
} catch {
|
|
317
|
+
return { isGitRepo: false };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// src/guards/preflight.ts
|
|
322
|
+
async function runPreflight(options) {
|
|
323
|
+
checkRecursion();
|
|
324
|
+
await checkBinary();
|
|
325
|
+
if (!options.skipAuth) {
|
|
326
|
+
await checkAuth();
|
|
327
|
+
}
|
|
328
|
+
let lockHandle = null;
|
|
329
|
+
if (!options.skipLock) {
|
|
330
|
+
lockHandle = checkLock(options.cwd);
|
|
331
|
+
}
|
|
332
|
+
if (options.requireGit) {
|
|
333
|
+
const { isGitRepo } = checkGit(options.cwd);
|
|
334
|
+
if (!isGitRepo) {
|
|
335
|
+
lockHandle?.release();
|
|
336
|
+
throw new NotGitRepoError(options.cwd);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return { lockHandle };
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// src/runner/exec-runner.ts
|
|
343
|
+
import { spawn } from "child_process";
|
|
344
|
+
|
|
345
|
+
// src/util/platform.ts
|
|
346
|
+
import os2 from "os";
|
|
347
|
+
import path2 from "path";
|
|
348
|
+
function getPlatform() {
|
|
349
|
+
const p = os2.platform();
|
|
350
|
+
if (p === "win32" || p === "darwin" || p === "linux") return p;
|
|
351
|
+
return "linux";
|
|
352
|
+
}
|
|
353
|
+
function isWindows() {
|
|
354
|
+
return getPlatform() === "win32";
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/runner/timeout.ts
|
|
358
|
+
function setupTimeout(child, timeoutMs) {
|
|
359
|
+
let timer = null;
|
|
360
|
+
let graceTimer = null;
|
|
361
|
+
const promise = new Promise((_resolve, reject) => {
|
|
362
|
+
timer = setTimeout(() => {
|
|
363
|
+
if (isWindows()) {
|
|
364
|
+
child.kill();
|
|
365
|
+
} else {
|
|
366
|
+
child.kill("SIGTERM");
|
|
367
|
+
graceTimer = setTimeout(() => {
|
|
368
|
+
try {
|
|
369
|
+
if (!child.killed) {
|
|
370
|
+
child.kill("SIGKILL");
|
|
371
|
+
}
|
|
372
|
+
} catch {
|
|
373
|
+
}
|
|
374
|
+
}, KILL_GRACE_MS);
|
|
375
|
+
}
|
|
376
|
+
reject(new TimeoutError(timeoutMs));
|
|
377
|
+
}, timeoutMs);
|
|
378
|
+
});
|
|
379
|
+
const clear = () => {
|
|
380
|
+
if (timer) clearTimeout(timer);
|
|
381
|
+
if (graceTimer) clearTimeout(graceTimer);
|
|
382
|
+
};
|
|
383
|
+
return { clear, promise };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/util/truncate.ts
|
|
387
|
+
function truncateResponse(text, maxChars = MAX_RESPONSE_CHARS) {
|
|
388
|
+
if (text.length <= maxChars) return text;
|
|
389
|
+
const omitted = text.length - maxChars;
|
|
390
|
+
return text.slice(0, maxChars) + `
|
|
391
|
+
|
|
392
|
+
[Response truncated at ${maxChars} characters. ${omitted} characters omitted.]`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/runner/output-parser.ts
|
|
396
|
+
function parseCodexOutput(raw) {
|
|
397
|
+
if (!raw.trim()) {
|
|
398
|
+
throw new EmptyOutputError();
|
|
399
|
+
}
|
|
400
|
+
const lines = raw.split("\n").filter((line) => line.trim());
|
|
401
|
+
const messages = [];
|
|
402
|
+
let resultContent = null;
|
|
403
|
+
for (const line of lines) {
|
|
404
|
+
try {
|
|
405
|
+
const parsed = JSON.parse(line);
|
|
406
|
+
if (parsed.type === "result" && typeof parsed.content === "string") {
|
|
407
|
+
resultContent = parsed.content;
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (parsed.type === "message" && typeof parsed.content === "string") {
|
|
411
|
+
messages.push(parsed.content);
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (parsed.item?.type === "agent_message" && typeof parsed.item.text === "string") {
|
|
415
|
+
messages.push(parsed.item.text);
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
if (parsed.itemType === "agent_message" && typeof parsed.text === "string") {
|
|
419
|
+
messages.push(parsed.text);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
} catch {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
let agentMessage;
|
|
426
|
+
if (resultContent !== null) {
|
|
427
|
+
agentMessage = resultContent;
|
|
428
|
+
} else if (messages.length > 0) {
|
|
429
|
+
agentMessage = messages.join("\n\n");
|
|
430
|
+
} else {
|
|
431
|
+
const substantiveLines = lines.filter(
|
|
432
|
+
(line) => !line.startsWith("OpenAI Codex") && !line.startsWith("---") && !line.startsWith("tokens used")
|
|
433
|
+
);
|
|
434
|
+
agentMessage = substantiveLines.join("\n").trim();
|
|
435
|
+
}
|
|
436
|
+
if (!agentMessage) {
|
|
437
|
+
throw new EmptyOutputError();
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
content: truncateResponse(agentMessage),
|
|
441
|
+
raw
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/runner/exec-runner.ts
|
|
446
|
+
function getTimeout(override) {
|
|
447
|
+
if (override !== void 0) return override;
|
|
448
|
+
const envVal = process.env[TIMEOUT_ENV];
|
|
449
|
+
if (envVal) {
|
|
450
|
+
const parsed = parseInt(envVal, 10);
|
|
451
|
+
if (!isNaN(parsed) && parsed > 0) return parsed;
|
|
452
|
+
}
|
|
453
|
+
return DEFAULT_TIMEOUT_MS;
|
|
454
|
+
}
|
|
455
|
+
function classifyError(exitCode, stderr) {
|
|
456
|
+
const lower = stderr.toLowerCase();
|
|
457
|
+
if (lower.includes("unauthorized") || lower.includes("401") || lower.includes("api key")) {
|
|
458
|
+
return new AuthExpiredError();
|
|
459
|
+
}
|
|
460
|
+
if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
|
|
461
|
+
return new RateLimitError();
|
|
462
|
+
}
|
|
463
|
+
if (["500", "502", "503", "504", "internal server error", "bad gateway", "service unavailable"].some((p) => lower.includes(p))) {
|
|
464
|
+
return new ServerError(stderr.slice(0, 200));
|
|
465
|
+
}
|
|
466
|
+
if (["econnreset", "econnrefused", "etimedout", "network error", "fetch failed", "socket hang up"].some((p) => lower.includes(p))) {
|
|
467
|
+
return new NetworkError(stderr.slice(0, 200));
|
|
468
|
+
}
|
|
469
|
+
return new BridgeError(
|
|
470
|
+
`Codex exited with code ${exitCode}: ${stderr.slice(0, 300)}`,
|
|
471
|
+
"EXEC_FAILED",
|
|
472
|
+
false
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
async function execCodex(params) {
|
|
476
|
+
const codexPath = getCachedBinaryPath();
|
|
477
|
+
if (codexPath === null) {
|
|
478
|
+
throw new CliNotFoundError();
|
|
479
|
+
}
|
|
480
|
+
return new Promise((resolve, reject) => {
|
|
481
|
+
const timeoutMs = getTimeout(params.timeoutMs);
|
|
482
|
+
const args = ["exec", "--json", "--skip-git-repo-check"];
|
|
483
|
+
if (params.mode === "full-auto") {
|
|
484
|
+
args.push("--full-auto");
|
|
485
|
+
} else {
|
|
486
|
+
args.push("--sandbox", "read-only");
|
|
487
|
+
}
|
|
488
|
+
const stdinPrompt = params.prompt;
|
|
489
|
+
args.push("-");
|
|
490
|
+
const env = {
|
|
491
|
+
...process.env,
|
|
492
|
+
[BRIDGE_DEPTH_ENV]: String(getNextDepth())
|
|
493
|
+
};
|
|
494
|
+
const child = spawn(codexPath, args, {
|
|
495
|
+
cwd: params.cwd,
|
|
496
|
+
env,
|
|
497
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
498
|
+
shell: false,
|
|
499
|
+
windowsHide: true
|
|
500
|
+
});
|
|
501
|
+
const { clear: clearTimeout_, promise: timeoutPromise } = setupTimeout(child, timeoutMs);
|
|
502
|
+
const stdoutChunks = [];
|
|
503
|
+
let stderr = "";
|
|
504
|
+
child.stdout?.on("data", (chunk) => {
|
|
505
|
+
stdoutChunks.push(chunk);
|
|
506
|
+
});
|
|
507
|
+
child.stderr?.on("data", (chunk) => {
|
|
508
|
+
stderr += chunk.toString();
|
|
509
|
+
});
|
|
510
|
+
child.stdin?.write(stdinPrompt);
|
|
511
|
+
child.stdin?.end();
|
|
512
|
+
const onClose = (exitCode) => {
|
|
513
|
+
clearTimeout_();
|
|
514
|
+
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
515
|
+
if (exitCode === 0 || exitCode === null) {
|
|
516
|
+
try {
|
|
517
|
+
const result = parseCodexOutput(stdout);
|
|
518
|
+
resolve(result);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
reject(err);
|
|
521
|
+
}
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
reject(classifyError(exitCode, stderr));
|
|
525
|
+
};
|
|
526
|
+
child.on("close", onClose);
|
|
527
|
+
child.on("error", (err) => {
|
|
528
|
+
clearTimeout_();
|
|
529
|
+
if (err.code === "ENOENT") {
|
|
530
|
+
reject(new CliNotFoundError());
|
|
531
|
+
} else {
|
|
532
|
+
reject(new BridgeError(`Failed to spawn codex: ${err.message}`, "SPAWN_ERROR", false));
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
timeoutPromise.catch((err) => {
|
|
536
|
+
reject(err);
|
|
537
|
+
});
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/runner/retry.ts
|
|
542
|
+
function getMaxRetries(override) {
|
|
543
|
+
if (override !== void 0) return override;
|
|
544
|
+
const envVal = process.env[MAX_RETRIES_ENV];
|
|
545
|
+
if (envVal) {
|
|
546
|
+
const parsed = parseInt(envVal, 10);
|
|
547
|
+
if (!isNaN(parsed) && parsed >= 0) return parsed;
|
|
548
|
+
}
|
|
549
|
+
return MAX_RETRIES;
|
|
550
|
+
}
|
|
551
|
+
function getDelay(attempt) {
|
|
552
|
+
const base = RETRY_DELAYS_MS[attempt] ?? RETRY_CAP_MS;
|
|
553
|
+
const capped = Math.min(base, RETRY_CAP_MS);
|
|
554
|
+
const jitter = 0.5 + Math.random();
|
|
555
|
+
return Math.round(capped * jitter);
|
|
556
|
+
}
|
|
557
|
+
function defaultShouldRetry(err) {
|
|
558
|
+
return err instanceof BridgeError && err.retryable;
|
|
559
|
+
}
|
|
560
|
+
function sleep(ms) {
|
|
561
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
562
|
+
}
|
|
563
|
+
async function withRetry(fn, options = {}) {
|
|
564
|
+
const maxRetries = getMaxRetries(options.maxRetries);
|
|
565
|
+
const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
|
|
566
|
+
for (let attempt = 0; ; attempt++) {
|
|
567
|
+
try {
|
|
568
|
+
return await fn();
|
|
569
|
+
} catch (err) {
|
|
570
|
+
const isRetryable = err instanceof Error && shouldRetry(err);
|
|
571
|
+
if (attempt < maxRetries && isRetryable) {
|
|
572
|
+
const delay = getDelay(attempt);
|
|
573
|
+
const errorName = err instanceof Error ? err.constructor.name : "UnknownError";
|
|
574
|
+
process.stderr.write(
|
|
575
|
+
`[skill-codex] ${errorName} (attempt ${attempt + 1}/${maxRetries}), retrying in ${delay}ms...
|
|
576
|
+
`
|
|
577
|
+
);
|
|
578
|
+
await sleep(delay);
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
throw err;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
// src/tools/codex-exec.ts
|
|
587
|
+
var TOOL_NAME = "codex_exec";
|
|
588
|
+
var TOOL_DESCRIPTION = "Execute a task using OpenAI Codex CLI. Use for code review, implementation tasks, or getting a second opinion. Codex output is a SUGGESTION \u2014 evaluate it critically before applying.";
|
|
589
|
+
var inputSchema = z.object({
|
|
590
|
+
prompt: z.string().describe("The task description for Codex"),
|
|
591
|
+
mode: z.enum(["exec", "full-auto"]).default("exec").describe("exec = read-only with confirmation, full-auto = can write files"),
|
|
592
|
+
cwd: z.string().optional().describe("Working directory (defaults to server cwd)"),
|
|
593
|
+
timeoutMs: z.number().optional().describe("Override default timeout in milliseconds"),
|
|
594
|
+
requireGit: z.boolean().default(false).describe("Fail if not inside a git repository")
|
|
595
|
+
});
|
|
596
|
+
function formatError(err) {
|
|
597
|
+
if (err instanceof BridgeError) {
|
|
598
|
+
return `[skill-codex error: ${err.code}] ${err.message}`;
|
|
599
|
+
}
|
|
600
|
+
if (err instanceof Error) {
|
|
601
|
+
return `[skill-codex error] ${err.message}`;
|
|
602
|
+
}
|
|
603
|
+
return `[skill-codex error] Unknown error: ${String(err)}`;
|
|
604
|
+
}
|
|
605
|
+
async function handleCodexExec(input, serverCwd) {
|
|
606
|
+
const rawCwd = input.cwd ?? serverCwd;
|
|
607
|
+
const cwd = path3.resolve(rawCwd);
|
|
608
|
+
if (!fs2.existsSync(cwd) || !fs2.statSync(cwd).isDirectory()) {
|
|
609
|
+
return {
|
|
610
|
+
content: [{ type: "text", text: `[skill-codex error: INVALID_CWD] cwd is not an existing directory: ${cwd}` }],
|
|
611
|
+
isError: true
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
let lockRelease = null;
|
|
615
|
+
try {
|
|
616
|
+
const { lockHandle } = await runPreflight({
|
|
617
|
+
cwd,
|
|
618
|
+
requireGit: input.requireGit
|
|
619
|
+
});
|
|
620
|
+
lockRelease = lockHandle?.release ?? null;
|
|
621
|
+
const result = await withRetry(
|
|
622
|
+
() => execCodex({
|
|
623
|
+
prompt: input.prompt,
|
|
624
|
+
cwd,
|
|
625
|
+
mode: input.mode,
|
|
626
|
+
timeoutMs: input.timeoutMs
|
|
627
|
+
})
|
|
628
|
+
);
|
|
629
|
+
return {
|
|
630
|
+
content: [{ type: "text", text: result.content }]
|
|
631
|
+
};
|
|
632
|
+
} catch (err) {
|
|
633
|
+
return {
|
|
634
|
+
content: [{ type: "text", text: formatError(err) }],
|
|
635
|
+
isError: true
|
|
636
|
+
};
|
|
637
|
+
} finally {
|
|
638
|
+
lockRelease?.();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/server.ts
|
|
643
|
+
function createServer(cwd) {
|
|
644
|
+
const server = new Server(
|
|
645
|
+
{ name: "skill-codex", version: "0.2.0" },
|
|
646
|
+
{ capabilities: { tools: {} } }
|
|
647
|
+
);
|
|
648
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
649
|
+
tools: [
|
|
650
|
+
{
|
|
651
|
+
name: TOOL_NAME,
|
|
652
|
+
description: TOOL_DESCRIPTION,
|
|
653
|
+
inputSchema: {
|
|
654
|
+
type: "object",
|
|
655
|
+
properties: {
|
|
656
|
+
prompt: { type: "string", description: "The task description for Codex" },
|
|
657
|
+
mode: {
|
|
658
|
+
type: "string",
|
|
659
|
+
enum: ["exec", "full-auto"],
|
|
660
|
+
default: "exec",
|
|
661
|
+
description: "exec = read-only, full-auto = can write files"
|
|
662
|
+
},
|
|
663
|
+
cwd: { type: "string", description: "Working directory (defaults to server cwd)" },
|
|
664
|
+
timeoutMs: { type: "number", description: "Override default timeout in milliseconds" },
|
|
665
|
+
requireGit: {
|
|
666
|
+
type: "boolean",
|
|
667
|
+
default: false,
|
|
668
|
+
description: "Fail if not inside a git repository"
|
|
669
|
+
}
|
|
670
|
+
},
|
|
671
|
+
required: ["prompt"]
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
]
|
|
675
|
+
}));
|
|
676
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
677
|
+
if (request.params.name !== TOOL_NAME) {
|
|
678
|
+
return {
|
|
679
|
+
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
680
|
+
isError: true
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
const parsed = inputSchema.safeParse(request.params.arguments);
|
|
684
|
+
if (!parsed.success) {
|
|
685
|
+
return {
|
|
686
|
+
content: [
|
|
687
|
+
{
|
|
688
|
+
type: "text",
|
|
689
|
+
text: `Invalid input: ${parsed.error.issues.map((i) => i.message).join(", ")}`
|
|
690
|
+
}
|
|
691
|
+
],
|
|
692
|
+
isError: true
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
return handleCodexExec(parsed.data, cwd);
|
|
696
|
+
});
|
|
697
|
+
return server;
|
|
698
|
+
}
|
|
699
|
+
async function startServer() {
|
|
700
|
+
const cwd = process.cwd();
|
|
701
|
+
const server = createServer(cwd);
|
|
702
|
+
const transport = new StdioServerTransport();
|
|
703
|
+
process.stderr.write("[skill-codex] MCP server starting...\n");
|
|
704
|
+
await server.connect(transport);
|
|
705
|
+
process.stderr.write("[skill-codex] MCP server connected via stdio\n");
|
|
706
|
+
process.on("uncaughtException", (err) => {
|
|
707
|
+
process.stderr.write(`[skill-codex] Uncaught exception: ${err.message}
|
|
708
|
+
`);
|
|
709
|
+
});
|
|
710
|
+
process.on("unhandledRejection", (reason) => {
|
|
711
|
+
process.stderr.write(`[skill-codex] Unhandled rejection: ${String(reason)}
|
|
712
|
+
`);
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/index.ts
|
|
717
|
+
startServer().catch((err) => {
|
|
718
|
+
process.stderr.write(`[skill-codex] Fatal: ${err instanceof Error ? err.message : String(err)}
|
|
719
|
+
`);
|
|
720
|
+
process.exit(1);
|
|
721
|
+
});
|
|
722
|
+
//# sourceMappingURL=index.js.map
|