oh-my-opencode 4.6.0 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/bin/version-mismatch.js +47 -0
  2. package/bin/version-mismatch.test.ts +120 -0
  3. package/dist/cli/codex-ulw-loop.d.ts +12 -0
  4. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
  5. package/dist/cli/index.js +5999 -5542
  6. package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -1
  7. package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
  8. package/dist/features/background-agent/concurrency.d.ts +1 -0
  9. package/dist/features/background-agent/process-cleanup.d.ts +6 -0
  10. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  11. package/dist/features/opencode-skill-loader/index.d.ts +1 -0
  12. package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
  13. package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
  14. package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
  15. package/dist/hooks/comment-checker/cli.d.ts +1 -0
  16. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  17. package/dist/index.js +4250 -3776
  18. package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
  19. package/dist/tools/skill/description-formatter.d.ts +5 -1
  20. package/dist/tools/skill/types.d.ts +1 -0
  21. package/package.json +13 -14
  22. package/packages/ast-grep-mcp/dist/cli.js +53 -9
  23. package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
  24. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +13 -0
  25. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +6 -2
  26. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +13 -2
  27. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +30 -79
  28. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +116 -0
  29. package/packages/omo-codex/plugin/components/lsp/src/mutated-file-paths.ts +88 -0
  30. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +206 -0
  31. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +5 -3
  32. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
  33. package/packages/omo-codex/plugin/components/rules/src/codex-hook-options.ts +1 -0
  34. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  35. package/packages/omo-codex/plugin/components/rules/src/rules/finder.ts +15 -2
  36. package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +4 -1
  37. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +28 -5
  38. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  39. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  40. package/packages/omo-codex/plugin/components/ultrawork/README.md +1 -1
  41. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +3 -1
  42. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
  43. package/packages/omo-codex/plugin/components/ultrawork/directive.md +1 -1
  44. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +5 -4
  45. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -3
  46. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  47. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  48. package/packages/omo-codex/plugin/hooks/hooks.json +24 -2
  49. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  50. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  51. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  52. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +4 -9
  53. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  54. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  55. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  56. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  57. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  58. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  59. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -7
  60. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -6
  61. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +5 -4
  62. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -3
  63. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  64. package/packages/omo-codex/plugin/test/aggregate.test.mjs +188 -19
  65. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  66. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +7 -27
  67. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  68. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +27 -1
  69. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +22 -0
  70. package/packages/omo-codex/scripts/install/cli-args.mjs +1 -1
  71. package/packages/omo-codex/scripts/install/config.mjs +2 -15
  72. package/packages/omo-codex/scripts/install/delegated-command.mjs +1 -1
  73. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  74. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  75. package/packages/omo-codex/scripts/install/permissions.mjs +11 -0
  76. package/packages/omo-codex/scripts/install/reasoning-config.mjs +65 -7
  77. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  78. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +83 -0
  79. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +82 -3
  80. package/packages/omo-codex/scripts/install-config.test.mjs +5 -6
  81. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +30 -2
  82. package/packages/omo-codex/scripts/install-local.mjs +1 -1
  83. package/packages/omo-codex/scripts/install-local.test.mjs +3 -1
  84. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  85. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  86. package/packages/shared-skills/skills/review-work/SKILL.md +7 -7
  87. package/packages/shared-skills/skills/start-work/SKILL.md +6 -6
  88. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  89. package/postinstall.mjs +36 -3
  90. package/dist/cli/install-codex/codex-config-mcp.d.ts +0 -1
@@ -8,6 +8,8 @@ export interface ExecuteHookOptions {
8
8
  zshPath?: string;
9
9
  /** Timeout in milliseconds. Process is killed after this. Default: 30000 */
10
10
  timeoutMs?: number;
11
+ /** Grace period before force-killing and resolving timed-out commands. Default: 5000 */
12
+ killGraceMs?: number;
11
13
  /** When provided, scrub process.env to only include these vars plus HOME/PATH/etc. Used for plugin-sourced hooks. */
12
14
  allowedEnvVars?: string[];
13
15
  }
@@ -1,3 +1,7 @@
1
1
  import type { SkillInfo } from "./types";
2
2
  import type { CommandInfo } from "../slashcommand/types";
3
- export declare function formatCombinedDescription(skills?: SkillInfo[], commands?: CommandInfo[]): string;
3
+ interface CombinedDescriptionOptions {
4
+ includeSkills?: boolean;
5
+ }
6
+ export declare function formatCombinedDescription(skills?: SkillInfo[], commands?: CommandInfo[], options?: CombinedDescriptionOptions): string;
7
+ export {};
@@ -66,4 +66,5 @@ export interface SkillLoadOptions {
66
66
  } | undefined>;
67
67
  dirs(): string[] | Promise<string[]>;
68
68
  };
