@xandreed/plugin-workspace 0.14.0 → 0.15.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 (63) hide show
  1. package/README.md +1 -1
  2. package/dist/bash-tool.adapter.js +10 -9
  3. package/dist/bash.adapter.d.ts +60 -8
  4. package/dist/bash.adapter.js +56 -13
  5. package/dist/file-system.adapter.d.ts +5 -1
  6. package/dist/file-system.adapter.js +1 -1
  7. package/dist/image-resizer.adapter.d.ts +5 -0
  8. package/dist/image-resizer.adapter.js +68 -0
  9. package/dist/image-resizer.entity.d.ts +23 -0
  10. package/dist/image-resizer.entity.functions.d.ts +15 -0
  11. package/dist/image-resizer.entity.functions.js +4 -0
  12. package/dist/image-resizer.entity.js +7 -0
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +4 -1
  15. package/dist/local-system.entity.d.ts +10 -3
  16. package/dist/local-system.entity.functions.d.ts +3 -2
  17. package/dist/local-system.entity.functions.js +16 -2
  18. package/dist/local-system.entity.js +3 -2
  19. package/dist/plugin.adapter.d.ts +103 -40
  20. package/dist/plugin.adapter.js +16 -5
  21. package/dist/read-tools.adapter.d.ts +10 -4
  22. package/dist/read-tools.adapter.js +40 -31
  23. package/dist/ripgrep.adapter.d.ts +74 -0
  24. package/dist/ripgrep.adapter.js +56 -0
  25. package/dist/ripgrep.entity.d.ts +63 -0
  26. package/dist/ripgrep.entity.functions.d.ts +87 -0
  27. package/dist/ripgrep.entity.functions.js +62 -0
  28. package/dist/ripgrep.entity.js +10 -0
  29. package/dist/shell.entity.functions.d.ts +2 -0
  30. package/dist/shell.entity.functions.js +1 -0
  31. package/dist/skills/skills-directory.adapter.d.ts +20 -14
  32. package/dist/skills/skills-directory.adapter.js +2 -2
  33. package/dist/skills/skills-directory.entity.d.ts +8 -5
  34. package/dist/skills/skills-directory.entity.functions.d.ts +58 -3
  35. package/dist/skills/skills-directory.entity.functions.js +5 -5
  36. package/dist/text-edit.entity.functions.js +3 -3
  37. package/dist/text-search.entity.functions.d.ts +7 -0
  38. package/dist/text-search.entity.functions.js +2 -2
  39. package/dist/workspace-path.entity.d.ts +21 -0
  40. package/dist/workspace-path.entity.functions.d.ts +39 -9
  41. package/dist/workspace-path.entity.functions.js +43 -18
  42. package/dist/workspace-path.entity.js +3 -2
  43. package/dist/workspace-tools.adapter.d.ts +48 -3
  44. package/dist/workspace-tools.adapter.js +23 -9
  45. package/dist/workspace-tools.entity.d.ts +136 -17
  46. package/dist/workspace-tools.entity.functions.d.ts +80 -5
  47. package/dist/workspace-tools.entity.functions.js +87 -31
  48. package/dist/workspace-tools.entity.js +26 -5
  49. package/dist/write-tools.adapter.js +10 -8
  50. package/package.json +6 -5
  51. package/dist/bash.adapter.test.d.ts +0 -1
  52. package/dist/file-system.adapter.test.d.ts +0 -1
  53. package/dist/local-system.entity.functions.test.d.ts +0 -1
  54. package/dist/plugin.adapter.test.d.ts +0 -1
  55. package/dist/skills/skills-directory.adapter.test.d.ts +0 -1
  56. package/dist/skills/skills-directory.entity.functions.test.d.ts +0 -1
  57. package/dist/text-edit.entity.functions.test.d.ts +0 -1
  58. package/dist/text-search.entity.functions.test.d.ts +0 -1
  59. package/dist/text-window.entity.functions.test.d.ts +0 -1
  60. package/dist/workspace-path.entity.functions.test.d.ts +0 -1
  61. package/dist/workspace-tools.adapter.test.d.ts +0 -1
  62. package/dist/workspace-tools.entity.functions.test.d.ts +0 -1
  63. package/dist/workspace-tools.journey.test.d.ts +0 -1
@@ -2,38 +2,63 @@ import { Effect, Option, Result } from "effect";
2
2
  import { compileGlob, FileSystem, FsError, SessionEnvironment } from "@xandreed/core";
3
3
  const failure = (error, message) => ({ error, message }), reasons = {
4
4
  NotFound: (display) => failure("NotFound", `${display} does not exist`),
5
- IsDirectory: (display) => failure("IsDirectory", `${display} is a directory`),
5
+ Exists: (display) => failure("AlreadyExists", `${display} already exists, and the file system would not replace it; give another path`),
6
+ IsDirectory: (display) => failure("IsDirectory", `${display} is a directory; give the path of a file`),
6
7
  NotDirectory: (display) => failure("NotDirectory", `${display} is not a directory, or a part of the path before it is a file`),
7
- Permission: (display) => failure("Permission", `permission denied: ${display}`)
8
+ NotEmpty: (display) => failure("DirectoryNotEmpty", `${display} is a directory with entries in it; give another path`),
9
+ Permission: (display) => failure("AccessDenied", `permission denied: ${display}`),
10
+ Invalid: (display) => failure("InvalidPath", `${display} is not a path the file system accepts; check it and try again`),
11
+ Io: (display) => failure("FileSystemError", `${display} could not be read or written (a file-system error); try again, or give another path`)
8
12
  };
