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/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 fs2 from "fs";
11
- import path3 from "path";
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 = 6e5;
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: 15e3 },
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 === "agent_message" && typeof parsed.item.text === "string") {
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
- raw
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
- const args = ["exec", "--json", "--skip-git-repo-check"];
483
- if (params.mode === "full-auto") {
484
- args.push("--full-auto");
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
- args.push("--sandbox", "read-only");
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: false,
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
- child.stdin?.write(stdinPrompt);
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
- async function handleCodexExec(input, serverCwd) {
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 = path3.resolve(rawCwd);
608
- if (!fs2.existsSync(cwd) || !fs2.statSync(cwd).isDirectory()) {
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
- timeoutMs: input.timeoutMs
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: result.content }]
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.2.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
- return handleCodexExec(parsed.data, cwd);
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
  }