skill-codex 0.3.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // bin/skill-codex.ts
4
- import fs7 from "fs";
5
- import path8 from "path";
4
+ import fs11 from "fs";
5
+ import path13 from "path";
6
6
  import { fileURLToPath } from "url";
7
7
 
8
8
  // setup/setup.ts
9
- import fs6 from "fs";
10
- import path7 from "path";
9
+ import fs7 from "fs";
10
+ import path9 from "path";
11
11
 
12
12
  // setup/install-mcp.ts
13
13
  import fs2 from "fs";
@@ -44,6 +44,9 @@ function getGlobalMcpConfigPath() {
44
44
  function getGlobalCommandsDir() {
45
45
  return path2.join(getClaudeDir(), "commands");
46
46
  }
47
+ function getGlobalSkillsDir() {
48
+ return path2.join(getClaudeDir(), "skills");
49
+ }
47
50
 
48
51
  // src/config/package-root.ts
49
52
  import fs from "fs";
@@ -243,8 +246,79 @@ function installHook() {
243
246
  };
244
247
  }
245
248
 
246
- // setup/verify.ts
249
+ // setup/install-skill.ts
247
250
  import fs5 from "fs";
251
+ import path7 from "path";
252
+ var SKILL_NAME = "codex-bridge";
253
+ function getSkillSourceDir() {
254
+ return path7.join(getPackageRoot(), "skills", SKILL_NAME);
255
+ }
256
+ function copyDirRecursive(source, target) {
257
+ const copied = [];
258
+ if (!fs5.existsSync(target)) {
259
+ fs5.mkdirSync(target, { recursive: true });
260
+ }
261
+ const entries = fs5.readdirSync(source, { withFileTypes: true });
262
+ for (const entry of entries) {
263
+ const sourcePath = path7.join(source, entry.name);
264
+ const targetPath = path7.join(target, entry.name);
265
+ if (entry.isDirectory()) {
266
+ copied.push(...copyDirRecursive(sourcePath, targetPath));
267
+ } else if (entry.isFile()) {
268
+ const sourceContent = fs5.readFileSync(sourcePath);
269
+ let shouldWrite = true;
270
+ if (fs5.existsSync(targetPath)) {
271
+ const existing = fs5.readFileSync(targetPath);
272
+ shouldWrite = !existing.equals(sourceContent);
273
+ }
274
+ if (shouldWrite) {
275
+ fs5.writeFileSync(targetPath, sourceContent);
276
+ copied.push(entry.name);
277
+ }
278
+ }
279
+ }
280
+ return copied;
281
+ }
282
+ function installSkill() {
283
+ const sourceDir = getSkillSourceDir();
284
+ const targetDir = path7.join(getGlobalSkillsDir(), SKILL_NAME);
285
+ if (!fs5.existsSync(sourceDir)) {
286
+ return {
287
+ installed: false,
288
+ targetDir,
289
+ copiedFiles: [],
290
+ message: `Skill source not found at ${sourceDir}`
291
+ };
292
+ }
293
+ const skillFile = path7.join(sourceDir, "SKILL.md");
294
+ if (!fs5.existsSync(skillFile)) {
295
+ return {
296
+ installed: false,
297
+ targetDir,
298
+ copiedFiles: [],
299
+ message: `SKILL.md missing in ${sourceDir}`
300
+ };
301
+ }
302
+ const copied = copyDirRecursive(sourceDir, targetDir);
303
+ return {
304
+ installed: true,
305
+ targetDir,
306
+ copiedFiles: copied,
307
+ message: copied.length > 0 ? `Installed skill '${SKILL_NAME}' to ${targetDir} (${copied.length} file(s))` : `Skill '${SKILL_NAME}' already up to date`
308
+ };
309
+ }
310
+ function uninstallSkill() {
311
+ const targetDir = path7.join(getGlobalSkillsDir(), SKILL_NAME);
312
+ if (!fs5.existsSync(targetDir)) {
313
+ return { removed: false, targetDir };
314
+ }
315
+ fs5.rmSync(targetDir, { recursive: true, force: true });
316
+ return { removed: true, targetDir };
317
+ }
318
+
319
+ // setup/verify.ts
320
+ import fs6 from "fs";
321
+ import path8 from "path";
248
322
  import which from "which";
