@cjhyy/code-shell-capability-coding 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +69 -0
  2. package/THIRD_PARTY_NOTICES.md +206 -0
  3. package/dist/bin/agent-server-stdio.d.ts +1 -0
  4. package/dist/bin/agent-server-stdio.js +4 -0
  5. package/dist/capability-runtime.d.ts +22 -0
  6. package/dist/capability-runtime.js +88 -0
  7. package/dist/cc-orchestrator/agent-adapter.d.ts +55 -0
  8. package/dist/cc-orchestrator/agent-adapter.js +143 -0
  9. package/dist/cc-orchestrator/cc-capability.d.ts +20 -0
  10. package/dist/cc-orchestrator/cc-capability.js +57 -0
  11. package/dist/cc-orchestrator/codex-session-discovery.d.ts +45 -0
  12. package/dist/cc-orchestrator/codex-session-discovery.js +251 -0
  13. package/dist/cc-orchestrator/codex-session-history.d.ts +39 -0
  14. package/dist/cc-orchestrator/codex-session-history.js +249 -0
  15. package/dist/cc-orchestrator/codex-user-text.d.ts +9 -0
  16. package/dist/cc-orchestrator/codex-user-text.js +25 -0
  17. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  18. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  19. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  20. package/dist/cc-orchestrator/external-agent-changes.js +231 -0
  21. package/dist/cc-orchestrator/external-agent-driver.d.ts +19 -0
  22. package/dist/cc-orchestrator/external-agent-driver.js +284 -0
  23. package/dist/cc-orchestrator/external-agent-session-store.d.ts +36 -0
  24. package/dist/cc-orchestrator/external-agent-session-store.js +185 -0
  25. package/dist/cc-orchestrator/index.d.ts +10 -0
  26. package/dist/cc-orchestrator/index.js +10 -0
  27. package/dist/cc-orchestrator/related-session-discovery.d.ts +21 -0
  28. package/dist/cc-orchestrator/related-session-discovery.js +77 -0
  29. package/dist/cc-orchestrator/relevance-judge.d.ts +16 -0
  30. package/dist/cc-orchestrator/relevance-judge.js +38 -0
  31. package/dist/cc-orchestrator/session-discovery.d.ts +99 -0
  32. package/dist/cc-orchestrator/session-discovery.js +295 -0
  33. package/dist/cc-orchestrator/session-history.d.ts +55 -0
  34. package/dist/cc-orchestrator/session-history.js +150 -0
  35. package/dist/external-agents/config.d.ts +3 -0
  36. package/dist/external-agents/config.js +16 -0
  37. package/dist/external-agents/types.d.ts +32 -0
  38. package/dist/external-agents/types.js +1 -0
  39. package/dist/external-runtimes/claude-code/event-translator.d.ts +54 -0
  40. package/dist/external-runtimes/claude-code/event-translator.js +201 -0
  41. package/dist/external-runtimes/claude-code/mcp-config.d.ts +84 -0
  42. package/dist/external-runtimes/claude-code/mcp-config.js +175 -0
  43. package/dist/external-runtimes/claude-code/runtime.d.ts +56 -0
  44. package/dist/external-runtimes/claude-code/runtime.js +168 -0
  45. package/dist/external-runtimes/codex/app-server-client.d.ts +66 -0
  46. package/dist/external-runtimes/codex/app-server-client.js +244 -0
  47. package/dist/external-runtimes/codex/event-translator.d.ts +68 -0
  48. package/dist/external-runtimes/codex/event-translator.js +187 -0
  49. package/dist/external-runtimes/codex/index.d.ts +7 -0
  50. package/dist/external-runtimes/codex/index.js +6 -0
  51. package/dist/external-runtimes/codex/runtime.d.ts +128 -0
  52. package/dist/external-runtimes/codex/runtime.js +216 -0
  53. package/dist/external-runtimes/index.d.ts +37 -0
  54. package/dist/external-runtimes/index.js +27 -0
  55. package/dist/external-runtimes/session-factory.d.ts +97 -0
  56. package/dist/external-runtimes/session-factory.js +110 -0
  57. package/dist/external-runtimes/shared/mcp-bridge.d.ts +66 -0
  58. package/dist/external-runtimes/shared/mcp-bridge.js +365 -0
  59. package/dist/external-runtimes/shared/session-context-store.d.ts +83 -0
  60. package/dist/external-runtimes/shared/session-context-store.js +114 -0
  61. package/dist/external-runtimes/shared/spawn-env.d.ts +30 -0
  62. package/dist/external-runtimes/shared/spawn-env.js +41 -0
  63. package/dist/git/parse-log.d.ts +14 -0
  64. package/dist/git/parse-log.js +21 -0
  65. package/dist/git/utils.d.ts +49 -0
  66. package/dist/git/utils.js +161 -0
  67. package/dist/git/worktree/crud.d.ts +108 -0
  68. package/dist/git/worktree/crud.js +415 -0
  69. package/dist/git/worktree/diff.d.ts +14 -0
  70. package/dist/git/worktree/diff.js +83 -0
  71. package/dist/git/worktree/git-exec.d.ts +7 -0
  72. package/dist/git/worktree/git-exec.js +86 -0
  73. package/dist/git/worktree/index.d.ts +6 -0
  74. package/dist/git/worktree/index.js +6 -0
  75. package/dist/git/worktree/query.d.ts +43 -0
  76. package/dist/git/worktree/query.js +128 -0
  77. package/dist/git/worktree/slug.d.ts +12 -0
  78. package/dist/git/worktree/slug.js +59 -0
  79. package/dist/git/worktree.d.ts +1 -0
  80. package/dist/git/worktree.js +5 -0
  81. package/dist/index.capability.d.ts +7 -0
  82. package/dist/index.capability.js +190 -0
  83. package/dist/index.d.ts +18 -0
  84. package/dist/index.git.d.ts +4 -0
  85. package/dist/index.git.js +4 -0
  86. package/dist/index.js +18 -0
  87. package/dist/index.orchestration.d.ts +7 -0
  88. package/dist/index.orchestration.js +5 -0
  89. package/dist/lsp/client.d.ts +41 -0
  90. package/dist/lsp/client.js +192 -0
  91. package/dist/lsp/manager.d.ts +53 -0
  92. package/dist/lsp/manager.js +248 -0
  93. package/dist/lsp/root-path.d.ts +9 -0
  94. package/dist/lsp/root-path.js +12 -0
  95. package/dist/lsp/servers.d.ts +16 -0
  96. package/dist/lsp/servers.js +60 -0
  97. package/dist/prompt/coding.md +42 -0
  98. package/dist/quota/credentials.d.ts +3 -0
  99. package/dist/quota/credentials.js +80 -0
  100. package/dist/quota/index.d.ts +36 -0
  101. package/dist/quota/index.js +161 -0
  102. package/dist/quota/types.d.ts +48 -0
  103. package/dist/quota/types.js +13 -0
  104. package/dist/review/review-prompt.d.ts +28 -0
  105. package/dist/review/review-prompt.js +81 -0
  106. package/dist/tools/apply-patch/LICENSE-codex +201 -0
  107. package/dist/tools/apply-patch/NOTICE.md +29 -0
  108. package/dist/tools/apply-patch/applier.d.ts +26 -0
  109. package/dist/tools/apply-patch/applier.js +306 -0
  110. package/dist/tools/apply-patch/backup-targets.d.ts +10 -0
  111. package/dist/tools/apply-patch/backup-targets.js +30 -0
  112. package/dist/tools/apply-patch/eol.d.ts +4 -0
  113. package/dist/tools/apply-patch/eol.js +9 -0
  114. package/dist/tools/apply-patch/index.d.ts +20 -0
  115. package/dist/tools/apply-patch/index.js +106 -0
  116. package/dist/tools/apply-patch/parser.d.ts +17 -0
  117. package/dist/tools/apply-patch/parser.js +209 -0
  118. package/dist/tools/apply-patch/seek-sequence.d.ts +18 -0
  119. package/dist/tools/apply-patch/seek-sequence.js +123 -0
  120. package/dist/tools/apply-patch/types.d.ts +49 -0
  121. package/dist/tools/apply-patch/types.js +13 -0
  122. package/dist/tools/brief.d.ts +6 -0
  123. package/dist/tools/brief.js +41 -0
  124. package/dist/tools/check-quota.d.ts +14 -0
  125. package/dist/tools/check-quota.js +38 -0
  126. package/dist/tools/drive-agent.d.ts +52 -0
  127. package/dist/tools/drive-agent.js +1184 -0
  128. package/dist/tools/lsp.d.ts +6 -0
  129. package/dist/tools/lsp.js +220 -0
  130. package/dist/tools/notebook-edit.d.ts +6 -0
  131. package/dist/tools/notebook-edit.js +127 -0
  132. package/dist/tools/worktree.d.ts +10 -0
  133. package/dist/tools/worktree.js +363 -0
  134. package/package.json +60 -0