9
- export const fsFailure = (display) => (error) => reasons[error.reason]?.(display) ?? failure(error.reason, `${display}: ${error.message}`), isWithin = (root, path) => path === root || path.startsWith(root.endsWith("/") ? root : `${root}/`), openWorkspace = (path) => Effect.gen(function* () {
10
- const fs = yield* FileSystem, { workspace } = yield* SessionEnvironment, lexicalRoot = path.resolve(workspace), root = yield* fs.realPath(lexicalRoot).pipe(Effect.mapError((error) => failure("WorkspaceMissing", `the workspace directory cannot be opened (${error.reason}): ${error.message}`)));
11
- return { fs, path, root, lexicalRoot };
12
- }), displayOf = (access, absolute) => {
13
+ export const fsFailure = (display) => (error) => reasons[error.reason](display);
14
+ const unopenable = {
15
+ NotFound: "does not exist",
16
+ NotDirectory: "is not a directory",
17
+ Permission: "cannot be opened for lack of permission",
18
+ Invalid: "is not a path the file system accepts",
19
+ Exists: "cannot be opened because of a file-system error",
20
+ IsDirectory: "cannot be opened because of a file-system error",
21
+ NotEmpty: "cannot be opened because of a file-system error",
22
+ Io: "cannot be opened because of a file-system error"
23
+ };
24
+ export const workspaceMissing = (reason) => failure("WorkspaceMissing", `the workspace directory ${unopenable[reason]}, so no workspace tool can run in this session; tell the user`), isWithin = (root, path) => path === root || path.startsWith(root.endsWith("/") ? root : `${root}/`), openWorkspace = Effect.fnUntraced(function* (path, options = { roots: [], workspaceAccess: "write" }) {
25
+ const fs = yield* FileSystem, { workspace } = yield* SessionEnvironment, lexicalRoot = path.resolve(workspace), root = yield* fs.realPath(lexicalRoot).pipe(Effect.mapError((error) => workspaceMissing(error.reason))), extra = yield* Effect.forEach(options.roots, (allowed) => {
26
+ const lexicalRoot = path.resolve(root, allowed.path);
27
+ return fs.realPath(lexicalRoot).pipe(Effect.flatMap((root) => fs.stat(root).pipe(Effect.flatMap((info) => info.kind === "directory" ? Effect.succeed(Option.some({ root, lexicalRoot, access: allowed.access })) : Effect.logWarning("an extra workspace root is not a directory", { path: allowed.path }).pipe(Effect.as(Option.none()))))), Effect.catch((error) => Effect.logWarning("an extra workspace root could not be opened", { path: allowed.path, reason: error.reason }).pipe(Effect.as(Option.none()))));
28
+ }, { concurrency: 1 });
29
+ return { fs, path, root, lexicalRoot, roots: [{ root, lexicalRoot, access: options.workspaceAccess }, ...extra.flatMap(Option.toArray)] };
30
+ }), withinAnyRoot = (access, path) => access.roots.some((root) => isWithin(root.root, path));
31
+ const folded = (path) => path.normalize("NFC").toLowerCase(), rootOf = (access, path, fold = (same) => same) => access.roots.reduce((selected, candidate) => isWithin(fold(candidate.root), fold(path)) && Option.match(selected, { onNone: () => !0, onSome: (current) => candidate.root.length >= current.root.length }) ? Option.some(candidate) : selected, Option.none()), readOnlyRootOf = (access, path) => Option.orElse(Option.filter(rootOf(access, path), (root) => root.access === "read"), () => Option.filter(rootOf(access, path, folded), (root) => root.access === "read"));
32
+ export const displayOf = (access, absolute) => {
13
33
  const base = isWithin(access.root, absolute) ? access.root : isWithin(access.lexicalRoot, absolute) ? access.lexicalRoot : void 0;
14
34
  if (base === void 0)
15
35
  return absolute;
16
36
  const relative = access.path.relative(base, absolute);
17
37
  return relative === "" ? "." : relative;
18
38
  };
19
- const canonicalOf = (access, target) => access.fs.realPath(target).pipe(Effect.catchIf((error) => error.reason === "NotFound", (missing) => {
39
+ const canonicalOf = (access, target, display) => access.fs.realPath(target).pipe(Effect.catch((error) => {
20
40
  const parent = access.path.dirname(target);
21
- if (parent === target)
22
- return Effect.fail(missing);
41
+ if (error.reason !== "NotFound" || parent === target)
42
+ return Effect.fail(fsFailure(display)(error));
23
43
  return access.fs.stat(target, { followSymlinks: !1 }).pipe(Effect.matchEffect({
24
- onSuccess: () => Effect.fail(new FsError({ path: target, reason: "Invalid", message: "it is a symbolic link to something that does not exist" })),
25
- onFailure: (error) => error.reason === "NotFound" ? canonicalOf(access, parent).pipe(Effect.map((dir) => access.path.join(dir, access.path.basename(target)))) : Effect.fail(error)
44
+ onSuccess: () => Effect.fail(failure("BrokenSymlink", `${display}: it is a symbolic link to something that does not exist`)),
45
+ onFailure: (missing) => missing.reason === "NotFound" ? canonicalOf(access, parent, display).pipe(Effect.map((dir) => access.path.join(dir, access.path.basename(target)))) : Effect.fail(fsFailure(display)(missing))
26
46
  }));
27
47
  }));
28
- export const resolveTarget = (access, input, options) => Effect.gen(function* () {
29
- const lexical = access.path.resolve(access.root, input.length === 0 ? "." : input), display = displayOf(access, lexical), inside = (path) => isWithin(access.root, path) || isWithin(access.lexicalRoot, path);
48
+ export const resolveTarget = Effect.fnUntraced(function* (access, input, options) {
49
+ const lexical = access.path.resolve(access.root, input.length === 0 ? "." : input), display = displayOf(access, lexical), inside = (path) => access.roots.some((root) => isWithin(root.root, path) || isWithin(root.lexicalRoot, path)), extras = access.roots.slice(1).map((root) => displayOf(access, root.root));
30
50
  if (options.confine && !inside(lexical))
31
- return yield* Effect.fail(failure("OutsideWorkspace", `${input} is outside the workspace; use a path under its root`));
32
- const path = yield* canonicalOf(access, lexical).pipe(Effect.mapError(fsFailure(display)));
33
- if (options.confine && !isWithin(access.root, path))
34
- return yield* Effect.fail(failure("OutsideWorkspace", `${display} leads outside the workspace through a symbolic link`));
51
+ return yield* Effect.fail(failure("OutsideWorkspace", extras.length === 0 ? `${input} is outside the workspace; use a path under its root` : `${input} is outside the workspace and its allowed roots (${extras.join(", ")}); use a path under one of them`));
52
+ const path = yield* canonicalOf(access, lexical, display);
53
+ if (options.confine && !withinAnyRoot(access, path))
54
+ return yield* Effect.fail(failure("OutsideWorkspace", extras.length === 0 ? `${display} leads outside the workspace through a symbolic link` : `${display} leads outside the workspace and its allowed roots (${extras.join(", ")}) through a symbolic link`));
55
+ const readOnly = options.confine && options.intent === "write" ? readOnlyRootOf(access, path) : Option.none();
56
+ if (Option.isSome(readOnly)) {
57
+ const place = readOnly.value.root === access.root ? "the workspace root" : displayOf(access, readOnly.value.root);
58
+ return yield* Effect.fail(failure("ReadOnly", `${display} is under ${place}, which the workspace tools may read but not change`));
59
+ }
35
60
  return { path, display };
36
- }), compileProtect = (globs) => Result.all(globs.map((glob) => Result.mapError(Result.map(compileGlob(glob, { dot: !0 }), (matcher) => ({ glob, matches: matcher.matches })), (reason) => `protect glob ${glob} is not usable: ${reason}`)));
61
+ }), compileProtect = (globs) => Result.all(globs.map((glob) => Result.mapError(Result.flatMap(compileGlob(glob, { dot: !0 }), (exact) => Result.map(compileGlob(folded(glob), { dot: !0 }), (caseless) => ({ glob, matches: (relative) => exact.matches(relative) || caseless.matches(folded(relative)) }))), (reason) => `protect glob ${glob} is not usable: ${reason}`)));
37
62
  const segmentsOf = (glob) => glob.split("/").filter((segment) => segment.length > 0 && segment !== "."), ownBraces = (segment) => (segment.match(/\{/g) ?? []).length === (segment.match(/\}/g) ?? []).length, remainderBelow = (pattern, names) => {
38
63
  const [head, ...rest] = pattern, [name, ...below] = names;
39
64
  if (name === void 0)
@@ -1,8 +1,9 @@
1
1
  import { Schema } from "effect";
2
- export const WorkspaceTarget = Schema.Struct({
2
+ export const WorkspaceAccessMode = Schema.Literals(["read", "write"]), WorkspaceAllowedRoot = Schema.Struct({ path: Schema.NonEmptyString, access: WorkspaceAccessMode }), AllowedRoot = Schema.Struct({ root: Schema.String, lexicalRoot: Schema.String, access: WorkspaceAccessMode }), WorkspaceTarget = Schema.Struct({
3
3
  path: Schema.String,
4
4
  display: Schema.String
5
5
  }), WorkspaceRoot = Schema.Struct({
6
6
  root: Schema.String,
7
- lexicalRoot: Schema.String
7
+ lexicalRoot: Schema.String,
8
+ roots: Schema.Array(AllowedRoot)
8
9
  });
@@ -2,14 +2,14 @@ import { Effect } from "effect";
2
2
  import type { Scope } from "effect";
3
3
  import { HarnessError } from "@xandreed/core";
4
4
  import type { Capability } from "@xandreed/core";
5
- import type { WorkspaceToolsConfig } from "./workspace-tools.entity.js";
6
5
  /**
7
6
  * The workspace tools as one capability, for the workspace in each call's
8
7
  * `SessionEnvironment`, over each call's `FileSystem` and `Bash`: `read`,
9
8
  * `ls`, `find` and `grep` (skill `workspace.read`, read-only), `write` and
10
9
  * `edit` (`workspace.edit`, needing `fs.write`) and `bash`
11
10
  * (`workspace.shell`, needing `shell.exec`), plus the
12
- * `workspace.environment` prompt section. Built once (in a scope, which
11
+ * `workspace.environment` prompt section, which reads the same services as
12
+ * the tools, where the turn renders it. Built once (in a scope, which
13
13
  * holds the per-file write locks): `bash` and its skill are left out, with
14
14
  * a warning, when there is no `Bash` service here; `protect` and `exclude`
15
15
  * globs that do not compile fail with `config.options` (through the plugin,
@@ -17,5 +17,50 @@ import type { WorkspaceToolsConfig } from "./workspace-tools.entity.js";
17
17
  * decoded, so a graph refuses them before activating anything). Tools are never
18
18
  * given guidance as skill instructions: it is in their descriptions and the
19
19
  * section, since an always-on skill's instructions never reach the model.
20
+ * Ripgrep requires FileSystem and Bash to see the same file tree, so
21
+ * `grepEngine: "auto"` uses it only when both, here, are this machine's
22
+ * (`localFileSystem` and a `makeBash` shell, as `localSystemPlugin`
23
+ * provides); with any other pair (an in-memory file system, a container's
24
+ * shell, a wrapper) it selects builtin without probing. A host whose pair
25
+ * shares a tree selects `ripgrep` itself.
26
+ *
27
+ * @public
20
28
  */
21
- export declare const workspaceTools: (config: WorkspaceToolsConfig) => Effect.Effect<Capability, HarnessError, Scope.Scope>;
29
+ export declare const workspaceTools: (config: {
30
+ readonly tools: readonly ("find" | "write" | "read" | "grep" | "bash" | "ls" | "edit")[];
31
+ readonly always: readonly ("read" | "edit" | "shell")[];
32
+ readonly confine: boolean;
33
+ readonly workspaceAccess: "write" | "read";
34
+ readonly roots: readonly {
35
+ readonly path: string;
36
+ readonly access: "write" | "read";
37
+ }[];
38
+ readonly protect: readonly string[];
39
+ readonly exclude: readonly string[];
40
+ readonly maxLines: number;
41
+ readonly maxBytes: number;
42
+ readonly lsEntries: number;
43
+ readonly findResults: number;
44
+ readonly grepMatches: number;
45
+ readonly grepLineChars: number;
46
+ readonly grepEngine: "auto" | "ripgrep" | "builtin";
47
+ readonly grepTimeoutMs: number;
48
+ readonly grepIgnoreFiles: boolean;
49
+ readonly maxFileBytes: number;
50
+ readonly readImages: boolean;
51
+ readonly imageMaxSide: number;
52
+ readonly fuzzyEdit: boolean;
53
+ readonly bashTimeoutMs: number;
54
+ readonly bashMaxTimeoutMs: number;
55
+ readonly bashEnv: {
56
+ readonly [x: string]: string;
57
+ };
58
+ readonly bashInheritEnv: boolean;
59
+ readonly bashEnvAllow: readonly string[];
60
+ readonly bashSessionEnv: boolean;
61
+ readonly spillDir: string | null;
62
+ readonly spillKeep: number;
63
+ readonly progressMs: number;
64
+ readonly environmentSection: boolean;
65
+ readonly sectionOrder: number;
66
+ }) => Effect.Effect<Capability, HarnessError, Scope.Scope>;
@@ -1,10 +1,12 @@
1
1
  import { Effect, Option, Path, RcMap, Result, Semaphore } from "effect";
2
- import { Bash, compileGlob, defineCapability, defineSkill, HarnessError, SessionEnvironment } from "@xandreed/core";
2
+ import { Bash, compileGlob, defineCapability, definePromptSection, defineSkill, FileSystem, HarnessError, ImageResizer, SessionEnvironment } from "@xandreed/core";
3
+ import { runsOnThisMachine } from "./bash.adapter.js";
3
4
  import { bashTool } from "./bash-tool.adapter.js";
4
- import { findTool, grepTool, lsTool, readTool } from "./read-tools.adapter.js";
5
+ import { localFileSystem } from "./file-system.adapter.js";
6
+ import { findTool, grepTool, lsTool, readTool, WORKSPACE_SERVICES } from "./read-tools.adapter.js";
5
7
  import { compileProtect, openWorkspace } from "./workspace-path.entity.functions.js";
6
8
  import { WORKSPACE_PERMISSIONS } from "./workspace-tools.entity.js";
7
- import { environmentText, skillOfTool } from "./workspace-tools.entity.functions.js";
9
+ import { bashEnvironment, environmentText, skillOfTool } from "./workspace-tools.entity.functions.js";
8
10
  import { editTool, writeTool } from "./write-tools.adapter.js";
9
11
  const ORDER = ["read", "ls", "find", "grep", "write", "edit", "bash"], builders = {
10
12
  read: readTool,
@@ -19,20 +21,32 @@ const ORDER = ["read", "ls", "find", "grep", "write", "edit", "bash"], builders
19
21
  edit: { id: "workspace.edit", summary: "Create and change files in the workspace (write, edit).", permissions: [WORKSPACE_PERMISSIONS.write] },
20
22
  shell: { id: "workspace.shell", summary: "Run shell commands in the workspace (bash).", permissions: [WORKSPACE_PERMISSIONS.exec] }
21
23
  }, invalid = (message) => new HarnessError({ code: "config.options", plugin: "@xandreed/plugin-workspace/tools", message });
22
- export const workspaceTools = (config) => Effect.gen(function* () {
24
+ export const workspaceTools = Effect.fnUntraced(function* (config) {
23
25
  const path = yield* Effect.service(Path.Path).pipe(Effect.provide(Path.layer)), protect = yield* Effect.fromResult(compileProtect(config.protect)).pipe(Effect.mapError(invalid)), badExclude = config.exclude.find((glob) => Result.isFailure(compileGlob(glob, { dot: !0 })));
24
26
  if (badExclude !== void 0)
25
27
  return yield* Effect.fail(invalid(`exclude glob ${badExclude} is not usable`));
26
- const locks = yield* RcMap.make({ lookup: (_path) => Semaphore.make(1), idleTimeToLive: "1 minute" }), hasBash = Option.isSome(yield* Effect.serviceOption(Bash)), wanted = ORDER.filter((tool) => config.tools.includes(tool));
28
+ const locks = yield* RcMap.make({ lookup: (_path) => Semaphore.make(1), idleTimeToLive: "1 minute" }), bash = yield* Effect.serviceOption(Bash), hasBash = Option.isSome(bash), sameTree = Option.exists(yield* Effect.serviceOption(FileSystem), (service) => service === localFileSystem) && Option.exists(bash, runsOnThisMachine);
29
+ if (config.tools.includes("grep") && config.grepEngine === "auto" && !sameTree)
30
+ yield* Effect.logDebug("grep: the FileSystem and Bash are not this machine's local pair; using the builtin engine");
31
+ const probe = config.tools.includes("grep") && (config.grepEngine === "ripgrep" || config.grepEngine === "auto" && sameTree) ? yield* Option.match(bash, {
32
+ onNone: () => Effect.succeed(!1),
33
+ onSome: (service) => service.exec("rg --version", { cwd: "/", timeoutMs: 5000, ...bashEnvironment(config, Option.none()) }).pipe(Effect.map((result) => result.termination === "exit" && Option.contains(result.exitCode, 0)), Effect.catch((error) => Effect.logDebug("grep: ripgrep probe failed", error).pipe(Effect.as(!1))))
34
+ }) : !1;
35
+ if (config.tools.includes("grep") && config.grepEngine === "ripgrep" && !probe)
36
+ return yield* Effect.fail(invalid("grepEngine ripgrep requires rg on the Bash service PATH"));
37
+ if (config.tools.includes("grep") && config.grepEngine === "auto" && sameTree && !probe)
38
+ yield* Effect.logDebug("grep: ripgrep is unavailable; using the builtin engine");
39
+ const resolved = { ...config, grepEngine: probe ? "ripgrep" : "builtin" }, wanted = ORDER.filter((tool) => config.tools.includes(tool));
27
40
  if (wanted.includes("bash") && !hasBash)
28
41
  yield* Effect.logWarning("@xandreed/plugin-workspace/tools: there is no Bash service, so the bash tool and the workspace.shell skill are left out (add localSystemPlugin, or a Bash, to have them)");
29
- const enabled = wanted.filter((tool) => tool !== "bash" || hasBash), context = { config, path, protect, locks }, section = {
42
+ const enabled = wanted.filter((tool) => tool !== "bash" || hasBash), context = { config: resolved, path, protect, locks, resizesImages: config.readImages && Option.isSome(yield* Effect.serviceOption(ImageResizer)) }, section = definePromptSection({
30
43
  id: "workspace.environment",
31
- version: "1",
44
+ version: "3",
32
45
  tier: "session",
33
46
  order: config.sectionOrder,
34
- render: () => openWorkspace(path).pipe(Effect.map((access) => access.root), Effect.catch(() => SessionEnvironment.pipe(Effect.map((environment) => path.resolve(environment.workspace)))), Effect.map((root) => Option.some(environmentText(root, config, enabled))))
35
- };
47
+ requires: WORKSPACE_SERVICES,
48
+ render: () => openWorkspace(path).pipe(Effect.map((access) => access.root), Effect.catch(() => SessionEnvironment.pipe(Effect.map((environment) => path.resolve(environment.workspace)))), Effect.map((root) => Option.some(environmentText(root, resolved, enabled))))
49
+ });
36
50
  return defineCapability({
37
51
  id: "@xandreed/plugin-workspace/tools",
38
52
  version: "1",
@@ -1,31 +1,47 @@
1
1
  import { Schema } from "effect";
2
- /** The seven workspace tools, by the names the model calls them. */
2
+ /**
3
+ * The seven workspace tools, by the names the model calls them.
4
+ *
5
+ * @public
6
+ */
3
7
  export declare const WorkspaceToolName: Schema.Literals<readonly ["read", "write", "edit", "ls", "find", "grep", "bash"]>;
4
8
  export type WorkspaceToolName = typeof WorkspaceToolName.Type;
5
9
  /**
6
10
  * The tools' three skills, by the short names `always` takes: `read`
7
11
  * (`workspace.read`: read, ls, find, grep), `edit` (`workspace.edit`: write,
8
12
  * edit) and `shell` (`workspace.shell`: bash).
13
+ *
14
+ * @public
9
15
  */
10
16
  export declare const WorkspaceSkillName: Schema.Literals<readonly ["read", "edit", "shell"]>;
11
17
  export type WorkspaceSkillName = typeof WorkspaceSkillName.Type;
12
- /** The grants the tools that change things need: `write` and `edit` need `fs.write`, `bash` needs `shell.exec`. */
18
+ /**
19
+ * The grants the tools that change things need: `write` and `edit` need `fs.write`, `bash` needs `shell.exec`.
20
+ *
21
+ * @public
22
+ */
13
23
  export declare const WORKSPACE_PERMISSIONS: {
14
24
  readonly write: "fs.write";
15
25
  readonly exec: "shell.exec";
16
26
  };
17
27
  /**
18
28
  * How `workspaceToolsPlugin` (and `workspaceTools`) works on the workspace,
19
- * `SessionEnvironment.workspace`. Flat: plugin defaults merge one level deep.
29
+ * `SessionEnvironment.workspace`. Flat.
20
30
  *
21
31
  * - `tools`: the tools to register (all seven by default). A skill whose
22
32
  * tools are all left out is left out too.
23
33
  * - `always`: the skills active from a turn's first step: `read` by default.
24
34
  * `edit` and `shell` here need their grants (`fs.write`, `shell.exec`), or
25
35
  * the turn fails with `capability.forbidden`; otherwise the model loads
26
- * them with `load_skill`, which refuses them without the grants.
36
+ * them with `load_skill`, which refuses them without the grants
37
+ * (`PermissionDenied`).
27
38
  * - `confine`: refuse paths whose real location (links resolved, through the
28
- * nearest ancestor that exists) is outside the workspace root.
39
+ * nearest ancestor that exists) is outside the workspace and extra roots.
40
+ * - `workspaceAccess`: `read` refuses writes in the workspace; `write`
41
+ * permits them. `roots` adds readable or writable directories, absolute
42
+ * or relative to the session's workspace. The longest matching real root
43
+ * decides access. These restrictions apply with `confine`; bash remains
44
+ * unconfined. An extra root that cannot be resolved is skipped and logged.
29
45
  * - `protect`: globs, relative to the root, that `write` and `edit` refuse.
30
46
  * - `exclude`: names (or, with a `/`, paths relative to the root) `find`
31
47
  * and `grep` never enter below the directory they search. Both lists are
@@ -34,8 +50,13 @@ export declare const WORKSPACE_PERMISSIONS: {
34
50
  * and the tail of a command's output `bash` keeps.
35
51
  * - `lsEntries`, `findResults`, `grepMatches`: the most entries, paths and
36
52
  * matching lines the model gets (a call may ask for fewer).
37
- * - `grepLineChars`: lines are cut to this many characters before `grep`
38
- * matches them, and shown cut.
53
+ * - `grepLineChars`: the displayed line width; builtin also cuts before matching.
54
+ * - `grepEngine`: auto probes for rg once over this machine's pair
55
+ * (`localSystemPlugin`'s FileSystem and Bash), falling back to builtin,
56
+ * and selects builtin over any other pair; ripgrep requires rg over
57
+ * whatever pair the host gives. `grepTimeoutMs` bounds rg commands; the synchronous builtin
58
+ * engine is not time-bounded. `grepIgnoreFiles` honours ignore files in rg
59
+ * only (false searches ignored files too).
39
60
  * - `maxFileBytes`: the largest file `read`, `edit` and `grep` load, and
40
61
  * the most of a command's output `bash` saves.
41
62
  * - `readImages`: `read` returns a PNG, JPEG, GIF or WebP image (by its
@@ -45,6 +66,9 @@ export declare const WORKSPACE_PERMISSIONS: {
45
66
  * `images`, or memory with `maxMediaChars: 0`) sets `false`: the model
46
67
  * would get a label line in the image's place, and the session would
47
68
  * still store the bytes.
69
+ * - `imageMaxSide`: with an ImageResizer, fit images to at most this many
70
+ * pixels a side (64 to 8000, default 8000). Without a resizer, read keeps
71
+ * the existing provider-limit behavior.
48
72
  * - `fuzzyEdit`: when an edit's text is not found, match it once more over
49
73
  * whole lines with whitespace, Unicode compatibility forms and smart
50
74
  * quotes normalised (the match must still be unique).
@@ -62,11 +86,18 @@ export declare const WORKSPACE_PERMISSIONS: {
62
86
  * one `tool.progress` event per run of output from one stream.
63
87
  * - `environmentSection`, `sectionOrder`: the `workspace.environment`
64
88
  * system-prompt section (tier `session`) and its order.
89
+ *
90
+ * @public
65
91
  */
66
92
  export declare const WorkspaceToolsConfig: Schema.Struct<{
67
93
  readonly tools: Schema.$Array<Schema.Literals<readonly ["read", "write", "edit", "ls", "find", "grep", "bash"]>>;
68
94
  readonly always: Schema.$Array<Schema.Literals<readonly ["read", "edit", "shell"]>>;
69
95
  readonly confine: Schema.Boolean;
96
+ readonly workspaceAccess: Schema.Literals<readonly ["read", "write"]>;
97
+ readonly roots: Schema.$Array<Schema.Struct<{
98
+ readonly path: Schema.NonEmptyString;
99
+ readonly access: Schema.Literals<readonly ["read", "write"]>;
100
+ }>>;
70
101
  readonly protect: Schema.$Array<Schema.String>;
71
102
  readonly exclude: Schema.$Array<Schema.String>;
72
103
  readonly maxLines: Schema.Int;
@@ -75,8 +106,12 @@ export declare const WorkspaceToolsConfig: Schema.Struct<{
75
106
  readonly findResults: Schema.Int;
76
107
  readonly grepMatches: Schema.Int;
77
108
  readonly grepLineChars: Schema.Int;
109
+ readonly grepEngine: Schema.Literals<readonly ["auto", "ripgrep", "builtin"]>;
110
+ readonly grepTimeoutMs: Schema.Int;
111
+ readonly grepIgnoreFiles: Schema.Boolean;
78
112
  readonly maxFileBytes: Schema.Int;
79
113
  readonly readImages: Schema.Boolean;
114
+ readonly imageMaxSide: Schema.Int;
80
115
  readonly fuzzyEdit: Schema.Boolean;
81
116
  readonly bashTimeoutMs: Schema.Int;
82
117
  readonly bashMaxTimeoutMs: Schema.Int;
@@ -91,8 +126,13 @@ export declare const WorkspaceToolsConfig: Schema.Struct<{
91
126
  readonly sectionOrder: Schema.Int;
92
127
  }>;
93
128
  export type WorkspaceToolsConfig = typeof WorkspaceToolsConfig.Type;
129
+ /** @public */
94
130
  export declare const workspaceToolsDefaults: WorkspaceToolsConfig;
95
- /** `read`: a file's lines from `offset` (1-based, default 1), at most `limit` of them (and never more than the configured window). */
131
+ /**
132
+ * `read`: a file's lines from `offset` (1-based, default 1), at most `limit` of them (and never more than the configured window).
133
+ *
134
+ * @public
135
+ */
96
136
  export declare const ReadParams: Schema.Struct<{
97
137
  readonly path: Schema.String;
98
138
  readonly offset: Schema.optionalKey<Schema.Int>;
@@ -112,6 +152,8 @@ export type ReadParams = typeof ReadParams.Type;
112
152
  * over `MEDIA_LIMITS.maxPixels` a side, or a header without a size) is an
113
153
  * `ImageRejected` note: its type, size and why. Any other binary file is
114
154
  * `Binary`, with the media type its name suggests.
155
+ *
156
+ * @public
115
157
  */
116
158
  export declare const ReadResult: Schema.Union<readonly [Schema.TaggedStruct<"Text", {
117
159
  readonly path: Schema.String;
@@ -137,6 +179,13 @@ export declare const ReadResult: Schema.Union<readonly [Schema.TaggedStruct<"Tex
137
179
  readonly name: Schema.optionalKey<Schema.String>;
138
180
  readonly alt: Schema.optionalKey<Schema.String>;
139
181
  }>;
182
+ /** Source dimensions and size when a resizer supplied the returned image; no original image bytes. */
183
+ readonly original: Schema.optionalKey<Schema.Struct<{
184
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
185
+ readonly bytes: Schema.Int;
186
+ readonly width: Schema.Int;
187
+ readonly height: Schema.Int;
188
+ }>>;
140
189
  }>, Schema.TaggedStruct<"ImageRejected", {
141
190
  readonly path: Schema.String;
142
191
  readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
@@ -145,24 +194,34 @@ export declare const ReadResult: Schema.Union<readonly [Schema.TaggedStruct<"Tex
145
194
  readonly message: Schema.String;
146
195
  }>]>;
147
196
  export type ReadResult = typeof ReadResult.Type;
197
+ /** @public */
148
198
  export declare const WriteParams: Schema.Struct<{
149
199
  readonly path: Schema.String;
150
200
  readonly content: Schema.String;
151
201
  }>;
152
202
  export type WriteParams = typeof WriteParams.Type;
153
- /** What `write` did: how many bytes it wrote, and whether the file is new. */
203
+ /**
204
+ * What `write` did: how many bytes it wrote, and whether the file is new.
205
+ *
206
+ * @public
207
+ */
154
208
  export declare const WriteResult: Schema.Struct<{
155
209
  readonly path: Schema.String;
156
210
  readonly bytes: Schema.Int;
157
211
  readonly created: Schema.Boolean;
158
212
  }>;
159
213
  export type WriteResult = typeof WriteResult.Type;
160
- /** One replacement: `oldText` (exactly once in the file) becomes `newText`. */
214
+ /**
215
+ * One replacement: `oldText` (exactly once in the file) becomes `newText`.
216
+ *
217
+ * @public
218
+ */
161
219
  export declare const TextEdit: Schema.Struct<{
162
220
  readonly oldText: Schema.String;
163
221
  readonly newText: Schema.String;
164
222
  }>;
165
223
  export type TextEdit = typeof TextEdit.Type;
224
+ /** @public */
166
225
  export declare const EditParams: Schema.Struct<{
167
226
  readonly path: Schema.String;
168
227
  readonly edits: Schema.NonEmptyArray<Schema.Struct<{
@@ -175,6 +234,8 @@ export type EditParams = typeof EditParams.Type;
175
234
  * What `edit` did: how many blocks it replaced, whether any was found only
176
235
  * after normalising, the first changed line, and the change as a unified
177
236
  * diff (for a host's UI; the model is shown a one-line summary).
237
+ *
238
+ * @public
178
239
  */
179
240
  export declare const EditResult: Schema.Struct<{
180
241
  readonly path: Schema.String;
@@ -184,17 +245,23 @@ export declare const EditResult: Schema.Struct<{
184
245
  readonly diff: Schema.String;
185
246
  }>;
186
247
  export type EditResult = typeof EditResult.Type;
248
+ /** @public */
187
249
  export declare const LsParams: Schema.Struct<{
188
250
  readonly path: Schema.optionalKey<Schema.String>;
189
251
  readonly limit: Schema.optionalKey<Schema.Int>;
190
252
  }>;
191
253
  export type LsParams = typeof LsParams.Type;
254
+ /** @public */
192
255
  export declare const LsEntry: Schema.Struct<{
193
256
  readonly name: Schema.String;
194
257
  readonly kind: Schema.Literals<readonly ["file", "directory", "symlink", "other"]>;
195
258
  }>;
196
259
  export type LsEntry = typeof LsEntry.Type;
197
- /** A directory's entries by name, at most the limit of them; `total` counts them all. */
260
+ /**
261
+ * A directory's entries by name, at most the limit of them; `total` counts them all.
262
+ *
263
+ * @public
264
+ */
198
265
  export declare const LsResult: Schema.Struct<{
199
266
  readonly path: Schema.String;
200
267
  readonly entries: Schema.$Array<Schema.Struct<{
@@ -204,13 +271,18 @@ export declare const LsResult: Schema.Struct<{
204
271
  readonly total: Schema.Int;
205
272
  }>;
206
273
  export type LsResult = typeof LsResult.Type;
274
+ /** @public */
207
275
  export declare const FindParams: Schema.Struct<{
208
276
  readonly pattern: Schema.String;
209
277
  readonly path: Schema.optionalKey<Schema.String>;
210
278
  readonly limit: Schema.optionalKey<Schema.Int>;
211
279
  }>;
212
280
  export type FindParams = typeof FindParams.Type;
213
- /** The files a glob matched, as paths relative to the workspace root; `truncated` when more matched. */
281
+ /**
282
+ * The files a glob matched, as paths relative to the workspace root; `truncated` when more matched.
283
+ *
284
+ * @public
285
+ */
214
286
  export declare const FindResult: Schema.Struct<{
215
287
  readonly pattern: Schema.String;
216
288
  readonly path: Schema.String;
@@ -218,6 +290,7 @@ export declare const FindResult: Schema.Struct<{
218
290
  readonly truncated: Schema.Boolean;
219
291
  }>;
220
292
  export type FindResult = typeof FindResult.Type;
293
+ /** @public */
221
294
  export declare const GrepParams: Schema.Struct<{
222
295
  readonly pattern: Schema.String;
223
296
  readonly path: Schema.optionalKey<Schema.String>;
@@ -228,7 +301,11 @@ export declare const GrepParams: Schema.Struct<{
228
301
  readonly limit: Schema.optionalKey<Schema.Int>;
229
302
  }>;
230
303
  export type GrepParams = typeof GrepParams.Type;
231
- /** One line `grep` shows: a matching line, or (`match: false`) a line of context around one. `text` is cut to the configured width. */
304
+ /**
305
+ * One line `grep` shows: a matching line, or (`match: false`) a line of context around one. `text` is cut to the configured width.
306
+ *
307
+ * @public
308
+ */
232
309
  export declare const GrepLine: Schema.Struct<{
233
310
  readonly path: Schema.String;
234
311
  readonly line: Schema.Int;
@@ -236,11 +313,26 @@ export declare const GrepLine: Schema.Struct<{
236
313
  readonly match: Schema.Boolean;
237
314
  }>;
238
315
  export type GrepLine = typeof GrepLine.Type;
316
+ /**
317
+ * A file `grep` could not read, and why (`Permission` when access was denied); its view says why in words.
318
+ *
319
+ * @public
320
+ */
321
+ export declare const GrepUnreadable: Schema.Struct<{
322
+ readonly path: Schema.String;
323
+ readonly reason: Schema.Literals<readonly ["NotFound", "Exists", "IsDirectory", "NotDirectory", "NotEmpty", "Permission", "Invalid", "Io"]>;
324
+ }>;
325
+ export type GrepUnreadable = typeof GrepUnreadable.Type;
239
326
  /**
240
327
  * What `grep` found: the matching lines (with their context) in walk order,
241
328
  * how many lines matched and in how many files, whether it stopped at the
242
- * limit, and how many files it skipped (binary, larger than `maxFileBytes`,
243
- * or resolving outside the workspace).
329
+ * limit, how many files it skipped (binary, larger than `maxFileBytes`, or
330
+ * resolving outside the workspace), and the files it could not read
331
+ * (`unreadable`, empty in results stored before it was recorded). `timedOut`
332
+ * marks an incomplete ripgrep search (false in older results). Ripgrep counts
333
+ * only binary skips it reports; files pruned by its walk/size limit are not counted.
334
+ *
335
+ * @public
244
336
  */
245
337
  export declare const GrepResult: Schema.Struct<{
246
338
  readonly pattern: Schema.String;
@@ -255,9 +347,18 @@ export declare const GrepResult: Schema.Struct<{
255
347
  readonly files: Schema.Int;
256
348
  readonly truncated: Schema.Boolean;
257
349
  readonly skipped: Schema.Int;
350
+ readonly unreadable: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Struct<{
351
+ readonly path: Schema.String;
352
+ readonly reason: Schema.Literals<readonly ["NotFound", "Exists", "IsDirectory", "NotDirectory", "NotEmpty", "Permission", "Invalid", "Io"]>;
353
+ }>>, never>;
354
+ readonly timedOut: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
258
355
  }>;
259
356
  export type GrepResult = typeof GrepResult.Type;
260
- /** `bash`: the command, its timeout in seconds, and what to write to its standard input. */
357
+ /**
358
+ * `bash`: the command, its timeout in seconds, and what to write to its standard input.
359
+ *
360
+ * @public
361
+ */
261
362
  export declare const BashParams: Schema.Struct<{
262
363
  readonly command: Schema.String;
263
364
  readonly timeout: Schema.optionalKey<Schema.Int>;
@@ -271,6 +372,8 @@ export type BashParams = typeof BashParams.Type;
271
372
  * workspace root; null when nothing was saved). The file holds at most
272
373
  * `maxFileBytes` of the output, its start: `fullOutputCut` when there was
273
374
  * more, which was not saved.
375
+ *
376
+ * @public
274
377
  */
275
378
  export declare const BashToolResult: Schema.Struct<{
276
379
  readonly command: Schema.String;
@@ -285,7 +388,11 @@ export declare const BashToolResult: Schema.Struct<{
285
388
  readonly durationMs: Schema.Number;
286
389
  }>;
287
390
  export type BashToolResult = typeof BashToolResult.Type;
288
- /** The tools' names and schemas, for `onTool` subscriptions (a UI showing `edit` diffs, say). */
391
+ /**
392
+ * The tools' names and schemas, for `onTool` subscriptions (a UI showing `edit` diffs, say).
393
+ *
394
+ * @public
395
+ */
289
396
  export declare const workspaceToolSchemas: {
290
397
  readonly read: {
291
398
  readonly name: "read";
@@ -318,6 +425,13 @@ export declare const workspaceToolSchemas: {
318
425
  readonly name: Schema.optionalKey<Schema.String>;
319
426
  readonly alt: Schema.optionalKey<Schema.String>;
320
427
  }>;
428
+ /** Source dimensions and size when a resizer supplied the returned image; no original image bytes. */
429
+ readonly original: Schema.optionalKey<Schema.Struct<{
430
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
431
+ readonly bytes: Schema.Int;
432
+ readonly width: Schema.Int;
433
+ readonly height: Schema.Int;
434
+ }>>;
321
435
  }>, Schema.TaggedStruct<"ImageRejected", {
322
436
  readonly path: Schema.String;
323
437
  readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
@@ -408,6 +522,11 @@ export declare const workspaceToolSchemas: {
408
522
  readonly files: Schema.Int;
409
523
  readonly truncated: Schema.Boolean;
410
524
  readonly skipped: Schema.Int;
525
+ readonly unreadable: Schema.withDecodingDefaultKey<Schema.$Array<Schema.Struct<{
526
+ readonly path: Schema.String;
527
+ readonly reason: Schema.Literals<readonly ["NotFound", "Exists", "IsDirectory", "NotDirectory", "NotEmpty", "Permission", "Invalid", "Io"]>;
528
+ }>>, never>;
529
+ readonly timedOut: Schema.withDecodingDefaultKey<Schema.Boolean, never>;
411
530
  }>;
412
531
  };
413
532
  readonly bash: {