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.
Files changed (147) hide show
  1. package/.agents/skills/opencode-qa/SKILL.md +194 -0
  2. package/.agents/skills/opencode-qa/references/cli-commands.md +188 -0
  3. package/.agents/skills/opencode-qa/references/db-investigation.md +197 -0
  4. package/.agents/skills/opencode-qa/references/events-hooks.md +110 -0
  5. package/.agents/skills/opencode-qa/references/sdk.md +96 -0
  6. package/.agents/skills/opencode-qa/references/server-api.md +200 -0
  7. package/.agents/skills/opencode-qa/references/testing-harness.md +218 -0
  8. package/.agents/skills/opencode-qa/references/tui-tmux.md +52 -0
  9. package/.agents/skills/opencode-qa/scripts/db-session-by-id.sh +53 -0
  10. package/.agents/skills/opencode-qa/scripts/db-session-by-name.sh +57 -0
  11. package/.agents/skills/opencode-qa/scripts/db-session-by-text.sh +158 -0
  12. package/.agents/skills/opencode-qa/scripts/export-roundtrip.sh +57 -0
  13. package/.agents/skills/opencode-qa/scripts/lib/common.sh +216 -0
  14. package/.agents/skills/opencode-qa/scripts/server-smoke.sh +64 -0
  15. package/.agents/skills/opencode-qa/scripts/sse-hook-probe.sh +106 -0
  16. package/.agents/skills/opencode-qa/scripts/tui-smoke.sh +89 -0
  17. package/README.ja.md +13 -3
  18. package/README.ko.md +13 -3
  19. package/README.md +24 -14
  20. package/README.ru.md +13 -3
  21. package/README.zh-cn.md +13 -3
  22. package/bin/oh-my-opencode.js +4 -3
  23. package/bin/oh-my-opencode.test.ts +35 -7
  24. package/bin/platform.d.ts +1 -1
  25. package/bin/platform.js +4 -4
  26. package/bin/platform.test.ts +31 -9
  27. package/dist/cli/cleanup-command.d.ts +4 -0
  28. package/dist/cli/cleanup.d.ts +11 -0
  29. package/dist/cli/cli-program.d.ts +2 -1
  30. package/dist/cli/index.js +1837 -450
  31. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  32. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  33. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  34. package/dist/cli/install-codex/codex-config-mcp.d.ts +1 -0
  35. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-config-reasoning.d.ts +1 -0
  37. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  38. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  39. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  40. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  41. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  42. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  43. package/dist/cli/install-codex/index.d.ts +4 -0
  44. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  45. package/dist/cli/install-codex/types.d.ts +20 -0
  46. package/dist/cli/run/event-state.d.ts +1 -0
  47. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  48. package/dist/cli/run/prompt-start.d.ts +7 -0
  49. package/dist/cli/star-request.d.ts +9 -0
  50. package/dist/config/schema/hooks.d.ts +0 -1
  51. package/dist/create-hooks.d.ts +0 -1
  52. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  53. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  54. package/dist/hooks/index.d.ts +0 -1
  55. package/dist/index.js +267 -114
  56. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  57. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  58. package/dist/plugin/messages-transform.d.ts +8 -1
  59. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  60. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  61. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  62. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  63. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  64. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  65. package/dist/shared/prompt-async-gate.d.ts +1 -1
  66. package/package.json +22 -17
  67. package/packages/git-bash-mcp/dist/cli.js +367 -0
  68. package/packages/omo-codex/plugin/.mcp.json +11 -0
  69. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  70. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  71. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  72. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  73. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  74. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  75. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  76. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  77. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  78. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  79. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  80. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  81. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  82. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  83. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  84. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  85. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  86. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  87. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  88. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  89. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +5 -4
  90. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  91. package/packages/omo-codex/plugin/components/ultrawork/README.md +2 -2
  92. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +1 -0
  93. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  94. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +2 -1
  95. package/packages/omo-codex/plugin/components/ultrawork/directive.md +31 -5
  96. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  97. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  98. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  99. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +27 -205
  100. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +230 -0
  101. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  102. package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
  103. package/packages/omo-codex/plugin/package-lock.json +19 -0
  104. package/packages/omo-codex/plugin/package.json +3 -1
  105. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  106. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  107. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +87 -0
  108. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +27 -2
  109. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +20 -0
  110. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +27 -205
  111. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +230 -0
  112. package/packages/omo-codex/plugin/test/aggregate.test.mjs +23 -8
  113. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +56 -11
  114. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  115. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  116. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  117. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  118. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +66 -0
  119. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +32 -2
  120. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  121. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  122. package/packages/omo-codex/scripts/install/config.mjs +36 -1
  123. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  124. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  125. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  126. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  127. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  128. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  129. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  130. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  131. package/packages/omo-codex/scripts/install/reasoning-config.mjs +14 -0
  132. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  133. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  134. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  135. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  136. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +62 -0
  137. package/packages/omo-codex/scripts/install-config.test.mjs +206 -0
  138. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +129 -0
  139. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  140. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  141. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  142. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  143. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  144. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  145. package/packages/shared-skills/skills/review-work/SKILL.md +27 -2
  146. package/packages/shared-skills/skills/start-work/SKILL.md +20 -0
  147. 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,10 @@
1
+ export {
2
+ applyGitBashPostCompactReset,
3
+ applyGitBashPreToolUseReminder,
4
+ parsePostCompactPayload,
5
+ parsePreToolUsePayload,
6
+ runGitBashHookCli,
7
+ type GitBashHookOptions,
8
+ type PostCompactPayload,
9
+ type PreToolUsePayload,
10
+ } from "./codex-hook.js";
@@ -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
- bunx lazycodex install
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 { dirname, resolve } from "node:path";
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 PACKAGE_LSP_MCP_CLI = "../../../../../lsp-tools-mcp/dist/cli.js";
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(dirname(fileURLToPath(import.meta.url)), PACKAGE_LSP_MCP_CLI);
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 { disposeDefaultLspManager } from "../../../../../lsp-tools-mcp/dist/lsp/manager.js";
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: (await runDiagnostics(filePath)).trim() };
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
+ }