@xandreed/plugin-workspace 0.0.0-stage → 0.14.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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/dist/bash-tool.adapter.d.ts +15 -0
  4. package/dist/bash-tool.adapter.js +72 -0
  5. package/dist/bash.adapter.d.ts +29 -0
  6. package/dist/bash.adapter.js +69 -0
  7. package/dist/bash.adapter.test.d.ts +1 -0
  8. package/dist/file-system.adapter.d.ts +20 -0
  9. package/dist/file-system.adapter.js +52 -0
  10. package/dist/file-system.adapter.test.d.ts +1 -0
  11. package/dist/index.d.ts +12 -0
  12. package/dist/index.js +35 -0
  13. package/dist/local-system.entity.d.ts +40 -0
  14. package/dist/local-system.entity.functions.d.ts +52 -0
  15. package/dist/local-system.entity.functions.js +32 -0
  16. package/dist/local-system.entity.functions.test.d.ts +1 -0
  17. package/dist/local-system.entity.js +11 -0
  18. package/dist/plugin.adapter.d.ts +120 -0
  19. package/dist/plugin.adapter.js +27 -0
  20. package/dist/plugin.adapter.test.d.ts +1 -0
  21. package/dist/read-tools.adapter.d.ts +33 -0
  22. package/dist/read-tools.adapter.js +100 -0
  23. package/dist/skills/skills-directory.adapter.d.ts +62 -0
  24. package/dist/skills/skills-directory.adapter.js +30 -0
  25. package/dist/skills/skills-directory.adapter.test.d.ts +1 -0
  26. package/dist/skills/skills-directory.entity.d.ts +96 -0
  27. package/dist/skills/skills-directory.entity.functions.d.ts +33 -0
  28. package/dist/skills/skills-directory.entity.functions.js +45 -0
  29. package/dist/skills/skills-directory.entity.functions.test.d.ts +1 -0
  30. package/dist/skills/skills-directory.entity.js +35 -0
  31. package/dist/text-edit.entity.d.ts +32 -0
  32. package/dist/text-edit.entity.functions.d.ts +32 -0
  33. package/dist/text-edit.entity.functions.js +68 -0
  34. package/dist/text-edit.entity.functions.test.d.ts +1 -0
  35. package/dist/text-edit.entity.js +15 -0
  36. package/dist/text-search.entity.d.ts +23 -0
  37. package/dist/text-search.entity.functions.d.ts +51 -0
  38. package/dist/text-search.entity.functions.js +30 -0
  39. package/dist/text-search.entity.functions.test.d.ts +1 -0
  40. package/dist/text-search.entity.js +10 -0
  41. package/dist/text-window.entity.d.ts +46 -0
  42. package/dist/text-window.entity.functions.d.ts +54 -0
  43. package/dist/text-window.entity.functions.js +88 -0
  44. package/dist/text-window.entity.functions.test.d.ts +1 -0
  45. package/dist/text-window.entity.js +21 -0
  46. package/dist/workspace-path.entity.d.ts +22 -0
  47. package/dist/workspace-path.entity.functions.d.ts +60 -0
  48. package/dist/workspace-path.entity.functions.js +63 -0
  49. package/dist/workspace-path.entity.functions.test.d.ts +1 -0
  50. package/dist/workspace-path.entity.js +8 -0
  51. package/dist/workspace-tools.adapter.d.ts +21 -0
  52. package/dist/workspace-tools.adapter.js +46 -0
  53. package/dist/workspace-tools.adapter.test.d.ts +1 -0
  54. package/dist/workspace-tools.entity.d.ts +433 -0
  55. package/dist/workspace-tools.entity.functions.d.ts +51 -0
  56. package/dist/workspace-tools.entity.functions.js +124 -0
  57. package/dist/workspace-tools.entity.functions.test.d.ts +1 -0
  58. package/dist/workspace-tools.entity.js +133 -0
  59. package/dist/workspace-tools.journey.test.d.ts +1 -0
  60. package/dist/write-tools.adapter.d.ts +12 -0
  61. package/dist/write-tools.adapter.js +77 -0
  62. package/package.json +29 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xand Reed
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,5 @@
1
- # Temporary Holding Version
1
+ # @xandreed/plugin-workspace
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Efferent composable agent framework, 0.14.0. Node 24 or Bun.
4
+
5
+ Documentation: https://github.com/xandreeddev/efferent
@@ -0,0 +1,15 @@
1
+ import type { RegisteredTool } from "@xandreed/core";
2
+ import type { WorkspaceToolContext } from "./read-tools.adapter.js";
3
+ /**
4
+ * `bash`: run a command in the workspace root through the `Bash` service of
5
+ * the call. Output is published as it comes (`tool.progress`: gathered for
6
+ * `progressMs`, then one event per run of output from one stream), and
7
+ * folded into a tail the model is shown: the last `maxLines` lines within
8
+ * `maxBytes`. Once the output is longer than that, it goes to a file under
9
+ * `spillDir` (named by time and call), whose path the result gives, up to
10
+ * `maxFileBytes` (what `read` opens; past that nothing more is saved, and
11
+ * the result says `fullOutputCut`); the newest `spillKeep` files are kept.
12
+ * A non-zero exit, a signal and a timeout are results; only a command that
13
+ * could not run at all is a failure.
14
+ */
15
+ export declare const bashTool: (context: WorkspaceToolContext) => RegisteredTool;
@@ -0,0 +1,72 @@
1
+ import { Tool } from "effect/ai";
2
+ import { Clock, Effect, Option, Random, Stream } from "effect";
3
+ import { Bash, CurrentToolInvocation, defineTool, Failure, publishToolProgress, RunContext } from "@xandreed/core";
4
+ import { clipHead, tailText, utf8Length } from "./text-window.entity.functions.js";
5
+ import { openWorkspace } from "./workspace-path.entity.functions.js";
6
+ import { BashParams, BashToolResult, WORKSPACE_PERMISSIONS } from "./workspace-tools.entity.js";
7
+ import { bashEnvironment, compactBash, renderBash, toolDescriptions } from "./workspace-tools.entity.functions.js";
8
+ import { failure } from "./read-tools.adapter.js";
9
+ const BashTool = (description) => Tool.make("bash", { description, parameters: BashParams, success: BashToolResult, failure: Failure, failureMode: "return" }), isChunk = (event) => event._tag === "BashChunk", isExit = (event) => event._tag === "BashExit", runsOf = (chunks) => chunks.reduce((runs, chunk) => {
10
+ const last = runs.at(-1);
11
+ if (last !== void 0 && last.stream === chunk.stream)
12
+ runs.splice(-1, 1, { stream: chunk.stream, text: last.text + chunk.text });
13
+ else
14
+ runs.push({ stream: chunk.stream, text: chunk.text });
15
+ return runs;
16
+ }, []), prune = (access, dir, keep) => access.fs.list(dir).pipe(Effect.map((entries) => entries.filter((entry) => entry.kind === "file" && /^bash-.*\.log$/.test(entry.name)).map((entry) => entry.name).sort()), Effect.flatMap((names) => Effect.forEach(names.slice(0, Math.max(0, names.length - keep)), (name) => access.fs.remove(access.path.join(dir, name)), { discard: !0 })), Effect.ignore);
17
+ export const bashTool = (context) => defineTool({
18
+ tool: BashTool(toolDescriptions(context.config).bash),
19
+ handler: (params) => Effect.gen(function* () {
20
+ const { config } = context, access = yield* openWorkspace(context.path), bash = yield* Bash, run = yield* Effect.serviceOption(RunContext), invocation = yield* Effect.service(CurrentToolInvocation), timeoutMs = Math.min(params.timeout === void 0 ? config.bashTimeoutMs : params.timeout * 1000, config.bashMaxTimeoutMs), options = {
21
+ cwd: access.root,
22
+ ...bashEnvironment(config, Option.map(run, (current) => ({ sessionId: String(current.session.id), runId: current.runId, workspace: access.root }))),
23
+ timeoutMs,
24
+ ...params.stdin === void 0 ? {} : { stdin: params.stdin }
25
+ }, spillDir = Option.map(Option.fromNullishOr(config.spillDir), (dir) => access.path.join(access.root, dir)), started = yield* Clock.currentTimeMillis, tag = yield* Option.match(invocation, {
26
+ onSome: (call) => Effect.succeed(call.invocationId.replace(/[^A-Za-z0-9._-]/g, "_")),
27
+ onNone: () => Random.nextIntBetween(0, 2147483647).pipe(Effect.map((value) => value.toString(36)))
28
+ }), spillName = `bash-${String(started).padStart(15, "0")}-${tag}.log`, fitting = (bytes, text) => {
29
+ const piece = clipHead(text, Math.max(0, config.maxFileBytes - bytes));
30
+ return { piece, bytes: bytes + utf8Length(piece), cut: piece.length < text.length };
31
+ }, startSpill = (text) => Option.match(spillDir, {
32
+ onNone: () => Effect.succeed(Option.none()),
33
+ onSome: (dir) => {
34
+ const file = access.path.join(dir, spillName), first = fitting(0, text);
35
+ return access.fs.write(file, first.piece, { createParents: !0 }).pipe(Effect.andThen(prune(access, dir, config.spillKeep)), Effect.as(Option.some({ file, bytes: first.bytes, cut: first.cut })), Effect.catch((error) => Effect.logWarning(`bash: the command's whole output could not be saved to ${file}: ${error.message}`).pipe(Effect.as(Option.none()))));
36
+ }
37
+ }), addToSpill = (spill, text) => {
38
+ if (spill.cut || text.length === 0)
39
+ return Effect.succeed(spill);
40
+ const next = fitting(spill.bytes, text);
41
+ return (next.piece.length === 0 ? Effect.void : access.fs.append(spill.file, next.piece).pipe(Effect.catch((error) => Effect.logWarning(`bash: could not add to ${spill.file}: ${error.message}`)))).pipe(Effect.as({ ...spill, bytes: next.bytes, cut: next.cut }));
42
+ }, over = (state) => state.bytes - state.crlf - 1 > config.maxBytes || state.breaks > config.maxLines, step = (state, group) => Effect.gen(function* () {
43
+ const chunks = group.filter(isChunk);
44
+ yield* Effect.forEach(runsOf(chunks), (current) => publishToolProgress(current.stream, current.text), { discard: !0 });
45
+ const text = chunks.map((chunk) => chunk.text).join(""), bytes = state.bytes + utf8Length(text), breaks = state.breaks + (text.split(`
46
+ `).length - 1), crlf = state.crlf + (text.split(`\r
47
+ `).length - 1) + (state.tail.endsWith("\r") && text.startsWith(`
48
+ `) ? 1 : 0), exit = Option.orElse(Option.fromNullishOr(group.find(isExit)), () => state.exit), base = { tail: (state.tail + text).slice(-(config.maxBytes + 2)), bytes, breaks, crlf, open: text.length === 0 ? state.open : !text.endsWith(`
49
+ `), exit };
50
+ if (Option.isSome(state.spill))
51
+ return { ...base, whole: Option.none(), spill: Option.some(yield* addToSpill(state.spill.value, text)) };
52
+ const whole = Option.map(state.whole, (sofar) => sofar + text);
53
+ if (!over(base) || Option.isNone(whole))
54
+ return { ...base, whole, spill: Option.none() };
55
+ return { ...base, whole: Option.none(), spill: yield* startSpill(whole.value) };
56
+ }), gathered = yield* bash.stream(params.command, options).pipe(Stream.groupedWithin(4096, `${config.progressMs} millis`), Stream.runFoldEffect(() => ({ tail: "", whole: Option.some(""), bytes: 0, breaks: 0, crlf: 0, open: !1, spill: Option.none(), exit: Option.none() }), step), Effect.mapError((error) => failure(error.reason, error.message))), shown = tailText(Option.getOrElse(gathered.whole, () => gathered.tail), config.maxLines, config.maxBytes), truncated = Option.isNone(gathered.whole) || shown.truncated, spill = Option.isSome(gathered.spill) || !truncated ? gathered.spill : yield* Option.match(gathered.whole, { onNone: () => Effect.succeed(Option.none()), onSome: startSpill }), ended = yield* Clock.currentTimeMillis;
57
+ return {
58
+ command: params.command,
59
+ output: shown.text,
60
+ exitCode: Option.getOrNull(Option.flatMap(gathered.exit, (exit) => exit.exitCode)),
61
+ signal: Option.getOrNull(Option.flatMap(gathered.exit, (exit) => exit.signal)),
62
+ termination: Option.match(gathered.exit, { onNone: () => "interrupted", onSome: (exit) => exit.termination }),
63
+ truncated,
64
+ totalLines: gathered.breaks + (gathered.open ? 1 : 0),
65
+ fullOutput: Option.getOrNull(Option.map(spill, (saved) => access.path.relative(access.root, saved.file))),
66
+ fullOutputCut: Option.exists(spill, (saved) => saved.cut),
67
+ durationMs: Option.match(gathered.exit, { onNone: () => ended - started, onSome: (exit) => exit.durationMs })
68
+ };
69
+ }),
70
+ view: { version: "1", render: renderBash, compact: compactBash },
71
+ annotations: { permissions: [WORKSPACE_PERMISSIONS.exec], pollable: !0 }
72
+ });
@@ -0,0 +1,29 @@
1
+ import { Effect, Layer } from "effect";
2
+ import type { Context } from "effect";
3
+ import { Bash, BashError } from "@xandreed/core";
4
+ import type { BashLiveOptions } from "./local-system.entity.js";
5
+ type BashShape = Context.Service.Shape<typeof Bash>;
6
+ /**
7
+ * Commands on the local machine through `node:child_process`, for Node and
8
+ * Bun: `<shell> -c <command>`, detached into a process group of its own, in
9
+ * an environment `resolveBashEnv` builds from `parentEnv`. The shell is
10
+ * resolved to an absolute path once, here, from the parent `PATH` (so a
11
+ * command whose own environment has no `PATH` still runs); none fails with
12
+ * `ShellMissing`. Each command checks its `cwd` first (`CwdMissing`); its
13
+ * stdin is written and closed (a command that never reads it is fine), its
14
+ * stdout and stderr decoded as UTF-8 per stream, a character split between
15
+ * reads arriving whole. A timeout, or a caller that stops reading before
16
+ * the end, kills the whole group; with `killBackground` (the default) so
17
+ * does the command's return. The stream ends once the command has exited
18
+ * and its output has closed, or `exitGraceMs` after the exit if something
19
+ * still holds the output open; the adapter then closes its end of the
20
+ * output pipes, so a background process kept with `killBackground: false`
21
+ * that still writes to them gets `EPIPE` or dies of `SIGPIPE` unless it was
22
+ * started with its own output (`cmd > log 2>&1 &`). Output is buffered
23
+ * without bound until it is read. POSIX only (Linux and macOS): it relies
24
+ * on process groups. Passes `bashConformance` on Bun and Node.
25
+ */
26
+ export declare const makeBash: (options?: BashLiveOptions) => Effect.Effect<BashShape, BashError>;
27
+ /** The local `Bash` (`makeBash`) as a layer; building it fails with `ShellMissing` when the shell is not installed. */
28
+ export declare const BashLive: (options?: BashLiveOptions) => Layer.Layer<Bash, BashError>;
29
+ export {};
@@ -0,0 +1,69 @@
1
+ import { spawn } from "node:child_process";
2
+ import { constants } from "node:fs";
3
+ import { access, stat } from "node:fs/promises";
4
+ import { Cause, Clock, Deferred, Effect, Layer, MutableRef, Option, Queue, Result, Stream } from "effect";
5
+ import { Bash, BashError, collectBashResult, resolveBashEnv } from "@xandreed/core";
6
+ import { bashExitOf, errorMessageOf, executableCandidates, isClosedInput } from "./local-system.entity.functions.js";
7
+ import { localSystemDefaults } from "./local-system.entity.js";
8
+ const isExecutableFile = (path) => Effect.tryPromise(() => access(path, constants.X_OK)).pipe(Effect.andThen(Effect.tryPromise(() => stat(path))), Effect.map((info) => info.isFile()), Effect.orElseSucceed(() => !1)), resolveShell = (shell, path) => Effect.findFirst(executableCandidates(shell, path), isExecutableFile).pipe(Effect.flatMap(Option.match({
9
+ onNone: () => Effect.fail(new BashError({
10
+ reason: "ShellMissing",
11
+ message: `no executable ${shell} ${shell.startsWith("/") ? "at that path" : `on the PATH (${Option.getOrElse(path, () => "unset")})`}`
12
+ })),
13
+ onSome: Effect.succeed
14
+ }))), checkCwd = (cwd) => (cwd.startsWith("/") ? Effect.tryPromise(() => stat(cwd)).pipe(Effect.map((info) => info.isDirectory()), Effect.orElseSucceed(() => !1)) : Effect.succeed(!1)).pipe(Effect.flatMap((directory) => directory ? Effect.void : Effect.fail(new BashError({ reason: "CwdMissing", message: `cwd is not an existing absolute directory: ${cwd}` })))), killGroup = (child, exited) => Option.match(Option.fromNullishOr(child.pid), {
15
+ onNone: () => {
16
+ return;
17
+ },
18
+ onSome: (pid) => {
19
+ const group = Result.try(() => process.kill(-pid, "SIGKILL"));
20
+ if (Result.isFailure(group) && !exited)
21
+ Result.try(() => process.kill(pid, "SIGKILL"));
22
+ }
23
+ });
24
+ export const makeBash = (options = {}) => Effect.gen(function* () {
25
+ const { shell, killBackground, exitGraceMs } = { ...localSystemDefaults, ...options }, parentEnv = () => options.parentEnv ?? process.env, shellPath = yield* resolveShell(shell, Option.fromNullishOr(parentEnv().PATH)), stream = (command, run) => Stream.callback((queue) => Effect.gen(function* () {
26
+ yield* checkCwd(run.cwd);
27
+ const clock = yield* Effect.clockWith((current) => Effect.succeed(current)), exited = yield* Deferred.make(), closed = yield* Deferred.make(), finished = MutableRef.make(!1), decoders = { stdout: new TextDecoder, stderr: new TextDecoder }, emit = (name, text) => text.length > 0 && Queue.offerUnsafe(queue, { _tag: "BashChunk", stream: name, text }), started = yield* Clock.currentTimeMillis, child = yield* Effect.acquireRelease(Effect.try({
28
+ try: () => {
29
+ const spawned = spawn(shellPath, ["-c", command], {
30
+ cwd: run.cwd,
31
+ env: { ...resolveBashEnv(parentEnv(), run) },
32
+ detached: !0,
33
+ stdio: [run.stdin === void 0 ? "ignore" : "pipe", "pipe", "pipe"]
34
+ });
35
+ spawned.stdout?.on("data", (chunk) => emit("stdout", decoders.stdout.decode(chunk, { stream: !0 })));
36
+ spawned.stderr?.on("data", (chunk) => emit("stderr", decoders.stderr.decode(chunk, { stream: !0 })));
37
+ spawned.once("error", (error) => Deferred.doneUnsafe(exited, Effect.fail(new BashError({ reason: "SpawnFailed", message: errorMessageOf(error) }))));
38
+ spawned.once("exit", (code, signal) => Deferred.doneUnsafe(exited, Effect.succeed({ code, signal, at: clock.currentTimeMillisUnsafe() })));
39
+ spawned.once("close", () => Deferred.doneUnsafe(closed, Effect.void));
40
+ spawned.stdin?.on("error", (error) => isClosedInput(error) ? !1 : Queue.failCauseUnsafe(queue, Cause.fail(new BashError({ reason: "SpawnFailed", message: `could not write the command's stdin: ${errorMessageOf(error)}` }))));
41
+ spawned.stdin?.end(run.stdin);
42
+ return spawned;
43
+ },
44
+ catch: (error) => new BashError({ reason: "SpawnFailed", message: errorMessageOf(error) })
45
+ }), (spawned) => Effect.sync(() => {
46
+ if (!MutableRef.get(finished))
47
+ killGroup(spawned, Deferred.isDoneUnsafe(exited));
48
+ spawned.stdout?.destroy();
49
+ spawned.stderr?.destroy();
50
+ spawned.stdin?.destroy();
51
+ })), awaited = Deferred.await(exited).pipe(Effect.map((end) => ({ end, timedOut: !1 }))), { end, timedOut } = yield* run.timeoutMs === void 0 ? awaited : Deferred.await(exited).pipe(Effect.timeoutOption(run.timeoutMs), Effect.flatMap(Option.match({
52
+ onSome: (end) => Effect.succeed({ end, timedOut: !1 }),
53
+ onNone: () => Effect.sync(() => killGroup(child, Deferred.isDoneUnsafe(exited))).pipe(Effect.andThen(awaited), Effect.map(({ end }) => ({ end, timedOut: !0 })))
54
+ })));
55
+ if (killBackground)
56
+ killGroup(child, !0);
57
+ const drained = yield* Deferred.await(closed).pipe(Effect.timeoutOption(exitGraceMs));
58
+ if (Option.isNone(drained)) {
59
+ child.stdout?.destroy();
60
+ child.stderr?.destroy();
61
+ }
62
+ emit("stdout", decoders.stdout.decode());
63
+ emit("stderr", decoders.stderr.decode());
64
+ MutableRef.set(finished, !0);
65
+ yield* Queue.offer(queue, bashExitOf({ code: end.code, signal: end.signal, timedOut, durationMs: end.at - started }));
66
+ yield* Queue.end(queue);
67
+ }));
68
+ return Bash.of({ stream, exec: (command, run) => collectBashResult(stream(command, run)) });
69
+ }), BashLive = (options = {}) => Layer.effect(Bash, makeBash(options));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,20 @@
1
+ import { Layer } from "effect";
2
+ import type { Context } from "effect";
3
+ import { FileSystem } from "@xandreed/core";
4
+ type Fs = Context.Service.Shape<typeof FileSystem>;
5
+ /**
6
+ * The machine's own file system through `node:fs/promises`, for Node and
7
+ * Bun. Paths must be absolute (a relative one fails with `Invalid`); every
8
+ * failure's `reason` comes from the error code (`fsReasonOf`). `glob` is
9
+ * `walkGlob` over `list`, so it matches what every other backend matches
10
+ * and never enters a linked directory. `remove` resolves the parent first
11
+ * and acts on the last name only, so it never follows that name and never
12
+ * removes the root (nor a path ending in `.` or `..`); `rename` names the
13
+ * side a failure came from. `stat` reports the permission bits as `mode`.
14
+ * POSIX only (Linux and macOS): a path must start with `/`. Passes
15
+ * `fileSystemConformance` on Bun and Node.
16
+ */
17
+ export declare const localFileSystem: Fs;
18
+ /** The local `FileSystem` (`localFileSystem`) as a layer. */
19
+ export declare const FileSystemLive: Layer.Layer<FileSystem>;
20
+ export {};
@@ -0,0 +1,52 @@
1
+ import { appendFile, lstat, mkdir, open, readdir, readFile, realpath, rename, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
2
+ import { dirname } from "node:path";
3
+ import { Effect, Layer, Option, Result } from "effect";
4
+ import { FileSystem, FsError, walkGlob } from "@xandreed/core";
5
+ import { errorMessageOf, fsReasonOf, pathIn, removalPlace } from "./local-system.entity.functions.js";
6
+ const refused = (path, reason, message) => new FsError({ path, reason, message }), attempt = (path, run) => path.startsWith("/") ? Effect.tryPromise({ try: run, catch: (error) => refused(path, fsReasonOf(error), errorMessageOf(error)) }) : Effect.fail(refused(path, "Invalid", `paths must be absolute: ${path}`)), kindOf = (entry) => entry.isSymbolicLink() ? "symlink" : entry.isFile() ? "file" : entry.isDirectory() ? "directory" : "other", byName = (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0, bytesOf = (buffer) => new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength), parentsOf = (path) => attempt(path, () => mkdir(dirname(path), { recursive: !0 })).pipe(Effect.asVoid), chunkSize = 65536, joined = (chunks) => {
7
+ const whole = new Uint8Array(chunks.reduce((total, chunk) => total + chunk.length, 0));
8
+ chunks.reduce((at, chunk) => {
9
+ whole.set(chunk, at);
10
+ return at + chunk.length;
11
+ }, 0);
12
+ return whole;
13
+ }, readRange = (path, offset, length) => Effect.acquireUseRelease(attempt(path, () => open(path, "r")), (handle) => attempt(path, () => handle.stat()).pipe(Effect.flatMap((info) => {
14
+ if (info.isDirectory())
15
+ return Effect.fail(refused(path, "IsDirectory", `is a directory: ${path}`));
16
+ const step = Math.max(chunkSize, info.size - offset), gather = (position, remaining, chunks) => {
17
+ const size = Math.min(remaining, step);
18
+ return size <= 0 ? Effect.succeed(chunks) : attempt(path, () => handle.read(new Uint8Array(size), 0, size, position)).pipe(Effect.flatMap(({ bytesRead, buffer }) => bytesRead === 0 ? Effect.succeed(chunks) : gather(position === null ? null : position + bytesRead, remaining - bytesRead, [...chunks, buffer.subarray(0, bytesRead)])));
19
+ };
20
+ return gather(offset === 0 ? null : offset, Option.getOrElse(length, () => Number.POSITIVE_INFINITY), []).pipe(Effect.map(joined));
21
+ })), (handle) => Effect.tryPromise(() => handle.close()).pipe(Effect.ignore)), readBytes = (path, range) => {
22
+ const offset = range?.offset ?? 0, length = Option.fromNullishOr(range?.length);
23
+ if (!(Number.isInteger(offset) && offset >= 0 && Option.match(length, { onNone: () => !0, onSome: (count) => Number.isInteger(count) && count >= 0 })))
24
+ return Effect.fail(refused(path, "Invalid", `offset and length must be whole numbers of at least 0, not ${JSON.stringify(range)}`));
25
+ return offset === 0 && Option.isNone(length) ? attempt(path, () => readFile(path)).pipe(Effect.map(bytesOf)) : readRange(path, offset, length);
26
+ }, list = (dir) => attempt(dir, () => readdir(dir, { withFileTypes: !0 })).pipe(Effect.map((entries) => entries.map((entry) => ({ name: entry.name, kind: kindOf(entry) })).toSorted(byName))), remove = (path, options) => Result.match(removalPlace(path), {
27
+ onFailure: (message) => Effect.fail(refused(path, "Invalid", message)),
28
+ onSuccess: ({ parent, name }) => attempt(path, () => realpath(parent)).pipe(Effect.map((dir) => pathIn(dir, name)), Effect.flatMap((target) => attempt(path, () => lstat(target)).pipe(Effect.flatMap((info) => !info.isDirectory() ? attempt(path, () => unlink(target)) : options?.recursive === !0 ? attempt(path, () => rm(target, { recursive: !0, force: !0 })) : attempt(path, () => rmdir(target)).pipe(Effect.mapError((error) => error.reason === "Exists" ? refused(path, "NotEmpty", error.message) : error))))), Effect.catchIf((error) => error.reason === "NotFound", () => Effect.void))
29
+ }), destinationOnly = new Set(["IsDirectory", "NotEmpty", "Exists"]), eitherSide = new Set(["NotFound", "NotDirectory", "Invalid"]), move = (from, to) => !to.startsWith("/") ? Effect.fail(refused(to, "Invalid", `paths must be absolute: ${to}`)) : attempt(from, () => rename(from, to)).pipe(Effect.catch((error) => {
30
+ const atTo = new FsError({ path: to, reason: error.reason === "Exists" ? "NotEmpty" : error.reason, message: error.message });
31
+ return destinationOnly.has(error.reason) ? Effect.fail(atTo) : eitherSide.has(error.reason) ? Effect.tryPromise(() => lstat(from)).pipe(Effect.match({ onFailure: () => error, onSuccess: () => atTo }), Effect.flatMap(Effect.fail)) : Effect.fail(error);
32
+ })), statOf = (path, options) => attempt(path, () => options?.followSymlinks === !1 ? lstat(path) : stat(path)).pipe(Effect.map((info) => ({
33
+ path,
34
+ kind: kindOf(info),
35
+ size: info.size,
36
+ mtimeMs: info.mtimeMs,
37
+ mode: Option.some(info.mode & 4095)
38
+ })));
39
+ export const localFileSystem = FileSystem.of({
40
+ read: (path) => attempt(path, () => readFile(path, "utf8")),
41
+ readBytes,
42
+ write: (path, content, options) => (options?.createParents === !0 ? parentsOf(path) : Effect.void).pipe(Effect.andThen(attempt(path, () => writeFile(path, content)))),
43
+ append: (path, content, options) => (options?.createParents === !0 ? parentsOf(path) : Effect.void).pipe(Effect.andThen(attempt(path, () => appendFile(path, content)))),
44
+ rename: move,
45
+ stat: statOf,
46
+ exists: (path) => attempt(path, () => stat(path)).pipe(Effect.as(!0), Effect.catchIf((error) => error.reason === "NotFound" || error.reason === "NotDirectory", () => Effect.succeed(!1))),
47
+ list,
48
+ glob: (pattern, options) => walkGlob(list, pattern, options),
49
+ mkdir: (dir, options) => attempt(dir, () => mkdir(dir, { recursive: options?.recursive === !0 })).pipe(Effect.asVoid),
50
+ realPath: (path) => attempt(path, () => realpath(path)),
51
+ remove
52
+ }), FileSystemLive = Layer.succeed(FileSystem, localFileSystem);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ export { BashLive, makeBash } from "./bash.adapter.js";
2
+ export { FileSystemLive, localFileSystem } from "./file-system.adapter.js";
3
+ export { LocalSystemConfig, localSystemDefaults } from "./local-system.entity.js";
4
+ export type { BashLiveOptions } from "./local-system.entity.js";
5
+ export { fsReasonOf } from "./local-system.entity.functions.js";
6
+ export { localSystemPlugin, LocalSystemPluginLive, workspaceToolsPlugin, WorkspaceToolsPluginLive } from "./plugin.adapter.js";
7
+ export { SkillsDirectoryConfig, skillsDirectoryDefaults } from "./skills/skills-directory.entity.js";
8
+ export { ALWAYS_SECTION_ID, SKILLS_CAPABILITY_ID, skillsCapability } from "./skills/skills-directory.entity.functions.js";
9
+ export { loadSkillsDirectory, skillsDirectoryPlugin, SkillsDirectoryPluginLive } from "./skills/skills-directory.adapter.js";
10
+ export { workspaceTools } from "./workspace-tools.adapter.js";
11
+ export { BashParams, BashToolResult, EditParams, EditResult, FindParams, FindResult, GrepLine, GrepParams, GrepResult, LsEntry, LsParams, LsResult, ReadParams, ReadResult, TextEdit, WORKSPACE_PERMISSIONS, WorkspaceSkillName, WorkspaceToolName, workspaceToolSchemas, WorkspaceToolsConfig, workspaceToolsDefaults, WriteParams, WriteResult, } from "./workspace-tools.entity.js";
12
+ export { applyTextEdits, normalizeLine } from "./text-edit.entity.functions.js";
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ export { BashLive, makeBash } from "./bash.adapter.js";
2
+ export { FileSystemLive, localFileSystem } from "./file-system.adapter.js";
3
+ export { LocalSystemConfig, localSystemDefaults } from "./local-system.entity.js";
4
+ export { fsReasonOf } from "./local-system.entity.functions.js";
5
+ export { localSystemPlugin, LocalSystemPluginLive, workspaceToolsPlugin, WorkspaceToolsPluginLive } from "./plugin.adapter.js";
6
+ export { SkillsDirectoryConfig, skillsDirectoryDefaults } from "./skills/skills-directory.entity.js";
7
+ export { ALWAYS_SECTION_ID, SKILLS_CAPABILITY_ID, skillsCapability } from "./skills/skills-directory.entity.functions.js";
8
+ export { loadSkillsDirectory, skillsDirectoryPlugin, SkillsDirectoryPluginLive } from "./skills/skills-directory.adapter.js";
9
+ export { workspaceTools } from "./workspace-tools.adapter.js";
10
+ export {
11
+ BashParams,
12
+ BashToolResult,
13
+ EditParams,
14
+ EditResult,
15
+ FindParams,
16
+ FindResult,
17
+ GrepLine,
18
+ GrepParams,
19
+ GrepResult,
20
+ LsEntry,
21
+ LsParams,
22
+ LsResult,
23
+ ReadParams,
24
+ ReadResult,
25
+ TextEdit,
26
+ WORKSPACE_PERMISSIONS,
27
+ WorkspaceSkillName,
28
+ WorkspaceToolName,
29
+ workspaceToolSchemas,
30
+ WorkspaceToolsConfig,
31
+ workspaceToolsDefaults,
32
+ WriteParams,
33
+ WriteResult
34
+ } from "./workspace-tools.entity.js";
35
+ export { applyTextEdits, normalizeLine } from "./text-edit.entity.functions.js";
@@ -0,0 +1,40 @@
1
+ import { Schema } from "effect";
2
+ /**
3
+ * How `BashLive` (and `localSystemPlugin`) runs commands. `shell` is the
4
+ * program commands run with (`<shell> -c <command>`): a name looked up once,
5
+ * when the layer is built, on the parent environment's `PATH` (its absolute
6
+ * entries), or an absolute path; there is no fallback, so a host without
7
+ * bash sets `shell: "sh"` (or another path) itself. With `killBackground`, a
8
+ * command's whole process group is killed when the command returns, so
9
+ * nothing it started in the background outlives it (a timeout or an
10
+ * interrupted caller always kills the group). `exitGraceMs` is how long,
11
+ * after the command exits, its output may take to close (a background
12
+ * process that kept it open) before the stream ends without the rest. The
13
+ * output pipes close then, so a background process kept with
14
+ * `killBackground: false` must have its own output (`cmd > log 2>&1 &`):
15
+ * one still writing to the command's dies of `SIGPIPE` or gets `EPIPE`.
16
+ */
17
+ export declare const LocalSystemConfig: Schema.Struct<{
18
+ readonly shell: Schema.NonEmptyString;
19
+ readonly killBackground: Schema.Boolean;
20
+ readonly exitGraceMs: Schema.Int;
21
+ }>;
22
+ export type LocalSystemConfig = typeof LocalSystemConfig.Type;
23
+ export declare const localSystemDefaults: LocalSystemConfig;
24
+ /**
25
+ * `BashLive`'s options: the plugin's configuration, each part optional, and
26
+ * `parentEnv`, the environment commands inherit from (filtered by
27
+ * `resolveBashEnv`) and the shell is looked up on; `process.env` when not
28
+ * given.
29
+ */
30
+ export type BashLiveOptions = Partial<LocalSystemConfig> & {
31
+ readonly parentEnv?: Readonly<Record<string, string | undefined>>;
32
+ };
33
+ /** How a spawned process ended, as Node reports it, and whether the adapter killed it for running past its timeout. */
34
+ export declare const ProcessEnd: Schema.Struct<{
35
+ readonly code: Schema.NullOr<Schema.Int>;
36
+ readonly signal: Schema.NullOr<Schema.String>;
37
+ readonly timedOut: Schema.Boolean;
38
+ readonly durationMs: Schema.Number;
39
+ }>;
40
+ export type ProcessEnd = typeof ProcessEnd.Type;
@@ -0,0 +1,52 @@
1
+ import { Option, Result } from "effect";
2
+ import type { BashExit, FsReason } from "@xandreed/core";
3
+ import type { ProcessEnd } from "./local-system.entity.js";
4
+ /** The `code` a Node error carries (`ENOENT`, `ERR_FS_EISDIR`, …), when it has one. */
5
+ export declare const errorCodeOf: (error: unknown) => Option.Option<string>;
6
+ /** A thrown value's message, for the `message` of the error it becomes. */
7
+ export declare const errorMessageOf: (error: unknown) => string;
8
+ /**
9
+ * The `FsReason` of a failed node:fs call, from its error code: `ENOENT`
10
+ * is `NotFound`, `EEXIST` `Exists`, `EISDIR` (and Node's `ERR_FS_EISDIR`)
11
+ * `IsDirectory`, `ENOTDIR` `NotDirectory`, `ENOTEMPTY` `NotEmpty`, `EACCES`
12
+ * and `EPERM` `Permission`; `EINVAL`, a symbolic link loop (`ELOOP`), a
13
+ * name too long and an unusable argument (a path with a NUL byte) are
14
+ * `Invalid`; anything else, or no code at all, is `Io`.
15
+ */
16
+ export declare const fsReasonOf: (error: unknown) => FsReason;
17
+ /**
18
+ * Whether a write to a command's standard input failed only because the
19
+ * command no longer reads it: it exited or closed its input first (`EPIPE`),
20
+ * or the stream was already torn down (`ERR_STREAM_DESTROYED`). The command
21
+ * is free to ignore its input, so these are not failures.
22
+ */
23
+ export declare const isClosedInput: (error: unknown) => boolean;
24
+ /**
25
+ * Where to look for an executable: an absolute path is itself; a bare name
26
+ * is tried in each absolute directory of `path` (a `PATH` value), in order;
27
+ * relative and empty entries, which would depend on the current directory,
28
+ * are skipped, as is a relative name with a slash.
29
+ */
30
+ export declare const executableCandidates: (name: string, path: Option.Option<string>) => ReadonlyArray<string>;
31
+ /**
32
+ * Where `remove(path)` acts, or why it never does. Trailing slashes are
33
+ * ignored, so a link named with one is still the link. The root (`/`, `//`)
34
+ * is refused, and so is a path whose last segment is `.` or `..`: such a
35
+ * path names a directory only by where it leads (`<link>/..` can be the
36
+ * root), so it is never removed. Otherwise the place is the parent as
37
+ * written (to be resolved, links and all) and the last name, never followed.
38
+ */
39
+ export declare const removalPlace: (path: string) => Result.Result<{
40
+ readonly parent: string;
41
+ readonly name: string;
42
+ }, string>;
43
+ /** `name` in the directory `dir` (an absolute, resolved path). */
44
+ export declare const pathIn: (dir: string, name: string) => string;
45
+ /**
46
+ * A process's end as the `BashExit` the stream closes with. A process the
47
+ * adapter killed for its timeout ends `timeout` (it has no exit code, and its
48
+ * signal is the one that killed it); one a signal killed ends `signal`;
49
+ * otherwise it exited, with its code. A process that exited on its own just
50
+ * as its timeout came keeps its exit code and ends `exit`.
51
+ */
52
+ export declare const bashExitOf: (end: ProcessEnd) => BashExit;
@@ -0,0 +1,32 @@
1
+ import { Option, Result } from "effect";
2
+ export const errorCodeOf = (error) => typeof error === "object" && error !== null && ("code" in error) && typeof error.code === "string" ? Option.some(error.code) : Option.none(), errorMessageOf = (error) => error instanceof Error ? error.message : String(error);
3
+ const reasons = new Map([
4
+ ["ENOENT", "NotFound"],
5
+ ["EEXIST", "Exists"],
6
+ ["EISDIR", "IsDirectory"],
7
+ ["ERR_FS_EISDIR", "IsDirectory"],
8
+ ["ENOTDIR", "NotDirectory"],
9
+ ["ENOTEMPTY", "NotEmpty"],
10
+ ["EACCES", "Permission"],
11
+ ["EPERM", "Permission"],
12
+ ["EINVAL", "Invalid"],
13
+ ["ELOOP", "Invalid"],
14
+ ["ENAMETOOLONG", "Invalid"],
15
+ ["ERR_INVALID_ARG_VALUE", "Invalid"]
16
+ ]);
17
+ export const fsReasonOf = (error) => Option.match(errorCodeOf(error), {
18
+ onNone: () => "Io",
19
+ onSome: (code) => reasons.get(code) ?? "Io"
20
+ }), isClosedInput = (error) => Option.match(errorCodeOf(error), { onNone: () => !1, onSome: (code) => code === "EPIPE" || code === "ERR_STREAM_DESTROYED" }), executableCandidates = (name, path) => name.startsWith("/") ? [name] : name.includes("/") || name.length === 0 ? [] : Option.getOrElse(path, () => "").split(":").filter((dir) => dir.startsWith("/")).map((dir) => `${dir.endsWith("/") ? dir.slice(0, -1) : dir}/${name}`), removalPlace = (path) => {
21
+ const segments = path.split("/").filter((segment) => segment.length > 0);
22
+ return !path.startsWith("/") ? Result.fail(`paths must be absolute: ${path}`) : Option.match(Option.fromNullishOr(segments.at(-1)), {
23
+ onNone: () => Result.fail("the root directory is never removed"),
24
+ onSome: (name) => name === "." || name === ".." ? Result.fail(`a path whose last segment is ${name} is never removed: ${path}`) : Result.succeed({ parent: `/${segments.slice(0, -1).join("/")}`, name })
25
+ });
26
+ }, pathIn = (dir, name) => dir.endsWith("/") ? `${dir}${name}` : `${dir}/${name}`, bashExitOf = (end) => ({
27
+ _tag: "BashExit",
28
+ termination: end.code !== null ? "exit" : end.timedOut ? "timeout" : end.signal !== null ? "signal" : "exit",
29
+ exitCode: Option.fromNullishOr(end.code),
30
+ signal: Option.fromNullishOr(end.signal),
31
+ durationMs: end.durationMs
32
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { Schema } from "effect";
2
+ export const LocalSystemConfig = Schema.Struct({
3
+ shell: Schema.NonEmptyString,
4
+ killBackground: Schema.Boolean,
5
+ exitGraceMs: Schema.Int.pipe(Schema.check(Schema.isBetween({ minimum: 0, maximum: 60000 })))
6
+ }), localSystemDefaults = { shell: "bash", killBackground: !0, exitGraceMs: 500 }, ProcessEnd = Schema.Struct({
7
+ code: Schema.NullOr(Schema.Int),
8
+ signal: Schema.NullOr(Schema.String),
9
+ timedOut: Schema.Boolean,
10
+ durationMs: Schema.Number
11
+ });
@@ -0,0 +1,120 @@
1
+ import { Layer } from "effect";
2
+ import { Bash, Capabilities, FileSystem } from "@xandreed/core";
3
+ /**
4
+ * The machine the agent runs on as its `FileSystem` and `Bash`: the local
5
+ * disk (`FileSystemLive`) and commands run with the configured shell
6
+ * (`BashLive`), built once per agent. Activation fails when the shell is not
7
+ * installed. Commands inherit the host process's environment through
8
+ * `resolveBashEnv`'s allowlist.
9
+ */
10
+ export declare const localSystemPlugin: import("@xandreed/core").TypedPlugin<{
11
+ readonly shell: string;
12
+ readonly killBackground: boolean;
13
+ readonly exitGraceMs: number;
14
+ }, {
15
+ readonly shell: string;
16
+ readonly killBackground: boolean;
17
+ readonly exitGraceMs: number;
18
+ }, FileSystem | Bash, import("@xandreed/core").BashError, never>;
19
+ /** The local system as a typed layer: provides FileSystem and Bash; requires nothing. */
20
+ export declare const LocalSystemPluginLive: (options?: Partial<{
21
+ readonly shell: string;
22
+ readonly killBackground: boolean;
23
+ readonly exitGraceMs: number;
24
+ }> | undefined) => Layer.Layer<FileSystem | Bash, import("@xandreed/core").HarnessError | import("@xandreed/core").BashError, never>;
25
+ /**
26
+ * The workspace tools (`workspaceTools`): read, ls, find and grep always on,
27
+ * write and edit behind `fs.write`, bash behind `shell.exec`, over the
28
+ * `FileSystem`, `Bash` and `SessionEnvironment` of each call. Built once
29
+ * per agent, so two sessions editing one file wait for each other. `Bash`
30
+ * is optional: without it there is no `bash` tool. Hosts grant the
31
+ * permissions through tool discovery's `grants` or a turn's
32
+ * `PermissionGrants`.
33
+ */
34
+ export declare const workspaceToolsPlugin: import("@xandreed/core").TypedPlugin<{
35
+ readonly tools: readonly ("write" | "find" | "read" | "grep" | "ls" | "edit" | "bash")[];
36
+ readonly always: readonly ("read" | "edit" | "shell")[];
37
+ readonly confine: boolean;
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 maxFileBytes: number;
47
+ readonly readImages: boolean;
48
+ readonly fuzzyEdit: boolean;
49
+ readonly bashTimeoutMs: number;
50
+ readonly bashMaxTimeoutMs: number;
51
+ readonly bashEnv: {
52
+ readonly [x: string]: string;
53
+ };
54
+ readonly bashInheritEnv: boolean;
55
+ readonly bashEnvAllow: readonly string[];
56
+ readonly bashSessionEnv: boolean;
57
+ readonly spillDir: string | null;
58
+ readonly spillKeep: number;
59
+ readonly progressMs: number;
60
+ readonly environmentSection: boolean;
61
+ readonly sectionOrder: number;
62
+ }, {
63
+ readonly tools: readonly ("write" | "find" | "read" | "grep" | "ls" | "edit" | "bash")[];
64
+ readonly always: readonly ("read" | "edit" | "shell")[];
65
+ readonly confine: boolean;
66
+ readonly protect: readonly string[];
67
+ readonly exclude: readonly string[];
68
+ readonly maxLines: number;
69
+ readonly maxBytes: number;
70
+ readonly lsEntries: number;
71
+ readonly findResults: number;
72
+ readonly grepMatches: number;
73
+ readonly grepLineChars: number;
74
+ readonly maxFileBytes: number;
75
+ readonly readImages: boolean;
76
+ readonly fuzzyEdit: boolean;
77
+ readonly bashTimeoutMs: number;
78
+ readonly bashMaxTimeoutMs: number;
79
+ readonly bashEnv: {
80
+ readonly [x: string]: string;
81
+ };
82
+ readonly bashInheritEnv: boolean;
83
+ readonly bashEnvAllow: readonly string[];
84
+ readonly bashSessionEnv: boolean;
85
+ readonly spillDir: string | null;
86
+ readonly spillKeep: number;
87
+ readonly progressMs: number;
88
+ readonly environmentSection: boolean;
89
+ readonly sectionOrder: number;
90
+ }, Capabilities, import("@xandreed/core").HarnessError, never>;
91
+ /** The workspace tools as a typed layer: contributes their capability; its handlers need FileSystem and SessionEnvironment (and Bash for bash) where the tools are opened. */
92
+ export declare const WorkspaceToolsPluginLive: (options?: Partial<{
93
+ readonly tools: readonly ("write" | "find" | "read" | "grep" | "ls" | "edit" | "bash")[];
94
+ readonly always: readonly ("read" | "edit" | "shell")[];
95
+ readonly confine: boolean;
96
+ readonly protect: readonly string[];
97
+ readonly exclude: readonly string[];
98
+ readonly maxLines: number;
99
+ readonly maxBytes: number;
100
+ readonly lsEntries: number;
101
+ readonly findResults: number;
102
+ readonly grepMatches: number;
103
+ readonly grepLineChars: number;
104
+ readonly maxFileBytes: number;
105
+ readonly readImages: boolean;
106
+ readonly fuzzyEdit: boolean;
107
+ readonly bashTimeoutMs: number;
108
+ readonly bashMaxTimeoutMs: number;
109
+ readonly bashEnv: {
110
+ readonly [x: string]: string;
111
+ };
112
+ readonly bashInheritEnv: boolean;
113
+ readonly bashEnvAllow: readonly string[];
114
+ readonly bashSessionEnv: boolean;
115
+ readonly spillDir: string | null;
116
+ readonly spillKeep: number;
117
+ readonly progressMs: number;
118
+ readonly environmentSection: boolean;
119
+ readonly sectionOrder: number;
120
+ }> | undefined) => Layer.Layer<Capabilities, import("@xandreed/core").HarnessError, never>;