@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.
- package/README.md +1 -1
- package/dist/bash-tool.adapter.js +10 -9
- package/dist/bash.adapter.d.ts +60 -8
- package/dist/bash.adapter.js +56 -13
- package/dist/file-system.adapter.d.ts +5 -1
- package/dist/file-system.adapter.js +1 -1
- package/dist/image-resizer.adapter.d.ts +5 -0
- package/dist/image-resizer.adapter.js +68 -0
- package/dist/image-resizer.entity.d.ts +23 -0
- package/dist/image-resizer.entity.functions.d.ts +15 -0
- package/dist/image-resizer.entity.functions.js +4 -0
- package/dist/image-resizer.entity.js +7 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -1
- package/dist/local-system.entity.d.ts +10 -3
- package/dist/local-system.entity.functions.d.ts +3 -2
- package/dist/local-system.entity.functions.js +16 -2
- package/dist/local-system.entity.js +3 -2
- package/dist/plugin.adapter.d.ts +103 -40
- package/dist/plugin.adapter.js +16 -5
- package/dist/read-tools.adapter.d.ts +10 -4
- package/dist/read-tools.adapter.js +40 -31
- package/dist/ripgrep.adapter.d.ts +74 -0
- package/dist/ripgrep.adapter.js +56 -0
- package/dist/ripgrep.entity.d.ts +63 -0
- package/dist/ripgrep.entity.functions.d.ts +87 -0
- package/dist/ripgrep.entity.functions.js +62 -0
- package/dist/ripgrep.entity.js +10 -0
- package/dist/shell.entity.functions.d.ts +2 -0
- package/dist/shell.entity.functions.js +1 -0
- package/dist/skills/skills-directory.adapter.d.ts +20 -14
- package/dist/skills/skills-directory.adapter.js +2 -2
- package/dist/skills/skills-directory.entity.d.ts +8 -5
- package/dist/skills/skills-directory.entity.functions.d.ts +58 -3
- package/dist/skills/skills-directory.entity.functions.js +5 -5
- package/dist/text-edit.entity.functions.js +3 -3
- package/dist/text-search.entity.functions.d.ts +7 -0
- package/dist/text-search.entity.functions.js +2 -2
- package/dist/workspace-path.entity.d.ts +21 -0
- package/dist/workspace-path.entity.functions.d.ts +39 -9
- package/dist/workspace-path.entity.functions.js +43 -18
- package/dist/workspace-path.entity.js +3 -2
- package/dist/workspace-tools.adapter.d.ts +48 -3
- package/dist/workspace-tools.adapter.js +23 -9
- package/dist/workspace-tools.entity.d.ts +136 -17
- package/dist/workspace-tools.entity.functions.d.ts +80 -5
- package/dist/workspace-tools.entity.functions.js +87 -31
- package/dist/workspace-tools.entity.js +26 -5
- package/dist/write-tools.adapter.js +10 -8
- package/package.json +6 -5
- package/dist/bash.adapter.test.d.ts +0 -1
- package/dist/file-system.adapter.test.d.ts +0 -1
- package/dist/local-system.entity.functions.test.d.ts +0 -1
- package/dist/plugin.adapter.test.d.ts +0 -1
- package/dist/skills/skills-directory.adapter.test.d.ts +0 -1
- package/dist/skills/skills-directory.entity.functions.test.d.ts +0 -1
- package/dist/text-edit.entity.functions.test.d.ts +0 -1
- package/dist/text-search.entity.functions.test.d.ts +0 -1
- package/dist/text-window.entity.functions.test.d.ts +0 -1
- package/dist/workspace-path.entity.functions.test.d.ts +0 -1
- package/dist/workspace-tools.adapter.test.d.ts +0 -1
- package/dist/workspace-tools.entity.functions.test.d.ts +0 -1
- 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
|
-
|
|
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
|
-
|
|
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]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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.
|
|
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(
|
|
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(
|
|
25
|
-
onFailure: (
|
|
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)
|
|
29
|
-
const lexical = access.path.resolve(access.root, input.length === 0 ? "." : input), display = displayOf(access, lexical), inside = (path) => isWithin(
|
|
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
|
|
33
|
-
if (options.confine && !
|
|
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 }), (
|
|
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
|
|
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:
|
|
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 {
|
|
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 =
|
|
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" }),
|
|
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: "
|
|
44
|
+
version: "3",
|
|
32
45
|
tier: "session",
|
|
33
46
|
order: config.sectionOrder,
|
|
34
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
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
|
|
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`:
|
|
38
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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,
|
|
243
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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: {
|