@@ -0,0 +1,29 @@
1
+ # Third-party attribution
2
+
3
+ Files in this directory contain code adapted from
4
+ [OpenAI Codex `codex-rs/apply-patch`](https://github.com/openai/codex/tree/main/codex-rs/apply-patch),
5
+ licensed under the Apache License 2.0. See `LICENSE-codex` for the full
6
+ license text.
7
+
8
+ ## What was adapted
9
+
10
+ | Source (Rust) | Target (TypeScript) | Notes |
11
+ | ---------------------------------- | ----------------------------- | --------------------------------------------- |
12
+ | `src/seek_sequence.rs` | `seek-sequence.ts` | Direct port — same four-pass strategy |
13
+ | `src/parser.rs` | `parser.ts` | Direct port of the V4A grammar parser |
14
+ | `src/lib.rs` (chunk replacement) | `applier.ts` | `compute_replacements` algorithm port |
15
+ | `apply_patch_tool_instructions.md` | tool description (`index.ts`) | Patch-format prompt cribbed from the original |
16
+ | `tests/fixtures/scenarios/` | `tests/fixtures/apply-patch/` | 22 golden scenarios used verbatim |
17
+
18
+ ## Intentional behavioral divergence
19
+
20
+ `applier.ts` adds a two-phase commit (plan → snapshot → write → rollback)
21
+ that Codex does not have. As a consequence:
22
+
23
+ - Fixture `015_failure_after_partial_success_leaves_changes` produces a
24
+ different on-disk result here than in Codex: we leave **no** files
25
+ behind when a hunk fails, whereas Codex leaves the partial writes from
26
+ earlier hunks. The conformance test asserts our stronger guarantee.
27
+
28
+ Set `allowPartialOnCommit: true` on `applyPatch()` to fall back to
29
+ codex-style non-atomic semantics if needed.
@@ -0,0 +1,26 @@
1
+ import { Hunk } from "./types.js";
2
+ export interface ApplyPatchOptions {
3
+ /** Working directory for resolving relative paths in the patch. */
4
+ cwd: string;
5
+ /**
6
+ * If true, skip the snapshot/rollback safety net and apply each planned
7
+ * change one at a time, leaving partial work on disk if a later write
8
+ * fails. Mostly useful for codex-parity testing. Default: false.
9
+ */
10
+ allowPartialOnCommit?: boolean;
11
+ }
12
+ export interface ApplyPatchResult {
13
+ added: string[];
14
+ modified: string[];
15
+ deleted: string[];
16
+ }
17
+ /**
18
+ * Plan and commit the patch atomically.
19
+ *
20
+ * Phase 1 (plan): walk every hunk, compute the resulting file content in
21
+ * memory. Throws if any hunk cannot be applied; nothing is written.
22
+ *
23
+ * Phase 2 (commit): write all planned changes. If any write fails, restore
24
+ * from the in-memory snapshot captured in phase 1 and rethrow.
25
+ */
26
+ export declare function applyPatch(hunks: Hunk[], options: ApplyPatchOptions): Promise<ApplyPatchResult>;
@@ -0,0 +1,306 @@
1
+ // Atomic patch applier — dry-runs all hunks, then commits all-or-nothing.
2
+ //
3
+ // This deliberately diverges from codex-rs/apply-patch which applies hunks
4
+ // one-by-one and leaves partial changes on disk if a later hunk fails (see
5
+ // fixture 015_failure_after_partial_success_leaves_changes for codex's
6
+ // behavior). We treat the patch as a transaction: either every hunk applies
7
+ // or none do. Set `allowPartialOnCommit` to mimic codex semantics for the
8
+ // rare case where you want it.
9
+ //
10
+ // Algorithm uses code adapted from codex-rs/apply-patch/src/lib.rs (Apache-2.0)
11
+ // for the chunk-replacement phase (compute_replacements, apply_replacements).
12
+ import { mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
13
+ import { existsSync, realpathSync } from "node:fs";
14
+ import { dirname, isAbsolute, resolve, sep } from "node:path";
15
+ import { seekSequence } from "./seek-sequence.js";
16
+ import { detectEol, toLf, applyEol } from "./eol.js";
17
+ /**
18
+ * Plan and commit the patch atomically.
19
+ *
20
+ * Phase 1 (plan): walk every hunk, compute the resulting file content in
21
+ * memory. Throws if any hunk cannot be applied; nothing is written.
22
+ *
23
+ * Phase 2 (commit): write all planned changes. If any write fails, restore
24
+ * from the in-memory snapshot captured in phase 1 and rethrow.
25
+ */
26
+ export async function applyPatch(hunks, options) {
27
+ if (hunks.length === 0) {
28
+ throw new Error("No files were modified.");
29
+ }
30
+ const planned = await planHunks(hunks, options.cwd);
31
+ return commitPlanned(planned, !!options.allowPartialOnCommit);
32
+ }
33
+ async function planHunks(hunks, cwd) {
34
+ const set = {
35
+ byPath: new Map(),
36
+ added: [],
37
+ modified: [],
38
+ deleted: [],
39
+ };
40
+ function schedule(path, change) {
41
+ if (set.byPath.has(path)) {
42
+ throw new Error(`Patch references ${path} more than once. Combine the operations into a single hunk.`);
43
+ }
44
+ set.byPath.set(path, change);
45
+ }
46
+ for (const hunk of hunks) {
47
+ const original = hunk.path;
48
+ const sourcePath = resolveAgainst(original, cwd);
49
+ if (hunk.kind === "add") {
50
+ const existed = await readIfExists(sourcePath);
51
+ schedule(sourcePath, {
52
+ path: sourcePath,
53
+ newContent: hunk.contents,
54
+ originalContent: existed,
55
+ });
56
+ set.added.push(original);
57
+ continue;
58
+ }
59
+ if (hunk.kind === "delete") {
60
+ const stats = await safeStat(sourcePath);
61
+ if (!stats)
62
+ throw new Error(`Failed to delete file: ${sourcePath} not found`);
63
+ if (stats.isDirectory()) {
64
+ throw new Error(`Failed to delete file: ${sourcePath} is a directory`);
65
+ }
66
+ const content = await readFile(sourcePath, "utf-8");
67
+ schedule(sourcePath, {
68
+ path: sourcePath,
69
+ newContent: null,
70
+ originalContent: content,
71
+ });
72
+ set.deleted.push(original);
73
+ continue;
74
+ }
75
+ // Update (with optional rename).
76
+ const stats = await safeStat(sourcePath);
77
+ if (!stats) {
78
+ throw new Error(`Failed to update file: ${sourcePath} does not exist`);
79
+ }
80
+ const originalRaw = await readFile(sourcePath, "utf-8");
81
+ // CRLF handling: patch context lines are LF (the model emits LF). Match in
82
+ // LF space, then write back in the file's original EOL so a CRLF file isn't
83
+ // silently converted to LF (which would diff the whole file). originalText
84
+ // (LF) is what we compare; originalRaw is preserved for rollback so a
85
+ // restore writes back the exact bytes. See eol.ts.
86
+ const eol = detectEol(originalRaw);
87
+ const originalText = toLf(originalRaw);
88
+ const newText = applyEol(applyChunksToText(originalText, hunk.chunks, sourcePath), eol);
89
+ if (hunk.movePath !== undefined) {
90
+ const destPath = resolveAgainst(hunk.movePath, cwd);
91
+ if (destPath === sourcePath) {
92
+ // Rename to self → degenerate update.
93
+ schedule(sourcePath, {
94
+ path: sourcePath,
95
+ newContent: newText,
96
+ originalContent: originalRaw,
97
+ });
98
+ }
99
+ else {
100
+ const destOriginal = await readIfExists(destPath);
101
+ schedule(sourcePath, {
102
+ path: sourcePath,
103
+ newContent: null,
104
+ originalContent: originalRaw,
105
+ });
106
+ schedule(destPath, {
107
+ path: destPath,
108
+ newContent: newText,
109
+ originalContent: destOriginal,
110
+ });
111
+ }
112
+ set.modified.push(original);
113
+ }
114
+ else {
115
+ schedule(sourcePath, {
116
+ path: sourcePath,
117
+ newContent: newText,
118
+ originalContent: originalRaw,
119
+ });
120
+ set.modified.push(original);
121
+ }
122
+ }
123
+ return set;
124
+ }
125
+ /** Apply the chunk list to a file's text and return the new text. */
126
+ function applyChunksToText(original, chunks, pathForError) {
127
+ // split('\n') leaves an empty trailing element when the file ends with \n;
128
+ // drop it so line indices match standard diff semantics. We re-add the
129
+ // trailing newline at the end.
130
+ const lines = original.split("\n");
131
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
132
+ lines.pop();
133
+ }
134
+ // Each replacement: [startIndex, oldLen, newLines]
135
+ const replacements = [];
136
+ let lineIndex = 0;
137
+ for (const chunk of chunks) {
138
+ if (chunk.changeContext !== undefined) {
139
+ const found = seekSequence(lines, [chunk.changeContext], lineIndex, false);
140
+ if (found === null) {
141
+ throw new Error(`Failed to find context '${chunk.changeContext}' in ${pathForError}`);
142
+ }
143
+ lineIndex = found + 1;
144
+ }
145
+ if (chunk.oldLines.length === 0) {
146
+ // Pure addition — append at end. Mirrors codex behavior: even when the
147
+ // chunk has a `@@ <context>` anchor, pure additions are inserted at
148
+ // EOF rather than near the anchor. Models should include at least one
149
+ // `-` or ` ` line if they want positional insertion.
150
+ const insertionIdx = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
151
+ replacements.push([insertionIdx, 0, [...chunk.newLines]]);
152
+ continue;
153
+ }
154
+ let pattern = chunk.oldLines;
155
+ let newSlice = chunk.newLines;
156
+ let found = seekSequence(lines, pattern, lineIndex, chunk.isEndOfFile);
157
+ if (found === null && pattern.length > 0 && pattern[pattern.length - 1] === "") {
158
+ // Retry without trailing empty sentinel that represents the file's final newline.
159
+ pattern = pattern.slice(0, -1);
160
+ if (newSlice.length > 0 && newSlice[newSlice.length - 1] === "") {
161
+ newSlice = newSlice.slice(0, -1);
162
+ }
163
+ found = seekSequence(lines, pattern, lineIndex, chunk.isEndOfFile);
164
+ }
165
+ if (found === null) {
166
+ throw new Error(`Failed to find expected lines in ${pathForError}:\n${chunk.oldLines.join("\n")}`);
167
+ }
168
+ replacements.push([found, pattern.length, [...newSlice]]);
169
+ lineIndex = found + pattern.length;
170
+ }
171
+ // Apply in descending start-index order so indices stay valid.
172
+ replacements.sort((a, b) => a[0] - b[0]);
173
+ const working = lines.slice();
174
+ for (let i = replacements.length - 1; i >= 0; i--) {
175
+ const [start, oldLen, newSeg] = replacements[i];
176
+ working.splice(start, oldLen, ...newSeg);
177
+ }
178
+ // Always end with a trailing newline.
179
+ if (working.length === 0 || working[working.length - 1] !== "") {
180
+ working.push("");
181
+ }
182
+ return working.join("\n");
183
+ }
184
+ // ─── Phase 2: commit ──────────────────────────────────────────────
185
+ async function commitPlanned(set, allowPartial) {
186
+ // Iterate insertion order (Map iteration is spec-guaranteed).
187
+ const ordered = [...set.byPath.values()];
188
+ if (allowPartial) {
189
+ for (const change of ordered) {
190
+ await writeChange(change);
191
+ }
192
+ return { added: set.added, modified: set.modified, deleted: set.deleted };
193
+ }
194
+ // Track which writes have completed so rollback only touches them. We use
195
+ // the originalContent captured during plan() rather than re-reading from
196
+ // disk — this avoids a TOCTOU window between planning and committing.
197
+ const committed = [];
198
+ try {
199
+ for (const change of ordered) {
200
+ await writeChange(change);
201
+ committed.push(change);
202
+ }
203
+ }
204
+ catch (err) {
205
+ for (let i = committed.length - 1; i >= 0; i--) {
206
+ const c = committed[i];
207
+ try {
208
+ if (c.originalContent === null) {
209
+ // File did not exist before we wrote it — remove our write.
210
+ if (existsSync(c.path))
211
+ await unlink(c.path);
212
+ }
213
+ else {
214
+ await writeFile(c.path, c.originalContent, "utf-8");
215
+ }
216
+ }
217
+ catch {
218
+ // Best-effort restore — surface the original error to the caller.
219
+ }
220
+ }
221
+ throw err;
222
+ }
223
+ return { added: set.added, modified: set.modified, deleted: set.deleted };
224
+ }
225
+ async function writeChange(change) {
226
+ if (change.newContent === null) {
227
+ if (existsSync(change.path))
228
+ await unlink(change.path);
229
+ return;
230
+ }
231
+ const parent = dirname(change.path);
232
+ if (!existsSync(parent)) {
233
+ await mkdir(parent, { recursive: true });
234
+ }
235
+ await writeFile(change.path, change.newContent, "utf-8");
236
+ }
237
+ // ─── helpers ──────────────────────────────────────────────────────
238
+ /**
239
+ * Resolve a patch-relative (or absolute) path against cwd, then HARD-ENFORCE
240
+ * that the result stays inside cwd.
241
+ *
242
+ * This is the applier's own containment guard, independent of the executor-level
243
+ * pathPolicy. apply-patch is reachable with the path gate bypassed (e.g.
244
+ * bypassPermissions, or a direct call), so the last line of defense lives here.
245
+ *
246
+ * Containment is checked against the REAL paths: we realpath cwd and walk the
247
+ * target up to its nearest existing ancestor and realpath that. This defeats a
248
+ * symlink planted inside cwd that points outside — a plain string `resolve()`
249
+ * would look contained while the write follows the link out.
250
+ */
251
+ function resolveAgainst(p, cwd) {
252
+ const resolved = isAbsolute(p) ? p : resolve(cwd, p);
253
+ const realCwd = nearestRealPath(cwd);
254
+ const realTarget = nearestRealPath(resolved);
255
+ if (!isInside(realTarget, realCwd)) {
256
+ throw new Error(`Refusing to apply patch outside the working directory: "${p}" resolves to "${realTarget}", which escapes "${realCwd}".`);
257
+ }
258
+ return resolved;
259
+ }
260
+ /**
261
+ * Realpath the nearest existing ancestor of `abs`, then re-attach the
262
+ * not-yet-existing trailing segments. Mirrors path-policy.safeRealpath so
263
+ * symlinked ancestors are resolved even when the leaf doesn't exist yet.
264
+ */
265
+ function nearestRealPath(abs) {
266
+ let candidate = abs;
267
+ const segments = [];
268
+ for (let i = 0; i < 64; i++) {
269
+ try {
270
+ const real = realpathSync(candidate);
271
+ if (segments.length === 0)
272
+ return real;
273
+ return resolve(real, ...segments.reverse());
274
+ }
275
+ catch {
276
+ const parent = dirname(candidate);
277
+ if (parent === candidate)
278
+ return abs;
279
+ segments.push(candidate.slice(parent.length + (parent.endsWith(sep) ? 0 : 1)));
280
+ candidate = parent;
281
+ }
282
+ }
283
+ return abs;
284
+ }
285
+ function isInside(child, parent) {
286
+ if (child === parent)
287
+ return true;
288
+ const p = parent.endsWith(sep) ? parent : parent + sep;
289
+ return child.startsWith(p);
290
+ }
291
+ async function safeStat(path) {
292
+ try {
293
+ return await stat(path);
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ async function readIfExists(path) {
300
+ try {
301
+ return await readFile(path, "utf-8");
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Coding capability files an ApplyPatch would modify-in-place or delete — i.e. the files
3
+ * that already exist and therefore have pre-patch content worth snapshotting
4
+ * for /undo. Pure (parse-only, no fs): given the patch text and the cwd to
5
+ * resolve relative paths against, return absolute target paths.
6
+ *
7
+ * "add" hunks are excluded: a newly-created file has no prior content to back
8
+ * up (undo of an add = delete, handled separately if/when /undo grows that).
9
+ */
10
+ export declare function patchBackupTargets(patchText: string, cwd: string): string[];
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Coding capability files an ApplyPatch would modify-in-place or delete — i.e. the files
3
+ * that already exist and therefore have pre-patch content worth snapshotting
4
+ * for /undo. Pure (parse-only, no fs): given the patch text and the cwd to
5
+ * resolve relative paths against, return absolute target paths.
6
+ *
7
+ * "add" hunks are excluded: a newly-created file has no prior content to back
8
+ * up (undo of an add = delete, handled separately if/when /undo grows that).
9
+ */
10
+ import { resolve as resolvePath } from "node:path";
11
+ import { parsePatch } from "./parser.js";
12
+ export function patchBackupTargets(patchText, cwd) {
13
+ let parsed;
14
+ try {
15
+ parsed = parsePatch(patchText, "lenient");
16
+ }
17
+ catch {
18
+ // Unparseable patch → nothing to back up; the tool itself will reject it.
19
+ return [];
20
+ }
21
+ const out = [];
22
+ for (const hunk of parsed.hunks) {
23
+ if (hunk.kind === "update" || hunk.kind === "delete") {
24
+ out.push(resolvePath(cwd, hunk.path));
25
+ }
26
+ }
27
+ // De-dupe while preserving order (a patch shouldn't target one file twice,
28
+ // but be defensive — saveSnapshot is idempotent on identical content anyway).
29
+ return [...new Set(out)];
30
+ }
@@ -0,0 +1,4 @@
1
+ export type Eol = "\r\n" | "\n";
2
+ export declare function detectEol(content: string): Eol;
3
+ export declare function toLf(content: string): string;
4
+ export declare function applyEol(content: string, eol: Eol): string;
@@ -0,0 +1,9 @@
1
+ export function detectEol(content) {
2
+ return content.includes("\r\n") ? "\r\n" : "\n";
3
+ }
4
+ export function toLf(content) {
5
+ return content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
6
+ }
7
+ export function applyEol(content, eol) {
8
+ return eol === "\n" ? content : content.replace(/\n/g, "\r\n");
9
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * ApplyPatch tool — atomic, multi-file V4A patch application.
3
+ *
4
+ * Adapted from openai/codex codex-rs/apply-patch (Apache-2.0). See LICENSE-codex.
5
+ *
6
+ * Compared to Codex's reference implementation:
7
+ * - We dry-run all hunks first; nothing is written until every hunk
8
+ * successfully resolves.
9
+ * - On commit we snapshot every target file, so a write failure rolls back
10
+ * to the pre-patch state instead of leaving partial changes.
11
+ *
12
+ * Compared to CodeShell's existing Edit:
13
+ * - Multi-file in one call (cuts LLM round-trips for large changes).
14
+ * - Supports create / update / delete / rename in one transaction.
15
+ * - Context-anchor matching tolerates whitespace and Unicode quirks.
16
+ */
17
+ import { type ToolContext, type ToolDefinition } from "@cjhyy/code-shell-core/extension";
18
+ export { applyPatch } from "./applier.js";
19
+ export declare const applyPatchToolDef: ToolDefinition;
20
+ export declare function applyPatchTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
@@ -0,0 +1,106 @@
1
+ /**
2
+ * ApplyPatch tool — atomic, multi-file V4A patch application.
3
+ *
4
+ * Adapted from openai/codex codex-rs/apply-patch (Apache-2.0). See LICENSE-codex.
5
+ *
6
+ * Compared to Codex's reference implementation:
7
+ * - We dry-run all hunks first; nothing is written until every hunk
8
+ * successfully resolves.
9
+ * - On commit we snapshot every target file, so a write failure rolls back
10
+ * to the pre-patch state instead of leaving partial changes.
11
+ *
12
+ * Compared to CodeShell's existing Edit:
13
+ * - Multi-file in one call (cuts LLM round-trips for large changes).
14
+ * - Supports create / update / delete / rename in one transaction.
15
+ * - Context-anchor matching tolerates whitespace and Unicode quirks.
16
+ */
17
+ import { invalidateFileCache } from "@cjhyy/code-shell-core/extension";
18
+ import { applyPatch } from "./applier.js";
19
+ import { parsePatch } from "./parser.js";
20
+ import { resolve as resolvePath } from "node:path";
21
+ export { applyPatch } from "./applier.js";
22
+ export const applyPatchToolDef = {
23
+ name: "ApplyPatch",
24
+ description: "Apply a V4A-format patch atomically across one or more files. " +
25
+ "Supports create, update, delete, and rename operations in a single call. " +
26
+ "If any hunk fails to apply, the entire patch is rejected and no files are modified. " +
27
+ "Patch envelope:\n" +
28
+ " *** Begin Patch\n" +
29
+ " *** Add File: <path>\n" +
30
+ " +<line>\n" +
31
+ " *** Update File: <path>\n" +
32
+ " *** Move to: <new path> # optional rename\n" +
33
+ " @@ <context> # optional anchor\n" +
34
+ " <unchanged line>\n" +
35
+ " -<removed line>\n" +
36
+ " +<added line>\n" +
37
+ " *** End of File # optional, anchors to file end\n" +
38
+ " *** Delete File: <path>\n" +
39
+ " *** End Patch\n" +
40
+ "Use relative paths; they resolve against the working directory.",
41
+ inputSchema: {
42
+ type: "object",
43
+ properties: {
44
+ patch: {
45
+ type: "string",
46
+ description: "Full patch text including '*** Begin Patch' and '*** End Patch' markers. " +
47
+ "All paths must be relative (preferred) or absolute. " +
48
+ "Each Update hunk must include enough context lines (' '-prefixed) " +
49
+ "to uniquely locate the change within the file.",
50
+ },
51
+ },
52
+ required: ["patch"],
53
+ },
54
+ };
55
+ export async function applyPatchTool(args, ctx) {
56
+ const patchText = args.patch;
57
+ if (typeof patchText !== "string" || patchText.length === 0) {
58
+ return "Error: patch is required and must be a non-empty string";
59
+ }
60
+ let parsed;
61
+ try {
62
+ parsed = parsePatch(patchText, "lenient");
63
+ }
64
+ catch (err) {
65
+ return `Error: ${err.message}`;
66
+ }
67
+ if (parsed.hunks.length === 0) {
68
+ return "Error: patch contains no hunks";
69
+ }
70
+ // A4: resolve relative patch paths against the Engine's cwd, not the
71
+ // host process cwd. See standard §S5.
72
+ const cwd = ctx?.cwd ?? process.cwd();
73
+ let result;
74
+ try {
75
+ result = await applyPatch(parsed.hunks, { cwd });
76
+ }
77
+ catch (err) {
78
+ return `Error applying patch: ${err.message}`;
79
+ }
80
+ // Invalidate file cache for every touched path. The cache is keyed by
81
+ // ABSOLUTE path (see file-cache.ts), and patch paths are relative to the
82
+ // Engine cwd — resolve them the same way the path-policy gate did above, or
83
+ // the stale entry survives and a later Read returns pre-patch content.
84
+ for (const hunk of parsed.hunks) {
85
+ invalidateFileCache(resolvePath(cwd, hunk.path));
86
+ if (hunk.kind === "update" && hunk.movePath) {
87
+ invalidateFileCache(resolvePath(cwd, hunk.movePath));
88
+ }
89
+ }
90
+ const parts = [];
91
+ if (result.added.length > 0)
92
+ parts.push(`added ${result.added.length}`);
93
+ if (result.modified.length > 0)
94
+ parts.push(`modified ${result.modified.length}`);
95
+ if (result.deleted.length > 0)
96
+ parts.push(`deleted ${result.deleted.length}`);
97
+ const summary = parts.join(", ");
98
+ const lines = [`Patch applied successfully — ${summary}`];
99
+ for (const p of result.added)
100
+ lines.push(` A ${p}`);
101
+ for (const p of result.modified)
102
+ lines.push(` M ${p}`);
103
+ for (const p of result.deleted)
104
+ lines.push(` D ${p}`);
105
+ return lines.join("\n");
106
+ }
@@ -0,0 +1,17 @@
1
+ import { Hunk } from "./types.js";
2
+ export declare const BEGIN_PATCH_MARKER = "*** Begin Patch";
3
+ export declare const END_PATCH_MARKER = "*** End Patch";
4
+ export declare const ADD_FILE_MARKER = "*** Add File: ";
5
+ export declare const DELETE_FILE_MARKER = "*** Delete File: ";
6
+ export declare const UPDATE_FILE_MARKER = "*** Update File: ";
7
+ export declare const MOVE_TO_MARKER = "*** Move to: ";
8
+ export declare const EOF_MARKER = "*** End of File";
9
+ export declare const CHANGE_CONTEXT_MARKER = "@@ ";
10
+ export declare const EMPTY_CHANGE_CONTEXT_MARKER = "@@";
11
+ export interface ParsedPatch {
12
+ hunks: Hunk[];
13
+ /** The patch body with any heredoc wrapper stripped. */
14
+ patch: string;
15
+ }
16
+ export type ParseMode = "strict" | "lenient";
17
+ export declare function parsePatch(text: string, mode?: ParseMode): ParsedPatch;