69
+ includeSkillsInDescription?: boolean;
69
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "4.6.0",
3
+ "version": "4.7.1",
4
4
  "description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
5
5
  "main": "./dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -71,7 +71,7 @@
71
71
  "typecheck:packages": "tsgo --noEmit -p packages/rules-engine/tsconfig.json && tsgo --noEmit -p packages/ast-grep-core/tsconfig.json && tsgo --noEmit -p packages/ast-grep-mcp/tsconfig.json && tsgo --noEmit -p packages/git-bash-mcp/tsconfig.json && tsgo --noEmit -p packages/utils/tsconfig.json && tsgo --noEmit -p packages/model-core/tsconfig.json && tsgo --noEmit -p packages/prompts-core/tsconfig.json && tsgo --noEmit -p packages/comment-checker-core/tsconfig.json && tsgo --noEmit -p packages/hashline-core/tsconfig.json && tsgo --noEmit -p packages/boulder-state/tsconfig.json && tsgo --noEmit -p packages/agents-md-core/tsconfig.json && tsgo --noEmit -p packages/omo-codex/tsconfig.json",
72
72
  "typecheck:script": "tsgo --noEmit -p script/tsconfig.json",
73
73
  "test": "bun test",
74
- "test:codex": "bun run build:ast-grep-mcp && bun run build:lsp-tools-mcp && npm --prefix packages/omo-codex/plugin ci && bun run --cwd packages/omo-codex/plugin build && bun test src/cli/cli-installer.platform.test.ts src/cli/install-codex/codex-cache.test.ts src/cli/install-codex/codex-cleanup.test.ts src/cli/install-codex/codex-config-agent-cleanup.test.ts src/cli/install-codex/codex-config-reasoning.test.ts src/cli/install-codex/codex-config-toml.test.ts src/cli/install-codex/codex-project-local-cleanup.test.ts src/cli/install-codex/install-codex-project-local-cleanup.test.ts src/cli/install-codex/install-codex.test.ts src/cli/install-codex/install-codex-packaged.test.ts src/cli/install-codex/link-cached-plugin-agents.test.ts packages/omo-codex/src/**/*.test.ts packages/utils/src/jsonc-parser.test.ts packages/utils/src/frontmatter.test.ts packages/hashline-core/src/hash-computation.test.ts packages/hashline-core/src/smoke-untested-modules.test.ts packages/rules-engine/src/index.test.ts packages/rules-engine/src/security-boundary.test.ts packages/agents-md-core/src/injector.test.ts packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts && node --test packages/omo-codex/plugin/test/*.test.mjs packages/omo-codex/scripts/install-cache-copy.test.mjs packages/omo-codex/scripts/install-cli-args.test.mjs packages/omo-codex/scripts/install-config-autonomous.test.mjs packages/omo-codex/scripts/install-config-reasoning.test.mjs packages/omo-codex/scripts/install-config.test.mjs packages/omo-codex/scripts/install-project-local-cleanup.test.mjs packages/omo-codex/scripts/install-local-entrypoint.test.mjs packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs packages/omo-codex/scripts/install-local.test.mjs packages/omo-codex/scripts/install-mcp-runtime.test.mjs packages/omo-codex/scripts/install-packaged-local.test.mjs packages/omo-codex/scripts/install/git-bash.test.mjs packages/omo-codex/scripts/install-agent-links.test.mjs packages/omo-codex/scripts/install-bin-links.test.mjs packages/omo-codex/scripts/sync-telemetry-component.test.mjs",
74
+ "test:codex": "bun run build:ast-grep-mcp && bun run build:lsp-tools-mcp && npm --prefix packages/omo-codex/plugin ci && bun run --cwd packages/omo-codex/plugin build && bun test src/cli/cli-installer.platform.test.ts src/cli/install-codex/codex-cache.test.ts src/cli/install-codex/codex-cleanup.test.ts src/cli/install-codex/codex-config-agent-cleanup.test.ts src/cli/install-codex/codex-config-autonomous-features.test.ts src/cli/install-codex/codex-config-reasoning.test.ts src/cli/install-codex/codex-config-toml.test.ts src/cli/install-codex/codex-project-local-cleanup.test.ts src/cli/install-codex/install-codex-project-local-cleanup.test.ts src/cli/install-codex/install-codex.test.ts src/cli/install-codex/install-codex-packaged.test.ts src/cli/install-codex/link-cached-plugin-agents.test.ts packages/omo-codex/src/**/*.test.ts packages/utils/src/jsonc-parser.test.ts packages/utils/src/frontmatter.test.ts packages/hashline-core/src/hash-computation.test.ts packages/hashline-core/src/smoke-untested-modules.test.ts packages/rules-engine/src/index.test.ts packages/rules-engine/src/security-boundary.test.ts packages/agents-md-core/src/injector.test.ts packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts && node --test packages/omo-codex/plugin/test/*.test.mjs packages/omo-codex/scripts/install-cache-copy.test.mjs packages/omo-codex/scripts/install-cli-args.test.mjs packages/omo-codex/scripts/install-config-autonomous-features.test.mjs packages/omo-codex/scripts/install-config-autonomous.test.mjs packages/omo-codex/scripts/install-config-reasoning.test.mjs packages/omo-codex/scripts/install-config.test.mjs packages/omo-codex/scripts/install-project-local-cleanup.test.mjs packages/omo-codex/scripts/install-local-entrypoint.test.mjs packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs packages/omo-codex/scripts/install-local.test.mjs packages/omo-codex/scripts/install-mcp-runtime.test.mjs packages/omo-codex/scripts/install-packaged-local.test.mjs packages/omo-codex/scripts/install/git-bash.test.mjs packages/omo-codex/scripts/install-agent-links.test.mjs packages/omo-codex/scripts/install-bin-links.test.mjs packages/omo-codex/scripts/sync-telemetry-component.test.mjs",
75
75
  "test:windows-codex": "bun run test:codex",
