oh-my-opencode 4.5.12 → 4.7.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 (189) 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/bin/version-mismatch.js +47 -0
  28. package/bin/version-mismatch.test.ts +120 -0
  29. package/dist/cli/cleanup-command.d.ts +4 -0
  30. package/dist/cli/cleanup.d.ts +11 -0
  31. package/dist/cli/cli-program.d.ts +2 -1
  32. package/dist/cli/codex-ulw-loop.d.ts +12 -0
  33. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
  34. package/dist/cli/index.js +2189 -529
  35. package/dist/cli/install-codex/codex-cache.d.ts +1 -0
  36. package/dist/cli/install-codex/codex-cleanup-config.d.ts +6 -0
  37. package/dist/cli/install-codex/codex-cleanup.d.ts +21 -0
  38. package/dist/cli/install-codex/codex-config-permissions.d.ts +1 -0
  39. package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -0
  40. package/dist/cli/install-codex/codex-config-toml.d.ts +2 -1
  41. package/dist/cli/install-codex/codex-installation-detection.d.ts +36 -0
  42. package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
  43. package/dist/cli/install-codex/codex-package-layout.d.ts +1 -0
  44. package/dist/cli/install-codex/codex-project-local-cleanup-best-effort.d.ts +7 -0
  45. package/dist/cli/install-codex/codex-project-local-cleanup.d.ts +35 -0
  46. package/dist/cli/install-codex/git-bash.d.ts +35 -0
  47. package/dist/cli/install-codex/index.d.ts +4 -0
  48. package/dist/cli/install-codex/toml-section-editor.d.ts +2 -0
  49. package/dist/cli/install-codex/types.d.ts +20 -0
  50. package/dist/cli/run/event-state.d.ts +1 -0
  51. package/dist/cli/run/poll-for-completion.d.ts +1 -0
  52. package/dist/cli/run/prompt-start.d.ts +7 -0
  53. package/dist/cli/star-request.d.ts +9 -0
  54. package/dist/config/schema/hooks.d.ts +0 -1
  55. package/dist/create-hooks.d.ts +0 -1
  56. package/dist/features/background-agent/concurrency.d.ts +1 -0
  57. package/dist/features/background-agent/process-cleanup.d.ts +6 -0
  58. package/dist/features/builtin-skills/skills/debugging.d.ts +2 -0
  59. package/dist/features/builtin-skills/skills/index.d.ts +1 -0
  60. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  61. package/dist/features/opencode-skill-loader/index.d.ts +1 -0
  62. package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
  63. package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
  64. package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
  65. package/dist/hooks/comment-checker/cli.d.ts +1 -0
  66. package/dist/hooks/index.d.ts +0 -1
  67. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  68. package/dist/index.js +1077 -563
  69. package/dist/plugin/hooks/create-core-hooks.d.ts +0 -1
  70. package/dist/plugin/hooks/create-session-hooks.d.ts +1 -2
  71. package/dist/plugin/messages-transform.d.ts +8 -1
  72. package/dist/plugin/user-abort-interrupted-recovery-guard.d.ts +6 -0
  73. package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
  74. package/dist/shared/prompt-async-gate/recent-dispatches.d.ts +14 -0
  75. package/dist/shared/prompt-async-gate/semantic-dedupe.d.ts +7 -0
  76. package/dist/shared/prompt-async-gate/session-idle-dispatch.d.ts +1 -0
  77. package/dist/shared/prompt-async-gate/timing.d.ts +1 -0
  78. package/dist/shared/prompt-async-gate/types.d.ts +2 -0
  79. package/dist/shared/prompt-async-gate.d.ts +1 -1
  80. package/dist/tools/skill/description-formatter.d.ts +5 -1
  81. package/dist/tools/skill/types.d.ts +1 -0
  82. package/package.json +22 -18
  83. package/packages/ast-grep-mcp/dist/cli.js +53 -9
  84. package/packages/git-bash-mcp/dist/cli.js +367 -0
  85. package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
  86. package/packages/omo-codex/plugin/.mcp.json +11 -0
  87. package/packages/omo-codex/plugin/components/comment-checker/README.md +1 -1
  88. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +29 -0
  89. package/packages/omo-codex/plugin/components/git-bash/package.json +23 -0
  90. package/packages/omo-codex/plugin/components/git-bash/src/cli.ts +33 -0
  91. package/packages/omo-codex/plugin/components/git-bash/src/codex-hook.ts +180 -0
  92. package/packages/omo-codex/plugin/components/git-bash/src/index.ts +10 -0
  93. package/packages/omo-codex/plugin/components/git-bash/test/codex-hook.test.ts +195 -0
  94. package/packages/omo-codex/plugin/components/git-bash/tsconfig.build.json +13 -0
  95. package/packages/omo-codex/plugin/components/git-bash/tsconfig.json +25 -0
  96. package/packages/omo-codex/plugin/components/lsp/README.md +1 -1
  97. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +5 -5
  98. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +33 -0
  99. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +19 -27
  100. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-cli.test.ts +28 -0
  101. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-errors.test.ts +55 -0
  102. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +7 -5
  103. package/packages/omo-codex/plugin/components/rules/README.md +1 -1
  104. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
  105. package/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md +10 -0
  106. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  107. package/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +3 -1
  108. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +97 -0
  109. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +6 -5
  110. package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +22 -0
  111. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  112. package/packages/omo-codex/plugin/components/ultrawork/README.md +3 -3
  113. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +4 -1
  114. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +8 -7
  115. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +9 -8
  116. package/packages/omo-codex/plugin/components/ultrawork/directive.md +32 -6
  117. package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +27 -4
  118. package/packages/omo-codex/plugin/components/ultrawork/test/package-smoke.test.ts +25 -0
  119. package/packages/omo-codex/plugin/components/ulw-loop/README.md +1 -1
  120. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +28 -205
  121. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +231 -0
  122. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  123. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  124. package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +102 -5
  125. package/packages/omo-codex/plugin/hooks/hooks.json +35 -2
  126. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  127. package/packages/omo-codex/plugin/package-lock.json +19 -0
  128. package/packages/omo-codex/plugin/package.json +3 -1
  129. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  130. package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +16 -1
  131. package/packages/omo-codex/plugin/scripts/build-components.mjs +2 -1
  132. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  133. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +89 -0
  134. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  135. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  136. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  137. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  138. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  139. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  140. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +33 -8
  141. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +25 -5
  142. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +28 -205
  143. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +231 -0
  144. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  145. package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -20
  146. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  147. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +58 -11
  148. package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +34 -0
  149. package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +21 -0
  150. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  151. package/packages/omo-codex/plugin/test/node-install-surface.test.mjs +48 -0
  152. package/packages/omo-codex/plugin/test/subagent-guidance.test.mjs +76 -0
  153. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +67 -0
  154. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +54 -2
  155. package/packages/omo-codex/scripts/install/cache.mjs +5 -3
  156. package/packages/omo-codex/scripts/install/cli-args.mjs +112 -0
  157. package/packages/omo-codex/scripts/install/config.mjs +23 -1
  158. package/packages/omo-codex/scripts/install/delegated-command.mjs +25 -0
  159. package/packages/omo-codex/scripts/install/git-bash.mjs +99 -0
  160. package/packages/omo-codex/scripts/install/git-bash.test.mjs +174 -0
  161. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  162. package/packages/omo-codex/scripts/install/mcp-runtime-cache.mjs +5 -1
  163. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  164. package/packages/omo-codex/scripts/install/multi-agent-v2-config.mjs +7 -1
  165. package/packages/omo-codex/scripts/install/permissions.d.mts +1 -0
  166. package/packages/omo-codex/scripts/install/permissions.mjs +26 -0
  167. package/packages/omo-codex/scripts/install/project-local-cleanup.mjs +229 -0
  168. package/packages/omo-codex/scripts/install/reasoning-config.mjs +72 -0
  169. package/packages/omo-codex/scripts/install/source-package-build.mjs +20 -0
  170. package/packages/omo-codex/scripts/install/toml-editor.mjs +19 -2
  171. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  172. package/packages/omo-codex/scripts/install-cli-args.test.mjs +146 -0
  173. package/packages/omo-codex/scripts/install-config-autonomous.test.mjs +48 -0
  174. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +141 -0
  175. package/packages/omo-codex/scripts/install-config.test.mjs +205 -0
  176. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +157 -0
  177. package/packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs +145 -0
  178. package/packages/omo-codex/scripts/install-local.mjs +91 -8
  179. package/packages/omo-codex/scripts/install-local.test.mjs +15 -0
  180. package/packages/omo-codex/scripts/install-mcp-runtime.test.mjs +60 -0
  181. package/packages/omo-codex/scripts/install-packaged-local.test.mjs +67 -0
  182. package/packages/omo-codex/scripts/install-project-local-cleanup.test.mjs +277 -0
  183. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  184. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  185. package/packages/shared-skills/skills/review-work/SKILL.md +33 -8
  186. package/packages/shared-skills/skills/start-work/SKILL.md +25 -5
  187. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  188. package/postinstall.mjs +36 -3
  189. package/dist/hooks/context-window-monitor.d.ts +0 -19
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { mkdir, writeFile } from "node:fs/promises";
2
+ import { realpathSync } from "node:fs";
3
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3
4
  import { homedir } from "node:os";
