codexslimedit 0.1.0 → 0.7.4-dev.29700855995.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.
package/README.md CHANGED
@@ -12,11 +12,11 @@ bunx codexslimedit
12
12
 
13
13
  The MCP server exposes:
14
14
 
15
- - `read`: returns a root-relative path and file content without footer boilerplate.
16
- - `edit`: replaces one unique exact string or a 1-based inclusive line range such as `55-64`, then returns `OK <path>`.
15
+ - `read_file`: returns a root-relative path and file content without footer boilerplate.
16
+ - `apply_patch`: accepts Codex `*** Begin Patch` envelopes for `Add File`, `Update File`, and `Delete File`. It also retains a compact `filePath`/`oldString`/`newString` form for one unique exact replacement or inclusive line range such as `55-64`.
17
17
 
18
- Both tools constrain access to the server working directory. Reads reject non-UTF-8, NUL-containing, missing, non-file, traversal, and symlink-escape targets. Edits validate before writing, reject ambiguous matches and invalid ranges, preserve line endings and file mode, and use same-directory atomic replacement.
18
+ Both tools constrain access to the server working directory. Reads reject non-UTF-8, NUL-containing, missing, non-file, traversal, and symlink-escape targets. Patches create, update, and delete regular UTF-8 files; exact edits reject ambiguous matches and invalid ranges, preserve line endings and file mode, and use same-directory atomic replacement.
19
19
 
20
20
  ## Codex adaptation
21
21
 
22
- This package adapts ideas from OpenSlimEdit for supported Codex extension surfaces. Codex plugins cannot replace descriptions of native tools or replace model-facing `PostToolUse` output, and Codex native patch editing has no OpenCode `oldString` contract. `codexslimedit` therefore provides separate concise MCP tools and makes no upstream token-saving claim.
22
+ This package adapts ideas from OpenSlimEdit for supported Codex extension surfaces. Codex qualifies MCP tool names with their server namespace, so the model-facing names are `mcp__codexslimedit__read_file` and `mcp__codexslimedit__apply_patch`; MCP cannot overwrite Codex's built-in `apply_patch` registration. HolyCodex instructions require these tools for workspace reads and writes. Native patch envelopes preserve familiar patch ergonomics, while the compact exact-replacement form reduces simple-edit arguments and results. No upstream token-saving percentage is claimed without Codex-specific benchmarks.
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { a as CODEX_SLIM_EDIT_VERSION, n as runCodexSlimEditMcpStdioServer, o as isVersionRequest, s as stackOrMessageFromError } from "./mcp.js";
2
+ import { d as stackOrMessageFromError, i as isVersionRequest, n as runCodexSlimEditMcpStdioServer, r as CODEX_SLIM_EDIT_VERSION } from "./mcp.js";
3
3
  import { stderr } from "node:process";
4
4
  //#region src/cli.ts
5
5
  /** Starts the CodexSlimEdit MCP stdio entrypoint. */
package/dist/index.d.ts CHANGED
@@ -8,8 +8,10 @@ export declare function isVersionRequest(args: readonly string[]): boolean;
8
8
 
9
9
  /** Identifies a workspace file operation failure. */
10
10
  export type WorkspaceFileErrorCode =
11
+ | "ALREADY_EXISTS"
11
12
  | "EXACT_MATCH_NOT_FOUND"
12
13
  | "DUPLICATE_MATCH"
14
+ | "INVALID_PATCH"
13
15
  | "INVALID_RANGE"
14
16
  | "NOT_A_FILE"
15
17
  | "NOT_FOUND"
@@ -49,6 +51,26 @@ export interface WorkspaceFileResult {
49
51
  readonly content: string;
50
52
  }
51
53
 