76
76
  "build:ast-grep-mcp": "bun run --cwd packages/ast-grep-mcp build",
77
77
  "build:git-bash-mcp": "bun run --cwd packages/git-bash-mcp build"
@@ -106,7 +106,6 @@
106
106
  "commander": "^14.0.3",
107
107
  "detect-libc": "^2.1.2",
108
108
  "diff": "^9.0.0",
109
- "effect": "4.0.0-beta.65",
110
109
  "js-yaml": "^4.1.1",
111
110
  "jsonc-parser": "^3.3.1",
112
111
  "picocolors": "^1.1.1",
@@ -136,17 +135,17 @@
136
135
  "zod": "^4.4.3"
137
136
  },
138
137
  "optionalDependencies": {
139
- "oh-my-opencode-darwin-arm64": "4.6.0",
140
- "oh-my-opencode-darwin-x64": "4.6.0",
141
- "oh-my-opencode-darwin-x64-baseline": "4.6.0",
142
- "oh-my-opencode-linux-arm64": "4.6.0",
143
- "oh-my-opencode-linux-arm64-musl": "4.6.0",
144
- "oh-my-opencode-linux-x64": "4.6.0",
145
- "oh-my-opencode-linux-x64-baseline": "4.6.0",
146
- "oh-my-opencode-linux-x64-musl": "4.6.0",
147
- "oh-my-opencode-linux-x64-musl-baseline": "4.6.0",
148
- "oh-my-opencode-windows-x64": "4.6.0",
149
- "oh-my-opencode-windows-x64-baseline": "4.6.0"
138
+ "oh-my-opencode-darwin-arm64": "4.7.1",
139
+ "oh-my-opencode-darwin-x64": "4.7.1",
140
+ "oh-my-opencode-darwin-x64-baseline": "4.7.1",
141
+ "oh-my-opencode-linux-arm64": "4.7.1",
142
+ "oh-my-opencode-linux-arm64-musl": "4.7.1",
143
+ "oh-my-opencode-linux-x64": "4.7.1",
144
+ "oh-my-opencode-linux-x64-baseline": "4.7.1",
145
+ "oh-my-opencode-linux-x64-musl": "4.7.1",
146
+ "oh-my-opencode-linux-x64-musl-baseline": "4.7.1",
147
+ "oh-my-opencode-windows-x64": "4.7.1",
148
+ "oh-my-opencode-windows-x64-baseline": "4.7.1"
150
149
  },
