@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
@@ -0,0 +1,133 @@
1
+ import { Result, Schema } from "effect";
2
+ import { AttachmentRejectedReason, BASH_ENV_ALLOWLIST, BashTermination, compileGlob, ImageMediaType, Media } from "@xandreed/core";
3
+ export const WorkspaceToolName = Schema.Literals(["read", "write", "edit", "ls", "find", "grep", "bash"]), WorkspaceSkillName = Schema.Literals(["read", "edit", "shell"]), WORKSPACE_PERMISSIONS = { write: "fs.write", exec: "shell.exec" };
4
+ const atLeast = (minimum) => Schema.Int.check(Schema.isGreaterThanOrEqualTo(minimum)), between = (minimum, maximum) => Schema.Int.check(Schema.isBetween({ minimum, maximum })), InsideWorkspace = Schema.String.check(Schema.makeFilter((path) => path.length > 0 && !path.startsWith("/") && !path.split("/").includes("..") ? !0 : "a path relative to the workspace root, without a .. segment")), Glob = Schema.String.check(Schema.makeFilter((glob) => Result.match(compileGlob(glob, { dot: !0 }), { onSuccess: () => !0, onFailure: (reason) => `not a usable glob: ${reason}` })));
5
+ export const WorkspaceToolsConfig = Schema.Struct({
6
+ tools: Schema.Array(WorkspaceToolName),
7
+ always: Schema.Array(WorkspaceSkillName),
8
+ confine: Schema.Boolean,
9
+ protect: Schema.Array(Glob),
10
+ exclude: Schema.Array(Glob),
11
+ maxLines: between(1, 1e5),
12
+ maxBytes: between(1024, 10485760),
13
+ lsEntries: between(1, 1e5),
14
+ findResults: between(1, 1e5),
15
+ grepMatches: between(1, 1e4),
16
+ grepLineChars: between(16, 1e5),
17
+ maxFileBytes: between(1024, 1073741824),
18
+ readImages: Schema.Boolean,
19
+ fuzzyEdit: Schema.Boolean,
20
+ bashTimeoutMs: atLeast(1),
21
+ bashMaxTimeoutMs: atLeast(1),
22
+ bashEnv: Schema.Record(Schema.String, Schema.String),
23
+ bashInheritEnv: Schema.Boolean,
24
+ bashEnvAllow: Schema.Array(Schema.String),
25
+ bashSessionEnv: Schema.Boolean,
26
+ spillDir: Schema.NullOr(InsideWorkspace),
27
+ spillKeep: between(1, 1e4),
28
+ progressMs: between(1, 60000),
29
+ environmentSection: Schema.Boolean,
30
+ sectionOrder: Schema.Int
31
+ }).check(Schema.makeFilter((config) => config.bashTimeoutMs <= config.bashMaxTimeoutMs ? !0 : "bashTimeoutMs must not exceed bashMaxTimeoutMs")), workspaceToolsDefaults = {
32
+ tools: ["read", "write", "edit", "ls", "find", "grep", "bash"],
33
+ always: ["read"],
34
+ confine: !0,
35
+ protect: [".git/**", ".efferent/**"],
36
+ exclude: ["node_modules", ".git", ".efferent"],
37
+ maxLines: 2000,
38
+ maxBytes: 51200,
39
+ lsEntries: 500,
40
+ findResults: 1000,
41
+ grepMatches: 100,
42
+ grepLineChars: 500,
43
+ maxFileBytes: 33554432,
44
+ readImages: !0,
45
+ fuzzyEdit: !0,
46
+ bashTimeoutMs: 120000,
47
+ bashMaxTimeoutMs: 600000,
48
+ bashEnv: {},
49
+ bashInheritEnv: !1,
50
+ bashEnvAllow: BASH_ENV_ALLOWLIST,
51
+ bashSessionEnv: !1,
52
+ spillDir: ".efferent/runtime/tool-output",
53
+ spillKeep: 50,
54
+ progressMs: 250,
55
+ environmentSection: !0,
56
+ sectionOrder: 800
57
+ }, ReadParams = Schema.Struct({
58
+ path: Schema.String,
59
+ offset: Schema.optionalKey(atLeast(1)),
60
+ limit: Schema.optionalKey(atLeast(1))
61
+ }), ReadResult = Schema.Union([
62
+ Schema.TaggedStruct("Text", {
63
+ path: Schema.String,
64
+ text: Schema.String,
65
+ firstLine: Schema.Int,
66
+ lastLine: Schema.Int,
67
+ totalLines: Schema.Int,
68
+ next: Schema.NullOr(Schema.Int),
69
+ clipped: Schema.Boolean,
70
+ lossy: Schema.Boolean
71
+ }),
72
+ Schema.TaggedStruct("Binary", {
73
+ path: Schema.String,
74
+ bytes: Schema.Int,
75
+ mediaType: Schema.NullOr(Schema.String)
76
+ }),
77
+ Schema.TaggedStruct("Image", {
78
+ path: Schema.String,
79
+ image: Media
80
+ }),
81
+ Schema.TaggedStruct("ImageRejected", {
82
+ path: Schema.String,
83
+ mediaType: ImageMediaType,
84
+ bytes: Schema.Int,
85
+ reason: AttachmentRejectedReason,
86
+ message: Schema.String
87
+ })
88
+ ]), WriteParams = Schema.Struct({ path: Schema.String, content: Schema.String }), WriteResult = Schema.Struct({ path: Schema.String, bytes: Schema.Int, created: Schema.Boolean }), TextEdit = Schema.Struct({ oldText: Schema.String, newText: Schema.String }), EditParams = Schema.Struct({ path: Schema.String, edits: Schema.NonEmptyArray(TextEdit) }), EditResult = Schema.Struct({
89
+ path: Schema.String,
90
+ replaced: Schema.Int,
91
+ normalized: Schema.Boolean,
92
+ firstChangedLine: Schema.Int,
93
+ diff: Schema.String
94
+ }), LsParams = Schema.Struct({ path: Schema.optionalKey(Schema.String), limit: Schema.optionalKey(atLeast(1)) }), LsEntry = Schema.Struct({ name: Schema.String, kind: Schema.Literals(["file", "directory", "symlink", "other"]) }), LsResult = Schema.Struct({ path: Schema.String, entries: Schema.Array(LsEntry), total: Schema.Int }), FindParams = Schema.Struct({ pattern: Schema.String, path: Schema.optionalKey(Schema.String), limit: Schema.optionalKey(atLeast(1)) }), FindResult = Schema.Struct({ pattern: Schema.String, path: Schema.String, paths: Schema.Array(Schema.String), truncated: Schema.Boolean }), GrepParams = Schema.Struct({
95
+ pattern: Schema.String,
96
+ path: Schema.optionalKey(Schema.String),
97
+ glob: Schema.optionalKey(Schema.String),
98
+ ignoreCase: Schema.optionalKey(Schema.Boolean),
99
+ literal: Schema.optionalKey(Schema.Boolean),
100
+ context: Schema.optionalKey(between(0, 10)),
101
+ limit: Schema.optionalKey(atLeast(1))
102
+ }), GrepLine = Schema.Struct({ path: Schema.String, line: Schema.Int, text: Schema.String, match: Schema.Boolean }), GrepResult = Schema.Struct({
103
+ pattern: Schema.String,
104
+ path: Schema.String,
105
+ lines: Schema.Array(GrepLine),
106
+ matches: Schema.Int,
107
+ files: Schema.Int,
108
+ truncated: Schema.Boolean,
109
+ skipped: Schema.Int
110
+ }), BashParams = Schema.Struct({
111
+ command: Schema.String,
112
+ timeout: Schema.optionalKey(atLeast(1)),
113
+ stdin: Schema.optionalKey(Schema.String)
114
+ }), BashToolResult = Schema.Struct({
115
+ command: Schema.String,
116
+ output: Schema.String,
117
+ exitCode: Schema.NullOr(Schema.Int),
118
+ signal: Schema.NullOr(Schema.String),
119
+ termination: BashTermination,
120
+ truncated: Schema.Boolean,
121
+ totalLines: Schema.Int,
122
+ fullOutput: Schema.NullOr(Schema.String),
123
+ fullOutputCut: Schema.Boolean,
124
+ durationMs: Schema.Number
125
+ }), workspaceToolSchemas = {
126
+ read: { name: "read", parametersSchema: ReadParams, successSchema: ReadResult },
127
+ write: { name: "write", parametersSchema: WriteParams, successSchema: WriteResult },
128
+ edit: { name: "edit", parametersSchema: EditParams, successSchema: EditResult },
129
+ ls: { name: "ls", parametersSchema: LsParams, successSchema: LsResult },
130
+ find: { name: "find", parametersSchema: FindParams, successSchema: FindResult },
131
+ grep: { name: "grep", parametersSchema: GrepParams, successSchema: GrepResult },
132
+ bash: { name: "bash", parametersSchema: BashParams, successSchema: BashToolResult }
133
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,12 @@
1
+ import type { RegisteredTool } from "@xandreed/core";
2
+ import type { WorkspaceToolContext } from "./read-tools.adapter.js";
3
+ /** `write`: create a file, or replace all of one, through a temporary file and a rename (see `save`). */
4
+ export declare const writeTool: (context: WorkspaceToolContext) => RegisteredTool;
5
+ /**
6
+ * `edit`: exact replacements in a UTF-8 text file, all matched against the
7
+ * file as it was, with the BOM and line endings kept. A file that is not
8
+ * valid UTF-8 is refused (`NotUtf8`): writing its decoded text back would
9
+ * turn every byte that is not into U+FFFD, lines the edits never touched
10
+ * included.
11
+ */
12
+ export declare const editTool: (context: WorkspaceToolContext) => RegisteredTool;
@@ -0,0 +1,77 @@
1
+ import { Tool } from "effect/ai";
2
+ import { Effect, Option, Random, RcMap } from "effect";
3
+ import { defineTool, Failure } from "@xandreed/core";
4
+ import { applyTextEdits, unifiedDiff } from "./text-edit.entity.functions.js";
5
+ import { decodeText, encodeText, formatBytes, looksBinary, utf8Length } from "./text-window.entity.functions.js";
6
+ import { checkWritable, fsFailure, openWorkspace, resolveTarget } from "./workspace-path.entity.functions.js";
7
+ import { EditParams, EditResult, WORKSPACE_PERMISSIONS, WriteParams, WriteResult } from "./workspace-tools.entity.js";
8
+ import { compactEdit, compactWrite, renderEdit, renderWrite, toolDescriptions } from "./workspace-tools.entity.functions.js";
9
+ import { failure, fileSubjects, notAFile } from "./read-tools.adapter.js";
10
+ const WriteTool = (description) => Tool.make("write", { description, parameters: WriteParams, success: WriteResult, failure: Failure, failureMode: "return" }), EditTool = (description) => Tool.make("edit", { description, parameters: EditParams, success: EditResult, failure: Failure, failureMode: "return" }), holding = (context, path) => (change) => Effect.scoped(RcMap.get(context.locks, path).pipe(Effect.flatMap((lock) => lock.withPermits(1)(change)))), existing = (access, target) => access.fs.stat(target.path).pipe(Effect.map(Option.some), Effect.catchIf((error) => error.reason === "NotFound", () => Effect.succeed(Option.none())), Effect.mapError(fsFailure(target.display))), notForNewFiles = 3657, save = (access, target, content, before) => {
11
+ const mode = Option.flatMap(before, (info) => info.mode), inPlace = access.fs.write(target.path, content), replaced = Effect.gen(function* () {
12
+ const suffix = (yield* Random.nextIntBetween(0, 2147483647)).toString(36), temporary = access.path.join(access.path.dirname(target.path), `.${access.path.basename(target.path)}.${suffix}.efferent-tmp`), discard = access.fs.remove(temporary).pipe(Effect.ignore);
13
+ yield* access.fs.write(temporary, content, { createParents: !0 });
14
+ if (!(yield* Option.match(mode, {
15
+ onNone: () => Effect.succeed(!0),
16
+ onSome: (kept) => access.fs.stat(temporary).pipe(Effect.map((fresh) => Option.contains(fresh.mode, kept)), Effect.tapError(() => discard))
17
+ })))
18
+ return yield* discard.pipe(Effect.andThen(inPlace));
19
+ yield* access.fs.rename(temporary, target.path).pipe(Effect.tapError(() => discard));
20
+ });
21
+ return (Option.exists(mode, (bits) => (bits & notForNewFiles) !== 0) ? inPlace : replaced).pipe(Effect.mapError(fsFailure(target.display)));
22
+ };
23
+ export const writeTool = (context) => defineTool({
24
+ tool: WriteTool(toolDescriptions(context.config).write),
25
+ handler: (params) => Effect.gen(function* () {
26
+ const access = yield* openWorkspace(context.path), target = yield* resolveTarget(access, params.path, { confine: context.config.confine });
27
+ yield* checkWritable(access, target, context.protect);
28
+ return yield* holding(context, target.path)(Effect.gen(function* () {
29
+ const before = yield* existing(access, target);
30
+ if (Option.exists(before, (info) => info.kind === "directory"))
31
+ return yield* Effect.fail(failure("IsDirectory", `${target.display} is a directory`));
32
+ if (Option.exists(before, (info) => info.kind !== "file"))
33
+ return yield* Effect.fail(notAFile(target.display, "write"));
34
+ if (params.content.length === 0 && Option.exists(before, (info) => info.size > 0))
35
+ return yield* Effect.fail(failure("WouldEmpty", `${target.display} has content; write refuses to empty it. Use edit to remove parts of it, or bash to delete it`));
36
+ yield* save(access, target, params.content, before);
37
+ return { path: target.display, bytes: utf8Length(params.content), created: Option.isNone(before) };
38
+ }));
39
+ }),
40
+ view: { version: "1", render: renderWrite, compact: compactWrite, subjects: (result) => fileSubjects(result.path) },
41
+ annotations: { permissions: [WORKSPACE_PERMISSIONS.write] }
42
+ }), editTool = (context) => defineTool({
43
+ tool: EditTool(toolDescriptions(context.config).edit),
44
+ handler: (params) => Effect.gen(function* () {
45
+ const { config } = context, access = yield* openWorkspace(context.path), target = yield* resolveTarget(access, params.path, { confine: config.confine });
46
+ yield* checkWritable(access, target, context.protect);
47
+ return yield* holding(context, target.path)(Effect.gen(function* () {
48
+ const before = yield* existing(access, target), info = yield* Option.match(before, {
49
+ onNone: () => Effect.fail(failure("NotFound", `${target.display} does not exist; use write to create it`)),
50
+ onSome: Effect.succeed
51
+ });
52
+ if (info.kind === "directory")
53
+ return yield* Effect.fail(failure("IsDirectory", `${target.display} is a directory`));
54
+ if (info.kind !== "file")
55
+ return yield* Effect.fail(notAFile(target.display, "edit"));
56
+ if (info.size > config.maxFileBytes)
57
+ return yield* Effect.fail(failure("FileTooLarge", `${target.display} is ${formatBytes(info.size)}, more than the ${formatBytes(config.maxFileBytes)} edit loads`));
58
+ const bytes = yield* access.fs.readBytes(target.path).pipe(Effect.mapError(fsFailure(target.display)));
59
+ if (looksBinary(bytes))
60
+ return yield* Effect.fail(failure("Binary", `${target.display} is a binary file; edit changes text files only`));
61
+ const decoded = decodeText(bytes);
62
+ if (decoded.lossy)
63
+ return yield* Effect.fail(failure("NotUtf8", `${target.display} is not valid UTF-8 (Latin-1 or another encoding, say); edit changes UTF-8 text files only, since it would replace the bytes it cannot decode. Use bash (sed, iconv) to change it`));
64
+ const plan = yield* Effect.fromResult(applyTextEdits(decoded.text, params.edits, { fuzzy: config.fuzzyEdit }));
65
+ yield* save(access, target, encodeText({ text: plan.text, bom: decoded.bom }), before);
66
+ return {
67
+ path: target.display,
68
+ replaced: plan.replaced,
69
+ normalized: plan.normalized,
70
+ firstChangedLine: plan.firstChangedLine,
71
+ diff: unifiedDiff(target.display, plan.before, plan.after, config.maxBytes)
72
+ };
73
+ }));
74
+ }),
75
+ view: { version: "1", render: renderEdit, compact: compactEdit, subjects: (result) => fileSubjects(result.path) },
76
+ annotations: { permissions: [WORKSPACE_PERMISSIONS.write] }
77
+ });
package/package.json CHANGED
@@ -1,6 +1,31 @@
1
1
  {
2
2
  "name": "@xandreed/plugin-workspace",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.14.0",
4
+ "engines": {
5
+ "node": ">=24",
6
+ "bun": ">=1.3.0"
7
+ },
8
+ "type": "module",
9
+ "description": "The local machine as the FileSystem and Bash ports (node:fs and node:child_process adapters and the plugin that provides them), the workspace tools an agent works with (read, write, edit, ls, find, grep and bash) and skills from directories of SKILL.md files (Node 24 and Bun, on Linux and macOS).",
10
+ "main": "./dist/index.js",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "default": "./dist/index.js"
15
+ }
16
+ },
17
+ "dependencies": {
18
+ "@xandreed/core": "0.14.0",
19
+ "effect": "4.0.7"
20
+ },
21
+ "private": false,
22
+ "types": "./dist/index.d.ts",
23
+ "files": [
24
+ "dist",
25
+ "skills",
26
+ "profiles",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "license": "MIT"
31
+ }