54
+ /** Input for writing complete UTF-8 content to a workspace file. */
55
+ export interface WriteWorkspaceFileInput extends WorkspaceFileInput {
56
+ /** Complete replacement content. */
57
+ readonly content: string;
58
+ }
59
+
60
+ /** Input for a Codex-compatible workspace patch envelope. */
61
+ export interface ApplyWorkspacePatchInput {
62
+ /** Workspace root used to constrain file access. */
63
+ readonly root: string;
64
+ /** Codex `*** Begin Patch` envelope. */
65
+ readonly patch: string;
66
+ }
67
+
68
+ /** Paths changed by a workspace patch. */
69
+ export interface WorkspacePatchResult {
70
+ /** Canonical root-relative paths in patch order. */
71
+ readonly paths: readonly string[];
72
+ }
73
+
52
74
  /** Options for the CodexSlimEdit MCP server. */
53
75
  export interface CodexSlimEditMcpOptions {
54
76
  /** Workspace root; defaults to the server process current directory. */
@@ -63,6 +85,26 @@ export declare function editWorkspaceFile(
63
85
  input: EditWorkspaceFileInput,
64
86
  ): Promise<WorkspaceFileResult>;
65
87
 
88
+ /** Replaces an existing workspace file with complete UTF-8 content. */
89
+ export declare function writeWorkspaceFile(
90
+ input: WriteWorkspaceFileInput,
91
+ ): Promise<WorkspaceFileResult>;
92
+
93
+ /** Creates a new workspace file in an existing directory. */
94
+ export declare function createWorkspaceFile(
95
+ input: WriteWorkspaceFileInput,
96
+ ): Promise<WorkspaceFileResult>;
97
+
98
+ /** Deletes one existing regular workspace file. */
99
+ export declare function deleteWorkspaceFile(
100
+ input: WorkspaceFileInput,
101
+ ): Promise<WorkspaceFileResult>;
102
+
103
+ /** Applies Codex add, update, and delete patch operations inside a workspace. */
104
+ export declare function applyWorkspacePatch(
105
+ input: ApplyWorkspacePatchInput,
106
+ ): Promise<WorkspacePatchResult>;
107
+
66
108
  /** Handles one CodexSlimEdit MCP JSON-RPC request. */
67
109
  export declare function handleCodexSlimEditMcpRequest(
68
110
  input: unknown,
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as CODEX_SLIM_EDIT_VERSION, c as WorkspaceFileError, i as readWorkspaceFile, n as runCodexSlimEditMcpStdioServer, o as isVersionRequest, r as editWorkspaceFile, t as handleCodexSlimEditMcpRequest } from "./mcp.js";
2
- export { CODEX_SLIM_EDIT_VERSION, WorkspaceFileError, editWorkspaceFile, handleCodexSlimEditMcpRequest, isVersionRequest, readWorkspaceFile, runCodexSlimEditMcpStdioServer };
1
+ import { a as applyWorkspacePatch, c as editWorkspaceFile, f as WorkspaceFileError, i as isVersionRequest, l as readWorkspaceFile, n as runCodexSlimEditMcpStdioServer, o as createWorkspaceFile, r as CODEX_SLIM_EDIT_VERSION, s as deleteWorkspaceFile, t as handleCodexSlimEditMcpRequest, u as writeWorkspaceFile } from "./mcp.js";
2
+ export { CODEX_SLIM_EDIT_VERSION, WorkspaceFileError, applyWorkspacePatch, createWorkspaceFile, deleteWorkspaceFile, editWorkspaceFile, handleCodexSlimEditMcpRequest, isVersionRequest, readWorkspaceFile, runCodexSlimEditMcpStdioServer, writeWorkspaceFile };
package/dist/mcp.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "node:child_process";
2
- import { chmod, open, readFile, realpath, rename, rm, stat } from "node:fs/promises";
2
+ import { chmod, lstat, mkdir, open, readFile, realpath, rename, rm, stat } from "node:fs/promises";
3
3
  import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
4
4
  //#region src/errors.ts
5
5
  /** Reports a typed, actionable workspace file operation failure. */
@@ -4508,14 +4508,6 @@ function createIdleTimer(idleTimeoutMs, log, onIdleTimeout) {
4508
4508
  };
4509
4509
  }
4510
4510
  //#endregion