151
150
  "overrides": {
152
151
  "hono": "^4.12.18",
@@ -342,13 +342,44 @@ function errorMessage(error) {
342
342
  import { createRequire } from "module";
343
343
  import { dirname, join as join2 } from "path";
344
344
  import { existsSync, statSync as statSync2 } from "fs";
345
+ var WINDOWS_EXECUTABLE_EXTENSIONS = [".exe", ".cmd", ".bat"];
345
346
  function isValidBinary(filePath) {
346
347
  try {
347
- return statSync2(filePath).size > 1e4;
348
+ const stats = statSync2(filePath);
349
+ if (!stats.isFile()) {
350
+ return false;
351
+ }
352
+ const size = stats.size;
353
+ const lowerPath = filePath.toLowerCase();
354
+ if (lowerPath.endsWith(".cmd") || lowerPath.endsWith(".bat")) {
355
+ return size > 0;
356
+ }
357
+ return size > 1e4;
348
358
  } catch {
349
359
  return false;
350
360
  }
351
361
  }
362
+ function executableCandidates(filePath, platform = process.platform) {
363
+ if (platform !== "win32")
364
+ return [filePath];
365
+ const candidates = [filePath];
366
+ const lowerPath = filePath.toLowerCase();
367
+ if (WINDOWS_EXECUTABLE_EXTENSIONS.some((extension) => lowerPath.endsWith(extension))) {
368
+ return candidates;
369
+ }
370
+ for (const extension of WINDOWS_EXECUTABLE_EXTENSIONS) {
371
+ candidates.push(`${filePath}${extension}`);
372
+ }
373
+ return candidates;
374
+ }
375
+ function findValidExecutable(filePath) {
376
+ for (const candidate of executableCandidates(filePath)) {
377
+ if (existsSync(candidate) && isValidBinary(candidate)) {
378
+ return candidate;
379
+ }
380
+ }
381
+ return null;
382
+ }
352
383
  function getPlatformPackageName() {
353
384
  const platform = process.platform;
354
385
  const arch = process.arch;
@@ -363,29 +394,42 @@ function getPlatformPackageName() {
363
394
  };
364
395
  return platformMap[`${platform}-${arch}`] ?? null;
365
396
  }
397
+ function isModuleResolutionFailure(error) {
398
+ return error instanceof Error && (error.message.includes("Cannot find module") || error.message.includes("Cannot find package"));
399
+ }
366
400
  function findSgCliPathSync() {
367
- const binaryName = process.platform === "win32" ? "sg.exe" : "sg";
401
+ const binaryName = "sg";
368
402
  try {
369
403
  const require2 = createRequire(import.meta.url);
370
404
  const cliPackageJsonPath = require2.resolve("@ast-grep/cli/package.json");
371
405
  const cliDirectory = dirname(cliPackageJsonPath);
372
406
  const sgPath = join2(cliDirectory, binaryName);
373
- if (existsSync(sgPath) && isValidBinary(sgPath)) {
374
- return sgPath;
407
+ const validSgPath = findValidExecutable(sgPath);
408
+ if (validSgPath) {
409
+ return validSgPath;
410
+ }
411
+ } catch (error) {
412
+ if (!isModuleResolutionFailure(error)) {
413
+ throw error;
375
414
  }
376
- } catch {}
415
+ }
377
416
  const platformPackage = getPlatformPackageName();
378
417
  if (platformPackage) {
379
418
  try {
380
419
  const require2 = createRequire(import.meta.url);
381
420
  const packageJsonPath = require2.resolve(`${platformPackage}/package.json`);
382
421
  const packageDirectory = dirname(packageJsonPath);
383
- const astGrepBinaryName = process.platform === "win32" ? "ast-grep.exe" : "ast-grep";
422
+ const astGrepBinaryName = "ast-grep";
384
423
  const binaryPath = join2(packageDirectory, astGrepBinaryName);
385
- if (existsSync(binaryPath) && isValidBinary(binaryPath)) {
386
- return binaryPath;
424
+ const validBinaryPath = findValidExecutable(binaryPath);
425
+ if (validBinaryPath) {
426
+ return validBinaryPath;
427
+ }
428
+ } catch (error) {
429
+ if (!isModuleResolutionFailure(error)) {
430
+ throw error;
387
431
  }
388
- } catch {}
432
+ }
389
433
  }
390
434
  if (process.platform === "darwin") {
391
435
  const homebrewPaths = ["/opt/homebrew/bin/sg", "/usr/local/bin/sg"];
@@ -97,7 +97,7 @@ function getWindowsPathExtensions(env) {
97
97
  .map((extension) => extension.trim())
98
98
  .filter(Boolean)
99
99
  .map((extension) => (extension.startsWith(".") ? extension : `.${extension}`));
100
- return [...new Set(["", ...extensions, ".exe", ".cmd", ".bat"])];
100
+ return [...new Set([...extensions, ".exe", ".cmd", ".bat", ""])];
101
101
  }
102
102
  function resolveWindowsCommand(command, env) {
103
103
  const hasPathSeparator = command.includes("/") || command.includes("\\");
@@ -12,6 +12,19 @@
12
12
  }
13
13
  ]
14
14
  }
15
+ ],
16
+ "PostCompact": [
17
+ {
18
+ "matcher": "manual|auto",
19
+ "hooks": [
20
+ {
21
+ "type": "command",
22
+ "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-compact",
23
+ "timeout": 5,
24
+ "statusMessage": "LazyCodex(0.2.0): Resetting LSP Diagnostics Cache"
25
+ }
26
+ ]
27
+ }
15
28
  ]
16
29
  }
17
30
  }
@@ -3,7 +3,7 @@ import { spawn } from "node:child_process";
3
3
  import { createRequire } from "node:module";
4
4
  import { argv, execPath, stderr } from "node:process";
5
5
 
6
- import { runPostToolUseHookCli } from "./codex-hook-cli.js";
6
+ import { runPostCompactHookCli, runPostToolUseHookCli } from "./codex-hook-cli.js";
7
7
 
