skill-codex 0.3.0 → 0.7.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 +64 -15
- package/commands/codex-do.md +4 -3
- package/commands/codex-review.md +17 -6
- package/dist/bin/skill-codex.js +109 -17
- package/dist/bin/skill-codex.js.map +1 -1
- package/dist/index.js +439 -48
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
- package/skills/codex-bridge/SKILL.md +217 -0
package/dist/index.js
CHANGED
|
@@ -7,8 +7,8 @@ import {
|
|
|
7
7
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
8
8
|
|
|
9
9
|
// src/tools/codex-exec.ts
|
|
10
|
-
import
|
|
11
|
-
import
|
|
10
|
+
import fs3 from "fs";
|
|
11
|
+
import path4 from "path";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
|
|
14
14
|
// src/errors/errors.ts
|
|
@@ -126,9 +126,10 @@ var NotGitRepoError = class extends BridgeError {
|
|
|
126
126
|
// src/config/constants.ts
|
|
127
127
|
var MAX_BRIDGE_DEPTH = 2;
|
|
128
128
|
var BRIDGE_DEPTH_ENV = "SKILL_CODEX_DEPTH";
|
|
129
|
-
var DEFAULT_TIMEOUT_MS =
|
|
129
|
+
var DEFAULT_TIMEOUT_MS = 3e5;
|
|
130
130
|
var TIMEOUT_ENV = "SKILL_CODEX_TIMEOUT_MS";
|
|
131
131
|
var KILL_GRACE_MS = 5e3;
|
|
132
|
+
var HEARTBEAT_INTERVAL_MS = 1e4;
|
|
132
133
|
var MAX_RETRIES = 3;
|
|
133
134
|
var MAX_RETRIES_ENV = "SKILL_CODEX_MAX_RETRIES";
|
|
134
135
|
var RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
|
|
@@ -136,6 +137,9 @@ var RETRY_CAP_MS = 1e4;
|
|
|
136
137
|
var MAX_RESPONSE_CHARS = 8e4;
|
|
137
138
|
var LOCK_STALE_MS = 9e5;
|
|
138
139
|
var LOCK_FILENAME = ".skill-codex.lock";
|
|
140
|
+
var LOG_ENV = "SKILL_CODEX_LOG";
|
|
141
|
+
var WINDOWS_SANDBOX_ENV = "SKILL_CODEX_WINDOWS_SANDBOX";
|
|
142
|
+
var WINDOWS_SANDBOX_DEFAULT = "unelevated";
|
|
139
143
|
|
|
140
144
|
// src/guards/check-recursion.ts
|
|
141
145
|
function getCurrentDepth() {
|
|
@@ -172,6 +176,16 @@ async function checkBinary(binary = "codex") {
|
|
|
172
176
|
|
|
173
177
|
// src/guards/check-auth.ts
|
|
174
178
|
import { execFile } from "child_process";
|
|
179
|
+
|
|
180
|
+
// src/runner/sandbox-args.ts
|
|
181
|
+
function getSandboxConfigArgs() {
|
|
182
|
+
if (process.platform !== "win32") return [];
|
|
183
|
+
const raw = process.env[WINDOWS_SANDBOX_ENV]?.trim() || WINDOWS_SANDBOX_DEFAULT;
|
|
184
|
+
const mode = /^[a-z-]+$/.test(raw) ? raw : WINDOWS_SANDBOX_DEFAULT;
|
|
185
|
+
return ["-c", `windows.sandbox=${mode}`];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/guards/check-auth.ts
|
|
175
189
|
var AUTH_CACHE_TTL_MS = 6e4;
|
|
176
190
|
var authCachedAt = null;
|
|
177
191
|
async function checkAuth() {
|
|
@@ -183,7 +197,7 @@ async function checkAuth() {
|
|
|
183
197
|
return new Promise((resolve, reject) => {
|
|
184
198
|
const child = execFile(
|
|
185
199
|
binary,
|
|
186
|
-
["exec", "--sandbox", "read-only", "--skip-git-repo-check", "--ephemeral", "echo ok"],
|
|
200
|
+
["exec", "--sandbox", "read-only", ...getSandboxConfigArgs(), "--skip-git-repo-check", "--ephemeral", "echo ok"],
|
|
187
201
|
{ timeout: 3e4, shell: process.platform === "win32" },
|
|
188
202
|
(error, _stdout, stderr) => {
|
|
189
203
|
if (!error) {
|
|
@@ -341,6 +355,7 @@ async function runPreflight(options) {
|
|
|
341
355
|
|
|
342
356
|
// src/runner/exec-runner.ts
|
|
343
357
|
import { spawn } from "child_process";
|
|
358
|
+
import { StringDecoder } from "string_decoder";
|
|
344
359
|
|
|
345
360
|
// src/util/platform.ts
|
|
346
361
|
import os2 from "os";
|
|
@@ -401,15 +416,21 @@ function parseCodexOutput(raw) {
|
|
|
401
416
|
const messages = [];
|
|
402
417
|
const activity = [];
|
|
403
418
|
let resultContent = null;
|
|
419
|
+
let sessionId;
|
|
404
420
|
let usage = null;
|
|
405
421
|
for (const line of lines) {
|
|
406
422
|
try {
|
|
407
423
|
const parsed = JSON.parse(line);
|
|
424
|
+
if (parsed.type === "thread.started" && typeof parsed.thread_id === "string") {
|
|
425
|
+
sessionId = parsed.thread_id;
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
408
428
|
if (parsed.type === "turn.completed" && parsed.usage) {
|
|
409
429
|
usage = {
|
|
410
430
|
input_tokens: parsed.usage.input_tokens ?? 0,
|
|
411
431
|
cached_input_tokens: parsed.usage.cached_input_tokens ?? 0,
|
|
412
|
-
output_tokens: parsed.usage.output_tokens ?? 0
|
|
432
|
+
output_tokens: parsed.usage.output_tokens ?? 0,
|
|
433
|
+
reasoning_output_tokens: parsed.usage.reasoning_output_tokens ?? 0
|
|
413
434
|
};
|
|
414
435
|
continue;
|
|
415
436
|
}
|
|
@@ -436,7 +457,7 @@ function parseCodexOutput(raw) {
|
|
|
436
457
|
}
|
|
437
458
|
continue;
|
|
438
459
|
}
|
|
439
|
-
if (parsed.item?.type === "agent_message" && typeof parsed.item.text === "string") {
|
|
460
|
+
if (parsed.item?.type === "agent_message" && typeof parsed.item.text === "string" && parsed.type !== "item.started" && parsed.type !== "item.updated") {
|
|
440
461
|
messages.push(parsed.item.text);
|
|
441
462
|
continue;
|
|
442
463
|
}
|
|
@@ -462,6 +483,27 @@ function parseCodexOutput(raw) {
|
|
|
462
483
|
});
|
|
463
484
|
continue;
|
|
464
485
|
}
|
|
486
|
+
if (parsed.item?.type === "file_change") {
|
|
487
|
+
const changes = Array.isArray(parsed.item.changes) ? parsed.item.changes : null;
|
|
488
|
+
if (changes && changes.length > 0) {
|
|
489
|
+
for (const change of changes) {
|
|
490
|
+
activity.push({
|
|
491
|
+
type: "write",
|
|
492
|
+
path: change?.path || "file",
|
|
493
|
+
icon: "\u270E",
|
|
494
|
+
status: change?.kind || "write"
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
} else {
|
|
498
|
+
activity.push({
|
|
499
|
+
type: "write",
|
|
500
|
+
path: parsed.item.path || "file",
|
|
501
|
+
icon: "\u270E",
|
|
502
|
+
status: "write"
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
465
507
|
} catch {
|
|
466
508
|
}
|
|
467
509
|
}
|
|
@@ -483,7 +525,158 @@ function parseCodexOutput(raw) {
|
|
|
483
525
|
content: truncateResponse(agentMessage),
|
|
484
526
|
activity,
|
|
485
527
|
usage,
|
|
486
|
-
raw
|
|
528
|
+
raw,
|
|
529
|
+
sessionId
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/util/text.ts
|
|
534
|
+
function oneLine(text, max) {
|
|
535
|
+
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
536
|
+
return collapsed.length > max ? collapsed.slice(0, max - 1) + "\u2026" : collapsed;
|
|
537
|
+
}
|
|
538
|
+
function baseName(p) {
|
|
539
|
+
if (!p) return "file";
|
|
540
|
+
const parts = p.split(/[\\/]/).filter(Boolean);
|
|
541
|
+
return parts.length > 0 ? parts[parts.length - 1] : p;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// src/runner/progress.ts
|
|
545
|
+
function formatProgressMessage(evt) {
|
|
546
|
+
if (!evt || typeof evt !== "object") return null;
|
|
547
|
+
const item = evt.item;
|
|
548
|
+
if (item?.type === "command_execution") {
|
|
549
|
+
const cmd = oneLine(String(item.command ?? ""), 60);
|
|
550
|
+
if (item.status === "in_progress") return `running: ${cmd}`;
|
|
551
|
+
if (item.status === "declined") return `blocked: ${cmd}`;
|
|
552
|
+
if (item.exit_code === 0) return `ran: ${cmd}`;
|
|
553
|
+
return `failed (exit ${String(item.exit_code)}): ${cmd}`;
|
|
554
|
+
}
|
|
555
|
+
if (item?.type === "file_read") return `reading ${baseName(String(item.path ?? "file"))}`;
|
|
556
|
+
if (item?.type === "file_write" || item?.type === "file_edit") {
|
|
557
|
+
return `editing ${baseName(String(item.path ?? "file"))}`;
|
|
558
|
+
}
|
|
559
|
+
if (item?.type === "file_change") {
|
|
560
|
+
const changes = Array.isArray(item.changes) ? item.changes : null;
|
|
561
|
+
if (changes && changes.length > 0) {
|
|
562
|
+
return changes.length === 1 ? `editing ${baseName(String(changes[0]?.path ?? "file"))}` : `editing ${changes.length} files`;
|
|
563
|
+
}
|
|
564
|
+
return `editing ${baseName(String(item.path ?? "file"))}`;
|
|
565
|
+
}
|
|
566
|
+
if (item?.type === "reasoning" || evt.type === "turn.started") return "thinking\u2026";
|
|
567
|
+
if (item?.type === "agent_message" && typeof item.text === "string" && evt.type !== "item.started" && evt.type !== "item.updated") {
|
|
568
|
+
return "writing response\u2026";
|
|
569
|
+
}
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/util/live-logger.ts
|
|
574
|
+
import fs2 from "fs";
|
|
575
|
+
import os3 from "os";
|
|
576
|
+
import path3 from "path";
|
|
577
|
+
import { createHash } from "crypto";
|
|
578
|
+
function resolveLogPath(cwd) {
|
|
579
|
+
const override = process.env[LOG_ENV];
|
|
580
|
+
if (override && override.trim()) return path3.resolve(override.trim());
|
|
581
|
+
const base = path3.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, "_") || "run";
|
|
582
|
+
const hash = createHash("sha1").update(cwd).digest("hex").slice(0, 8);
|
|
583
|
+
return path3.join(os3.tmpdir(), "skill-codex", `${base}-${hash}.log`);
|
|
584
|
+
}
|
|
585
|
+
function formatLogLines(evt) {
|
|
586
|
+
if (!evt || typeof evt !== "object") return [];
|
|
587
|
+
const item = evt.item;
|
|
588
|
+
if (item?.type === "command_execution") {
|
|
589
|
+
if (item.status === "in_progress") return [` $ ${oneLine(String(item.command ?? ""), 120)}`];
|
|
590
|
+
if (item.status === "declined") return [" \u2718 blocked"];
|
|
591
|
+
if (item.exit_code === 0) return [" \u2714 ok"];
|
|
592
|
+
return [` \u2718 exit ${String(item.exit_code)}`];
|
|
593
|
+
}
|
|
594
|
+
if (item?.type === "file_read") return [` read ${String(item.path ?? "file")}`];
|
|
595
|
+
if (item?.type === "file_write" || item?.type === "file_edit") {
|
|
596
|
+
return [` write ${String(item.path ?? "file")}`];
|
|
597
|
+
}
|
|
598
|
+
if (item?.type === "file_change") {
|
|
599
|
+
const changes = Array.isArray(item.changes) ? item.changes : null;
|
|
600
|
+
if (changes && changes.length > 0) {
|
|
601
|
+
return changes.map(
|
|
602
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
603
|
+
(change) => ` write ${String(change?.path ?? "file")} (${String(change?.kind ?? "write")})`
|
|
604
|
+
);
|
|
605
|
+
}
|
|
606
|
+
return [` write ${String(item.path ?? "file")}`];
|
|
607
|
+
}
|
|
608
|
+
if (item?.type === "agent_message" && typeof item.text === "string" && evt.type !== "item.started" && evt.type !== "item.updated") {
|
|
609
|
+
return [` msg ${oneLine(item.text, 400)}`];
|
|
610
|
+
}
|
|
611
|
+
if (evt.type === "message" && typeof evt.content === "string") {
|
|
612
|
+
return [` msg ${oneLine(evt.content, 400)}`];
|
|
613
|
+
}
|
|
614
|
+
if (evt.type === "turn.completed" && evt.usage) {
|
|
615
|
+
const u = evt.usage;
|
|
616
|
+
const reasoning = u.reasoning_output_tokens ?? 0;
|
|
617
|
+
return [
|
|
618
|
+
` tokens: ${u.input_tokens ?? 0} in \u2192 ${u.output_tokens ?? 0} out${reasoning > 0 ? ` (+${reasoning} reasoning)` : ""}`
|
|
619
|
+
];
|
|
620
|
+
}
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
function createLiveLogger(opts) {
|
|
624
|
+
const logPath = resolveLogPath(opts.cwd);
|
|
625
|
+
let stream = null;
|
|
626
|
+
try {
|
|
627
|
+
fs2.mkdirSync(path3.dirname(logPath), { recursive: true });
|
|
628
|
+
stream = fs2.createWriteStream(logPath, { flags: "a" });
|
|
629
|
+
} catch {
|
|
630
|
+
stream = null;
|
|
631
|
+
}
|
|
632
|
+
const line = (text) => {
|
|
633
|
+
try {
|
|
634
|
+
stream?.write(text + "\n");
|
|
635
|
+
} catch {
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
639
|
+
line("");
|
|
640
|
+
line("=".repeat(60));
|
|
641
|
+
line(`> codex ${opts.mode} ${startedAt}`);
|
|
642
|
+
line(` cwd: ${opts.cwd}`);
|
|
643
|
+
line(` task: ${oneLine(opts.prompt, 200)}`);
|
|
644
|
+
line("-".repeat(60));
|
|
645
|
+
const handleEvent = (raw) => {
|
|
646
|
+
const trimmed = raw.trim();
|
|
647
|
+
if (!trimmed) return;
|
|
648
|
+
let evt;
|
|
649
|
+
try {
|
|
650
|
+
evt = JSON.parse(trimmed);
|
|
651
|
+
} catch {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
for (const l of formatLogLines(evt)) line(l);
|
|
655
|
+
};
|
|
656
|
+
let buffer = "";
|
|
657
|
+
return {
|
|
658
|
+
path: logPath,
|
|
659
|
+
write(fragment) {
|
|
660
|
+
buffer += fragment;
|
|
661
|
+
let idx;
|
|
662
|
+
while ((idx = buffer.indexOf("\n")) >= 0) {
|
|
663
|
+
const lineStr = buffer.slice(0, idx);
|
|
664
|
+
buffer = buffer.slice(idx + 1);
|
|
665
|
+
handleEvent(lineStr);
|
|
666
|
+
}
|
|
667
|
+
},
|
|
668
|
+
finish(summary) {
|
|
669
|
+
if (buffer.trim()) {
|
|
670
|
+
handleEvent(buffer);
|
|
671
|
+
buffer = "";
|
|
672
|
+
}
|
|
673
|
+
line("-".repeat(60));
|
|
674
|
+
line(`# done ${(/* @__PURE__ */ new Date()).toISOString()} ${summary}`);
|
|
675
|
+
try {
|
|
676
|
+
stream?.end();
|
|
677
|
+
} catch {
|
|
678
|
+
}
|
|
679
|
+
}
|
|
487
680
|
};
|
|
488
681
|
}
|
|
489
682
|
|
|
@@ -524,14 +717,41 @@ async function execCodex(params) {
|
|
|
524
717
|
}
|
|
525
718
|
return new Promise((resolve, reject) => {
|
|
526
719
|
const timeoutMs = getTimeout(params.timeoutMs);
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
720
|
+
let args;
|
|
721
|
+
let sendStdinPrompt;
|
|
722
|
+
if (params.review) {
|
|
723
|
+
args = ["exec", "review", "--json", "--skip-git-repo-check"];
|
|
724
|
+
if (params.reviewBase) {
|
|
725
|
+
args.push("--base", params.reviewBase);
|
|
726
|
+
sendStdinPrompt = false;
|
|
727
|
+
} else if (params.reviewCommit) {
|
|
728
|
+
args.push("--commit", params.reviewCommit);
|
|
729
|
+
sendStdinPrompt = false;
|
|
730
|
+
} else if (params.prompt?.trim()) {
|
|
731
|
+
sendStdinPrompt = true;
|
|
732
|
+
} else {
|
|
733
|
+
args.push("--uncommitted");
|
|
734
|
+
sendStdinPrompt = false;
|
|
735
|
+
}
|
|
736
|
+
} else if (params.sessionId) {
|
|
737
|
+
args = ["exec", "resume", params.sessionId, "--json", "--skip-git-repo-check"];
|
|
738
|
+
sendStdinPrompt = true;
|
|
530
739
|
} else {
|
|
531
|
-
|
|
740
|
+
const sandbox = params.sandbox ?? (params.mode === "full-auto" ? "workspace-write" : "read-only");
|
|
741
|
+
args = ["exec", "--json", "--skip-git-repo-check", "--sandbox", sandbox];
|
|
742
|
+
sendStdinPrompt = true;
|
|
743
|
+
}
|
|
744
|
+
if (params.model) {
|
|
745
|
+
args.push("-m", params.model);
|
|
746
|
+
}
|
|
747
|
+
if (params.reasoningEffort) {
|
|
748
|
+
args.push("-c", `model_reasoning_effort=${params.reasoningEffort}`);
|
|
749
|
+
}
|
|
750
|
+
args.push(...getSandboxConfigArgs());
|
|
751
|
+
const stdinPrompt = params.prompt ?? "";
|
|
752
|
+
if (sendStdinPrompt) {
|
|
753
|
+
args.push("-");
|
|
532
754
|
}
|
|
533
|
-
const stdinPrompt = params.prompt;
|
|
534
|
-
args.push("-");
|
|
535
755
|
const env = {
|
|
536
756
|
...process.env,
|
|
537
757
|
[BRIDGE_DEPTH_ENV]: String(getNextDepth())
|
|
@@ -544,23 +764,82 @@ async function execCodex(params) {
|
|
|
544
764
|
windowsHide: true
|
|
545
765
|
});
|
|
546
766
|
const { clear: clearTimeout_, promise: timeoutPromise } = setupTimeout(child, timeoutMs);
|
|
767
|
+
const startedAt = Date.now();
|
|
768
|
+
const logger = createLiveLogger({
|
|
769
|
+
cwd: params.cwd,
|
|
770
|
+
mode: params.mode,
|
|
771
|
+
prompt: params.prompt
|
|
772
|
+
});
|
|
773
|
+
process.stderr.write(`[skill-codex] live log: ${logger.path}
|
|
774
|
+
`);
|
|
775
|
+
let heartbeat = null;
|
|
776
|
+
if (params.onProgress) {
|
|
777
|
+
heartbeat = setInterval(() => {
|
|
778
|
+
const secs = Math.round((Date.now() - startedAt) / 1e3);
|
|
779
|
+
params.onProgress?.(`Codex working\u2026 ${secs}s elapsed`);
|
|
780
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
781
|
+
if (typeof heartbeat.unref === "function") heartbeat.unref();
|
|
782
|
+
}
|
|
783
|
+
const stopHeartbeat = () => {
|
|
784
|
+
if (heartbeat) {
|
|
785
|
+
clearInterval(heartbeat);
|
|
786
|
+
heartbeat = null;
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
const decoder = new StringDecoder("utf8");
|
|
790
|
+
let progressBuf = "";
|
|
791
|
+
const consumeForProgress = (text) => {
|
|
792
|
+
if (!params.onProgress) return;
|
|
793
|
+
progressBuf += text;
|
|
794
|
+
let idx;
|
|
795
|
+
while ((idx = progressBuf.indexOf("\n")) >= 0) {
|
|
796
|
+
const lineStr = progressBuf.slice(0, idx).trim();
|
|
797
|
+
progressBuf = progressBuf.slice(idx + 1);
|
|
798
|
+
if (!lineStr) continue;
|
|
799
|
+
try {
|
|
800
|
+
const msg = formatProgressMessage(JSON.parse(lineStr));
|
|
801
|
+
if (msg) params.onProgress(msg);
|
|
802
|
+
} catch {
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
};
|
|
806
|
+
let logFinished = false;
|
|
807
|
+
const finishLog = (summary) => {
|
|
808
|
+
stopHeartbeat();
|
|
809
|
+
if (logFinished) return;
|
|
810
|
+
logFinished = true;
|
|
811
|
+
try {
|
|
812
|
+
logger.write(decoder.end());
|
|
813
|
+
logger.finish(summary);
|
|
814
|
+
} catch {
|
|
815
|
+
}
|
|
816
|
+
};
|
|
547
817
|
const stdoutChunks = [];
|
|
548
818
|
let stderr = "";
|
|
549
819
|
child.stdout?.on("data", (chunk) => {
|
|
550
820
|
stdoutChunks.push(chunk);
|
|
821
|
+
const text = decoder.write(chunk);
|
|
822
|
+
try {
|
|
823
|
+
logger.write(text);
|
|
824
|
+
} catch {
|
|
825
|
+
}
|
|
826
|
+
consumeForProgress(text);
|
|
551
827
|
});
|
|
552
828
|
child.stderr?.on("data", (chunk) => {
|
|
553
829
|
stderr += chunk.toString();
|
|
554
830
|
});
|
|
555
|
-
|
|
831
|
+
if (sendStdinPrompt) {
|
|
832
|
+
child.stdin?.write(stdinPrompt);
|
|
833
|
+
}
|
|
556
834
|
child.stdin?.end();
|
|
557
835
|
const onClose = (exitCode) => {
|
|
558
836
|
clearTimeout_();
|
|
837
|
+
finishLog(exitCode === 0 || exitCode === null ? "ok" : `exit ${exitCode}`);
|
|
559
838
|
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
560
839
|
if (exitCode === 0 || exitCode === null) {
|
|
561
840
|
try {
|
|
562
841
|
const result = parseCodexOutput(stdout);
|
|
563
|
-
resolve(result);
|
|
842
|
+
resolve({ ...result, logPath: logger.path, durationMs: Date.now() - startedAt });
|
|
564
843
|
} catch (err) {
|
|
565
844
|
reject(err);
|
|
566
845
|
}
|
|
@@ -571,6 +850,7 @@ async function execCodex(params) {
|
|
|
571
850
|
child.on("close", onClose);
|
|
572
851
|
child.on("error", (err) => {
|
|
573
852
|
clearTimeout_();
|
|
853
|
+
finishLog("spawn error");
|
|
574
854
|
if (err.code === "ENOENT") {
|
|
575
855
|
reject(new CliNotFoundError());
|
|
576
856
|
} else {
|
|
@@ -578,6 +858,7 @@ async function execCodex(params) {
|
|
|
578
858
|
}
|
|
579
859
|
});
|
|
580
860
|
timeoutPromise.catch((err) => {
|
|
861
|
+
finishLog("timeout");
|
|
581
862
|
reject(err);
|
|
582
863
|
});
|
|
583
864
|
});
|
|
@@ -632,12 +913,81 @@ async function withRetry(fn, options = {}) {
|
|
|
632
913
|
var TOOL_NAME = "codex_exec";
|
|
633
914
|
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.";
|
|
634
915
|
var inputSchema = z.object({
|
|
635
|
-
prompt: z.string().describe("The task description for Codex"),
|
|
916
|
+
prompt: z.string().optional().describe("The task description for Codex"),
|
|
636
917
|
mode: z.enum(["exec", "full-auto"]).default("exec").describe("exec = read-only with confirmation, full-auto = can write files"),
|
|
918
|
+
sandbox: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional().describe(
|
|
919
|
+
"Explicit Codex sandbox policy; overrides mode. read-only = no writes, workspace-write = write within cwd, danger-full-access = unrestricted (use with care)."
|
|
920
|
+
),
|
|
921
|
+
sessionId: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/, "sessionId must be a Codex thread id (letters, digits, '-', '_')").optional().describe(
|
|
922
|
+
"Resume a prior Codex session by its thread id (returned in a previous response) so Codex retains context across calls."
|
|
923
|
+
),
|
|
924
|
+
model: z.string().regex(/^[A-Za-z0-9._-]{1,64}$/).optional().describe(
|
|
925
|
+
"Codex model to use (e.g. gpt-5.5, gpt-5.4, gpt-5.4-mini). Omit to use Codex's configured default."
|
|
926
|
+
),
|
|
927
|
+
reasoningEffort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional().describe("How much reasoning effort Codex spends. Omit for the model's default."),
|
|
928
|
+
review: z.boolean().optional().describe(
|
|
929
|
+
"Run Codex's native diff-scoped review (`codex exec review`) instead of a freeform prompt. The prompt becomes optional custom review instructions."
|
|
930
|
+
),
|
|
931
|
+
reviewBase: z.string().regex(/^[A-Za-z0-9._\/-]{1,128}$/).optional().describe("With review: diff against this base branch (default: uncommitted changes)."),
|
|
932
|
+
reviewCommit: z.string().regex(/^[0-9a-fA-F]{4,64}$/).optional().describe("With review: review the changes introduced by this commit SHA."),
|
|
637
933
|
cwd: z.string().optional().describe("Working directory (defaults to server cwd)"),
|
|
638
934
|
timeoutMs: z.number().optional().describe("Override default timeout in milliseconds"),
|
|
639
935
|
requireGit: z.boolean().default(false).describe("Fail if not inside a git repository")
|
|
640
936
|
});
|
|
937
|
+
var TOOL_INPUT_JSON_SCHEMA = {
|
|
938
|
+
type: "object",
|
|
939
|
+
properties: {
|
|
940
|
+
prompt: { type: "string", description: "The task description for Codex" },
|
|
941
|
+
mode: {
|
|
942
|
+
type: "string",
|
|
943
|
+
enum: ["exec", "full-auto"],
|
|
944
|
+
default: "exec",
|
|
945
|
+
description: "exec = read-only, full-auto = can write files"
|
|
946
|
+
},
|
|
947
|
+
sandbox: {
|
|
948
|
+
type: "string",
|
|
949
|
+
enum: ["read-only", "workspace-write", "danger-full-access"],
|
|
950
|
+
description: "Explicit Codex sandbox policy; overrides mode. read-only = no writes, workspace-write = write within cwd, danger-full-access = unrestricted (use with care)."
|
|
951
|
+
},
|
|
952
|
+
sessionId: {
|
|
953
|
+
type: "string",
|
|
954
|
+
pattern: "^[A-Za-z0-9_-]{1,128}$",
|
|
955
|
+
description: "Resume a prior Codex session by its thread id (returned in a previous response) so Codex retains context across calls."
|
|
956
|
+
},
|
|
957
|
+
model: {
|
|
958
|
+
type: "string",
|
|
959
|
+
pattern: "^[A-Za-z0-9._-]{1,64}$",
|
|
960
|
+
description: "Codex model to use (e.g. gpt-5.5, gpt-5.4, gpt-5.4-mini). Omit to use Codex's configured default."
|
|
961
|
+
},
|
|
962
|
+
reasoningEffort: {
|
|
963
|
+
type: "string",
|
|
964
|
+
enum: ["minimal", "low", "medium", "high", "xhigh"],
|
|
965
|
+
description: "How much reasoning effort Codex spends. Omit for the model's default."
|
|
966
|
+
},
|
|
967
|
+
review: {
|
|
968
|
+
type: "boolean",
|
|
969
|
+
description: "Run Codex's native diff-scoped review (`codex exec review`) instead of a freeform prompt. The prompt becomes optional custom review instructions."
|
|
970
|
+
},
|
|
971
|
+
reviewBase: {
|
|
972
|
+
type: "string",
|
|
973
|
+
pattern: "^[A-Za-z0-9._\\/-]{1,128}$",
|
|
974
|
+
description: "With review: diff against this base branch (default: uncommitted changes)."
|
|
975
|
+
},
|
|
976
|
+
reviewCommit: {
|
|
977
|
+
type: "string",
|
|
978
|
+
pattern: "^[0-9a-fA-F]{4,64}$",
|
|
979
|
+
description: "With review: review the changes introduced by this commit SHA."
|
|
980
|
+
},
|
|
981
|
+
cwd: { type: "string", description: "Working directory (defaults to server cwd)" },
|
|
982
|
+
timeoutMs: { type: "number", description: "Override default timeout in milliseconds" },
|
|
983
|
+
requireGit: {
|
|
984
|
+
type: "boolean",
|
|
985
|
+
default: false,
|
|
986
|
+
description: "Fail if not inside a git repository"
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
required: []
|
|
990
|
+
};
|
|
641
991
|
function formatError(err) {
|
|
642
992
|
if (err instanceof BridgeError) {
|
|
643
993
|
return `[skill-codex error: ${err.code}] ${err.message}`;
|
|
@@ -649,15 +999,37 @@ function formatError(err) {
|
|
|
649
999
|
}
|
|
650
1000
|
function formatRichResponse(result, input, cwd) {
|
|
651
1001
|
const lines = [];
|
|
652
|
-
const
|
|
653
|
-
const
|
|
1002
|
+
const sandboxLabel = input.sandbox ?? (input.mode === "full-auto" ? "workspace-write" : "read-only");
|
|
1003
|
+
const label = input.review ? "review" : input.sessionId ? "resumed" : sandboxLabel;
|
|
1004
|
+
const metaParts = [label];
|
|
1005
|
+
if (input.model) {
|
|
1006
|
+
metaParts.push(input.model);
|
|
1007
|
+
}
|
|
1008
|
+
if (input.reasoningEffort) {
|
|
1009
|
+
metaParts.push(`effort:${input.reasoningEffort}`);
|
|
1010
|
+
}
|
|
1011
|
+
metaParts.push(cwd);
|
|
1012
|
+
if (typeof result.durationMs === "number") {
|
|
1013
|
+
metaParts.push(`${(result.durationMs / 1e3).toFixed(1)}s`);
|
|
1014
|
+
}
|
|
654
1015
|
if (result.usage) {
|
|
655
|
-
const {
|
|
1016
|
+
const {
|
|
1017
|
+
input_tokens: inp,
|
|
1018
|
+
output_tokens: out,
|
|
1019
|
+
cached_input_tokens: cached,
|
|
1020
|
+
reasoning_output_tokens: reasoning
|
|
1021
|
+
} = result.usage;
|
|
656
1022
|
metaParts.push(
|
|
657
|
-
`${inp} tok in${cached > 0 ? ` (${cached} cached)` : ""} \u2192 ${out} out`
|
|
1023
|
+
`${inp} tok in${cached > 0 ? ` (${cached} cached)` : ""} \u2192 ${out} out${reasoning > 0 ? ` (+${reasoning} reasoning)` : ""}`
|
|
658
1024
|
);
|
|
659
1025
|
}
|
|
660
1026
|
lines.push(`[${metaParts.join(" \u2502 ")}]`);
|
|
1027
|
+
if (result.sessionId) {
|
|
1028
|
+
lines.push(` session: ${result.sessionId} (pass as sessionId to continue this conversation)`);
|
|
1029
|
+
}
|
|
1030
|
+
if (result.logPath) {
|
|
1031
|
+
lines.push(` live log: ${result.logPath}`);
|
|
1032
|
+
}
|
|
661
1033
|
if (result.activity.length > 0) {
|
|
662
1034
|
for (const a of result.activity) {
|
|
663
1035
|
if (a.type === "exec") {
|
|
@@ -673,15 +1045,35 @@ function formatRichResponse(result, input, cwd) {
|
|
|
673
1045
|
lines.push(result.content);
|
|
674
1046
|
return lines.join("\n");
|
|
675
1047
|
}
|
|
676
|
-
async function handleCodexExec(input, serverCwd) {
|
|
1048
|
+
async function handleCodexExec(input, serverCwd, onProgress) {
|
|
677
1049
|
const rawCwd = input.cwd ?? serverCwd;
|
|
678
|
-
const cwd =
|
|
679
|
-
if (!
|
|
1050
|
+
const cwd = path4.resolve(rawCwd);
|
|
1051
|
+
if (!fs3.existsSync(cwd) || !fs3.statSync(cwd).isDirectory()) {
|
|
680
1052
|
return {
|
|
681
1053
|
content: [{ type: "text", text: `[skill-codex error: INVALID_CWD] cwd is not an existing directory: ${cwd}` }],
|
|
682
1054
|
isError: true
|
|
683
1055
|
};
|
|
684
1056
|
}
|
|
1057
|
+
const optError = (msg) => ({
|
|
1058
|
+
content: [{ type: "text", text: `[skill-codex error: INVALID_OPTIONS] ${msg}` }],
|
|
1059
|
+
isError: true
|
|
1060
|
+
});
|
|
1061
|
+
if (input.review && input.sessionId) return optError("review and sessionId are mutually exclusive");
|
|
1062
|
+
if (input.reviewBase && input.reviewCommit) return optError("reviewBase and reviewCommit are mutually exclusive");
|
|
1063
|
+
if ((input.reviewBase ?? input.reviewCommit) && !input.review) {
|
|
1064
|
+
return optError("reviewBase/reviewCommit require review: true");
|
|
1065
|
+
}
|
|
1066
|
+
if (input.review && (input.reviewBase ?? input.reviewCommit) && input.prompt?.trim()) {
|
|
1067
|
+
return optError(
|
|
1068
|
+
"a review target (reviewBase/reviewCommit) can't be combined with a prompt \u2014 Codex review takes a target OR instructions, not both"
|
|
1069
|
+
);
|
|
1070
|
+
}
|
|
1071
|
+
if (!input.review && !input.prompt?.trim()) {
|
|
1072
|
+
return {
|
|
1073
|
+
content: [{ type: "text", text: "[skill-codex error: MISSING_PROMPT] prompt is required unless review is set" }],
|
|
1074
|
+
isError: true
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
685
1077
|
let lockRelease = null;
|
|
686
1078
|
try {
|
|
687
1079
|
const { lockHandle } = await runPreflight({
|
|
@@ -691,10 +1083,18 @@ async function handleCodexExec(input, serverCwd) {
|
|
|
691
1083
|
lockRelease = lockHandle?.release ?? null;
|
|
692
1084
|
const result = await withRetry(
|
|
693
1085
|
() => execCodex({
|
|
694
|
-
prompt: input.prompt,
|
|
1086
|
+
prompt: input.prompt ?? "",
|
|
695
1087
|
cwd,
|
|
696
1088
|
mode: input.mode,
|
|
697
|
-
|
|
1089
|
+
sandbox: input.sandbox,
|
|
1090
|
+
sessionId: input.sessionId,
|
|
1091
|
+
model: input.model,
|
|
1092
|
+
reasoningEffort: input.reasoningEffort,
|
|
1093
|
+
review: input.review,
|
|
1094
|
+
reviewBase: input.reviewBase,
|
|
1095
|
+
reviewCommit: input.reviewCommit,
|
|
1096
|
+
timeoutMs: input.timeoutMs,
|
|
1097
|
+
onProgress
|
|
698
1098
|
})
|
|
699
1099
|
);
|
|
700
1100
|
const formatted = formatRichResponse(result, input, cwd);
|
|
@@ -714,7 +1114,7 @@ async function handleCodexExec(input, serverCwd) {
|
|
|
714
1114
|
// src/server.ts
|
|
715
1115
|
function createServer(cwd) {
|
|
716
1116
|
const server = new Server(
|
|
717
|
-
{ name: "skill-codex", version: "0.
|
|
1117
|
+
{ name: "skill-codex", version: "0.7.1" },
|
|
718
1118
|
{ capabilities: { tools: {} } }
|
|
719
1119
|
);
|
|
720
1120
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -722,30 +1122,11 @@ function createServer(cwd) {
|
|
|
722
1122
|
{
|
|
723
1123
|
name: TOOL_NAME,
|
|
724
1124
|
description: TOOL_DESCRIPTION,
|
|
725
|
-
inputSchema:
|
|
726
|
-
type: "object",
|
|
727
|
-
properties: {
|
|
728
|
-
prompt: { type: "string", description: "The task description for Codex" },
|
|
729
|
-
mode: {
|
|
730
|
-
type: "string",
|
|
731
|
-
enum: ["exec", "full-auto"],
|
|
732
|
-
default: "exec",
|
|
733
|
-
description: "exec = read-only, full-auto = can write files"
|
|
734
|
-
},
|
|
735
|
-
cwd: { type: "string", description: "Working directory (defaults to server cwd)" },
|
|
736
|
-
timeoutMs: { type: "number", description: "Override default timeout in milliseconds" },
|
|
737
|
-
requireGit: {
|
|
738
|
-
type: "boolean",
|
|
739
|
-
default: false,
|
|
740
|
-
description: "Fail if not inside a git repository"
|
|
741
|
-
}
|
|
742
|
-
},
|
|
743
|
-
required: ["prompt"]
|
|
744
|
-
}
|
|
1125
|
+
inputSchema: TOOL_INPUT_JSON_SCHEMA
|
|
745
1126
|
}
|
|
746
1127
|
]
|
|
747
1128
|
}));
|
|
748
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1129
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
749
1130
|
if (request.params.name !== TOOL_NAME) {
|
|
750
1131
|
return {
|
|
751
1132
|
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
@@ -764,7 +1145,17 @@ function createServer(cwd) {
|
|
|
764
1145
|
isError: true
|
|
765
1146
|
};
|
|
766
1147
|
}
|
|
767
|
-
|
|
1148
|
+
const progressToken = request.params._meta?.progressToken;
|
|
1149
|
+
let progressCounter = 0;
|
|
1150
|
+
const onProgress = progressToken === void 0 ? void 0 : (message) => {
|
|
1151
|
+
progressCounter += 1;
|
|
1152
|
+
void extra.sendNotification({
|
|
1153
|
+
method: "notifications/progress",
|
|
1154
|
+
params: { progressToken, progress: progressCounter, message }
|
|
1155
|
+
}).catch(() => {
|
|
1156
|
+
});
|
|
1157
|
+
};
|
|
1158
|
+
return handleCodexExec(parsed.data, cwd, onProgress);
|
|
768
1159
|
});
|
|
769
1160
|
return server;
|
|
770
1161
|
}
|