oh-my-opencode 5.1.2 → 5.1.4

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 (75) hide show
  1. package/dist/cli/index.js +233 -81
  2. package/dist/cli-node/index.js +233 -81
  3. package/dist/index.js +187 -24
  4. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  5. package/dist/tui.js +148 -18
  6. package/package.json +14 -14
  7. package/packages/lsp-core/src/lsp/outside-context-workspace.ts +1 -1
  8. package/packages/lsp-core/src/missing-dependency-result.test.ts +32 -2
  9. package/packages/lsp-core/src/missing-dependency-result.ts +3 -0
  10. package/packages/lsp-core/src/post-edit/file-location.ts +50 -0
  11. package/packages/lsp-core/src/post-edit/index.ts +2 -0
  12. package/packages/lsp-core/src/post-edit/not-installed-guidance.ts +36 -0
  13. package/packages/lsp-core/src/post-edit/not-installed.test.ts +199 -0
  14. package/packages/lsp-core/src/post-edit/orchestration.ts +26 -5
  15. package/packages/lsp-daemon/dist/cli.js +2 -1
  16. package/packages/lsp-daemon/dist/client.js +2 -1
  17. package/packages/lsp-daemon/dist/index.js +2 -1
  18. package/packages/lsp-tools-mcp/dist/cli.js +2 -1
  19. package/packages/lsp-tools-mcp/dist/mcp.js +2 -1
  20. package/packages/lsp-tools-mcp/dist/tools.js +2 -1
  21. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  22. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/comment-checker/dist/cli.js +33 -7
  25. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/comment-checker/src/runner.ts +4 -4
  28. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  29. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +4 -4
  33. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +141 -107
  34. package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +2 -2
  35. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  36. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +2 -2
  38. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  39. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  40. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  41. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  42. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  43. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  44. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  45. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  46. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  47. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  49. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  71. package/packages/omo-codex/plugin/package-lock.json +12 -12
  72. package/packages/omo-codex/plugin/package.json +1 -1
  73. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  74. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  75. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { LspServerLookupError } from "./lsp/errors.js";
2
+ import { type InstallDecision, loadInstallDecision } from "./lsp/server-install-state.js";
2
3
  import { handleMissingDependencyError } from "./lsp/startup-failure.js";
3
4
  import { lspRequestContext } from "./request-context.js";
4
5
  import type { ToolExecutionResult } from "./tools.js";
@@ -20,6 +21,7 @@ export type MissingDependencyAvailability =
20
21
  readonly installHint: string;
21
22
  readonly installDecisionTool: boolean;
22
23
  readonly installDecisionsPath: string;
24
+ readonly decision: InstallDecision | null;
23
25
  };
24
26
 
25
27
  export function missingDependencyResult<TDetails extends object>(
@@ -67,6 +69,7 @@ function missingDependencyAvailability(error: unknown): MissingDependencyAvailab
67
69
  installHint: error.lookup.installHint,
68
70
  installDecisionTool: context.capabilities.installDecisionTool,
69
71
  installDecisionsPath: context.installDecisionsPath,
72
+ decision: loadInstallDecision(error.lookup.server.id)?.decision ?? null,
70
73
  };