8
8
  const require = createRequire(import.meta.url);
9
9
  const PACKAGE_LSP_MCP_CLI = "@code-yeongyu/lsp-tools-mcp/dist/cli.js";
@@ -15,13 +15,17 @@ async function main(): Promise<void> {
15
15
  await runPostToolUseHookCli();
16
16
  return;
17
17
  }
18
+ if (command === "hook" && subcommand === "post-compact") {
19
+ await runPostCompactHookCli();
20
+ return;
21
+ }
18
22
 
19
23
  if (command === "mcp") {
20
24
  await runPackageLspMcpCli();
21
25
  return;
22
26
  }
23
27
 
24
- stderr.write("Usage: omo-lsp [mcp | hook post-tool-use]\n");
28
+ stderr.write("Usage: omo-lsp [mcp | hook post-tool-use | hook post-compact]\n");
25
29
  process.exitCode = 2;
26
30
  }
27
31
 
@@ -2,9 +2,20 @@ import { stdin as processStdin } from "node:process";
2
2
 
3
3
  import { disposeDefaultLspManager } from "@code-yeongyu/lsp-tools-mcp/dist/lsp/manager.js";
4
4
 
5
- import { isRecord, runLspPostToolUseHook } from "./codex-hook.js";
5
+ import { isRecord, runLspPostCompactHook, runLspPostToolUseHook } from "./codex-hook.js";
6
6
 
7
7
  export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
