@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.
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/bash-tool.adapter.d.ts +15 -0
- package/dist/bash-tool.adapter.js +72 -0
- package/dist/bash.adapter.d.ts +29 -0
- package/dist/bash.adapter.js +69 -0
- package/dist/bash.adapter.test.d.ts +1 -0
- package/dist/file-system.adapter.d.ts +20 -0
- package/dist/file-system.adapter.js +52 -0
- package/dist/file-system.adapter.test.d.ts +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +35 -0
- package/dist/local-system.entity.d.ts +40 -0
- package/dist/local-system.entity.functions.d.ts +52 -0
- package/dist/local-system.entity.functions.js +32 -0
- package/dist/local-system.entity.functions.test.d.ts +1 -0
- package/dist/local-system.entity.js +11 -0
- package/dist/plugin.adapter.d.ts +120 -0
- package/dist/plugin.adapter.js +27 -0
- package/dist/plugin.adapter.test.d.ts +1 -0
- package/dist/read-tools.adapter.d.ts +33 -0
- package/dist/read-tools.adapter.js +100 -0
- package/dist/skills/skills-directory.adapter.d.ts +62 -0
- package/dist/skills/skills-directory.adapter.js +30 -0
- package/dist/skills/skills-directory.adapter.test.d.ts +1 -0
- package/dist/skills/skills-directory.entity.d.ts +96 -0
- package/dist/skills/skills-directory.entity.functions.d.ts +33 -0
- package/dist/skills/skills-directory.entity.functions.js +45 -0
- package/dist/skills/skills-directory.entity.functions.test.d.ts +1 -0
- package/dist/skills/skills-directory.entity.js +35 -0
- package/dist/text-edit.entity.d.ts +32 -0
- package/dist/text-edit.entity.functions.d.ts +32 -0
- package/dist/text-edit.entity.functions.js +68 -0
- package/dist/text-edit.entity.functions.test.d.ts +1 -0
- package/dist/text-edit.entity.js +15 -0
- package/dist/text-search.entity.d.ts +23 -0
- package/dist/text-search.entity.functions.d.ts +51 -0
- package/dist/text-search.entity.functions.js +30 -0
- package/dist/text-search.entity.functions.test.d.ts +1 -0
- package/dist/text-search.entity.js +10 -0
- package/dist/text-window.entity.d.ts +46 -0
- package/dist/text-window.entity.functions.d.ts +54 -0
- package/dist/text-window.entity.functions.js +88 -0
- package/dist/text-window.entity.functions.test.d.ts +1 -0
- package/dist/text-window.entity.js +21 -0
- package/dist/workspace-path.entity.d.ts +22 -0
- package/dist/workspace-path.entity.functions.d.ts +60 -0
- package/dist/workspace-path.entity.functions.js +63 -0
- package/dist/workspace-path.entity.functions.test.d.ts +1 -0
- package/dist/workspace-path.entity.js +8 -0
- package/dist/workspace-tools.adapter.d.ts +21 -0
- package/dist/workspace-tools.adapter.js +46 -0
- package/dist/workspace-tools.adapter.test.d.ts +1 -0
- package/dist/workspace-tools.entity.d.ts +433 -0
- package/dist/workspace-tools.entity.functions.d.ts +51 -0
- package/dist/workspace-tools.entity.functions.js +124 -0
- package/dist/workspace-tools.entity.functions.test.d.ts +1 -0
- package/dist/workspace-tools.entity.js +133 -0
- package/dist/workspace-tools.journey.test.d.ts +1 -0
- package/dist/write-tools.adapter.d.ts +12 -0
- package/dist/write-tools.adapter.js +77 -0
- 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
|
-
#
|
|
1
|
+
# @xandreed/plugin-workspace
|
|
2
2
|
|
|
3
|
-
|
|
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 {};
|
package/dist/index.d.ts
ADDED
|
@@ -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>;
|