4
- import { join, resolve } from "node:path";
5
+ import { dirname, join, resolve } from "node:path";
5
6
  import { fileURLToPath } from "node:url";
6
7
 
7
8
  import {
@@ -12,6 +13,10 @@ import {
12
13
  } from "./install/cache.mjs";
13
14
  import { linkCachedPluginAgents } from "./install/agents.mjs";
14
15
  import { updateCodexConfig } from "./install/config.mjs";
16
+ import {
17
+ emptyProjectLocalCodexCleanupResult,
18
+ repairNearestProjectLocalCodexArtifacts,
19
+ } from "./install/project-local-cleanup.mjs";
15
20
  import { trustedHookStatesForPlugin } from "./install/hook-trust.mjs";
16
21
  import { defaultRunCommand } from "./install/process.mjs";
17
22
  import { writeInstalledMarketplaceSnapshot } from "./install/snapshot.mjs";
@@ -21,6 +26,10 @@ import {
21
26
  resolvePluginSource,
22
27
  validatePathSegment,
23
28
  } from "./install/marketplace.mjs";
29
+ import { prepareGitBashForInstall, resolveGitBashForCurrentProcess } from "./install/git-bash.mjs";
30
+ import { formatLazyCodexInstallHelp, parseLazyCodexInstallCliArgs } from "./install/cli-args.mjs";
31
+ import { runDelegatedOmoCommand } from "./install/delegated-command.mjs";
32
+ import { shouldBuildSourcePackages } from "./install/source-package-build.mjs";
24
33
 
25
34
  const LEGACY_CODEX_PLUGIN_MARKETPLACE = ["code", "yeongyu", "codex", "plugins"].join("-");
26
35
  const SISYPHUS_LEGACY_CACHE_MARKETPLACES = ["lazycodex", LEGACY_CODEX_PLUGIN_MARKETPLACE];
@@ -41,10 +50,24 @@ export async function installMarketplaceLocally(options = {}) {
41
50
  const env = options.env ?? process.env;
42
51
  const homeDir = resolve(options.homeDir ?? homedir());
43
52
  const codexHome = resolve(options.codexHome ?? nonEmptyEnvValue(env, "CODEX_HOME") ?? join(homeDir, ".codex"));
53
+ const projectDirectory = resolve(options.projectDirectory ?? nonEmptyEnvValue(env, "OMO_CODEX_PROJECT") ?? process.cwd());
44
54
  const binDir = resolve(options.binDir ?? resolveCodexInstallerBinDir({ codexHome, env, homeDir }));
45
55
  const platform = options.platform ?? process.platform;
46
56
  const runCommand = options.runCommand ?? defaultRunCommand;
47
57
  const log = options.log ?? console.log;
58
+ const buildSource = await shouldBuildSourcePackages(repoRoot);
59
+ const gitBashResolution = await prepareGitBashForInstall({
60
+ platform,
61
+ env,
62
+ cwd: repoRoot,
63
+ runCommand,
64
+ resolveGitBash: platform === "win32"
65
+ ? (options.gitBashResolver ?? (() => resolveGitBashForCurrentProcess({ platform, env })))
66
+ : undefined,
67
+ });
68
+ if (!gitBashResolution.found) {
69
+ throw new Error(gitBashResolution.installHint);
70
+ }
48
71
  const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
49
72
  const marketplace = await readMarketplace(repoRoot, {
50
73
  marketplacePath: join(codexPackageRoot, "marketplace.json"),
@@ -66,6 +89,7 @@ export async function installMarketplaceLocally(options = {}) {
66
89
 
67
90
  log(`Building ${entry.name}@${version}`);
68
91
  const plugin = await installCachedPlugin({
92
+ buildSource,
69
93
  codexHome,
70
94
  marketplaceName: marketplace.name,
71
95
  name: entry.name,
@@ -120,15 +144,38 @@ export async function installMarketplaceLocally(options = {}) {
120
144
  marketplaceName: marketplace.name,
121
145
  marketplaceSource: { sourceType: "local", source: marketplaceRoot },
122
146
  pluginNames,
147
+ platform,
123
148
  trustedHookStates,
124
149
  agentConfigs: [...agentConfigs.values()].sort((left, right) => left.name.localeCompare(right.name)),
150
+ autonomousPermissions: options.autonomousPermissions !== false,
125
151
  });
152
+ const projectCleanup = await repairProjectLocalCodexArtifactsBestEffort({ startDirectory: projectDirectory, codexHome, log });
153
+ for (const configCleanup of projectCleanup.configs) {
154
+ if (!configCleanup.changed) continue;
155
+ log(`Repaired project Codex config ${configCleanup.configPath} (backup: ${configCleanup.backupPath})`);
156
+ }
157
+ for (const artifact of projectCleanup.artifacts) {
158
+ log(`Found project-local legacy artifact ${artifact.path}; left in place`);
159
+ }
126
160
 
127
161
  for (const plugin of installed) {
128
162
  log(`Installed ${plugin.name}@${marketplace.name} -> ${plugin.path}`);
129
163
  }
130
164
 
131
- return { marketplaceName: marketplace.name, installed };
165
+ return { marketplaceName: marketplace.name, installed, gitBashPath: gitBashResolution.path, projectCleanup };
166
+ }
167
+
168
+ async function repairProjectLocalCodexArtifactsBestEffort({ startDirectory, codexHome, log }) {
169
+ try {
170
+ return await repairNearestProjectLocalCodexArtifacts({ startDirectory, codexHome });
171
+ } catch (error) {
172
+ log(`Skipped project-local Codex cleanup: ${formatUnknownError(error)}`);
173
+ return emptyProjectLocalCodexCleanupResult();
174
+ }
175
+ }
176
+
177
+ function formatUnknownError(error) {
178
+ return error instanceof Error ? error.message : String(error);
132
179
  }
133
180
 
134
181
  function agentNameFromToml(fileName) {
@@ -170,21 +217,57 @@ function nonEmptyEnvValue(env, key) {
170
217
  const value = env[key];
171
218
  if (typeof value !== "string") return undefined;
172
219
  const trimmed = value.trim();
173
- return trimmed.length === 0 ? undefined : value;
220
+ return trimmed.length === 0 ? undefined : trimmed;
174
221
  }
175
222
 
176
223
  function legacyCacheMarketplaces(marketplaceName) {
177
224
  return marketplaceName === "sisyphuslabs" ? SISYPHUS_LEGACY_CACHE_MARKETPLACES : [];
178
225
  }
179
226
 
227
+ export function resolveDefaultRepoRoot() {
228
+ return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
229
+ }
230
+
180
231
  async function main() {
181
- const repoRoot = process.argv[2] ? resolve(process.argv[2]) : process.cwd();
182
- const result = await installMarketplaceLocally({ repoRoot });
232
+ const parsed = parseLazyCodexInstallCliArgs(process.argv.slice(2));
233
+ if (parsed.kind === "help") {
234
+ console.log(formatLazyCodexInstallHelp());
235
+ return;
236
+ }
237
+ if (parsed.kind === "version") {
238
+ const packageJson = JSON.parse(await readFile(join(resolveDefaultRepoRoot(), "package.json"), "utf8"));
239
+ const version = typeof packageJson.version === "string" ? packageJson.version : "unknown";
240
+ console.log(`lazycodex-ai ${version}`);
241
+ return;
242
+ }
243
+ if (parsed.kind === "command") {
244
+ await runDelegatedOmoCommand(parsed, { cwd: process.cwd(), log: console.log, runCommand: defaultRunCommand });
245
+ return;
246
+ }
247
+
248
+ const repoRoot = parsed.repoRoot ? resolve(parsed.repoRoot) : resolveDefaultRepoRoot();
249
+ const result = await installMarketplaceLocally({
250
+ repoRoot,
251
+ autonomousPermissions: parsed.autonomousPermissions,
252
+ });
183
253
  console.log(`Installed ${result.installed.length} plugin(s) from ${result.marketplaceName}.`);
184
254
  }
185
255
 
186
- const invokedPath = process.argv[1] ? resolve(process.argv[1]) : "";
187
- if (invokedPath === fileURLToPath(import.meta.url)) {
256
+ function resolveEntrypointPath(path) {
257
+ try {
258
+ return realpathSync(resolve(path));
259
+ } catch (error) {
260
+ if (error instanceof Error) return resolve(path);
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ function isEntrypointInvocation(invokedPath) {
266
+ if (!invokedPath) return false;
267
+ return resolveEntrypointPath(invokedPath) === resolveEntrypointPath(fileURLToPath(import.meta.url));
268
+ }
269
+
270
+ if (isEntrypointInvocation(process.argv[1] ?? "")) {
188
271
  main().catch((error) => {
189
272
  console.error(error instanceof Error ? error.message : error);
190
273
  process.exitCode = 1;
@@ -67,6 +67,21 @@ test("#given explicit CODEX_LOCAL_BIN_DIR #when resolving local installer bin di
67
67
  );
68
68
  });
69
69
 
70
+ test("#given CODEX_LOCAL_BIN_DIR with surrounding whitespace #when resolving local installer bin dir #then trims the env value before use", () => {
71
+ const homeDir = join(tmpdir(), "omo-codex-home-trim");
72
+ const codexHome = join(tmpdir(), "omo-codex-install-trim");
73
+ const explicitBinDir = join(tmpdir(), "omo-codex-trim-bin");
74
+
75
+ assert.equal(
76
+ resolveCodexInstallerBinDir({
77
+ codexHome,
78
+ env: { CODEX_LOCAL_BIN_DIR: ` ${explicitBinDir} ` },
79
+ homeDir,
80
+ }),
81
+ explicitBinDir,
82
+ );
83
+ });
84
+
70
85
  test("#given omo plugin source #when inspecting identity #then uses sisyphuslabs omo metadata", async () => {
71
86
  const pluginRoot = join(scriptDir, "..", "plugin");
72
87
 
@@ -12,6 +12,7 @@ test("#given external MCP package runtime #when installing cached plugin #then r
12
12
  const codexHome = await makeTempDir();
13
13
  const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
14
14
  const astGrepPackageRoot = join(repoRoot, "packages", "ast-grep-mcp");
15
+ const gitBashPackageRoot = join(repoRoot, "packages", "git-bash-mcp");
15
16
  const lspPackageRoot = join(repoRoot, "packages", "lsp-tools-mcp");
16
17
 
17
18
  await writeJson(join(astGrepPackageRoot, "package.json"), {
@@ -26,6 +27,12 @@ test("#given external MCP package runtime #when installing cached plugin #then r
26
27
  type: "module",
27
28
  bin: { "omo-lsp": "./dist/cli.js" },
28
29
  });
30
+ await writeJson(join(gitBashPackageRoot, "package.json"), {
31
+ name: "@example/git-bash-mcp",
32
+ version: "0.1.0",
33
+ type: "module",
34
+ bin: { "omo-git-bash": "./dist/cli.js" },
35
+ });
29
36
  await writeJson(join(sourceRoot, "package.json"), {
30
37
  name: "@example/omo",
31
38
  version: "0.1.0",
@@ -37,6 +44,11 @@ test("#given external MCP package runtime #when installing cached plugin #then r
37
44
  args: ["../../ast-grep-mcp/dist/cli.js", "mcp"],
38
45
  cwd: ".",
39
46
  },
47
+ git_bash: {
48
+ command: "node",
49
+ args: ["../../git-bash-mcp/dist/cli.js", "mcp"],
50
+ cwd: ".",
51
+ },
40
52
  lsp: {
41
53
  command: "node",
42
54
  args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
@@ -45,6 +57,7 @@ test("#given external MCP package runtime #when installing cached plugin #then r
45
57
  },
46
58
  });
47
59
  await writeJson(join(astGrepPackageRoot, "dist", "cli.js"), { executable: true });
60
+ await writeJson(join(gitBashPackageRoot, "dist", "cli.js"), { executable: true });
48
61
  await writeJson(join(lspPackageRoot, "dist", "cli.js"), { executable: true });
49
62
  await writeJson(join(lspPackageRoot, "dist", "lsp", "manager.js"), { copied: true });
50
63
 
@@ -59,13 +72,17 @@ test("#given external MCP package runtime #when installing cached plugin #then r
59
72
 
60
73
  const cachedMcp = JSON.parse(await readFile(join(result.path, ".mcp.json"), "utf8"));
61
74
  const copiedAstGrepCli = join(result.path, "mcp", "ast_grep", "dist", "cli.js");
75
+ const copiedGitBashCli = join(result.path, "mcp", "git_bash", "dist", "cli.js");
62
76
  const copiedCli = join(result.path, "mcp", "lsp", "dist", "cli.js");
63
77
 
64
78
  assert.deepEqual(cachedMcp.mcpServers.ast_grep.args, [copiedAstGrepCli, "mcp"]);
79
+ assert.deepEqual(cachedMcp.mcpServers.git_bash.args, [copiedGitBashCli, "mcp"]);
65
80
  assert.deepEqual(cachedMcp.mcpServers.lsp.args, [copiedCli, "mcp"]);
66
81
  assert.equal(Object.hasOwn(cachedMcp.mcpServers.ast_grep, "cwd"), false);
82
+ assert.equal(Object.hasOwn(cachedMcp.mcpServers.git_bash, "cwd"), false);
67
83
  assert.equal(Object.hasOwn(cachedMcp.mcpServers.lsp, "cwd"), false);
68
84
  assert.equal((await stat(copiedAstGrepCli)).isFile(), true);
85
+ assert.equal((await stat(copiedGitBashCli)).isFile(), true);
69
86
  assert.equal((await stat(copiedCli)).isFile(), true);
70
87
  assert.equal((await stat(join(result.path, "mcp", "lsp", "dist", "lsp", "manager.js"))).isFile(), true);
71
88
  });
@@ -171,3 +188,46 @@ test("#given structurally valid external MCP package without mcp suffix #when in
171
188
  assert.deepEqual(cachedMcp.mcpServers.language_tools.args, [copiedCli, "mcp", join(sourceRoot, "..", "local-config.json")]);
172
189
  assert.equal((await stat(copiedCli)).isFile(), true);
173
190
  });
191
+
192
+ test("#given packaged external MCP runtime has only dist files #when installing cached plugin #then runtime is copied into the plugin cache", async () => {
193
+ // given
194
+ const repoRoot = await makeTempDir();
195
+ const codexHome = await makeTempDir();
196
+ const sourceRoot = join(repoRoot, "packages", "omo-codex", "plugin");
197
+ const lspPackageRoot = join(repoRoot, "packages", "lsp-tools-mcp");
198
+
199
+ await writeJson(join(sourceRoot, "package.json"), {
200
+ name: "@example/omo",
201
+ version: "0.1.0",
202
+ });
203
+ await writeJson(join(sourceRoot, ".mcp.json"), {
204
+ mcpServers: {
205
+ lsp: {
206
+ command: "node",
207
+ args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
208
+ cwd: ".",
209
+ },
210
+ },
211
+ });
212
+ await writeJson(join(lspPackageRoot, "dist", "cli.js"), { executable: true });
213
+ await writeJson(join(lspPackageRoot, "dist", "lsp", "manager.js"), { copied: true });
214
+
215
+ // when
216
+ const result = await installCachedPlugin({
217
+ codexHome,
218
+ marketplaceName: "sisyphuslabs",
219
+ name: "omo",
220
+ runCommand: async () => {},
221
+ sourcePath: sourceRoot,
222
+ version: "0.1.0",
223
+ });
224
+
225
+ // then
226
+ const cachedMcp = JSON.parse(await readFile(join(result.path, ".mcp.json"), "utf8"));
227
+ const copiedCli = join(result.path, "mcp", "lsp", "dist", "cli.js");
228
+ assert.deepEqual(cachedMcp.mcpServers.lsp.args, [copiedCli, "mcp"]);
229
+ assert.equal(Object.hasOwn(cachedMcp.mcpServers.lsp, "cwd"), false);
230
+ assert.equal((await stat(copiedCli)).isFile(), true);
231
+ assert.equal((await stat(join(result.path, "mcp", "lsp", "dist", "lsp", "manager.js"))).isFile(), true);
232
+ assert.notEqual(cachedMcp.mcpServers.lsp.args[0], join(lspPackageRoot, "dist", "cli.js"));
233
+ });
@@ -0,0 +1,67 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ import { installMarketplaceLocally } from "./install-local.mjs";
7
+ import { makeTempDir, writeJson, writePluginAt } from "./install-test-fixtures.mjs";
8
+
9
+ test("#given packaged lazycodex adapter #when installing locally #then uses bundled artifacts without source builds", async () => {
10
+ // given
11
+ const repoRoot = await makeTempDir();
12
+ const codexHome = await makeTempDir();
13
+ const binDir = await makeTempDir();
14
+ const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
15
+ const pluginRoot = join(codexPackageRoot, "plugin");
16
+ const lspRuntimeRoot = join(repoRoot, "packages", "lsp-tools-mcp");
17
+
18
+ await writeJson(join(repoRoot, "package.json"), {
19
+ name: "lazycodex-ai",
20
+ version: "0.1.2",
21
+ });
22
+ await writeJson(join(codexPackageRoot, "marketplace.json"), {
23
+ name: "sisyphuslabs",
24
+ plugins: [{ name: "omo", source: "./plugins/omo" }],
25
+ });
26
+ await writePluginAt(pluginRoot, "omo", "0.1.0");
27
+ await writeJson(join(pluginRoot, ".mcp.json"), {
28
+ mcpServers: {
29
+ lsp: {
30
+ command: "node",
31
+ args: ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
32
+ cwd: ".",
33
+ },
34
+ },
35
+ });
36
+ await mkdir(join(pluginRoot, "dist"), { recursive: true });
37
+ await writeFile(join(pluginRoot, "dist", "cli.js"), "#!/usr/bin/env node\nconsole.log('prebuilt')\n");
38
+ await writeJson(join(lspRuntimeRoot, "dist", "cli.js"), { executable: true });
39
+
40
+ const commands = [];
41
+
42
+ // when
43
+ const result = await installMarketplaceLocally({
44
+ repoRoot,
45
+ codexHome,
46
+ binDir,
47
+ platform: "linux",
48
+ runCommand: async (command, args, options) => {
49
+ commands.push([command, args.join(" "), options.cwd]);
50
+ },
51
+ log: () => {},
52
+ });
53
+
54
+ // then
55
+ const pluginCacheRoot = join(codexHome, "plugins", "cache", "sisyphuslabs", "omo", "0.1.0");
56
+ const cachedMcp = JSON.parse(await readFile(join(pluginCacheRoot, ".mcp.json"), "utf8"));
57
+ const cachedLspCli = join(pluginCacheRoot, "mcp", "lsp", "dist", "cli.js");
58
+
59
+ assert.deepEqual(result.installed.map((plugin) => `${plugin.name}@${plugin.version}`), ["omo@0.1.0"]);
60
+ assert.deepEqual(
61
+ commands.map(([command, args, cwd]) => [command, args, cwd]),
62
+ [["npm", "install --omit=dev", pluginCacheRoot]],
63
+ );
64
+ assert.deepEqual(cachedMcp.mcpServers.lsp.args, [cachedLspCli, "mcp"]);
65
+ assert.equal((await stat(cachedLspCli)).isFile(), true);
66
+ assert.notEqual(cachedMcp.mcpServers.lsp.args[0], join(lspRuntimeRoot, "dist", "cli.js"));
67
+ });
@@ -0,0 +1,277 @@
1
+ import assert from "node:assert/strict";
2
+ import { lstat, mkdir, readFile, symlink, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+
6
+ import { installMarketplaceLocally } from "./install-local.mjs";
7
+ import { repairNearestProjectLocalCodexArtifacts } from "./install/project-local-cleanup.mjs";
8
+ import { makeTempDir, writeJson, writePluginAt } from "./install-test-fixtures.mjs";
9
+
10
+ test("#given stale project-local Codex config #when Node installer runs #then repairs the local conflict", async () => {
11
+ const repoRoot = await makeTempDir();
12
+ const codexHome = await makeTempDir();
13
+ const binDir = await makeTempDir();
14
+ const projectRoot = await makeTempDir();
15
+ const projectDirectory = join(projectRoot, "nested");
16
+ const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
17
+ const pluginRoot = join(codexPackageRoot, "plugin");
18
+ const projectConfigPath = join(projectRoot, ".codex", "config.toml");
19
+
20
+ await mkdir(projectDirectory, { recursive: true });
21
+ await mkdir(join(projectRoot, ".git"), { recursive: true });
22
+ await mkdir(join(projectRoot, ".codex"), { recursive: true });
23
+ await writeJson(join(codexPackageRoot, "marketplace.json"), {
24
+ name: "debug-marketplace",
25
+ plugins: [{ name: "alpha", source: "./plugin" }],
26
+ });
27
+ await writePluginAt(pluginRoot, "alpha", "1.2.3");
28
+ await writeFile(
29
+ projectConfigPath,
30
+ [
31
+ "[features.multi_agent_v2]",
32
+ "enabled = true",
33
+ "",
34
+ "[agents]",
35
+ " max_threads = 10",
36
+ "max_depth = 4",
37
+ "",
38
+ ].join("\n"),
39
+ );
40
+
41
+ const result = await installMarketplaceLocally({
42
+ repoRoot,
43
+ codexHome,
44
+ binDir,
45
+ projectDirectory,
46
+ platform: "linux",
47
+ runCommand: async (command, args, options) => {
48
+ if (command === "npm" && args.join(" ") === "run build") {
49
+ await mkdir(join(options.cwd, "dist"), { recursive: true });
50
+ await writeFile(join(options.cwd, "dist", "cli.js"), "#!/usr/bin/env node\n");
51
+ }
52
+ },
53
+ log: () => {},
54
+ });
55
+
56
+ assert.equal(result.projectCleanup.configPath, projectConfigPath);
57
+ assert.equal(result.projectCleanup.changed, true);
58
+ assert.deepEqual(result.projectCleanup.removedKeys, ["max_threads"]);
59
+ assert.equal(result.projectCleanup.configs.length, 1);
60
+ assert.match(await readFile(result.projectCleanup.backupPath, "utf8"), /max_threads = 10/);
61
+ const content = await readFile(projectConfigPath, "utf8");
62
+ assert.doesNotMatch(content, /^\s*max_threads\s*=/m);
63
+ assert.match(content, /max_depth = 4/);
64
+ });
65
+
66
+ test("#given root and nested project-local Codex configs #when script cleanup runs #then it repairs every loaded project layer", async () => {
67
+ const projectRoot = await makeTempDir();
68
+ const projectDirectory = join(projectRoot, "nested");
69
+ const rootConfigPath = join(projectRoot, ".codex", "config.toml");
70
+ const nestedConfigPath = join(projectDirectory, ".codex", "config.toml");
71
+
72
+ await mkdir(join(projectRoot, ".git"), { recursive: true });
73
+ await mkdir(join(projectRoot, ".codex"), { recursive: true });
74
+ await mkdir(join(projectDirectory, ".codex"), { recursive: true });
75
+ await mkdir(join(projectDirectory, ".omx"), { recursive: true });
76
+ await writeFile(join(projectDirectory, ".codex", "hooks.json"), "{}\n");
77
+ await writeFile(
78
+ rootConfigPath,
79
+ [
80
+ "[features.multi_agent_v2]",
81
+ "enabled = true",
82
+ "",
83
+ "[agents]",
84
+ "max_threads = 10",
85
+ "max_depth = 4",
86
+ "",
87
+ ].join("\n"),
88
+ );
89
+ await writeFile(
90
+ nestedConfigPath,
91
+ [
92
+ "[features.multi_agent_v2]",
93
+ "enabled = true",
94
+ "",
95
+ "[agents]",
96
+ "job_max_runtime_seconds = 7200",
97
+ "",
98
+ ].join("\n"),
99
+ );
100
+
101
+ const result = await repairNearestProjectLocalCodexArtifacts({
102
+ startDirectory: projectDirectory,
103
+ now: () => new Date("2026-06-01T01:02:03.004Z"),
104
+ });
105
+
106
+ assert.equal(result.projectRoot, projectRoot);
107
+ assert.equal(result.changed, true);
108
+ assert.equal(result.configPath, rootConfigPath);
109
+ assert.deepEqual(
110
+ result.configs.map((config) => config.configPath),
111
+ [rootConfigPath, nestedConfigPath],
112
+ );
113
+ assert.deepEqual(
114
+ result.configs.map((config) => config.changed),
115
+ [true, false],
116
+ );
117
+ assert.equal(result.backupPath, `${rootConfigPath}.backup-2026-06-01T01-02-03-004Z`);
118
+ const rootContent = await readFile(rootConfigPath, "utf8");
119
+ const nestedContent = await readFile(nestedConfigPath, "utf8");
120
+ assert.doesNotMatch(rootContent, /^max_threads\s*=/m);
121
+ assert.match(rootContent, /max_depth = 4/);
122
+ assert.match(nestedContent, /job_max_runtime_seconds = 7200/);
123
+ assert.deepEqual(
124
+ result.artifacts.map((artifact) => artifact.path).sort(),
125
+ [join(projectDirectory, ".codex", "hooks.json"), join(projectDirectory, ".omx")],
126
+ );
127
+ });
128
+
129
+ test("#given no project-local config and CODEX_HOME in the parent chain #when script cleanup runs #then global Codex config is not treated as project state", async () => {
130
+ const homeRoot = await makeTempDir();
131
+ const codexHome = join(homeRoot, ".codex");
132
+ const projectDirectory = join(homeRoot, "workspace", "nested");
133
+ const globalConfigPath = join(codexHome, "config.toml");
134
+
135
+ await mkdir(projectDirectory, { recursive: true });
136
+ await mkdir(codexHome, { recursive: true });
137
+ await writeFile(
138
+ globalConfigPath,
139
+ [
140
+ "[features.multi_agent_v2]",
141
+ "enabled = true",
142
+ "",
143
+ "[agents]",
144
+ "max_threads = 10",
145
+ "max_depth = 4",
146
+ "",
147
+ ].join("\n"),
148
+ );
149
+
150
+ const result = await repairNearestProjectLocalCodexArtifacts({ startDirectory: projectDirectory, codexHome });
151
+
152
+ assert.equal(result.configPath, null);
153
+ assert.equal(result.changed, false);
154
+ const content = await readFile(globalConfigPath, "utf8");
155
+ assert.match(content, /max_threads = 10/);
156
+ assert.match(content, /max_depth = 4/);
157
+ });
158
+
159
+ test("#given project-local config is a symlink to CODEX_HOME #when script cleanup runs #then it skips the link without mutating the target", async () => {
160
+ if (process.platform === "win32") return;
161
+
162
+ const codexHome = await makeTempDir();
163
+ const projectRoot = await makeTempDir();
164
+ const globalConfigPath = join(codexHome, "config.toml");
165
+ const projectConfigPath = join(projectRoot, ".codex", "config.toml");
166
+
167
+ await mkdir(join(projectRoot, ".git"), { recursive: true });
168
+ await mkdir(join(projectRoot, ".codex"), { recursive: true });
169
+ await writeFile(
170
+ globalConfigPath,
171
+ [
172
+ "[features.multi_agent_v2]",
173
+ "enabled = true",
174
+ "",
175
+ "[agents]",
176
+ "max_threads = 10",
177
+ "max_depth = 4",
178
+ "",
179
+ ].join("\n"),
180
+ );
181
+ await symlink(globalConfigPath, projectConfigPath);
182
+
183
+ const result = await repairNearestProjectLocalCodexArtifacts({
184
+ startDirectory: projectRoot,
185
+ codexHome,
186
+ now: () => new Date("2026-06-01T00:00:00Z"),
187
+ });
188
+
189
+ assert.equal(result.configPath, null);
190
+ assert.equal(result.changed, false);
191
+ assert.equal(await pathExists(`${projectConfigPath}.backup-2026-06-01T00-00-00-000Z`), false);
192
+ const content = await readFile(globalConfigPath, "utf8");
193
+ assert.match(content, /max_threads = 10/);
194
+ assert.match(content, /max_depth = 4/);
195
+ });
196
+
197
+ test("#given malformed project directory from the environment #when script cleanup runs #then it skips project-local cleanup without failing install", async () => {
198
+ const codexHome = await makeTempDir();
199
+
200
+ const result = await repairNearestProjectLocalCodexArtifacts({
201
+ startDirectory: `bad${"\0"}path`,
202
+ codexHome,
203
+ });
204
+
205
+ assert.deepEqual(result, {
206
+ projectRoot: null,
207
+ configPath: null,
208
+ changed: false,
209
+ removedKeys: [],
210
+ configs: [],
211
+ artifacts: [],
212
+ });
213
+ });
214
+
215
+ test("#given absent project directory from the script surface #when script cleanup runs #then it skips project-local cleanup without throwing", async () => {
216
+ const codexHome = await makeTempDir();
217
+
218
+ const result = await repairNearestProjectLocalCodexArtifacts({ codexHome });
219
+
220
+ assert.deepEqual(result, {
221
+ projectRoot: null,
222
+ configPath: null,
223
+ changed: false,
224
+ removedKeys: [],
225
+ configs: [],
226
+ artifacts: [],
227
+ });
228
+ });
229
+
230
+ test("#given project cleanup hits a filesystem edge #when Node installer runs #then install succeeds and reports skipped cleanup", async () => {
231
+ const repoRoot = await makeTempDir();
232
+ const codexHome = await makeTempDir();
233
+ const binDir = await makeTempDir();
234
+ const projectRoot = await makeTempDir();
235
+ const projectDirectory = join(projectRoot, "not-a-directory");
236
+ const codexPackageRoot = join(repoRoot, "packages", "omo-codex");
237
+ const pluginRoot = join(codexPackageRoot, "plugin");
238
+ const logs = [];
239
+
240
+ await writeJson(join(codexPackageRoot, "marketplace.json"), {
241
+ name: "debug-marketplace",
242
+ plugins: [{ name: "alpha", source: "./plugin" }],
243
+ });
244
+ await writePluginAt(pluginRoot, "alpha", "1.2.3");
245
+ await writeFile(projectDirectory, "file, not directory\n");
246
+
247
+ const result = await installMarketplaceLocally({
248
+ repoRoot,
249
+ codexHome,
250
+ binDir,
251
+ projectDirectory,
252
+ platform: "linux",
253
+ runCommand: async (command, args, options) => {
254
+ if (command === "npm" && args.join(" ") === "run build") {
255
+ await mkdir(join(options.cwd, "dist"), { recursive: true });
256
+ await writeFile(join(options.cwd, "dist", "cli.js"), "#!/usr/bin/env node\n");
257
+ }
258
+ },
259
+ log: (message) => logs.push(message),
260
+ });
261
+
262
+ assert.equal(result.projectCleanup.projectRoot, null);
263
+ assert.equal(result.projectCleanup.changed, false);
264
+ assert.equal(logs.some((message) => message.includes("Skipped project-local Codex cleanup")), true);
265
+ assert.equal(logs.some((message) => message.includes("not a directory")), true);
266
+ assert.equal(await pathExists(join(codexHome, "config.toml")), true);
267
+ });
268
+
269
+ async function pathExists(path) {
270
+ try {
271
+ await lstat(path);
272
+ return true;
273
+ } catch (error) {
274
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
275
+ throw error;
276
+ }
277
+ }