8
+ await runHookCli((input) => runLspPostToolUseHook(input), stdin);
9
+ }
10
+
11
+ export async function runPostCompactHookCli(stdin: NodeJS.ReadStream = processStdin): Promise<void> {
12
+ await runHookCli((input) => runLspPostCompactHook(input), stdin);
13
+ }
14
+
15
+ async function runHookCli(
16
+ runHook: (input: Record<string, unknown>) => Promise<string>,
17
+ stdin: NodeJS.ReadStream,
18
+ ): Promise<void> {
8
19
  try {
9
20
  const raw = await readStdin(stdin);
10
21
  if (!raw.trim()) return;
@@ -16,7 +27,7 @@ export async function runPostToolUseHookCli(stdin: NodeJS.ReadStream = processSt
16
27
  throw error;
17
28
  }
18
29
  const input = isRecord(parsed) ? parsed : {};
19
- const output = await runLspPostToolUseHook(input);
30
+ const output = await runHook(input);
20
31
  if (output) process.stdout.write(output);
21
32
  } finally {
22
33
  await disposeDefaultLspManager();
@@ -2,15 +2,31 @@ import { readFileSync } from "node:fs";
2
2
 
3
3
  import { executeLspDiagnostics } from "@code-yeongyu/lsp-tools-mcp/dist/tools.js";
4
4
 
5
+ import {
6
+ isUnavailableLspDiagnostics,
7
+ markLspSessionCompacted,
8
+ recordLspDiagnosticsObservations,
9
+ sessionIdFrom,
10
+ shouldSkipUnavailableLspDiagnostics,
11
+ } from "./lsp-session-state.js";
12
+ import { extractMutatedFilePaths } from "./mutated-file-paths.js";
13
+
14
+ export { extractMutatedFilePaths } from "./mutated-file-paths.js";
15
+
5
16
  export type DiagnosticsRunner = (filePath: string) => Promise<string>;
6
17
 
7
18
  export interface CodexPostToolUseInput {
19
+ session_id?: unknown;
8
20
  tool_name?: unknown;
9
21
  tool_input?: unknown;
10
22
  tool_response?: unknown;
11
23
  transcript_path?: unknown;
12
24
  }
13
25
 
26
+ export interface CodexPostCompactInput {
27
+ session_id?: unknown;
28
+ }
29
+
14
30
  interface DiagnosticBlock {
15
31
  filePath: string;
16
32
  diagnostics: string;
@@ -25,7 +41,6 @@ interface PostToolUseHookOutput {
25
41
  };
26
42
  }
27
43
 
28
- const MUTATION_TOOL_NAMES = new Set(["apply_patch", "write", "edit", "multiedit", "multi_edit"]);
29
44
  const CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
30
45
  const UNSUPPORTED_EXTENSION_TEXT = "No LSP server configured for extension:";
31
46
  const DIAGNOSTIC_START_PATTERN = /(?:error|warning|information|hint)\[[^\]\r\n]+\] \(\d+\) at \d+:\d+:/g;
@@ -52,14 +67,22 @@ export async function runLspPostToolUseHook(
52
67
  input: CodexPostToolUseInput,
53
68
  runDiagnostics: DiagnosticsRunner = runLspDiagnosticsText,
54
69
  ): Promise<string> {
55
- const filePaths = extractMutatedFilePaths(input);
70
+ const sessionId = sessionIdFrom(input);
71
+ const filePaths = extractMutatedFilePaths(input).filter(
72
+ (filePath) => !shouldSkipUnavailableLspDiagnostics(filePath, sessionId),
73
+ );
56
74
  if (filePaths.length === 0) return "";
57
75
 
58
76
  const blocks: DiagnosticBlock[] = [];
77
+ const observations: Array<{ filePath: string; unavailable: boolean }> = [];
59
78
  for (const { filePath, diagnostics } of await collectDiagnostics(filePaths, runDiagnostics)) {
79
+ const unavailable = isUnavailableLspDiagnostics(diagnostics);
80
+ observations.push({ filePath, unavailable });
60
81
  if (isCleanDiagnostics(diagnostics)) continue;
82
+ if (unavailable) continue;
61
83
  blocks.push({ filePath, diagnostics });
62
84
  }
85
+ recordLspDiagnosticsObservations(sessionId, observations);
63
86
 
64
87
  if (blocks.length === 0) return "";
65
88
 
@@ -76,6 +99,11 @@ export async function runLspPostToolUseHook(
76
99
  return `${JSON.stringify(output)}\n`;
77
100
  }
78
101
 
102
+ export async function runLspPostCompactHook(input: CodexPostCompactInput): Promise<string> {
103
+ markLspSessionCompacted(sessionIdFrom(input));
104
+ return "";
105
+ }
106
+
79
107
  async function collectDiagnostics(
80
108
  filePaths: readonly string[],
81
109
  runDiagnostics: DiagnosticsRunner,
@@ -187,29 +215,6 @@ function limitHookText(text: string, maxChars: number): string {
187
215
  return `${head}${marker}`;
188
216
  }
189
217
 
190
- export function extractMutatedFilePaths(input: CodexPostToolUseInput): string[] {
191
- if (!isMutationTool(input.tool_name)) return [];
192
- if (isFailedToolResponse(input.tool_response)) return [];
193
-
194
- const toolInput = isRecord(input.tool_input) ? input.tool_input : {};
195
- const paths = new Set<string>();
196
- addStringValue(paths, toolInput["path"]);
197
- addStringValue(paths, toolInput["filePath"]);
198
- addStringValue(paths, toolInput["file_path"]);
199
- addStringArray(paths, toolInput["paths"]);
200
- addStringArray(paths, toolInput["filePaths"]);
201
- addStringArray(paths, toolInput["file_paths"]);
202
- addPatchPayloads(paths, toolInput);
203
- addPatchFiles(paths, toolInput["files"]);
204
- addPatchFiles(paths, toolInput["changes"]);
205
- return [...paths];
206
- }
207
-
208
- function isMutationTool(value: unknown): boolean {
209
- if (typeof value !== "string") return false;
210
- return MUTATION_TOOL_NAMES.has(value.toLowerCase());
211
- }
212
-
213
218
  function isCleanDiagnostics(diagnostics: string): boolean {
214
219
  return (
215
220
  diagnostics.length === 0 ||
@@ -218,60 +223,6 @@ function isCleanDiagnostics(diagnostics: string): boolean {
218
223
  );
219
224
  }
220
225
 
221
- function isFailedToolResponse(value: unknown): boolean {
222
- if (!isRecord(value)) return false;
223
- return (
224
- value["isError"] === true || value["is_error"] === true || value["error"] === true || value["status"] === "error"
225
- );
226
- }
227
-
228
- function addStringValue(paths: Set<string>, value: unknown): void {
229
- if (typeof value === "string" && value.length > 0) {
230
- paths.add(value);
231
- }
232
- }
233
-
234
- function addStringArray(paths: Set<string>, value: unknown): void {
235
- if (!Array.isArray(value)) return;
236
- for (const item of value) {
237
- addStringValue(paths, item);
238
- }
239
- }
240
-
241
- function addPatchPayloads(paths: Set<string>, input: Record<string, unknown>): void {
242
- addPatchInput(paths, input["input"]);
243
- addPatchInput(paths, input["patch"]);
244
- addPatchInput(paths, input["command"]);
245
- }
246
-
247
- function addPatchInput(paths: Set<string>, value: unknown): void {
248
- if (typeof value !== "string") return;
249
- for (const line of value.split("\n")) {
250
- const path = extractPatchHeaderPath(line);
251
- if (path !== undefined) paths.add(path);
252
- }
253
- }
254
-
255
- function extractPatchHeaderPath(line: string): string | undefined {
256
- const prefixes = ["*** Add File: ", "*** Update File: ", "*** Move to: "] as const;
257
- for (const prefix of prefixes) {
258
- if (line.startsWith(prefix)) return line.slice(prefix.length).trim();
259
- }
260
- return undefined;
261
- }
262
-
263
- function addPatchFiles(paths: Set<string>, value: unknown): void {
264
- if (!Array.isArray(value)) return;
265
- for (const item of value) {
266
- if (!isRecord(item)) continue;
267
- addStringValue(paths, item["path"]);
268
- addStringValue(paths, item["filePath"]);
269
- addStringValue(paths, item["file_path"]);
270
- addStringValue(paths, item["movePath"]);
271
- addStringValue(paths, item["move_path"]);
272
- }
273
- }
274
-
275
226
  export function isRecord(value: unknown): value is Record<string, unknown> {
276
227
  return typeof value === "object" && value !== null && !Array.isArray(value);
277
228
  }
@@ -0,0 +1,116 @@
1
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, extname, join } from "node:path";
4
+
5
+ interface LspSessionState {
6
+ readonly unavailableExtensions: readonly string[];
7
+ readonly postCompactProbePending?: boolean;
8
+ }
9
+
10
+ export interface DiagnosticsObservation {
11
+ readonly filePath: string;
12
+ readonly unavailable: boolean;
13
+ }
14
+
15
+ export function sessionIdFrom(input: { readonly session_id?: unknown }): string | undefined {
16
+ return typeof input.session_id === "string" && input.session_id.length > 0 ? input.session_id : undefined;
17
+ }
18
+
19
+ export function shouldSkipUnavailableLspDiagnostics(filePath: string, sessionId: string | undefined): boolean {
20
+ if (sessionId === undefined) return false;
21
+ const state = readSessionState(sessionStatePath(sessionId));
22
+ const extension = extensionKey(filePath);
23
+ return (
24
+ extension !== undefined &&
25
+ state.postCompactProbePending !== true &&
26
+ state.unavailableExtensions.includes(extension)
27
+ );
28
+ }
29
+
30
+ export function recordLspDiagnosticsObservations(
31
+ sessionId: string | undefined,
32
+ observations: readonly DiagnosticsObservation[],
33
+ ): void {
34
+ if (sessionId === undefined || observations.length === 0) return;
35
+ const state = readSessionState(sessionStatePath(sessionId));
36
+ const unavailableExtensions = new Set(state.unavailableExtensions);
37
+
38
+ for (const observation of observations) {
39
+ const extension = extensionKey(observation.filePath);
40
+ if (extension === undefined) continue;
41
+ if (observation.unavailable) {
42
+ unavailableExtensions.add(extension);
43
+ } else {
44
+ unavailableExtensions.delete(extension);
45
+ }
46
+ }
47
+
48
+ writeSessionState(sessionStatePath(sessionId), { unavailableExtensions: [...unavailableExtensions].sort() });
49
+ }
50
+
51
+ export function markLspSessionCompacted(sessionId: string | undefined): void {
52
+ if (sessionId === undefined) return;
53
+ const state = readSessionState(sessionStatePath(sessionId));
54
+ if (state.unavailableExtensions.length === 0) return;
55
+ writeSessionState(sessionStatePath(sessionId), {
56
+ unavailableExtensions: state.unavailableExtensions,
57
+ postCompactProbePending: true,
58
+ });
59
+ }
60
+
61
+ export function isUnavailableLspDiagnostics(diagnostics: string): boolean {
62
+ const normalized = diagnostics.trim();
63
+ return (
64
+ normalized.includes("LSP request timeout (method: initialize)") ||
65
+ normalized.includes("LSP server is still initializing") ||
66
+ normalized.includes("NOT INSTALLED") ||
67
+ normalized.includes("Command not found:")
68
+ );
69
+ }
70
+
71
+ function sessionStatePath(sessionId: string): string {
72
+ const root = process.env["PLUGIN_DATA"] ?? join(homedir(), ".codex", "codex-lsp");
73
+ return join(root, "sessions", `${safePathSegment(sessionId)}.json`);
74
+ }
75
+
76
+ function readSessionState(path: string): LspSessionState {
77
+ try {
78
+ const parsed: unknown = JSON.parse(readFileSync(path, "utf8"));
79
+ if (isLspSessionState(parsed)) return parsed;
80
+ return emptyState();
81
+ } catch (error) {
82
+ if (error instanceof SyntaxError || (isRecord(error) && error["code"] === "ENOENT")) return emptyState();
83
+ throw error;
84
+ }
85
+ }
86
+
87
+ function writeSessionState(path: string, state: LspSessionState): void {
88
+ mkdirSync(dirname(path), { recursive: true });
89
+ writeFileSync(path, `${JSON.stringify(state)}\n`);
90
+ }
91
+
92
+ function emptyState(): LspSessionState {
93
+ return { unavailableExtensions: [] };
94
+ }
95
+
96
+ function extensionKey(filePath: string): string | undefined {
97
+ const extension = extname(filePath).toLowerCase();
98
+ return extension.length === 0 ? undefined : extension;
99
+ }
100
+
101
+ function safePathSegment(value: string): string {
102
+ return value.replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 120) || "unknown-session";
103
+ }
104
+
105
+ function isLspSessionState(value: unknown): value is LspSessionState {
106
+ if (!isRecord(value) || !Array.isArray(value["unavailableExtensions"])) return false;
107
+ const postCompactProbePending = value["postCompactProbePending"];
108
+ return (
109
+ value["unavailableExtensions"].every((item) => typeof item === "string") &&
110
+ (postCompactProbePending === undefined || typeof postCompactProbePending === "boolean")
111
+ );
112
+ }
113
+
114
+ function isRecord(value: unknown): value is Record<string, unknown> {
115
+ return typeof value === "object" && value !== null && !Array.isArray(value);
116
+ }
@@ -0,0 +1,88 @@
1
+ const MUTATION_TOOL_NAMES = new Set(["apply_patch", "write", "edit", "multiedit", "multi_edit"]);
2
+
3
+ export interface MutatedFileInput {
4
+ readonly tool_name?: unknown;
5
+ readonly tool_input?: unknown;
6
+ readonly tool_response?: unknown;
7
+ }
8
+
9
+ export function extractMutatedFilePaths(input: MutatedFileInput): string[] {
10
+ if (!isMutationTool(input.tool_name)) return [];
11
+ if (isFailedToolResponse(input.tool_response)) return [];
12
+
13
+ const toolInput = isRecord(input.tool_input) ? input.tool_input : {};
14
+ const paths = new Set<string>();
15
+ addStringValue(paths, toolInput["path"]);
16
+ addStringValue(paths, toolInput["filePath"]);
17
+ addStringValue(paths, toolInput["file_path"]);
18
+ addStringArray(paths, toolInput["paths"]);
19
+ addStringArray(paths, toolInput["filePaths"]);
20
+ addStringArray(paths, toolInput["file_paths"]);
21
+ addPatchPayloads(paths, toolInput);
22
+ addPatchFiles(paths, toolInput["files"]);
23
+ addPatchFiles(paths, toolInput["changes"]);
24
+ return [...paths];
25
+ }
26
+
27
+ function isMutationTool(value: unknown): boolean {
28
+ if (typeof value !== "string") return false;
29
+ return MUTATION_TOOL_NAMES.has(value.toLowerCase());
30
+ }
31
+
32
+ function isFailedToolResponse(value: unknown): boolean {
33
+ if (!isRecord(value)) return false;
34
+ return (
35
+ value["isError"] === true || value["is_error"] === true || value["error"] === true || value["status"] === "error"
36
+ );
37
+ }
38
+
39
+ function addStringValue(paths: Set<string>, value: unknown): void {
40
+ if (typeof value === "string" && value.length > 0) {
41
+ paths.add(value);
42
+ }
43
+ }
44
+
45
+ function addStringArray(paths: Set<string>, value: unknown): void {
46
+ if (!Array.isArray(value)) return;
47
+ for (const item of value) {
48
+ addStringValue(paths, item);
49
+ }
50
+ }
51
+
52
+ function addPatchPayloads(paths: Set<string>, input: Record<string, unknown>): void {
53
+ addPatchInput(paths, input["input"]);
54
+ addPatchInput(paths, input["patch"]);
55
+ addPatchInput(paths, input["command"]);
56
+ }
57
+
58
+ function addPatchInput(paths: Set<string>, value: unknown): void {
59
+ if (typeof value !== "string") return;
60
+ for (const line of value.split("\n")) {
61
+ const path = extractPatchHeaderPath(line);
62
+ if (path !== undefined) paths.add(path);
63
+ }
64
+ }
65
+
66
+ function extractPatchHeaderPath(line: string): string | undefined {
67
+ const prefixes = ["*** Add File: ", "*** Update File: ", "*** Move to: "] as const;
68
+ for (const prefix of prefixes) {
69
+ if (line.startsWith(prefix)) return line.slice(prefix.length).trim();
70
+ }
71
+ return undefined;
72
+ }
73
+
74
+ function addPatchFiles(paths: Set<string>, value: unknown): void {
75
+ if (!Array.isArray(value)) return;
76
+ for (const item of value) {
77
+ if (!isRecord(item)) continue;
78
+ addStringValue(paths, item["path"]);
79
+ addStringValue(paths, item["filePath"]);
80
+ addStringValue(paths, item["file_path"]);
81
+ addStringValue(paths, item["movePath"]);
82
+ addStringValue(paths, item["move_path"]);
83
+ }
84
+ }
85
+
86
+ function isRecord(value: unknown): value is Record<string, unknown> {
87
+ return typeof value === "object" && value !== null && !Array.isArray(value);
88
+ }