71
74
  default: {
72
75
  const exhaustive: never = error.lookup;
@@ -0,0 +1,50 @@
1
+ import { tmpdir } from "node:os";
2
+ import { dirname, resolve } from "node:path";
3
+
4
+ import { nearestMarkedAncestor } from "../lsp/outside-context-workspace.js";
5
+ import { canonicalizeExistingOrNearestAncestor, isPathInside } from "../request-context.js";
6
+
7
+ export type PostEditFileLocation = "project" | "outside_project" | "agent_config" | "temp";
8
+
9
+ export interface PostEditFileLocationOptions {
10
+ readonly cwd: string;
11
+ readonly agentDirs: readonly string[];
12
+ /** Temp roots; defaults to the canonical OS temp dir (plus `/tmp` off Windows). */
13
+ readonly tempDirs?: readonly string[];
14
+ }
15
+
16
+ /**
17
+ * Classifies an edited file for install guidance.
18
+ *
19
+ * A project root is the nearest ancestor carrying one of lsp-core's `WORKSPACE_MARKERS` (the marker set the
20
+ * out-of-cwd workspace resolver uses). Inside a temp dir, only a marked root strictly below the temp root
21
+ * counts, so a stray `package.json` left in the temp root does not turn every scratch file into a project.
22
+ * Paths are canonicalized first because macOS `/tmp` and `/var` are symlinks.
23
+ */
24
+ export function classifyPostEditFileLocation(
25
+ filePath: string,
26
+ options: PostEditFileLocationOptions,
27
+ ): PostEditFileLocation {
28
+ const file = canonicalizeExistingOrNearestAncestor(resolve(options.cwd, filePath));
29
+ if (canonicalDirectories(options.agentDirs).some((directory) => isPathInside(directory, file))) {
30
+ return "agent_config";
31
+ }
32
+
33
+ const projectRoot = nearestMarkedAncestor(dirname(file));
34
+ const tempRoot = canonicalDirectories(options.tempDirs ?? defaultTempDirectories()).find((directory) =>
35
+ isPathInside(directory, file),
36
+ );
37
+ if (tempRoot !== undefined) {
38
+ const ownProject = projectRoot !== undefined && projectRoot !== tempRoot && isPathInside(tempRoot, projectRoot);
39
+ return ownProject ? "project" : "temp";
40
+ }
41
+ return projectRoot === undefined ? "outside_project" : "project";
42
+ }
43
+
44
+ function defaultTempDirectories(): readonly string[] {
45
+ return process.platform === "win32" ? [tmpdir()] : [tmpdir(), "/tmp"];
46
+ }
47
+
48
+ function canonicalDirectories(directories: readonly string[]): readonly string[] {
49
+ return directories.map((directory) => canonicalizeExistingOrNearestAncestor(directory));
50
+ }
@@ -1 +1,3 @@
1
+ export * from "./file-location.js";
2
+ export * from "./not-installed-guidance.js";
1
3
  export * from "./orchestration.js";
@@ -0,0 +1,36 @@
1
+ import type { PostEditFileLocation } from "./file-location.js";
2
+
3
+ export interface PostEditNotInstalledOutcome {
4
+ readonly kind: "not_installed";
5
+ readonly serverId: string;
6
+ readonly installDecisionTool: boolean;
7
+ /** A recorded install decision; absent while the user has not decided. */
8
+ readonly decision?: "declined" | "allowed";
9
+ readonly text: string;
10
+ }
11
+
12
+ export interface NotInstalledGuidanceState {
13
+ readonly notInstalledServers: Set<string>;
14
+ }
15
+
16
+ /**
17
+ * Decides whether a missing-server nudge reaches the model.
18
+ *
19
+ * Recorded decisions keep their own text. Undecided nudges are skipped for files outside a project
20
+ * (no marked root, the agent's config dir, or a temp dir) because a repo-local install there would
21
+ * create a manifest in HOME or a temp dir. Without a decision tool nothing can be recorded, so a
22
+ * server is nudged at most once per session state.
23
+ */
24
+ export function notInstalledGuidance(
25
+ filePath: string,
26
+ outcome: PostEditNotInstalledOutcome,
27
+ state: NotInstalledGuidanceState,
28
+ locateFile: ((filePath: string) => PostEditFileLocation) | undefined,
29
+ ): string | undefined {
30
+ if (outcome.decision !== undefined) return outcome.text;
31
+ if (locateFile !== undefined && locateFile(filePath) !== "project") return undefined;
32
+ if (outcome.installDecisionTool) return outcome.text;
33
+ if (state.notInstalledServers.has(outcome.serverId)) return undefined;
34
+ state.notInstalledServers.add(outcome.serverId);
35
+ return outcome.text;
36
+ }
@@ -0,0 +1,199 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, it } from "bun:test";
5
+
6
+ import {
7
+ classifyPostEditFileLocation,
8
+ collectPostEditDiagnostics,
9
+ createPostEditNotConfiguredCache,
10
+ type PostEditDiagnosticsOutcome,
11
+ type PostEditFileLocation,
12
+ resetPostEditNotConfiguredCache,
13
+ } from "./index.js";
14
+
15
+ const tempDirectories: string[] = [];
16
+
17
+ afterEach(() => {
18
+ for (const directory of tempDirectories.splice(0)) {
19
+ rmSync(directory, { recursive: true, force: true });
20
+ }
21
+ });
22
+
23
+ function tempRoot(prefix: string): string {
24
+ const root = mkdtempSync(join(tmpdir(), prefix));
25
+ tempDirectories.push(root);
26
+ return root;
27
+ }
28
+
29
+ function touch(path: string, text = ""): string {
30
+ mkdirSync(join(path, ".."), { recursive: true });
31
+ writeFileSync(path, text);
32
+ return path;
33
+ }
34
+
35
+ const FULL_NUDGE = [
36
+ "LSP server 'typescript' for .ts is NOT INSTALLED.",
37
+ "To install in THIS repository (preferred — no global install needed):",
38
+ "ACTION REQUIRED — ASK THE USER whether to install this LSP server.",
39
+ ].join("\n");
40
+
41
+ function notInstalled(
42
+ serverId: string,
43
+ overrides: Partial<Extract<PostEditDiagnosticsOutcome, { kind: "not_installed" }>> = {},
44
+ ): PostEditDiagnosticsOutcome {
45
+ return {
46
+ kind: "not_installed",
47
+ serverId,
48
+ installDecisionTool: false,
49
+ text: `${FULL_NUDGE} (${serverId})`,
50
+ ...overrides,
51
+ };
52
+ }
53
+
54
+ function at(location: PostEditFileLocation): () => PostEditFileLocation {
55
+ return () => location;
56
+ }
57
+
58
+ describe("classifyPostEditFileLocation", () => {
59
+ it("#given a file with no workspace marker above it #when classified #then it is outside any project", () => {
60
+ const root = tempRoot("lsp-postedit-noproject-");
61
+ const file = touch(join(root, "scratch", "note.mjs"));
62
+
63
+ expect(classifyPostEditFileLocation(file, { cwd: root, agentDirs: [], tempDirs: [] })).toBe("outside_project");
64
+ });
65
+
66
+ it("#given a file under the agent config dir inside a git repository #when classified #then the agent config dir wins", () => {
67
+ const root = tempRoot("lsp-postedit-agentdir-");
68
+ mkdirSync(join(root, ".git"));
69
+ const agentDir = join(root, ".omo", "agent");
70
+ const file = touch(join(agentDir, "models.json"), "{}\n");
71
+
72
+ expect(classifyPostEditFileLocation(file, { cwd: root, agentDirs: [agentDir], tempDirs: [] })).toBe(
73
+ "agent_config",
74
+ );
75
+ });
76
+
77
+ it("#given a temp dir holding a stray package.json #when a scratch file there is classified #then it is a temp file", () => {
78
+ const temp = tempRoot("lsp-postedit-temp-");
79
+ touch(join(temp, "package.json"), "{}\n");
80
+ const file = touch(join(temp, "rules_test.mjs"));
81
+
82
+ expect(classifyPostEditFileLocation(file, { cwd: temp, agentDirs: [], tempDirs: [temp] })).toBe("temp");
83
+ });
84
+
85
+ it("#given the default temp dirs #when an OS temp file is classified #then the canonical temp dir matches", () => {
86
+ const root = tempRoot("lsp-postedit-ostemp-");
87
+ const file = touch(join(root, "scratch.mjs"));
88
+
89
+ expect(classifyPostEditFileLocation(file, { cwd: root, agentDirs: [] })).toBe("temp");
90
+ if (process.platform !== "win32") {
91
+ const slashTmp = join("/tmp", `lsp-postedit-missing-${process.pid}`, "rules_test.mjs");
92
+ expect(classifyPostEditFileLocation(slashTmp, { cwd: root, agentDirs: [] })).toBe("temp");
93
+ }
94
+ });
95
+
96
+ it("#given a project with its own marker inside a temp dir #when classified #then it is a project", () => {
97
+ const temp = tempRoot("lsp-postedit-tempproject-");
98
+ const project = join(temp, "checkout");
99
+ touch(join(project, "package.json"), "{}\n");
100
+ const file = touch(join(project, "src", "a.ts"));
101
+
102
+ expect(classifyPostEditFileLocation(file, { cwd: project, agentDirs: [], tempDirs: [temp] })).toBe("project");
103
+ expect(classifyPostEditFileLocation("src/a.ts", { cwd: project, agentDirs: [], tempDirs: [temp] })).toBe(
104
+ "project",
105
+ );
106
+ });
107
+ });
108
+
109
+ describe("collectPostEditDiagnostics not-installed guidance", () => {
110
+ for (const location of ["outside_project", "agent_config", "temp"] as const) {
111
+ it(`#given a missing server for a ${location} file #when diagnostics run #then the install nudge is skipped`, async () => {
112
+ const result = await collectPostEditDiagnostics({
113
+ filePaths: ["file.ts"],
114
+ runDiagnostics: async () => notInstalled("typescript"),
115
+ locateFile: at(location),
116
+ });
117
+
118
+ expect(result.blocks).toEqual([]);
119
+ expect(result.observations).toEqual([{ filePath: "file.ts", kind: "not_installed" }]);
120
+ });
121
+ }
122
+
123
+ it("#given a missing server for a project file #when diagnostics run #then today's full guidance is kept", async () => {
124
+ const result = await collectPostEditDiagnostics({
125
+ filePaths: ["src/a.ts"],
126
+ runDiagnostics: async () => notInstalled("typescript"),
127
+ locateFile: at("project"),
128
+ });
129
+
130
+ expect(result.blocks).toEqual([{ filePath: "src/a.ts", diagnostics: `${FULL_NUDGE} (typescript)` }]);
131
+ });
132
+
133
+ it("#given no decision tool #when repeated edits hit missing servers #then each server is nudged once per session", async () => {
134
+ const cache = createPostEditNotConfiguredCache();
135
+ const serverFor = (filePath: string): string => (filePath.endsWith(".json") ? "biome" : "typescript");
136
+ const run = (filePaths: readonly string[]) =>
137
+ collectPostEditDiagnostics({
138
+ filePaths,
139
+ cache,
140
+ runDiagnostics: async (filePath) => notInstalled(serverFor(filePath)),
141
+ locateFile: at("project"),
142
+ });
143
+
144
+ const first = await run(["a.ts", "b.ts"]);
145
+ const second = await run(["c.ts", "d.json"]);
146
+ const third = await run(["e.json", "f.ts"]);
147
+
148
+ expect(first.blocks.map((block) => block.filePath)).toEqual(["a.ts"]);
149
+ expect(second.blocks.map((block) => block.filePath)).toEqual(["d.json"]);
150
+ expect(third.blocks).toEqual([]);
151
+
152
+ resetPostEditNotConfiguredCache(cache);
153
+ const afterReset = await run(["g.ts"]);
154
+ expect(afterReset.blocks.map((block) => block.filePath)).toEqual(["g.ts"]);
155
+ });
156
+
157
+ it("#given a decision tool #when repeated edits hit a missing server #then every edit keeps the guidance", async () => {
158
+ const cache = createPostEditNotConfiguredCache();
159
+ const run = (filePath: string) =>
160
+ collectPostEditDiagnostics({
161
+ filePaths: [filePath],
162
+ cache,
163
+ runDiagnostics: async () => notInstalled("typescript", { installDecisionTool: true }),
164
+ locateFile: at("project"),
165
+ });
166
+
167
+ expect((await run("a.ts")).blocks).toHaveLength(1);
168
+ expect((await run("b.ts")).blocks).toHaveLength(1);
169
+ });
170
+
171
+ for (const decision of ["declined", "allowed"] as const) {
172
+ it(`#given a recorded ${decision} decision #when edits repeat anywhere #then that branch's text is unchanged every time`, async () => {
173
+ const cache = createPostEditNotConfiguredCache();
174
+ const text = `LSP server 'typescript' ${decision} branch text`;
175
+ const outcomes: PostEditFileLocation[] = ["project", "outside_project", "project"];
176
+ const blocks: string[] = [];
177
+ for (const location of outcomes) {
178
+ const result = await collectPostEditDiagnostics({
179
+ filePaths: ["a.ts"],
180
+ cache,
181
+ runDiagnostics: async () => notInstalled("typescript", { decision, text }),
182
+ locateFile: at(location),
183
+ });
184
+ blocks.push(...result.blocks.map((block) => block.diagnostics));
185
+ }
186
+
187
+ expect(blocks).toEqual([text, text, text]);
188
+ });
189
+ }
190
+
191
+ it("#given no file locator #when a missing server is reported #then the full guidance is kept", async () => {
192
+ const result = await collectPostEditDiagnostics({
193
+ filePaths: ["a.ts"],
194
+ runDiagnostics: async () => notInstalled("typescript"),
195
+ });
196
+
197
+ expect(result.blocks).toEqual([{ filePath: "a.ts", diagnostics: `${FULL_NUDGE} (typescript)` }]);
198
+ });
199
+ });
@@ -1,5 +1,8 @@
1
1
  import { extname } from "node:path";
