skill-codex 0.2.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 +72 -20
- 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 +510 -47
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- 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,8 +197,8 @@ 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"],
|
|
187
|
-
{ timeout:
|
|
200
|
+
["exec", "--sandbox", "read-only", ...getSandboxConfigArgs(), "--skip-git-repo-check", "--ephemeral", "echo ok"],
|
|
201
|
+
{ timeout: 3e4, shell: process.platform === "win32" },
|
|
188
202
|
(error, _stdout, stderr) => {
|
|
189
203
|
if (!error) {
|
|
190
204
|
authCachedAt = Date.now();
|
|
@@ -322,7 +336,7 @@ function checkGit(cwd) {
|
|
|
322
336
|
async function runPreflight(options) {
|
|
323
337
|
checkRecursion();
|
|
324
338
|
await checkBinary();
|
|
325
|
-
if (!options.skipAuth) {
|
|
339
|
+
if (!options.skipAuth && process.platform !== "win32") {
|
|
326
340
|
await checkAuth();
|
|
327
341
|
}
|
|
328
342
|
let lockHandle = null;
|
|
@@ -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";
|
|
@@ -399,10 +414,26 @@ function parseCodexOutput(raw) {
|
|
|
399
414
|
}
|
|
400
415
|
const lines = raw.split("\n").filter((line) => line.trim());
|
|
401
416
|
const messages = [];
|
|
417
|
+
const activity = [];
|
|
402
418
|
let resultContent = null;
|
|
419
|
+
let sessionId;
|
|
420
|
+
let usage = null;
|
|
403
421
|
for (const line of lines) {
|
|
404
422
|
try {
|
|
405
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
|
+
}
|
|
428
|
+
if (parsed.type === "turn.completed" && parsed.usage) {
|
|
429
|
+
usage = {
|
|
430
|
+
input_tokens: parsed.usage.input_tokens ?? 0,
|
|
431
|
+
cached_input_tokens: parsed.usage.cached_input_tokens ?? 0,
|
|
432
|
+
output_tokens: parsed.usage.output_tokens ?? 0,
|
|
433
|
+
reasoning_output_tokens: parsed.usage.reasoning_output_tokens ?? 0
|
|
434
|
+
};
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
406
437
|
if (parsed.type === "result" && typeof parsed.content === "string") {
|
|
407
438
|
resultContent = parsed.content;
|
|
408
439
|
continue;
|
|
@@ -411,7 +442,22 @@ function parseCodexOutput(raw) {
|
|
|
411
442
|
messages.push(parsed.content);
|
|
412
443
|
continue;
|
|
413
444
|
}
|
|
414
|
-
if (parsed.item?.type === "
|
|
445
|
+
if (parsed.item?.type === "command_execution") {
|
|
446
|
+
const cmd = parsed.item;
|
|
447
|
+
const shortCmd = cmd.command?.length > 80 ? cmd.command.slice(0, 77) + "..." : cmd.command;
|
|
448
|
+
const statusIcon = cmd.status === "declined" ? "\u2718" : cmd.exit_code === 0 ? "\u2714" : cmd.exit_code !== null ? "\u2718" : "\u25B6";
|
|
449
|
+
const statusLabel = cmd.status === "declined" ? "blocked" : cmd.status === "in_progress" ? "running" : cmd.exit_code === 0 ? "ok" : `exit ${cmd.exit_code}`;
|
|
450
|
+
if (cmd.status !== "in_progress") {
|
|
451
|
+
activity.push({
|
|
452
|
+
type: "exec",
|
|
453
|
+
command: shortCmd,
|
|
454
|
+
icon: statusIcon,
|
|
455
|
+
status: statusLabel
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (parsed.item?.type === "agent_message" && typeof parsed.item.text === "string" && parsed.type !== "item.started" && parsed.type !== "item.updated") {
|
|
415
461
|
messages.push(parsed.item.text);
|
|
416
462
|
continue;
|
|
417
463
|
}
|
|
@@ -419,6 +465,45 @@ function parseCodexOutput(raw) {
|
|
|
419
465
|
messages.push(parsed.text);
|
|
420
466
|
continue;
|
|
421
467
|
}
|
|
468
|
+
if (parsed.item?.type === "file_read") {
|
|
469
|
+
activity.push({
|
|
470
|
+
type: "read",
|
|
471
|
+
path: parsed.item.path || "file",
|
|
472
|
+
icon: "\u25B6",
|
|
473
|
+
status: "read"
|
|
474
|
+
});
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
if (parsed.item?.type === "file_write" || parsed.item?.type === "file_edit") {
|
|
478
|
+
activity.push({
|
|
479
|
+
type: "write",
|
|
480
|
+
path: parsed.item.path || "file",
|
|
481
|
+
icon: "\u270E",
|
|
482
|
+
status: "write"
|
|
483
|
+
});
|
|
484
|
+
continue;
|
|
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
|
+
}
|
|
422
507
|
} catch {
|
|
423
508
|
}
|
|
424
509
|
}
|
|
@@ -438,7 +523,160 @@ function parseCodexOutput(raw) {
|
|
|
438
523
|
}
|
|
439
524
|
return {
|
|
440
525
|
content: truncateResponse(agentMessage),
|
|
441
|
-
|
|
526
|
+
activity,
|
|
527
|
+
usage,
|
|
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
|
+
}
|
|
442
680
|
};
|
|
443
681
|
}
|
|
444
682
|
|
|
@@ -479,14 +717,41 @@ async function execCodex(params) {
|
|
|
479
717
|
}
|
|
480
718
|
return new Promise((resolve, reject) => {
|
|
481
719
|
const timeoutMs = getTimeout(params.timeoutMs);
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
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;
|
|
485
739
|
} else {
|
|
486
|
-
|
|
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("-");
|
|
487
754
|
}
|
|
488
|
-
const stdinPrompt = params.prompt;
|
|
489
|
-
args.push("-");
|
|
490
755
|
const env = {
|
|
491
756
|
...process.env,
|
|
492
757
|
[BRIDGE_DEPTH_ENV]: String(getNextDepth())
|
|
@@ -495,27 +760,86 @@ async function execCodex(params) {
|
|
|
495
760
|
cwd: params.cwd,
|
|
496
761
|
env,
|
|
497
762
|
stdio: ["pipe", "pipe", "pipe"],
|
|
498
|
-
shell:
|
|
763
|
+
shell: process.platform === "win32",
|
|
499
764
|
windowsHide: true
|
|
500
765
|
});
|
|
501
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
|
+
};
|
|
502
817
|
const stdoutChunks = [];
|
|
503
818
|
let stderr = "";
|
|
504
819
|
child.stdout?.on("data", (chunk) => {
|
|
505
820
|
stdoutChunks.push(chunk);
|
|
821
|
+
const text = decoder.write(chunk);
|
|
822
|
+
try {
|
|
823
|
+
logger.write(text);
|
|
824
|
+
} catch {
|
|
825
|
+
}
|
|
826
|
+
consumeForProgress(text);
|
|
506
827
|
});
|
|
507
828
|
child.stderr?.on("data", (chunk) => {
|
|
508
829
|
stderr += chunk.toString();
|
|
509
830
|
});
|
|
510
|
-
|
|
831
|
+
if (sendStdinPrompt) {
|
|
832
|
+
child.stdin?.write(stdinPrompt);
|
|
833
|
+
}
|
|
511
834
|
child.stdin?.end();
|
|
512
835
|
const onClose = (exitCode) => {
|
|
513
836
|
clearTimeout_();
|
|
837
|
+
finishLog(exitCode === 0 || exitCode === null ? "ok" : `exit ${exitCode}`);
|
|
514
838
|
const stdout = Buffer.concat(stdoutChunks).toString();
|
|
515
839
|
if (exitCode === 0 || exitCode === null) {
|
|
516
840
|
try {
|
|
517
841
|
const result = parseCodexOutput(stdout);
|
|
518
|
-
resolve(result);
|
|
842
|
+
resolve({ ...result, logPath: logger.path, durationMs: Date.now() - startedAt });
|
|
519
843
|
} catch (err) {
|
|
520
844
|
reject(err);
|
|
521
845
|
}
|
|
@@ -526,6 +850,7 @@ async function execCodex(params) {
|
|
|
526
850
|
child.on("close", onClose);
|
|
527
851
|
child.on("error", (err) => {
|
|
528
852
|
clearTimeout_();
|
|
853
|
+
finishLog("spawn error");
|
|
529
854
|
if (err.code === "ENOENT") {
|
|
530
855
|
reject(new CliNotFoundError());
|
|
531
856
|
} else {
|
|
@@ -533,6 +858,7 @@ async function execCodex(params) {
|
|
|
533
858
|
}
|
|
534
859
|
});
|
|
535
860
|
timeoutPromise.catch((err) => {
|
|
861
|
+
finishLog("timeout");
|
|
536
862
|
reject(err);
|
|
537
863
|
});
|
|
538
864
|
});
|
|
@@ -587,12 +913,81 @@ async function withRetry(fn, options = {}) {
|
|
|
587
913
|
var TOOL_NAME = "codex_exec";
|
|
588
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.";
|
|
589
915
|
var inputSchema = z.object({
|
|
590
|
-
prompt: z.string().describe("The task description for Codex"),
|
|
916
|
+
prompt: z.string().optional().describe("The task description for Codex"),
|
|
591
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."),
|
|
592
933
|
cwd: z.string().optional().describe("Working directory (defaults to server cwd)"),
|
|
593
934
|
timeoutMs: z.number().optional().describe("Override default timeout in milliseconds"),
|
|
594
935
|
requireGit: z.boolean().default(false).describe("Fail if not inside a git repository")
|
|
595
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
|
+
};
|
|
596
991
|
function formatError(err) {
|
|
597
992
|
if (err instanceof BridgeError) {
|
|
598
993
|
return `[skill-codex error: ${err.code}] ${err.message}`;
|
|
@@ -602,15 +997,83 @@ function formatError(err) {
|
|
|
602
997
|
}
|
|
603
998
|
return `[skill-codex error] Unknown error: ${String(err)}`;
|
|
604
999
|
}
|
|
605
|
-
|
|
1000
|
+
function formatRichResponse(result, input, cwd) {
|
|
1001
|
+
const lines = [];
|
|
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
|
+
}
|
|
1015
|
+
if (result.usage) {
|
|
1016
|
+
const {
|
|
1017
|
+
input_tokens: inp,
|
|
1018
|
+
output_tokens: out,
|
|
1019
|
+
cached_input_tokens: cached,
|
|
1020
|
+
reasoning_output_tokens: reasoning
|
|
1021
|
+
} = result.usage;
|
|
1022
|
+
metaParts.push(
|
|
1023
|
+
`${inp} tok in${cached > 0 ? ` (${cached} cached)` : ""} \u2192 ${out} out${reasoning > 0 ? ` (+${reasoning} reasoning)` : ""}`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
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
|
+
}
|
|
1033
|
+
if (result.activity.length > 0) {
|
|
1034
|
+
for (const a of result.activity) {
|
|
1035
|
+
if (a.type === "exec") {
|
|
1036
|
+
lines.push(` ${a.icon} exec: ${a.command} (${a.status})`);
|
|
1037
|
+
} else if (a.type === "read") {
|
|
1038
|
+
lines.push(` \u25B6 read: ${a.path}`);
|
|
1039
|
+
} else if (a.type === "write") {
|
|
1040
|
+
lines.push(` \u270E write: ${a.path}`);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
lines.push("");
|
|
1045
|
+
lines.push(result.content);
|
|
1046
|
+
return lines.join("\n");
|
|
1047
|
+
}
|
|
1048
|
+
async function handleCodexExec(input, serverCwd, onProgress) {
|
|
606
1049
|
const rawCwd = input.cwd ?? serverCwd;
|
|
607
|
-
const cwd =
|
|
608
|
-
if (!
|
|
1050
|
+
const cwd = path4.resolve(rawCwd);
|
|
1051
|
+
if (!fs3.existsSync(cwd) || !fs3.statSync(cwd).isDirectory()) {
|
|
609
1052
|
return {
|
|
610
1053
|
content: [{ type: "text", text: `[skill-codex error: INVALID_CWD] cwd is not an existing directory: ${cwd}` }],
|
|
611
1054
|
isError: true
|
|
612
1055
|
};
|
|
613
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
|
+
}
|
|
614
1077
|
let lockRelease = null;
|
|
615
1078
|
try {
|
|
616
1079
|
const { lockHandle } = await runPreflight({
|
|
@@ -620,14 +1083,23 @@ async function handleCodexExec(input, serverCwd) {
|
|
|
620
1083
|
lockRelease = lockHandle?.release ?? null;
|
|
621
1084
|
const result = await withRetry(
|
|
622
1085
|
() => execCodex({
|
|
623
|
-
prompt: input.prompt,
|
|
1086
|
+
prompt: input.prompt ?? "",
|
|
624
1087
|
cwd,
|
|
625
1088
|
mode: input.mode,
|
|
626
|
-
|
|
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
|
|
627
1098
|
})
|
|
628
1099
|
);
|
|
1100
|
+
const formatted = formatRichResponse(result, input, cwd);
|
|
629
1101
|
return {
|
|
630
|
-
content: [{ type: "text", text:
|
|
1102
|
+
content: [{ type: "text", text: formatted }]
|
|
631
1103
|
};
|
|
632
1104
|
} catch (err) {
|
|
633
1105
|
return {
|
|
@@ -642,7 +1114,7 @@ async function handleCodexExec(input, serverCwd) {
|
|
|
642
1114
|
// src/server.ts
|
|
643
1115
|
function createServer(cwd) {
|
|
644
1116
|
const server = new Server(
|
|
645
|
-
{ name: "skill-codex", version: "0.
|
|
1117
|
+
{ name: "skill-codex", version: "0.7.1" },
|
|
646
1118
|
{ capabilities: { tools: {} } }
|
|
647
1119
|
);
|
|
648
1120
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
@@ -650,30 +1122,11 @@ function createServer(cwd) {
|
|
|
650
1122
|
{
|
|
651
1123
|
name: TOOL_NAME,
|
|
652
1124
|
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
|
-
}
|
|
1125
|
+
inputSchema: TOOL_INPUT_JSON_SCHEMA
|
|
673
1126
|
}
|
|
674
1127
|
]
|
|
675
1128
|
}));
|
|
676
|
-
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
1129
|
+
server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
677
1130
|
if (request.params.name !== TOOL_NAME) {
|
|
678
1131
|
return {
|
|
679
1132
|
content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
|
|
@@ -692,7 +1145,17 @@ function createServer(cwd) {
|
|
|
692
1145
|
isError: true
|
|
693
1146
|
};
|
|
694
1147
|
}
|
|
695
|
-
|
|
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);
|
|
696
1159
|
});
|
|
697
1160
|
return server;
|
|
698
1161
|
}
|