@kood/claude-code 0.7.9 → 0.7.11

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
@@ -23,7 +23,7 @@ var banner = () => {
23
23
 
24
24
  // src/commands/init.ts
25
25
  import fs9 from "fs-extra";
26
- import os from "os";
26
+ import os2 from "os";
27
27
  import path11 from "path";
28
28
 
29
29
  // src/features/templates/template-path-resolver.ts
@@ -327,6 +327,7 @@ var copyCommands = createExtrasCopier("commands");
327
327
  var copyAgents = createExtrasCopier("agents");
328
328
  var copyInstructions = createExtrasCopier("instructions");
329
329
  var copyScripts = createExtrasCopier("scripts");
330
+ var copyHooks = createExtrasCopier("hooks");
330
331
  var getSkillsToInstall = async (skillsSrc, templates) => {
331
332
  const metadataMap = await loadAllSkillMetadata(skillsSrc);
332
333
  const isNonUITemplate = templates.some((t) => NON_UI_TEMPLATES.includes(t));
@@ -372,6 +373,7 @@ var checkExistingClaudeFiles = async (targetDir) => {
372
373
  const agentsDir = path8.join(targetDir, ".claude", "agents");
373
374
  const instructionsDir = path8.join(targetDir, ".claude", "instructions");
374
375
  const scriptsDir = path8.join(targetDir, ".claude", "scripts");
376
+ const hooksDir = path8.join(targetDir, ".claude", "hooks");
375
377
  if (await fs6.pathExists(skillsDir)) {
376
378
  existingFiles.push(".claude/skills/");
377
379
  }
@@ -387,6 +389,9 @@ var checkExistingClaudeFiles = async (targetDir) => {
387
389
  if (await fs6.pathExists(scriptsDir)) {
388
390
  existingFiles.push(".claude/scripts/");
389
391
  }
392
+ if (await fs6.pathExists(hooksDir)) {
393
+ existingFiles.push(".claude/hooks/");
394
+ }
390
395
  return existingFiles;
391
396
  };
392
397
  var checkAllExtrasExist = async (_templates) => {
@@ -396,12 +401,21 @@ var checkAllExtrasExist = async (_templates) => {
396
401
  const agentsSrc = path8.join(claudeDir, "agents");
397
402
  const instructionsSrc = path8.join(claudeDir, "instructions");
398
403
  const scriptsSrc = path8.join(claudeDir, "scripts");
404
+ const hooksSrc = path8.join(claudeDir, "hooks");
399
405
  const hasSkills = await hasFiles(skillsSrc);
400
406
  const hasCommands = await hasFiles(commandsSrc);
401
407
  const hasAgents = await hasFiles(agentsSrc);
402
408
  const hasInstructions = await hasFiles(instructionsSrc);
403
409
  const hasScripts = await hasFiles(scriptsSrc);
404
- return { hasSkills, hasCommands, hasAgents, hasInstructions, hasScripts };
410
+ const hasHooks = await hasFiles(hooksSrc);
411
+ return {
412
+ hasSkills,
413
+ hasCommands,
414
+ hasAgents,
415
+ hasInstructions,
416
+ hasScripts,
417
+ hasHooks
418
+ };
405
419
  };
406
420
 
407
421
  // src/features/extras/extras-installer.ts
@@ -475,6 +489,22 @@ async function installScriptsIfNeeded(templates, targetDir, shouldInstall, hasSc
475
489
  );
476
490
  return scriptsResult;
477
491
  }
492
+ async function installHooksIfNeeded(templates, targetDir, shouldInstall, hasHooks) {
493
+ if (!shouldInstall) {
494
+ return { files: 0, directories: 0 };
495
+ }
496
+ if (!hasHooks) {
497
+ logger.warn("No hooks found in selected templates.");
498
+ return { files: 0, directories: 0 };
499
+ }
500
+ logger.blank();
501
+ logger.info("Installing hooks...");
502
+ const hooksResult = await copyHooks(templates, targetDir);
503
+ logger.success(
504
+ `Hooks: ${hooksResult.files} files, ${hooksResult.directories} directories`
505
+ );
506
+ return hooksResult;
507
+ }
478
508
  async function installInstructionsIfNeeded(templates, targetDir, shouldInstall, hasInstructions) {
479
509
  if (!shouldInstall) {
480
510
  return { files: 0, directories: 0 };
@@ -497,9 +527,10 @@ async function installExtras(templates, targetDir, flags, availability) {
497
527
  installCommands,
498
528
  installAgents,
499
529
  installInstructions,
500
- installScripts
530
+ installScripts,
531
+ installHooks
501
532
  } = flags;
502
- if (!installSkills && !installCommands && !installAgents && !installInstructions && !installScripts) {
533
+ if (!installSkills && !installCommands && !installAgents && !installInstructions && !installScripts && !installHooks) {
503
534
  return { files: 0, directories: 0 };
504
535
  }
505
536
  const existingClaudeFiles = await checkExistingClaudeFiles(targetDir);
@@ -534,8 +565,14 @@ async function installExtras(templates, targetDir, flags, availability) {
534
565
  installScripts,
535
566
  availability.hasScripts
536
567
  );
537
- const totalFiles = skillsResult.files + commandsResult.files + agentsResult.files + instructionsResult.files + scriptsResult.files;
538
- const totalDirectories = skillsResult.directories + commandsResult.directories + agentsResult.directories + instructionsResult.directories + scriptsResult.directories;
568
+ const hooksResult = await installHooksIfNeeded(
569
+ templates,
570
+ targetDir,
571
+ installHooks,
572
+ availability.hasHooks
573
+ );
574
+ const totalFiles = skillsResult.files + commandsResult.files + agentsResult.files + instructionsResult.files + scriptsResult.files + hooksResult.files;
575
+ const totalDirectories = skillsResult.directories + commandsResult.directories + agentsResult.directories + instructionsResult.directories + scriptsResult.directories + hooksResult.directories;
539
576
  return { files: totalFiles, directories: totalDirectories };
540
577
  }
541
578
 
@@ -620,19 +657,22 @@ async function promptExtrasSelection(options) {
620
657
  agents,
621
658
  instructions,
622
659
  scripts,
660
+ hooks,
623
661
  hasSkills,
624
662
  hasCommands,
625
663
  hasAgents,
626
664
  hasInstructions,
627
- hasScripts
665
+ hasScripts,
666
+ hasHooks
628
667
  } = options;
629
668
  let installSkills = skills ?? false;
630
669
  let installCommands = commands ?? false;
631
670
  const installAgents = agents ?? hasAgents;
632
671
  const installInstructions = instructions ?? hasInstructions;
633
672
  const installScripts = scripts ?? hasScripts;
634
- const noOptionsProvided = skills === void 0 && commands === void 0 && agents === void 0 && instructions === void 0 && scripts === void 0;
635
- if (noOptionsProvided && (hasSkills || hasCommands || hasAgents || hasInstructions || hasScripts)) {
673
+ const installHooks = hooks ?? hasHooks;
674
+ const noOptionsProvided = skills === void 0 && commands === void 0 && agents === void 0 && instructions === void 0 && scripts === void 0 && hooks === void 0;
675
+ if (noOptionsProvided && (hasSkills || hasCommands || hasAgents || hasInstructions || hasScripts || hasHooks)) {
636
676
  logger.blank();
637
677
  if (hasSkills) {
638
678
  const result = await promptConfirm(
@@ -654,7 +694,8 @@ async function promptExtrasSelection(options) {
654
694
  installCommands,
655
695
  installAgents,
656
696
  installInstructions,
657
- installScripts
697
+ installScripts,
698
+ installHooks
658
699
  };
659
700
  }
660
701
  async function promptScopeSelection(options) {
@@ -690,7 +731,7 @@ async function promptScopeSelection(options) {
690
731
  return { scope: response.value };
691
732
  }
692
733
  async function promptCodexSync(options) {
693
- const { providedSyncCodex, codexSkillsPath } = options;
734
+ const { providedSyncCodex, codexPath } = options;
694
735
  if (providedSyncCodex !== void 0) {
695
736
  return { syncCodex: providedSyncCodex };
696
737
  }
@@ -698,7 +739,7 @@ async function promptCodexSync(options) {
698
739
  return { syncCodex: false };
699
740
  }
700
741
  logger.blank();
701
- const pathHint = codexSkillsPath ? ` (${codexSkillsPath})` : "";
742
+ const pathHint = codexPath ? ` (${codexPath})` : "";
702
743
  const result = await promptConfirm(
703
744
  `Also sync with Codex now?${pathHint}`,
704
745
  false
@@ -786,8 +827,15 @@ async function updateGitignore(targetDir, options = {}) {
786
827
 
787
828
  // src/features/codex-sync/codex-sync.ts
788
829
  import fs8 from "fs-extra";
830
+ import os from "os";
789
831
  import path10 from "path";
790
832
  var RESERVED_CODEX_SKILL_DIRS = /* @__PURE__ */ new Set([".system", "claude-commands"]);
833
+ var COMMAND_INSTRUCTION_PREFIX_REGEX = /^@\.\.\/instructions\//gm;
834
+ var COMMAND_INSTRUCTION_PREFIX_REPLACEMENT = "@../../../instructions/";
835
+ var MAX_REFERENCE_ISSUE_SAMPLES = 10;
836
+ var CLAUDE_STATE_FILE = ".claude.json";
837
+ var CLAUDE_LOCAL_MCP_FILE = ".mcp.json";
838
+ var CODEX_CONFIG_FILE = "config.toml";
791
839
  function normalizeLineEndings(content) {
792
840
  return content.replace(/\r\n/g, "\n");
793
841
  }
@@ -816,6 +864,10 @@ function extractDescriptionFromFrontmatter(markdown) {
816
864
  function buildCommandSkillContent(commandPath, commandRaw) {
817
865
  const commandName = path10.basename(commandPath, ".md");
818
866
  const normalizedCommand = normalizeLineEndings(commandRaw).trim();
867
+ const rewrittenCommand = normalizedCommand.replace(
868
+ COMMAND_INSTRUCTION_PREFIX_REGEX,
869
+ COMMAND_INSTRUCTION_PREFIX_REPLACEMENT
870
+ );
819
871
  const sourcePath = path10.resolve(commandPath);
820
872
  const description = extractDescriptionFromFrontmatter(normalizedCommand) || `${commandName} command imported from .claude/commands`;
821
873
  return `---
@@ -830,18 +882,344 @@ resolve them from the current workspace first; if missing, ask for the intended
830
882
 
831
883
  ---
832
884
 
833
- ${normalizedCommand}
885
+ ${rewrittenCommand}
834
886
  `;
835
887
  }
888
+ function isRecord(value) {
889
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
890
+ }
891
+ function asStringArray(value) {
892
+ if (!Array.isArray(value)) {
893
+ return void 0;
894
+ }
895
+ const items = value.filter(
896
+ (item) => typeof item === "string"
897
+ );
898
+ return items.length > 0 ? items : void 0;
899
+ }
900
+ function asStringRecord(value) {
901
+ if (!isRecord(value)) {
902
+ return void 0;
903
+ }
904
+ const entries = Object.entries(value).filter(
905
+ (entry) => typeof entry[1] === "string"
906
+ );
907
+ if (entries.length === 0) {
908
+ return void 0;
909
+ }
910
+ return Object.fromEntries(entries);
911
+ }
912
+ function normalizeClaudeMcpServer(value) {
913
+ if (!isRecord(value)) {
914
+ return void 0;
915
+ }
916
+ const type = typeof value.type === "string" ? value.type : void 0;
917
+ const command = typeof value.command === "string" ? value.command : void 0;
918
+ const args = asStringArray(value.args);
919
+ const env = asStringRecord(value.env);
920
+ const cwd = typeof value.cwd === "string" ? value.cwd : void 0;
921
+ const url = typeof value.url === "string" ? value.url : void 0;
922
+ const headers = asStringRecord(value.headers);
923
+ const bearerTokenEnvVar = typeof value.bearerTokenEnvVar === "string" ? value.bearerTokenEnvVar : typeof value.bearer_token_env_var === "string" ? value.bearer_token_env_var : void 0;
924
+ const envHttpHeaders = asStringRecord(value.envHttpHeaders) || asStringRecord(value.env_http_headers);
925
+ return {
926
+ type,
927
+ command,
928
+ args,
929
+ env,
930
+ cwd,
931
+ url,
932
+ headers,
933
+ bearerTokenEnvVar,
934
+ envHttpHeaders
935
+ };
936
+ }
937
+ function normalizeClaudeMcpServers(value) {
938
+ if (!isRecord(value)) {
939
+ return {};
940
+ }
941
+ const normalizedEntries = Object.entries(value).map(([name, server]) => [name, normalizeClaudeMcpServer(server)]).filter((entry) => Boolean(entry[1]));
942
+ return Object.fromEntries(normalizedEntries);
943
+ }
944
+ function getProjectState(claudeState, targetDir) {
945
+ const projects = claudeState.projects;
946
+ if (!isRecord(projects)) {
947
+ return void 0;
948
+ }
949
+ const normalizedTarget = path10.resolve(targetDir);
950
+ for (const [projectPath, projectState] of Object.entries(projects)) {
951
+ if (path10.resolve(projectPath) !== normalizedTarget) {
952
+ continue;
953
+ }
954
+ if (isRecord(projectState)) {
955
+ return projectState;
956
+ }
957
+ }
958
+ return void 0;
959
+ }
960
+ function filterMcpJsonServersByProjectState(servers, projectState) {
961
+ if (!projectState) {
962
+ return servers;
963
+ }
964
+ const enabled = asStringArray(projectState.enabledMcpjsonServers) || [];
965
+ const disabled = new Set(
966
+ asStringArray(projectState.disabledMcpjsonServers) || []
967
+ );
968
+ const enabledSet = new Set(enabled);
969
+ const hasEnabledFilter = enabledSet.size > 0;
970
+ const filteredEntries = Object.entries(servers).filter(([name]) => {
971
+ if (hasEnabledFilter && !enabledSet.has(name)) {
972
+ return false;
973
+ }
974
+ if (disabled.has(name)) {
975
+ return false;
976
+ }
977
+ return true;
978
+ });
979
+ return Object.fromEntries(filteredEntries);
980
+ }
981
+ async function readJsonFile(filePath) {
982
+ if (!await fs8.pathExists(filePath)) {
983
+ return void 0;
984
+ }
985
+ try {
986
+ const raw = await fs8.readFile(filePath, "utf8");
987
+ const parsed = JSON.parse(raw);
988
+ if (!isRecord(parsed)) {
989
+ return void 0;
990
+ }
991
+ return parsed;
992
+ } catch {
993
+ return void 0;
994
+ }
995
+ }
996
+ function toCodexMcpServer(server) {
997
+ const serverType = server.type?.toLowerCase();
998
+ const isHttp = serverType === "http" || Boolean(server.url);
999
+ if (isHttp) {
1000
+ if (!server.url) {
1001
+ return void 0;
1002
+ }
1003
+ return {
1004
+ kind: "http",
1005
+ url: server.url,
1006
+ bearerTokenEnvVar: server.bearerTokenEnvVar,
1007
+ httpHeaders: server.headers,
1008
+ envHttpHeaders: server.envHttpHeaders
1009
+ };
1010
+ }
1011
+ if (!server.command) {
1012
+ return void 0;
1013
+ }
1014
+ return {
1015
+ kind: "stdio",
1016
+ command: server.command,
1017
+ args: server.args,
1018
+ env: server.env,
1019
+ cwd: server.cwd
1020
+ };
1021
+ }
1022
+ function tomlQuote(value) {
1023
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
1024
+ }
1025
+ function tomlKey(value) {
1026
+ return /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlQuote(value);
1027
+ }
1028
+ function parseTomlPath(input) {
1029
+ const pathInput = input.trim();
1030
+ if (!pathInput) {
1031
+ return void 0;
1032
+ }
1033
+ const segments = [];
1034
+ let current = "";
1035
+ let inQuotes = false;
1036
+ let escaped = false;
1037
+ for (const char of pathInput) {
1038
+ if (inQuotes) {
1039
+ if (escaped) {
1040
+ current += char;
1041
+ escaped = false;
1042
+ continue;
1043
+ }
1044
+ if (char === "\\") {
1045
+ escaped = true;
1046
+ continue;
1047
+ }
1048
+ if (char === '"') {
1049
+ inQuotes = false;
1050
+ continue;
1051
+ }
1052
+ current += char;
1053
+ continue;
1054
+ }
1055
+ if (char === '"') {
1056
+ inQuotes = true;
1057
+ continue;
1058
+ }
1059
+ if (char === ".") {
1060
+ const value = current.trim();
1061
+ if (!value) {
1062
+ return void 0;
1063
+ }
1064
+ segments.push(value);
1065
+ current = "";
1066
+ continue;
1067
+ }
1068
+ current += char;
1069
+ }
1070
+ if (inQuotes || escaped) {
1071
+ return void 0;
1072
+ }
1073
+ const tail = current.trim();
1074
+ if (!tail) {
1075
+ return void 0;
1076
+ }
1077
+ segments.push(tail);
1078
+ return segments;
1079
+ }
1080
+ function stripCodexMcpSections(configToml) {
1081
+ const lines = normalizeLineEndings(configToml).split("\n");
1082
+ const kept = [];
1083
+ let droppingMcpSection = false;
1084
+ for (const line of lines) {
1085
+ const headerMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
1086
+ if (headerMatch) {
1087
+ const headerPath = parseTomlPath(headerMatch[1]);
1088
+ droppingMcpSection = Boolean(
1089
+ headerPath && headerPath.length > 0 && headerPath[0] === "mcp_servers"
1090
+ );
1091
+ }
1092
+ if (!droppingMcpSection) {
1093
+ kept.push(line);
1094
+ }
1095
+ }
1096
+ return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
1097
+ }
1098
+ function renderTomlStringArray(values) {
1099
+ if (!values || values.length === 0) {
1100
+ return void 0;
1101
+ }
1102
+ return `[${values.map((value) => tomlQuote(value)).join(", ")}]`;
1103
+ }
1104
+ function renderTomlStringTable(header, values) {
1105
+ if (!values || Object.keys(values).length === 0) {
1106
+ return void 0;
1107
+ }
1108
+ const lines = Object.entries(values).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => `${tomlKey(key)} = ${tomlQuote(value)}`);
1109
+ return `${header}
1110
+ ${lines.join("\n")}`;
1111
+ }
1112
+ function renderCodexMcpServer(name, server) {
1113
+ const sectionRoot = `[mcp_servers.${tomlKey(name)}]`;
1114
+ const lines = [sectionRoot];
1115
+ if (server.kind === "http") {
1116
+ lines.push(`url = ${tomlQuote(server.url ?? "")}`);
1117
+ if (server.bearerTokenEnvVar) {
1118
+ lines.push(
1119
+ `bearer_token_env_var = ${tomlQuote(server.bearerTokenEnvVar)}`
1120
+ );
1121
+ }
1122
+ } else {
1123
+ lines.push(`command = ${tomlQuote(server.command ?? "")}`);
1124
+ const args = renderTomlStringArray(server.args);
1125
+ if (args) {
1126
+ lines.push(`args = ${args}`);
1127
+ }
1128
+ if (server.cwd) {
1129
+ lines.push(`cwd = ${tomlQuote(server.cwd)}`);
1130
+ }
1131
+ }
1132
+ const envSection = server.kind === "stdio" ? renderTomlStringTable(`[mcp_servers.${tomlKey(name)}.env]`, server.env) : void 0;
1133
+ const httpHeadersSection = server.kind === "http" ? renderTomlStringTable(
1134
+ `[mcp_servers.${tomlKey(name)}.http_headers]`,
1135
+ server.httpHeaders
1136
+ ) : void 0;
1137
+ const envHttpHeadersSection = server.kind === "http" ? renderTomlStringTable(
1138
+ `[mcp_servers.${tomlKey(name)}.env_http_headers]`,
1139
+ server.envHttpHeaders
1140
+ ) : void 0;
1141
+ const sections = [
1142
+ lines.join("\n"),
1143
+ envSection,
1144
+ httpHeadersSection,
1145
+ envHttpHeadersSection
1146
+ ].filter((section) => Boolean(section));
1147
+ return sections.join("\n\n");
1148
+ }
1149
+ function mergeCodexConfigToml(existingToml, renderedMcpBlocks) {
1150
+ const base = stripCodexMcpSections(existingToml);
1151
+ const mcp = renderedMcpBlocks.trim();
1152
+ if (!base) {
1153
+ return mcp;
1154
+ }
1155
+ if (!mcp) {
1156
+ return base;
1157
+ }
1158
+ return `${base}
1159
+
1160
+ ${mcp}`;
1161
+ }
1162
+ async function loadClaudeMcpServers(targetDir) {
1163
+ const homeDir = path10.resolve(os.homedir());
1164
+ const normalizedTarget = path10.resolve(targetDir);
1165
+ const isUserScope = normalizedTarget === homeDir;
1166
+ const claudeStatePath = path10.join(homeDir, CLAUDE_STATE_FILE);
1167
+ const claudeState = await readJsonFile(claudeStatePath);
1168
+ if (isUserScope) {
1169
+ return {
1170
+ scope: "global",
1171
+ servers: normalizeClaudeMcpServers(claudeState?.mcpServers)
1172
+ };
1173
+ }
1174
+ const projectState = claudeState ? getProjectState(claudeState, normalizedTarget) : void 0;
1175
+ const projectServers = normalizeClaudeMcpServers(projectState?.mcpServers);
1176
+ const localMcpPath = path10.join(normalizedTarget, CLAUDE_LOCAL_MCP_FILE);
1177
+ const localMcpState = await readJsonFile(localMcpPath);
1178
+ const localMcpServers = filterMcpJsonServersByProjectState(
1179
+ normalizeClaudeMcpServers(localMcpState?.mcpServers),
1180
+ projectState
1181
+ );
1182
+ return {
1183
+ scope: "local",
1184
+ servers: {
1185
+ ...localMcpServers,
1186
+ ...projectServers
1187
+ }
1188
+ };
1189
+ }
1190
+ async function syncMcpServers(targetDir) {
1191
+ const homeDir = path10.resolve(os.homedir());
1192
+ const normalizedTarget = path10.resolve(targetDir);
1193
+ const isUserScope = normalizedTarget === homeDir;
1194
+ const codexBaseDir = isUserScope ? path10.join(homeDir, ".codex") : path10.join(normalizedTarget, ".codex");
1195
+ const codexMcpConfigPath = path10.join(codexBaseDir, CODEX_CONFIG_FILE);
1196
+ const { scope, servers } = await loadClaudeMcpServers(normalizedTarget);
1197
+ const codexServers = Object.entries(servers).map(([name, server]) => [name, toCodexMcpServer(server)]).filter((entry) => Boolean(entry[1])).sort((a, b) => a[0].localeCompare(b[0]));
1198
+ const renderedMcpBlocks = codexServers.map(([name, server]) => renderCodexMcpServer(name, server)).join("\n\n");
1199
+ const existingConfig = await fs8.pathExists(codexMcpConfigPath) ? await fs8.readFile(codexMcpConfigPath, "utf8") : "";
1200
+ const nextConfig = mergeCodexConfigToml(existingConfig, renderedMcpBlocks);
1201
+ await fs8.ensureDir(codexBaseDir);
1202
+ await fs8.writeFile(codexMcpConfigPath, `${nextConfig.trimEnd()}
1203
+ `);
1204
+ return {
1205
+ codexMcpConfigPath,
1206
+ syncedMcpServers: codexServers.length,
1207
+ scope
1208
+ };
1209
+ }
836
1210
  async function syncSkills(claudeSkillsDir, codexSkillsDir) {
837
1211
  await fs8.ensureDir(codexSkillsDir);
838
1212
  let sourceSkillNames = [];
839
1213
  if (await fs8.pathExists(claudeSkillsDir)) {
840
- const sourceEntries = await fs8.readdir(claudeSkillsDir, { withFileTypes: true });
1214
+ const sourceEntries = await fs8.readdir(claudeSkillsDir, {
1215
+ withFileTypes: true
1216
+ });
841
1217
  sourceSkillNames = sourceEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => name !== ".system");
842
1218
  }
843
1219
  const sourceSkillSet = new Set(sourceSkillNames);
844
- const codexEntries = await fs8.readdir(codexSkillsDir, { withFileTypes: true });
1220
+ const codexEntries = await fs8.readdir(codexSkillsDir, {
1221
+ withFileTypes: true
1222
+ });
845
1223
  for (const entry of codexEntries) {
846
1224
  if (!entry.isDirectory()) {
847
1225
  continue;
@@ -883,26 +1261,126 @@ async function syncCommands(claudeCommandsDir, codexCommandsDir) {
883
1261
  const skillDir = path10.join(codexCommandsDir, commandName);
884
1262
  const skillPath = path10.join(skillDir, "SKILL.md");
885
1263
  await fs8.ensureDir(skillDir);
886
- await fs8.writeFile(skillPath, buildCommandSkillContent(commandPath, commandRaw));
1264
+ await fs8.writeFile(
1265
+ skillPath,
1266
+ buildCommandSkillContent(commandPath, commandRaw)
1267
+ );
887
1268
  }
888
1269
  return commandFiles.length;
889
1270
  }
1271
+ async function syncInstructions(claudeInstructionsDir, codexInstructionsDir) {
1272
+ if (!await fs8.pathExists(claudeInstructionsDir)) {
1273
+ await fs8.remove(codexInstructionsDir);
1274
+ return 0;
1275
+ }
1276
+ await fs8.remove(codexInstructionsDir);
1277
+ await fs8.ensureDir(codexInstructionsDir);
1278
+ const counter = { files: 0, directories: 0 };
1279
+ await copyRecursive(claudeInstructionsDir, codexInstructionsDir, counter);
1280
+ return counter.files;
1281
+ }
1282
+ function extractReferenceCandidate(line) {
1283
+ const trimmed = line.trim();
1284
+ if (!trimmed.startsWith("@")) {
1285
+ return void 0;
1286
+ }
1287
+ const [candidate] = trimmed.slice(1).split(/\s+/);
1288
+ if (!candidate || candidate.startsWith("#") || candidate.includes("://")) {
1289
+ return void 0;
1290
+ }
1291
+ const looksLikePath = candidate.startsWith("./") || candidate.startsWith("../") || candidate.startsWith("/") || candidate.includes("/") || /\.[a-z0-9]+$/i.test(candidate);
1292
+ if (!looksLikePath) {
1293
+ return void 0;
1294
+ }
1295
+ return candidate;
1296
+ }
1297
+ async function collectSkillMarkdownFiles(rootDir, collector = []) {
1298
+ if (!await fs8.pathExists(rootDir)) {
1299
+ return collector;
1300
+ }
1301
+ const entries = await fs8.readdir(rootDir, { withFileTypes: true });
1302
+ for (const entry of entries) {
1303
+ const fullPath = path10.join(rootDir, entry.name);
1304
+ if (entry.isDirectory()) {
1305
+ await collectSkillMarkdownFiles(fullPath, collector);
1306
+ continue;
1307
+ }
1308
+ if (entry.isFile() && entry.name === "SKILL.md") {
1309
+ collector.push(fullPath);
1310
+ }
1311
+ }
1312
+ return collector;
1313
+ }
1314
+ async function validateSkillReferences(codexSkillsDir) {
1315
+ const skillFiles = await collectSkillMarkdownFiles(codexSkillsDir);
1316
+ let count = 0;
1317
+ const samples = [];
1318
+ for (const skillFile of skillFiles) {
1319
+ const content = await fs8.readFile(skillFile, "utf8");
1320
+ const lines = normalizeLineEndings(content).split("\n");
1321
+ for (const line of lines) {
1322
+ const candidate = extractReferenceCandidate(line);
1323
+ if (!candidate) {
1324
+ continue;
1325
+ }
1326
+ const referencePath = candidate.split(/[?#]/, 1)[0];
1327
+ if (!referencePath) {
1328
+ continue;
1329
+ }
1330
+ const resolvedPath = path10.resolve(path10.dirname(skillFile), referencePath);
1331
+ if (await fs8.pathExists(resolvedPath)) {
1332
+ continue;
1333
+ }
1334
+ count += 1;
1335
+ if (samples.length < MAX_REFERENCE_ISSUE_SAMPLES) {
1336
+ samples.push({
1337
+ skillPath: skillFile,
1338
+ reference: candidate,
1339
+ resolvedPath
1340
+ });
1341
+ }
1342
+ }
1343
+ }
1344
+ return { count, samples };
1345
+ }
890
1346
  async function syncWithCodex(targetDir) {
891
1347
  const codexHome = path10.resolve(targetDir, ".codex");
892
1348
  const codexSkillsDir = path10.join(codexHome, "skills");
893
1349
  const codexCommandsDir = path10.join(codexSkillsDir, "claude-commands");
1350
+ const codexInstructionsDir = path10.join(codexHome, "instructions");
894
1351
  await fs8.ensureDir(codexSkillsDir);
895
1352
  const claudeRootDir = path10.join(targetDir, ".claude");
896
1353
  const claudeSkillsDir = path10.join(claudeRootDir, "skills");
897
1354
  const claudeCommandsDir = path10.join(claudeRootDir, "commands");
1355
+ const claudeInstructionsDir = path10.join(claudeRootDir, "instructions");
898
1356
  const syncedSkills = await syncSkills(claudeSkillsDir, codexSkillsDir);
899
- const syncedCommands = await syncCommands(claudeCommandsDir, codexCommandsDir);
1357
+ const syncedInstructions = await syncInstructions(
1358
+ claudeInstructionsDir,
1359
+ codexInstructionsDir
1360
+ );
1361
+ const syncedCommands = await syncCommands(
1362
+ claudeCommandsDir,
1363
+ codexCommandsDir
1364
+ );
1365
+ const {
1366
+ codexMcpConfigPath,
1367
+ syncedMcpServers,
1368
+ scope: mcpScope
1369
+ } = await syncMcpServers(targetDir);
1370
+ const { count: referenceIssueCount, samples: referenceIssueSamples } = await validateSkillReferences(codexSkillsDir);
900
1371
  return {
901
1372
  codexHome,
902
1373
  codexSkillsDir,
903
1374
  codexCommandsDir,
1375
+ codexInstructionsDir,
904
1376
  syncedSkills,
905
- syncedCommands
1377
+ syncedCommands,
1378
+ syncedInstructions,
1379
+ codexMcpConfigPath,
1380
+ syncedMcpServers,
1381
+ mcpScope,
1382
+ referenceIssueCount,
1383
+ referenceIssueSamples
906
1384
  };
907
1385
  }
908
1386
 
@@ -983,29 +1461,32 @@ async function installTemplates(templates, targetDir) {
983
1461
  logger.success(`Total: ${totalFiles} files, ${totalDirectories} directories`);
984
1462
  return { files: totalFiles, directories: totalDirectories };
985
1463
  }
986
- async function promptForExtrasInstallation(options, hasSkills, hasCommands, hasAgents, hasInstructions, hasScripts) {
1464
+ async function promptForExtrasInstallation(options, hasSkills, hasCommands, hasAgents, hasInstructions, hasScripts, hasHooks) {
987
1465
  return await promptExtrasSelection({
988
1466
  skills: options.skills,
989
1467
  commands: options.commands,
990
1468
  agents: options.agents,
991
1469
  instructions: options.instructions,
992
1470
  scripts: options.scripts,
1471
+ hooks: options.hooks,
993
1472
  hasSkills,
994
1473
  hasCommands,
995
1474
  hasAgents,
996
1475
  hasInstructions,
997
- hasScripts
1476
+ hasScripts,
1477
+ hasHooks
998
1478
  });
999
1479
  }
1000
- function showInstallationSummary(templates, flags, hasSkills, hasCommands, hasAgents, hasInstructions, hasScripts, scope) {
1480
+ function showInstallationSummary(templates, flags, hasSkills, hasCommands, hasAgents, hasInstructions, hasScripts, hasHooks, scope) {
1001
1481
  const {
1002
1482
  installSkills,
1003
1483
  installCommands,
1004
1484
  installAgents,
1005
1485
  installInstructions,
1006
- installScripts
1486
+ installScripts,
1487
+ installHooks
1007
1488
  } = flags;
1008
- const hasExtrasInstalled = installSkills && hasSkills || installCommands && hasCommands || installAgents && hasAgents || installInstructions && hasInstructions || installScripts && hasScripts;
1489
+ const hasExtrasInstalled = installSkills && hasSkills || installCommands && hasCommands || installAgents && hasAgents || installInstructions && hasInstructions || installScripts && hasScripts || installHooks && hasHooks;
1009
1490
  if (templates.length === 0 && !hasExtrasInstalled) {
1010
1491
  logger.blank();
1011
1492
  logger.info("No templates or extras installed.");
@@ -1038,6 +1519,9 @@ function showInstallationSummary(templates, flags, hasSkills, hasCommands, hasAg
1038
1519
  if (installScripts && hasScripts) {
1039
1520
  logger.step("Scripts \u2192 .claude/scripts/");
1040
1521
  }
1522
+ if (installHooks && hasHooks) {
1523
+ logger.step("Hooks \u2192 .claude/hooks/");
1524
+ }
1041
1525
  }
1042
1526
  logger.blank();
1043
1527
  logger.info("Next steps:");
@@ -1056,7 +1540,7 @@ var init = async (options) => {
1056
1540
  const { scope } = await promptScopeSelection({
1057
1541
  providedScope: options.scope
1058
1542
  });
1059
- const targetDir = scope === "user" ? os.homedir() : projectDir;
1543
+ const targetDir = scope === "user" ? os2.homedir() : projectDir;
1060
1544
  const isUserScope = scope === "user";
1061
1545
  if (isUserScope) {
1062
1546
  logger.blank();
@@ -1076,21 +1560,30 @@ var init = async (options) => {
1076
1560
  }
1077
1561
  }
1078
1562
  const templatesToCheck = templates.length > 0 ? templates : availableTemplates;
1079
- const { hasSkills, hasCommands, hasAgents, hasInstructions, hasScripts } = await checkAllExtrasExist(templatesToCheck);
1563
+ const {
1564
+ hasSkills,
1565
+ hasCommands,
1566
+ hasAgents,
1567
+ hasInstructions,
1568
+ hasScripts,
1569
+ hasHooks
1570
+ } = await checkAllExtrasExist(templatesToCheck);
1080
1571
  const flags = await promptForExtrasInstallation(
1081
1572
  options,
1082
1573
  hasSkills,
1083
1574
  hasCommands,
1084
1575
  hasAgents,
1085
1576
  hasInstructions,
1086
- hasScripts
1577
+ hasScripts,
1578
+ hasHooks
1087
1579
  );
1088
1580
  await installExtras(templatesToCheck, targetDir, flags, {
1089
1581
  hasSkills,
1090
1582
  hasCommands,
1091
1583
  hasAgents,
1092
1584
  hasInstructions,
1093
- hasScripts
1585
+ hasScripts,
1586
+ hasHooks
1094
1587
  });
1095
1588
  showInstallationSummary(
1096
1589
  templates,
@@ -1100,16 +1593,17 @@ var init = async (options) => {
1100
1593
  hasAgents,
1101
1594
  hasInstructions,
1102
1595
  hasScripts,
1596
+ hasHooks,
1103
1597
  scope
1104
1598
  );
1105
- const codexSkillsPath = path11.join(targetDir, ".codex", "skills");
1599
+ const codexSyncPath = path11.join(targetDir, ".codex");
1106
1600
  const { syncCodex } = await promptCodexSync({
1107
1601
  providedSyncCodex: options.syncCodex,
1108
- codexSkillsPath
1602
+ codexPath: codexSyncPath
1109
1603
  });
1110
1604
  if (syncCodex) {
1111
1605
  logger.blank();
1112
- logger.info("Syncing .claude skills/commands to Codex...");
1606
+ logger.info("Syncing .claude skills/commands/instructions/MCP to Codex...");
1113
1607
  try {
1114
1608
  const result = await syncWithCodex(targetDir);
1115
1609
  if (result.syncedSkills > 0) {
@@ -1118,12 +1612,33 @@ var init = async (options) => {
1118
1612
  if (result.syncedCommands > 0) {
1119
1613
  logger.step(`Commands synced: ${result.syncedCommands}`);
1120
1614
  }
1121
- if (result.syncedSkills === 0 && result.syncedCommands === 0) {
1615
+ if (result.syncedInstructions > 0) {
1616
+ logger.step(`Instructions synced: ${result.syncedInstructions}`);
1617
+ }
1618
+ if (result.syncedMcpServers > 0) {
1619
+ logger.step(
1620
+ `MCP servers synced (${result.mcpScope}): ${result.syncedMcpServers}`
1621
+ );
1622
+ }
1623
+ if (result.syncedSkills === 0 && result.syncedCommands === 0 && result.syncedInstructions === 0 && result.syncedMcpServers === 0) {
1122
1624
  logger.warn(
1123
- "Nothing was synced. .claude/skills and .claude/commands were not found."
1625
+ "Nothing was synced. .claude/skills, .claude/commands, .claude/instructions, and Claude MCP settings were not found."
1124
1626
  );
1125
1627
  } else {
1126
1628
  logger.success(`Codex sync complete: ${result.codexSkillsDir}`);
1629
+ logger.step(`Codex MCP config: ${result.codexMcpConfigPath}`);
1630
+ if (result.referenceIssueCount > 0) {
1631
+ logger.warn(
1632
+ `Codex skill reference issues found: ${result.referenceIssueCount} (showing up to ${result.referenceIssueSamples.length})`
1633
+ );
1634
+ for (const issue of result.referenceIssueSamples) {
1635
+ const skillPath = path11.relative(targetDir, issue.skillPath);
1636
+ const resolvedPath = path11.relative(targetDir, issue.resolvedPath);
1637
+ logger.step(
1638
+ `${skillPath} -> @${issue.reference} (missing: ${resolvedPath})`
1639
+ );
1640
+ }
1641
+ }
1127
1642
  }
1128
1643
  } catch (error) {
1129
1644
  logger.warn(
@@ -1144,13 +1659,13 @@ var init = async (options) => {
1144
1659
 
1145
1660
  // src/index.ts
1146
1661
  var program = new Command();
1147
- program.name("claude-code").description("Claude Code documentation installer for projects").version("0.7.9");
1662
+ program.name("claude-code").description("Claude Code documentation installer for projects").version("0.7.11");
1148
1663
  program.option(
1149
1664
  "-t, --template <names>",
1150
1665
  "template names (comma-separated: tanstack-start,hono)"
1151
1666
  ).option("-f, --force", "overwrite existing files without prompting").option("--cwd <path>", "target directory (default: current directory)").option("--list", "list available templates").option("-s, --skills", "install skills to .claude/skills/").option("-c, --commands", "install commands to .claude/commands/").option("-a, --agents", "install agents to .claude/agents/").option(
1152
1667
  "--sync-codex",
1153
- "sync installed .claude skills/commands to selected-scope .codex/skills/"
1668
+ "sync installed .claude skills/commands/instructions and Claude MCP settings to selected-scope .codex/"
1154
1669
  ).option("--scope <scope>", "installation scope (project|user)").action(async (options) => {
1155
1670
  banner();
1156
1671
  if (options.list) {