4511
- //#region src/version.ts
4512
- /** Current independent codexslimedit package version. */
4513
- var CODEX_SLIM_EDIT_VERSION = "0.1.0";
4514
- /** Returns whether command-line arguments request the package version. */
4515
- function isVersionRequest(args) {
4516
- return args.includes("--version");
4517
- }
4518
- //#endregion
4519
4511
  //#region src/workspace.ts
4520
4512
  var RANGE_PATTERN = /^\s*(?<start>[1-9]\d*)\s*(?:-\s*(?<end>[1-9]\d*)\s*)?$/;
4521
4513
  var TEMPORARY_FILE_PREFIX = ".codexslimedit-";
@@ -4537,6 +4529,53 @@ async function editWorkspaceFile(input) {
4537
4529
  content: nextContent
4538
4530
  };
4539
4531
  }
4532
+ /** Replaces an existing workspace file with complete UTF-8 content. */
4533
+ async function writeWorkspaceFile(input) {
4534
+ const target = await resolveWorkspaceFile(input);
4535
+ validateText(input.content);
4536
+ await atomicWrite(target.absolutePath, input.content);
4537
+ return {
4538
+ path: target.relativePath,
4539
+ content: input.content
4540
+ };
4541
+ }
4542
+ /** Creates a new workspace file in an existing directory. */
4543
+ async function createWorkspaceFile(input) {
4544
+ const target = await resolveNewWorkspaceFile(input);
4545
+ validateText(input.content);
4546
+ let file;
4547
+ let created = false;
4548
+ try {
4549
+ file = await open(target.absolutePath, "wx", 420);
4550
+ created = true;
4551
+ await file.writeFile(input.content, "utf8");
4552
+ await file.sync();
4553
+ await file.close();
4554
+ file = void 0;
4555
+ } catch (error) {
4556
+ await file?.close();
4557
+ if (created) await rm(target.absolutePath, { force: true });
4558
+ throw new WorkspaceFileError("WRITE_FAILED", `Could not create filePath: ${message(error)}`);
4559
+ }
4560
+ return {
4561
+ path: target.relativePath,
4562
+ content: input.content
4563
+ };
4564
+ }
4565
+ /** Deletes one existing regular workspace file. */
4566
+ async function deleteWorkspaceFile(input) {
4567
+ const target = await resolveWorkspaceFile(input);
4568
+ const content = await readUtf8Text(target.absolutePath);
4569
+ try {
4570
+ await rm(target.absolutePath);
4571
+ } catch (error) {
4572
+ throw new WorkspaceFileError("WRITE_FAILED", `Could not delete filePath: ${message(error)}`);
4573
+ }
4574
+ return {
4575
+ path: target.relativePath,
4576
+ content
4577
+ };
4578
+ }
4540
4579
  async function resolveWorkspaceFile(input) {
4541
4580
  const rootPath = await existingDirectory(input.root);
4542
4581
  const candidate = resolve(rootPath, normalizeSeparators(input.filePath));
@@ -4548,6 +4587,38 @@ async function resolveWorkspaceFile(input) {
4548
4587
  relativePath: relative(rootPath, resolvedCandidate).split(sep).join("/")
4549
4588
  };
4550
4589
  }
