oh-my-opencode 4.5.12 → 4.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/opencode-qa/SKILL.md +194 -0
- package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
- package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
- package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
- package/.agents/skills/opencode-qa/references/sdk.md +96 -0
- package/.agents/skills/opencode-qa/references/server-api.md +200 -0
- package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
- package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
- package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
- package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
- package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
- package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
- package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
- package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
- package/README.ja.md +13 -3
- package/README.ko.md +13 -3
- package/README.md +24 -14
- package/README.ru.md +13 -3
- package/README.zh-cn.md +13 -3
- package/bin/oh-my-opencode.js +4 -3
- package/bin/oh-my-opencode.test.ts +35 -7
- package/bin/platform.d.ts +1 -1
- package/bin/platform.js +4 -4
- package/bin/platform.test.ts +31 -9
- package/dist/cli/cleanup-command.d.ts +4 -0
- package/dist/cli/cleanup.d.ts +11 -0
- package/dist/cli/cli-program.d.ts +2 -1
- package/dist/cli/index.js +1837 -450
- package/dist/cli/install-codex/codex-cache.d.ts +1 -0
- package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
- package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
- package/dist/cli/install-codex/codex-config-mcp.d.ts +1 -0
- package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
- package/dist/cli/install-codex/codex-config-reasoning.d.ts +1 -0
- package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
- package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
- package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
- package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
- package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
- package/dist/cli/install-codex/git-bash.d.ts +35 -0
- package/dist/cli/install-codex/index.d.ts +4 -0
- package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
- package/dist/cli/install-codex/types.d.ts +20 -0
- package/dist/cli/run/event-state.d.ts +1 -0
- package/dist/cli/run/poll-for-completion.d.ts +1 -0
- package/dist/cli/run/prompt-start.d.ts +7 -0
- package/dist/cli/star-request.d.ts +9 -0
- package/dist/config/schema/hooks.d.ts +0 -1
- package/dist/create-hooks.d.ts +0 -1
- package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
- package/dist/features/builtin-skills/skills/index.d.ts +1 -0
- package/dist/hooks/index.d.ts +0 -1
- package/dist/index.js +267 -114
- package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
- package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
- package/dist/plugin/messages-transform.d.ts +8 -1
- package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
- package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
- package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
- package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
- package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
- package/dist/shared/prompt-async-gate/types.d.ts +2 -0
- package/dist/shared/prompt-async-gate.d.ts +1 -1
- package/package.json +22 -17
- package/packages/git-bash-mcp/dist/cli.js +367 -0
- package/packages/omo-codex/plugin/.mcp.json +11 -0
- package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
- package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
- package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
- package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
- package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
- package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
- package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
- package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
- package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
- package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
- package/packages/omo-codex/plugin/components/rules/README.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
- package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +5 -4
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
- package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +1 -0
- package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
- package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +31 -5
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
- package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
- package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +27 -205
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +230 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
- package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
- package/packages/omo-codex/plugin/package-lock.json +19 -0
- package/packages/omo-codex/plugin/package.json +3 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
- package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
- package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +87 -0
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +27 -2
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +20 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +27 -205
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +230 -0
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +23 -8
- package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +56 -11
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
- package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
- package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
- package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
- package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +66 -0
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +32 -2
- package/packages/omo-codex/scripts/install/cache.mjs +5 -3
- package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
- package/packages/omo-codex/scripts/install/config.mjs +36 -1
- package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
- package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
- package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
- package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
- package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
- package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
- package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
- package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
- package/packages/omo-codex/scripts/install/reasoning-config.mjs +14 -0
- package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
- package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
- package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
- package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
- package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +62 -0
- package/packages/omo-codex/scripts/install-config.test.mjs +206 -0
- package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +129 -0
- package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
- package/packages/omo-codex/scripts/install-local.mjs +91 -8
- package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
- package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
- package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
- package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
- package/packages/shared-skills/skills/review-work/SKILL.md +27 -2
- package/packages/shared-skills/skills/start-work/SKILL.md +20 -0
- package/dist/hooks/context-window-monitor.d.ts +0 -19
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sisyphuslabs/codex-git-bash-hook",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Codex hook component that reminds Windows sessions to prefer the OMO git_bash MCP.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": true,
|
|
7
|
+
"bin": {
|
|
8
|
+
"omo-git-bash-hook": "./dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": ["dist", "hooks"],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc -p tsconfig.build.json",
|
|
13
|
+
"test": "bun test test/*.test.ts",
|
|
14
|
+
"typecheck": "tsc --noEmit"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^25.7.0",
|
|
18
|
+
"typescript": "^6.0.3"
|
|
19
|
+
},
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { runGitBashHookCli } from "./codex-hook.js";
|
|
3
|
+
|
|
4
|
+
const TOP_LEVEL_HELP =
|
|
5
|
+
"Usage:\n omo-git-bash-hook hook pre-tool-use\n omo-git-bash-hook hook post-compact\n omo-git-bash-hook help | --help | -h\n";
|
|
6
|
+
|
|
7
|
+
async function main(): Promise<number> {
|
|
8
|
+
const argv = process.argv.slice(2);
|
|
9
|
+
const command = argv[0];
|
|
10
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
11
|
+
process.stdout.write(TOP_LEVEL_HELP);
|
|
12
|
+
return 0;
|
|
13
|
+
}
|
|
14
|
+
if (command === "hook" && argv[1] === "pre-tool-use") {
|
|
15
|
+
await runGitBashHookCli(process.stdin, process.stdout, "pre-tool-use");
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
if (command === "hook" && argv[1] === "post-compact") {
|
|
19
|
+
await runGitBashHookCli(process.stdin, process.stdout, "post-compact");
|
|
20
|
+
return 0;
|
|
21
|
+
}
|
|
22
|
+
process.stderr.write(`[omo-git-bash-hook] unknown command: ${argv.join(" ")}\n${TOP_LEVEL_HELP}`);
|
|
23
|
+
return 1;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
main()
|
|
27
|
+
.then((code) => {
|
|
28
|
+
process.exit(code);
|
|
29
|
+
})
|
|
30
|
+
.catch((error: unknown) => {
|
|
31
|
+
process.stderr.write(`[omo-git-bash-hook] ${error instanceof Error ? error.message : String(error)}\n`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
});
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
|
|
5
|
+
export interface PreToolUsePayload {
|
|
6
|
+
readonly cwd: string;
|
|
7
|
+
readonly hook_event_name: "PreToolUse";
|
|
8
|
+
readonly model: string;
|
|
9
|
+
readonly permission_mode: string;
|
|
10
|
+
readonly session_id: string;
|
|
11
|
+
readonly tool_input: unknown;
|
|
12
|
+
readonly tool_name: string;
|
|
13
|
+
readonly tool_use_id: string;
|
|
14
|
+
readonly transcript_path: string | null;
|
|
15
|
+
readonly turn_id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface GitBashHookOptions {
|
|
19
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
20
|
+
readonly platform?: NodeJS.Platform | string;
|
|
21
|
+
readonly pluginDataRoot?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface PostCompactPayload {
|
|
25
|
+
readonly hook_event_name: "PostCompact";
|
|
26
|
+
readonly session_id: string;
|
|
27
|
+
readonly transcript_path?: string | null;
|
|
28
|
+
readonly trigger?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface PreToolUseHookOutput {
|
|
32
|
+
readonly hookSpecificOutput: {
|
|
33
|
+
readonly hookEventName: "PreToolUse";
|
|
34
|
+
readonly additionalContext: string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const BASH_TOOL_NAME = "Bash";
|
|
39
|
+
const REMINDER =
|
|
40
|
+
"On Windows, prefer the OMO git_bash MCP for shell commands before using built-in exec_command. Use exec_command only when git_bash is unavailable or for non-shell operations.";
|
|
41
|
+
|
|
42
|
+
export function parsePreToolUsePayload(raw: string): PreToolUsePayload | null {
|
|
43
|
+
if (raw.trim().length === 0) return null;
|
|
44
|
+
try {
|
|
45
|
+
const parsed: unknown = JSON.parse(raw);
|
|
46
|
+
return isPreToolUsePayload(parsed) ? parsed : null;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
if (error instanceof SyntaxError) return null;
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parsePostCompactPayload(raw: string): PostCompactPayload | null {
|
|
54
|
+
if (raw.trim().length === 0) return null;
|
|
55
|
+
try {
|
|
56
|
+
const parsed: unknown = JSON.parse(raw);
|
|
57
|
+
return isPostCompactPayload(parsed) ? parsed : null;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if (error instanceof SyntaxError) return null;
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function applyGitBashPreToolUseReminder(payload: PreToolUsePayload, options: GitBashHookOptions = {}): string {
|
|
65
|
+
if (payload.hook_event_name !== "PreToolUse") return "";
|
|
66
|
+
if (payload.tool_name !== BASH_TOOL_NAME) return "";
|
|
67
|
+
if (!isWindowsHost(options)) return "";
|
|
68
|
+
|
|
69
|
+
const markerPath = reminderMarkerPath(payload.session_id, options.pluginDataRoot);
|
|
70
|
+
if (hasReminderMarker(markerPath)) return "";
|
|
71
|
+
mkdirSync(dirname(markerPath), { recursive: true });
|
|
72
|
+
writeFileSync(markerPath, `${new Date().toISOString()}\n`);
|
|
73
|
+
|
|
74
|
+
const output: PreToolUseHookOutput = {
|
|
75
|
+
hookSpecificOutput: {
|
|
76
|
+
hookEventName: "PreToolUse",
|
|
77
|
+
additionalContext: REMINDER,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
return `${JSON.stringify(output)}\n`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function applyGitBashPostCompactReset(payload: PostCompactPayload, options: GitBashHookOptions = {}): string {
|
|
84
|
+
if (payload.hook_event_name !== "PostCompact") return "";
|
|
85
|
+
rmSync(reminderMarkerPath(payload.session_id, options.pluginDataRoot), { force: true });
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export async function runGitBashHookCli(
|
|
90
|
+
stdin: NodeJS.ReadableStream,
|
|
91
|
+
stdout: NodeJS.WritableStream,
|
|
92
|
+
eventName: "pre-tool-use" | "post-compact" = "pre-tool-use",
|
|
93
|
+
options: GitBashHookOptions = {},
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
try {
|
|
96
|
+
const raw = await readAll(stdin);
|
|
97
|
+
const output =
|
|
98
|
+
eventName === "post-compact" ? postCompactOutput(raw, options) : preToolUseOutput(raw, options);
|
|
99
|
+
if (output.length > 0) stdout.write(output);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof Error) return;
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function preToolUseOutput(raw: string, options: GitBashHookOptions): string {
|
|
107
|
+
const payload = parsePreToolUsePayload(raw);
|
|
108
|
+
if (payload === null) return "";
|
|
109
|
+
return applyGitBashPreToolUseReminder(payload, options);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function postCompactOutput(raw: string, options: GitBashHookOptions): string {
|
|
113
|
+
const payload = parsePostCompactPayload(raw);
|
|
114
|
+
if (payload === null) return "";
|
|
115
|
+
return applyGitBashPostCompactReset(payload, options);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isWindowsHost(options: GitBashHookOptions): boolean {
|
|
119
|
+
const platform = options.platform ?? process.platform;
|
|
120
|
+
if (platform === "win32") return true;
|
|
121
|
+
const env = options.env ?? process.env;
|
|
122
|
+
return env["OS"] === "Windows_NT" || env["ComSpec"] !== undefined || env["SystemRoot"] !== undefined;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hasReminderMarker(path: string): boolean {
|
|
126
|
+
return existsSync(path);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function reminderMarkerPath(sessionId: string, pluginDataRoot?: string): string {
|
|
130
|
+
const root = pluginDataRoot ?? process.env["PLUGIN_DATA"] ?? join(homedir(), ".codex", "omo-git-bash");
|
|
131
|
+
return join(root, "git-bash-reminder", `${safePathSegment(sessionId)}.seen`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function safePathSegment(value: string): string {
|
|
135
|
+
return value.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function isPreToolUsePayload(value: unknown): value is PreToolUsePayload {
|
|
139
|
+
if (!isRecord(value)) return false;
|
|
140
|
+
return (
|
|
141
|
+
value["hook_event_name"] === "PreToolUse" &&
|
|
142
|
+
typeof value["cwd"] === "string" &&
|
|
143
|
+
typeof value["model"] === "string" &&
|
|
144
|
+
typeof value["permission_mode"] === "string" &&
|
|
145
|
+
typeof value["session_id"] === "string" &&
|
|
146
|
+
typeof value["tool_name"] === "string" &&
|
|
147
|
+
typeof value["tool_use_id"] === "string" &&
|
|
148
|
+
(value["transcript_path"] === null || typeof value["transcript_path"] === "string") &&
|
|
149
|
+
typeof value["turn_id"] === "string" &&
|
|
150
|
+
Object.hasOwn(value, "tool_input")
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isPostCompactPayload(value: unknown): value is PostCompactPayload {
|
|
155
|
+
if (!isRecord(value)) return false;
|
|
156
|
+
return (
|
|
157
|
+
value["hook_event_name"] === "PostCompact" &&
|
|
158
|
+
typeof value["session_id"] === "string" &&
|
|
159
|
+
(value["transcript_path"] === undefined ||
|
|
160
|
+
value["transcript_path"] === null ||
|
|
161
|
+
typeof value["transcript_path"] === "string") &&
|
|
162
|
+
(value["trigger"] === undefined || typeof value["trigger"] === "string")
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
167
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function readAll(stdin: NodeJS.ReadableStream): Promise<string> {
|
|
171
|
+
return new Promise((resolve, reject) => {
|
|
172
|
+
let data = "";
|
|
173
|
+
stdin.setEncoding("utf8");
|
|
174
|
+
stdin.on("data", (chunk: unknown) => {
|
|
175
|
+
data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
|
176
|
+
});
|
|
177
|
+
stdin.once("error", reject);
|
|
178
|
+
stdin.once("end", () => resolve(data));
|
|
179
|
+
});
|
|
180
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { Readable, Writable } from "node:stream";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
applyGitBashPostCompactReset,
|
|
9
|
+
applyGitBashPreToolUseReminder,
|
|
10
|
+
runGitBashHookCli,
|
|
11
|
+
type PostCompactPayload,
|
|
12
|
+
type PreToolUsePayload,
|
|
13
|
+
} from "../src/codex-hook.js";
|
|
14
|
+
|
|
15
|
+
const temporaryDirectories: string[] = [];
|
|
16
|
+
|
|
17
|
+
function createTemporaryDirectory(prefix: string): string {
|
|
18
|
+
const directory = mkdtempSync(join(tmpdir(), prefix));
|
|
19
|
+
temporaryDirectories.push(directory);
|
|
20
|
+
return directory;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
for (const directory of temporaryDirectories.splice(0)) {
|
|
25
|
+
rmSync(directory, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function preToolPayload(toolName: string, sessionId = "session-1"): PreToolUsePayload {
|
|
30
|
+
return {
|
|
31
|
+
cwd: "/repo",
|
|
32
|
+
hook_event_name: "PreToolUse",
|
|
33
|
+
model: "gpt-5.5",
|
|
34
|
+
permission_mode: "default",
|
|
35
|
+
session_id: sessionId,
|
|
36
|
+
tool_input: { command: "pwd" },
|
|
37
|
+
tool_name: toolName,
|
|
38
|
+
tool_use_id: "call-1",
|
|
39
|
+
transcript_path: null,
|
|
40
|
+
turn_id: "turn-1",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function postCompactPayload(sessionId = "session-1"): PostCompactPayload {
|
|
45
|
+
return {
|
|
46
|
+
hook_event_name: "PostCompact",
|
|
47
|
+
session_id: sessionId,
|
|
48
|
+
transcript_path: null,
|
|
49
|
+
trigger: "manual",
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function windowsEnv(): NodeJS.ProcessEnv {
|
|
54
|
+
return { OS: "Windows_NT", ComSpec: "C:\\Windows\\System32\\cmd.exe" };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function captureStdout(): { readonly stdout: Writable; readonly read: () => string } {
|
|
58
|
+
let captured = "";
|
|
59
|
+
const stdout = new Writable({
|
|
60
|
+
write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
|
|
61
|
+
captured += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
|
62
|
+
callback();
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
return { stdout, read: () => captured };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
describe("applyGitBashPreToolUseReminder", () => {
|
|
69
|
+
it("#given first Windows Bash call #when hook runs #then emits non-blocking git_bash guidance", () => {
|
|
70
|
+
// given
|
|
71
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
72
|
+
|
|
73
|
+
// when
|
|
74
|
+
const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
|
|
75
|
+
env: windowsEnv(),
|
|
76
|
+
platform: "linux",
|
|
77
|
+
pluginDataRoot,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// then
|
|
81
|
+
const parsed = JSON.parse(output);
|
|
82
|
+
expect(parsed.hookSpecificOutput).toEqual({
|
|
83
|
+
hookEventName: "PreToolUse",
|
|
84
|
+
additionalContext:
|
|
85
|
+
"On Windows, prefer the OMO git_bash MCP for shell commands before using built-in exec_command. Use exec_command only when git_bash is unavailable or for non-shell operations.",
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("#given second Windows Bash call in same session #when hook runs #then it stays silent", () => {
|
|
90
|
+
// given
|
|
91
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
92
|
+
const payload = preToolPayload("Bash");
|
|
93
|
+
|
|
94
|
+
// when
|
|
95
|
+
const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
96
|
+
const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
97
|
+
|
|
98
|
+
// then
|
|
99
|
+
expect(first).toContain("git_bash");
|
|
100
|
+
expect(second).toBe("");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("#given non-Windows Bash call #when hook runs #then it stays silent", () => {
|
|
104
|
+
// given
|
|
105
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
106
|
+
|
|
107
|
+
// when
|
|
108
|
+
const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
|
|
109
|
+
env: {},
|
|
110
|
+
platform: "darwin",
|
|
111
|
+
pluginDataRoot,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// then
|
|
115
|
+
expect(output).toBe("");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("#given non-Bash tool call #when hook runs #then it stays silent", () => {
|
|
119
|
+
// given
|
|
120
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
121
|
+
|
|
122
|
+
// when
|
|
123
|
+
const output = applyGitBashPreToolUseReminder(preToolPayload("exec_command"), {
|
|
124
|
+
env: windowsEnv(),
|
|
125
|
+
platform: "linux",
|
|
126
|
+
pluginDataRoot,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// then
|
|
130
|
+
expect(output).toBe("");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("applyGitBashPostCompactReset", () => {
|
|
135
|
+
it("#given reminder already emitted #when PostCompact runs #then next Windows Bash call emits reminder again", () => {
|
|
136
|
+
// given
|
|
137
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
138
|
+
const payload = preToolPayload("Bash");
|
|
139
|
+
const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
140
|
+
const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
141
|
+
|
|
142
|
+
// when
|
|
143
|
+
applyGitBashPostCompactReset(postCompactPayload(), { pluginDataRoot });
|
|
144
|
+
const afterCompact = applyGitBashPreToolUseReminder(payload, {
|
|
145
|
+
env: windowsEnv(),
|
|
146
|
+
platform: "linux",
|
|
147
|
+
pluginDataRoot,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// then
|
|
151
|
+
expect(first).toContain("git_bash");
|
|
152
|
+
expect(second).toBe("");
|
|
153
|
+
expect(afterCompact).toContain("git_bash");
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("runGitBashHookCli", () => {
|
|
158
|
+
it("#given Codex PreToolUse stdin on Windows #when CLI hook runs #then it writes reminder JSON", async () => {
|
|
159
|
+
// given
|
|
160
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
161
|
+
const stdin = Readable.from([JSON.stringify(preToolPayload("Bash"))]);
|
|
162
|
+
const capture = captureStdout();
|
|
163
|
+
|
|
164
|
+
// when
|
|
165
|
+
await runGitBashHookCli(stdin, capture.stdout, "pre-tool-use", {
|
|
166
|
+
env: windowsEnv(),
|
|
167
|
+
platform: "linux",
|
|
168
|
+
pluginDataRoot,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// then
|
|
172
|
+
expect(capture.read()).toContain("git_bash MCP");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("#given PostCompact stdin #when CLI hook runs #then it resets the one-shot reminder", async () => {
|
|
176
|
+
// given
|
|
177
|
+
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
|
178
|
+
const payload = preToolPayload("Bash");
|
|
179
|
+
applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
|
180
|
+
const resetStdin = Readable.from([JSON.stringify(postCompactPayload())]);
|
|
181
|
+
const capture = captureStdout();
|
|
182
|
+
|
|
183
|
+
// when
|
|
184
|
+
await runGitBashHookCli(resetStdin, capture.stdout, "post-compact", { pluginDataRoot });
|
|
185
|
+
const afterCompact = applyGitBashPreToolUseReminder(payload, {
|
|
186
|
+
env: windowsEnv(),
|
|
187
|
+
platform: "linux",
|
|
188
|
+
pluginDataRoot,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// then
|
|
192
|
+
expect(capture.read()).toBe("");
|
|
193
|
+
expect(afterCompact).toContain("git_bash");
|
|
194
|
+
});
|
|
195
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"allowImportingTsExtensions": false,
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"outDir": "dist",
|
|
7
|
+
"rootDir": "src",
|
|
8
|
+
"noEmit": false,
|
|
9
|
+
"types": ["node"]
|
|
10
|
+
},
|
|
11
|
+
"include": ["src/**/*"],
|
|
12
|
+
"exclude": ["test/**/*"]
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "Node16",
|
|
5
|
+
"moduleResolution": "Node16",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"strict": true,
|
|
8
|
+
"exactOptionalPropertyTypes": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"allowImportingTsExtensions": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"forceConsistentCasingInFileNames": true,
|
|
21
|
+
"types": ["node", "bun-types"],
|
|
22
|
+
"noEmit": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src/**/*", "test/**/*"]
|
|
25
|
+
}
|
|
@@ -118,7 +118,7 @@ printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | node dist/cli.j
|
|
|
118
118
|
## Local Codex Installation
|
|
119
119
|
|
|
120
120
|
```bash
|
|
121
|
-
|
|
121
|
+
npx lazycodex-ai install
|
|
122
122
|
```
|
|
123
123
|
|
|
124
124
|
The installer builds and copies the plugin into `~/.codex/plugins/cache/sisyphuslabs/omo/0.1.0`, registers the `sisyphuslabs` marketplace from the `lazycodex` Git repository, and enables:
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
4
|
import { argv, execPath, stderr } from "node:process";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
7
|
-
import { runPostToolUseHookCli } from "./codex-hook.js";
|
|
6
|
+
import { runPostToolUseHookCli } from "./codex-hook-cli.js";
|
|
8
7
|
|
|
9
|
-
const
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const PACKAGE_LSP_MCP_CLI = "@code-yeongyu/lsp-tools-mcp/dist/cli.js";
|
|
10
10
|
|
|
11
11
|
async function main(): Promise<void> {
|
|
12
12
|
const [command = "mcp", subcommand = ""] = argv.slice(2);
|
|
@@ -31,7 +31,7 @@ main().catch((error: unknown) => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
async function runPackageLspMcpCli(): Promise<void> {
|
|
34
|
-
const cliPath = resolve(
|
|
34
|
+
const cliPath = require.resolve(PACKAGE_LSP_MCP_CLI);
|
|
35
35
|
const child = spawn(execPath, [cliPath, "mcp"], { stdio: "inherit" });
|
|
36
36
|
await new Promise<void>((resolve, reject) => {
|
|
37
37
|
child.once("error", reject);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { stdin as processStdin } from "node:process";
|
|
2
|
+
|
|
3
|
+
import { disposeDefaultLspManager } from "@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js";
|
|
4
|
+
|
|
5
|
+
import { isRecord, runLspPostToolUseHook } from "./codex-hook.js";
|
|
6
|
+
|
|
7
|
+
export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await readStdin(stdin);
|
|
10
|
+
if (!raw.trim()) return;
|
|
11
|
+
let parsed: unknown;
|
|
12
|
+
try {
|
|
13
|
+
parsed = JSON.parse(raw);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
if (error instanceof SyntaxError) return;
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
const input = isRecord(parsed) ? parsed : {};
|
|
19
|
+
const output = await runLspPostToolUseHook(input);
|
|
20
|
+
if (output) process.stdout.write(output);
|
|
21
|
+
} finally {
|
|
22
|
+
await disposeDefaultLspManager();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
|
|
27
|
+
stdin.setEncoding("utf8");
|
|
28
|
+
let raw = "";
|
|
29
|
+
for await (const chunk of stdin) {
|
|
30
|
+
raw += chunk;
|
|
31
|
+
}
|
|
32
|
+
return raw;
|
|
33
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { stdin as processStdin } from "node:process";
|
|
3
2
|
|
|
4
|
-
import {
|
|
5
|
-
import { executeLspDiagnostics } from "../../../../../lsp-tools-mcp/dist/tools.js";
|
|
3
|
+
import { executeLspDiagnostics } from "@code-yeongyu/lsp-tools-mcp/dist/tools.js";
|
|
6
4
|
|
|
7
5
|
export type DiagnosticsRunner = (filePath: string) => Promise<string>;
|
|
8
6
|
|
|
@@ -91,13 +89,29 @@ async function collectDiagnostics(
|
|
|
91
89
|
nextIndex += 1;
|
|
92
90
|
const filePath = filePaths[index];
|
|
93
91
|
if (filePath === undefined) return;
|
|
94
|
-
results[index] = { filePath, diagnostics:
|
|
92
|
+
results[index] = { filePath, diagnostics: await collectFileDiagnostics(filePath, runDiagnostics) };
|
|
95
93
|
}
|
|
96
94
|
}
|
|
97
95
|
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
98
96
|
return results;
|
|
99
97
|
}
|
|
100
98
|
|
|
99
|
+
async function collectFileDiagnostics(filePath: string, runDiagnostics: DiagnosticsRunner): Promise<string> {
|
|
100
|
+
try {
|
|
101
|
+
return (await runDiagnostics(filePath)).trim();
|
|
102
|
+
} catch (error) {
|
|
103
|
+
return formatDiagnosticsError(error);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function formatDiagnosticsError(error: unknown): string {
|
|
108
|
+
if (error instanceof Error) {
|
|
109
|
+
const message = error.message.trim();
|
|
110
|
+
if (message.length > 0) return message;
|
|
111
|
+
}
|
|
112
|
+
return String(error).trim();
|
|
113
|
+
}
|
|
114
|
+
|
|
101
115
|
function formatDiagnosticBlock({ filePath, diagnostics }: DiagnosticBlock): string {
|
|
102
116
|
return `LSP diagnostics after editing ${filePath}:\n\n${formatDiagnosticsForDisplay(diagnostics)}`;
|
|
103
117
|
}
|
|
@@ -191,19 +205,6 @@ export function extractMutatedFilePaths(input: CodexPostToolUseInput): string[]
|
|
|
191
205
|
return [...paths];
|
|
192
206
|
}
|
|
193
207
|
|
|
194
|
-
export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
|
|
195
|
-
try {
|
|
196
|
-
const raw = await readStdin(stdin);
|
|
197
|
-
if (!raw.trim()) return;
|
|
198
|
-
const parsed: unknown = JSON.parse(raw);
|
|
199
|
-
const input = isRecord(parsed) ? parsed : {};
|
|
200
|
-
const output = await runLspPostToolUseHook(input);
|
|
201
|
-
if (output) process.stdout.write(output);
|
|
202
|
-
} finally {
|
|
203
|
-
await disposeDefaultLspManager();
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
208
|
function isMutationTool(value: unknown): boolean {
|
|
208
209
|
if (typeof value !== "string") return false;
|
|
209
210
|
return MUTATION_TOOL_NAMES.has(value.toLowerCase());
|
|
@@ -271,15 +272,6 @@ function addPatchFiles(paths: Set<string>, value: unknown): void {
|
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
|
|
274
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
275
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
275
276
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
276
277
|
}
|
|
277
|
-
|
|
278
|
-
async function readStdin(stdin: NodeJS.ReadStream): Promise<string> {
|
|
279
|
-
stdin.setEncoding("utf8");
|
|
280
|
-
let raw = "";
|
|
281
|
-
for await (const chunk of stdin) {
|
|
282
|
-
raw += chunk;
|
|
283
|
-
}
|
|
284
|
-
return raw;
|
|
285
|
-
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
describe("codex PostToolUse hook CLI", () => {
|
|
8
|
+
it("#given malformed post-tool-use stdin #when hook CLI runs #then it no-ops without stderr", () => {
|
|
9
|
+
// given
|
|
10
|
+
const input = "break;\n";
|
|
11
|
+
|
|
12
|
+
// when
|
|
13
|
+
const result = runBuiltHookCli(input);
|
|
14
|
+
|
|
15
|
+
// then
|
|
16
|
+
expect(result.status).toBe(0);
|
|
17
|
+
expect(result.stderr).toBe("");
|
|
18
|
+
expect(result.stdout).toBe("");
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function runBuiltHookCli(input: string): ReturnType<typeof spawnSync> {
|
|
23
|
+
const cliPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../dist/cli.js");
|
|
24
|
+
return spawnSync(process.execPath, [cliPath, "hook", "post-tool-use"], {
|
|
25
|
+
input,
|
|
26
|
+
encoding: "utf8",
|
|
27
|
+
});
|
|
28
|
+
}
|