249
323
  async function runVerification() {
250
324
  const results = [];
@@ -264,9 +338,9 @@ async function runVerification() {
264
338
  }
265
339
  const mcpPath = getGlobalMcpConfigPath();
266
340
  let mcpRegistered = false;
267
- if (fs5.existsSync(mcpPath)) {
341
+ if (fs6.existsSync(mcpPath)) {
268
342
  try {
269
- const raw = fs5.readFileSync(mcpPath, "utf-8");
343
+ const raw = fs6.readFileSync(mcpPath, "utf-8");
270
344
  const config = JSON.parse(raw);
271
345
  mcpRegistered = "skill-codex" in (config.mcpServers ?? {});
272
346
  } catch {
@@ -280,13 +354,20 @@ async function runVerification() {
280
354
  const commandsDir = getGlobalCommandsDir();
281
355
  const expectedCommands = ["codex-review.md", "codex-do.md", "codex-consult.md"];
282
356
  const missingCommands = expectedCommands.filter(
283
- (cmd) => !fs5.existsSync(`${commandsDir}/${cmd}`)
357
+ (cmd) => !fs6.existsSync(`${commandsDir}/${cmd}`)
284
358
  );
285
359
  results.push({
286
360
  name: "Slash commands installed",
287
361
  pass: missingCommands.length === 0,
288
362
  detail: missingCommands.length === 0 ? `All 3 commands in ${commandsDir}` : `Missing: ${missingCommands.join(", ")}`
289
363
  });
364
+ const skillFile = path8.join(getGlobalSkillsDir(), "codex-bridge", "SKILL.md");
365
+ const skillInstalled = fs6.existsSync(skillFile);
366
+ results.push({
367
+ name: "Agent skill installed",
368
+ pass: skillInstalled,
369
+ detail: skillInstalled ? skillFile : `Not found at ${skillFile}`
370
+ });
290
371
  return {
291
372
  results,
292
373
  allPassed: results.every((r) => r.pass)
@@ -309,6 +390,9 @@ async function runSetup(options = {}) {
309
390
  log(" ", "Registering auto-review hook...");
310
391
  const hookResult = installHook();
311
392
  log(hookResult.installed ? "[ok]" : "[--]", hookResult.message);
393
+ log(" ", "Installing agent skill...");
394
+ const skillResult = installSkill();
395
+ log(skillResult.installed ? "[ok]" : "[--]", skillResult.message);
312
396
  log("\n ", "Verifying installation...\n");
313
397
  const verification = await runVerification();
314
398
  for (const check of verification.results) {
@@ -323,6 +407,8 @@ async function runSetup(options = {}) {
323
407
  log(" ", " /codex-do - Delegate a task to Codex");
324
408
  log(" ", " /codex-consult - Get a second opinion from Codex");
325
409
  log("", "");
410
+ log(" ", "Agent skill installed: codex-bridge (auto-triggers on implementation/review requests)");
411
+ log("", "");
326
412
  log(" ", "Tip: Add .skill-codex.lock to your .gitignore");
327
413
  } else {
328
414
  log("[!!]", "Setup completed with warnings. Fix the issues above and run: npx skill-codex verify\n");
@@ -333,12 +419,12 @@ async function runUninstall() {
333
419
  log(">>", "skill-codex uninstall\n");
334
420
  const mcpConfigPath = getGlobalMcpConfigPath();
335
421
  try {
336
- const raw = fs6.readFileSync(mcpConfigPath, "utf-8");
422
+ const raw = fs7.readFileSync(mcpConfigPath, "utf-8");
337
423
  const config = JSON.parse(raw);
338
424
  const mcpServers = config.mcpServers;
339
425
  if (mcpServers && "skill-codex" in mcpServers) {
340
426
  delete mcpServers["skill-codex"];
341
- fs6.writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
427
+ fs7.writeFileSync(mcpConfigPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
342
428
  log("[ok]", "Removed MCP server");
343
429
  } else {
344
430
  log("[--]", "MCP server not found");
@@ -349,17 +435,23 @@ async function runUninstall() {
349
435
  const commandsDir = getGlobalCommandsDir();
350
436
  const commandFiles = ["codex-review.md", "codex-do.md", "codex-consult.md"];
351
437
  for (const file of commandFiles) {
352
- const filePath = path7.join(commandsDir, file);
438
+ const filePath = path9.join(commandsDir, file);
353
439
  try {
354
- fs6.unlinkSync(filePath);
440
+ fs7.unlinkSync(filePath);
355
441
  log("[ok]", `Removed ${file}`);
356
442
  } catch {
357
443
  log("[--]", `${file} not found`);
358
444
  }
359
445
  }
446
+ const skillUninstall = uninstallSkill();
447
+ if (skillUninstall.removed) {
448
+ log("[ok]", `Removed skill: ${skillUninstall.targetDir}`);
449
+ } else {
450
+ log("[--]", "Agent skill not found");
451
+ }
360
452
  const settingsPath = getClaudeSettingsPath();
361
453
  try {
362
- const raw = fs6.readFileSync(settingsPath, "utf-8");
454
+ const raw = fs7.readFileSync(settingsPath, "utf-8");
363
455
  const settings = JSON.parse(raw);
364
456
  const hooks = settings.hooks;
365
457
  if (hooks) {
@@ -372,7 +464,7 @@ async function runUninstall() {
372
464
  if (hooks[eventType].length < before) removed = true;
373
465
  }
374
466
  if (removed) {
375
- fs6.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
467
+ fs7.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
376
468
  log("[ok]", "Removed PostToolUse hook");
377
469
  } else {
378
470
  log("[--]", "PostToolUse hook not found");
@@ -387,14 +479,1180 @@ async function runUninstall() {
387
479
  log("[ok]", "Uninstall complete. Restart Claude Code to apply changes.");
388
480
  }
389
481
 
482
+ // src/server.ts
483
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
484
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
485
+ import {
486
+ CallToolRequestSchema,
487
+ ListToolsRequestSchema
488
+ } from "@modelcontextprotocol/sdk/types.js";
489
+
490
+ // src/tools/codex-exec.ts
491
+ import fs10 from "fs";
492
+ import path12 from "path";
493
+ import { z } from "zod";
494
+
495
+ // src/errors/errors.ts
496
+ var BridgeError = class extends Error {
497
+ code;
498
+ retryable;
499
+ constructor(message, code, retryable) {
500
+ super(message);
501
+ this.name = "BridgeError";
502
+ this.code = code;
503
+ this.retryable = retryable;
504
+ }
505
+ };
506
+ var CliNotFoundError = class extends BridgeError {
507
+ constructor(binary = "codex") {
508
+ super(
509
+ `${binary} CLI not found on PATH. Install it with: npm i -g @openai/codex`,
510
+ "CLI_NOT_FOUND",
511
+ false
512
+ );
513
+ this.name = "CliNotFoundError";
514
+ }
515
+ };
516
+ var AuthExpiredError = class extends BridgeError {
517
+ constructor() {
518
+ super(
519
+ "Codex authentication expired or not found. Run `codex login` to re-authenticate.",
520
+ "AUTH_EXPIRED",
521
+ false
522
+ );
523
+ this.name = "AuthExpiredError";
524
+ }
525
+ };
526
+ var RecursionLimitError = class extends BridgeError {
527
+ constructor(depth, max) {
528
+ super(
529
+ `Maximum bridge nesting depth reached (${depth} >= ${max}). This prevents infinite recursion between Claude and Codex.`,
530
+ "RECURSION_LIMIT",
531
+ false
532
+ );
533
+ this.name = "RecursionLimitError";
534
+ }
535
+ };
536
+ var LockConflictError = class extends BridgeError {
537
+ constructor(pid) {
538
+ super(
539
+ `Another skill-codex instance is running (PID ${pid}). Wait for it to finish or delete the lock file.`,
540
+ "LOCK_CONFLICT",
541
+ false
542
+ );
543
+ this.name = "LockConflictError";
544
+ }
545
+ };
546
+ var TimeoutError = class extends BridgeError {
547
+ constructor(timeoutMs) {
548
+ super(
549
+ `Codex timed out after ${Math.round(timeoutMs / 1e3)}s. Increase SKILL_CODEX_TIMEOUT_MS if needed.`,
550
+ "TIMEOUT",
551
+ true
552
+ );
553
+ this.name = "TimeoutError";
554
+ }
555
+ };
556
+ var RateLimitError = class extends BridgeError {
557
+ constructor() {
558
+ super(
559
+ "Codex rate limited (429). Will retry with backoff.",
560
+ "RATE_LIMIT",
561
+ true
562
+ );
563
+ this.name = "RateLimitError";
564
+ }
565
+ };
566
+ var ServerError = class extends BridgeError {
567
+ constructor(detail = "") {
568
+ super(
569
+ `Codex server error${detail ? `: ${detail}` : ""}. Will retry.`,
570
+ "SERVER_ERROR",
571
+ true
572
+ );
573
+ this.name = "ServerError";
574
+ }
575
+ };
576
+ var NetworkError = class extends BridgeError {
577
+ constructor(detail = "") {
578
+ super(
579
+ `Network error connecting to Codex${detail ? `: ${detail}` : ""}. Check your connection.`,
580
+ "NETWORK_ERROR",
581
+ true
582
+ );
583
+ this.name = "NetworkError";
584
+ }
585
+ };
586
+ var EmptyOutputError = class extends BridgeError {
587
+ constructor() {
588
+ super(
589
+ "Codex returned empty output. This may be a transient issue.",
590
+ "EMPTY_OUTPUT",
591
+ true
592
+ );
593
+ this.name = "EmptyOutputError";
594
+ }
595
+ };
596
+ var NotGitRepoError = class extends BridgeError {
597
+ constructor(cwd) {
598
+ super(
599
+ `Not a git repository: ${cwd}. This operation requires a git repo.`,
600
+ "NOT_GIT_REPO",
601
+ false
602
+ );
603
+ this.name = "NotGitRepoError";
604
+ }
605
+ };
606
+
607
+ // src/config/constants.ts
608
+ var MAX_BRIDGE_DEPTH = 2;
609
+ var BRIDGE_DEPTH_ENV = "SKILL_CODEX_DEPTH";
610
+ var DEFAULT_TIMEOUT_MS = 3e5;
611
+ var TIMEOUT_ENV = "SKILL_CODEX_TIMEOUT_MS";
612
+ var KILL_GRACE_MS = 5e3;
613
+ var HEARTBEAT_INTERVAL_MS = 1e4;
614
+ var MAX_RETRIES = 3;
615
+ var MAX_RETRIES_ENV = "SKILL_CODEX_MAX_RETRIES";
616
+ var RETRY_DELAYS_MS = [1e3, 2e3, 4e3];
617
+ var RETRY_CAP_MS = 1e4;
618
+ var MAX_RESPONSE_CHARS = 8e4;
619
+ var LOCK_STALE_MS = 9e5;
620
+ var LOCK_FILENAME = ".skill-codex.lock";
621
+ var LOG_ENV = "SKILL_CODEX_LOG";
622
+ var WINDOWS_SANDBOX_ENV = "SKILL_CODEX_WINDOWS_SANDBOX";
623
+ var WINDOWS_SANDBOX_DEFAULT = "unelevated";
624
+
625
+ // src/guards/check-recursion.ts
626
+ function getCurrentDepth() {
627
+ return parseInt(process.env[BRIDGE_DEPTH_ENV] ?? "0", 10);
628
+ }
629
+ function getNextDepth() {
630
+ return getCurrentDepth() + 1;
631
+ }
632
+ function checkRecursion() {
633
+ const depth = getCurrentDepth();
634
+ if (depth >= MAX_BRIDGE_DEPTH) {
635
+ throw new RecursionLimitError(depth, MAX_BRIDGE_DEPTH);
636
+ }
637
+ }
638
+
639
+ // src/guards/check-binary.ts
640
+ import which2 from "which";
641
+ var cachedBinaryPath = null;
642
+ function getCachedBinaryPath() {
643
+ return cachedBinaryPath;
644
+ }
645
+ async function checkBinary(binary = "codex") {
646
+ if (cachedBinaryPath !== null) {
647
+ return { found: true, path: cachedBinaryPath };
648
+ }
649
+ try {
650
+ const resolved = await which2(binary);
651
+ cachedBinaryPath = resolved;
652
+ return { found: true, path: resolved };
653
+ } catch {
654
+ throw new CliNotFoundError(binary);
655
+ }
656
+ }
657
+
658
+ // src/guards/check-auth.ts
659
+ import { execFile } from "child_process";
660
+
661
+ // src/runner/sandbox-args.ts
662
+ function getSandboxConfigArgs() {
663
+ if (process.platform !== "win32") return [];
664
+ const raw = process.env[WINDOWS_SANDBOX_ENV]?.trim() || WINDOWS_SANDBOX_DEFAULT;
665
+ const mode = /^[a-z-]+$/.test(raw) ? raw : WINDOWS_SANDBOX_DEFAULT;
666
+ return ["-c", `windows.sandbox=${mode}`];
667
+ }
668
+
669
+ // src/guards/check-auth.ts
670
+ var AUTH_CACHE_TTL_MS = 6e4;
671
+ var authCachedAt = null;
672
+ async function checkAuth() {
673
+ const now = Date.now();
674
+ if (authCachedAt !== null && now - authCachedAt < AUTH_CACHE_TTL_MS) {
675
+ return;
676
+ }
677
+ const binary = getCachedBinaryPath() ?? "codex";
678
+ return new Promise((resolve, reject) => {
679
+ const child = execFile(
680
+ binary,
681
+ ["exec", "--sandbox", "read-only", ...getSandboxConfigArgs(), "--skip-git-repo-check", "--ephemeral", "echo ok"],
682
+ { timeout: 3e4, shell: process.platform === "win32" },
683
+ (error, _stdout, stderr) => {
684
+ if (!error) {
685
+ authCachedAt = Date.now();
686
+ resolve();
687
+ return;
688
+ }
689
+ const lower = (stderr ?? error.message ?? "").toLowerCase();
690
+ if (error.code === "ENOENT" || error.code === "ENOENT") {
691
+ reject(new CliNotFoundError());
692
+ return;
693
+ }
694
+ if (error.killed) {
695
+ reject(new NetworkError("Auth check timed out \u2014 check your network connection"));
696
+ return;
697
+ }
698
+ if (["econnrefused", "econnreset", "etimedout", "network error", "fetch failed"].some((p) => lower.includes(p))) {
699
+ reject(new NetworkError("Network error during auth check"));
700
+ return;
701
+ }
702
+ reject(new AuthExpiredError());
703
+ }
704
+ );
705
+ child.stdin?.end();
706
+ });
707
+ }
708
+
709
+ // src/lock/lock-file.ts
710
+ import fs8 from "fs";
711
+ import path10 from "path";
712
+ import os2 from "os";
713
+ function getLockPath(cwd) {
714
+ return path10.join(cwd, LOCK_FILENAME);
715
+ }
716
+ function isProcessAlive(pid) {
717
+ try {
718
+ process.kill(pid, 0);
719
+ return true;
720
+ } catch {
721
+ return false;
722
+ }
723
+ }
724
+ function isLockStale(data) {
725
+ const age = Date.now() - data.timestamp;
726
+ if (age > LOCK_STALE_MS) return true;
727
+ if (!isProcessAlive(data.pid)) return true;
728
+ return false;
729
+ }
730
+ function tryRemoveStaleLock(lockPath) {
731
+ try {
732
+ const raw = fs8.readFileSync(lockPath, "utf-8");
733
+ const data = JSON.parse(raw);
734
+ if (isLockStale(data)) {
735
+ fs8.unlinkSync(lockPath);
736
+ return true;
737
+ }
738
+ throw new LockConflictError(data.pid);
739
+ } catch (err) {
740
+ if (err instanceof LockConflictError) throw err;
741
+ try {
742
+ fs8.unlinkSync(lockPath);
743
+ } catch {
744
+ }
745
+ return true;
746
+ }
747
+ }
748
+ function acquireLock(cwd) {
749
+ const lockPath = getLockPath(cwd);
750
+ const lockData = {
751
+ pid: process.pid,
752
+ timestamp: Date.now(),
753
+ hostname: os2.hostname()
754
+ };
755
+ const content = JSON.stringify(lockData, null, 2);
756
+ try {
757
+ fs8.writeFileSync(lockPath, content, { flag: "wx" });
758
+ } catch (err) {
759
+ const fsErr = err;
760
+ if (fsErr.code === "EEXIST") {
761
+ tryRemoveStaleLock(lockPath);
762
+ try {
763
+ fs8.writeFileSync(lockPath, content, { flag: "wx" });
764
+ } catch (retryErr) {
765
+ const retryFsErr = retryErr;
766
+ if (retryFsErr.code === "EEXIST") {
767
+ throw new LockConflictError(0);
768
+ }
769
+ throw retryErr;
770
+ }
771
+ } else {
772
+ throw err;
773
+ }
774
+ }
775
+ const onExit = () => {
776
+ try {
777
+ fs8.unlinkSync(lockPath);
778
+ } catch {
779
+ }
780
+ };
781
+ process.on("exit", onExit);
782
+ process.on("SIGINT", onExit);
783
+ process.on("SIGTERM", onExit);
784
+ const release = () => {
785
+ process.removeListener("exit", onExit);
786
+ process.removeListener("SIGINT", onExit);
787
+ process.removeListener("SIGTERM", onExit);
788
+ try {
789
+ fs8.unlinkSync(lockPath);
790
+ } catch {
791
+ }
792
+ };
793
+ return { release };
794
+ }
795
+
796
+ // src/guards/check-lock.ts
797
+ function checkLock(cwd) {
798
+ return acquireLock(cwd);
799
+ }
800
+
801
+ // src/guards/check-git.ts
802
+ import { execFileSync } from "child_process";
803
+ function checkGit(cwd) {
804
+ try {
805
+ execFileSync("git", ["rev-parse", "--is-inside-work-tree"], {
806
+ cwd,
807
+ stdio: "pipe",
808
+ timeout: 5e3
809
+ });
810
+ return { isGitRepo: true };
811
+ } catch {
812
+ return { isGitRepo: false };
813
+ }
814
+ }
815
+
816
+ // src/guards/preflight.ts
817
+ async function runPreflight(options) {
818
+ checkRecursion();
819
+ await checkBinary();
820
+ if (!options.skipAuth && process.platform !== "win32") {
821
+ await checkAuth();
822
+ }
823
+ let lockHandle = null;
824
+ if (!options.skipLock) {
825
+ lockHandle = checkLock(options.cwd);
826
+ }
827
+ if (options.requireGit) {
828
+ const { isGitRepo } = checkGit(options.cwd);
829
+ if (!isGitRepo) {
830
+ lockHandle?.release();
831
+ throw new NotGitRepoError(options.cwd);
832
+ }
833
+ }
834
+ return { lockHandle };
835
+ }
836
+
837
+ // src/runner/exec-runner.ts
838
+ import { spawn } from "child_process";
839
+ import { StringDecoder } from "string_decoder";
840
+
841
+ // src/runner/timeout.ts
842
+ function setupTimeout(child, timeoutMs) {
843
+ let timer = null;
844
+ let graceTimer = null;
845
+ const promise = new Promise((_resolve, reject) => {
846
+ timer = setTimeout(() => {
847
+ if (isWindows()) {
848
+ child.kill();
849
+ } else {
850
+ child.kill("SIGTERM");
851
+ graceTimer = setTimeout(() => {
852
+ try {
853
+ if (!child.killed) {
854
+ child.kill("SIGKILL");
855
+ }
856
+ } catch {
857
+ }
858
+ }, KILL_GRACE_MS);
859
+ }
860
+ reject(new TimeoutError(timeoutMs));
861
+ }, timeoutMs);
862
+ });
863
+ const clear = () => {
864
+ if (timer) clearTimeout(timer);
865
+ if (graceTimer) clearTimeout(graceTimer);
866
+ };
867
+ return { clear, promise };
868
+ }
869
+
870
+ // src/util/truncate.ts
871
+ function truncateResponse(text, maxChars = MAX_RESPONSE_CHARS) {
872
+ if (text.length <= maxChars) return text;
873
+ const omitted = text.length - maxChars;
874
+ return text.slice(0, maxChars) + `
875
+
876
+ [Response truncated at ${maxChars} characters. ${omitted} characters omitted.]`;
877
+ }
878
+
879
+ // src/runner/output-parser.ts
880
+ function parseCodexOutput(raw) {
881
+ if (!raw.trim()) {
882
+ throw new EmptyOutputError();
883
+ }
884
+ const lines = raw.split("\n").filter((line) => line.trim());
885
+ const messages = [];
886
+ const activity = [];
887
+ let resultContent = null;
888
+ let sessionId;
889
+ let usage = null;
890
+ for (const line of lines) {
891
+ try {
892
+ const parsed = JSON.parse(line);
893
+ if (parsed.type === "thread.started" && typeof parsed.thread_id === "string") {
894
+ sessionId = parsed.thread_id;
895
+ continue;
896
+ }
897
+ if (parsed.type === "turn.completed" && parsed.usage) {
898
+ usage = {
899
+ input_tokens: parsed.usage.input_tokens ?? 0,
900
+ cached_input_tokens: parsed.usage.cached_input_tokens ?? 0,
901
+ output_tokens: parsed.usage.output_tokens ?? 0,
902
+ reasoning_output_tokens: parsed.usage.reasoning_output_tokens ?? 0
903
+ };
904
+ continue;
905
+ }
906
+ if (parsed.type === "result" && typeof parsed.content === "string") {
907
+ resultContent = parsed.content;
908
+ continue;
909
+ }
910
+ if (parsed.type === "message" && typeof parsed.content === "string") {
911
+ messages.push(parsed.content);
912
+ continue;
913
+ }
914
+ if (parsed.item?.type === "command_execution") {
915
+ const cmd = parsed.item;
916
+ const shortCmd = cmd.command?.length > 80 ? cmd.command.slice(0, 77) + "..." : cmd.command;
917
+ const statusIcon = cmd.status === "declined" ? "\u2718" : cmd.exit_code === 0 ? "\u2714" : cmd.exit_code !== null ? "\u2718" : "\u25B6";
918
+ const statusLabel = cmd.status === "declined" ? "blocked" : cmd.status === "in_progress" ? "running" : cmd.exit_code === 0 ? "ok" : `exit ${cmd.exit_code}`;
919
+ if (cmd.status !== "in_progress") {
920
+ activity.push({
921
+ type: "exec",
922
+ command: shortCmd,
923
+ icon: statusIcon,
924
+ status: statusLabel
925
+ });
926
+ }
927
+ continue;
928
+ }
929
+ if (parsed.item?.type === "agent_message" && typeof parsed.item.text === "string" && parsed.type !== "item.started" && parsed.type !== "item.updated") {
930
+ messages.push(parsed.item.text);
931
+ continue;
932
+ }
933
+ if (parsed.itemType === "agent_message" && typeof parsed.text === "string") {
934
+ messages.push(parsed.text);
935
+ continue;
936
+ }
937
+ if (parsed.item?.type === "file_read") {
938
+ activity.push({
939
+ type: "read",
940
+ path: parsed.item.path || "file",
941
+ icon: "\u25B6",
942
+ status: "read"
943
+ });
944
+ continue;
945
+ }
946
+ if (parsed.item?.type === "file_write" || parsed.item?.type === "file_edit") {
947
+ activity.push({
948
+ type: "write",
949
+ path: parsed.item.path || "file",
950
+ icon: "\u270E",
951
+ status: "write"
952
+ });
953
+ continue;
954
+ }
955
+ if (parsed.item?.type === "file_change") {
956
+ const changes = Array.isArray(parsed.item.changes) ? parsed.item.changes : null;
957
+ if (changes && changes.length > 0) {
958
+ for (const change of changes) {
959
+ activity.push({
960
+ type: "write",
961
+ path: change?.path || "file",
962
+ icon: "\u270E",
963
+ status: change?.kind || "write"
964
+ });
965
+ }
966
+ } else {
967
+ activity.push({
968
+ type: "write",
969
+ path: parsed.item.path || "file",
970
+ icon: "\u270E",
971
+ status: "write"
972
+ });
973
+ }
974
+ continue;
975
+ }
976
+ } catch {
977
+ }
978
+ }
979
+ let agentMessage;
980
+ if (resultContent !== null) {
981
+ agentMessage = resultContent;
982
+ } else if (messages.length > 0) {
983
+ agentMessage = messages.join("\n\n");
984
+ } else {
985
+ const substantiveLines = lines.filter(
986
+ (line) => !line.startsWith("OpenAI Codex") && !line.startsWith("---") && !line.startsWith("tokens used")
987
+ );
988
+ agentMessage = substantiveLines.join("\n").trim();
989
+ }
990
+ if (!agentMessage) {
991
+ throw new EmptyOutputError();
992
+ }
993
+ return {
994
+ content: truncateResponse(agentMessage),
995
+ activity,
996
+ usage,
997
+ raw,
998
+ sessionId
999
+ };
1000
+ }
1001
+
1002
+ // src/util/text.ts
1003
+ function oneLine(text, max) {
1004
+ const collapsed = text.replace(/\s+/g, " ").trim();
1005
+ return collapsed.length > max ? collapsed.slice(0, max - 1) + "\u2026" : collapsed;
1006
+ }
1007
+ function baseName(p) {
1008
+ if (!p) return "file";
1009
+ const parts = p.split(/[\\/]/).filter(Boolean);
1010
+ return parts.length > 0 ? parts[parts.length - 1] : p;
1011
+ }
1012
+
1013
+ // src/runner/progress.ts
1014
+ function formatProgressMessage(evt) {
1015
+ if (!evt || typeof evt !== "object") return null;
1016
+ const item = evt.item;
1017
+ if (item?.type === "command_execution") {
1018
+ const cmd = oneLine(String(item.command ?? ""), 60);
1019
+ if (item.status === "in_progress") return `running: ${cmd}`;
1020
+ if (item.status === "declined") return `blocked: ${cmd}`;
1021
+ if (item.exit_code === 0) return `ran: ${cmd}`;
1022
+ return `failed (exit ${String(item.exit_code)}): ${cmd}`;
1023
+ }
1024
+ if (item?.type === "file_read") return `reading ${baseName(String(item.path ?? "file"))}`;
1025
+ if (item?.type === "file_write" || item?.type === "file_edit") {
1026
+ return `editing ${baseName(String(item.path ?? "file"))}`;
1027
+ }
1028
+ if (item?.type === "file_change") {
1029
+ const changes = Array.isArray(item.changes) ? item.changes : null;
1030
+ if (changes && changes.length > 0) {
1031
+ return changes.length === 1 ? `editing ${baseName(String(changes[0]?.path ?? "file"))}` : `editing ${changes.length} files`;
1032
+ }
1033
+ return `editing ${baseName(String(item.path ?? "file"))}`;
1034
+ }
1035
+ if (item?.type === "reasoning" || evt.type === "turn.started") return "thinking\u2026";
1036
+ if (item?.type === "agent_message" && typeof item.text === "string" && evt.type !== "item.started" && evt.type !== "item.updated") {
1037
+ return "writing response\u2026";
1038
+ }
1039
+ return null;
1040
+ }
1041
+
1042
+ // src/util/live-logger.ts
1043
+ import fs9 from "fs";
1044
+ import os3 from "os";
1045
+ import path11 from "path";
1046
+ import { createHash } from "crypto";
1047
+ function resolveLogPath(cwd) {
1048
+ const override = process.env[LOG_ENV];
1049
+ if (override && override.trim()) return path11.resolve(override.trim());
1050
+ const base = path11.basename(cwd).replace(/[^a-zA-Z0-9._-]/g, "_") || "run";
1051
+ const hash = createHash("sha1").update(cwd).digest("hex").slice(0, 8);
1052
+ return path11.join(os3.tmpdir(), "skill-codex", `${base}-${hash}.log`);
1053
+ }
1054
+ function formatLogLines(evt) {
1055
+ if (!evt || typeof evt !== "object") return [];
1056
+ const item = evt.item;
1057
+ if (item?.type === "command_execution") {
1058
+ if (item.status === "in_progress") return [` $ ${oneLine(String(item.command ?? ""), 120)}`];
1059
+ if (item.status === "declined") return [" \u2718 blocked"];
1060
+ if (item.exit_code === 0) return [" \u2714 ok"];
1061
+ return [` \u2718 exit ${String(item.exit_code)}`];
1062
+ }
1063
+ if (item?.type === "file_read") return [` read ${String(item.path ?? "file")}`];
1064
+ if (item?.type === "file_write" || item?.type === "file_edit") {
1065
+ return [` write ${String(item.path ?? "file")}`];
1066
+ }
1067
+ if (item?.type === "file_change") {
1068
+ const changes = Array.isArray(item.changes) ? item.changes : null;
1069
+ if (changes && changes.length > 0) {
1070
+ return changes.map(
1071
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1072
+ (change) => ` write ${String(change?.path ?? "file")} (${String(change?.kind ?? "write")})`
1073
+ );
1074
+ }
1075
+ return [` write ${String(item.path ?? "file")}`];
1076
+ }
1077
+ if (item?.type === "agent_message" && typeof item.text === "string" && evt.type !== "item.started" && evt.type !== "item.updated") {
1078
+ return [` msg ${oneLine(item.text, 400)}`];
1079
+ }
1080
+ if (evt.type === "message" && typeof evt.content === "string") {
1081
+ return [` msg ${oneLine(evt.content, 400)}`];
1082
+ }
1083
+ if (evt.type === "turn.completed" && evt.usage) {
1084
+ const u = evt.usage;
1085
+ const reasoning = u.reasoning_output_tokens ?? 0;
1086
+ return [
1087
+ ` tokens: ${u.input_tokens ?? 0} in \u2192 ${u.output_tokens ?? 0} out${reasoning > 0 ? ` (+${reasoning} reasoning)` : ""}`
1088
+ ];
1089
+ }
1090
+ return [];
1091
+ }
1092
+ function createLiveLogger(opts) {
1093
+ const logPath = resolveLogPath(opts.cwd);
1094
+ let stream = null;
1095
+ try {
1096
+ fs9.mkdirSync(path11.dirname(logPath), { recursive: true });
1097
+ stream = fs9.createWriteStream(logPath, { flags: "a" });
1098
+ } catch {
1099
+ stream = null;
1100
+ }
1101
+ const line = (text) => {
1102
+ try {
1103
+ stream?.write(text + "\n");
1104
+ } catch {
1105
+ }
1106
+ };
1107
+ const startedAt = (/* @__PURE__ */ new Date()).toISOString();
1108
+ line("");
1109
+ line("=".repeat(60));
1110
+ line(`> codex ${opts.mode} ${startedAt}`);
1111
+ line(` cwd: ${opts.cwd}`);
1112
+ line(` task: ${oneLine(opts.prompt, 200)}`);
1113
+ line("-".repeat(60));
1114
+ const handleEvent = (raw) => {
1115
+ const trimmed = raw.trim();
1116
+ if (!trimmed) return;
1117
+ let evt;
1118
+ try {
1119
+ evt = JSON.parse(trimmed);
1120
+ } catch {
1121
+ return;
1122
+ }
1123
+ for (const l of formatLogLines(evt)) line(l);
1124
+ };
1125
+ let buffer = "";
1126
+ return {
1127
+ path: logPath,
1128
+ write(fragment) {
1129
+ buffer += fragment;
1130
+ let idx;
1131
+ while ((idx = buffer.indexOf("\n")) >= 0) {
1132
+ const lineStr = buffer.slice(0, idx);
1133
+ buffer = buffer.slice(idx + 1);
1134
+ handleEvent(lineStr);
1135
+ }
1136
+ },
1137
+ finish(summary) {
1138
+ if (buffer.trim()) {
1139
+ handleEvent(buffer);
1140
+ buffer = "";
1141
+ }
1142
+ line("-".repeat(60));
1143
+ line(`# done ${(/* @__PURE__ */ new Date()).toISOString()} ${summary}`);
1144
+ try {
1145
+ stream?.end();
1146
+ } catch {
1147
+ }
1148
+ }
1149
+ };
1150
+ }
1151
+
1152
+ // src/runner/exec-runner.ts
1153
+ function getTimeout(override) {
1154
+ if (override !== void 0) return override;
1155
+ const envVal = process.env[TIMEOUT_ENV];
1156
+ if (envVal) {
1157
+ const parsed = parseInt(envVal, 10);
1158
+ if (!isNaN(parsed) && parsed > 0) return parsed;
1159
+ }
1160
+ return DEFAULT_TIMEOUT_MS;
1161
+ }
1162
+ function classifyError(exitCode, stderr) {
1163
+ const lower = stderr.toLowerCase();
1164
+ if (lower.includes("unauthorized") || lower.includes("401") || lower.includes("api key")) {
1165
+ return new AuthExpiredError();
1166
+ }
1167
+ if (lower.includes("rate limit") || lower.includes("429") || lower.includes("too many requests")) {
1168
+ return new RateLimitError();
1169
+ }
1170
+ if (["500", "502", "503", "504", "internal server error", "bad gateway", "service unavailable"].some((p) => lower.includes(p))) {
1171
+ return new ServerError(stderr.slice(0, 200));
1172
+ }
1173
+ if (["econnreset", "econnrefused", "etimedout", "network error", "fetch failed", "socket hang up"].some((p) => lower.includes(p))) {
1174
+ return new NetworkError(stderr.slice(0, 200));
1175
+ }
1176
+ return new BridgeError(
1177
+ `Codex exited with code ${exitCode}: ${stderr.slice(0, 300)}`,
1178
+ "EXEC_FAILED",
1179
+ false
1180
+ );
1181
+ }
1182
+ async function execCodex(params) {
1183
+ const codexPath = getCachedBinaryPath();
1184
+ if (codexPath === null) {
1185
+ throw new CliNotFoundError();
1186
+ }
1187
+ return new Promise((resolve, reject) => {
1188
+ const timeoutMs = getTimeout(params.timeoutMs);
1189
+ let args2;
1190
+ let sendStdinPrompt;
1191
+ if (params.review) {
1192
+ args2 = ["exec", "review", "--json", "--skip-git-repo-check"];
1193
+ if (params.reviewBase) {
1194
+ args2.push("--base", params.reviewBase);
1195
+ sendStdinPrompt = false;
1196
+ } else if (params.reviewCommit) {
1197
+ args2.push("--commit", params.reviewCommit);
1198
+ sendStdinPrompt = false;
1199
+ } else if (params.prompt?.trim()) {
1200
+ sendStdinPrompt = true;
1201
+ } else {
1202
+ args2.push("--uncommitted");
1203
+ sendStdinPrompt = false;
1204
+ }
1205
+ } else if (params.sessionId) {
1206
+ args2 = ["exec", "resume", params.sessionId, "--json", "--skip-git-repo-check"];
1207
+ sendStdinPrompt = true;
1208
+ } else {
1209
+ const sandbox = params.sandbox ?? (params.mode === "full-auto" ? "workspace-write" : "read-only");
1210
+ args2 = ["exec", "--json", "--skip-git-repo-check", "--sandbox", sandbox];
1211
+ sendStdinPrompt = true;
1212
+ }
1213
+ if (params.model) {
1214
+ args2.push("-m", params.model);
1215
+ }
1216
+ if (params.reasoningEffort) {
1217
+ args2.push("-c", `model_reasoning_effort=${params.reasoningEffort}`);
1218
+ }
1219
+ args2.push(...getSandboxConfigArgs());
1220
+ const stdinPrompt = params.prompt ?? "";
1221
+ if (sendStdinPrompt) {
1222
+ args2.push("-");
1223
+ }
1224
+ const env = {
1225
+ ...process.env,
1226
+ [BRIDGE_DEPTH_ENV]: String(getNextDepth())
1227
+ };
1228
+ const child = spawn(codexPath, args2, {
1229
+ cwd: params.cwd,
1230
+ env,
1231
+ stdio: ["pipe", "pipe", "pipe"],
1232
+ shell: process.platform === "win32",
1233
+ windowsHide: true
1234
+ });
1235
+ const { clear: clearTimeout_, promise: timeoutPromise } = setupTimeout(child, timeoutMs);
1236
+ const startedAt = Date.now();
1237
+ const logger = createLiveLogger({
1238
+ cwd: params.cwd,
1239
+ mode: params.mode,
1240
+ prompt: params.prompt
1241
+ });
1242
+ process.stderr.write(`[skill-codex] live log: ${logger.path}
1243
+ `);
1244
+ let heartbeat = null;
1245
+ if (params.onProgress) {
1246
+ heartbeat = setInterval(() => {
1247
+ const secs = Math.round((Date.now() - startedAt) / 1e3);
1248
+ params.onProgress?.(`Codex working\u2026 ${secs}s elapsed`);
1249
+ }, HEARTBEAT_INTERVAL_MS);
1250
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
1251
+ }
1252
+ const stopHeartbeat = () => {
1253
+ if (heartbeat) {
1254
+ clearInterval(heartbeat);
1255
+ heartbeat = null;
1256
+ }
1257
+ };
1258
+ const decoder = new StringDecoder("utf8");
1259
+ let progressBuf = "";
1260
+ const consumeForProgress = (text) => {
1261
+ if (!params.onProgress) return;
1262
+ progressBuf += text;
1263
+ let idx;
1264
+ while ((idx = progressBuf.indexOf("\n")) >= 0) {
1265
+ const lineStr = progressBuf.slice(0, idx).trim();
1266
+ progressBuf = progressBuf.slice(idx + 1);
1267
+ if (!lineStr) continue;
1268
+ try {
1269
+ const msg = formatProgressMessage(JSON.parse(lineStr));
1270
+ if (msg) params.onProgress(msg);
1271
+ } catch {
1272
+ }
1273
+ }
1274
+ };
1275
+ let logFinished = false;
1276
+ const finishLog = (summary) => {
1277
+ stopHeartbeat();
1278
+ if (logFinished) return;
1279
+ logFinished = true;
1280
+ try {
1281
+ logger.write(decoder.end());
1282
+ logger.finish(summary);
1283
+ } catch {
1284
+ }
1285
+ };
1286
+ const stdoutChunks = [];
1287
+ let stderr = "";
1288
+ child.stdout?.on("data", (chunk) => {
1289
+ stdoutChunks.push(chunk);
1290
+ const text = decoder.write(chunk);
1291
+ try {
1292
+ logger.write(text);
1293
+ } catch {
1294
+ }
1295
+ consumeForProgress(text);
1296
+ });
1297
+ child.stderr?.on("data", (chunk) => {
1298
+ stderr += chunk.toString();
1299
+ });
1300
+ if (sendStdinPrompt) {
1301
+ child.stdin?.write(stdinPrompt);
1302
+ }
1303
+ child.stdin?.end();
1304
+ const onClose = (exitCode) => {
1305
+ clearTimeout_();
1306
+ finishLog(exitCode === 0 || exitCode === null ? "ok" : `exit ${exitCode}`);
1307
+ const stdout = Buffer.concat(stdoutChunks).toString();
1308
+ if (exitCode === 0 || exitCode === null) {
1309
+ try {
1310
+ const result = parseCodexOutput(stdout);
1311
+ resolve({ ...result, logPath: logger.path, durationMs: Date.now() - startedAt });
1312
+ } catch (err) {
1313
+ reject(err);
1314
+ }
1315
+ return;
1316
+ }
1317
+ reject(classifyError(exitCode, stderr));
1318
+ };
1319
+ child.on("close", onClose);
1320
+ child.on("error", (err) => {
1321
+ clearTimeout_();
1322
+ finishLog("spawn error");
1323
+ if (err.code === "ENOENT") {
1324
+ reject(new CliNotFoundError());
1325
+ } else {
1326
+ reject(new BridgeError(`Failed to spawn codex: ${err.message}`, "SPAWN_ERROR", false));
1327
+ }
1328
+ });
1329
+ timeoutPromise.catch((err) => {
1330
+ finishLog("timeout");
1331
+ reject(err);
1332
+ });
1333
+ });
1334
+ }
1335
+
1336
+ // src/runner/retry.ts
1337
+ function getMaxRetries(override) {
1338
+ if (override !== void 0) return override;
1339
+ const envVal = process.env[MAX_RETRIES_ENV];
1340
+ if (envVal) {
1341
+ const parsed = parseInt(envVal, 10);
1342
+ if (!isNaN(parsed) && parsed >= 0) return parsed;
1343
+ }
1344
+ return MAX_RETRIES;
1345
+ }
1346
+ function getDelay(attempt) {
1347
+ const base = RETRY_DELAYS_MS[attempt] ?? RETRY_CAP_MS;
1348
+ const capped = Math.min(base, RETRY_CAP_MS);
1349
+ const jitter = 0.5 + Math.random();
1350
+ return Math.round(capped * jitter);
1351
+ }
1352
+ function defaultShouldRetry(err) {
1353
+ return err instanceof BridgeError && err.retryable;
1354
+ }
1355
+ function sleep(ms) {
1356
+ return new Promise((resolve) => setTimeout(resolve, ms));
1357
+ }
1358
+ async function withRetry(fn, options = {}) {
1359
+ const maxRetries = getMaxRetries(options.maxRetries);
1360
+ const shouldRetry = options.shouldRetry ?? defaultShouldRetry;
1361
+ for (let attempt = 0; ; attempt++) {
1362
+ try {
1363
+ return await fn();
1364
+ } catch (err) {
1365
+ const isRetryable = err instanceof Error && shouldRetry(err);
1366
+ if (attempt < maxRetries && isRetryable) {
1367
+ const delay = getDelay(attempt);
1368
+ const errorName = err instanceof Error ? err.constructor.name : "UnknownError";
1369
+ process.stderr.write(
1370
+ `[skill-codex] ${errorName} (attempt ${attempt + 1}/${maxRetries}), retrying in ${delay}ms...
1371
+ `
1372
+ );
1373
+ await sleep(delay);
1374
+ continue;
1375
+ }
1376
+ throw err;
1377
+ }
1378
+ }
1379
+ }
1380
+
1381
+ // src/tools/codex-exec.ts
1382
+ var TOOL_NAME = "codex_exec";
1383
+ 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.";
1384
+ var inputSchema = z.object({
1385
+ prompt: z.string().optional().describe("The task description for Codex"),
1386
+ mode: z.enum(["exec", "full-auto"]).default("exec").describe("exec = read-only with confirmation, full-auto = can write files"),
1387
+ sandbox: z.enum(["read-only", "workspace-write", "danger-full-access"]).optional().describe(
1388
+ "Explicit Codex sandbox policy; overrides mode. read-only = no writes, workspace-write = write within cwd, danger-full-access = unrestricted (use with care)."
1389
+ ),
1390
+ sessionId: z.string().regex(/^[A-Za-z0-9_-]{1,128}$/, "sessionId must be a Codex thread id (letters, digits, '-', '_')").optional().describe(
1391
+ "Resume a prior Codex session by its thread id (returned in a previous response) so Codex retains context across calls."
1392
+ ),
1393
+ model: z.string().regex(/^[A-Za-z0-9._-]{1,64}$/).optional().describe(
1394
+ "Codex model to use (e.g. gpt-5.5, gpt-5.4, gpt-5.4-mini). Omit to use Codex's configured default."
1395
+ ),
1396
+ reasoningEffort: z.enum(["minimal", "low", "medium", "high", "xhigh"]).optional().describe("How much reasoning effort Codex spends. Omit for the model's default."),
1397
+ review: z.boolean().optional().describe(
1398
+ "Run Codex's native diff-scoped review (`codex exec review`) instead of a freeform prompt. The prompt becomes optional custom review instructions."
1399
+ ),
1400
+ reviewBase: z.string().regex(/^[A-Za-z0-9._\/-]{1,128}$/).optional().describe("With review: diff against this base branch (default: uncommitted changes)."),
1401
+ reviewCommit: z.string().regex(/^[0-9a-fA-F]{4,64}$/).optional().describe("With review: review the changes introduced by this commit SHA."),
1402
+ cwd: z.string().optional().describe("Working directory (defaults to server cwd)"),
1403
+ timeoutMs: z.number().optional().describe("Override default timeout in milliseconds"),
1404
+ requireGit: z.boolean().default(false).describe("Fail if not inside a git repository")
1405
+ });
1406
+ var TOOL_INPUT_JSON_SCHEMA = {
1407
+ type: "object",
1408
+ properties: {
1409
+ prompt: { type: "string", description: "The task description for Codex" },
1410
+ mode: {
1411
+ type: "string",
1412
+ enum: ["exec", "full-auto"],
1413
+ default: "exec",
1414
+ description: "exec = read-only, full-auto = can write files"
1415
+ },
1416
+ sandbox: {
1417
+ type: "string",
1418
+ enum: ["read-only", "workspace-write", "danger-full-access"],
1419
+ description: "Explicit Codex sandbox policy; overrides mode. read-only = no writes, workspace-write = write within cwd, danger-full-access = unrestricted (use with care)."
1420
+ },
1421
+ sessionId: {
1422
+ type: "string",
1423
+ pattern: "^[A-Za-z0-9_-]{1,128}$",
1424
+ description: "Resume a prior Codex session by its thread id (returned in a previous response) so Codex retains context across calls."
1425
+ },
1426
+ model: {
1427
+ type: "string",
1428
+ pattern: "^[A-Za-z0-9._-]{1,64}$",
1429
+ description: "Codex model to use (e.g. gpt-5.5, gpt-5.4, gpt-5.4-mini). Omit to use Codex's configured default."
1430
+ },
1431
+ reasoningEffort: {
1432
+ type: "string",
1433
+ enum: ["minimal", "low", "medium", "high", "xhigh"],
1434
+ description: "How much reasoning effort Codex spends. Omit for the model's default."
1435
+ },
1436
+ review: {
1437
+ type: "boolean",
1438
+ description: "Run Codex's native diff-scoped review (`codex exec review`) instead of a freeform prompt. The prompt becomes optional custom review instructions."
1439
+ },
1440
+ reviewBase: {
1441
+ type: "string",
1442
+ pattern: "^[A-Za-z0-9._\\/-]{1,128}$",
1443
+ description: "With review: diff against this base branch (default: uncommitted changes)."
1444
+ },
1445
+ reviewCommit: {
1446
+ type: "string",
1447
+ pattern: "^[0-9a-fA-F]{4,64}$",
1448
+ description: "With review: review the changes introduced by this commit SHA."
1449
+ },
1450
+ cwd: { type: "string", description: "Working directory (defaults to server cwd)" },
1451
+ timeoutMs: { type: "number", description: "Override default timeout in milliseconds" },
1452
+ requireGit: {
1453
+ type: "boolean",
1454
+ default: false,
1455
+ description: "Fail if not inside a git repository"
1456
+ }
1457
+ },
1458
+ required: []
1459
+ };
1460
+ function formatError(err) {
1461
+ if (err instanceof BridgeError) {
1462
+ return `[skill-codex error: ${err.code}] ${err.message}`;
1463
+ }
1464
+ if (err instanceof Error) {
1465
+ return `[skill-codex error] ${err.message}`;
1466
+ }
1467
+ return `[skill-codex error] Unknown error: ${String(err)}`;
1468
+ }
1469
+ function formatRichResponse(result, input, cwd) {
1470
+ const lines = [];
1471
+ const sandboxLabel = input.sandbox ?? (input.mode === "full-auto" ? "workspace-write" : "read-only");
1472
+ const label = input.review ? "review" : input.sessionId ? "resumed" : sandboxLabel;
1473
+ const metaParts = [label];
1474
+ if (input.model) {
1475
+ metaParts.push(input.model);
1476
+ }
1477
+ if (input.reasoningEffort) {
1478
+ metaParts.push(`effort:${input.reasoningEffort}`);
1479
+ }
1480
+ metaParts.push(cwd);
1481
+ if (typeof result.durationMs === "number") {
1482
+ metaParts.push(`${(result.durationMs / 1e3).toFixed(1)}s`);
1483
+ }
1484
+ if (result.usage) {
1485
+ const {
1486
+ input_tokens: inp,
1487
+ output_tokens: out,
1488
+ cached_input_tokens: cached,
1489
+ reasoning_output_tokens: reasoning
1490
+ } = result.usage;
1491
+ metaParts.push(
1492
+ `${inp} tok in${cached > 0 ? ` (${cached} cached)` : ""} \u2192 ${out} out${reasoning > 0 ? ` (+${reasoning} reasoning)` : ""}`
1493
+ );
1494
+ }
1495
+ lines.push(`[${metaParts.join(" \u2502 ")}]`);
1496
+ if (result.sessionId) {
1497
+ lines.push(` session: ${result.sessionId} (pass as sessionId to continue this conversation)`);
1498
+ }
1499
+ if (result.logPath) {
1500
+ lines.push(` live log: ${result.logPath}`);
1501
+ }
1502
+ if (result.activity.length > 0) {
1503
+ for (const a of result.activity) {
1504
+ if (a.type === "exec") {
1505
+ lines.push(` ${a.icon} exec: ${a.command} (${a.status})`);
1506
+ } else if (a.type === "read") {
1507
+ lines.push(` \u25B6 read: ${a.path}`);
1508
+ } else if (a.type === "write") {
1509
+ lines.push(` \u270E write: ${a.path}`);
1510
+ }
1511
+ }
1512
+ }
1513
+ lines.push("");
1514
+ lines.push(result.content);
1515
+ return lines.join("\n");
1516
+ }
1517
+ async function handleCodexExec(input, serverCwd, onProgress) {
1518
+ const rawCwd = input.cwd ?? serverCwd;
1519
+ const cwd = path12.resolve(rawCwd);
1520
+ if (!fs10.existsSync(cwd) || !fs10.statSync(cwd).isDirectory()) {
1521
+ return {
1522
+ content: [{ type: "text", text: `[skill-codex error: INVALID_CWD] cwd is not an existing directory: ${cwd}` }],
1523
+ isError: true
1524
+ };
1525
+ }
1526
+ const optError = (msg) => ({
1527
+ content: [{ type: "text", text: `[skill-codex error: INVALID_OPTIONS] ${msg}` }],
1528
+ isError: true
1529
+ });
1530
+ if (input.review && input.sessionId) return optError("review and sessionId are mutually exclusive");
1531
+ if (input.reviewBase && input.reviewCommit) return optError("reviewBase and reviewCommit are mutually exclusive");
1532
+ if ((input.reviewBase ?? input.reviewCommit) && !input.review) {
1533
+ return optError("reviewBase/reviewCommit require review: true");
1534
+ }
1535
+ if (input.review && (input.reviewBase ?? input.reviewCommit) && input.prompt?.trim()) {
1536
+ return optError(
1537
+ "a review target (reviewBase/reviewCommit) can't be combined with a prompt \u2014 Codex review takes a target OR instructions, not both"
1538
+ );
1539
+ }
1540
+ if (!input.review && !input.prompt?.trim()) {
1541
+ return {
1542
+ content: [{ type: "text", text: "[skill-codex error: MISSING_PROMPT] prompt is required unless review is set" }],
1543
+ isError: true
1544
+ };
1545
+ }
1546
+ let lockRelease = null;
1547
+ try {
1548
+ const { lockHandle } = await runPreflight({
1549
+ cwd,
1550
+ requireGit: input.requireGit
1551
+ });
1552
+ lockRelease = lockHandle?.release ?? null;
1553
+ const result = await withRetry(
1554
+ () => execCodex({
1555
+ prompt: input.prompt ?? "",
1556
+ cwd,
1557
+ mode: input.mode,
1558
+ sandbox: input.sandbox,
1559
+ sessionId: input.sessionId,
1560
+ model: input.model,
1561
+ reasoningEffort: input.reasoningEffort,
1562
+ review: input.review,
1563
+ reviewBase: input.reviewBase,
1564
+ reviewCommit: input.reviewCommit,
1565
+ timeoutMs: input.timeoutMs,
1566
+ onProgress
1567
+ })
1568
+ );
1569
+ const formatted = formatRichResponse(result, input, cwd);
1570
+ return {
1571
+ content: [{ type: "text", text: formatted }]
1572
+ };
1573
+ } catch (err) {
1574
+ return {
1575
+ content: [{ type: "text", text: formatError(err) }],
1576
+ isError: true
1577
+ };
1578
+ } finally {
1579
+ lockRelease?.();
1580
+ }
1581
+ }
1582
+
1583
+ // src/server.ts
1584
+ function createServer(cwd) {
1585
+ const server = new Server(
1586
+ { name: "skill-codex", version: "0.8.0" },
1587
+ { capabilities: { tools: {} } }
1588
+ );
1589
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
1590
+ tools: [
1591
+ {
1592
+ name: TOOL_NAME,
1593
+ description: TOOL_DESCRIPTION,
1594
+ inputSchema: TOOL_INPUT_JSON_SCHEMA
1595
+ }
1596
+ ]
1597
+ }));
1598
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
1599
+ if (request.params.name !== TOOL_NAME) {
1600
+ return {
1601
+ content: [{ type: "text", text: `Unknown tool: ${request.params.name}` }],
1602
+ isError: true
1603
+ };
1604
+ }
1605
+ const parsed = inputSchema.safeParse(request.params.arguments);
1606
+ if (!parsed.success) {
1607
+ return {
1608
+ content: [
1609
+ {
1610
+ type: "text",
1611
+ text: `Invalid input: ${parsed.error.issues.map((i) => i.message).join(", ")}`
1612
+ }
1613
+ ],
1614
+ isError: true
1615
+ };
1616
+ }
1617
+ const progressToken = request.params._meta?.progressToken;
1618
+ let progressCounter = 0;
1619
+ const onProgress = progressToken === void 0 ? void 0 : (message) => {
1620
+ progressCounter += 1;
1621
+ void extra.sendNotification({
1622
+ method: "notifications/progress",
1623
+ params: { progressToken, progress: progressCounter, message }
1624
+ }).catch(() => {
1625
+ });
1626
+ };
1627
+ return handleCodexExec(parsed.data, cwd, onProgress);
1628
+ });
1629
+ return server;
1630
+ }
1631
+ async function startServer() {
1632
+ const cwd = process.cwd();
1633
+ const server = createServer(cwd);
1634
+ const transport = new StdioServerTransport();
1635
+ process.stderr.write("[skill-codex] MCP server starting...\n");
1636
+ await server.connect(transport);
1637
+ process.stderr.write("[skill-codex] MCP server connected via stdio\n");
1638
+ process.on("uncaughtException", (err) => {
1639
+ process.stderr.write(`[skill-codex] Uncaught exception: ${err.message}
1640
+ `);
1641
+ });
1642
+ process.on("unhandledRejection", (reason) => {
1643
+ process.stderr.write(`[skill-codex] Unhandled rejection: ${String(reason)}
1644
+ `);
1645
+ });
1646
+ }
1647
+
390
1648
  // bin/skill-codex.ts
391
1649
  var args = process.argv.slice(2);
392
1650
  var command = args[0];
393
1651
  function getVersion() {
394
- const __dirname = path8.dirname(fileURLToPath(import.meta.url));
395
- const pkgPath = path8.resolve(__dirname, "..", "package.json");
1652
+ const __dirname = path13.dirname(fileURLToPath(import.meta.url));
1653
+ const pkgPath = path13.resolve(__dirname, "..", "package.json");
396
1654
  try {
397
- const pkg = JSON.parse(fs7.readFileSync(pkgPath, "utf-8"));
1655
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
398
1656
  return pkg.version ?? "0.0.0";
399
1657
  } catch {
400
1658
  return "0.0.0";
@@ -443,6 +1701,11 @@ Usage:
443
1701
  await runUninstall();
444
1702
  break;
445
1703
  }
1704
+ case "mcp":
1705
+ case "serve": {
1706
+ await startServer();
1707
+ break;
1708
+ }
446
1709
  default: {
447
1710
  const version = getVersion();
448
1711
  process.stdout.write(`skill-codex v${version}