4590
+ async function resolveNewWorkspaceFile(input) {
4591
+ const rootPath = await existingDirectory(input.root);
4592
+ const candidate = resolve(rootPath, normalizeSeparators(input.filePath));
4593
+ if (!isInside(rootPath, candidate)) throw new WorkspaceFileError("PATH_OUTSIDE_ROOT", "filePath must remain inside the workspace root.");
4594
+ try {
4595
+ await lstat(candidate);
4596
+ throw new WorkspaceFileError("ALREADY_EXISTS", "filePath already exists.");
4597
+ } catch (error) {
4598
+ if (error instanceof WorkspaceFileError) throw error;
4599
+ }
4600
+ const absolutePath = resolve(await createSafeParent(rootPath, dirname(candidate)), basename(candidate));
4601
+ return {
4602
+ absolutePath,
4603
+ relativePath: relative(rootPath, absolutePath).split(sep).join("/")
4604
+ };
4605
+ }
4606
+ async function createSafeParent(rootPath, requestedParent) {
4607
+ const missingDirectories = [];
4608
+ let existingParent = requestedParent;
4609
+ while (true) try {
4610
+ existingParent = await realpath(existingParent);
4611
+ break;
4612
+ } catch {
4613
+ if (existingParent === rootPath) throw new WorkspaceFileError("NOT_FOUND", "Workspace root does not exist.");
4614
+ missingDirectories.push(basename(existingParent));
4615
+ existingParent = dirname(existingParent);
4616
+ }
4617
+ if (existingParent !== rootPath && !isInside(rootPath, existingParent)) throw new WorkspaceFileError("PATH_OUTSIDE_ROOT", "filePath parent resolves outside the workspace root through a symlink.");
4618
+ const parentPath = resolve(existingParent, ...missingDirectories.reverse());
4619
+ await mkdir(parentPath, { recursive: true });
4620
+ return parentPath;
4621
+ }
4551
4622
  async function existingDirectory(root) {
4552
4623
  try {
4553
4624
  const resolvedRoot = await realpath(resolve(root));
@@ -4584,22 +4655,25 @@ async function readUtf8Text(path) {
4584
4655
  }
4585
4656
  try {
4586
4657
  const content = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
4587
- if (content.includes("\0")) throw new TypeError("NUL byte");
4658
+ validateText(content);
4588
4659
  return content;
4589
4660
  } catch {
4590
4661
  throw new WorkspaceFileError("UNSUPPORTED_TEXT", "filePath must contain UTF-8 text without NUL bytes.");
4591
4662
  }
4592
4663
  }
4664
+ function validateText(content) {
4665
+ if (content.includes("\0")) throw new WorkspaceFileError("UNSUPPORTED_TEXT", "Text must not contain NUL bytes.");
4666
+ }
4593
4667
  function replaceContent(content, oldString, newString) {
4668
+ const range = parseRange(oldString, lineCount(content));
4669
+ if (range !== null) return replaceLineRange(content, range.start, range.end, newString);
4594
4670
  const matches = exactMatchOffsets(content, oldString);
4595
4671
  if (matches.length === 1) {
4596
4672
  const [start] = matches;
4597
4673
  return `${content.slice(0, start)}${newString}${content.slice(start + oldString.length)}`;
4598
4674
  }
4599
4675
  if (matches.length > 1) throw new WorkspaceFileError("DUPLICATE_MATCH", "oldString matches more than once; provide unique exact content or a line range.");
4600
- const range = parseRange(oldString, lineCount(content));
4601
- if (range === null) throw new WorkspaceFileError("EXACT_MATCH_NOT_FOUND", "oldString was not found exactly and is not a valid line range.");
4602
- return replaceLineRange(content, range.start, range.end, newString);
4676
+ throw new WorkspaceFileError("EXACT_MATCH_NOT_FOUND", "oldString was not found exactly and is not a valid line range.");
4603
4677
  }
4604
4678
  function exactMatchOffsets(content, needle) {
4605
4679
  if (needle.length === 0) return [];
@@ -4633,7 +4707,7 @@ function replaceLineRange(content, start, end, newString) {
4633
4707
  const lines = content.split(/\r\n|\n|\r/);
4634
4708
  const hasFinalNewline = endsWithLineBreak(content);
4635
4709
  const editableLines = hasFinalNewline ? lines.slice(0, -1) : lines;
4636
- const replacement = newString === "" ? [] : normalizeLineEndings(newString, lineEnding).split(/\r\n|\n|\r/);
4710
+ const replacement = replacementLines(newString, lineEnding);
4637
4711
  const nextLines = [
4638
4712
  ...editableLines.slice(0, start - 1),
4639
4713
  ...replacement,
@@ -4642,6 +4716,12 @@ function replaceLineRange(content, start, end, newString) {
4642
4716
  if (nextLines.length === 0) return "";
4643
4717
  return `${nextLines.join(lineEnding)}${hasFinalNewline ? lineEnding : ""}`;
4644
4718
  }
4719
+ function replacementLines(newString, lineEnding) {
4720
+ if (newString === "") return [];
4721
+ const normalized = normalizeLineEndings(newString, lineEnding);
4722
+ const lines = normalized.split(lineEnding);
4723
+ return endsWithLineBreak(normalized) ? lines.slice(0, -1) : lines;
4724
+ }
4645
4725
  function endsWithLineBreak(content) {
4646
4726
  return /(?:\r\n|\n|\r)$/.test(content);
4647
4727
  }
@@ -4681,14 +4761,168 @@ function message(error) {
4681
4761
  return error instanceof Error ? error.message : String(error);
4682
4762
  }
4683
4763
  //#endregion
4764
+ //#region src/patch.ts
4765
+ /** Applies Codex add, update, and delete patch operations inside a workspace. */
4766
+ async function applyWorkspacePatch(input) {
4767
+ const operations = parsePatch(input.patch);
4768
+ const paths = [];
4769
+ for (const operation of operations) {
4770
+ if (operation.kind === "add") {
4771
+ const result = await createWorkspaceFile({
4772
+ root: input.root,
4773
+ filePath: operation.filePath,
4774
+ content: operation.content
4775
+ });
4776
+ paths.push(result.path);
4777
+ continue;
4778
+ }
4779
+ if (operation.kind === "delete") {
4780
+ const result = await deleteWorkspaceFile({
4781
+ root: input.root,
4782
+ filePath: operation.filePath
4783
+ });
4784
+ paths.push(result.path);
4785
+ continue;
4786
+ }
4787
+ const current = await readWorkspaceFile({
4788
+ root: input.root,
4789
+ filePath: operation.filePath
4790
+ });
4791
+ const result = await writeWorkspaceFile({
4792
+ root: input.root,
4793
+ filePath: operation.filePath,
4794
+ content: applyUpdateHunks(current.content, operation.hunks)
4795
+ });
4796
+ paths.push(result.path);
4797
+ }
4798
+ return { paths };
4799
+ }
4800
+ function parsePatch(patch) {
4801
+ const lines = patch.replace(/\r\n|\r/g, "\n").split("\n");
4802
+ while (lines.at(-1) === "") lines.pop();
4803
+ if (lines[0] !== "*** Begin Patch" || lines.at(-1) !== "*** End Patch") invalidPatch("Patch must start with `*** Begin Patch` and end with `*** End Patch`.");
4804
+ const operations = [];
4805
+ let index = 1;
4806
+ while (index < lines.length - 1) {
4807
+ const header = lines[index];
4808
+ const addPath = header?.match(/^\*\*\* Add File: (.+)$/)?.[1];
4809
+ const updatePath = header?.match(/^\*\*\* Update File: (.+)$/)?.[1];
4810
+ const deletePath = header?.match(/^\*\*\* Delete File: (.+)$/)?.[1];
4811
+ index += 1;
4812
+ if (addPath !== void 0) {
4813
+ const contentLines = [];
4814
+ while (index < lines.length - 1 && !lines[index]?.startsWith("*** ")) {
4815
+ const line = lines[index];
4816
+ if (!line?.startsWith("+")) invalidPatch("Add File lines must start with `+`.");
4817
+ contentLines.push(line.slice(1));
4818
+ index += 1;
4819
+ }
4820
+ operations.push({
4821
+ kind: "add",
4822
+ filePath: filePath(addPath),
4823
+ content: contentLines.length === 0 ? "" : `${contentLines.join("\n")}\n`
4824
+ });
4825
+ continue;
4826
+ }
4827
+ if (deletePath !== void 0) {
4828
+ operations.push({
4829
+ kind: "delete",
4830
+ filePath: filePath(deletePath)
4831
+ });
4832
+ continue;
4833
+ }
4834
+ if (updatePath !== void 0) {
4835
+ const hunks = [];
4836
+ let oldLines;
4837
+ let newLines;
4838
+ while (index < lines.length - 1 && !lines[index]?.startsWith("*** ")) {
4839
+ const line = lines[index];
4840
+ if (line?.startsWith("@@")) {
4841
+ if (oldLines !== void 0 && newLines !== void 0) hunks.push(validHunk(oldLines, newLines));
4842
+ oldLines = [];
4843
+ newLines = [];
4844
+ index += 1;
4845
+ continue;
4846
+ }
4847
+ if (oldLines === void 0 || newLines === void 0 || line === void 0) invalidPatch("Update File content must begin with an `@@` hunk header.");
4848
+ const prefix = line[0];
4849
+ const content = line.slice(1);
4850
+ if (prefix === " " || prefix === "-") oldLines.push(content);
4851
+ if (prefix === " " || prefix === "+") newLines.push(content);
4852
+ if (prefix !== " " && prefix !== "-" && prefix !== "+") invalidPatch("Update hunk lines must start with a space, `-`, or `+`.");
4853
+ index += 1;
4854
+ }
4855
+ if (oldLines !== void 0 && newLines !== void 0) hunks.push(validHunk(oldLines, newLines));
4856
+ if (hunks.length === 0) invalidPatch("Update File requires at least one hunk.");
4857
+ operations.push({
4858
+ kind: "update",
4859
+ filePath: filePath(updatePath),
4860
+ hunks
4861
+ });
4862
+ continue;
4863
+ }
4864
+ invalidPatch(`Unknown patch operation: ${header ?? "<missing>"}`);
4865
+ }
4866
+ if (operations.length === 0) invalidPatch("Patch must contain at least one file operation.");
4867
+ return operations;
4868
+ }
4869
+ function validHunk(oldLines, newLines) {
4870
+ if (oldLines.length === 0) invalidPatch("Update hunks require old or context lines for deterministic placement.");
4871
+ return {
4872
+ oldLines,
4873
+ newLines
4874
+ };
4875
+ }
4876
+ function applyUpdateHunks(content, hunks) {
4877
+ const lineEnding = content.includes("\r\n") ? "\r\n" : "\n";
4878
+ const normalized = content.replace(/\r\n|\r/g, "\n");
4879
+ const hasFinalNewline = normalized.endsWith("\n");
4880
+ const lines = normalized.split("\n");
4881
+ if (hasFinalNewline) lines.pop();
4882
+ let cursor = 0;
4883
+ for (const hunk of hunks) {
4884
+ const offset = uniqueSequenceOffset(lines, hunk.oldLines, cursor);
4885
+ lines.splice(offset, hunk.oldLines.length, ...hunk.newLines);
4886
+ cursor = offset + hunk.newLines.length;
4887
+ }
4888
+ if (lines.length === 0) return "";
4889
+ return `${lines.join(lineEnding)}${hasFinalNewline ? lineEnding : ""}`;
4890
+ }
4891
+ function uniqueSequenceOffset(lines, expected, cursor) {
4892
+ const offsets = [];
4893
+ for (let offset = cursor; offset <= lines.length - expected.length; offset += 1) if (expected.every((line, index) => lines[offset + index] === line)) offsets.push(offset);
4894
+ if (offsets.length === 1) return offsets[0] ?? 0;
4895
+ if (offsets.length > 1) throw new WorkspaceFileError("DUPLICATE_MATCH", "Patch hunk matches more than once; include more unchanged context.");
4896
+ throw new WorkspaceFileError("EXACT_MATCH_NOT_FOUND", "Patch hunk context was not found exactly.");
4897
+ }
4898
+ function filePath(value) {
4899
+ const trimmed = value.trim();
4900
+ if (trimmed === "") invalidPatch("Patch file paths must not be empty.");
4901
+ return trimmed;
4902
+ }
4903
+ function invalidPatch(message) {
4904
+ throw new WorkspaceFileError("INVALID_PATCH", message);
4905
+ }
4906
+ //#endregion
4907
+ //#region src/version.ts
4908
+ /** Current independent codexslimedit package version. */
4909
+ var CODEX_SLIM_EDIT_VERSION = "0.7.4-dev.29700855995.1";
4910
+ /** Returns whether command-line arguments request the package version. */
4911
+ function isVersionRequest(args) {
4912
+ return args.includes("--version");
4913
+ }
4914
+ //#endregion
4684
4915
  //#region src/mcp.ts
4685
4916
  var InitializeParamsSchema = looseObject({ protocolVersion: string() });
4686
4917
  var ReadArgumentsSchema = strictObject({ filePath: string().min(1) });
4687
- var EditArgumentsSchema = strictObject({
4918
+ var ExactEditArgumentsSchema = strictObject({
4688
4919
  filePath: string().min(1),
4689
4920
  oldString: string().min(1),
4690
4921
  newString: string()
4691
4922
  });
4923
+ var ApplyPatchArgumentsSchema = union([strictObject({ patch: string().min(1) }), ExactEditArgumentsSchema]);
4924
+ var READ_FILE_TOOL_NAME = "read_file";
4925
+ var APPLY_PATCH_TOOL_NAME = "apply_patch";
4692
4926
  /** Handles one CodexSlimEdit MCP JSON-RPC request. */
4693
4927
  async function handleCodexSlimEditMcpRequest(input, options = {}) {
4694
4928
  const request = JsonRpcRequestSchema.safeParse(input);
@@ -4725,53 +4959,89 @@ async function runCodexSlimEditMcpStdioServer(input, output, options = {}) {
4725
4959
  });
4726
4960
  }
4727
4961
  var TOOL_DEFINITIONS = [{
4728
- name: "read",
4729
- description: "Read one UTF-8 workspace file concisely.",
4962
+ name: READ_FILE_TOOL_NAME,
4963
+ description: "Required tool for reading one complete UTF-8 workspace file. Returns the workspace-relative path and exact content without metadata or footer boilerplate.",
4730
4964
  inputSchema: {
4731
4965
  type: "object",
4732
- properties: { filePath: { type: "string" } },
4966
+ properties: { filePath: {
4967
+ type: "string",
4968
+ description: "The workspace-relative path of the file to read."
4969
+ } },
4733
4970
  required: ["filePath"],
4734
4971
  additionalProperties: false
4972
+ },
4973
+ annotations: {
4974
+ readOnlyHint: true,
4975
+ destructiveHint: false,
4976
+ idempotentHint: true,
4977
+ openWorldHint: false
4735
4978
  }
4736
4979
  }, {
4737
- name: "edit",
4738
- description: "Atomically replace unique content or an inclusive line range.",
4980
+ name: APPLY_PATCH_TOOL_NAME,
4981
+ description: "Required workspace write tool. Use a native `*** Begin Patch` envelope to add, update, or delete files; for a smaller single replacement, pass filePath, oldString, and newString.",
4739
4982
  inputSchema: {
4740
4983
  type: "object",
4741
4984
  properties: {
4742
- filePath: { type: "string" },
4743
- oldString: { type: "string" },
4744
- newString: { type: "string" }
4985
+ patch: {
4986
+ type: "string",
4987
+ description: "Native Codex patch envelope with Add File, Update File, or Delete File operations."
4988
+ },
4989
+ filePath: {
4990
+ type: "string",
4991
+ description: "The workspace-relative path for a compact single-file replacement."
4992
+ },
4993
+ oldString: {
4994
+ type: "string",
4995
+ description: "The unique exact text, or inclusive 1-based line number or N-M range to replace."
4996
+ },
4997
+ newString: {
4998
+ type: "string",
4999
+ description: "Replacement text; may be empty."
5000
+ }
4745
5001
  },
4746
- required: [
5002
+ oneOf: [{ required: ["patch"] }, { required: [
4747
5003
  "filePath",
4748
5004
  "oldString",
4749
5005
  "newString"
4750
- ],
5006
+ ] }],
4751
5007
  additionalProperties: false
5008
+ },
5009
+ annotations: {
5010
+ readOnlyHint: false,
5011
+ destructiveHint: true,
5012
+ idempotentHint: false,
5013
+ openWorldHint: false
4752
5014
  }
4753
5015
  }];
4754
5016
  async function callTool(id, name, arguments_, options) {
4755
5017
  const root = options.root ?? process.cwd();
4756
- if (name === "read") {
5018
+ if (name === READ_FILE_TOOL_NAME) {
4757
5019
  const parsed = ReadArgumentsSchema.safeParse(arguments_);
4758
- if (!parsed.success) return toolResponse(id, "read.filePath must be a non-empty string.", true);
4759
- return fileOperationResponse(id, () => readWorkspaceFile({
5020
+ if (!parsed.success) return toolResponse(id, `${READ_FILE_TOOL_NAME}.filePath must be a non-empty string.`, true);
5021
+ return operationResponse(id, () => readWorkspaceFile({
4760
5022
  root,
4761
5023
  ...parsed.data
4762
5024
  }), (result) => `${result.path}\n${result.content}`);
4763
5025
  }
4764
- if (name === "edit") {
4765
- const parsed = EditArgumentsSchema.safeParse(arguments_);
4766
- if (!parsed.success) return toolResponse(id, "edit requires non-empty filePath and oldString plus string newString.", true);
4767
- return fileOperationResponse(id, () => editWorkspaceFile({
5026
+ if (name === APPLY_PATCH_TOOL_NAME) {
5027
+ const parsed = ApplyPatchArgumentsSchema.safeParse(arguments_);
5028
+ if (!parsed.success) return toolResponse(id, `${APPLY_PATCH_TOOL_NAME} requires a non-empty patch envelope or non-empty filePath and oldString plus string newString.`, true);
5029
+ const parsedArguments = parsed.data;
5030
+ if ("patch" in parsedArguments) {
5031
+ const patch = parsedArguments.patch;
5032
+ return operationResponse(id, () => applyWorkspacePatch({
5033
+ root,
5034
+ patch
5035
+ }), () => "Done!");
5036
+ }
5037
+ return operationResponse(id, () => editWorkspaceFile({
4768
5038
  root,
4769
- ...parsed.data
5039
+ ...parsedArguments
4770
5040
  }), (result) => `OK ${result.path}`);
4771
5041
  }
4772
5042
  return toolResponse(id, `Unknown codexslimedit tool: ${name}`, true);
4773
5043
  }
4774
- async function fileOperationResponse(id, operation, successText) {
5044
+ async function operationResponse(id, operation, successText) {
4775
5045
  try {
4776
5046
  return toolResponse(id, successText(await operation()));
4777
5047
  } catch (error) {
@@ -4788,4 +5058,4 @@ function toolResponse(id, text, isError = false) {
4788
5058
  });
4789
5059
  }
4790
5060
  //#endregion
4791
- export { CODEX_SLIM_EDIT_VERSION as a, WorkspaceFileError as c, readWorkspaceFile as i, runCodexSlimEditMcpStdioServer as n, isVersionRequest as o, editWorkspaceFile as r, stackOrMessageFromError as s, handleCodexSlimEditMcpRequest as t };
5061
+ export { applyWorkspacePatch as a, editWorkspaceFile as c, stackOrMessageFromError as d, WorkspaceFileError as f, isVersionRequest as i, readWorkspaceFile as l, runCodexSlimEditMcpStdioServer as n, createWorkspaceFile as o, CODEX_SLIM_EDIT_VERSION as r, deleteWorkspaceFile as s, handleCodexSlimEditMcpRequest as t, writeWorkspaceFile as u };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexslimedit",
3
- "version": "0.1.0",
3
+ "version": "0.7.4-dev.29700855995.1",
4
4
  "description": "Concise, workspace-safe MCP file reading and editing.",
5
5
  "license": "SUL-1.0",
6
6
  "repository": {