@warlock.js/ai-workspace 5.39.1 → 5.40.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.
- package/CHANGELOG.md +14 -0
- package/README.md +2 -2
- package/cjs/index.cjs +691 -114
- package/cjs/index.cjs.map +1 -1
- package/esm/contracts/index.d.mts +2 -2
- package/esm/contracts/tool-io.type.d.mts +32 -1
- package/esm/contracts/tool-io.type.d.mts.map +1 -1
- package/esm/contracts/workspace-ops.contract.d.mts +10 -1
- package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
- package/esm/contracts/workspace.contract.d.mts +7 -1
- package/esm/contracts/workspace.contract.d.mts.map +1 -1
- package/esm/errors.d.mts +17 -1
- package/esm/errors.d.mts.map +1 -1
- package/esm/errors.mjs +1 -0
- package/esm/errors.mjs.map +1 -1
- package/esm/index.d.mts +6 -2
- package/esm/index.mjs +5 -1
- package/esm/ops.d.mts.map +1 -1
- package/esm/ops.mjs +16 -0
- package/esm/ops.mjs.map +1 -1
- package/esm/patch/apply-hunks.d.mts +14 -0
- package/esm/patch/apply-hunks.d.mts.map +1 -0
- package/esm/patch/apply-hunks.mjs +89 -0
- package/esm/patch/apply-hunks.mjs.map +1 -0
- package/esm/patch/apply-patch-generic.d.mts +14 -0
- package/esm/patch/apply-patch-generic.d.mts.map +1 -0
- package/esm/patch/apply-patch-generic.mjs +68 -0
- package/esm/patch/apply-patch-generic.mjs.map +1 -0
- package/esm/patch/execute-patch.mjs +151 -0
- package/esm/patch/execute-patch.mjs.map +1 -0
- package/esm/patch/parse-patch.d.mts +45 -0
- package/esm/patch/parse-patch.d.mts.map +1 -0
- package/esm/patch/parse-patch.mjs +201 -0
- package/esm/patch/parse-patch.mjs.map +1 -0
- package/esm/tools/apply-patch.d.mts +27 -0
- package/esm/tools/apply-patch.d.mts.map +1 -0
- package/esm/tools/apply-patch.mjs +61 -0
- package/esm/tools/apply-patch.mjs.map +1 -0
- package/esm/tools/edit-file.mjs +1 -1
- package/esm/tools/glob.mjs +1 -1
- package/esm/tools/grep.mjs +1 -1
- package/esm/tools/json-schemas.mjs +10 -1
- package/esm/tools/json-schemas.mjs.map +1 -1
- package/esm/tools/list-dir.mjs +1 -1
- package/esm/tools/multi-edit.mjs +1 -1
- package/esm/tools/read-file.mjs +1 -1
- package/esm/tools/run-shell.mjs +1 -1
- package/esm/tools/run-tests.mjs +1 -1
- package/esm/tools/shell-kill.mjs +1 -1
- package/esm/tools/shell-output.mjs +1 -1
- package/esm/tools/todo.mjs +1 -1
- package/esm/tools/write-file.mjs +1 -1
- package/esm/workspace.d.mts.map +1 -1
- package/esm/workspace.mjs +9 -0
- package/esm/workspace.mjs.map +1 -1
- package/llms-full.txt +7 -6
- package/llms.txt +2 -2
- package/package.json +3 -3
- package/skills/build-loop-agent/SKILL.md +1 -1
- package/skills/index.md +1 -1
- package/skills/use-a-workspace/SKILL.md +5 -4
package/cjs/index.cjs
CHANGED
|
@@ -89,6 +89,7 @@ var WorkspaceEditError = class extends _warlock_js_ai.AIError {
|
|
|
89
89
|
this.expectedHash = options.expectedHash;
|
|
90
90
|
this.actualHash = options.actualHash;
|
|
91
91
|
this.editIndex = options.editIndex;
|
|
92
|
+
this.hunkIndex = options.hunkIndex;
|
|
92
93
|
}
|
|
93
94
|
};
|
|
94
95
|
/**
|
|
@@ -1900,6 +1901,493 @@ function buildCommandEnv(policy) {
|
|
|
1900
1901
|
return env;
|
|
1901
1902
|
}
|
|
1902
1903
|
|
|
1904
|
+
//#endregion
|
|
1905
|
+
//#region ../../packages/ai-workspace/src/patch/apply-hunks.ts
|
|
1906
|
+
/** The text of a line without a trailing carriage return. */
|
|
1907
|
+
const bare = (line) => line.endsWith("\r") ? line.slice(0, -1) : line;
|
|
1908
|
+
/** Same, and ignoring trailing blanks (a second, looser matching pass). */
|
|
1909
|
+
const loose = (line) => bare(line).trimEnd();
|
|
1910
|
+
/** Split file content into lines (carriage returns kept) and note the final newline. */
|
|
1911
|
+
function split(content) {
|
|
1912
|
+
if (content === "") return {
|
|
1913
|
+
lines: [],
|
|
1914
|
+
trailingNewline: false
|
|
1915
|
+
};
|
|
1916
|
+
const parts = content.split("\n");
|
|
1917
|
+
const trailingNewline = parts[parts.length - 1] === "";
|
|
1918
|
+
if (trailingNewline) parts.pop();
|
|
1919
|
+
return {
|
|
1920
|
+
lines: parts,
|
|
1921
|
+
trailingNewline
|
|
1922
|
+
};
|
|
1923
|
+
}
|
|
1924
|
+
/** Whether most of the file's lines end in CRLF. */
|
|
1925
|
+
function usesCrlf(lines) {
|
|
1926
|
+
const crlf = lines.filter((line) => line.endsWith("\r")).length;
|
|
1927
|
+
return crlf > 0 && crlf * 2 >= lines.length;
|
|
1928
|
+
}
|
|
1929
|
+
/** First index at or after `from` (searching outward from `expected`) where `old` matches. */
|
|
1930
|
+
function findMatch(lines, old, expected, from, normalize) {
|
|
1931
|
+
if (old.length === 0) return Math.min(Math.max(expected, from), lines.length);
|
|
1932
|
+
const last = lines.length - old.length;
|
|
1933
|
+
const wanted = old.map(normalize);
|
|
1934
|
+
const matches = (start) => start >= from && start <= last && wanted.every((line, offset) => normalize(lines[start + offset]) === line);
|
|
1935
|
+
for (let distance = 0; distance <= lines.length; distance++) {
|
|
1936
|
+
if (matches(expected + distance)) return expected + distance;
|
|
1937
|
+
if (distance > 0 && matches(expected - distance)) return expected - distance;
|
|
1938
|
+
}
|
|
1939
|
+
return -1;
|
|
1940
|
+
}
|
|
1941
|
+
/**
|
|
1942
|
+
* Apply a file's hunks to its content, in memory. Hunks are located by their
|
|
1943
|
+
* context (not only by line number, so a few lines of drift are fine), must
|
|
1944
|
+
* not overlap, and keep the file's line endings: untouched lines are
|
|
1945
|
+
* byte-for-byte as they were and added lines use the file's CRLF/LF style.
|
|
1946
|
+
*
|
|
1947
|
+
* Throws a `WorkspaceEditError` (`hunk-failed`) naming the first hunk whose
|
|
1948
|
+
* context is not in the file; nothing is returned in that case.
|
|
1949
|
+
*/
|
|
1950
|
+
function applyHunks(content, hunks, displayPath) {
|
|
1951
|
+
const { lines, trailingNewline: initialTrailing } = split(content);
|
|
1952
|
+
const wasEmpty = lines.length === 0;
|
|
1953
|
+
const eol = usesCrlf(lines) ? "\r" : "";
|
|
1954
|
+
let trailingNewline = initialTrailing;
|
|
1955
|
+
let cursor = 0;
|
|
1956
|
+
let delta = 0;
|
|
1957
|
+
hunks.forEach((hunk, hunkIndex) => {
|
|
1958
|
+
const old = hunk.lines.filter((line) => line.op !== "+").map((line) => line.text);
|
|
1959
|
+
const expected = (old.length === 0 ? hunk.oldStart : hunk.oldStart - 1) + delta;
|
|
1960
|
+
let start = findMatch(lines, old, expected, cursor, bare);
|
|
1961
|
+
if (start === -1) start = findMatch(lines, old, expected, cursor, loose);
|
|
1962
|
+
if (start === -1) {
|
|
1963
|
+
const probe = old.find((line) => line.trim() !== "") ?? old[0] ?? "";
|
|
1964
|
+
throw new WorkspaceEditError(`Hunk ${hunkIndex + 1} of ${displayPath} does not apply: its context was not found in the file${probe === "" ? "" : ` (looked for a line like ${JSON.stringify(probe.slice(0, 80))})`}. Re-read the file and regenerate the patch.`, {
|
|
1965
|
+
type: "hunk-failed",
|
|
1966
|
+
path: displayPath,
|
|
1967
|
+
hunkIndex
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
const replacement = [];
|
|
1971
|
+
let consumed = start;
|
|
1972
|
+
for (const line of hunk.lines) if (line.op === " ") {
|
|
1973
|
+
replacement.push(lines[consumed]);
|
|
1974
|
+
consumed++;
|
|
1975
|
+
} else if (line.op === "-") consumed++;
|
|
1976
|
+
else replacement.push(line.text + eol);
|
|
1977
|
+
lines.splice(start, old.length, ...replacement);
|
|
1978
|
+
cursor = start + replacement.length;
|
|
1979
|
+
delta += replacement.length - old.length;
|
|
1980
|
+
if (cursor >= lines.length) {
|
|
1981
|
+
if (hunk.newNoNewline) trailingNewline = false;
|
|
1982
|
+
else if (hunk.oldNoNewline || wasEmpty) trailingNewline = true;
|
|
1983
|
+
}
|
|
1984
|
+
});
|
|
1985
|
+
if (lines.length === 0) return "";
|
|
1986
|
+
return lines.join("\n") + (trailingNewline ? "\n" : "");
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
//#endregion
|
|
1990
|
+
//#region ../../packages/ai-workspace/src/patch/parse-patch.ts
|
|
1991
|
+
/** Longest patch text accepted, in characters. */
|
|
1992
|
+
const MAX_PATCH_CHARS = 2097152;
|
|
1993
|
+
/** Most files one patch may touch. */
|
|
1994
|
+
const MAX_PATCH_FILES = 200;
|
|
1995
|
+
const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/;
|
|
1996
|
+
function invalid(message, path = "") {
|
|
1997
|
+
return new WorkspaceEditError(message, {
|
|
1998
|
+
type: "patch-invalid",
|
|
1999
|
+
path
|
|
2000
|
+
});
|
|
2001
|
+
}
|
|
2002
|
+
/** Unquote a git-quoted path (`"a/with\ttab"`) and strip a trailing timestamp. */
|
|
2003
|
+
function cleanPath(raw) {
|
|
2004
|
+
let value = raw.trim();
|
|
2005
|
+
if (value.startsWith("\"")) {
|
|
2006
|
+
const end = value.lastIndexOf("\"");
|
|
2007
|
+
return (end > 0 ? value.slice(1, end) : value.slice(1)).replace(/\\(["\\tn])/g, (_match, char) => char === "t" ? " " : char === "n" ? "\n" : char);
|
|
2008
|
+
}
|
|
2009
|
+
const tab = value.indexOf(" ");
|
|
2010
|
+
if (tab !== -1) value = value.slice(0, tab);
|
|
2011
|
+
return value.trimEnd();
|
|
2012
|
+
}
|
|
2013
|
+
/** `a/x` → `x`; `/dev/null` → `null`. */
|
|
2014
|
+
function stripPrefix(raw, git) {
|
|
2015
|
+
const value = cleanPath(raw);
|
|
2016
|
+
if (value === "/dev/null") return null;
|
|
2017
|
+
return git && /^[ab]\//.test(value) ? value.slice(2) : value;
|
|
2018
|
+
}
|
|
2019
|
+
/**
|
|
2020
|
+
* Parse a unified diff (plain or `git diff` style) into per-file patches:
|
|
2021
|
+
* modifications, creates (`--- /dev/null`), deletes (`+++ /dev/null`) and
|
|
2022
|
+
* renames (`rename from` / `rename to`). A binary patch, a copy, or text that
|
|
2023
|
+
* is not a diff throws a `WorkspaceEditError` (`patch-invalid` /
|
|
2024
|
+
* `binary-file`) naming the problem.
|
|
2025
|
+
*
|
|
2026
|
+
* A CRLF-wrapped patch is accepted: one trailing `\r` is dropped from every
|
|
2027
|
+
* line.
|
|
2028
|
+
*/
|
|
2029
|
+
function parsePatch(text) {
|
|
2030
|
+
if (text.length > 2097152) throw invalid(`The patch is larger than ${MAX_PATCH_CHARS} characters; split it up.`);
|
|
2031
|
+
const lines = text.split("\n").map((line) => line.endsWith("\r") ? line.slice(0, -1) : line);
|
|
2032
|
+
const files = [];
|
|
2033
|
+
let current;
|
|
2034
|
+
const finish = () => {
|
|
2035
|
+
if (current === void 0) return;
|
|
2036
|
+
const state = current;
|
|
2037
|
+
current = void 0;
|
|
2038
|
+
let dashOld = state.oldPath;
|
|
2039
|
+
let dashNew = state.newPath;
|
|
2040
|
+
if (!state.git) {
|
|
2041
|
+
const oldOk = dashOld === void 0 || dashOld === null || dashOld.startsWith("a/");
|
|
2042
|
+
const newOk = dashNew === void 0 || dashNew === null || dashNew.startsWith("b/");
|
|
2043
|
+
if (oldOk && newOk) {
|
|
2044
|
+
dashOld = typeof dashOld === "string" ? dashOld.slice(2) : dashOld;
|
|
2045
|
+
dashNew = typeof dashNew === "string" ? dashNew.slice(2) : dashNew;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
let oldPath = state.renameFrom ?? dashOld;
|
|
2049
|
+
let newPath = state.renameTo ?? dashNew;
|
|
2050
|
+
if (state.isNew) oldPath = null;
|
|
2051
|
+
if (state.isDeleted) newPath = null;
|
|
2052
|
+
if (oldPath === void 0 && newPath === void 0) throw invalid("A file section of the patch names no file.");
|
|
2053
|
+
if (oldPath === void 0) oldPath = newPath ?? null;
|
|
2054
|
+
if (newPath === void 0) newPath = oldPath;
|
|
2055
|
+
const kind = oldPath === null ? "create" : newPath === null ? "delete" : oldPath !== newPath ? "rename" : "modify";
|
|
2056
|
+
if (state.hunks.length === 0 && kind === "modify") return;
|
|
2057
|
+
files.push({
|
|
2058
|
+
kind,
|
|
2059
|
+
oldPath,
|
|
2060
|
+
newPath,
|
|
2061
|
+
hunks: state.hunks
|
|
2062
|
+
});
|
|
2063
|
+
};
|
|
2064
|
+
const open = (git) => {
|
|
2065
|
+
finish();
|
|
2066
|
+
current = {
|
|
2067
|
+
git,
|
|
2068
|
+
oldPath: void 0,
|
|
2069
|
+
newPath: void 0,
|
|
2070
|
+
isNew: false,
|
|
2071
|
+
isDeleted: false,
|
|
2072
|
+
hunks: []
|
|
2073
|
+
};
|
|
2074
|
+
return current;
|
|
2075
|
+
};
|
|
2076
|
+
for (let index = 0; index < lines.length; index++) {
|
|
2077
|
+
const line = lines[index];
|
|
2078
|
+
if (line.startsWith("diff --git ")) {
|
|
2079
|
+
const section = open(true);
|
|
2080
|
+
const rest = line.slice(11);
|
|
2081
|
+
const split = rest.indexOf(" b/");
|
|
2082
|
+
if (rest.startsWith("a/") && split !== -1) {
|
|
2083
|
+
section.oldPath = rest.slice(2, split);
|
|
2084
|
+
section.newPath = rest.slice(split + 3);
|
|
2085
|
+
}
|
|
2086
|
+
continue;
|
|
2087
|
+
}
|
|
2088
|
+
if (line.startsWith("--- ") && lines[index + 1]?.startsWith("+++ ") === true && (current === void 0 || !current.git && current.hunks.length > 0)) open(false);
|
|
2089
|
+
else if (current === void 0) {
|
|
2090
|
+
if (line.startsWith("*** Begin Patch")) throw invalid("This is the \"*** Begin Patch\" format, not a unified diff. Send a unified diff (`--- a/file` / `+++ b/file` / `@@` hunks).");
|
|
2091
|
+
continue;
|
|
2092
|
+
}
|
|
2093
|
+
const section = current;
|
|
2094
|
+
if (line.startsWith("GIT binary patch") || /^Binary files .* differ$/.test(line)) {
|
|
2095
|
+
const shown = section.newPath ?? section.oldPath ?? "a file";
|
|
2096
|
+
throw new WorkspaceEditError(`The patch changes a binary file (${shown}); binary files cannot be patched.`, {
|
|
2097
|
+
type: "binary-file",
|
|
2098
|
+
path: shown ?? ""
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
if (line.startsWith("copy from ") || line.startsWith("copy to ")) throw invalid("Copies are not supported in a patch; create the new file instead.");
|
|
2102
|
+
if (line.startsWith("rename from ")) {
|
|
2103
|
+
section.renameFrom = cleanPath(line.slice(12));
|
|
2104
|
+
continue;
|
|
2105
|
+
}
|
|
2106
|
+
if (line.startsWith("rename to ")) {
|
|
2107
|
+
section.renameTo = cleanPath(line.slice(10));
|
|
2108
|
+
continue;
|
|
2109
|
+
}
|
|
2110
|
+
if (line.startsWith("new file mode")) {
|
|
2111
|
+
section.isNew = true;
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2114
|
+
if (line.startsWith("deleted file mode")) {
|
|
2115
|
+
section.isDeleted = true;
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
if (line.startsWith("--- ") && section.hunks.length === 0) {
|
|
2119
|
+
section.oldPath = stripPrefix(line.slice(4), section.git);
|
|
2120
|
+
if (section.oldPath === null) section.isNew = true;
|
|
2121
|
+
continue;
|
|
2122
|
+
}
|
|
2123
|
+
if (line.startsWith("+++ ") && section.hunks.length === 0) {
|
|
2124
|
+
section.newPath = stripPrefix(line.slice(4), section.git);
|
|
2125
|
+
if (section.newPath === null) section.isDeleted = true;
|
|
2126
|
+
continue;
|
|
2127
|
+
}
|
|
2128
|
+
const header = HUNK_HEADER.exec(line);
|
|
2129
|
+
if (header !== null) {
|
|
2130
|
+
const oldStart = Number(header[1]);
|
|
2131
|
+
const oldCount = header[2] === void 0 ? 1 : Number(header[2]);
|
|
2132
|
+
let newCount = header[4] === void 0 ? 1 : Number(header[4]);
|
|
2133
|
+
let remainingOld = oldCount;
|
|
2134
|
+
const hunk = {
|
|
2135
|
+
oldStart,
|
|
2136
|
+
oldCount,
|
|
2137
|
+
lines: [],
|
|
2138
|
+
oldNoNewline: false,
|
|
2139
|
+
newNoNewline: false
|
|
2140
|
+
};
|
|
2141
|
+
while (remainingOld > 0 || newCount > 0 || lines[index + 1]?.startsWith("\\")) {
|
|
2142
|
+
const body = lines[index + 1];
|
|
2143
|
+
if (body === void 0) break;
|
|
2144
|
+
if (body.startsWith("\\")) {
|
|
2145
|
+
const last = hunk.lines[hunk.lines.length - 1];
|
|
2146
|
+
if (last?.op !== "+") hunk.oldNoNewline = true;
|
|
2147
|
+
if (last?.op !== "-") hunk.newNoNewline = true;
|
|
2148
|
+
index++;
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2151
|
+
if (remainingOld <= 0 && newCount <= 0) break;
|
|
2152
|
+
const op = body.charAt(0);
|
|
2153
|
+
if (op === "+" && newCount > 0) {
|
|
2154
|
+
hunk.lines.push({
|
|
2155
|
+
op: "+",
|
|
2156
|
+
text: body.slice(1)
|
|
2157
|
+
});
|
|
2158
|
+
newCount--;
|
|
2159
|
+
} else if (op === "-" && remainingOld > 0) {
|
|
2160
|
+
hunk.lines.push({
|
|
2161
|
+
op: "-",
|
|
2162
|
+
text: body.slice(1)
|
|
2163
|
+
});
|
|
2164
|
+
remainingOld--;
|
|
2165
|
+
} else if ((op === " " || body === "") && remainingOld > 0 && newCount > 0) {
|
|
2166
|
+
hunk.lines.push({
|
|
2167
|
+
op: " ",
|
|
2168
|
+
text: body.slice(1)
|
|
2169
|
+
});
|
|
2170
|
+
remainingOld--;
|
|
2171
|
+
newCount--;
|
|
2172
|
+
} else break;
|
|
2173
|
+
index++;
|
|
2174
|
+
}
|
|
2175
|
+
if (remainingOld > 0 || newCount > 0) throw invalid(`Hunk ${section.hunks.length + 1} of ${section.newPath ?? section.oldPath ?? "the patch"} is truncated or its line counts in the @@ header are wrong.`, section.newPath ?? section.oldPath ?? "");
|
|
2176
|
+
section.hunks.push(hunk);
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
finish();
|
|
2181
|
+
if (files.length === 0) throw invalid("The patch contains no file changes. Send a unified diff with ---/+++ headers and @@ hunks.");
|
|
2182
|
+
if (files.length > 200) throw invalid(`The patch touches more than ${200} files; split it up.`);
|
|
2183
|
+
return files;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
//#endregion
|
|
2187
|
+
//#region ../../packages/ai-workspace/src/patch/execute-patch.ts
|
|
2188
|
+
/** Largest file, in UTF-8 bytes, `applyPatch` will read and rewrite. */
|
|
2189
|
+
const MAX_PATCH_FILE_BYTES = 10485760;
|
|
2190
|
+
async function executePatch(input, io) {
|
|
2191
|
+
{
|
|
2192
|
+
const filePatches = parsePatch(input.patch);
|
|
2193
|
+
const targets = await Promise.all(filePatches.map(async (filePatch) => ({
|
|
2194
|
+
filePatch,
|
|
2195
|
+
from: filePatch.oldPath === null ? null : await io.resolve(filePatch.oldPath),
|
|
2196
|
+
to: filePatch.newPath === null ? null : await io.resolve(filePatch.newPath)
|
|
2197
|
+
})));
|
|
2198
|
+
const lockPaths = [...new Set(targets.flatMap((target) => [target.from?.absolutePath, target.to?.absolutePath].filter((value) => value !== void 0)))].sort();
|
|
2199
|
+
const run = async () => {
|
|
2200
|
+
const staged = /* @__PURE__ */ new Map();
|
|
2201
|
+
const original = /* @__PURE__ */ new Map();
|
|
2202
|
+
const results = [];
|
|
2203
|
+
const load = async (absolutePath, shown) => {
|
|
2204
|
+
if (staged.has(absolutePath)) return staged.get(absolutePath) ?? null;
|
|
2205
|
+
if (!await io.exists(absolutePath)) {
|
|
2206
|
+
original.set(absolutePath, null);
|
|
2207
|
+
return null;
|
|
2208
|
+
}
|
|
2209
|
+
const stat = io.stat ? await io.stat(absolutePath) : void 0;
|
|
2210
|
+
if (stat !== void 0 && stat.type !== "file") throw new WorkspaceEditError(`"${shown}" is not a regular file; it cannot be patched.`, {
|
|
2211
|
+
type: "file-missing",
|
|
2212
|
+
path: shown
|
|
2213
|
+
});
|
|
2214
|
+
if (stat !== void 0 && stat.size > MAX_PATCH_FILE_BYTES) throw new WorkspaceEditError(`"${shown}" is larger than ${MAX_PATCH_FILE_BYTES} bytes; it cannot be patched.`, {
|
|
2215
|
+
type: "patch-invalid",
|
|
2216
|
+
path: shown
|
|
2217
|
+
});
|
|
2218
|
+
const content = await io.read(absolutePath);
|
|
2219
|
+
if (content.includes("\0")) throw new WorkspaceEditError(`"${shown}" is a binary file; binary files cannot be patched.`, {
|
|
2220
|
+
type: "binary-file",
|
|
2221
|
+
path: shown
|
|
2222
|
+
});
|
|
2223
|
+
original.set(absolutePath, content);
|
|
2224
|
+
return content;
|
|
2225
|
+
};
|
|
2226
|
+
for (const { filePatch, from, to } of targets) {
|
|
2227
|
+
const shownFrom = from?.relativePath || filePatch.oldPath || "";
|
|
2228
|
+
const shownTo = to?.relativePath || filePatch.newPath || "";
|
|
2229
|
+
if (filePatch.kind === "create") {
|
|
2230
|
+
const absolutePath = to.absolutePath;
|
|
2231
|
+
if (await load(absolutePath, shownTo) !== null) throw new WorkspaceEditError(`"${shownTo}" already exists; the patch cannot create it.`, {
|
|
2232
|
+
type: "file-exists",
|
|
2233
|
+
path: shownTo
|
|
2234
|
+
});
|
|
2235
|
+
const content = applyHunks("", filePatch.hunks, shownTo);
|
|
2236
|
+
staged.set(absolutePath, content);
|
|
2237
|
+
results.push({
|
|
2238
|
+
path: shownTo,
|
|
2239
|
+
action: "created",
|
|
2240
|
+
hunks: filePatch.hunks.length,
|
|
2241
|
+
hash: _warlock_js_fs.fs.hash.string(content)
|
|
2242
|
+
});
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
const source = from.absolutePath;
|
|
2246
|
+
const current = await load(source, shownFrom);
|
|
2247
|
+
if (current === null) throw new WorkspaceEditError(`"${shownFrom}" does not exist; the patch cannot change it.`, {
|
|
2248
|
+
type: "file-missing",
|
|
2249
|
+
path: shownFrom
|
|
2250
|
+
});
|
|
2251
|
+
const updated = applyHunks(current, filePatch.hunks, shownFrom);
|
|
2252
|
+
if (filePatch.kind === "delete") {
|
|
2253
|
+
if (updated !== "") throw new WorkspaceEditError(`The patch deletes "${shownFrom}" but its hunks do not remove all of the file's content.`, {
|
|
2254
|
+
type: "hunk-failed",
|
|
2255
|
+
path: shownFrom,
|
|
2256
|
+
hunkIndex: Math.max(filePatch.hunks.length - 1, 0)
|
|
2257
|
+
});
|
|
2258
|
+
staged.set(source, null);
|
|
2259
|
+
results.push({
|
|
2260
|
+
path: shownFrom,
|
|
2261
|
+
action: "deleted",
|
|
2262
|
+
hunks: filePatch.hunks.length
|
|
2263
|
+
});
|
|
2264
|
+
continue;
|
|
2265
|
+
}
|
|
2266
|
+
if (filePatch.kind === "rename") {
|
|
2267
|
+
const destination = to.absolutePath;
|
|
2268
|
+
if (destination !== source && await load(destination, shownTo) !== null) throw new WorkspaceEditError(`"${shownTo}" already exists; the patch cannot rename "${shownFrom}" onto it.`, {
|
|
2269
|
+
type: "file-exists",
|
|
2270
|
+
path: shownTo
|
|
2271
|
+
});
|
|
2272
|
+
staged.set(source, null);
|
|
2273
|
+
staged.set(destination, updated);
|
|
2274
|
+
results.push({
|
|
2275
|
+
path: shownTo,
|
|
2276
|
+
action: "renamed",
|
|
2277
|
+
oldPath: shownFrom,
|
|
2278
|
+
hunks: filePatch.hunks.length,
|
|
2279
|
+
hash: _warlock_js_fs.fs.hash.string(updated)
|
|
2280
|
+
});
|
|
2281
|
+
continue;
|
|
2282
|
+
}
|
|
2283
|
+
staged.set(source, updated);
|
|
2284
|
+
results.push({
|
|
2285
|
+
path: shownFrom,
|
|
2286
|
+
action: "modified",
|
|
2287
|
+
hunks: filePatch.hunks.length,
|
|
2288
|
+
hash: _warlock_js_fs.fs.hash.string(updated)
|
|
2289
|
+
});
|
|
2290
|
+
}
|
|
2291
|
+
if (input.dryRun === true) return {
|
|
2292
|
+
files: results,
|
|
2293
|
+
dryRun: true
|
|
2294
|
+
};
|
|
2295
|
+
const touched = [];
|
|
2296
|
+
try {
|
|
2297
|
+
for (const [absolutePath, content] of staged) if (content !== null && content !== (original.get(absolutePath) ?? null)) {
|
|
2298
|
+
await io.write(absolutePath, content);
|
|
2299
|
+
touched.push(absolutePath);
|
|
2300
|
+
}
|
|
2301
|
+
for (const [absolutePath, content] of staged) if (content === null && original.get(absolutePath) !== null && original.has(absolutePath)) {
|
|
2302
|
+
await io.remove(absolutePath);
|
|
2303
|
+
touched.push(absolutePath);
|
|
2304
|
+
}
|
|
2305
|
+
} catch (error) {
|
|
2306
|
+
const failures = [];
|
|
2307
|
+
for (const absolutePath of touched.reverse()) try {
|
|
2308
|
+
const before = original.get(absolutePath) ?? null;
|
|
2309
|
+
if (before === null) {
|
|
2310
|
+
if (await io.exists(absolutePath)) await io.remove(absolutePath);
|
|
2311
|
+
} else await io.write(absolutePath, before);
|
|
2312
|
+
} catch {
|
|
2313
|
+
failures.push(io.display(absolutePath));
|
|
2314
|
+
}
|
|
2315
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
2316
|
+
throw new WorkspaceEditError(`Writing the patch failed (${reason}); ${failures.length === 0 ? "every change was rolled back" : `could not restore: ${failures.join(", ")}`}.`, {
|
|
2317
|
+
type: "patch-invalid",
|
|
2318
|
+
path: ""
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
return {
|
|
2322
|
+
files: results,
|
|
2323
|
+
dryRun: false
|
|
2324
|
+
};
|
|
2325
|
+
};
|
|
2326
|
+
return lockPaths.reduceRight((inner, lockPath) => () => io.withLock(lockPath, inner), run)();
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
|
|
2330
|
+
//#endregion
|
|
2331
|
+
//#region ../../packages/ai-workspace/src/patch/apply-patch-generic.ts
|
|
2332
|
+
/** Most lines the generic reader asks `readFile` for. */
|
|
2333
|
+
const MAX_READ_LINES = 1e6;
|
|
2334
|
+
/**
|
|
2335
|
+
* Raw content of a file through `ops.readFile`, which returns numbered lines.
|
|
2336
|
+
* The numbering is stripped and the trailing newline restored by checking the
|
|
2337
|
+
* result against the file's hash, so the content is exact or the read fails.
|
|
2338
|
+
*/
|
|
2339
|
+
async function readRaw(ops, filePath) {
|
|
2340
|
+
const result = await ops.readFile(filePath, {
|
|
2341
|
+
offset: 1,
|
|
2342
|
+
limit: MAX_READ_LINES
|
|
2343
|
+
});
|
|
2344
|
+
if (result.truncated || result.bytesCapped) throw new WorkspaceEditError(`"${filePath}" is too large to read in full; it cannot be patched.`, {
|
|
2345
|
+
type: "patch-invalid",
|
|
2346
|
+
path: filePath
|
|
2347
|
+
});
|
|
2348
|
+
const joined = (result.totalLines === 0 ? [] : result.content.split("\n").map((line) => line.replace(/^ *\d+\t/, ""))).join("\n");
|
|
2349
|
+
for (const candidate of [
|
|
2350
|
+
joined + "\n",
|
|
2351
|
+
joined,
|
|
2352
|
+
""
|
|
2353
|
+
]) if (_warlock_js_fs.fs.hash.string(candidate) === result.hash) return candidate;
|
|
2354
|
+
throw new WorkspaceEditError(`Could not read "${filePath}" exactly; it cannot be patched.`, {
|
|
2355
|
+
type: "patch-invalid",
|
|
2356
|
+
path: filePath
|
|
2357
|
+
});
|
|
2358
|
+
}
|
|
2359
|
+
/**
|
|
2360
|
+
* `applyPatch` for a custom {@link WorkspaceOps} that does not provide its
|
|
2361
|
+
* own: the same engine and the same all-or-nothing behaviour, built on the
|
|
2362
|
+
* ops it does have (`exists`, `readFile`, `writeFile`, `remove`). Every
|
|
2363
|
+
* hunk of every file is checked in memory first; the writes then run, and if
|
|
2364
|
+
* one fails the ones already done are put back.
|
|
2365
|
+
*/
|
|
2366
|
+
function applyPatchWithOps(ops, input) {
|
|
2367
|
+
const chain = /* @__PURE__ */ new Map();
|
|
2368
|
+
return executePatch(input, {
|
|
2369
|
+
resolve: async (inputPath) => {
|
|
2370
|
+
const key = node_path.default.posix.normalize(inputPath.replace(/\\/g, "/"));
|
|
2371
|
+
return {
|
|
2372
|
+
absolutePath: key,
|
|
2373
|
+
relativePath: key
|
|
2374
|
+
};
|
|
2375
|
+
},
|
|
2376
|
+
exists: (key) => ops.exists(key),
|
|
2377
|
+
read: (key) => readRaw(ops, key),
|
|
2378
|
+
write: async (key, content) => {
|
|
2379
|
+
await ops.writeFile(key, content);
|
|
2380
|
+
},
|
|
2381
|
+
remove: (key) => ops.remove(key),
|
|
2382
|
+
withLock: async (key, task) => {
|
|
2383
|
+
const current = (chain.get(key) ?? Promise.resolve()).then(task, task);
|
|
2384
|
+
chain.set(key, current.catch(() => void 0));
|
|
2385
|
+
return current;
|
|
2386
|
+
},
|
|
2387
|
+
display: (key) => key
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
|
|
1903
2391
|
//#endregion
|
|
1904
2392
|
//#region ../../packages/ai-workspace/src/shell-jobs.ts
|
|
1905
2393
|
/** Finished commands kept for a last `shell_output` read before the oldest are pruned. */
|
|
@@ -2405,6 +2893,21 @@ var Ops = class {
|
|
|
2405
2893
|
};
|
|
2406
2894
|
});
|
|
2407
2895
|
}
|
|
2896
|
+
async applyPatch(input) {
|
|
2897
|
+
return executePatch(input, {
|
|
2898
|
+
resolve: (inputPath) => resolveInJail(this.policy, inputPath),
|
|
2899
|
+
exists: (absolutePath) => this.backend.exists(absolutePath),
|
|
2900
|
+
stat: this.backend.stat ? (absolutePath) => this.backend.stat(absolutePath) : void 0,
|
|
2901
|
+
read: (absolutePath) => this.backend.readFile(absolutePath),
|
|
2902
|
+
write: async (absolutePath, content) => {
|
|
2903
|
+
await this.backend.mkdir(node_path.default.dirname(absolutePath));
|
|
2904
|
+
await this.backend.writeFile(absolutePath, content);
|
|
2905
|
+
},
|
|
2906
|
+
remove: (absolutePath) => this.backend.remove(absolutePath),
|
|
2907
|
+
withLock: (key, task) => this.withPathLock(key, task),
|
|
2908
|
+
display: (absolutePath) => node_path.default.relative(this.policy.cwd, absolutePath)
|
|
2909
|
+
});
|
|
2910
|
+
}
|
|
2408
2911
|
/**
|
|
2409
2912
|
* Check a command line against the shell policy: every command in it
|
|
2410
2913
|
* (argv or shell mode), then — shell mode — every redirection target and
|
|
@@ -3370,120 +3873,6 @@ function isMockBackendSeed(seed) {
|
|
|
3370
3873
|
return typeof seed.files === "object" || typeof seed.commands === "object";
|
|
3371
3874
|
}
|
|
3372
3875
|
|
|
3373
|
-
//#endregion
|
|
3374
|
-
//#region ../../packages/ai-workspace/src/tools/schema.ts
|
|
3375
|
-
/**
|
|
3376
|
-
* Tiny, dependency-free [Standard Schema](https://standardschema.dev)
|
|
3377
|
-
* builders for the workspace tools' input validation. The package pins
|
|
3378
|
-
* only `@warlock.js/ai` and `@warlock.js/fs` as runtime dependencies, so
|
|
3379
|
-
* rather than pull in a schema library we hand-roll the few shapes the
|
|
3380
|
-
* file tools need — exactly the pattern `@warlock.js/ai`'s own `tool()`
|
|
3381
|
-
* tests use. Each builder returns a `StandardSchemaV1`, which is what
|
|
3382
|
-
* `tool({ input })` validates against before calling `execute`.
|
|
3383
|
-
*
|
|
3384
|
-
* These intentionally cover only the primitive cases the FILE tools
|
|
3385
|
-
* require (`string`, `optional string`, `optional number`, `optional
|
|
3386
|
-
* boolean`, and an `object` of fields). They are not a general-purpose
|
|
3387
|
-
* validator.
|
|
3388
|
-
*/
|
|
3389
|
-
/** The vendor tag stamped on every issue these builders produce. */
|
|
3390
|
-
const VENDOR = "ai-workspace";
|
|
3391
|
-
/** Attach a JSON Schema description to a hand-rolled Standard Schema. */
|
|
3392
|
-
function withJsonSchema(schema, jsonSchema) {
|
|
3393
|
-
return Object.assign(schema, { jsonSchema });
|
|
3394
|
-
}
|
|
3395
|
-
/** Required string field — rejects anything that is not a string. */
|
|
3396
|
-
function stringField() {
|
|
3397
|
-
return (value, key) => {
|
|
3398
|
-
if (typeof value === "string") return { value };
|
|
3399
|
-
return { issues: [{
|
|
3400
|
-
message: `"${key}" must be a string`,
|
|
3401
|
-
path: [key]
|
|
3402
|
-
}] };
|
|
3403
|
-
};
|
|
3404
|
-
}
|
|
3405
|
-
/**
|
|
3406
|
-
* Optional string field — accepts `undefined` (the property absent or
|
|
3407
|
-
* explicitly undefined) or a string, and rejects every other type.
|
|
3408
|
-
*/
|
|
3409
|
-
function optionalStringField() {
|
|
3410
|
-
return (value, key) => {
|
|
3411
|
-
if (value === void 0) return { value: void 0 };
|
|
3412
|
-
if (typeof value === "string") return { value };
|
|
3413
|
-
return { issues: [{
|
|
3414
|
-
message: `"${key}" must be a string when provided`,
|
|
3415
|
-
path: [key]
|
|
3416
|
-
}] };
|
|
3417
|
-
};
|
|
3418
|
-
}
|
|
3419
|
-
/**
|
|
3420
|
-
* Optional finite-number field — accepts `undefined` or a finite number,
|
|
3421
|
-
* rejecting `NaN`/`Infinity` and non-number types.
|
|
3422
|
-
*/
|
|
3423
|
-
function optionalNumberField() {
|
|
3424
|
-
return (value, key) => {
|
|
3425
|
-
if (value === void 0) return { value: void 0 };
|
|
3426
|
-
if (typeof value === "number" && Number.isFinite(value)) return { value };
|
|
3427
|
-
return { issues: [{
|
|
3428
|
-
message: `"${key}" must be a finite number when provided`,
|
|
3429
|
-
path: [key]
|
|
3430
|
-
}] };
|
|
3431
|
-
};
|
|
3432
|
-
}
|
|
3433
|
-
/** Optional boolean field — accepts `undefined` or a boolean. */
|
|
3434
|
-
function optionalBooleanField() {
|
|
3435
|
-
return (value, key) => {
|
|
3436
|
-
if (value === void 0) return { value: void 0 };
|
|
3437
|
-
if (typeof value === "boolean") return { value };
|
|
3438
|
-
return { issues: [{
|
|
3439
|
-
message: `"${key}" must be a boolean when provided`,
|
|
3440
|
-
path: [key]
|
|
3441
|
-
}] };
|
|
3442
|
-
};
|
|
3443
|
-
}
|
|
3444
|
-
/**
|
|
3445
|
-
* Optional array-of-strings field — accepts `undefined` or an array whose
|
|
3446
|
-
* every element is a string (at most `maxItems` of them).
|
|
3447
|
-
*/
|
|
3448
|
-
function optionalStringArrayField(maxItems = 50) {
|
|
3449
|
-
return (value, key) => {
|
|
3450
|
-
if (value === void 0) return { value: void 0 };
|
|
3451
|
-
if (Array.isArray(value) && value.length <= maxItems && value.every((item) => typeof item === "string")) return { value };
|
|
3452
|
-
return { issues: [{
|
|
3453
|
-
message: `"${key}" must be an array of at most ${maxItems} strings when provided`,
|
|
3454
|
-
path: [key]
|
|
3455
|
-
}] };
|
|
3456
|
-
};
|
|
3457
|
-
}
|
|
3458
|
-
function objectSchema(shape, jsonSchema) {
|
|
3459
|
-
const schema = buildObjectSchema(shape);
|
|
3460
|
-
return jsonSchema === void 0 ? schema : withJsonSchema(schema, jsonSchema);
|
|
3461
|
-
}
|
|
3462
|
-
/** The validator behind {@link objectSchema}. */
|
|
3463
|
-
function buildObjectSchema(shape) {
|
|
3464
|
-
return { "~standard": {
|
|
3465
|
-
version: 1,
|
|
3466
|
-
vendor: VENDOR,
|
|
3467
|
-
validate(input) {
|
|
3468
|
-
if (typeof input !== "object" || input === null || Array.isArray(input)) return { issues: [{ message: "input must be an object" }] };
|
|
3469
|
-
const source = input;
|
|
3470
|
-
const issues = [];
|
|
3471
|
-
const result = {};
|
|
3472
|
-
for (const key of Object.keys(shape)) {
|
|
3473
|
-
const field = shape[key];
|
|
3474
|
-
const outcome = field(source[key], key);
|
|
3475
|
-
if ("issues" in outcome) {
|
|
3476
|
-
issues.push(...outcome.issues);
|
|
3477
|
-
continue;
|
|
3478
|
-
}
|
|
3479
|
-
if (outcome.value !== void 0) result[key] = outcome.value;
|
|
3480
|
-
}
|
|
3481
|
-
if (issues.length > 0) return { issues };
|
|
3482
|
-
return { value: result };
|
|
3483
|
-
}
|
|
3484
|
-
} };
|
|
3485
|
-
}
|
|
3486
|
-
|
|
3487
3876
|
//#endregion
|
|
3488
3877
|
//#region ../../packages/ai-workspace/src/tools/json-schemas.ts
|
|
3489
3878
|
/**
|
|
@@ -3530,6 +3919,15 @@ const EDIT_FILE_JSON_SCHEMA = {
|
|
|
3530
3919
|
"newString"
|
|
3531
3920
|
]
|
|
3532
3921
|
};
|
|
3922
|
+
/** `apply_patch`. */
|
|
3923
|
+
const APPLY_PATCH_JSON_SCHEMA = {
|
|
3924
|
+
type: "object",
|
|
3925
|
+
properties: {
|
|
3926
|
+
patch: string("Unified diff (--- a/file, +++ b/file, @@ hunks; git style with rename from/to, new file, deleted file is fine). Applies to all files or none. Binary files are refused."),
|
|
3927
|
+
dryRun: boolean("Check that the patch applies without writing anything.")
|
|
3928
|
+
},
|
|
3929
|
+
required: ["patch"]
|
|
3930
|
+
};
|
|
3533
3931
|
/** `multi_edit`. */
|
|
3534
3932
|
const MULTI_EDIT_JSON_SCHEMA = {
|
|
3535
3933
|
type: "object",
|
|
@@ -3657,6 +4055,174 @@ const TODO_READ_JSON_SCHEMA = {
|
|
|
3657
4055
|
properties: {}
|
|
3658
4056
|
};
|
|
3659
4057
|
|
|
4058
|
+
//#endregion
|
|
4059
|
+
//#region ../../packages/ai-workspace/src/tools/schema.ts
|
|
4060
|
+
/**
|
|
4061
|
+
* Tiny, dependency-free [Standard Schema](https://standardschema.dev)
|
|
4062
|
+
* builders for the workspace tools' input validation. The package pins
|
|
4063
|
+
* only `@warlock.js/ai` and `@warlock.js/fs` as runtime dependencies, so
|
|
4064
|
+
* rather than pull in a schema library we hand-roll the few shapes the
|
|
4065
|
+
* file tools need — exactly the pattern `@warlock.js/ai`'s own `tool()`
|
|
4066
|
+
* tests use. Each builder returns a `StandardSchemaV1`, which is what
|
|
4067
|
+
* `tool({ input })` validates against before calling `execute`.
|
|
4068
|
+
*
|
|
4069
|
+
* These intentionally cover only the primitive cases the FILE tools
|
|
4070
|
+
* require (`string`, `optional string`, `optional number`, `optional
|
|
4071
|
+
* boolean`, and an `object` of fields). They are not a general-purpose
|
|
4072
|
+
* validator.
|
|
4073
|
+
*/
|
|
4074
|
+
/** The vendor tag stamped on every issue these builders produce. */
|
|
4075
|
+
const VENDOR = "ai-workspace";
|
|
4076
|
+
/** Attach a JSON Schema description to a hand-rolled Standard Schema. */
|
|
4077
|
+
function withJsonSchema(schema, jsonSchema) {
|
|
4078
|
+
return Object.assign(schema, { jsonSchema });
|
|
4079
|
+
}
|
|
4080
|
+
/** Required string field — rejects anything that is not a string. */
|
|
4081
|
+
function stringField() {
|
|
4082
|
+
return (value, key) => {
|
|
4083
|
+
if (typeof value === "string") return { value };
|
|
4084
|
+
return { issues: [{
|
|
4085
|
+
message: `"${key}" must be a string`,
|
|
4086
|
+
path: [key]
|
|
4087
|
+
}] };
|
|
4088
|
+
};
|
|
4089
|
+
}
|
|
4090
|
+
/**
|
|
4091
|
+
* Optional string field — accepts `undefined` (the property absent or
|
|
4092
|
+
* explicitly undefined) or a string, and rejects every other type.
|
|
4093
|
+
*/
|
|
4094
|
+
function optionalStringField() {
|
|
4095
|
+
return (value, key) => {
|
|
4096
|
+
if (value === void 0) return { value: void 0 };
|
|
4097
|
+
if (typeof value === "string") return { value };
|
|
4098
|
+
return { issues: [{
|
|
4099
|
+
message: `"${key}" must be a string when provided`,
|
|
4100
|
+
path: [key]
|
|
4101
|
+
}] };
|
|
4102
|
+
};
|
|
4103
|
+
}
|
|
4104
|
+
/**
|
|
4105
|
+
* Optional finite-number field — accepts `undefined` or a finite number,
|
|
4106
|
+
* rejecting `NaN`/`Infinity` and non-number types.
|
|
4107
|
+
*/
|
|
4108
|
+
function optionalNumberField() {
|
|
4109
|
+
return (value, key) => {
|
|
4110
|
+
if (value === void 0) return { value: void 0 };
|
|
4111
|
+
if (typeof value === "number" && Number.isFinite(value)) return { value };
|
|
4112
|
+
return { issues: [{
|
|
4113
|
+
message: `"${key}" must be a finite number when provided`,
|
|
4114
|
+
path: [key]
|
|
4115
|
+
}] };
|
|
4116
|
+
};
|
|
4117
|
+
}
|
|
4118
|
+
/** Optional boolean field — accepts `undefined` or a boolean. */
|
|
4119
|
+
function optionalBooleanField() {
|
|
4120
|
+
return (value, key) => {
|
|
4121
|
+
if (value === void 0) return { value: void 0 };
|
|
4122
|
+
if (typeof value === "boolean") return { value };
|
|
4123
|
+
return { issues: [{
|
|
4124
|
+
message: `"${key}" must be a boolean when provided`,
|
|
4125
|
+
path: [key]
|
|
4126
|
+
}] };
|
|
4127
|
+
};
|
|
4128
|
+
}
|
|
4129
|
+
/**
|
|
4130
|
+
* Optional array-of-strings field — accepts `undefined` or an array whose
|
|
4131
|
+
* every element is a string (at most `maxItems` of them).
|
|
4132
|
+
*/
|
|
4133
|
+
function optionalStringArrayField(maxItems = 50) {
|
|
4134
|
+
return (value, key) => {
|
|
4135
|
+
if (value === void 0) return { value: void 0 };
|
|
4136
|
+
if (Array.isArray(value) && value.length <= maxItems && value.every((item) => typeof item === "string")) return { value };
|
|
4137
|
+
return { issues: [{
|
|
4138
|
+
message: `"${key}" must be an array of at most ${maxItems} strings when provided`,
|
|
4139
|
+
path: [key]
|
|
4140
|
+
}] };
|
|
4141
|
+
};
|
|
4142
|
+
}
|
|
4143
|
+
function objectSchema(shape, jsonSchema) {
|
|
4144
|
+
const schema = buildObjectSchema(shape);
|
|
4145
|
+
return jsonSchema === void 0 ? schema : withJsonSchema(schema, jsonSchema);
|
|
4146
|
+
}
|
|
4147
|
+
/** The validator behind {@link objectSchema}. */
|
|
4148
|
+
function buildObjectSchema(shape) {
|
|
4149
|
+
return { "~standard": {
|
|
4150
|
+
version: 1,
|
|
4151
|
+
vendor: VENDOR,
|
|
4152
|
+
validate(input) {
|
|
4153
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)) return { issues: [{ message: "input must be an object" }] };
|
|
4154
|
+
const source = input;
|
|
4155
|
+
const issues = [];
|
|
4156
|
+
const result = {};
|
|
4157
|
+
for (const key of Object.keys(shape)) {
|
|
4158
|
+
const field = shape[key];
|
|
4159
|
+
const outcome = field(source[key], key);
|
|
4160
|
+
if ("issues" in outcome) {
|
|
4161
|
+
issues.push(...outcome.issues);
|
|
4162
|
+
continue;
|
|
4163
|
+
}
|
|
4164
|
+
if (outcome.value !== void 0) result[key] = outcome.value;
|
|
4165
|
+
}
|
|
4166
|
+
if (issues.length > 0) return { issues };
|
|
4167
|
+
return { value: result };
|
|
4168
|
+
}
|
|
4169
|
+
} };
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
//#endregion
|
|
4173
|
+
//#region ../../packages/ai-workspace/src/tools/apply-patch.ts
|
|
4174
|
+
/** Default tool name exposed to the LLM. */
|
|
4175
|
+
const DEFAULT_NAME$7 = "apply_patch";
|
|
4176
|
+
/** Input schema for the `apply_patch` tool. */
|
|
4177
|
+
const inputSchema$7 = withJsonSchema({ "~standard": {
|
|
4178
|
+
version: 1,
|
|
4179
|
+
vendor: "ai-workspace",
|
|
4180
|
+
validate(value) {
|
|
4181
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return { issues: [{ message: "input must be an object" }] };
|
|
4182
|
+
const candidate = value;
|
|
4183
|
+
const issues = [];
|
|
4184
|
+
if (typeof candidate.patch !== "string" || candidate.patch.trim() === "") issues.push({
|
|
4185
|
+
message: "\"patch\" must be a non-empty string",
|
|
4186
|
+
path: ["patch"]
|
|
4187
|
+
});
|
|
4188
|
+
if (candidate.dryRun !== void 0 && typeof candidate.dryRun !== "boolean") issues.push({
|
|
4189
|
+
message: "\"dryRun\" must be a boolean when provided",
|
|
4190
|
+
path: ["dryRun"]
|
|
4191
|
+
});
|
|
4192
|
+
if (issues.length > 0 || typeof candidate.patch !== "string") return { issues };
|
|
4193
|
+
return { value: {
|
|
4194
|
+
patch: candidate.patch,
|
|
4195
|
+
...candidate.dryRun === true ? { dryRun: true } : {}
|
|
4196
|
+
} };
|
|
4197
|
+
}
|
|
4198
|
+
} }, APPLY_PATCH_JSON_SCHEMA);
|
|
4199
|
+
/**
|
|
4200
|
+
* Build the agent-facing `apply_patch` tool over a workspace's policy-enforced
|
|
4201
|
+
* {@link WorkspaceOps}: apply a unified diff that modifies, creates, deletes
|
|
4202
|
+
* and renames files. It is **atomic** — every hunk of every file applies, or
|
|
4203
|
+
* nothing on disk changes — every path stays inside the workspace jail, and a
|
|
4204
|
+
* patch that touches a binary file is refused with a clear error. A failure
|
|
4205
|
+
* surfaces as a `WorkspaceEditError` in the result's `error` field.
|
|
4206
|
+
*
|
|
4207
|
+
* @param ops - The shared, policy-enforced operation layer.
|
|
4208
|
+
* @param options - Optional overrides; `name` renames the LLM-visible tool.
|
|
4209
|
+
*
|
|
4210
|
+
* @example
|
|
4211
|
+
* const patch = makeApplyPatchTool(ops);
|
|
4212
|
+
* const { data, error } = await patch.invoke({
|
|
4213
|
+
* patch: "--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1 @@\n-old\n+new\n",
|
|
4214
|
+
* });
|
|
4215
|
+
*/
|
|
4216
|
+
function makeApplyPatchTool(ops, options) {
|
|
4217
|
+
return (0, _warlock_js_ai.tool)({
|
|
4218
|
+
name: options?.name ?? DEFAULT_NAME$7,
|
|
4219
|
+
description: "Apply a unified diff to the workspace: modify, create (--- /dev/null), delete (+++ /dev/null) and rename (git rename from/to) files in one call. All of it applies or none of it does. Hunks are located by their context, so small line drift is fine; binary files are refused. Use dryRun to check first.",
|
|
4220
|
+
action: (input) => input.dryRun === true ? "Checking a patch" : "Applying a patch",
|
|
4221
|
+
input: inputSchema$7,
|
|
4222
|
+
execute: (input) => ops.applyPatch ? ops.applyPatch(input) : applyPatchWithOps(ops, input)
|
|
4223
|
+
});
|
|
4224
|
+
}
|
|
4225
|
+
|
|
3660
4226
|
//#endregion
|
|
3661
4227
|
//#region ../../packages/ai-workspace/src/tools/edit-file.ts
|
|
3662
4228
|
/** Default tool name exposed to the LLM. */
|
|
@@ -4470,6 +5036,7 @@ const ALL_TOOL_NAMES = [
|
|
|
4470
5036
|
"readFile",
|
|
4471
5037
|
"editFile",
|
|
4472
5038
|
"multiEdit",
|
|
5039
|
+
"applyPatch",
|
|
4473
5040
|
"writeFile",
|
|
4474
5041
|
"listDir",
|
|
4475
5042
|
"runShell",
|
|
@@ -4549,6 +5116,7 @@ var WorkspaceImpl = class WorkspaceImpl {
|
|
|
4549
5116
|
readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),
|
|
4550
5117
|
editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),
|
|
4551
5118
|
multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),
|
|
5119
|
+
applyPatch: (opts) => erase(makeApplyPatchTool(this.ops, opts)),
|
|
4552
5120
|
writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),
|
|
4553
5121
|
listDir: (opts) => erase(makeListDirTool(this.ops, opts)),
|
|
4554
5122
|
runShell: (opts) => erase(makeRunShellTool(this.ops, {
|
|
@@ -4575,6 +5143,7 @@ var WorkspaceImpl = class WorkspaceImpl {
|
|
|
4575
5143
|
readFile: (opts) => build("readFile", opts),
|
|
4576
5144
|
editFile: (opts) => build("editFile", opts),
|
|
4577
5145
|
multiEdit: (opts) => build("multiEdit", opts),
|
|
5146
|
+
applyPatch: (opts) => build("applyPatch", opts),
|
|
4578
5147
|
writeFile: (opts) => build("writeFile", opts),
|
|
4579
5148
|
listDir: (opts) => build("listDir", opts),
|
|
4580
5149
|
runShell: (opts) => build("runShell", opts),
|
|
@@ -4614,6 +5183,10 @@ var WorkspaceImpl = class WorkspaceImpl {
|
|
|
4614
5183
|
this.assertWritable("multiEdit");
|
|
4615
5184
|
return this.ops.multiEdit(input);
|
|
4616
5185
|
}
|
|
5186
|
+
async applyPatch(input) {
|
|
5187
|
+
this.assertWritable("applyPatch");
|
|
5188
|
+
return this.ops.applyPatch ? this.ops.applyPatch(input) : applyPatchWithOps(this.ops, input);
|
|
5189
|
+
}
|
|
4617
5190
|
listDir(dirPath, opts) {
|
|
4618
5191
|
return this.ops.listDir(dirPath, opts);
|
|
4619
5192
|
}
|
|
@@ -4734,12 +5307,15 @@ exports.WorkspaceEditError = WorkspaceEditError;
|
|
|
4734
5307
|
exports.WorkspaceJobError = WorkspaceJobError;
|
|
4735
5308
|
exports.WorkspacePolicyError = WorkspacePolicyError;
|
|
4736
5309
|
exports.analyzeArgvCommand = analyzeArgvCommand;
|
|
5310
|
+
exports.applyHunks = applyHunks;
|
|
5311
|
+
exports.applyPatchWithOps = applyPatchWithOps;
|
|
4737
5312
|
exports.buildEnv = buildEnv;
|
|
4738
5313
|
exports.checkShellCommand = checkShellCommand;
|
|
4739
5314
|
exports.createLocalBackend = createLocalBackend;
|
|
4740
5315
|
exports.createMockBackend = createMockBackend;
|
|
4741
5316
|
exports.createOps = createOps;
|
|
4742
5317
|
exports.isCommandAllowed = isCommandAllowed;
|
|
5318
|
+
exports.makeApplyPatchTool = makeApplyPatchTool;
|
|
4743
5319
|
exports.makeEditFileTool = makeEditFileTool;
|
|
4744
5320
|
exports.makeGlobTool = makeGlobTool;
|
|
4745
5321
|
exports.makeGrepTool = makeGrepTool;
|
|
@@ -4753,6 +5329,7 @@ exports.makeShellOutputTool = makeShellOutputTool;
|
|
|
4753
5329
|
exports.makeTodoReadTool = makeTodoReadTool;
|
|
4754
5330
|
exports.makeTodoWriteTool = makeTodoWriteTool;
|
|
4755
5331
|
exports.makeWriteFileTool = makeWriteFileTool;
|
|
5332
|
+
exports.parsePatch = parsePatch;
|
|
4756
5333
|
exports.resolveInJail = resolveInJail;
|
|
4757
5334
|
exports.tokenizeCommand = tokenizeCommand;
|
|
4758
5335
|
exports.workspace = workspace;
|