oh-my-opencode 4.7.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 (23) hide show
  1. package/dist/cli/index.js +5334 -5150
  2. package/dist/index.js +3447 -3334
  3. package/package.json +13 -13
  4. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +13 -0
  5. package/packages/omo-codex/plugin/components/lsp/src/cli.ts +6 -2
  6. package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +13 -2
  7. package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +30 -79
  8. package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +116 -0
  9. package/packages/omo-codex/plugin/components/lsp/src/mutated-file-paths.ts +88 -0
  10. package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +206 -0
  11. package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +5 -3
  12. package/packages/omo-codex/plugin/components/rules/src/codex-hook-options.ts +1 -0
  13. package/packages/omo-codex/plugin/components/rules/src/rules/finder.ts +15 -2
  14. package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +4 -1
  15. package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +28 -5
  16. package/packages/omo-codex/plugin/hooks/hooks.json +13 -2
  17. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +1 -8
  18. package/packages/omo-codex/plugin/test/aggregate.test.mjs +16 -0
  19. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +6 -28
  20. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +26 -1
  21. package/packages/omo-codex/scripts/install/permissions.mjs +11 -0
  22. package/packages/omo-codex/scripts/install-config-autonomous-features.test.mjs +83 -0
  23. package/packages/omo-codex/scripts/install-local.test.mjs +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "4.7.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"
@@ -135,17 +135,17 @@
135
135
  "zod": "^4.4.3"
136
136
  },
137
137
  "optionalDependencies": {
138
- "oh-my-opencode-darwin-arm64": "4.7.0",
139
- "oh-my-opencode-darwin-x64": "4.7.0",
140
- "oh-my-opencode-darwin-x64-baseline": "4.7.0",
141
- "oh-my-opencode-linux-arm64": "4.7.0",
142
- "oh-my-opencode-linux-arm64-musl": "4.7.0",
143
- "oh-my-opencode-linux-x64": "4.7.0",
144
- "oh-my-opencode-linux-x64-baseline": "4.7.0",
145
- "oh-my-opencode-linux-x64-musl": "4.7.0",
146
- "oh-my-opencode-linux-x64-musl-baseline": "4.7.0",
147
- "oh-my-opencode-windows-x64": "4.7.0",
148
- "oh-my-opencode-windows-x64-baseline": "4.7.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"
149
149
  },
150
150
  "overrides": {
151
151
  "hono": "^4.12.18",
@@ -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
+ }