@kood/claude-code 0.7.8 → 0.7.10
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 +522 -56
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ var banner = () => {
|
|
|
24
24
|
// src/commands/init.ts
|
|
25
25
|
import fs9 from "fs-extra";
|
|
26
26
|
import os2 from "os";
|
|
27
|
+
import path11 from "path";
|
|
27
28
|
|
|
28
29
|
// src/features/templates/template-path-resolver.ts
|
|
29
30
|
import path2 from "path";
|
|
@@ -689,7 +690,7 @@ async function promptScopeSelection(options) {
|
|
|
689
690
|
return { scope: response.value };
|
|
690
691
|
}
|
|
691
692
|
async function promptCodexSync(options) {
|
|
692
|
-
const { providedSyncCodex } = options;
|
|
693
|
+
const { providedSyncCodex, codexPath } = options;
|
|
693
694
|
if (providedSyncCodex !== void 0) {
|
|
694
695
|
return { syncCodex: providedSyncCodex };
|
|
695
696
|
}
|
|
@@ -697,8 +698,9 @@ async function promptCodexSync(options) {
|
|
|
697
698
|
return { syncCodex: false };
|
|
698
699
|
}
|
|
699
700
|
logger.blank();
|
|
701
|
+
const pathHint = codexPath ? ` (${codexPath})` : "";
|
|
700
702
|
const result = await promptConfirm(
|
|
701
|
-
|
|
703
|
+
`Also sync with Codex now?${pathHint}`,
|
|
702
704
|
false
|
|
703
705
|
);
|
|
704
706
|
return { syncCodex: result.confirmed };
|
|
@@ -720,6 +722,7 @@ var CLAUDE_GENERATED_FOLDERS = [
|
|
|
720
722
|
".claude/research/",
|
|
721
723
|
".claude/validation-results/"
|
|
722
724
|
];
|
|
725
|
+
var CODEX_GENERATED_FOLDERS = [".codex/"];
|
|
723
726
|
function normalizeIgnorePattern(pattern) {
|
|
724
727
|
return pattern.replace(/\\/g, "/").replace(/^\.?\//, "").replace(/\/+$/, "").trim();
|
|
725
728
|
}
|
|
@@ -732,9 +735,11 @@ function parseIgnoreLine(line) {
|
|
|
732
735
|
const normalized = normalizeIgnorePattern(rawPattern);
|
|
733
736
|
return normalized || null;
|
|
734
737
|
}
|
|
735
|
-
async function updateGitignore(targetDir) {
|
|
738
|
+
async function updateGitignore(targetDir, options = {}) {
|
|
739
|
+
const { includeCodex = false } = options;
|
|
736
740
|
const gitignorePath = path9.join(targetDir, ".gitignore");
|
|
737
741
|
const sectionComment = "# Claude Code generated files";
|
|
742
|
+
const targetFolders = includeCodex ? [...CLAUDE_GENERATED_FOLDERS, ...CODEX_GENERATED_FOLDERS] : CLAUDE_GENERATED_FOLDERS;
|
|
738
743
|
let content = "";
|
|
739
744
|
let hasGitignore = false;
|
|
740
745
|
try {
|
|
@@ -748,7 +753,7 @@ async function updateGitignore(targetDir) {
|
|
|
748
753
|
const existingPatterns = new Set(
|
|
749
754
|
lines.map((line) => parseIgnoreLine(line)).filter((pattern) => Boolean(pattern))
|
|
750
755
|
);
|
|
751
|
-
const linesToAdd =
|
|
756
|
+
const linesToAdd = targetFolders.filter(
|
|
752
757
|
(folder) => !existingPatterns.has(normalizeIgnorePattern(folder))
|
|
753
758
|
);
|
|
754
759
|
if (linesToAdd.length === 0) {
|
|
@@ -783,13 +788,13 @@ async function updateGitignore(targetDir) {
|
|
|
783
788
|
import fs8 from "fs-extra";
|
|
784
789
|
import os from "os";
|
|
785
790
|
import path10 from "path";
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
791
|
+
var RESERVED_CODEX_SKILL_DIRS = /* @__PURE__ */ new Set([".system", "claude-commands"]);
|
|
792
|
+
var COMMAND_INSTRUCTION_PREFIX_REGEX = /^@\.\.\/instructions\//gm;
|
|
793
|
+
var COMMAND_INSTRUCTION_PREFIX_REPLACEMENT = "@../../../instructions/";
|
|
794
|
+
var MAX_REFERENCE_ISSUE_SAMPLES = 10;
|
|
795
|
+
var CLAUDE_STATE_FILE = ".claude.json";
|
|
796
|
+
var CLAUDE_LOCAL_MCP_FILE = ".mcp.json";
|
|
797
|
+
var CODEX_CONFIG_FILE = "config.toml";
|
|
793
798
|
function normalizeLineEndings(content) {
|
|
794
799
|
return content.replace(/\r\n/g, "\n");
|
|
795
800
|
}
|
|
@@ -818,6 +823,10 @@ function extractDescriptionFromFrontmatter(markdown) {
|
|
|
818
823
|
function buildCommandSkillContent(commandPath, commandRaw) {
|
|
819
824
|
const commandName = path10.basename(commandPath, ".md");
|
|
820
825
|
const normalizedCommand = normalizeLineEndings(commandRaw).trim();
|
|
826
|
+
const rewrittenCommand = normalizedCommand.replace(
|
|
827
|
+
COMMAND_INSTRUCTION_PREFIX_REGEX,
|
|
828
|
+
COMMAND_INSTRUCTION_PREFIX_REPLACEMENT
|
|
829
|
+
);
|
|
821
830
|
const sourcePath = path10.resolve(commandPath);
|
|
822
831
|
const description = extractDescriptionFromFrontmatter(normalizedCommand) || `${commandName} command imported from .claude/commands`;
|
|
823
832
|
return `---
|
|
@@ -832,39 +841,374 @@ resolve them from the current workspace first; if missing, ask for the intended
|
|
|
832
841
|
|
|
833
842
|
---
|
|
834
843
|
|
|
835
|
-
${
|
|
844
|
+
${rewrittenCommand}
|
|
836
845
|
`;
|
|
837
846
|
}
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
847
|
+
function isRecord(value) {
|
|
848
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
849
|
+
}
|
|
850
|
+
function asStringArray(value) {
|
|
851
|
+
if (!Array.isArray(value)) {
|
|
852
|
+
return void 0;
|
|
841
853
|
}
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
854
|
+
const items = value.filter(
|
|
855
|
+
(item) => typeof item === "string"
|
|
856
|
+
);
|
|
857
|
+
return items.length > 0 ? items : void 0;
|
|
858
|
+
}
|
|
859
|
+
function asStringRecord(value) {
|
|
860
|
+
if (!isRecord(value)) {
|
|
861
|
+
return void 0;
|
|
862
|
+
}
|
|
863
|
+
const entries = Object.entries(value).filter(
|
|
864
|
+
(entry) => typeof entry[1] === "string"
|
|
865
|
+
);
|
|
866
|
+
if (entries.length === 0) {
|
|
867
|
+
return void 0;
|
|
868
|
+
}
|
|
869
|
+
return Object.fromEntries(entries);
|
|
870
|
+
}
|
|
871
|
+
function normalizeClaudeMcpServer(value) {
|
|
872
|
+
if (!isRecord(value)) {
|
|
873
|
+
return void 0;
|
|
874
|
+
}
|
|
875
|
+
const type = typeof value.type === "string" ? value.type : void 0;
|
|
876
|
+
const command = typeof value.command === "string" ? value.command : void 0;
|
|
877
|
+
const args = asStringArray(value.args);
|
|
878
|
+
const env = asStringRecord(value.env);
|
|
879
|
+
const cwd = typeof value.cwd === "string" ? value.cwd : void 0;
|
|
880
|
+
const url = typeof value.url === "string" ? value.url : void 0;
|
|
881
|
+
const headers = asStringRecord(value.headers);
|
|
882
|
+
const bearerTokenEnvVar = typeof value.bearerTokenEnvVar === "string" ? value.bearerTokenEnvVar : typeof value.bearer_token_env_var === "string" ? value.bearer_token_env_var : void 0;
|
|
883
|
+
const envHttpHeaders = asStringRecord(value.envHttpHeaders) || asStringRecord(value.env_http_headers);
|
|
884
|
+
return {
|
|
885
|
+
type,
|
|
886
|
+
command,
|
|
887
|
+
args,
|
|
888
|
+
env,
|
|
889
|
+
cwd,
|
|
890
|
+
url,
|
|
891
|
+
headers,
|
|
892
|
+
bearerTokenEnvVar,
|
|
893
|
+
envHttpHeaders
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
function normalizeClaudeMcpServers(value) {
|
|
897
|
+
if (!isRecord(value)) {
|
|
898
|
+
return {};
|
|
899
|
+
}
|
|
900
|
+
const normalizedEntries = Object.entries(value).map(([name, server]) => [name, normalizeClaudeMcpServer(server)]).filter((entry) => Boolean(entry[1]));
|
|
901
|
+
return Object.fromEntries(normalizedEntries);
|
|
902
|
+
}
|
|
903
|
+
function getProjectState(claudeState, targetDir) {
|
|
904
|
+
const projects = claudeState.projects;
|
|
905
|
+
if (!isRecord(projects)) {
|
|
906
|
+
return void 0;
|
|
907
|
+
}
|
|
908
|
+
const normalizedTarget = path10.resolve(targetDir);
|
|
909
|
+
for (const [projectPath, projectState] of Object.entries(projects)) {
|
|
910
|
+
if (path10.resolve(projectPath) !== normalizedTarget) {
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (isRecord(projectState)) {
|
|
914
|
+
return projectState;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return void 0;
|
|
918
|
+
}
|
|
919
|
+
function filterMcpJsonServersByProjectState(servers, projectState) {
|
|
920
|
+
if (!projectState) {
|
|
921
|
+
return servers;
|
|
922
|
+
}
|
|
923
|
+
const enabled = asStringArray(projectState.enabledMcpjsonServers) || [];
|
|
924
|
+
const disabled = new Set(
|
|
925
|
+
asStringArray(projectState.disabledMcpjsonServers) || []
|
|
926
|
+
);
|
|
927
|
+
const enabledSet = new Set(enabled);
|
|
928
|
+
const hasEnabledFilter = enabledSet.size > 0;
|
|
929
|
+
const filteredEntries = Object.entries(servers).filter(([name]) => {
|
|
930
|
+
if (hasEnabledFilter && !enabledSet.has(name)) {
|
|
931
|
+
return false;
|
|
932
|
+
}
|
|
933
|
+
if (disabled.has(name)) {
|
|
934
|
+
return false;
|
|
935
|
+
}
|
|
936
|
+
return true;
|
|
937
|
+
});
|
|
938
|
+
return Object.fromEntries(filteredEntries);
|
|
939
|
+
}
|
|
940
|
+
async function readJsonFile(filePath) {
|
|
941
|
+
if (!await fs8.pathExists(filePath)) {
|
|
942
|
+
return void 0;
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
const raw = await fs8.readFile(filePath, "utf8");
|
|
946
|
+
const parsed = JSON.parse(raw);
|
|
947
|
+
if (!isRecord(parsed)) {
|
|
948
|
+
return void 0;
|
|
949
|
+
}
|
|
950
|
+
return parsed;
|
|
951
|
+
} catch {
|
|
952
|
+
return void 0;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function toCodexMcpServer(server) {
|
|
956
|
+
const serverType = server.type?.toLowerCase();
|
|
957
|
+
const isHttp = serverType === "http" || Boolean(server.url);
|
|
958
|
+
if (isHttp) {
|
|
959
|
+
if (!server.url) {
|
|
960
|
+
return void 0;
|
|
961
|
+
}
|
|
962
|
+
return {
|
|
963
|
+
kind: "http",
|
|
964
|
+
url: server.url,
|
|
965
|
+
bearerTokenEnvVar: server.bearerTokenEnvVar,
|
|
966
|
+
httpHeaders: server.headers,
|
|
967
|
+
envHttpHeaders: server.envHttpHeaders
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
if (!server.command) {
|
|
971
|
+
return void 0;
|
|
972
|
+
}
|
|
973
|
+
return {
|
|
974
|
+
kind: "stdio",
|
|
975
|
+
command: server.command,
|
|
976
|
+
args: server.args,
|
|
977
|
+
env: server.env,
|
|
978
|
+
cwd: server.cwd
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function tomlQuote(value) {
|
|
982
|
+
return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
983
|
+
}
|
|
984
|
+
function tomlKey(value) {
|
|
985
|
+
return /^[A-Za-z0-9_-]+$/.test(value) ? value : tomlQuote(value);
|
|
986
|
+
}
|
|
987
|
+
function parseTomlPath(input) {
|
|
988
|
+
const pathInput = input.trim();
|
|
989
|
+
if (!pathInput) {
|
|
990
|
+
return void 0;
|
|
991
|
+
}
|
|
992
|
+
const segments = [];
|
|
993
|
+
let current = "";
|
|
994
|
+
let inQuotes = false;
|
|
995
|
+
let escaped = false;
|
|
996
|
+
for (const char of pathInput) {
|
|
997
|
+
if (inQuotes) {
|
|
998
|
+
if (escaped) {
|
|
999
|
+
current += char;
|
|
1000
|
+
escaped = false;
|
|
1001
|
+
continue;
|
|
1002
|
+
}
|
|
1003
|
+
if (char === "\\") {
|
|
1004
|
+
escaped = true;
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (char === '"') {
|
|
1008
|
+
inQuotes = false;
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
current += char;
|
|
1012
|
+
continue;
|
|
1013
|
+
}
|
|
1014
|
+
if (char === '"') {
|
|
1015
|
+
inQuotes = true;
|
|
1016
|
+
continue;
|
|
1017
|
+
}
|
|
1018
|
+
if (char === ".") {
|
|
1019
|
+
const value = current.trim();
|
|
1020
|
+
if (!value) {
|
|
1021
|
+
return void 0;
|
|
1022
|
+
}
|
|
1023
|
+
segments.push(value);
|
|
1024
|
+
current = "";
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
current += char;
|
|
1028
|
+
}
|
|
1029
|
+
if (inQuotes || escaped) {
|
|
1030
|
+
return void 0;
|
|
1031
|
+
}
|
|
1032
|
+
const tail = current.trim();
|
|
1033
|
+
if (!tail) {
|
|
1034
|
+
return void 0;
|
|
1035
|
+
}
|
|
1036
|
+
segments.push(tail);
|
|
1037
|
+
return segments;
|
|
1038
|
+
}
|
|
1039
|
+
function stripCodexMcpSections(configToml) {
|
|
1040
|
+
const lines = normalizeLineEndings(configToml).split("\n");
|
|
1041
|
+
const kept = [];
|
|
1042
|
+
let droppingMcpSection = false;
|
|
1043
|
+
for (const line of lines) {
|
|
1044
|
+
const headerMatch = line.match(/^\s*\[([^\]]+)\]\s*$/);
|
|
1045
|
+
if (headerMatch) {
|
|
1046
|
+
const headerPath = parseTomlPath(headerMatch[1]);
|
|
1047
|
+
droppingMcpSection = Boolean(
|
|
1048
|
+
headerPath && headerPath.length > 0 && headerPath[0] === "mcp_servers"
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
if (!droppingMcpSection) {
|
|
1052
|
+
kept.push(line);
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return kept.join("\n").replace(/\n{3,}/g, "\n\n").trimEnd();
|
|
1056
|
+
}
|
|
1057
|
+
function renderTomlStringArray(values) {
|
|
1058
|
+
if (!values || values.length === 0) {
|
|
1059
|
+
return void 0;
|
|
1060
|
+
}
|
|
1061
|
+
return `[${values.map((value) => tomlQuote(value)).join(", ")}]`;
|
|
1062
|
+
}
|
|
1063
|
+
function renderTomlStringTable(header, values) {
|
|
1064
|
+
if (!values || Object.keys(values).length === 0) {
|
|
1065
|
+
return void 0;
|
|
1066
|
+
}
|
|
1067
|
+
const lines = Object.entries(values).sort((a, b) => a[0].localeCompare(b[0])).map(([key, value]) => `${tomlKey(key)} = ${tomlQuote(value)}`);
|
|
1068
|
+
return `${header}
|
|
1069
|
+
${lines.join("\n")}`;
|
|
1070
|
+
}
|
|
1071
|
+
function renderCodexMcpServer(name, server) {
|
|
1072
|
+
const sectionRoot = `[mcp_servers.${tomlKey(name)}]`;
|
|
1073
|
+
const lines = [sectionRoot];
|
|
1074
|
+
if (server.kind === "http") {
|
|
1075
|
+
lines.push(`url = ${tomlQuote(server.url ?? "")}`);
|
|
1076
|
+
if (server.bearerTokenEnvVar) {
|
|
1077
|
+
lines.push(
|
|
1078
|
+
`bearer_token_env_var = ${tomlQuote(server.bearerTokenEnvVar)}`
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
} else {
|
|
1082
|
+
lines.push(`command = ${tomlQuote(server.command ?? "")}`);
|
|
1083
|
+
const args = renderTomlStringArray(server.args);
|
|
1084
|
+
if (args) {
|
|
1085
|
+
lines.push(`args = ${args}`);
|
|
1086
|
+
}
|
|
1087
|
+
if (server.cwd) {
|
|
1088
|
+
lines.push(`cwd = ${tomlQuote(server.cwd)}`);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
const envSection = server.kind === "stdio" ? renderTomlStringTable(`[mcp_servers.${tomlKey(name)}.env]`, server.env) : void 0;
|
|
1092
|
+
const httpHeadersSection = server.kind === "http" ? renderTomlStringTable(
|
|
1093
|
+
`[mcp_servers.${tomlKey(name)}.http_headers]`,
|
|
1094
|
+
server.httpHeaders
|
|
1095
|
+
) : void 0;
|
|
1096
|
+
const envHttpHeadersSection = server.kind === "http" ? renderTomlStringTable(
|
|
1097
|
+
`[mcp_servers.${tomlKey(name)}.env_http_headers]`,
|
|
1098
|
+
server.envHttpHeaders
|
|
1099
|
+
) : void 0;
|
|
1100
|
+
const sections = [
|
|
1101
|
+
lines.join("\n"),
|
|
1102
|
+
envSection,
|
|
1103
|
+
httpHeadersSection,
|
|
1104
|
+
envHttpHeadersSection
|
|
1105
|
+
].filter((section) => Boolean(section));
|
|
1106
|
+
return sections.join("\n\n");
|
|
1107
|
+
}
|
|
1108
|
+
function mergeCodexConfigToml(existingToml, renderedMcpBlocks) {
|
|
1109
|
+
const base = stripCodexMcpSections(existingToml);
|
|
1110
|
+
const mcp = renderedMcpBlocks.trim();
|
|
1111
|
+
if (!base) {
|
|
1112
|
+
return mcp;
|
|
1113
|
+
}
|
|
1114
|
+
if (!mcp) {
|
|
1115
|
+
return base;
|
|
1116
|
+
}
|
|
1117
|
+
return `${base}
|
|
1118
|
+
|
|
1119
|
+
${mcp}`;
|
|
1120
|
+
}
|
|
1121
|
+
async function loadClaudeMcpServers(targetDir) {
|
|
1122
|
+
const homeDir = path10.resolve(os.homedir());
|
|
1123
|
+
const normalizedTarget = path10.resolve(targetDir);
|
|
1124
|
+
const isUserScope = normalizedTarget === homeDir;
|
|
1125
|
+
const claudeStatePath = path10.join(homeDir, CLAUDE_STATE_FILE);
|
|
1126
|
+
const claudeState = await readJsonFile(claudeStatePath);
|
|
1127
|
+
if (isUserScope) {
|
|
1128
|
+
return {
|
|
1129
|
+
scope: "global",
|
|
1130
|
+
servers: normalizeClaudeMcpServers(claudeState?.mcpServers)
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
const projectState = claudeState ? getProjectState(claudeState, normalizedTarget) : void 0;
|
|
1134
|
+
const projectServers = normalizeClaudeMcpServers(projectState?.mcpServers);
|
|
1135
|
+
const localMcpPath = path10.join(normalizedTarget, CLAUDE_LOCAL_MCP_FILE);
|
|
1136
|
+
const localMcpState = await readJsonFile(localMcpPath);
|
|
1137
|
+
const localMcpServers = filterMcpJsonServersByProjectState(
|
|
1138
|
+
normalizeClaudeMcpServers(localMcpState?.mcpServers),
|
|
1139
|
+
projectState
|
|
1140
|
+
);
|
|
1141
|
+
return {
|
|
1142
|
+
scope: "local",
|
|
1143
|
+
servers: {
|
|
1144
|
+
...localMcpServers,
|
|
1145
|
+
...projectServers
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
async function syncMcpServers(targetDir) {
|
|
1150
|
+
const homeDir = path10.resolve(os.homedir());
|
|
1151
|
+
const normalizedTarget = path10.resolve(targetDir);
|
|
1152
|
+
const isUserScope = normalizedTarget === homeDir;
|
|
1153
|
+
const codexBaseDir = isUserScope ? path10.join(homeDir, ".codex") : path10.join(normalizedTarget, ".codex");
|
|
1154
|
+
const codexMcpConfigPath = path10.join(codexBaseDir, CODEX_CONFIG_FILE);
|
|
1155
|
+
const { scope, servers } = await loadClaudeMcpServers(normalizedTarget);
|
|
1156
|
+
const codexServers = Object.entries(servers).map(([name, server]) => [name, toCodexMcpServer(server)]).filter((entry) => Boolean(entry[1])).sort((a, b) => a[0].localeCompare(b[0]));
|
|
1157
|
+
const renderedMcpBlocks = codexServers.map(([name, server]) => renderCodexMcpServer(name, server)).join("\n\n");
|
|
1158
|
+
const existingConfig = await fs8.pathExists(codexMcpConfigPath) ? await fs8.readFile(codexMcpConfigPath, "utf8") : "";
|
|
1159
|
+
const nextConfig = mergeCodexConfigToml(existingConfig, renderedMcpBlocks);
|
|
1160
|
+
await fs8.ensureDir(codexBaseDir);
|
|
1161
|
+
await fs8.writeFile(codexMcpConfigPath, `${nextConfig.trimEnd()}
|
|
1162
|
+
`);
|
|
1163
|
+
return {
|
|
1164
|
+
codexMcpConfigPath,
|
|
1165
|
+
syncedMcpServers: codexServers.length,
|
|
1166
|
+
scope
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
async function syncSkills(claudeSkillsDir, codexSkillsDir) {
|
|
1170
|
+
await fs8.ensureDir(codexSkillsDir);
|
|
1171
|
+
let sourceSkillNames = [];
|
|
1172
|
+
if (await fs8.pathExists(claudeSkillsDir)) {
|
|
1173
|
+
const sourceEntries = await fs8.readdir(claudeSkillsDir, {
|
|
1174
|
+
withFileTypes: true
|
|
1175
|
+
});
|
|
1176
|
+
sourceSkillNames = sourceEntries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).filter((name) => name !== ".system");
|
|
1177
|
+
}
|
|
1178
|
+
const sourceSkillSet = new Set(sourceSkillNames);
|
|
1179
|
+
const codexEntries = await fs8.readdir(codexSkillsDir, {
|
|
1180
|
+
withFileTypes: true
|
|
1181
|
+
});
|
|
1182
|
+
for (const entry of codexEntries) {
|
|
845
1183
|
if (!entry.isDirectory()) {
|
|
846
1184
|
continue;
|
|
847
1185
|
}
|
|
848
|
-
if (entry.name
|
|
1186
|
+
if (RESERVED_CODEX_SKILL_DIRS.has(entry.name)) {
|
|
849
1187
|
continue;
|
|
850
1188
|
}
|
|
851
|
-
|
|
852
|
-
|
|
1189
|
+
if (!sourceSkillSet.has(entry.name)) {
|
|
1190
|
+
await fs8.remove(path10.join(codexSkillsDir, entry.name));
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
for (const skillName of sourceSkillNames) {
|
|
1194
|
+
const src = path10.join(claudeSkillsDir, skillName);
|
|
1195
|
+
const dest = path10.join(codexSkillsDir, skillName);
|
|
853
1196
|
if (await fs8.pathExists(dest)) {
|
|
854
1197
|
await fs8.remove(dest);
|
|
855
1198
|
}
|
|
856
1199
|
await copyRecursive(src, dest, { files: 0, directories: 0 });
|
|
857
|
-
syncedSkills++;
|
|
858
1200
|
}
|
|
859
|
-
return
|
|
1201
|
+
return sourceSkillNames.length;
|
|
860
1202
|
}
|
|
861
1203
|
async function syncCommands(claudeCommandsDir, codexCommandsDir) {
|
|
862
1204
|
if (!await fs8.pathExists(claudeCommandsDir)) {
|
|
1205
|
+
await fs8.remove(codexCommandsDir);
|
|
863
1206
|
return 0;
|
|
864
1207
|
}
|
|
865
1208
|
const entries = await fs8.readdir(claudeCommandsDir, { withFileTypes: true });
|
|
866
1209
|
const commandFiles = entries.filter((entry) => entry.isFile() && entry.name.endsWith(".md")).map((entry) => entry.name).sort((a, b) => a.localeCompare(b));
|
|
867
1210
|
if (commandFiles.length === 0) {
|
|
1211
|
+
await fs8.remove(codexCommandsDir);
|
|
868
1212
|
return 0;
|
|
869
1213
|
}
|
|
870
1214
|
await fs8.remove(codexCommandsDir);
|
|
@@ -876,26 +1220,126 @@ async function syncCommands(claudeCommandsDir, codexCommandsDir) {
|
|
|
876
1220
|
const skillDir = path10.join(codexCommandsDir, commandName);
|
|
877
1221
|
const skillPath = path10.join(skillDir, "SKILL.md");
|
|
878
1222
|
await fs8.ensureDir(skillDir);
|
|
879
|
-
await fs8.writeFile(
|
|
1223
|
+
await fs8.writeFile(
|
|
1224
|
+
skillPath,
|
|
1225
|
+
buildCommandSkillContent(commandPath, commandRaw)
|
|
1226
|
+
);
|
|
880
1227
|
}
|
|
881
1228
|
return commandFiles.length;
|
|
882
1229
|
}
|
|
1230
|
+
async function syncInstructions(claudeInstructionsDir, codexInstructionsDir) {
|
|
1231
|
+
if (!await fs8.pathExists(claudeInstructionsDir)) {
|
|
1232
|
+
await fs8.remove(codexInstructionsDir);
|
|
1233
|
+
return 0;
|
|
1234
|
+
}
|
|
1235
|
+
await fs8.remove(codexInstructionsDir);
|
|
1236
|
+
await fs8.ensureDir(codexInstructionsDir);
|
|
1237
|
+
const counter = { files: 0, directories: 0 };
|
|
1238
|
+
await copyRecursive(claudeInstructionsDir, codexInstructionsDir, counter);
|
|
1239
|
+
return counter.files;
|
|
1240
|
+
}
|
|
1241
|
+
function extractReferenceCandidate(line) {
|
|
1242
|
+
const trimmed = line.trim();
|
|
1243
|
+
if (!trimmed.startsWith("@")) {
|
|
1244
|
+
return void 0;
|
|
1245
|
+
}
|
|
1246
|
+
const [candidate] = trimmed.slice(1).split(/\s+/);
|
|
1247
|
+
if (!candidate || candidate.startsWith("#") || candidate.includes("://")) {
|
|
1248
|
+
return void 0;
|
|
1249
|
+
}
|
|
1250
|
+
const looksLikePath = candidate.startsWith("./") || candidate.startsWith("../") || candidate.startsWith("/") || candidate.includes("/") || /\.[a-z0-9]+$/i.test(candidate);
|
|
1251
|
+
if (!looksLikePath) {
|
|
1252
|
+
return void 0;
|
|
1253
|
+
}
|
|
1254
|
+
return candidate;
|
|
1255
|
+
}
|
|
1256
|
+
async function collectSkillMarkdownFiles(rootDir, collector = []) {
|
|
1257
|
+
if (!await fs8.pathExists(rootDir)) {
|
|
1258
|
+
return collector;
|
|
1259
|
+
}
|
|
1260
|
+
const entries = await fs8.readdir(rootDir, { withFileTypes: true });
|
|
1261
|
+
for (const entry of entries) {
|
|
1262
|
+
const fullPath = path10.join(rootDir, entry.name);
|
|
1263
|
+
if (entry.isDirectory()) {
|
|
1264
|
+
await collectSkillMarkdownFiles(fullPath, collector);
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (entry.isFile() && entry.name === "SKILL.md") {
|
|
1268
|
+
collector.push(fullPath);
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
return collector;
|
|
1272
|
+
}
|
|
1273
|
+
async function validateSkillReferences(codexSkillsDir) {
|
|
1274
|
+
const skillFiles = await collectSkillMarkdownFiles(codexSkillsDir);
|
|
1275
|
+
let count = 0;
|
|
1276
|
+
const samples = [];
|
|
1277
|
+
for (const skillFile of skillFiles) {
|
|
1278
|
+
const content = await fs8.readFile(skillFile, "utf8");
|
|
1279
|
+
const lines = normalizeLineEndings(content).split("\n");
|
|
1280
|
+
for (const line of lines) {
|
|
1281
|
+
const candidate = extractReferenceCandidate(line);
|
|
1282
|
+
if (!candidate) {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const referencePath = candidate.split(/[?#]/, 1)[0];
|
|
1286
|
+
if (!referencePath) {
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
const resolvedPath = path10.resolve(path10.dirname(skillFile), referencePath);
|
|
1290
|
+
if (await fs8.pathExists(resolvedPath)) {
|
|
1291
|
+
continue;
|
|
1292
|
+
}
|
|
1293
|
+
count += 1;
|
|
1294
|
+
if (samples.length < MAX_REFERENCE_ISSUE_SAMPLES) {
|
|
1295
|
+
samples.push({
|
|
1296
|
+
skillPath: skillFile,
|
|
1297
|
+
reference: candidate,
|
|
1298
|
+
resolvedPath
|
|
1299
|
+
});
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return { count, samples };
|
|
1304
|
+
}
|
|
883
1305
|
async function syncWithCodex(targetDir) {
|
|
884
|
-
const codexHome =
|
|
1306
|
+
const codexHome = path10.resolve(targetDir, ".codex");
|
|
885
1307
|
const codexSkillsDir = path10.join(codexHome, "skills");
|
|
886
1308
|
const codexCommandsDir = path10.join(codexSkillsDir, "claude-commands");
|
|
1309
|
+
const codexInstructionsDir = path10.join(codexHome, "instructions");
|
|
887
1310
|
await fs8.ensureDir(codexSkillsDir);
|
|
888
1311
|
const claudeRootDir = path10.join(targetDir, ".claude");
|
|
889
1312
|
const claudeSkillsDir = path10.join(claudeRootDir, "skills");
|
|
890
1313
|
const claudeCommandsDir = path10.join(claudeRootDir, "commands");
|
|
1314
|
+
const claudeInstructionsDir = path10.join(claudeRootDir, "instructions");
|
|
891
1315
|
const syncedSkills = await syncSkills(claudeSkillsDir, codexSkillsDir);
|
|
892
|
-
const
|
|
1316
|
+
const syncedInstructions = await syncInstructions(
|
|
1317
|
+
claudeInstructionsDir,
|
|
1318
|
+
codexInstructionsDir
|
|
1319
|
+
);
|
|
1320
|
+
const syncedCommands = await syncCommands(
|
|
1321
|
+
claudeCommandsDir,
|
|
1322
|
+
codexCommandsDir
|
|
1323
|
+
);
|
|
1324
|
+
const {
|
|
1325
|
+
codexMcpConfigPath,
|
|
1326
|
+
syncedMcpServers,
|
|
1327
|
+
scope: mcpScope
|
|
1328
|
+
} = await syncMcpServers(targetDir);
|
|
1329
|
+
const { count: referenceIssueCount, samples: referenceIssueSamples } = await validateSkillReferences(codexSkillsDir);
|
|
893
1330
|
return {
|
|
894
1331
|
codexHome,
|
|
895
1332
|
codexSkillsDir,
|
|
896
1333
|
codexCommandsDir,
|
|
1334
|
+
codexInstructionsDir,
|
|
897
1335
|
syncedSkills,
|
|
898
|
-
syncedCommands
|
|
1336
|
+
syncedCommands,
|
|
1337
|
+
syncedInstructions,
|
|
1338
|
+
codexMcpConfigPath,
|
|
1339
|
+
syncedMcpServers,
|
|
1340
|
+
mcpScope,
|
|
1341
|
+
referenceIssueCount,
|
|
1342
|
+
referenceIssueSamples
|
|
899
1343
|
};
|
|
900
1344
|
}
|
|
901
1345
|
|
|
@@ -1095,54 +1539,76 @@ var init = async (options) => {
|
|
|
1095
1539
|
hasScripts,
|
|
1096
1540
|
scope
|
|
1097
1541
|
);
|
|
1098
|
-
|
|
1542
|
+
const codexSyncPath = path11.join(targetDir, ".codex");
|
|
1543
|
+
const { syncCodex } = await promptCodexSync({
|
|
1544
|
+
providedSyncCodex: options.syncCodex,
|
|
1545
|
+
codexPath: codexSyncPath
|
|
1546
|
+
});
|
|
1547
|
+
if (syncCodex) {
|
|
1548
|
+
logger.blank();
|
|
1549
|
+
logger.info("Syncing .claude skills/commands/instructions/MCP to Codex...");
|
|
1099
1550
|
try {
|
|
1100
|
-
await
|
|
1551
|
+
const result = await syncWithCodex(targetDir);
|
|
1552
|
+
if (result.syncedSkills > 0) {
|
|
1553
|
+
logger.step(`Skills synced: ${result.syncedSkills}`);
|
|
1554
|
+
}
|
|
1555
|
+
if (result.syncedCommands > 0) {
|
|
1556
|
+
logger.step(`Commands synced: ${result.syncedCommands}`);
|
|
1557
|
+
}
|
|
1558
|
+
if (result.syncedInstructions > 0) {
|
|
1559
|
+
logger.step(`Instructions synced: ${result.syncedInstructions}`);
|
|
1560
|
+
}
|
|
1561
|
+
if (result.syncedMcpServers > 0) {
|
|
1562
|
+
logger.step(
|
|
1563
|
+
`MCP servers synced (${result.mcpScope}): ${result.syncedMcpServers}`
|
|
1564
|
+
);
|
|
1565
|
+
}
|
|
1566
|
+
if (result.syncedSkills === 0 && result.syncedCommands === 0 && result.syncedInstructions === 0 && result.syncedMcpServers === 0) {
|
|
1567
|
+
logger.warn(
|
|
1568
|
+
"Nothing was synced. .claude/skills, .claude/commands, .claude/instructions, and Claude MCP settings were not found."
|
|
1569
|
+
);
|
|
1570
|
+
} else {
|
|
1571
|
+
logger.success(`Codex sync complete: ${result.codexSkillsDir}`);
|
|
1572
|
+
logger.step(`Codex MCP config: ${result.codexMcpConfigPath}`);
|
|
1573
|
+
if (result.referenceIssueCount > 0) {
|
|
1574
|
+
logger.warn(
|
|
1575
|
+
`Codex skill reference issues found: ${result.referenceIssueCount} (showing up to ${result.referenceIssueSamples.length})`
|
|
1576
|
+
);
|
|
1577
|
+
for (const issue of result.referenceIssueSamples) {
|
|
1578
|
+
const skillPath = path11.relative(targetDir, issue.skillPath);
|
|
1579
|
+
const resolvedPath = path11.relative(targetDir, issue.resolvedPath);
|
|
1580
|
+
logger.step(
|
|
1581
|
+
`${skillPath} -> @${issue.reference} (missing: ${resolvedPath})`
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1101
1586
|
} catch (error) {
|
|
1102
1587
|
logger.warn(
|
|
1103
|
-
`
|
|
1588
|
+
`Codex sync failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1104
1589
|
);
|
|
1105
1590
|
}
|
|
1106
1591
|
}
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
return;
|
|
1112
|
-
}
|
|
1113
|
-
logger.blank();
|
|
1114
|
-
logger.info("Syncing .claude skills/commands to Codex...");
|
|
1115
|
-
try {
|
|
1116
|
-
const result = await syncWithCodex(targetDir);
|
|
1117
|
-
if (result.syncedSkills > 0) {
|
|
1118
|
-
logger.step(`Skills synced: ${result.syncedSkills}`);
|
|
1119
|
-
}
|
|
1120
|
-
if (result.syncedCommands > 0) {
|
|
1121
|
-
logger.step(`Commands synced: ${result.syncedCommands}`);
|
|
1122
|
-
}
|
|
1123
|
-
if (result.syncedSkills === 0 && result.syncedCommands === 0) {
|
|
1592
|
+
if (!isUserScope) {
|
|
1593
|
+
try {
|
|
1594
|
+
await updateGitignore(targetDir, { includeCodex: syncCodex });
|
|
1595
|
+
} catch (error) {
|
|
1124
1596
|
logger.warn(
|
|
1125
|
-
|
|
1597
|
+
`Failed to update .gitignore: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1126
1598
|
);
|
|
1127
|
-
} else {
|
|
1128
|
-
logger.success(`Codex sync complete: ${result.codexSkillsDir}`);
|
|
1129
1599
|
}
|
|
1130
|
-
} catch (error) {
|
|
1131
|
-
logger.warn(
|
|
1132
|
-
`Codex sync failed: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
1133
|
-
);
|
|
1134
1600
|
}
|
|
1135
1601
|
};
|
|
1136
1602
|
|
|
1137
1603
|
// src/index.ts
|
|
1138
1604
|
var program = new Command();
|
|
1139
|
-
program.name("claude-code").description("Claude Code documentation installer for projects").version("0.7.
|
|
1605
|
+
program.name("claude-code").description("Claude Code documentation installer for projects").version("0.7.10");
|
|
1140
1606
|
program.option(
|
|
1141
1607
|
"-t, --template <names>",
|
|
1142
1608
|
"template names (comma-separated: tanstack-start,hono)"
|
|
1143
1609
|
).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(
|
|
1144
1610
|
"--sync-codex",
|
|
1145
|
-
"sync installed .claude skills/commands to
|
|
1611
|
+
"sync installed .claude skills/commands/instructions and Claude MCP settings to selected-scope .codex/"
|
|
1146
1612
|
).option("--scope <scope>", "installation scope (project|user)").action(async (options) => {
|
|
1147
1613
|
banner();
|
|
1148
1614
|
if (options.list) {
|