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/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,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
- const args = ["exec", "--json", "--skip-git-repo-check"];
528
- if (params.mode === "full-auto") {
529
- 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;
530
739
  } else {
531
- 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("-");
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
- child.stdin?.write(stdinPrompt);
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 modeLabel = input.mode === "full-auto" ? "full-auto" : "read-only";
653
- const metaParts = [modeLabel, cwd];
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 { input_tokens: inp, output_tokens: out, cached_input_tokens: cached } = 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;
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 = path3.resolve(rawCwd);
679
- if (!fs2.existsSync(cwd) || !fs2.statSync(cwd).isDirectory()) {
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
- 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
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.2.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
- 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);
768
1159
  });
769
1160
  return server;
770
1161
  }