@warlock.js/ai-workspace 5.39.2 → 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.
Files changed (61) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -2
  3. package/cjs/index.cjs +691 -114
  4. package/cjs/index.cjs.map +1 -1
  5. package/esm/contracts/index.d.mts +2 -2
  6. package/esm/contracts/tool-io.type.d.mts +32 -1
  7. package/esm/contracts/tool-io.type.d.mts.map +1 -1
  8. package/esm/contracts/workspace-ops.contract.d.mts +10 -1
  9. package/esm/contracts/workspace-ops.contract.d.mts.map +1 -1
  10. package/esm/contracts/workspace.contract.d.mts +7 -1
  11. package/esm/contracts/workspace.contract.d.mts.map +1 -1
  12. package/esm/errors.d.mts +17 -1
  13. package/esm/errors.d.mts.map +1 -1
  14. package/esm/errors.mjs +1 -0
  15. package/esm/errors.mjs.map +1 -1
  16. package/esm/index.d.mts +6 -2
  17. package/esm/index.mjs +5 -1
  18. package/esm/ops.d.mts.map +1 -1
  19. package/esm/ops.mjs +16 -0
  20. package/esm/ops.mjs.map +1 -1
  21. package/esm/patch/apply-hunks.d.mts +14 -0
  22. package/esm/patch/apply-hunks.d.mts.map +1 -0
  23. package/esm/patch/apply-hunks.mjs +89 -0
  24. package/esm/patch/apply-hunks.mjs.map +1 -0
  25. package/esm/patch/apply-patch-generic.d.mts +14 -0
  26. package/esm/patch/apply-patch-generic.d.mts.map +1 -0
  27. package/esm/patch/apply-patch-generic.mjs +68 -0
  28. package/esm/patch/apply-patch-generic.mjs.map +1 -0
  29. package/esm/patch/execute-patch.mjs +151 -0
  30. package/esm/patch/execute-patch.mjs.map +1 -0
  31. package/esm/patch/parse-patch.d.mts +45 -0
  32. package/esm/patch/parse-patch.d.mts.map +1 -0
  33. package/esm/patch/parse-patch.mjs +201 -0
  34. package/esm/patch/parse-patch.mjs.map +1 -0
  35. package/esm/tools/apply-patch.d.mts +27 -0
  36. package/esm/tools/apply-patch.d.mts.map +1 -0
  37. package/esm/tools/apply-patch.mjs +61 -0
  38. package/esm/tools/apply-patch.mjs.map +1 -0
  39. package/esm/tools/edit-file.mjs +1 -1
  40. package/esm/tools/glob.mjs +1 -1
  41. package/esm/tools/grep.mjs +1 -1
  42. package/esm/tools/json-schemas.mjs +10 -1
  43. package/esm/tools/json-schemas.mjs.map +1 -1
  44. package/esm/tools/list-dir.mjs +1 -1
  45. package/esm/tools/multi-edit.mjs +1 -1
  46. package/esm/tools/read-file.mjs +1 -1
  47. package/esm/tools/run-shell.mjs +1 -1
  48. package/esm/tools/run-tests.mjs +1 -1
  49. package/esm/tools/shell-kill.mjs +1 -1
  50. package/esm/tools/shell-output.mjs +1 -1
  51. package/esm/tools/todo.mjs +1 -1
  52. package/esm/tools/write-file.mjs +1 -1
  53. package/esm/workspace.d.mts.map +1 -1
  54. package/esm/workspace.mjs +9 -0
  55. package/esm/workspace.mjs.map +1 -1
  56. package/llms-full.txt +7 -6
  57. package/llms.txt +2 -2
  58. package/package.json +3 -3
  59. package/skills/build-loop-agent/SKILL.md +1 -1
  60. package/skills/index.md +1 -1
  61. package/skills/use-a-workspace/SKILL.md +5 -4
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-patch.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/patch/parse-patch.ts"],"sourcesContent":["import { WorkspaceEditError } from \"../errors\";\n\n/** One line of a hunk: context (`\" \"`), removal (`\"-\"`) or addition (`\"+\"`). */\nexport interface PatchLine {\n /** What the line does. */\n op: \" \" | \"-\" | \"+\";\n /** The line's text, without its line ending. */\n text: string;\n}\n\n/** One `@@ … @@` block of a file's patch. */\nexport interface PatchHunk {\n /** 1-based first line in the old file (the line *after* which text is inserted when `oldCount` is 0). */\n oldStart: number;\n /** Number of old-file lines the hunk covers. */\n oldCount: number;\n /** The hunk body. */\n lines: PatchLine[];\n /** The old side ends without a trailing newline. */\n oldNoNewline: boolean;\n /** The new side ends without a trailing newline. */\n newNoNewline: boolean;\n}\n\n/** What a file's patch does to the tree. */\nexport type FilePatchKind = \"modify\" | \"create\" | \"delete\" | \"rename\";\n\n/** Everything a unified diff says about one file. */\nexport interface FilePatch {\n kind: FilePatchKind;\n /** Path before the change (`null` for a create). */\n oldPath: string | null;\n /** Path after the change (`null` for a delete). */\n newPath: string | null;\n hunks: PatchHunk[];\n}\n\n/** Longest patch text accepted, in characters. */\nexport const MAX_PATCH_CHARS = 2 * 1024 * 1024;\n\n/** Most files one patch may touch. */\nexport const MAX_PATCH_FILES = 200;\n\nconst HUNK_HEADER = /^@@ -(\\d+)(?:,(\\d+))? \\+(\\d+)(?:,(\\d+))? @@/;\n\nfunction invalid(message: string, path = \"\"): WorkspaceEditError {\n return new WorkspaceEditError(message, { type: \"patch-invalid\", path });\n}\n\n/** Unquote a git-quoted path (`\"a/with\\ttab\"`) and strip a trailing timestamp. */\nfunction cleanPath(raw: string): string {\n let value = raw.trim();\n\n if (value.startsWith('\"')) {\n const end = value.lastIndexOf('\"');\n const body = end > 0 ? value.slice(1, end) : value.slice(1);\n\n return body.replace(/\\\\([\"\\\\tn])/g, (_match, char: string) =>\n char === \"t\" ? \"\\t\" : char === \"n\" ? \"\\n\" : char,\n );\n }\n\n // `--- a/file.ts\\t2026-01-01 00:00:00` — the timestamp follows a tab.\n const tab = value.indexOf(\"\\t\");\n\n if (tab !== -1) {\n value = value.slice(0, tab);\n }\n\n return value.trimEnd();\n}\n\n/** `a/x` → `x`; `/dev/null` → `null`. */\nfunction stripPrefix(raw: string, git: boolean): string | null {\n const value = cleanPath(raw);\n\n if (value === \"/dev/null\") {\n return null;\n }\n\n return git && /^[ab]\\//.test(value) ? value.slice(2) : value;\n}\n\n/**\n * Parse a unified diff (plain or `git diff` style) into per-file patches:\n * modifications, creates (`--- /dev/null`), deletes (`+++ /dev/null`) and\n * renames (`rename from` / `rename to`). A binary patch, a copy, or text that\n * is not a diff throws a `WorkspaceEditError` (`patch-invalid` /\n * `binary-file`) naming the problem.\n *\n * A CRLF-wrapped patch is accepted: one trailing `\\r` is dropped from every\n * line.\n */\nexport function parsePatch(text: string): FilePatch[] {\n if (text.length > MAX_PATCH_CHARS) {\n throw invalid(`The patch is larger than ${MAX_PATCH_CHARS} characters; split it up.`);\n }\n\n const lines = text.split(\"\\n\").map((line) => (line.endsWith(\"\\r\") ? line.slice(0, -1) : line));\n const files: FilePatch[] = [];\n\n let current:\n | {\n git: boolean;\n oldPath: string | null | undefined;\n newPath: string | null | undefined;\n renameFrom?: string;\n renameTo?: string;\n isNew: boolean;\n isDeleted: boolean;\n hunks: PatchHunk[];\n }\n | undefined;\n\n const finish = (): void => {\n if (current === undefined) {\n return;\n }\n\n const state = current;\n\n current = undefined;\n\n let dashOld = state.oldPath;\n let dashNew = state.newPath;\n\n // A plain diff keeps its `a/` / `b/` prefixes (as `git diff --no-index`\n // and `diff -u a/x b/x` write them); drop them when both sides carry one.\n if (!state.git) {\n const oldOk = dashOld === undefined || dashOld === null || dashOld.startsWith(\"a/\");\n const newOk = dashNew === undefined || dashNew === null || dashNew.startsWith(\"b/\");\n\n if (oldOk && newOk) {\n dashOld = typeof dashOld === \"string\" ? dashOld.slice(2) : dashOld;\n dashNew = typeof dashNew === \"string\" ? dashNew.slice(2) : dashNew;\n }\n }\n\n let oldPath = state.renameFrom ?? dashOld;\n let newPath = state.renameTo ?? dashNew;\n\n if (state.isNew) {\n oldPath = null;\n }\n\n if (state.isDeleted) {\n newPath = null;\n }\n\n if (oldPath === undefined && newPath === undefined) {\n throw invalid(\"A file section of the patch names no file.\");\n }\n\n if (oldPath === undefined) {\n oldPath = newPath ?? null;\n }\n\n if (newPath === undefined) {\n newPath = oldPath;\n }\n\n const kind: FilePatchKind =\n oldPath === null\n ? \"create\"\n : newPath === null\n ? \"delete\"\n : oldPath !== newPath\n ? \"rename\"\n : \"modify\";\n\n // A section with neither hunks nor a structural change (e.g. a mode-only\n // change) has nothing to apply; skip it rather than touch the file.\n if (state.hunks.length === 0 && kind === \"modify\") {\n return;\n }\n\n files.push({ kind, oldPath, newPath, hunks: state.hunks });\n };\n\n const open = (git: boolean) => {\n finish();\n current = {\n git,\n oldPath: undefined,\n newPath: undefined,\n isNew: false,\n isDeleted: false,\n hunks: [],\n };\n\n return current;\n };\n\n for (let index = 0; index < lines.length; index++) {\n const line = lines[index] as string;\n\n if (line.startsWith(\"diff --git \")) {\n const section = open(true);\n const rest = line.slice(\"diff --git \".length);\n // Best effort for header-only sections (pure renames, mode changes):\n // `a/x b/y`. Refined by `---`/`+++`/`rename` lines when present.\n const split = rest.indexOf(\" b/\");\n\n if (rest.startsWith(\"a/\") && split !== -1) {\n section.oldPath = rest.slice(2, split);\n section.newPath = rest.slice(split + 3);\n }\n\n continue;\n }\n\n const startsPlainFile =\n line.startsWith(\"--- \") &&\n lines[index + 1]?.startsWith(\"+++ \") === true &&\n (current === undefined || (!current.git && current.hunks.length > 0));\n\n if (startsPlainFile) {\n open(false);\n } else if (current === undefined) {\n // Text before the first file header (a commit message, prose).\n if (line.startsWith(\"*** Begin Patch\")) {\n throw invalid(\n 'This is the \"*** Begin Patch\" format, not a unified diff. Send a unified diff (`--- a/file` / `+++ b/file` / `@@` hunks).',\n );\n }\n\n continue;\n }\n\n const section = current as NonNullable<typeof current>;\n\n if (line.startsWith(\"GIT binary patch\") || /^Binary files .* differ$/.test(line)) {\n const shown = section.newPath ?? section.oldPath ?? \"a file\";\n\n throw new WorkspaceEditError(\n `The patch changes a binary file (${shown}); binary files cannot be patched.`,\n { type: \"binary-file\", path: shown ?? \"\" },\n );\n }\n\n if (line.startsWith(\"copy from \") || line.startsWith(\"copy to \")) {\n throw invalid(\"Copies are not supported in a patch; create the new file instead.\");\n }\n\n if (line.startsWith(\"rename from \")) {\n section.renameFrom = cleanPath(line.slice(\"rename from \".length));\n continue;\n }\n\n if (line.startsWith(\"rename to \")) {\n section.renameTo = cleanPath(line.slice(\"rename to \".length));\n continue;\n }\n\n if (line.startsWith(\"new file mode\")) {\n section.isNew = true;\n continue;\n }\n\n if (line.startsWith(\"deleted file mode\")) {\n section.isDeleted = true;\n continue;\n }\n\n if (line.startsWith(\"--- \") && section.hunks.length === 0) {\n section.oldPath = stripPrefix(line.slice(4), section.git);\n\n if (section.oldPath === null) {\n section.isNew = true;\n }\n\n continue;\n }\n\n if (line.startsWith(\"+++ \") && section.hunks.length === 0) {\n section.newPath = stripPrefix(line.slice(4), section.git);\n\n if (section.newPath === null) {\n section.isDeleted = true;\n }\n\n continue;\n }\n\n const header = HUNK_HEADER.exec(line);\n\n if (header !== null) {\n const oldStart = Number(header[1]);\n const oldCount = header[2] === undefined ? 1 : Number(header[2]);\n let newCount = header[4] === undefined ? 1 : Number(header[4]);\n let remainingOld = oldCount;\n const hunk: PatchHunk = {\n oldStart,\n oldCount,\n lines: [],\n oldNoNewline: false,\n newNoNewline: false,\n };\n\n while (remainingOld > 0 || newCount > 0 || lines[index + 1]?.startsWith(\"\\\\\")) {\n const body = lines[index + 1];\n\n if (body === undefined) {\n break;\n }\n\n if (body.startsWith(\"\\\\\")) {\n const last = hunk.lines[hunk.lines.length - 1];\n\n if (last?.op !== \"+\") {\n hunk.oldNoNewline = true;\n }\n\n if (last?.op !== \"-\") {\n hunk.newNoNewline = true;\n }\n\n index++;\n continue;\n }\n\n if (remainingOld <= 0 && newCount <= 0) {\n break;\n }\n\n const op = body.charAt(0);\n\n if (op === \"+\" && newCount > 0) {\n hunk.lines.push({ op: \"+\", text: body.slice(1) });\n newCount--;\n } else if (op === \"-\" && remainingOld > 0) {\n hunk.lines.push({ op: \"-\", text: body.slice(1) });\n remainingOld--;\n } else if ((op === \" \" || body === \"\") && remainingOld > 0 && newCount > 0) {\n // An empty line is a context line whose single space was stripped.\n hunk.lines.push({ op: \" \", text: body.slice(1) });\n remainingOld--;\n newCount--;\n } else {\n break;\n }\n\n index++;\n }\n\n if (remainingOld > 0 || newCount > 0) {\n throw invalid(\n `Hunk ${section.hunks.length + 1} of ${section.newPath ?? section.oldPath ?? \"the patch\"} is truncated or its line counts in the @@ header are wrong.`,\n section.newPath ?? section.oldPath ?? \"\",\n );\n }\n\n section.hunks.push(hunk);\n continue;\n }\n\n // Anything else between sections (`index …`, `old mode`, `similarity index`, blanks) is ignored.\n }\n\n finish();\n\n if (files.length === 0) {\n throw invalid(\n \"The patch contains no file changes. Send a unified diff with ---/+++ headers and @@ hunks.\",\n );\n }\n\n if (files.length > MAX_PATCH_FILES) {\n throw invalid(`The patch touches more than ${MAX_PATCH_FILES} files; split it up.`);\n }\n\n return files;\n}\n"],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { ApplyPatchInput, ApplyPatchResult } from "../contracts/tool-io.type.mjs";
2
+ import { WorkspaceOps } from "../contracts/workspace-ops.contract.mjs";
3
+ import "../contracts/index.mjs";
4
+ import { ToolContract } from "@warlock.js/ai";
5
+ //#region ../../packages/ai-workspace/src/tools/apply-patch.d.ts
6
+ /**
7
+ * Build the agent-facing `apply_patch` tool over a workspace's policy-enforced
8
+ * {@link WorkspaceOps}: apply a unified diff that modifies, creates, deletes
9
+ * and renames files. It is **atomic** — every hunk of every file applies, or
10
+ * nothing on disk changes — every path stays inside the workspace jail, and a
11
+ * patch that touches a binary file is refused with a clear error. A failure
12
+ * surfaces as a `WorkspaceEditError` in the result's `error` field.
13
+ *
14
+ * @param ops - The shared, policy-enforced operation layer.
15
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
16
+ *
17
+ * @example
18
+ * const patch = makeApplyPatchTool(ops);
19
+ * const { data, error } = await patch.invoke({
20
+ * patch: "--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1 @@\n-old\n+new\n",
21
+ * });
22
+ */
23
+ export declare function makeApplyPatchTool(ops: WorkspaceOps, options?: {
24
+ name?: string;
25
+ }): ToolContract<ApplyPatchInput, ApplyPatchResult>;
26
+ //#endregion
27
+ //# sourceMappingURL=apply-patch.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-patch.d.mts","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/apply-patch.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;wBAiEgB,mBACd,KAAK,cACL;EAAY;IACX,aAAa,iBAAiB"}
@@ -0,0 +1,61 @@
1
+ import { applyPatchWithOps } from "../patch/apply-patch-generic.mjs";
2
+ import { APPLY_PATCH_JSON_SCHEMA } from "./json-schemas.mjs";
3
+ import { withJsonSchema } from "./schema.mjs";
4
+ import { tool } from "@warlock.js/ai";
5
+
6
+ //#region ../../packages/ai-workspace/src/tools/apply-patch.ts
7
+ /** Default tool name exposed to the LLM. */
8
+ const DEFAULT_NAME = "apply_patch";
9
+ /** Input schema for the `apply_patch` tool. */
10
+ const inputSchema = withJsonSchema({ "~standard": {
11
+ version: 1,
12
+ vendor: "ai-workspace",
13
+ validate(value) {
14
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return { issues: [{ message: "input must be an object" }] };
15
+ const candidate = value;
16
+ const issues = [];
17
+ if (typeof candidate.patch !== "string" || candidate.patch.trim() === "") issues.push({
18
+ message: "\"patch\" must be a non-empty string",
19
+ path: ["patch"]
20
+ });
21
+ if (candidate.dryRun !== void 0 && typeof candidate.dryRun !== "boolean") issues.push({
22
+ message: "\"dryRun\" must be a boolean when provided",
23
+ path: ["dryRun"]
24
+ });
25
+ if (issues.length > 0 || typeof candidate.patch !== "string") return { issues };
26
+ return { value: {
27
+ patch: candidate.patch,
28
+ ...candidate.dryRun === true ? { dryRun: true } : {}
29
+ } };
30
+ }
31
+ } }, APPLY_PATCH_JSON_SCHEMA);
32
+ /**
33
+ * Build the agent-facing `apply_patch` tool over a workspace's policy-enforced
34
+ * {@link WorkspaceOps}: apply a unified diff that modifies, creates, deletes
35
+ * and renames files. It is **atomic** — every hunk of every file applies, or
36
+ * nothing on disk changes — every path stays inside the workspace jail, and a
37
+ * patch that touches a binary file is refused with a clear error. A failure
38
+ * surfaces as a `WorkspaceEditError` in the result's `error` field.
39
+ *
40
+ * @param ops - The shared, policy-enforced operation layer.
41
+ * @param options - Optional overrides; `name` renames the LLM-visible tool.
42
+ *
43
+ * @example
44
+ * const patch = makeApplyPatchTool(ops);
45
+ * const { data, error } = await patch.invoke({
46
+ * patch: "--- a/src/a.ts\n+++ b/src/a.ts\n@@ -1 +1 @@\n-old\n+new\n",
47
+ * });
48
+ */
49
+ function makeApplyPatchTool(ops, options) {
50
+ return tool({
51
+ name: options?.name ?? DEFAULT_NAME,
52
+ 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.",
53
+ action: (input) => input.dryRun === true ? "Checking a patch" : "Applying a patch",
54
+ input: inputSchema,
55
+ execute: (input) => ops.applyPatch ? ops.applyPatch(input) : applyPatchWithOps(ops, input)
56
+ });
57
+ }
58
+
59
+ //#endregion
60
+ export { makeApplyPatchTool };
61
+ //# sourceMappingURL=apply-patch.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-patch.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/apply-patch.ts"],"sourcesContent":["import type { StandardSchemaV1 } from \"@standard-schema/spec\";\nimport { tool, type ToolContract } from \"@warlock.js/ai\";\nimport type { ApplyPatchInput, ApplyPatchResult, WorkspaceOps } from \"../contracts\";\nimport { applyPatchWithOps } from \"../patch/apply-patch-generic\";\nimport { APPLY_PATCH_JSON_SCHEMA } from \"./json-schemas\";\nimport { withJsonSchema } from \"./schema\";\n\n/** Default tool name exposed to the LLM. */\nconst DEFAULT_NAME = \"apply_patch\";\n\n/** Input schema for the `apply_patch` tool. */\nconst inputSchema = withJsonSchema<ApplyPatchInput>(\n {\n \"~standard\": {\n version: 1,\n vendor: \"ai-workspace\",\n validate(value) {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n return { issues: [{ message: \"input must be an object\" }] };\n }\n\n const candidate = value as Record<string, unknown>;\n const issues: StandardSchemaV1.Issue[] = [];\n\n if (typeof candidate.patch !== \"string\" || candidate.patch.trim() === \"\") {\n issues.push({ message: '\"patch\" must be a non-empty string', path: [\"patch\"] });\n }\n\n if (candidate.dryRun !== undefined && typeof candidate.dryRun !== \"boolean\") {\n issues.push({ message: '\"dryRun\" must be a boolean when provided', path: [\"dryRun\"] });\n }\n\n if (issues.length > 0 || typeof candidate.patch !== \"string\") {\n return { issues };\n }\n\n return {\n value: {\n patch: candidate.patch,\n ...(candidate.dryRun === true ? { dryRun: true } : {}),\n },\n };\n },\n },\n },\n APPLY_PATCH_JSON_SCHEMA,\n);\n\n/**\n * Build the agent-facing `apply_patch` tool over a workspace's policy-enforced\n * {@link WorkspaceOps}: apply a unified diff that modifies, creates, deletes\n * and renames files. It is **atomic** — every hunk of every file applies, or\n * nothing on disk changes — every path stays inside the workspace jail, and a\n * patch that touches a binary file is refused with a clear error. A failure\n * surfaces as a `WorkspaceEditError` in the result's `error` field.\n *\n * @param ops - The shared, policy-enforced operation layer.\n * @param options - Optional overrides; `name` renames the LLM-visible tool.\n *\n * @example\n * const patch = makeApplyPatchTool(ops);\n * const { data, error } = await patch.invoke({\n * patch: \"--- a/src/a.ts\\n+++ b/src/a.ts\\n@@ -1 +1 @@\\n-old\\n+new\\n\",\n * });\n */\nexport function makeApplyPatchTool(\n ops: WorkspaceOps,\n options?: { name?: string },\n): ToolContract<ApplyPatchInput, ApplyPatchResult> {\n return tool<ApplyPatchInput, ApplyPatchResult>({\n name: options?.name ?? DEFAULT_NAME,\n description:\n \"Apply a unified diff to the workspace: modify, create (--- /dev/null), \" +\n \"delete (+++ /dev/null) and rename (git rename from/to) files in one call. \" +\n \"All of it applies or none of it does. Hunks are located by their context, \" +\n \"so small line drift is fine; binary files are refused. Use dryRun to check first.\",\n action: (input) => (input.dryRun === true ? \"Checking a patch\" : \"Applying a patch\"),\n input: inputSchema,\n execute: (input) => (ops.applyPatch ? ops.applyPatch(input) : applyPatchWithOps(ops, input)),\n });\n}\n"],"mappings":";;;;;;;AAQA,MAAM,eAAe;;AAGrB,MAAM,cAAc,eAClB,EACE,aAAa;CACX,SAAS;CACT,QAAQ;CACR,SAAS,OAAO;EACd,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GACpE,OAAO,EAAE,QAAQ,CAAC,EAAE,SAAS,0BAA0B,CAAC,EAAE;EAG5D,MAAM,YAAY;EAClB,MAAM,SAAmC,CAAC;EAE1C,IAAI,OAAO,UAAU,UAAU,YAAY,UAAU,MAAM,KAAK,MAAM,IACpE,OAAO,KAAK;GAAE,SAAS;GAAsC,MAAM,CAAC,OAAO;EAAE,CAAC;EAGhF,IAAI,UAAU,WAAW,UAAa,OAAO,UAAU,WAAW,WAChE,OAAO,KAAK;GAAE,SAAS;GAA4C,MAAM,CAAC,QAAQ;EAAE,CAAC;EAGvF,IAAI,OAAO,SAAS,KAAK,OAAO,UAAU,UAAU,UAClD,OAAO,EAAE,OAAO;EAGlB,OAAO,EACL,OAAO;GACL,OAAO,UAAU;GACjB,GAAI,UAAU,WAAW,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;EACtD,EACF;CACF;AACF,EACF,GACA,uBACF;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,mBACd,KACA,SACiD;CACjD,OAAO,KAAwC;EAC7C,MAAM,SAAS,QAAQ;EACvB,aACE;EAIF,SAAS,UAAW,MAAM,WAAW,OAAO,qBAAqB;EACjE,OAAO;EACP,UAAU,UAAW,IAAI,aAAa,IAAI,WAAW,KAAK,IAAI,kBAAkB,KAAK,KAAK;CAC5F,CAAC;AACH"}
@@ -1,5 +1,5 @@
1
- import { objectSchema, optionalBooleanField, optionalStringField, stringField } from "./schema.mjs";
2
1
  import { EDIT_FILE_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, optionalBooleanField, optionalStringField, stringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/edit-file.ts
@@ -1,5 +1,5 @@
1
- import { objectSchema, stringField } from "./schema.mjs";
2
1
  import { GLOB_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, stringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/glob.ts
@@ -1,5 +1,5 @@
1
- import { objectSchema, optionalBooleanField, optionalStringField, stringField } from "./schema.mjs";
2
1
  import { GREP_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, optionalBooleanField, optionalStringField, stringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/grep.ts
@@ -43,6 +43,15 @@ const EDIT_FILE_JSON_SCHEMA = {
43
43
  "newString"
44
44
  ]
45
45
  };
46
+ /** `apply_patch`. */
47
+ const APPLY_PATCH_JSON_SCHEMA = {
48
+ type: "object",
49
+ properties: {
50
+ 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."),
51
+ dryRun: boolean("Check that the patch applies without writing anything.")
52
+ },
53
+ required: ["patch"]
54
+ };
46
55
  /** `multi_edit`. */
47
56
  const MULTI_EDIT_JSON_SCHEMA = {
48
57
  type: "object",
@@ -171,5 +180,5 @@ const TODO_READ_JSON_SCHEMA = {
171
180
  };
172
181
 
173
182
  //#endregion
174
- export { EDIT_FILE_JSON_SCHEMA, GLOB_JSON_SCHEMA, GREP_JSON_SCHEMA, LIST_DIR_JSON_SCHEMA, MULTI_EDIT_JSON_SCHEMA, READ_FILE_JSON_SCHEMA, RUN_TESTS_JSON_SCHEMA, SHELL_KILL_JSON_SCHEMA, SHELL_OUTPUT_JSON_SCHEMA, TODO_READ_JSON_SCHEMA, TODO_WRITE_JSON_SCHEMA, WRITE_FILE_JSON_SCHEMA, runShellJsonSchema };
183
+ export { APPLY_PATCH_JSON_SCHEMA, EDIT_FILE_JSON_SCHEMA, GLOB_JSON_SCHEMA, GREP_JSON_SCHEMA, LIST_DIR_JSON_SCHEMA, MULTI_EDIT_JSON_SCHEMA, READ_FILE_JSON_SCHEMA, RUN_TESTS_JSON_SCHEMA, SHELL_KILL_JSON_SCHEMA, SHELL_OUTPUT_JSON_SCHEMA, TODO_READ_JSON_SCHEMA, TODO_WRITE_JSON_SCHEMA, WRITE_FILE_JSON_SCHEMA, runShellJsonSchema };
175
184
  //# sourceMappingURL=json-schemas.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"json-schemas.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/json-schemas.ts"],"sourcesContent":["import type { JsonSchemaObject } from \"./schema\";\n\n/**\n * The JSON Schemas the workspace tools publish for their input (read by\n * `extractJsonSchema` and sent to the provider with each tool), kept short\n * because they ride along on every model call. They describe what the\n * hand-rolled validators accept; the validators remain the authority.\n */\n\nconst string = (description: string): JsonSchemaObject => ({ type: \"string\", description });\nconst integer = (description: string): JsonSchemaObject => ({ type: \"integer\", description });\nconst boolean = (description: string): JsonSchemaObject => ({ type: \"boolean\", description });\n\n/** `read_file`. */\nexport const READ_FILE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n startLine: integer(\"1-based first line to return.\"),\n limit: integer(\"Most lines to return.\"),\n },\n required: [\"path\"],\n};\n\n/** `edit_file`. */\nexport const EDIT_FILE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n oldString: string(\"Exact text to replace; must be unique unless replaceAll.\"),\n newString: string(\"Replacement text.\"),\n replaceAll: boolean(\"Replace every occurrence.\"),\n expectHash: string(\"Hash from read_file; rejects the edit if the file changed.\"),\n },\n required: [\"path\", \"oldString\", \"newString\"],\n};\n\n/** `multi_edit`. */\nexport const MULTI_EDIT_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n edits: {\n type: \"array\",\n description:\n \"Replacements applied in order, each to the result of the previous; all or none.\",\n minItems: 1,\n maxItems: 100,\n items: {\n type: \"object\",\n properties: {\n oldString: string(\"Exact text to replace; must be unique unless replaceAll.\"),\n newString: string(\"Replacement text.\"),\n replaceAll: boolean(\"Replace every occurrence.\"),\n },\n required: [\"oldString\", \"newString\"],\n },\n },\n expectHash: string(\"Hash from read_file; rejects every edit if the file changed.\"),\n },\n required: [\"path\", \"edits\"],\n};\n\n/** `write_file`. */\nexport const WRITE_FILE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n content: string(\"Full file content.\"),\n },\n required: [\"path\", \"content\"],\n};\n\n/** `list_dir`. */\nexport const LIST_DIR_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string('Workspace-relative directory (default \".\").'),\n depth: integer(\"Levels to list; 1 = direct children (default).\"),\n ignore: { type: \"array\", items: { type: \"string\" }, description: \"Extra globs to leave out.\" },\n },\n};\n\n/** `run_shell` (the `background` property is only listed when background commands are enabled). */\nexport function runShellJsonSchema(background: boolean): JsonSchemaObject {\n return {\n type: \"object\",\n properties: {\n command: string(\"The command line to run.\"),\n timeoutMs: integer(\"Wall-clock cap in ms (can only lower the workspace limit).\"),\n ...(background\n ? {\n background: boolean(\n \"Start it in the background and return its id at once (dev servers, watchers).\",\n ),\n }\n : {}),\n },\n required: [\"command\"],\n };\n}\n\n/** `shell_output`. */\nexport const SHELL_OUTPUT_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n id: string(\"Background command id from run_shell.\"),\n since: integer(\"A cursor from an earlier result; omit for output since the last read.\"),\n waitMs: integer(\"Wait up to this long (max 30000) for new output.\"),\n },\n required: [\"id\"],\n};\n\n/** `shell_kill`. */\nexport const SHELL_KILL_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: { id: string(\"Background command id from run_shell.\") },\n required: [\"id\"],\n};\n\n/** `run_tests`. */\nexport const RUN_TESTS_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: { pattern: string(\"Optional test path or name filter.\") },\n};\n\n/** `grep`. */\nexport const GREP_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n pattern: string(\"Regular expression to search for.\"),\n glob: string('Only search files matching this glob (e.g. \"src/**/*.ts\").'),\n ignoreCase: boolean(\"Case-insensitive match.\"),\n },\n required: [\"pattern\"],\n};\n\n/** `glob`. */\nexport const GLOB_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: { pattern: string('Glob such as \"src/**/*.ts\".') },\n required: [\"pattern\"],\n};\n\n/** `todo_write`. */\nexport const TODO_WRITE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n todos: {\n type: \"array\",\n description: \"The complete updated list (replaces the old one).\",\n maxItems: 100,\n items: {\n type: \"object\",\n properties: {\n id: string(\"Stable id; omit to number items by position.\"),\n content: string(\"The step.\"),\n status: { type: \"string\", enum: [\"pending\", \"in_progress\", \"completed\"] },\n },\n required: [\"content\", \"status\"],\n },\n },\n },\n required: [\"todos\"],\n};\n\n/** `todo_read`. */\nexport const TODO_READ_JSON_SCHEMA: JsonSchemaObject = { type: \"object\", properties: {} };\n"],"mappings":"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"}
1
+ {"version":3,"file":"json-schemas.mjs","names":[],"sources":["../../../../../../../../packages/ai-workspace/src/tools/json-schemas.ts"],"sourcesContent":["import type { JsonSchemaObject } from \"./schema\";\n\n/**\n * The JSON Schemas the workspace tools publish for their input (read by\n * `extractJsonSchema` and sent to the provider with each tool), kept short\n * because they ride along on every model call. They describe what the\n * hand-rolled validators accept; the validators remain the authority.\n */\n\nconst string = (description: string): JsonSchemaObject => ({ type: \"string\", description });\nconst integer = (description: string): JsonSchemaObject => ({ type: \"integer\", description });\nconst boolean = (description: string): JsonSchemaObject => ({ type: \"boolean\", description });\n\n/** `read_file`. */\nexport const READ_FILE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n startLine: integer(\"1-based first line to return.\"),\n limit: integer(\"Most lines to return.\"),\n },\n required: [\"path\"],\n};\n\n/** `edit_file`. */\nexport const EDIT_FILE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n oldString: string(\"Exact text to replace; must be unique unless replaceAll.\"),\n newString: string(\"Replacement text.\"),\n replaceAll: boolean(\"Replace every occurrence.\"),\n expectHash: string(\"Hash from read_file; rejects the edit if the file changed.\"),\n },\n required: [\"path\", \"oldString\", \"newString\"],\n};\n\n/** `apply_patch`. */\nexport const APPLY_PATCH_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n patch: string(\n \"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.\",\n ),\n dryRun: boolean(\"Check that the patch applies without writing anything.\"),\n },\n required: [\"patch\"],\n};\n\n/** `multi_edit`. */\nexport const MULTI_EDIT_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n edits: {\n type: \"array\",\n description:\n \"Replacements applied in order, each to the result of the previous; all or none.\",\n minItems: 1,\n maxItems: 100,\n items: {\n type: \"object\",\n properties: {\n oldString: string(\"Exact text to replace; must be unique unless replaceAll.\"),\n newString: string(\"Replacement text.\"),\n replaceAll: boolean(\"Replace every occurrence.\"),\n },\n required: [\"oldString\", \"newString\"],\n },\n },\n expectHash: string(\"Hash from read_file; rejects every edit if the file changed.\"),\n },\n required: [\"path\", \"edits\"],\n};\n\n/** `write_file`. */\nexport const WRITE_FILE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string(\"Workspace-relative file path.\"),\n content: string(\"Full file content.\"),\n },\n required: [\"path\", \"content\"],\n};\n\n/** `list_dir`. */\nexport const LIST_DIR_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n path: string('Workspace-relative directory (default \".\").'),\n depth: integer(\"Levels to list; 1 = direct children (default).\"),\n ignore: { type: \"array\", items: { type: \"string\" }, description: \"Extra globs to leave out.\" },\n },\n};\n\n/** `run_shell` (the `background` property is only listed when background commands are enabled). */\nexport function runShellJsonSchema(background: boolean): JsonSchemaObject {\n return {\n type: \"object\",\n properties: {\n command: string(\"The command line to run.\"),\n timeoutMs: integer(\"Wall-clock cap in ms (can only lower the workspace limit).\"),\n ...(background\n ? {\n background: boolean(\n \"Start it in the background and return its id at once (dev servers, watchers).\",\n ),\n }\n : {}),\n },\n required: [\"command\"],\n };\n}\n\n/** `shell_output`. */\nexport const SHELL_OUTPUT_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n id: string(\"Background command id from run_shell.\"),\n since: integer(\"A cursor from an earlier result; omit for output since the last read.\"),\n waitMs: integer(\"Wait up to this long (max 30000) for new output.\"),\n },\n required: [\"id\"],\n};\n\n/** `shell_kill`. */\nexport const SHELL_KILL_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: { id: string(\"Background command id from run_shell.\") },\n required: [\"id\"],\n};\n\n/** `run_tests`. */\nexport const RUN_TESTS_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: { pattern: string(\"Optional test path or name filter.\") },\n};\n\n/** `grep`. */\nexport const GREP_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n pattern: string(\"Regular expression to search for.\"),\n glob: string('Only search files matching this glob (e.g. \"src/**/*.ts\").'),\n ignoreCase: boolean(\"Case-insensitive match.\"),\n },\n required: [\"pattern\"],\n};\n\n/** `glob`. */\nexport const GLOB_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: { pattern: string('Glob such as \"src/**/*.ts\".') },\n required: [\"pattern\"],\n};\n\n/** `todo_write`. */\nexport const TODO_WRITE_JSON_SCHEMA: JsonSchemaObject = {\n type: \"object\",\n properties: {\n todos: {\n type: \"array\",\n description: \"The complete updated list (replaces the old one).\",\n maxItems: 100,\n items: {\n type: \"object\",\n properties: {\n id: string(\"Stable id; omit to number items by position.\"),\n content: string(\"The step.\"),\n status: { type: \"string\", enum: [\"pending\", \"in_progress\", \"completed\"] },\n },\n required: [\"content\", \"status\"],\n },\n },\n },\n required: [\"todos\"],\n};\n\n/** `todo_read`. */\nexport const TODO_READ_JSON_SCHEMA: JsonSchemaObject = { type: \"object\", properties: {} };\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
- import { objectSchema, optionalNumberField, optionalStringArrayField, optionalStringField } from "./schema.mjs";
2
1
  import { LIST_DIR_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, optionalNumberField, optionalStringArrayField, optionalStringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/list-dir.ts
@@ -1,6 +1,6 @@
1
1
  import "../ops.mjs";
2
- import { withJsonSchema } from "./schema.mjs";
3
2
  import { MULTI_EDIT_JSON_SCHEMA } from "./json-schemas.mjs";
3
+ import { withJsonSchema } from "./schema.mjs";
4
4
  import { tool } from "@warlock.js/ai";
5
5
 
6
6
  //#region ../../packages/ai-workspace/src/tools/multi-edit.ts
@@ -1,5 +1,5 @@
1
- import { objectSchema, optionalNumberField, stringField } from "./schema.mjs";
2
1
  import { READ_FILE_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, optionalNumberField, stringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/read-file.ts
@@ -1,6 +1,6 @@
1
1
  import { WorkspacePolicyError } from "../errors.mjs";
2
- import { withJsonSchema } from "./schema.mjs";
3
2
  import { runShellJsonSchema } from "./json-schemas.mjs";
3
+ import { withJsonSchema } from "./schema.mjs";
4
4
  import { tool } from "@warlock.js/ai";
5
5
 
6
6
  //#region ../../packages/ai-workspace/src/tools/run-shell.ts
@@ -1,5 +1,5 @@
1
- import { withJsonSchema } from "./schema.mjs";
2
1
  import { RUN_TESTS_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { withJsonSchema } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/run-tests.ts
@@ -1,5 +1,5 @@
1
- import { objectSchema, stringField } from "./schema.mjs";
2
1
  import { SHELL_KILL_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, stringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/shell-kill.ts
@@ -1,5 +1,5 @@
1
- import { objectSchema, optionalNumberField, stringField } from "./schema.mjs";
2
1
  import { SHELL_OUTPUT_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, optionalNumberField, stringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/shell-output.ts
@@ -1,5 +1,5 @@
1
- import { withJsonSchema } from "./schema.mjs";
2
1
  import { TODO_READ_JSON_SCHEMA, TODO_WRITE_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { withJsonSchema } from "./schema.mjs";
3
3
  import { TODO_STATUSES, todoListProblems } from "../todos.mjs";
4
4
  import { tool } from "@warlock.js/ai";
5
5
 
@@ -1,5 +1,5 @@
1
- import { objectSchema, stringField } from "./schema.mjs";
2
1
  import { WRITE_FILE_JSON_SCHEMA } from "./json-schemas.mjs";
2
+ import { objectSchema, stringField } from "./schema.mjs";
3
3
  import { tool } from "@warlock.js/ai";
4
4
 
5
5
  //#region ../../packages/ai-workspace/src/tools/write-file.ts
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAuZgB,UAAU,QAAQ,kBAAkB;;;;;;;;YAWxC;;IAER,UAAU,QAAQ,kBAAkB"}
1
+ {"version":3,"file":"workspace.d.mts","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBAoagB,UAAU,QAAQ,kBAAkB;;;;;;;;YAWxC;;IAER,UAAU,QAAQ,kBAAkB"}
package/esm/workspace.mjs CHANGED
@@ -1,8 +1,10 @@
1
1
  import { WorkspacePolicyError } from "./errors.mjs";
2
+ import { applyPatchWithOps } from "./patch/apply-patch-generic.mjs";
2
3
  import { ShellJobs } from "./shell-jobs.mjs";
3
4
  import { createOps } from "./ops.mjs";
4
5
  import { createLocalBackend } from "./backends/local.mjs";
5
6
  import { createMockBackend } from "./backends/mock.mjs";
7
+ import { makeApplyPatchTool } from "./tools/apply-patch.mjs";
6
8
  import { makeEditFileTool } from "./tools/edit-file.mjs";
7
9
  import { makeGlobTool } from "./tools/glob.mjs";
8
10
  import { makeGrepTool } from "./tools/grep.mjs";
@@ -28,6 +30,7 @@ const ALL_TOOL_NAMES = [
28
30
  "readFile",
29
31
  "editFile",
30
32
  "multiEdit",
33
+ "applyPatch",
31
34
  "writeFile",
32
35
  "listDir",
33
36
  "runShell",
@@ -107,6 +110,7 @@ var WorkspaceImpl = class WorkspaceImpl {
107
110
  readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),
108
111
  editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),
109
112
  multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),
113
+ applyPatch: (opts) => erase(makeApplyPatchTool(this.ops, opts)),
110
114
  writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),
111
115
  listDir: (opts) => erase(makeListDirTool(this.ops, opts)),
112
116
  runShell: (opts) => erase(makeRunShellTool(this.ops, {
@@ -133,6 +137,7 @@ var WorkspaceImpl = class WorkspaceImpl {
133
137
  readFile: (opts) => build("readFile", opts),
134
138
  editFile: (opts) => build("editFile", opts),
135
139
  multiEdit: (opts) => build("multiEdit", opts),
140
+ applyPatch: (opts) => build("applyPatch", opts),
136
141
  writeFile: (opts) => build("writeFile", opts),
137
142
  listDir: (opts) => build("listDir", opts),
138
143
  runShell: (opts) => build("runShell", opts),
@@ -172,6 +177,10 @@ var WorkspaceImpl = class WorkspaceImpl {
172
177
  this.assertWritable("multiEdit");
173
178
  return this.ops.multiEdit(input);
174
179
  }
180
+ async applyPatch(input) {
181
+ this.assertWritable("applyPatch");
182
+ return this.ops.applyPatch ? this.ops.applyPatch(input) : applyPatchWithOps(this.ops, input);
183
+ }
175
184
  listDir(dirPath, opts) {
176
185
  return this.ops.listDir(dirPath, opts);
177
186
  }
@@ -1 +1 @@
1
- {"version":3,"file":"workspace.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"sourcesContent":["import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { ShellJobs } from \"./shell-jobs\";\nimport { TodoStore } from \"./todos\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeListDirTool } from \"./tools/list-dir\";\nimport { makeMultiEditTool } from \"./tools/multi-edit\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeShellKillTool } from \"./tools/shell-kill\";\nimport { makeShellOutputTool } from \"./tools/shell-output\";\nimport { makeTodoReadTool, makeTodoWriteTool } from \"./tools/todo\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepResult,\n ListDirOptions,\n ListDirResult,\n MultiEditInput,\n MultiEditResult,\n ReadFileResult,\n RunShellResult,\n ShellJobInfo,\n ShellOutputOptions,\n ShellOutputResult,\n StartShellOptions,\n TodoListResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n WorkspaceTodoInput,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"multiEdit\",\n \"writeFile\",\n \"listDir\",\n \"runShell\",\n \"shellOutput\",\n \"shellKill\",\n \"runTests\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * tools that cannot change the tree. `editFile` / `multiEdit` /\n * `writeFile` / the shell tools / `runTests` are deliberately omitted so a\n * reviewer agent has no path to change it. The todo tools stay: they only\n * touch the in-memory plan.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"listDir\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/** Tools vended only when the policy enables background commands. */\nconst BACKGROUND_TOOL_NAMES: ReadonlySet<WorkspaceToolName> = new Set([\"shellOutput\", \"shellKill\"]);\n\n/**\n * State one workspace shares with every projection made from it: its\n * background commands and its todo list.\n */\ninterface WorkspaceState {\n jobs: ShellJobs;\n todos: TodoStore;\n}\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n /** Background commands + todo list, shared with projections. */\n private readonly state: WorkspaceState;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n state?: WorkspaceState,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n this.state = state ?? { jobs: new ShellJobs(), todos: new TodoStore() };\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy, { jobs: this.state.jobs });\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(contract: ToolContract<TInput, TOutput>): ToolContract =>\n contract as unknown as ToolContract;\n\n const shell = this.policy.shell;\n const background = shell?.background !== undefined;\n const interpreter = shell?.interpreter ?? \"none\";\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n listDir: (opts) => erase(makeListDirTool(this.ops, opts)),\n runShell: (opts) =>\n erase(\n makeRunShellTool(this.ops, {\n ...(opts?.name !== undefined ? { name: opts.name } : {}),\n ...(shell?.timeoutMs !== undefined ? { maxTimeoutMs: shell.timeoutMs } : {}),\n background,\n }),\n ),\n shellOutput: (opts) => erase(makeShellOutputTool(this.ops, opts)),\n shellKill: (opts) => erase(makeShellKillTool(this.ops, opts)),\n runTests: (opts) => erase(makeRunTestsTool(this.ops, { ...opts, interpreter })),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n todoWrite: (opts) => erase(makeTodoWriteTool(this.state.todos, opts)),\n todoRead: (opts) => erase(makeTodoReadTool(this.state.todos, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n // `shell_output` / `shell_kill` only mean something when background\n // commands can be started, so `all()` leaves them out otherwise.\n const vendedByAll = (name: WorkspaceToolName): boolean =>\n this.allowedTools.has(name) && (background || !BACKGROUND_TOOL_NAMES.has(name));\n\n return {\n all: () => ALL_TOOL_NAMES.filter(vendedByAll).map((name) => build(name)),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n multiEdit: (opts) => build(\"multiEdit\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n listDir: (opts) => build(\"listDir\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n shellOutput: (opts) => build(\"shellOutput\", opts),\n shellKill: (opts) => build(\"shellKill\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n todoWrite: (opts) => build(\"todoWrite\", opts),\n todoRead: (opts) => build(\"todoRead\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<ReadFileResult> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async multiEdit(input: MultiEditInput): Promise<MultiEditResult> {\n this.assertWritable(\"multiEdit\");\n\n return this.ops.multiEdit(input);\n }\n\n public listDir(dirPath?: string, opts?: ListDirOptions): Promise<ListDirResult> {\n return this.ops.listDir(dirPath, opts);\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public async startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo> {\n this.assertWritable(\"startShell\");\n\n return this.ops.startShell(command, opts);\n }\n\n public shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n return this.ops.shellOutput(id, opts);\n }\n\n public async shellKill(id: string): Promise<ShellJobInfo> {\n this.assertWritable(\"shellKill\");\n\n return this.ops.shellKill(id);\n }\n\n public listShells(): ShellJobInfo[] {\n return this.ops.listShells();\n }\n\n public readTodos(): TodoListResult {\n return this.state.todos.read();\n }\n\n public writeTodos(todos: WorkspaceTodoInput[]): TodoListResult {\n return this.state.todos.write(todos);\n }\n\n public dispose(): Promise<void> {\n return this.ops.dispose();\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string): Promise<string[]> {\n return this.ops.glob(pattern);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read / list /\n * grep / glob / todo tools are vended and every mutating direct method\n * rejects with a {@link WorkspacePolicyError}. A fresh ops/backend is\n * built from the identical policy, so the projection sees the same jailed\n * tree; it shares this workspace's background commands and todo list.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(\n this.policy,\n READONLY_TOOL_NAMES.filter((name) => this.allowedTools.has(name)),\n this.state,\n );\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root. It\n * shares this workspace's background commands and todo list.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n this.state,\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /\n * `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /\n * `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)\n * and `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` /\n * `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /\n * `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /\n * `remove` / `readTodos` / `writeTodos`, each delegating to the shared\n * ops layer, and `dispose()`.\n * - **`readonly()`** — a projection that vends only the read-side tools and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
1
+ {"version":3,"file":"workspace.mjs","names":[],"sources":["../../../../../../../packages/ai-workspace/src/workspace.ts"],"sourcesContent":["import path from \"node:path\";\nimport { ai, type ToolContract } from \"@warlock.js/ai\";\nimport { createLocalBackend } from \"./backends/local\";\nimport { createMockBackend } from \"./backends/mock\";\nimport { WorkspacePolicyError } from \"./errors\";\nimport { createOps } from \"./ops\";\nimport { ShellJobs } from \"./shell-jobs\";\nimport { TodoStore } from \"./todos\";\nimport { applyPatchWithOps } from \"./patch/apply-patch-generic\";\nimport { makeApplyPatchTool } from \"./tools/apply-patch\";\nimport { makeEditFileTool } from \"./tools/edit-file\";\nimport { makeGlobTool } from \"./tools/glob\";\nimport { makeGrepTool } from \"./tools/grep\";\nimport { makeListDirTool } from \"./tools/list-dir\";\nimport { makeMultiEditTool } from \"./tools/multi-edit\";\nimport { makeReadFileTool } from \"./tools/read-file\";\nimport { makeRunShellTool } from \"./tools/run-shell\";\nimport { makeRunTestsTool } from \"./tools/run-tests\";\nimport { makeShellKillTool } from \"./tools/shell-kill\";\nimport { makeShellOutputTool } from \"./tools/shell-output\";\nimport { makeTodoReadTool, makeTodoWriteTool } from \"./tools/todo\";\nimport { makeWriteFileTool } from \"./tools/write-file\";\nimport type {\n ApplyPatchInput,\n ApplyPatchResult,\n EditFileInput,\n EditFileResult,\n ExecOptions,\n GrepResult,\n ListDirOptions,\n ListDirResult,\n MultiEditInput,\n MultiEditResult,\n ReadFileResult,\n RunShellResult,\n ShellJobInfo,\n ShellOutputOptions,\n ShellOutputResult,\n StartShellOptions,\n TodoListResult,\n Workspace,\n WorkspaceBackend,\n WorkspaceOps,\n WorkspacePolicy,\n WorkspaceTodoInput,\n WorkspaceToolName,\n WorkspaceTools,\n} from \"./contracts\";\n\n/**\n * The full set of tool names a writable workspace vends, in a stable\n * canonical order so `tools.all()` is deterministic.\n */\nconst ALL_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"editFile\",\n \"multiEdit\",\n \"applyPatch\",\n \"writeFile\",\n \"listDir\",\n \"runShell\",\n \"shellOutput\",\n \"shellKill\",\n \"runTests\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/**\n * The subset a {@link Workspace.readonly} projection exposes — the\n * tools that cannot change the tree. `editFile` / `multiEdit` /\n * `writeFile` / the shell tools / `runTests` are deliberately omitted so a\n * reviewer agent has no path to change it. The todo tools stay: they only\n * touch the in-memory plan.\n */\nconst READONLY_TOOL_NAMES: readonly WorkspaceToolName[] = [\n \"readFile\",\n \"listDir\",\n \"grep\",\n \"glob\",\n \"todoWrite\",\n \"todoRead\",\n];\n\n/** Tools vended only when the policy enables background commands. */\nconst BACKGROUND_TOOL_NAMES: ReadonlySet<WorkspaceToolName> = new Set([\"shellOutput\", \"shellKill\"]);\n\n/**\n * State one workspace shares with every projection made from it: its\n * background commands and its todo list.\n */\ninterface WorkspaceState {\n jobs: ShellJobs;\n todos: TodoStore;\n}\n\n/**\n * Choose the dumb IO executor for a policy. `\"mock\"` selects the\n * in-memory backend (hermetic tests); anything else — including the\n * `\"local\"` default and an absent `backend` — selects the real-disk\n * local backend.\n */\nfunction selectBackend(policy: WorkspacePolicy): WorkspaceBackend {\n if (policy.backend === \"mock\") {\n return createMockBackend();\n }\n\n return createLocalBackend();\n}\n\n/**\n * The internal {@link Workspace} implementation. Holds the resolved\n * backend, the policy, and the single shared {@link WorkspaceOps} seam\n * that both the agent-facing `.tools.*` factories and the human-facing\n * direct methods funnel through — one jail, one rule set, two callers.\n *\n * The `allowedTools` set narrows what `tools.*` will vend and which\n * mutating direct methods are permitted: a full workspace allows every\n * name; a {@link WorkspaceImpl.readonly} projection allows only the\n * read/grep/glob subset and rejects writes/edits/shell/mkdir/remove.\n *\n * Constructed via {@link workspace}; the class itself is internal.\n */\nclass WorkspaceImpl implements Workspace {\n /** The shared, policy-enforced operation layer (jail + guards). */\n private readonly ops: WorkspaceOps;\n\n /** Tool names this projection is permitted to vend / mutate through. */\n private readonly allowedTools: ReadonlySet<WorkspaceToolName>;\n\n public readonly policy: WorkspacePolicy;\n\n public readonly tools: WorkspaceTools;\n\n /** Background commands + todo list, shared with projections. */\n private readonly state: WorkspaceState;\n\n public constructor(\n policy: WorkspacePolicy,\n allowedTools: readonly WorkspaceToolName[] = ALL_TOOL_NAMES,\n state?: WorkspaceState,\n ) {\n this.policy = policy;\n this.allowedTools = new Set(allowedTools);\n this.state = state ?? { jobs: new ShellJobs(), todos: new TodoStore() };\n\n const backend = selectBackend(policy);\n this.ops = createOps(backend, policy, { jobs: this.state.jobs });\n this.tools = this.buildTools();\n }\n\n /**\n * Assemble the agent-facing tool namespace. Each factory builds its\n * tool over the shared `ops`; `all()` returns every *allowed* tool in\n * canonical order and `pick(...)` returns the named subset (silently\n * dropping any name this projection does not allow, so a `readonly()`\n * workspace can never be coaxed into vending a mutating tool).\n */\n private buildTools(): WorkspaceTools {\n // Each `make*Tool` returns a precisely-typed\n // `ToolContract<SpecificInput, SpecificOutput>`, but the agent-facing\n // `WorkspaceTools` surface vends the type-erased `ToolContract`\n // (`ToolContract<unknown, unknown>`). Because `ToolContract` puts its\n // input in a contravariant position (`execute(input)` / `action(input)`),\n // a specific contract is not assignable to the erased one — so erase it\n // once, here, through `unknown`. The runtime object is identical; only\n // the static input type is widened for the shared surface.\n const erase = <TInput, TOutput>(contract: ToolContract<TInput, TOutput>): ToolContract =>\n contract as unknown as ToolContract;\n\n const shell = this.policy.shell;\n const background = shell?.background !== undefined;\n const interpreter = shell?.interpreter ?? \"none\";\n\n const factories: Record<\n WorkspaceToolName,\n (opts?: { name?: string; command?: string }) => ToolContract\n > = {\n readFile: (opts) => erase(makeReadFileTool(this.ops, opts)),\n editFile: (opts) => erase(makeEditFileTool(this.ops, opts)),\n multiEdit: (opts) => erase(makeMultiEditTool(this.ops, opts)),\n applyPatch: (opts) => erase(makeApplyPatchTool(this.ops, opts)),\n writeFile: (opts) => erase(makeWriteFileTool(this.ops, opts)),\n listDir: (opts) => erase(makeListDirTool(this.ops, opts)),\n runShell: (opts) =>\n erase(\n makeRunShellTool(this.ops, {\n ...(opts?.name !== undefined ? { name: opts.name } : {}),\n ...(shell?.timeoutMs !== undefined ? { maxTimeoutMs: shell.timeoutMs } : {}),\n background,\n }),\n ),\n shellOutput: (opts) => erase(makeShellOutputTool(this.ops, opts)),\n shellKill: (opts) => erase(makeShellKillTool(this.ops, opts)),\n runTests: (opts) => erase(makeRunTestsTool(this.ops, { ...opts, interpreter })),\n grep: (opts) => erase(makeGrepTool(this.ops, opts)),\n glob: (opts) => erase(makeGlobTool(this.ops, opts)),\n todoWrite: (opts) => erase(makeTodoWriteTool(this.state.todos, opts)),\n todoRead: (opts) => erase(makeTodoReadTool(this.state.todos, opts)),\n };\n\n const build = (name: WorkspaceToolName, opts?: { name?: string; command?: string }) =>\n factories[name](opts);\n\n // `shell_output` / `shell_kill` only mean something when background\n // commands can be started, so `all()` leaves them out otherwise.\n const vendedByAll = (name: WorkspaceToolName): boolean =>\n this.allowedTools.has(name) && (background || !BACKGROUND_TOOL_NAMES.has(name));\n\n return {\n all: () => ALL_TOOL_NAMES.filter(vendedByAll).map((name) => build(name)),\n pick: (...names: WorkspaceToolName[]) =>\n names.filter((name) => this.allowedTools.has(name)).map((name) => build(name)),\n readFile: (opts) => build(\"readFile\", opts),\n editFile: (opts) => build(\"editFile\", opts),\n multiEdit: (opts) => build(\"multiEdit\", opts),\n applyPatch: (opts) => build(\"applyPatch\", opts),\n writeFile: (opts) => build(\"writeFile\", opts),\n listDir: (opts) => build(\"listDir\", opts),\n runShell: (opts) => build(\"runShell\", opts),\n shellOutput: (opts) => build(\"shellOutput\", opts),\n shellKill: (opts) => build(\"shellKill\", opts),\n runTests: (opts) => build(\"runTests\", opts),\n grep: (opts) => build(\"grep\", opts),\n glob: (opts) => build(\"glob\", opts),\n todoWrite: (opts) => build(\"todoWrite\", opts),\n todoRead: (opts) => build(\"todoRead\", opts),\n };\n }\n\n /**\n * Reject a mutating direct method on a read-only projection — surfaced\n * as a {@link WorkspacePolicyError} (the same typed error a denied\n * command produces) so a caller branches on `error.type`.\n */\n private assertWritable(operation: string): void {\n if (this.allowedTools.has(\"writeFile\")) {\n return;\n }\n\n throw new WorkspacePolicyError(\n `Operation \"${operation}\" is not permitted on a read-only workspace.`,\n { type: \"denied-command\", command: operation },\n );\n }\n\n public readFile(\n filePath: string,\n opts?: { offset?: number; limit?: number },\n ): Promise<ReadFileResult> {\n return this.ops.readFile(filePath, opts);\n }\n\n public async writeFile(\n filePath: string,\n content: string,\n ): Promise<{ hash: string; bytesWritten: number }> {\n this.assertWritable(\"writeFile\");\n\n return this.ops.writeFile(filePath, content);\n }\n\n public async editFile(input: EditFileInput): Promise<EditFileResult> {\n this.assertWritable(\"editFile\");\n\n return this.ops.editFile(input);\n }\n\n public async multiEdit(input: MultiEditInput): Promise<MultiEditResult> {\n this.assertWritable(\"multiEdit\");\n\n return this.ops.multiEdit(input);\n }\n\n public async applyPatch(input: ApplyPatchInput): Promise<ApplyPatchResult> {\n this.assertWritable(\"applyPatch\");\n\n return this.ops.applyPatch ? this.ops.applyPatch(input) : applyPatchWithOps(this.ops, input);\n }\n\n public listDir(dirPath?: string, opts?: ListDirOptions): Promise<ListDirResult> {\n return this.ops.listDir(dirPath, opts);\n }\n\n public async exec(command: string, opts?: ExecOptions): Promise<RunShellResult> {\n this.assertWritable(\"exec\");\n\n return this.ops.exec(command, opts);\n }\n\n public async startShell(command: string, opts?: StartShellOptions): Promise<ShellJobInfo> {\n this.assertWritable(\"startShell\");\n\n return this.ops.startShell(command, opts);\n }\n\n public shellOutput(id: string, opts?: ShellOutputOptions): Promise<ShellOutputResult> {\n return this.ops.shellOutput(id, opts);\n }\n\n public async shellKill(id: string): Promise<ShellJobInfo> {\n this.assertWritable(\"shellKill\");\n\n return this.ops.shellKill(id);\n }\n\n public listShells(): ShellJobInfo[] {\n return this.ops.listShells();\n }\n\n public readTodos(): TodoListResult {\n return this.state.todos.read();\n }\n\n public writeTodos(todos: WorkspaceTodoInput[]): TodoListResult {\n return this.state.todos.write(todos);\n }\n\n public dispose(): Promise<void> {\n return this.ops.dispose();\n }\n\n public grep(\n pattern: string,\n opts?: { glob?: string; ignoreCase?: boolean },\n ): Promise<GrepResult> {\n return this.ops.grep(pattern, opts);\n }\n\n public glob(pattern: string): Promise<string[]> {\n return this.ops.glob(pattern);\n }\n\n public exists(filePath: string): Promise<boolean> {\n return this.ops.exists(filePath);\n }\n\n public async mkdir(filePath: string): Promise<void> {\n this.assertWritable(\"mkdir\");\n\n return this.ops.mkdir(filePath);\n }\n\n public async remove(filePath: string): Promise<void> {\n this.assertWritable(\"remove\");\n\n return this.ops.remove(filePath);\n }\n\n /**\n * A read-only projection over the SAME policy — only the read / list /\n * grep / glob / todo tools are vended and every mutating direct method\n * rejects with a {@link WorkspacePolicyError}. A fresh ops/backend is\n * built from the identical policy, so the projection sees the same jailed\n * tree; it shares this workspace's background commands and todo list.\n */\n public readonly(): Workspace {\n return new WorkspaceImpl(\n this.policy,\n READONLY_TOOL_NAMES.filter((name) => this.allowedTools.has(name)),\n this.state,\n );\n }\n\n /**\n * A sub-jailed view rooted at `subdir` (relative to this workspace's\n * `cwd`). Returns a brand-new workspace whose policy is this policy\n * with `cwd` narrowed to `join(cwd, subdir)` — same backend selection,\n * same allow/deny/shell/read sub-policies, but a tighter jail root. It\n * shares this workspace's background commands and todo list.\n */\n public scope(subdir: string): Workspace {\n return new WorkspaceImpl(\n { ...this.policy, cwd: path.join(this.policy.cwd, subdir) },\n [...this.allowedTools],\n this.state,\n );\n }\n}\n\n/**\n * Build a {@link Workspace} — the integrator that wires a\n * {@link WorkspacePolicy} to a backend, the shared policy-enforced ops\n * layer, and the agent-facing tool factories.\n *\n * The backend is chosen from `policy.backend`: `\"mock\"` runs in memory\n * (hermetic tests); the `\"local\"` default (and any absent value) runs\n * over the real disk via `@warlock.js/fs` + `node:child_process`. The\n * returned workspace exposes:\n *\n * - **`tools.*`** — `readFile` / `editFile` / `multiEdit` / `writeFile` /\n * `listDir` / `runShell` / `shellOutput` / `shellKill` / `runTests` /\n * `grep` / `glob` / `todoWrite` / `todoRead`, plus `all()` (every tool)\n * and `pick(...)` (a least-privilege subset).\n * - **direct methods** — `readFile` / `writeFile` / `editFile` /\n * `multiEdit` / `listDir` / `exec` / `startShell` / `shellOutput` /\n * `shellKill` / `listShells` / `grep` / `glob` / `exists` / `mkdir` /\n * `remove` / `readTodos` / `writeTodos`, each delegating to the shared\n * ops layer, and `dispose()`.\n * - **`readonly()`** — a projection that vends only the read-side tools and\n * rejects every mutating direct method.\n * - **`scope(subdir)`** — a sub-jailed workspace rooted at `subdir`.\n *\n * Available at runtime as `ai.workspace(policy)` once this module is\n * imported (it registers the verb on the shared `ai` object).\n *\n * @param policy - The policy bounding the workspace (its `cwd` is the jail root).\n * @returns A fully-wired {@link Workspace}.\n *\n * @example\n * const ws = workspace({ cwd: \"/srv/acme-api\", shell: { allow: [\"npm\"], inheritEnv: [\"PATH\"] } });\n * const dev = ai.agent({ model, tools: ws.tools.all() });\n * await dev.execute(\"Make the failing cart-total suite green.\");\n *\n * @example\n * // Least-privilege reviewer — no write, no shell.\n * const reviewer = ai.agent({ model, tools: ws.readonly().tools.all() });\n */\nexport function workspace(policy: WorkspacePolicy): Workspace {\n return new WorkspaceImpl(policy);\n}\n\n/**\n * Attach the `workspace` verb to the `ai` namespace via module augmentation,\n * per the `ai.`-namespace convention. `@warlock.js/ai` now exposes a named `Ai`\n * interface for exactly this, so after a bare `import \"@warlock.js/ai-workspace\"`,\n * `ai.workspace(...)` is globally typed — no view/cast needed.\n */\ndeclare module \"@warlock.js/ai\" {\n interface Ai {\n /** Build a policy-jailed filesystem + shell {@link Workspace}. */\n workspace(policy: WorkspacePolicy): Workspace;\n }\n}\n\n// Runtime registration: attach `workspace` onto the shared `ai` object the\n// moment this package is imported (the augmentation above types it).\nai.workspace = workspace;\n"],"mappings":"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"}
package/llms-full.txt CHANGED
@@ -7,7 +7,7 @@
7
7
  ## Package orientation
8
8
 
9
9
  ---
10
- description: 'Policy-confined file and shell tools for coding agents: `workspace`, `createLocalBackend`, `createMockBackend`, `makeEditFileTool`, `makeRunShellTool` and `WorkspacePolicyError`. Use for "let an agent edit files", "sandbox shell commands" or "loop until tests pass". Not this package → @warlock.js/ai-tools for MCP/web tools; @warlock.js/ai for agents; @warlock.js/ai-cli for installed coding CLIs.'
10
+ description: 'Policy-confined file and shell tools for coding agents: `workspace`, `createLocalBackend`, `createMockBackend`, `makeEditFileTool`, `makeReadFileTool`, `makeWriteFileTool`, `makeRunShellTool`, `makeRunTestsTool` and `WorkspacePolicyError`. Use for "let an agent edit files", "sandbox shell commands", "apply a patch" or "loop until tests pass". Not this package → @warlock.js/ai-tools for MCP/web tools; @warlock.js/ai for agents; @warlock.js/ai-cli for installed coding CLIs.'
11
11
  ---
12
12
 
13
13
  # @warlock.js/ai-workspace
@@ -53,7 +53,7 @@ const ws = ai.workspace({
53
53
 
54
54
  const dev = ai.agent({
55
55
  model,
56
- tools: ws.tools.all(), // read/edit/multi_edit/write, list_dir, run_shell, run_tests, grep, glob, todo_write/todo_read
56
+ tools: ws.tools.all(), // read/edit/multi_edit/apply_patch/write, list_dir, run_shell, run_tests, grep, glob, todo_write/todo_read
57
57
  maxTrips: 25, // give the loop room to iterate
58
58
  });
59
59
 
@@ -138,7 +138,7 @@ ai.supervisor({
138
138
 
139
139
  ---
140
140
  name: use-a-workspace
141
- description: 'Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `ws.multiEdit`, `ws.listDir`, `ws.startShell`, `ws.shellOutput`, `ws.shellKill`, `ws.dispose`, `ws.readTodos`, `ws.writeTodos`, `WorkspacePolicyError`, `WorkspaceEditError`, `WorkspaceJobError`, `WorkspaceShellRefusalReason`, `checkShellCommand`, `denyPaths`, `denyPatterns`, `allowCommand`, `interpreter`, `shell.background`, `list.ignore`, `inheritEnv`, `expectHash`, `read.maxBytes`, `bytesCapped`, `cancelled`, `ExecOptions`, `tokenizeCommand`, `read_file`/`edit_file`/`multi_edit`/`write_file`/`list_dir`/`run_shell`/`shell_output`/`shell_kill`/`run_tests`/`grep`/`todo_write`/`todo_read`; ''give an agent file tools'', ''jail an agent to a directory'', ''let an agent read/write/grep a repo'', ''read-only workspace for a reviewer'', ''scope an agent to a subdirectory'', ''run a shell command under policy'', ''edit a file with a stale-hash guard'', ''command injection guard for an agent shell tool'', ''ReDoS-safe grep for an agent'', ''cap how many bytes a read returns'', ''cancel a running workspace command'', ''let the agent use pipes and &&'', ''why was my shell command refused'', ''deny git push --force'', ''run a dev server in the background'', ''atomic multi-edit'', ''list a directory for an agent'', ''todo list tool for an agent''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — the `build-loop-agent` topic; defining a non-filesystem custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
141
+ description: 'Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `ws.multiEdit`, `ws.applyPatch`, `ws.listDir`, `ws.startShell`, `ws.shellOutput`, `ws.shellKill`, `ws.dispose`, `ws.readTodos`, `ws.writeTodos`, `WorkspacePolicyError`, `WorkspaceEditError`, `WorkspaceJobError`, `WorkspaceShellRefusalReason`, `checkShellCommand`, `denyPaths`, `denyPatterns`, `allowCommand`, `interpreter`, `shell.background`, `list.ignore`, `inheritEnv`, `expectHash`, `read.maxBytes`, `bytesCapped`, `cancelled`, `ExecOptions`, `tokenizeCommand`, `read_file`/`edit_file`/`multi_edit`/`apply_patch`/`write_file`/`list_dir`/`run_shell`/`shell_output`/`shell_kill`/`run_tests`/`grep`/`todo_write`/`todo_read`; ''give an agent file tools'', ''jail an agent to a directory'', ''let an agent read/write/grep a repo'', ''read-only workspace for a reviewer'', ''scope an agent to a subdirectory'', ''run a shell command under policy'', ''edit a file with a stale-hash guard'', ''command injection guard for an agent shell tool'', ''ReDoS-safe grep for an agent'', ''cap how many bytes a read returns'', ''cancel a running workspace command'', ''let the agent use pipes and &&'', ''why was my shell command refused'', ''deny git push --force'', ''run a dev server in the background'', ''atomic multi-edit'', ''list a directory for an agent'', ''todo list tool for an agent''; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — the `build-loop-agent` topic; defining a non-filesystem custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.'
142
142
  ---
143
143
 
144
144
  # Use a workspace — a policy jail for an agent's filesystem + shell
@@ -242,7 +242,7 @@ export async function commitWork(cwd: string) {
242
242
 
243
243
  ### Agent-facing — `ws.tools.*`
244
244
 
245
- Each tool is a `ToolContract` (with a JSON Schema of its input) ready for `ai.agent({ tools })`. The LLM-visible tool names are `read_file`, `edit_file`, `multi_edit`, `write_file`, `list_dir`, `run_shell`, `shell_output`, `shell_kill`, `run_tests`, `grep`, `glob`, `todo_write`, `todo_read` — `shell_output` / `shell_kill` only in `all()` when `shell.background` is set.
245
+ Each tool is a `ToolContract` (with a JSON Schema of its input) ready for `ai.agent({ tools })`. The LLM-visible tool names are `read_file`, `edit_file`, `multi_edit`, `apply_patch`, `write_file`, `list_dir`, `run_shell`, `shell_output`, `shell_kill`, `run_tests`, `grep`, `glob`, `todo_write`, `todo_read` — `shell_output` / `shell_kill` only in `all()` when `shell.background` is set.
246
246
 
247
247
  ```ts
248
248
  ws.tools.all(); // every tool, canonical order
@@ -310,6 +310,7 @@ export async function devLoop(cwd: string) {
310
310
  ```
311
311
 
312
312
  - `list_dir({ path?, depth?, ignore? })` → `{ path, entries: [{ path, type, size?, depth }], truncated }`; `denyPaths` always hidden, `list.ignore` (default `node_modules`, `.git`) hidden from listings, symlinks reported but never followed.
313
+ - `apply_patch({ patch, dryRun? })` → `{ files: [{ path, action, oldPath?, hunks, hash? }], dryRun }`; a unified diff that modifies, creates, deletes and renames files, all or none; paths jailed, hunks matched by context, line endings kept, binary files refused (`binary-file`); `dryRun` checks only. Not in `readonly()`.
313
314
  - `multi_edit({ path, edits, expectHash? })` → `{ path, edits, replacements, hash }`; same uniqueness rules as `edit_file`, at most 100 edits.
314
315
  - Background: `shell_output({ id, since?, waitMs? })` → `{ stdout, stderr, cursor, droppedBytes, truncated, status, exitCode }`; only the newest `shell.maxOutputBytes` per command are kept. States `"running"`, `"exited"`, `"killed"`, `"timed-out"`. Unknown id → `WorkspaceJobError`. Background commands outlive the agent run; `dispose()` (or process exit) kills them. Only processes the workspace spawned are ever signalled.
315
316
  - Todos: one in-memory list per workspace, shared with `readonly()` / `scope()`; `todo_write` replaces the whole list (≤ 100 items, status `pending` / `in_progress` / `completed`).
@@ -376,7 +377,7 @@ try {
376
377
  ```
377
378
 
378
379
  - `WorkspacePolicyError.type`: `"path-escape"` (jail escape / deny glob) | `"denied-command"` (the shell policy refused the line — `reason` says why) | `"unsafe-pattern"` (`grep` pattern too long or nested-quantifier ReDoS shape).
379
- - `WorkspaceEditError.type`: `"not-found"` | `"not-unique"` | `"stale-hash"` (+ `editIndex` for `multiEdit`).
380
+ - `WorkspaceEditError.type`: `"not-found"` | `"not-unique"` | `"stale-hash"` | `"patch-invalid"` | `"hunk-failed"` | `"binary-file"` | `"file-exists"` | `"file-missing"` (+ `editIndex` for `multiEdit`, `hunkIndex` for `applyPatch`).
380
381
  - `WorkspaceJobError.type`: `"not-found"` (unknown background command id).
381
382
 
382
383
  ## Composition — `readonly()` and `scope()`
@@ -386,7 +387,7 @@ const ro = ws.readonly(); // vends only read/grep/glob; mutating direct methods
386
387
  const api = ws.scope("packages/api"); // a sub-jailed workspace rooted at the subdir
387
388
  ```
388
389
 
389
- `readonly()` is the least-privilege reviewer projection — `tools.all()` on it returns `read_file`, `list_dir`, `grep`, `glob` and the todo tools only, and `writeFile` / `editFile` / `multiEdit` / `exec` / `startShell` / `shellKill` / `mkdir` / `remove` throw a `WorkspacePolicyError`. Projections share the workspace's background commands and todo list. `scope(subdir)` narrows `cwd` to `join(cwd, subdir)` (same sub-policies, same backend), so a per-package agent gets a tighter blast radius.
390
+ `readonly()` is the least-privilege reviewer projection — `tools.all()` on it returns `read_file`, `list_dir`, `grep`, `glob` and the todo tools only, and `writeFile` / `editFile` / `multiEdit` / `applyPatch` / `exec` / `startShell` / `shellKill` / `mkdir` / `remove` throw a `WorkspacePolicyError`. Projections share the workspace's background commands and todo list. `scope(subdir)` narrows `cwd` to `join(cwd, subdir)` (same sub-policies, same backend), so a per-package agent gets a tighter blast radius.
390
391
 
391
392
  ## Hermetic tests with the mock backend
392
393
 
package/llms.txt CHANGED
@@ -6,6 +6,6 @@
6
6
 
7
7
  ## Skills
8
8
 
9
- - [Package orientation](@warlock.js/ai-workspace/skills/index.md): Policy-confined file and shell tools for coding agents: `workspace`, `createLocalBackend`, `createMockBackend`, `makeEditFileTool`, `makeRunShellTool` and `WorkspacePolicyError`. Use for "let an agent edit files", "sandbox shell commands" or "loop until tests pass". Not this package → @warlock.js/ai-tools for MCP/web tools; @warlock.js/ai for agents; @warlock.js/ai-cli for installed coding CLIs.
9
+ - [Package orientation](@warlock.js/ai-workspace/skills/index.md): Policy-confined file and shell tools for coding agents: `workspace`, `createLocalBackend`, `createMockBackend`, `makeEditFileTool`, `makeReadFileTool`, `makeWriteFileTool`, `makeRunShellTool`, `makeRunTestsTool` and `WorkspacePolicyError`. Use for "let an agent edit files", "sandbox shell commands", "apply a patch" or "loop until tests pass". Not this package → @warlock.js/ai-tools for MCP/web tools; @warlock.js/ai for agents; @warlock.js/ai-cli for installed coding CLIs.
10
10
  - [build-loop-agent](@warlock.js/ai-workspace/skills/build-loop-agent/SKILL.md): Wire a @warlock.js/ai-workspace into a coding agent that closes the read → edit → run-tests loop on its own. Triggers: `ws.tools.all()`, `ai.agent({ tools: ws.tools.all() })`, `maxTrips`, `run_tests`, `run_shell`, 'agent that fixes a failing test', 'make the suite green', 'coding agent that edits a repo', 'build-loop agent', 'self-correcting agent', 'read-only code reviewer', 'scaffold then hand off to an agent', 'share one workspace across a supervisor', 'least-privilege agent tools', 'errors as tool data'; typical wiring `const ws = ai.workspace({ cwd, shell }); ai.agent({ model, tools: ws.tools.all(), maxTrips })`. Skip: constructing the workspace / its policy / direct methods / readonly / scope — the `use-a-workspace` topic; the agent loop / maxTrips / events themselves — the `run-ai-agent` topic of the `warlock-js-ai` skill.
11
- - [use-a-workspace](@warlock.js/ai-workspace/skills/use-a-workspace/SKILL.md): Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `ws.multiEdit`, `ws.listDir`, `ws.startShell`, `ws.shellOutput`, `ws.shellKill`, `ws.dispose`, `ws.readTodos`, `ws.writeTodos`, `WorkspacePolicyError`, `WorkspaceEditError`, `WorkspaceJobError`, `WorkspaceShellRefusalReason`, `checkShellCommand`, `denyPaths`, `denyPatterns`, `allowCommand`, `interpreter`, `shell.background`, `list.ignore`, `inheritEnv`, `expectHash`, `read.maxBytes`, `bytesCapped`, `cancelled`, `ExecOptions`, `tokenizeCommand`, `read_file`/`edit_file`/`multi_edit`/`write_file`/`list_dir`/`run_shell`/`shell_output`/`shell_kill`/`run_tests`/`grep`/`todo_write`/`todo_read`; 'give an agent file tools', 'jail an agent to a directory', 'let an agent read/write/grep a repo', 'read-only workspace for a reviewer', 'scope an agent to a subdirectory', 'run a shell command under policy', 'edit a file with a stale-hash guard', 'command injection guard for an agent shell tool', 'ReDoS-safe grep for an agent', 'cap how many bytes a read returns', 'cancel a running workspace command', 'let the agent use pipes and &&', 'why was my shell command refused', 'deny git push --force', 'run a dev server in the background', 'atomic multi-edit', 'list a directory for an agent', 'todo list tool for an agent'; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — the `build-loop-agent` topic; defining a non-filesystem custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.
11
+ - [use-a-workspace](@warlock.js/ai-workspace/skills/use-a-workspace/SKILL.md): Build and operate a policy-jailed filesystem + shell workspace with @warlock.js/ai-workspace. Triggers: `ai.workspace`, `workspace(`, `WorkspacePolicy`, `WorkspaceShellPolicy`, `WorkspaceToolName`, `ws.tools.all`, `ws.tools.pick`, `ws.readFile`, `ws.writeFile`, `ws.editFile`, `ws.exec`, `ws.grep`, `ws.glob`, `ws.readonly`, `ws.scope`, `ws.multiEdit`, `ws.applyPatch`, `ws.listDir`, `ws.startShell`, `ws.shellOutput`, `ws.shellKill`, `ws.dispose`, `ws.readTodos`, `ws.writeTodos`, `WorkspacePolicyError`, `WorkspaceEditError`, `WorkspaceJobError`, `WorkspaceShellRefusalReason`, `checkShellCommand`, `denyPaths`, `denyPatterns`, `allowCommand`, `interpreter`, `shell.background`, `list.ignore`, `inheritEnv`, `expectHash`, `read.maxBytes`, `bytesCapped`, `cancelled`, `ExecOptions`, `tokenizeCommand`, `read_file`/`edit_file`/`multi_edit`/`apply_patch`/`write_file`/`list_dir`/`run_shell`/`shell_output`/`shell_kill`/`run_tests`/`grep`/`todo_write`/`todo_read`; 'give an agent file tools', 'jail an agent to a directory', 'let an agent read/write/grep a repo', 'read-only workspace for a reviewer', 'scope an agent to a subdirectory', 'run a shell command under policy', 'edit a file with a stale-hash guard', 'command injection guard for an agent shell tool', 'ReDoS-safe grep for an agent', 'cap how many bytes a read returns', 'cancel a running workspace command', 'let the agent use pipes and &&', 'why was my shell command refused', 'deny git push --force', 'run a dev server in the background', 'atomic multi-edit', 'list a directory for an agent', 'todo list tool for an agent'; typical import `import "@warlock.js/ai-workspace"; import { ai } from "@warlock.js/ai"`. Skip: building the agent loop that consumes the tools — the `build-loop-agent` topic; defining a non-filesystem custom tool — the `define-ai-tool` topic of the `warlock-js-ai` skill.
package/package.json CHANGED
@@ -19,13 +19,13 @@
19
19
  "directory": "packages/ai-workspace"
20
20
  },
21
21
  "peerDependencies": {
22
- "@warlock.js/ai": "5.39.2"
22
+ "@warlock.js/ai": "5.40.0"
23
23
  },
24
24
  "dependencies": {
25
25
  "@standard-schema/spec": "^1.0.0",
26
- "@warlock.js/fs": "5.39.2"
26
+ "@warlock.js/fs": "5.40.0"
27
27
  },
28
- "version": "5.39.2",
28
+ "version": "5.40.0",
29
29
  "main": "./cjs/index.cjs",
30
30
  "module": "./esm/index.mjs",
31
31
  "types": "./esm/index.d.mts",
@@ -19,7 +19,7 @@ const ws = ai.workspace({
19
19
 
20
20
  const dev = ai.agent({
21
21
  model,
22
- tools: ws.tools.all(), // read/edit/multi_edit/write, list_dir, run_shell, run_tests, grep, glob, todo_write/todo_read
22
+ tools: ws.tools.all(), // read/edit/multi_edit/apply_patch/write, list_dir, run_shell, run_tests, grep, glob, todo_write/todo_read
23
23
  maxTrips: 25, // give the loop room to iterate
24
24
  });
25
25
 
package/skills/index.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: 'Policy-confined file and shell tools for coding agents: `workspace`, `createLocalBackend`, `createMockBackend`, `makeEditFileTool`, `makeRunShellTool` and `WorkspacePolicyError`. Use for "let an agent edit files", "sandbox shell commands" or "loop until tests pass". Not this package → @warlock.js/ai-tools for MCP/web tools; @warlock.js/ai for agents; @warlock.js/ai-cli for installed coding CLIs.'
2
+ description: 'Policy-confined file and shell tools for coding agents: `workspace`, `createLocalBackend`, `createMockBackend`, `makeEditFileTool`, `makeReadFileTool`, `makeWriteFileTool`, `makeRunShellTool`, `makeRunTestsTool` and `WorkspacePolicyError`. Use for "let an agent edit files", "sandbox shell commands", "apply a patch" or "loop until tests pass". Not this package → @warlock.js/ai-tools for MCP/web tools; @warlock.js/ai for agents; @warlock.js/ai-cli for installed coding CLIs.'
3
3
  ---
4
4
 
5
5
  # @warlock.js/ai-workspace