2
2
 
3
+ import type { PostEditFileLocation } from "./file-location.js";
4
+ import { notInstalledGuidance, type PostEditNotInstalledOutcome } from "./not-installed-guidance.js";
5
+
3
6
  const DEFAULT_MAX_CONCURRENCY = 4;
4
7
  const CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
5
8
 
@@ -8,7 +11,8 @@ export type PostEditDiagnosticsOutcome =
8
11
  | {
9
12
  readonly kind: "not_configured";
10
13
  readonly extension: string;
11
- };
14
+ }
15
+ | PostEditNotInstalledOutcome;
12
16
 
13
17
  export type DiagnosticsRunner = (filePath: string) => Promise<PostEditDiagnosticsOutcome>;
14
18
 
@@ -19,7 +23,7 @@ export interface PostEditDiagnosticsBlock {
19
23
 
20
24
  export interface PostEditDiagnosticsObservation {
21
25
  readonly filePath: string;
22
- readonly kind: "block" | "clean" | "not_configured";
26
+ readonly kind: "block" | "clean" | "not_configured" | "not_installed";
23
27
  }
24
28
 
25
29
  export interface PostEditDiagnosticsResult {
@@ -29,6 +33,7 @@ export interface PostEditDiagnosticsResult {
29
33
 
30
34
  export interface PostEditNotConfiguredCache {
31
35
  readonly notConfiguredExtensions: Set<string>;
36
+ readonly notInstalledServers: Set<string>;
32
37
  }
33
38
 
34
39
  export interface CollectPostEditDiagnosticsInput {
@@ -36,6 +41,7 @@ export interface CollectPostEditDiagnosticsInput {
36
41
  readonly runDiagnostics: DiagnosticsRunner;
37
42
  readonly cache?: PostEditNotConfiguredCache;
38
43
  readonly maxConcurrency?: number;
44
+ readonly locateFile?: (filePath: string) => PostEditFileLocation;
39
45
  }
40
46
 
41
47
  interface IndexedFilePath {
@@ -50,11 +56,12 @@ interface IndexedDiagnostics {
50
56
  }
51
57
 
52
58
  export function createPostEditNotConfiguredCache(): PostEditNotConfiguredCache {
53
- return { notConfiguredExtensions: new Set() };
59
+ return { notConfiguredExtensions: new Set(), notInstalledServers: new Set() };
54
60
  }
55
61
 
56
62
  export function resetPostEditNotConfiguredCache(cache: PostEditNotConfiguredCache): void {
57
63
  cache.notConfiguredExtensions.clear();
64
+ cache.notInstalledServers.clear();
58
65
  }
59
66
 
60
67
  export async function collectPostEditDiagnostics(
@@ -67,8 +74,15 @@ export async function collectPostEditDiagnostics(
67
74
  const observations: PostEditDiagnosticsObservation[] = [];
68
75
 
69
76
  for (const result of results) {
70
- const classification = classifyDiagnostics(result.outcome);
77
+ const outcome = result.outcome;
78
+ const classification =
79
+ typeof outcome !== "string" && outcome.kind === "not_installed"
80
+ ? classifyNotInstalled(notInstalledGuidance(result.filePath, outcome, cache, input.locateFile))
81
+ : classifyDiagnostics(outcome);
71
82
  switch (classification.kind) {
83
+ case "not_installed":
84
+ observations.push({ filePath: result.filePath, kind: "not_installed" });
85
+ break;
72
86
  case "clean":
73
87
  observations.push({ filePath: result.filePath, kind: "clean" });
74
88
  break;
@@ -146,9 +160,16 @@ async function collectFileDiagnostics(
146
160
 
147
161
  function normalizeDiagnosticsOutcome(outcome: PostEditDiagnosticsOutcome): PostEditDiagnosticsOutcome {
148
162
  if (typeof outcome === "string") return normalizeDiagnosticsText(outcome);
163
+ if (outcome.kind === "not_installed") return { ...outcome, text: normalizeDiagnosticsText(outcome.text) };
149
164
  return outcome;
150
165
  }
151
166
 
167
+ function classifyNotInstalled(
168
+ guidance: string | undefined,
169
+ ): { readonly kind: "not_installed" } | { readonly kind: "block"; readonly diagnostics: string } {
170
+ return guidance === undefined ? { kind: "not_installed" } : { kind: "block", diagnostics: guidance };
171
+ }
172
+
152
173
  function normalizeDiagnosticsText(text: string): string {
153
174
  return text.trim();
154
175
  }
@@ -159,7 +180,7 @@ function formatDiagnosticsError(error: Error): string {
159
180
  }
160
181
 
161
182
  function classifyDiagnostics(
162
- outcome: PostEditDiagnosticsOutcome,
183
+ outcome: Exclude<PostEditDiagnosticsOutcome, PostEditNotInstalledOutcome>,
163
184
  ):
164
185
  | { readonly kind: "clean" }
165
186
  | { readonly kind: "not_configured"; readonly extension: string }
@@ -5350,7 +5350,8 @@ function missingDependencyAvailability(error) {
5350
5350
  extensions: [...error.lookup.server.extensions],
5351
5351
  installHint: error.lookup.installHint,
5352
5352
  installDecisionTool: context.capabilities.installDecisionTool,
5353
- installDecisionsPath: context.installDecisionsPath
5353
+ installDecisionsPath: context.installDecisionsPath,
5354
+ decision: loadInstallDecision(error.lookup.server.id)?.decision ?? null
5354
5355
  };
5355
5356
  default: {
5356
5357
  const exhaustive = error.lookup;
@@ -5586,7 +5586,8 @@ function missingDependencyAvailability(error) {
5586
5586
  extensions: [...error.lookup.server.extensions],
5587
5587
  installHint: error.lookup.installHint,
5588
5588
  installDecisionTool: context.capabilities.installDecisionTool,
5589
- installDecisionsPath: context.installDecisionsPath
5589
+ installDecisionsPath: context.installDecisionsPath,
5590
+ decision: loadInstallDecision(error.lookup.server.id)?.decision ?? null
5590
5591
  };
5591
5592
  default: {
5592
5593
  const exhaustive = error.lookup;
@@ -5581,7 +5581,8 @@ function missingDependencyAvailability(error) {
5581
5581
  extensions: [...error.lookup.server.extensions],
5582
5582
  installHint: error.lookup.installHint,
5583
5583
  installDecisionTool: context.capabilities.installDecisionTool,
5584
- installDecisionsPath: context.installDecisionsPath
5584
+ installDecisionsPath: context.installDecisionsPath,
5585
+ decision: loadInstallDecision(error.lookup.server.id)?.decision ?? null
5585
5586
  };
5586
5587
  default: {
5587
5588
  const exhaustive = error.lookup;
@@ -4919,7 +4919,8 @@ function missingDependencyAvailability(error) {
4919
4919
  extensions: [...error.lookup.server.extensions],
4920
4920
  installHint: error.lookup.installHint,
4921
4921
  installDecisionTool: context.capabilities.installDecisionTool,
4922
- installDecisionsPath: context.installDecisionsPath
4922
+ installDecisionsPath: context.installDecisionsPath,
4923
+ decision: loadInstallDecision(error.lookup.server.id)?.decision ?? null
4923
4924
  };
4924
4925
  default: {
4925
4926
  const exhaustive = error.lookup;
@@ -4915,7 +4915,8 @@ function missingDependencyAvailability(error) {
4915
4915
  extensions: [...error.lookup.server.extensions],
4916
4916
  installHint: error.lookup.installHint,
4917
4917
  installDecisionTool: context.capabilities.installDecisionTool,
4918
- installDecisionsPath: context.installDecisionsPath
4918
+ installDecisionsPath: context.installDecisionsPath,
4919
+ decision: loadInstallDecision(error.lookup.server.id)?.decision ?? null
4919
4920
  };
4920
4921
  default: {
4921
4922
  const exhaustive = error.lookup;
@@ -4603,7 +4603,8 @@ function missingDependencyAvailability(error) {
4603
4603
  extensions: [...error.lookup.server.extensions],
4604
4604
  installHint: error.lookup.installHint,
4605
4605
  installDecisionTool: context.capabilities.installDecisionTool,
4606
- installDecisionsPath: context.installDecisionsPath
4606
+ installDecisionsPath: context.installDecisionsPath,
4607
+ decision: loadInstallDecision(error.lookup.server.id)?.decision ?? null
4607
4608
  };
4608
4609
  default: {
4609
4610
  const exhaustive = error.lookup;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omo",
3
- "version": "5.1.2",
3
+ "version": "5.1.4",
4
4
  "description": "One Codex plugin namespace for Yeongyu's local Codex components.",
5
5
  "author": {
6
6
  "name": "Yeongyu Kim",
@@ -8,7 +8,7 @@
8
8
  "command": "node \"${PLUGIN_ROOT}/components/bootstrap/dist/cli.js\" hook session-start",
9
9
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\bootstrap.ps1\"",
10
10
  "timeout": 30,
11
- "statusMessage": "(OmO 5.1.2) Checking Bootstrap Provisioning"
11
+ "statusMessage": "(OmO 5.1.4) Checking Bootstrap Provisioning"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-bootstrap",
3
- "version": "5.1.2",
3
+ "version": "5.1.4",
4
4
  "description": "Codex SessionStart bootstrap component that provisions LazyCodex runtime dependencies from a detached worker.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -146,6 +146,34 @@ function joinPatchLines(lines) {
146
146
  `)}
147
147
  `;
148
148
  }
149
+ // ../../comment-checker-core/src/package-binary.ts
150
+ import { createRequire } from "node:module";
151
+ import { dirname, join } from "node:path";
152
+ var COMMENT_CHECKER_PACKAGE_NAME = "@code-yeongyu/comment-checker";
153
+ function isModuleResolutionMiss(error) {
154
+ return error instanceof Error || typeof error === "object" && error !== null && "name" in error && error.name === "ResolveMessage";
155
+ }
156
+ function findCommentCheckerPackageBinary(input) {
157
+ const packageName = input.packageName ?? COMMENT_CHECKER_PACKAGE_NAME;
158
+ const platformKey = `${input.platform ?? process.platform}-${input.arch ?? process.arch}`;
159
+ const packageDir = dirname(input.packageJsonPath);
160
+ const candidates = [
161
+ resolvePlatformPackageBinary(input.packageJsonPath, `${packageName}-${platformKey}`, input.binaryName),
162
+ join(packageDir, "vendor", platformKey, input.binaryName),
163
+ join(packageDir, "bin", input.binaryName)
164
+ ];
165
+ return candidates.find((candidate) => candidate !== null && input.existsSync(candidate)) ?? null;
166
+ }
167
+ function resolvePlatformPackageBinary(packageJsonPath, platformPackageName, binaryName) {
168
+ try {
169
+ const manifestPath = createRequire(packageJsonPath).resolve(`${platformPackageName}/package.json`);
170
+ return join(dirname(manifestPath), "bin", binaryName);
171
+ } catch (error) {
172
+ if (isModuleResolutionMiss(error))
173
+ return null;
174
+ throw error;
175
+ }
176
+ }
149
177
  // components/comment-checker/src/hook-input.ts
150
178
  function toHookInput(request, context) {
151
179
  return {
@@ -302,8 +330,7 @@ function getContentText(content) {
302
330
  // components/comment-checker/src/runner.ts
303
331
  import { spawn } from "node:child_process";
304
332
  import { existsSync } from "node:fs";
305
- import { createRequire } from "node:module";
306
- import { dirname, join } from "node:path";
333
+ import { createRequire as createRequire2 } from "node:module";
307
334
  var MAX_PROCESS_OUTPUT_BYTES = 64 * 1024;
308
335
  async function runCommentChecker2(input, options = {}) {
309
336
  const binaryPath = options.binaryPath ?? (options.resolveBinary ? options.resolveBinary() : resolveCommentCheckerBinary2());
@@ -361,7 +388,7 @@ function resolveCommentCheckerBinary2() {
361
388
  }
362
389
  function resolvePackageApiBinary() {
363
390
  try {
364
- const require2 = createRequire(import.meta.url);
391
+ const require2 = createRequire2(import.meta.url);
365
392
  const packageExports = require2(commentCheckerPackageName());
366
393
  if (!isCommentCheckerPackage(packageExports))
367
394
  return;
@@ -373,10 +400,9 @@ function resolvePackageApiBinary() {
373
400
  }
374
401
  function resolvePackageBinary(binaryName) {
375
402
  try {
376
- const require2 = createRequire(import.meta.url);
377
- const packagePath = require2.resolve(`${commentCheckerPackageName()}/package.json`);
378
- const binaryPath = join(dirname(packagePath), "bin", binaryName);
379
- return existsSync(binaryPath) ? binaryPath : undefined;
403
+ const require2 = createRequire2(import.meta.url);
404
+ const packageJsonPath = require2.resolve(`${commentCheckerPackageName()}/package.json`);
405
+ return findCommentCheckerPackageBinary({ packageJsonPath, binaryName, existsSync }) ?? undefined;
380
406
  } catch {
381
407
  return;
382
408
  }
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 30,
11
- "statusMessage": "(OmO 5.1.2) Checking Comments"
11
+ "statusMessage": "(OmO 5.1.4) Checking Comments"
12
12
  }
13
13
  ]
14
14
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-comment-checker",
3
- "version": "5.1.2",
3
+ "version": "5.1.4",
4
4
  "description": "Codex plugin that runs comment-checker after edit-like PostToolUse hooks.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -1,7 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
3
  import { createRequire } from "node:module";
4
- import { dirname, join } from "node:path";
4
+
5
+ import { findCommentCheckerPackageBinary } from "@oh-my-opencode/comment-checker-core";
5
6
 
6
7
  import type { CommentCheckerHookInput } from "./core.js";
7
8
 
@@ -108,9 +109,8 @@ function resolvePackageApiBinary(): string | undefined {
108
109
  function resolvePackageBinary(binaryName: string): string | undefined {
109
110
  try {
110
111
  const require = createRequire(import.meta.url);
111
- const packagePath = require.resolve(`${commentCheckerPackageName()}/package.json`);
112
- const binaryPath = join(dirname(packagePath), "bin", binaryName);
113
- return existsSync(binaryPath) ? binaryPath : undefined;
112
+ const packageJsonPath = require.resolve(`${commentCheckerPackageName()}/package.json`);
113
+ return findCommentCheckerPackageBinary({ packageJsonPath, binaryName, existsSync }) ?? undefined;
114
114
  } catch {
115
115
  return undefined;
116
116
  }
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
10
10
  "timeout": 5,
11
- "statusMessage": "(OmO 5.1.2) Recommending Git Bash MCP"
11
+ "statusMessage": "(OmO 5.1.4) Recommending Git Bash MCP"
12
12
  }
13
13
  ]
14
14
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-compact",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 5.1.2) Resetting Git Bash MCP Reminder"
23
+ "statusMessage": "(OmO 5.1.4) Resetting Git Bash MCP Reminder"
24
24
  }
25
25
  ]
26
26
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sisyphuslabs/codex-git-bash-hook",
3
- "version": "5.1.2",
3
+ "version": "5.1.4",
4
4
  "description": "Codex hook component that reminds Windows sessions to prefer the OMO git_bash MCP.",
5
5
  "type": "module",
6
6
  "private": true,
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/components/lazycodex-executor-verify/dist/cli.js\" hook subagent-stop",
10
10
  "timeout": 10,
11
- "statusMessage": "(OmO 5.1.2) Verifying LazyCodex Executor Evidence"
11
+ "statusMessage": "(OmO 5.1.4) Verifying LazyCodex Executor Evidence"
12
12
  }
13
13
  ]
14
14
  }