holycodex 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +82 -0
- package/README.md +107 -0
- package/THIRD-PARTY-NOTICES.md +7 -0
- package/bin/holycodex.js +2 -0
- package/marketplace.json +12 -0
- package/package.json +59 -0
- package/plugin/.codex-plugin/plugin.json +29 -0
- package/plugin/.mcp.json +8 -0
- package/plugin/agents/explorer.toml +6 -0
- package/plugin/agents/librarian.toml +6 -0
- package/plugin/agents/worker.toml +6 -0
- package/plugin/hooks/hooks.json +93 -0
- package/plugin/runtime/bootstrap.js +23 -0
- package/plugin/runtime/cli.js +233 -0
- package/plugin/runtime/git-bash.js +359 -0
- package/plugin/runtime/lsp.js +3674 -0
- package/plugin/runtime/rules.js +146 -0
- package/plugin/runtime/src-tRIOClWZ.js +209 -0
- package/plugin/skills/ast-grep/LICENSE +21 -0
- package/plugin/skills/ast-grep/SKILL.md +20 -0
- package/plugin/skills/ast-grep/references/cli.md +3 -0
- package/plugin/skills/ast-grep/references/install.md +3 -0
- package/plugin/skills/ast-grep/references/patterns.md +3 -0
- package/plugin/skills/ast-grep/references/pitfalls.md +3 -0
- package/plugin/skills/ast-grep/references/recipes.md +9 -0
- package/plugin/skills/ast-grep/references/sgconfig.md +3 -0
- package/plugin/skills/ast-grep/references/yaml-rules.md +3 -0
- package/plugin/skills/caveman/LICENSE +21 -0
- package/plugin/skills/caveman/SKILL.md +84 -0
- package/plugin/skills/comment-checker/SKILL.md +16 -0
- package/plugin/skills/compress/SKILL.md +26 -0
- package/plugin/skills/debugging/SKILL.md +23 -0
- package/plugin/skills/debugging/references/runtimes/README.md +9 -0
- package/plugin/skills/debugging/references/tools/README.md +8 -0
- package/plugin/skills/define-goal/LICENSE.txt +201 -0
- package/plugin/skills/define-goal/SKILL.md +36 -0
- package/plugin/skills/frontend/ATTRIBUTION.md +218 -0
- package/plugin/skills/frontend/LICENSE-Apache-2.0.txt +201 -0
- package/plugin/skills/frontend/SKILL.md +42 -0
- package/plugin/skills/frontend/references/perfection/README.md +5 -0
- package/plugin/skills/frontend/references/perfection/react-perf-tooling.md +3 -0
- package/plugin/skills/frontend/references/ui-ux-db/README.md +5 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/charts.csv +26 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/colors.csv +162 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/icons.csv +106 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/landing.csv +35 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/products.csv +162 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/react-performance.csv +45 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/astro.csv +54 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/flutter.csv +53 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/html-tailwind.csv +56 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/jetpack-compose.csv +53 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/nextjs.csv +53 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/nuxt-ui.csv +51 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/nuxtjs.csv +59 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/react-native.csv +52 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/react.csv +54 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/shadcn.csv +61 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/svelte.csv +54 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/swiftui.csv +51 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/stacks/vue.csv +50 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/styles.csv +85 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/typography.csv +74 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/ui-reasoning.csv +162 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/ux-guidelines.csv +100 -0
- package/plugin/skills/frontend/references/ui-ux-db/data/web-interface.csv +31 -0
- package/plugin/skills/frontend/references/ui-ux-db/scripts/core.py +262 -0
- package/plugin/skills/frontend/references/ui-ux-db/scripts/design_system.py +1148 -0
- package/plugin/skills/frontend/references/ui-ux-db/scripts/search.py +114 -0
- package/plugin/skills/handoff/SKILL.md +10 -0
- package/plugin/skills/lsp/SKILL.md +35 -0
- package/plugin/skills/lsp-setup/SKILL.md +18 -0
- package/plugin/skills/lsp-setup/scripts/detect-lsp.ts +233 -0
- package/plugin/skills/lsp-setup/scripts/lsp-server-table.ts +188 -0
- package/plugin/skills/lsp-setup/scripts/tsconfig.json +17 -0
- package/plugin/skills/lsp-setup/scripts/verify-lsp.ts +166 -0
- package/plugin/skills/plan/SKILL.md +14 -0
- package/plugin/skills/plan-review/SKILL.md +12 -0
- package/plugin/skills/programming/SKILL.md +35 -0
- package/plugin/skills/programming/references/code-smells.md +3 -0
- package/plugin/skills/programming/references/go/README.md +3 -0
- package/plugin/skills/programming/references/logging.md +3 -0
- package/plugin/skills/programming/references/python/README.md +3 -0
- package/plugin/skills/programming/references/rust/README.md +3 -0
- package/plugin/skills/programming/references/rust-ub/README.md +3 -0
- package/plugin/skills/programming/references/typescript/README.md +3 -0
- package/plugin/skills/refactor/SKILL.md +21 -0
- package/plugin/skills/remove-ai-slops/SKILL.md +20 -0
- package/plugin/skills/rules/SKILL.md +16 -0
- package/plugin/skills/security-research/SKILL.md +38 -0
- package/plugin/skills/tdd/SKILL.md +24 -0
- package/plugin/skills/tdd/mocking.md +60 -0
- package/plugin/skills/tdd/tests.md +77 -0
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import { copyFile, cp, lstat, mkdir, readFile, readdir, readlink, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import process$1 from "node:process";
|
|
4
|
+
import { homedir, tmpdir } from "node:os";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
//#region src/config.ts
|
|
7
|
+
var START = "# >>> holycodex managed >>>";
|
|
8
|
+
var END = "# <<< holycodex managed <<<";
|
|
9
|
+
var OLD_NAMESPACES = [
|
|
10
|
+
"marketplaces.sisyphuslabs",
|
|
11
|
+
"plugins.\"omo@sisyphuslabs\"",
|
|
12
|
+
"marketplaces.lazycodex",
|
|
13
|
+
"plugins.\"omo@lazycodex\"",
|
|
14
|
+
"marketplaces.code-yeongyu-codex-plugins",
|
|
15
|
+
"plugins.\"omo@code-yeongyu-codex-plugins\"",
|
|
16
|
+
"agents.plan",
|
|
17
|
+
"agents.metis",
|
|
18
|
+
"agents.momus",
|
|
19
|
+
"agents.oracle",
|
|
20
|
+
"agents.sisyphus",
|
|
21
|
+
"agents.prometheus",
|
|
22
|
+
"agents.atlas",
|
|
23
|
+
"agents.hephaestus",
|
|
24
|
+
"hooks.state.\"omo@sisyphuslabs",
|
|
25
|
+
"hooks.state.\"omo@lazycodex",
|
|
26
|
+
"hooks.state.\"omo@code-yeongyu-codex-plugins"
|
|
27
|
+
];
|
|
28
|
+
function removeManaged(input) {
|
|
29
|
+
const escapedStart = START.replaceAll(">", "\\>");
|
|
30
|
+
const escapedEnd = END.replaceAll("<", "\\<");
|
|
31
|
+
return input.replace(new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}\\r?\\n?`, "g"), "").trim();
|
|
32
|
+
}
|
|
33
|
+
function removeLegacyOmo(input) {
|
|
34
|
+
return input.split(/(?=^\s*\[)/m).filter((section) => {
|
|
35
|
+
const header = /^\s*\[([^\]]+)]/.exec(section)?.[1];
|
|
36
|
+
if (header === void 0) return true;
|
|
37
|
+
if (OLD_NAMESPACES.some((name) => header === name || header.startsWith(`${name}.`) || name.includes("\"omo@") && header.startsWith(name))) return false;
|
|
38
|
+
return ![
|
|
39
|
+
"agents.explorer",
|
|
40
|
+
"agents.librarian",
|
|
41
|
+
"agents.worker"
|
|
42
|
+
].some((name) => header === name || header.startsWith(`${name}.`)) || !/(?:sisyphuslabs|omo@|oh-my|code-yeongyu)/i.test(section);
|
|
43
|
+
}).join("").trimEnd();
|
|
44
|
+
}
|
|
45
|
+
function rewriteForbiddenReasoning(input) {
|
|
46
|
+
return input.split(/(?=^\s*\[)/m).map((section) => {
|
|
47
|
+
const model = /^\s*model\s*=\s*"([^"]+)"/im.exec(section)?.[1]?.toLowerCase();
|
|
48
|
+
if (model === void 0 || !/(?:sol|terra|luna)/.test(model)) return section;
|
|
49
|
+
const allowsHigh = model.includes("luna");
|
|
50
|
+
const rewritten = section.replace(/^(\s*model_reasoning_effort\s*=\s*)"([^"]+)"/gim, (_match, prefix, effort) => {
|
|
51
|
+
const normalized = effort.toLowerCase();
|
|
52
|
+
if (normalized === "low" || normalized === "medium" || allowsHigh && normalized === "high") return `${prefix}"${normalized}"`;
|
|
53
|
+
return `${prefix}"${normalized === "high" ? "medium" : "low"}"`;
|
|
54
|
+
});
|
|
55
|
+
return /^\s*model_reasoning_effort\s*=/im.test(rewritten) ? rewritten : rewritten.replace(/^(\s*model\s*=\s*"[^"]+")/im, "$1\nmodel_reasoning_effort = \"low\"");
|
|
56
|
+
}).join("");
|
|
57
|
+
}
|
|
58
|
+
function installConfig(input, autonomous) {
|
|
59
|
+
const base = rewriteForbiddenReasoning(removeLegacyOmo(removeManaged(input)));
|
|
60
|
+
const firstTable = base.search(/^\s*\[/m);
|
|
61
|
+
const rootSection = firstTable < 0 ? base : base.slice(0, firstTable);
|
|
62
|
+
const tables = firstTable < 0 ? "" : base.slice(firstTable);
|
|
63
|
+
const preservedRoot = rootSection.replace(/^\s*max_concurrent_threads_per_session\s*=.*\r?\n?/gm, "").replace(autonomous ? /^\s*(?:approval_policy|sandbox_mode)\s*=.*\r?\n?/gm : /$^/g, "").trimEnd();
|
|
64
|
+
const rootBlock = `${START}\n${/^\s*model\s*=/m.test(preservedRoot) ? "" : "model = \"gpt-5.6-sol\"\nmodel_reasoning_effort = \"low\"\n"}${autonomous ? "approval_policy = \"never\"\nsandbox_mode = \"danger-full-access\"\n" : ""}max_concurrent_threads_per_session = 2\n${END}`;
|
|
65
|
+
const agents = [
|
|
66
|
+
"explorer",
|
|
67
|
+
"librarian",
|
|
68
|
+
"worker"
|
|
69
|
+
].filter((name) => !new RegExp(`^\\s*\\[agents\\.${name}]`, "m").test(base)).map((name) => `[agents.${name}]\nconfig_file = "holycodex/agents/${name}.toml"`).join("\n\n");
|
|
70
|
+
const pluginBlock = `${START}\n[marketplaces.holycodex]\nsource = "https://github.com/davidbasilefilho/holycodex.git"\n\n[plugins."holycodex@holycodex"]\nenabled = true${agents.length > 0 ? `\n\n${agents}` : ""}\n${END}`;
|
|
71
|
+
return `${rootBlock}${preservedRoot.length > 0 ? `\n${preservedRoot}` : ""}${tables.length > 0 ? `\n\n${tables}` : ""}\n\n${pluginBlock}\n`;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/files.ts
|
|
75
|
+
async function exists(path) {
|
|
76
|
+
try {
|
|
77
|
+
await stat(path);
|
|
78
|
+
return true;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function backup(path, root) {
|
|
85
|
+
if (!await exists(path)) return void 0;
|
|
86
|
+
const target = join(root, path.replace(/^([A-Za-z]:)?[\\/]+/, "").replaceAll(":", ""));
|
|
87
|
+
await mkdir(dirname(target), { recursive: true });
|
|
88
|
+
await copyBackup(path, target);
|
|
89
|
+
return target;
|
|
90
|
+
}
|
|
91
|
+
async function copyBackup(source, target) {
|
|
92
|
+
const metadata = await lstat(source);
|
|
93
|
+
if (metadata.isSymbolicLink()) {
|
|
94
|
+
await writeFile(`${target}.symlink`, await readlink(source), "utf8");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!metadata.isDirectory()) {
|
|
98
|
+
await copyFile(source, target);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
await mkdir(target, { recursive: true });
|
|
102
|
+
for (const entry of await readdir(source)) await copyBackup(join(source, entry), join(target, entry));
|
|
103
|
+
}
|
|
104
|
+
async function atomicWrite(path, content) {
|
|
105
|
+
await mkdir(dirname(path), { recursive: true });
|
|
106
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
107
|
+
await writeFile(temporary, content, "utf8");
|
|
108
|
+
await rename(temporary, path);
|
|
109
|
+
}
|
|
110
|
+
async function readText(path) {
|
|
111
|
+
return await exists(path) ? readFile(path, "utf8") : "";
|
|
112
|
+
}
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/install.ts
|
|
115
|
+
var moduleDirectory = dirname(fileURLToPath(import.meta.url));
|
|
116
|
+
var packageRoot = join(moduleDirectory, basename(moduleDirectory) === "runtime" ? "../.." : "..");
|
|
117
|
+
function paths(home = process.env.CODEX_HOME ?? join(homedir(), ".codex")) {
|
|
118
|
+
const cacheRoot = join(home, "plugins", "cache", "holycodex", "holycodex");
|
|
119
|
+
return {
|
|
120
|
+
home,
|
|
121
|
+
config: join(home, "config.toml"),
|
|
122
|
+
cacheRoot,
|
|
123
|
+
cache: join(cacheRoot, "0.2.1"),
|
|
124
|
+
agents: join(home, "holycodex", "agents"),
|
|
125
|
+
legacy: [
|
|
126
|
+
join(home, "plugins", "cache", "sisyphuslabs", "omo"),
|
|
127
|
+
join(home, "plugins", "cache", "lazycodex", "omo"),
|
|
128
|
+
join(home, "plugins", "cache", "code-yeongyu-codex-plugins", "omo")
|
|
129
|
+
]
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function backupRoot() {
|
|
133
|
+
return join(tmpdir(), "holycodex-backups", (/* @__PURE__ */ new Date()).toISOString().replaceAll(":", "-"));
|
|
134
|
+
}
|
|
135
|
+
async function install(options) {
|
|
136
|
+
const target = paths();
|
|
137
|
+
const root = backupRoot();
|
|
138
|
+
const backups = [
|
|
139
|
+
await backup(target.config, root),
|
|
140
|
+
await backup(target.cacheRoot, root),
|
|
141
|
+
await backup(target.agents, root),
|
|
142
|
+
...await Promise.all(target.legacy.map((path) => backup(path, root)))
|
|
143
|
+
].filter((path) => path !== void 0);
|
|
144
|
+
const config = installConfig(await readText(target.config), options.autonomous);
|
|
145
|
+
await atomicWrite(target.config, config);
|
|
146
|
+
await rm(target.cacheRoot, {
|
|
147
|
+
recursive: true,
|
|
148
|
+
force: true
|
|
149
|
+
});
|
|
150
|
+
await mkdir(dirname(target.cache), { recursive: true });
|
|
151
|
+
await cp(join(packageRoot, "plugin"), target.cache, { recursive: true });
|
|
152
|
+
await rm(target.agents, {
|
|
153
|
+
recursive: true,
|
|
154
|
+
force: true
|
|
155
|
+
});
|
|
156
|
+
await cp(join(packageRoot, "plugin", "agents"), target.agents, { recursive: true });
|
|
157
|
+
const removedLegacy = [];
|
|
158
|
+
for (const path of target.legacy) {
|
|
159
|
+
if (!await exists(path)) continue;
|
|
160
|
+
await rm(path, { recursive: true });
|
|
161
|
+
removedLegacy.push(path);
|
|
162
|
+
}
|
|
163
|
+
return {
|
|
164
|
+
action: "install",
|
|
165
|
+
changed: [
|
|
166
|
+
target.config,
|
|
167
|
+
target.cache,
|
|
168
|
+
target.agents,
|
|
169
|
+
...removedLegacy
|
|
170
|
+
],
|
|
171
|
+
backups
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
async function cleanup(_options) {
|
|
175
|
+
const target = paths();
|
|
176
|
+
const root = backupRoot();
|
|
177
|
+
const backups = [
|
|
178
|
+
await backup(target.config, root),
|
|
179
|
+
await backup(target.cacheRoot, root),
|
|
180
|
+
await backup(target.agents, root)
|
|
181
|
+
].filter((path) => path !== void 0);
|
|
182
|
+
const changed = [];
|
|
183
|
+
if (await exists(target.config)) {
|
|
184
|
+
const current = await readText(target.config);
|
|
185
|
+
const unmanaged = removeManaged(current);
|
|
186
|
+
const cleaned = `${unmanaged}\n`;
|
|
187
|
+
if (unmanaged.length === 0 && current.includes("# >>> holycodex managed >>>")) {
|
|
188
|
+
await rm(target.config);
|
|
189
|
+
changed.push(target.config);
|
|
190
|
+
} else if (cleaned !== current) {
|
|
191
|
+
await atomicWrite(target.config, cleaned);
|
|
192
|
+
changed.push(target.config);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (await exists(target.cacheRoot)) {
|
|
196
|
+
await rm(target.cacheRoot, { recursive: true });
|
|
197
|
+
changed.push(target.cacheRoot);
|
|
198
|
+
}
|
|
199
|
+
if (await exists(target.agents)) {
|
|
200
|
+
await rm(target.agents, { recursive: true });
|
|
201
|
+
changed.push(target.agents);
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
action: "cleanup",
|
|
205
|
+
changed,
|
|
206
|
+
backups
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
//#endregion
|
|
210
|
+
//#region src/cli.ts
|
|
211
|
+
var VERSION = "0.2.1";
|
|
212
|
+
var HELP = `HolyCodex ${VERSION}\n\nUsage: holycodex <install|cleanup> [options]\n\nOptions:\n --help Show help\n --version Show version\n --no-tui Accepted; commands are noninteractive\n --codex-autonomous Set autonomous Codex permissions\n --json Print machine-readable result\n`;
|
|
213
|
+
async function main() {
|
|
214
|
+
const args = process$1.argv.slice(2);
|
|
215
|
+
if (args.includes("--help") || args.length === 0) {
|
|
216
|
+
process$1.stdout.write(HELP);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (args.includes("--version")) {
|
|
220
|
+
process$1.stdout.write(`${VERSION}\n`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const command = args.find((arg) => !arg.startsWith("--"));
|
|
224
|
+
const options = {
|
|
225
|
+
autonomous: args.includes("--codex-autonomous"),
|
|
226
|
+
json: args.includes("--json")
|
|
227
|
+
};
|
|
228
|
+
const result = command === "install" ? await install(options) : command === "cleanup" ? await cleanup(options) : void 0;
|
|
229
|
+
if (result === void 0) throw new Error(`Unknown command: ${command ?? ""}`);
|
|
230
|
+
process$1.stdout.write(options.json ? `${JSON.stringify(result)}\n` : `HolyCodex ${result.action} complete.\n`);
|
|
231
|
+
}
|
|
232
|
+
await main();
|
|
233
|
+
//#endregion
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { a as successResponse, n as errorResponse, o as isPlainRecord, r as jsonRpcId, t as runJsonRpcStdioServer } from "./src-tRIOClWZ.js";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { argv, stderr } from "node:process";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
7
|
+
import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
//#region packages/git-bash-mcp/src/git-bash-resolver.ts
|
|
9
|
+
var GIT_BASH_ENV_KEY = "HOLYCODEX_GIT_BASH_PATH";
|
|
10
|
+
var PROGRAM_FILES = "C:\\Program Files\\Git\\bin\\bash.exe";
|
|
11
|
+
var PROGRAM_FILES_X86 = "C:\\Program Files (x86)\\Git\\bin\\bash.exe";
|
|
12
|
+
var INVALID_LAUNCHERS = ["\\windows\\system32\\", "\\microsoft\\windowsapps\\"];
|
|
13
|
+
function resolveGitBash(input) {
|
|
14
|
+
if (input.platform !== "win32") return {
|
|
15
|
+
found: true,
|
|
16
|
+
path: null,
|
|
17
|
+
source: "not-required",
|
|
18
|
+
checkedPaths: []
|
|
19
|
+
};
|
|
20
|
+
const checkedPaths = [];
|
|
21
|
+
const configured = input.env[GIT_BASH_ENV_KEY]?.trim();
|
|
22
|
+
if (configured) {
|
|
23
|
+
checkedPaths.push(configured);
|
|
24
|
+
return isBash(configured) && input.exists(configured) ? {
|
|
25
|
+
found: true,
|
|
26
|
+
path: configured,
|
|
27
|
+
source: "env",
|
|
28
|
+
checkedPaths
|
|
29
|
+
} : missing(checkedPaths);
|
|
30
|
+
}
|
|
31
|
+
for (const candidate of [{
|
|
32
|
+
path: PROGRAM_FILES,
|
|
33
|
+
source: "program-files"
|
|
34
|
+
}, {
|
|
35
|
+
path: PROGRAM_FILES_X86,
|
|
36
|
+
source: "program-files-x86"
|
|
37
|
+
}]) {
|
|
38
|
+
checkedPaths.push(candidate.path);
|
|
39
|
+
if (input.exists(candidate.path)) return {
|
|
40
|
+
found: true,
|
|
41
|
+
path: candidate.path,
|
|
42
|
+
source: candidate.source,
|
|
43
|
+
checkedPaths
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
for (const raw of input.where("bash")) {
|
|
47
|
+
const candidate = raw.trim();
|
|
48
|
+
if (!candidate) continue;
|
|
49
|
+
checkedPaths.push(candidate);
|
|
50
|
+
const normalized = candidate.replaceAll("/", "\\").toLowerCase();
|
|
51
|
+
if (INVALID_LAUNCHERS.some((part) => normalized.includes(part))) continue;
|
|
52
|
+
if (isBash(candidate) && input.exists(candidate)) return {
|
|
53
|
+
found: true,
|
|
54
|
+
path: candidate,
|
|
55
|
+
source: "path",
|
|
56
|
+
checkedPaths
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
return missing(checkedPaths);
|
|
60
|
+
}
|
|
61
|
+
function resolveGitBashForCurrentProcess(input = {}) {
|
|
62
|
+
return resolveGitBash({
|
|
63
|
+
platform: input.platform ?? process.platform,
|
|
64
|
+
env: input.env ?? process.env,
|
|
65
|
+
exists: existsSync,
|
|
66
|
+
where: (command) => {
|
|
67
|
+
try {
|
|
68
|
+
return execFileSync("where", [command], { encoding: "utf8" }).split(/\r?\n/).filter(Boolean);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if (error instanceof Error) return [];
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function isBash(path) {
|
|
77
|
+
return path.toLowerCase().endsWith("bash.exe");
|
|
78
|
+
}
|
|
79
|
+
function missing(checkedPaths) {
|
|
80
|
+
return {
|
|
81
|
+
found: false,
|
|
82
|
+
checkedPaths,
|
|
83
|
+
installHint: `Git Bash required. Install: winget install --id Git.Git -e --source winget\nCustom path: set ${GIT_BASH_ENV_KEY}=C:\\path\\to\\bash.exe`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region packages/git-bash-mcp/src/runner.ts
|
|
88
|
+
async function runGitBashCommand(input) {
|
|
89
|
+
return await new Promise((resolve, reject) => {
|
|
90
|
+
const outputDirectory = mkdtempSync(join(tmpdir(), "holycodex-git-bash-run-"));
|
|
91
|
+
const stdoutPath = join(outputDirectory, "stdout");
|
|
92
|
+
const stderrPath = join(outputDirectory, "stderr");
|
|
93
|
+
const stdoutFd = openSync(stdoutPath, "w+");
|
|
94
|
+
const stderrFd = openSync(stderrPath, "w+");
|
|
95
|
+
let outputClosed = false;
|
|
96
|
+
function closeOutputFiles() {
|
|
97
|
+
if (outputClosed) return;
|
|
98
|
+
closeSync(stdoutFd);
|
|
99
|
+
closeSync(stderrFd);
|
|
100
|
+
outputClosed = true;
|
|
101
|
+
}
|
|
102
|
+
function readAndRemoveOutput() {
|
|
103
|
+
closeOutputFiles();
|
|
104
|
+
const stdout = readFileSync(stdoutPath, "utf8");
|
|
105
|
+
const stderr = readFileSync(stderrPath, "utf8");
|
|
106
|
+
rmSync(outputDirectory, {
|
|
107
|
+
recursive: true,
|
|
108
|
+
force: true
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
stdout,
|
|
112
|
+
stderr
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const child = spawn(input.bashPath, ["-lc", input.command], {
|
|
116
|
+
cwd: input.cwd,
|
|
117
|
+
env: input.env,
|
|
118
|
+
windowsHide: true,
|
|
119
|
+
stdio: [
|
|
120
|
+
"ignore",
|
|
121
|
+
stdoutFd,
|
|
122
|
+
stderrFd
|
|
123
|
+
]
|
|
124
|
+
});
|
|
125
|
+
let timedOut = false;
|
|
126
|
+
const timeout = setTimeout(() => {
|
|
127
|
+
timedOut = true;
|
|
128
|
+
child.kill();
|
|
129
|
+
}, input.timeoutMs);
|
|
130
|
+
timeout.unref();
|
|
131
|
+
child.on("error", (error) => {
|
|
132
|
+
clearTimeout(timeout);
|
|
133
|
+
closeOutputFiles();
|
|
134
|
+
rmSync(outputDirectory, {
|
|
135
|
+
recursive: true,
|
|
136
|
+
force: true
|
|
137
|
+
});
|
|
138
|
+
reject(error);
|
|
139
|
+
});
|
|
140
|
+
child.on("close", (exitCode) => {
|
|
141
|
+
clearTimeout(timeout);
|
|
142
|
+
resolve({
|
|
143
|
+
exitCode,
|
|
144
|
+
...readAndRemoveOutput(),
|
|
145
|
+
timedOut
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
//#endregion
|
|
151
|
+
//#region packages/git-bash-mcp/src/mcp.ts
|
|
152
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
153
|
+
var MAX_TIMEOUT_MS = 30 * 6e4;
|
|
154
|
+
var EXEC_COMMAND_TIMEOUT_ENV_KEYS = [
|
|
155
|
+
"HOLYCODEX_GIT_BASH_TIMEOUT_MS",
|
|
156
|
+
"HOLYCODEX_EXEC_COMMAND_TIMEOUT_MS",
|
|
157
|
+
"CODEX_EXEC_COMMAND_TIMEOUT_MS",
|
|
158
|
+
"EXEC_COMMAND_TIMEOUT_MS"
|
|
159
|
+
];
|
|
160
|
+
async function handleGitBashMcpRequest(input, options = {}) {
|
|
161
|
+
if (!isPlainRecord(input)) return errorResponse(null, -32600, "Invalid Request");
|
|
162
|
+
const id = jsonRpcId(input["id"]);
|
|
163
|
+
const method = typeof input["method"] === "string" ? input["method"] : null;
|
|
164
|
+
if (method === "initialize") return successResponse(id, {
|
|
165
|
+
capabilities: { tools: { listChanged: false } },
|
|
166
|
+
serverInfo: {
|
|
167
|
+
name: "git_bash",
|
|
168
|
+
version: "0.2.1"
|
|
169
|
+
},
|
|
170
|
+
protocolVersion: protocolVersionFromInput(input) ?? "2024-11-05"
|
|
171
|
+
});
|
|
172
|
+
if (method === "tools/list") return successResponse(id, { tools: toolsForOptions(options) });
|
|
173
|
+
if (method === "tools/call") {
|
|
174
|
+
const params = isPlainRecord(input["params"]) ? input["params"] : {};
|
|
175
|
+
return await callTool(id, typeof params["name"] === "string" ? params["name"] : "", isPlainRecord(params["arguments"]) ? params["arguments"] : {}, options);
|
|
176
|
+
}
|
|
177
|
+
if (method === "notifications/initialized") return void 0;
|
|
178
|
+
return errorResponse(id, -32601, "Method not found");
|
|
179
|
+
}
|
|
180
|
+
async function runMcpStdioServer(input, output, options = {}) {
|
|
181
|
+
if (!canRunGitBash(options)) return;
|
|
182
|
+
await runJsonRpcStdioServer({
|
|
183
|
+
input,
|
|
184
|
+
output,
|
|
185
|
+
handler: handleGitBashMcpRequest,
|
|
186
|
+
handlerOptions: options,
|
|
187
|
+
idleTimeoutMs: 0,
|
|
188
|
+
log: options.lifecycleLog,
|
|
189
|
+
parseErrorResponse: () => errorResponse(null, -32601, "Method not found")
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async function callTool(id, name, args, options) {
|
|
193
|
+
if (name === "which_bash") return toolResponse(id, whichBashPayload(resolve$1(options)));
|
|
194
|
+
if (name === "diagnose") return toolResponse(id, diagnosePayload(resolve$1(options), platformFromOptions(options)));
|
|
195
|
+
if (name === "run") return await runToolResponse(id, args, options);
|
|
196
|
+
return toolResponse(id, `Unknown git_bash tool: ${name}`, true);
|
|
197
|
+
}
|
|
198
|
+
async function runToolResponse(id, args, options) {
|
|
199
|
+
if (platformFromOptions(options) !== "win32") return toolResponse(id, "git_bash run is only available on native Windows.", true);
|
|
200
|
+
const command = typeof args.command === "string" ? args.command.trim() : "";
|
|
201
|
+
if (command.length === 0) return toolResponse(id, "run.command must be a non-empty string.", true);
|
|
202
|
+
const cwd = parseWorkdir(args);
|
|
203
|
+
if (cwd === null) return toolResponse(id, "run.workdir must be a non-empty string when provided.", true);
|
|
204
|
+
const timeoutMs = parseTimeoutMs(args.timeout ?? args.timeout_ms, options);
|
|
205
|
+
if (timeoutMs === null) return toolResponse(id, `run.timeout must be an integer between 1 and ${MAX_TIMEOUT_MS}.`, true);
|
|
206
|
+
const resolution = resolve$1(options);
|
|
207
|
+
if (!resolution.found || resolution.path === null) return toolResponse(id, whichBashPayload(resolution), true);
|
|
208
|
+
try {
|
|
209
|
+
return toolResponse(id, runPayload(await (options.runGitBash ?? runGitBashCommand)({
|
|
210
|
+
bashPath: resolution.path,
|
|
211
|
+
command,
|
|
212
|
+
cwd,
|
|
213
|
+
timeoutMs,
|
|
214
|
+
env: options.env ?? process.env
|
|
215
|
+
})));
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return toolResponse(id, error instanceof Error ? error.message : String(error), true);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function toolsForOptions(options) {
|
|
221
|
+
const sharedTools = [{
|
|
222
|
+
name: "which_bash",
|
|
223
|
+
description: "Use before Windows shell work when the Git Bash executable path must be confirmed.",
|
|
224
|
+
inputSchema: {
|
|
225
|
+
type: "object",
|
|
226
|
+
properties: {},
|
|
227
|
+
additionalProperties: false
|
|
228
|
+
}
|
|
229
|
+
}, {
|
|
230
|
+
name: "diagnose",
|
|
231
|
+
description: "Use when git_bash cannot run or Windows shell readiness needs diagnosis.",
|
|
232
|
+
inputSchema: {
|
|
233
|
+
type: "object",
|
|
234
|
+
properties: {},
|
|
235
|
+
additionalProperties: false
|
|
236
|
+
}
|
|
237
|
+
}];
|
|
238
|
+
if (!canRunGitBash(options)) return sharedTools;
|
|
239
|
+
return [{
|
|
240
|
+
name: "run",
|
|
241
|
+
description: "Use on native Windows for Bash commands, POSIX behavior, Git tooling, or Unix utilities; use exec_command only when git_bash is unavailable or the operation is not shell work.",
|
|
242
|
+
inputSchema: {
|
|
243
|
+
type: "object",
|
|
244
|
+
properties: {
|
|
245
|
+
command: {
|
|
246
|
+
type: "string",
|
|
247
|
+
description: "The command to execute."
|
|
248
|
+
},
|
|
249
|
+
timeout: {
|
|
250
|
+
type: "integer",
|
|
251
|
+
minimum: 1,
|
|
252
|
+
maximum: MAX_TIMEOUT_MS,
|
|
253
|
+
description: `Optional timeout in milliseconds. If omitted, uses the inherited exec_command timeout when configured; otherwise ${defaultTimeoutMs(options)}ms.`
|
|
254
|
+
},
|
|
255
|
+
workdir: {
|
|
256
|
+
type: "string",
|
|
257
|
+
description: "The working directory to run the command in. Defaults to the current directory. Use this instead of 'cd' commands."
|
|
258
|
+
},
|
|
259
|
+
description: {
|
|
260
|
+
type: "string",
|
|
261
|
+
description: "Clear, concise description of what this command does in 5-10 words."
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
required: ["command"],
|
|
265
|
+
additionalProperties: false
|
|
266
|
+
}
|
|
267
|
+
}, ...sharedTools];
|
|
268
|
+
}
|
|
269
|
+
function canRunGitBash(options) {
|
|
270
|
+
if (platformFromOptions(options) !== "win32") return false;
|
|
271
|
+
const resolution = resolve$1(options);
|
|
272
|
+
return resolution.found && resolution.path !== null;
|
|
273
|
+
}
|
|
274
|
+
function resolve$1(options) {
|
|
275
|
+
if (options.exists === void 0 && options.where === void 0) return resolveGitBashForCurrentProcess({
|
|
276
|
+
platform: options.platform,
|
|
277
|
+
env: options.env
|
|
278
|
+
});
|
|
279
|
+
return resolveGitBash({
|
|
280
|
+
platform: platformFromOptions(options),
|
|
281
|
+
env: options.env ?? process.env,
|
|
282
|
+
exists: options.exists ?? (() => false),
|
|
283
|
+
where: options.where ?? (() => [])
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
function platformFromOptions(options) {
|
|
287
|
+
return options.platform ?? process.platform;
|
|
288
|
+
}
|
|
289
|
+
function whichBashPayload(resolution) {
|
|
290
|
+
return JSON.stringify(resolution, null, 2);
|
|
291
|
+
}
|
|
292
|
+
function diagnosePayload(resolution, platform) {
|
|
293
|
+
const enabled = platform === "win32" && resolution.found && resolution.path !== null;
|
|
294
|
+
return JSON.stringify({
|
|
295
|
+
platform,
|
|
296
|
+
enabled,
|
|
297
|
+
status: platform === "win32" ? enabled ? "ready" : "missing-git-bash" : "disabled: git_bash command execution is only exposed on native Windows",
|
|
298
|
+
resolution
|
|
299
|
+
}, null, 2);
|
|
300
|
+
}
|
|
301
|
+
function runPayload(result) {
|
|
302
|
+
return JSON.stringify(result, null, 2);
|
|
303
|
+
}
|
|
304
|
+
function toolResponse(id, text, isError = false) {
|
|
305
|
+
return successResponse(id, {
|
|
306
|
+
content: [{
|
|
307
|
+
type: "text",
|
|
308
|
+
text
|
|
309
|
+
}],
|
|
310
|
+
isError
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function parseWorkdir(args) {
|
|
314
|
+
const value = args.workdir ?? args.cwd;
|
|
315
|
+
if (value === void 0) return void 0;
|
|
316
|
+
return typeof value === "string" && value.trim().length > 0 ? value : null;
|
|
317
|
+
}
|
|
318
|
+
function parseTimeoutMs(value, options) {
|
|
319
|
+
if (value === void 0) return defaultTimeoutMs(options);
|
|
320
|
+
return normalizeTimeoutMs(value);
|
|
321
|
+
}
|
|
322
|
+
function defaultTimeoutMs(options) {
|
|
323
|
+
const configured = normalizeTimeoutMs(options.defaultTimeoutMs);
|
|
324
|
+
if (configured !== null) return configured;
|
|
325
|
+
const env = options.env ?? process.env;
|
|
326
|
+
for (const key of EXEC_COMMAND_TIMEOUT_ENV_KEYS) {
|
|
327
|
+
const timeoutMs = normalizeTimeoutMs(env[key]);
|
|
328
|
+
if (timeoutMs !== null) return timeoutMs;
|
|
329
|
+
}
|
|
330
|
+
return DEFAULT_TIMEOUT_MS;
|
|
331
|
+
}
|
|
332
|
+
function normalizeTimeoutMs(value) {
|
|
333
|
+
const parsed = typeof value === "string" && value.trim().length > 0 ? Number(value) : value;
|
|
334
|
+
if (!Number.isInteger(parsed)) return null;
|
|
335
|
+
const timeoutMs = Number(parsed);
|
|
336
|
+
if (timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) return null;
|
|
337
|
+
return timeoutMs;
|
|
338
|
+
}
|
|
339
|
+
function protocolVersionFromInput(input) {
|
|
340
|
+
if (!isPlainRecord(input["params"])) return null;
|
|
341
|
+
const params = input["params"];
|
|
342
|
+
return typeof params["protocolVersion"] === "string" ? params["protocolVersion"] : null;
|
|
343
|
+
}
|
|
344
|
+
//#endregion
|
|
345
|
+
//#region packages/git-bash-mcp/src/cli.ts
|
|
346
|
+
async function main() {
|
|
347
|
+
const [command = "mcp"] = argv.slice(2);
|
|
348
|
+
if (command === "mcp") {
|
|
349
|
+
await runMcpStdioServer(process.stdin, process.stdout);
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
stderr.write("Usage: holycodex-git-bash [mcp]\n");
|
|
353
|
+
process.exitCode = 2;
|
|
354
|
+
}
|
|
355
|
+
main().catch((error) => {
|
|
356
|
+
stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
|
|
357
|
+
process.exitCode = 1;
|
|
358
|
+
});
|
|
359
|
+
//#endregion
|