@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,433 @@
1
+ import { Schema } from "effect";
2
+ /** The seven workspace tools, by the names the model calls them. */
3
+ export declare const WorkspaceToolName: Schema.Literals<readonly ["read", "write", "edit", "ls", "find", "grep", "bash"]>;
4
+ export type WorkspaceToolName = typeof WorkspaceToolName.Type;
5
+ /**
6
+ * The tools' three skills, by the short names `always` takes: `read`
7
+ * (`workspace.read`: read, ls, find, grep), `edit` (`workspace.edit`: write,
8
+ * edit) and `shell` (`workspace.shell`: bash).
9
+ */
10
+ export declare const WorkspaceSkillName: Schema.Literals<readonly ["read", "edit", "shell"]>;
11
+ export type WorkspaceSkillName = typeof WorkspaceSkillName.Type;
12
+ /** The grants the tools that change things need: `write` and `edit` need `fs.write`, `bash` needs `shell.exec`. */
13
+ export declare const WORKSPACE_PERMISSIONS: {
14
+ readonly write: "fs.write";
15
+ readonly exec: "shell.exec";
16
+ };
17
+ /**
18
+ * How `workspaceToolsPlugin` (and `workspaceTools`) works on the workspace,
19
+ * `SessionEnvironment.workspace`. Flat: plugin defaults merge one level deep.
20
+ *
21
+ * - `tools`: the tools to register (all seven by default). A skill whose
22
+ * tools are all left out is left out too.
23
+ * - `always`: the skills active from a turn's first step: `read` by default.
24
+ * `edit` and `shell` here need their grants (`fs.write`, `shell.exec`), or
25
+ * the turn fails with `capability.forbidden`; otherwise the model loads
26
+ * them with `load_skill`, which refuses them without the grants.
27
+ * - `confine`: refuse paths whose real location (links resolved, through the
28
+ * nearest ancestor that exists) is outside the workspace root.
29
+ * - `protect`: globs, relative to the root, that `write` and `edit` refuse.
30
+ * - `exclude`: names (or, with a `/`, paths relative to the root) `find`
31
+ * and `grep` never enter below the directory they search. Both lists are
32
+ * checked with `compileGlob` when the options are decoded.
33
+ * - `maxLines`, `maxBytes`: the most `read`, `find` and `grep` show at once,
34
+ * and the tail of a command's output `bash` keeps.
35
+ * - `lsEntries`, `findResults`, `grepMatches`: the most entries, paths and
36
+ * matching lines the model gets (a call may ask for fewer).
37
+ * - `grepLineChars`: lines are cut to this many characters before `grep`
38
+ * matches them, and shown cut.
39
+ * - `maxFileBytes`: the largest file `read`, `edit` and `grep` load, and
40
+ * the most of a command's output `bash` saves.
41
+ * - `readImages`: `read` returns a PNG, JPEG, GIF or WebP image (by its
42
+ * bytes, whatever its name) as an image the model is sent; `false`
43
+ * reports it as a binary file, as any other. A host whose models read
44
+ * text only (a route without image parts, such as an endpoint without
45
+ * `images`, or memory with `maxMediaChars: 0`) sets `false`: the model
46
+ * would get a label line in the image's place, and the session would
47
+ * still store the bytes.
48
+ * - `fuzzyEdit`: when an edit's text is not found, match it once more over
49
+ * whole lines with whitespace, Unicode compatibility forms and smart
50
+ * quotes normalised (the match must still be unique).
51
+ * - `bashTimeoutMs`, `bashMaxTimeoutMs`: a command's timeout unless the
52
+ * model asks for another, and the most it may ask for.
53
+ * - `bashEnv`: variables every command gets. `bashInheritEnv: true` passes
54
+ * the host's whole environment (provider keys included); otherwise only
55
+ * the names in `bashEnvAllow` (a trailing `*` matches a prefix) pass.
56
+ * `bashSessionEnv` adds `EFFERENT_SESSION_ID`, `EFFERENT_RUN_ID` and
57
+ * `EFFERENT_WORKSPACE`.
58
+ * - `spillDir`: where a command's whole output (its first `maxFileBytes`)
59
+ * is saved once it is longer than `bash` shows (relative to the root;
60
+ * `null` saves nothing), keeping the newest `spillKeep` files.
61
+ * - `progressMs`: how long output is gathered before it is published, as
62
+ * one `tool.progress` event per run of output from one stream.
63
+ * - `environmentSection`, `sectionOrder`: the `workspace.environment`
64
+ * system-prompt section (tier `session`) and its order.
65
+ */
66
+ export declare const WorkspaceToolsConfig: Schema.Struct<{
67
+ readonly tools: Schema.$Array<Schema.Literals<readonly ["read", "write", "edit", "ls", "find", "grep", "bash"]>>;
68
+ readonly always: Schema.$Array<Schema.Literals<readonly ["read", "edit", "shell"]>>;
69
+ readonly confine: Schema.Boolean;
70
+ readonly protect: Schema.$Array<Schema.String>;
71
+ readonly exclude: Schema.$Array<Schema.String>;
72
+ readonly maxLines: Schema.Int;
73
+ readonly maxBytes: Schema.Int;
74
+ readonly lsEntries: Schema.Int;
75
+ readonly findResults: Schema.Int;
76
+ readonly grepMatches: Schema.Int;
77
+ readonly grepLineChars: Schema.Int;
78
+ readonly maxFileBytes: Schema.Int;
79
+ readonly readImages: Schema.Boolean;
80
+ readonly fuzzyEdit: Schema.Boolean;
81
+ readonly bashTimeoutMs: Schema.Int;
82
+ readonly bashMaxTimeoutMs: Schema.Int;
83
+ readonly bashEnv: Schema.$Record<Schema.String, Schema.String>;
84
+ readonly bashInheritEnv: Schema.Boolean;
85
+ readonly bashEnvAllow: Schema.$Array<Schema.String>;
86
+ readonly bashSessionEnv: Schema.Boolean;
87
+ readonly spillDir: Schema.NullOr<Schema.String>;
88
+ readonly spillKeep: Schema.Int;
89
+ readonly progressMs: Schema.Int;
90
+ readonly environmentSection: Schema.Boolean;
91
+ readonly sectionOrder: Schema.Int;
92
+ }>;
93
+ export type WorkspaceToolsConfig = typeof WorkspaceToolsConfig.Type;
94
+ export declare const workspaceToolsDefaults: WorkspaceToolsConfig;
95
+ /** `read`: a file's lines from `offset` (1-based, default 1), at most `limit` of them (and never more than the configured window). */
96
+ export declare const ReadParams: Schema.Struct<{
97
+ readonly path: Schema.String;
98
+ readonly offset: Schema.optionalKey<Schema.Int>;
99
+ readonly limit: Schema.optionalKey<Schema.Int>;
100
+ }>;
101
+ export type ReadParams = typeof ReadParams.Type;
102
+ /**
103
+ * What `read` found: a window of a text file's lines (`next` is the offset
104
+ * to continue from when lines are left; `clipped` when the first line alone
105
+ * was longer than the window's bytes and only its start is shown; `lossy`
106
+ * when the file is not valid UTF-8 and the bytes that are not show as
107
+ * U+FFFD, which `edit` refuses), or a binary file (a NUL byte in its first
108
+ * 8 KB). With `readImages`, a binary file whose bytes are a PNG, JPEG, GIF
109
+ * or WebP image is an `Image` (core's `mediaFromBytes`: the type its bytes
110
+ * are, its size and fingerprint), which the view sends the model as an
111
+ * image of the result; one core refuses (over `MEDIA_LIMITS.imageBytes`,
112
+ * over `MEDIA_LIMITS.maxPixels` a side, or a header without a size) is an
113
+ * `ImageRejected` note: its type, size and why. Any other binary file is
114
+ * `Binary`, with the media type its name suggests.
115
+ */
116
+ export declare const ReadResult: Schema.Union<readonly [Schema.TaggedStruct<"Text", {
117
+ readonly path: Schema.String;
118
+ readonly text: Schema.String;
119
+ readonly firstLine: Schema.Int;
120
+ readonly lastLine: Schema.Int;
121
+ readonly totalLines: Schema.Int;
122
+ readonly next: Schema.NullOr<Schema.Int>;
123
+ readonly clipped: Schema.Boolean;
124
+ readonly lossy: Schema.Boolean;
125
+ }>, Schema.TaggedStruct<"Binary", {
126
+ readonly path: Schema.String;
127
+ readonly bytes: Schema.Int;
128
+ readonly mediaType: Schema.NullOr<Schema.String>;
129
+ }>, Schema.TaggedStruct<"Image", {
130
+ readonly path: Schema.String;
131
+ readonly image: Schema.Struct<{
132
+ readonly type: Schema.Literal<"media">;
133
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
134
+ readonly data: Schema.String;
135
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
136
+ readonly bytes: Schema.Int;
137
+ readonly name: Schema.optionalKey<Schema.String>;
138
+ readonly alt: Schema.optionalKey<Schema.String>;
139
+ }>;
140
+ }>, Schema.TaggedStruct<"ImageRejected", {
141
+ readonly path: Schema.String;
142
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
143
+ readonly bytes: Schema.Int;
144
+ readonly reason: Schema.Literals<readonly ["Empty", "InvalidBase64", "UnsupportedType", "TooLarge", "TooManyPixels", "Unreadable", "TooMany", "TooLargeTogether"]>;
145
+ readonly message: Schema.String;
146
+ }>]>;
147
+ export type ReadResult = typeof ReadResult.Type;
148
+ export declare const WriteParams: Schema.Struct<{
149
+ readonly path: Schema.String;
150
+ readonly content: Schema.String;
151
+ }>;
152
+ export type WriteParams = typeof WriteParams.Type;
153
+ /** What `write` did: how many bytes it wrote, and whether the file is new. */
154
+ export declare const WriteResult: Schema.Struct<{
155
+ readonly path: Schema.String;
156
+ readonly bytes: Schema.Int;
157
+ readonly created: Schema.Boolean;
158
+ }>;
159
+ export type WriteResult = typeof WriteResult.Type;
160
+ /** One replacement: `oldText` (exactly once in the file) becomes `newText`. */
161
+ export declare const TextEdit: Schema.Struct<{
162
+ readonly oldText: Schema.String;
163
+ readonly newText: Schema.String;
164
+ }>;
165
+ export type TextEdit = typeof TextEdit.Type;
166
+ export declare const EditParams: Schema.Struct<{
167
+ readonly path: Schema.String;
168
+ readonly edits: Schema.NonEmptyArray<Schema.Struct<{
169
+ readonly oldText: Schema.String;
170
+ readonly newText: Schema.String;
171
+ }>>;
172
+ }>;
173
+ export type EditParams = typeof EditParams.Type;
174
+ /**
175
+ * What `edit` did: how many blocks it replaced, whether any was found only
176
+ * after normalising, the first changed line, and the change as a unified
177
+ * diff (for a host's UI; the model is shown a one-line summary).
178
+ */
179
+ export declare const EditResult: Schema.Struct<{
180
+ readonly path: Schema.String;
181
+ readonly replaced: Schema.Int;
182
+ readonly normalized: Schema.Boolean;
183
+ readonly firstChangedLine: Schema.Int;
184
+ readonly diff: Schema.String;
185
+ }>;
186
+ export type EditResult = typeof EditResult.Type;
187
+ export declare const LsParams: Schema.Struct<{
188
+ readonly path: Schema.optionalKey<Schema.String>;
189
+ readonly limit: Schema.optionalKey<Schema.Int>;
190
+ }>;
191
+ export type LsParams = typeof LsParams.Type;
192
+ export declare const LsEntry: Schema.Struct<{
193
+ readonly name: Schema.String;
194
+ readonly kind: Schema.Literals<readonly ["file", "directory", "symlink", "other"]>;
195
+ }>;
196
+ export type LsEntry = typeof LsEntry.Type;
197
+ /** A directory's entries by name, at most the limit of them; `total` counts them all. */
198
+ export declare const LsResult: Schema.Struct<{
199
+ readonly path: Schema.String;
200
+ readonly entries: Schema.$Array<Schema.Struct<{
201
+ readonly name: Schema.String;
202
+ readonly kind: Schema.Literals<readonly ["file", "directory", "symlink", "other"]>;
203
+ }>>;
204
+ readonly total: Schema.Int;
205
+ }>;
206
+ export type LsResult = typeof LsResult.Type;
207
+ export declare const FindParams: Schema.Struct<{
208
+ readonly pattern: Schema.String;
209
+ readonly path: Schema.optionalKey<Schema.String>;
210
+ readonly limit: Schema.optionalKey<Schema.Int>;
211
+ }>;
212
+ export type FindParams = typeof FindParams.Type;
213
+ /** The files a glob matched, as paths relative to the workspace root; `truncated` when more matched. */
214
+ export declare const FindResult: Schema.Struct<{
215
+ readonly pattern: Schema.String;
216
+ readonly path: Schema.String;
217
+ readonly paths: Schema.$Array<Schema.String>;
218
+ readonly truncated: Schema.Boolean;
219
+ }>;
220
+ export type FindResult = typeof FindResult.Type;
221
+ export declare const GrepParams: Schema.Struct<{
222
+ readonly pattern: Schema.String;
223
+ readonly path: Schema.optionalKey<Schema.String>;
224
+ readonly glob: Schema.optionalKey<Schema.String>;
225
+ readonly ignoreCase: Schema.optionalKey<Schema.Boolean>;
226
+ readonly literal: Schema.optionalKey<Schema.Boolean>;
227
+ readonly context: Schema.optionalKey<Schema.Int>;
228
+ readonly limit: Schema.optionalKey<Schema.Int>;
229
+ }>;
230
+ export type GrepParams = typeof GrepParams.Type;
231
+ /** One line `grep` shows: a matching line, or (`match: false`) a line of context around one. `text` is cut to the configured width. */
232
+ export declare const GrepLine: Schema.Struct<{
233
+ readonly path: Schema.String;
234
+ readonly line: Schema.Int;
235
+ readonly text: Schema.String;
236
+ readonly match: Schema.Boolean;
237
+ }>;
238
+ export type GrepLine = typeof GrepLine.Type;
239
+ /**
240
+ * What `grep` found: the matching lines (with their context) in walk order,
241
+ * how many lines matched and in how many files, whether it stopped at the
242
+ * limit, and how many files it skipped (binary, larger than `maxFileBytes`,
243
+ * or resolving outside the workspace).
244
+ */
245
+ export declare const GrepResult: Schema.Struct<{
246
+ readonly pattern: Schema.String;
247
+ readonly path: Schema.String;
248
+ readonly lines: Schema.$Array<Schema.Struct<{
249
+ readonly path: Schema.String;
250
+ readonly line: Schema.Int;
251
+ readonly text: Schema.String;
252
+ readonly match: Schema.Boolean;
253
+ }>>;
254
+ readonly matches: Schema.Int;
255
+ readonly files: Schema.Int;
256
+ readonly truncated: Schema.Boolean;
257
+ readonly skipped: Schema.Int;
258
+ }>;
259
+ export type GrepResult = typeof GrepResult.Type;
260
+ /** `bash`: the command, its timeout in seconds, and what to write to its standard input. */
261
+ export declare const BashParams: Schema.Struct<{
262
+ readonly command: Schema.String;
263
+ readonly timeout: Schema.optionalKey<Schema.Int>;
264
+ readonly stdin: Schema.optionalKey<Schema.String>;
265
+ }>;
266
+ export type BashParams = typeof BashParams.Type;
267
+ /**
268
+ * How a command went: the tail of its output (stdout and stderr in the
269
+ * order they arrived), how it ended, and, when the output was longer than
270
+ * the tail, where the whole of it was saved (`fullOutput`, relative to the
271
+ * workspace root; null when nothing was saved). The file holds at most
272
+ * `maxFileBytes` of the output, its start: `fullOutputCut` when there was
273
+ * more, which was not saved.
274
+ */
275
+ export declare const BashToolResult: Schema.Struct<{
276
+ readonly command: Schema.String;
277
+ readonly output: Schema.String;
278
+ readonly exitCode: Schema.NullOr<Schema.Int>;
279
+ readonly signal: Schema.NullOr<Schema.String>;
280
+ readonly termination: Schema.Literals<readonly ["exit", "signal", "timeout", "interrupted"]>;
281
+ readonly truncated: Schema.Boolean;
282
+ readonly totalLines: Schema.Int;
283
+ readonly fullOutput: Schema.NullOr<Schema.String>;
284
+ readonly fullOutputCut: Schema.Boolean;
285
+ readonly durationMs: Schema.Number;
286
+ }>;
287
+ export type BashToolResult = typeof BashToolResult.Type;
288
+ /** The tools' names and schemas, for `onTool` subscriptions (a UI showing `edit` diffs, say). */
289
+ export declare const workspaceToolSchemas: {
290
+ readonly read: {
291
+ readonly name: "read";
292
+ readonly parametersSchema: Schema.Struct<{
293
+ readonly path: Schema.String;
294
+ readonly offset: Schema.optionalKey<Schema.Int>;
295
+ readonly limit: Schema.optionalKey<Schema.Int>;
296
+ }>;
297
+ readonly successSchema: Schema.Union<readonly [Schema.TaggedStruct<"Text", {
298
+ readonly path: Schema.String;
299
+ readonly text: Schema.String;
300
+ readonly firstLine: Schema.Int;
301
+ readonly lastLine: Schema.Int;
302
+ readonly totalLines: Schema.Int;
303
+ readonly next: Schema.NullOr<Schema.Int>;
304
+ readonly clipped: Schema.Boolean;
305
+ readonly lossy: Schema.Boolean;
306
+ }>, Schema.TaggedStruct<"Binary", {
307
+ readonly path: Schema.String;
308
+ readonly bytes: Schema.Int;
309
+ readonly mediaType: Schema.NullOr<Schema.String>;
310
+ }>, Schema.TaggedStruct<"Image", {
311
+ readonly path: Schema.String;
312
+ readonly image: Schema.Struct<{
313
+ readonly type: Schema.Literal<"media">;
314
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
315
+ readonly data: Schema.String;
316
+ readonly fingerprint: Schema.brand<Schema.String, "MediaFingerprint">;
317
+ readonly bytes: Schema.Int;
318
+ readonly name: Schema.optionalKey<Schema.String>;
319
+ readonly alt: Schema.optionalKey<Schema.String>;
320
+ }>;
321
+ }>, Schema.TaggedStruct<"ImageRejected", {
322
+ readonly path: Schema.String;
323
+ readonly mediaType: Schema.Literals<readonly ["image/png", "image/jpeg", "image/gif", "image/webp"]>;
324
+ readonly bytes: Schema.Int;
325
+ readonly reason: Schema.Literals<readonly ["Empty", "InvalidBase64", "UnsupportedType", "TooLarge", "TooManyPixels", "Unreadable", "TooMany", "TooLargeTogether"]>;
326
+ readonly message: Schema.String;
327
+ }>]>;
328
+ };
329
+ readonly write: {
330
+ readonly name: "write";
331
+ readonly parametersSchema: Schema.Struct<{
332
+ readonly path: Schema.String;
333
+ readonly content: Schema.String;
334
+ }>;
335
+ readonly successSchema: Schema.Struct<{
336
+ readonly path: Schema.String;
337
+ readonly bytes: Schema.Int;
338
+ readonly created: Schema.Boolean;
339
+ }>;
340
+ };
341
+ readonly edit: {
342
+ readonly name: "edit";
343
+ readonly parametersSchema: Schema.Struct<{
344
+ readonly path: Schema.String;
345
+ readonly edits: Schema.NonEmptyArray<Schema.Struct<{
346
+ readonly oldText: Schema.String;
347
+ readonly newText: Schema.String;
348
+ }>>;
349
+ }>;
350
+ readonly successSchema: Schema.Struct<{
351
+ readonly path: Schema.String;
352
+ readonly replaced: Schema.Int;
353
+ readonly normalized: Schema.Boolean;
354
+ readonly firstChangedLine: Schema.Int;
355
+ readonly diff: Schema.String;
356
+ }>;
357
+ };
358
+ readonly ls: {
359
+ readonly name: "ls";
360
+ readonly parametersSchema: Schema.Struct<{
361
+ readonly path: Schema.optionalKey<Schema.String>;
362
+ readonly limit: Schema.optionalKey<Schema.Int>;
363
+ }>;
364
+ readonly successSchema: Schema.Struct<{
365
+ readonly path: Schema.String;
366
+ readonly entries: Schema.$Array<Schema.Struct<{
367
+ readonly name: Schema.String;
368
+ readonly kind: Schema.Literals<readonly ["file", "directory", "symlink", "other"]>;
369
+ }>>;
370
+ readonly total: Schema.Int;
371
+ }>;
372
+ };
373
+ readonly find: {
374
+ readonly name: "find";
375
+ readonly parametersSchema: Schema.Struct<{
376
+ readonly pattern: Schema.String;
377
+ readonly path: Schema.optionalKey<Schema.String>;
378
+ readonly limit: Schema.optionalKey<Schema.Int>;
379
+ }>;
380
+ readonly successSchema: Schema.Struct<{
381
+ readonly pattern: Schema.String;
382
+ readonly path: Schema.String;
383
+ readonly paths: Schema.$Array<Schema.String>;
384
+ readonly truncated: Schema.Boolean;
385
+ }>;
386
+ };
387
+ readonly grep: {
388
+ readonly name: "grep";
389
+ readonly parametersSchema: Schema.Struct<{
390
+ readonly pattern: Schema.String;
391
+ readonly path: Schema.optionalKey<Schema.String>;
392
+ readonly glob: Schema.optionalKey<Schema.String>;
393
+ readonly ignoreCase: Schema.optionalKey<Schema.Boolean>;
394
+ readonly literal: Schema.optionalKey<Schema.Boolean>;
395
+ readonly context: Schema.optionalKey<Schema.Int>;
396
+ readonly limit: Schema.optionalKey<Schema.Int>;
397
+ }>;
398
+ readonly successSchema: Schema.Struct<{
399
+ readonly pattern: Schema.String;
400
+ readonly path: Schema.String;
401
+ readonly lines: Schema.$Array<Schema.Struct<{
402
+ readonly path: Schema.String;
403
+ readonly line: Schema.Int;
404
+ readonly text: Schema.String;
405
+ readonly match: Schema.Boolean;
406
+ }>>;
407
+ readonly matches: Schema.Int;
408
+ readonly files: Schema.Int;
409
+ readonly truncated: Schema.Boolean;
410
+ readonly skipped: Schema.Int;
411
+ }>;
412
+ };
413
+ readonly bash: {
414
+ readonly name: "bash";
415
+ readonly parametersSchema: Schema.Struct<{
416
+ readonly command: Schema.String;
417
+ readonly timeout: Schema.optionalKey<Schema.Int>;
418
+ readonly stdin: Schema.optionalKey<Schema.String>;
419
+ }>;
420
+ readonly successSchema: Schema.Struct<{
421
+ readonly command: Schema.String;
422
+ readonly output: Schema.String;
423
+ readonly exitCode: Schema.NullOr<Schema.Int>;
424
+ readonly signal: Schema.NullOr<Schema.String>;
425
+ readonly termination: Schema.Literals<readonly ["exit", "signal", "timeout", "interrupted"]>;
426
+ readonly truncated: Schema.Boolean;
427
+ readonly totalLines: Schema.Int;
428
+ readonly fullOutput: Schema.NullOr<Schema.String>;
429
+ readonly fullOutputCut: Schema.Boolean;
430
+ readonly durationMs: Schema.Number;
431
+ }>;
432
+ };
433
+ };
@@ -0,0 +1,51 @@
1
+ import { Effect, Option } from "effect";
2
+ import type { BashOptions, Media } from "@xandreed/core";
3
+ import type { BashToolResult, EditResult, FindResult, GrepResult, LsResult, ReadResult, WorkspaceSkillName, WorkspaceToolName, WorkspaceToolsConfig, WriteResult } from "./workspace-tools.entity.js";
4
+ /** The skill each tool belongs to, by its short name. */
5
+ export declare const skillOfTool: Readonly<Record<WorkspaceToolName, WorkspaceSkillName>>;
6
+ /** What the model is told each tool does, with the configured limits in it. */
7
+ export declare const toolDescriptions: (config: WorkspaceToolsConfig) => Readonly<Record<WorkspaceToolName, string>>;
8
+ /** The text of the `workspace.environment` section: where the workspace is and how the enabled tools behave there. */
9
+ export declare const environmentText: (root: string, config: WorkspaceToolsConfig, tools: ReadonlyArray<WorkspaceToolName>) => string;
10
+ /**
11
+ * The environment of a command the bash tool runs: the configured
12
+ * variables, the session's when `bashSessionEnv` is set, and the parent
13
+ * environment through the allowlist (or all of it with `bashInheritEnv`).
14
+ */
15
+ export declare const bashEnvironment: (config: WorkspaceToolsConfig, session: Option.Option<{
16
+ readonly sessionId: string;
17
+ readonly runId: string;
18
+ readonly workspace: string;
19
+ }>) => Pick<BashOptions, "env" | "inheritEnv">;
20
+ /**
21
+ * What `read` returns for a binary file (a NUL byte in its first 8 KB).
22
+ * With `readImages`, bytes that are a PNG, JPEG, GIF or WebP image (sniffed,
23
+ * whatever the name says) become an `Image` through `mediaFromBytes`, named
24
+ * by the path; an image it refuses (over `MEDIA_LIMITS.imageBytes`, over
25
+ * `MEDIA_LIMITS.maxPixels` a side, or a header without a size) is an
26
+ * `ImageRejected` note, without the bytes. Anything else is `Binary`, with
27
+ * the media type the name suggests.
28
+ */
29
+ export declare const binaryRead: (path: string, bytes: Uint8Array, readImages: boolean) => Effect.Effect<ReadResult>;
30
+ /**
31
+ * What the model can do about a file over `maxFileBytes`, which `read`
32
+ * refuses unread: with `readImages`, a file named as a PNG, JPEG, GIF or
33
+ * WebP image is told to read a smaller copy; any other to read parts of it.
34
+ */
35
+ export declare const fileTooLargeHint: (path: string, readImages: boolean) => string;
36
+ /** The images `read`'s view sends with its result: an `Image`'s, once. */
37
+ export declare const readMedia: (result: ReadResult) => ReadonlyArray<Media>;
38
+ export declare const renderRead: (result: ReadResult) => string;
39
+ export declare const compactRead: (result: ReadResult) => string;
40
+ export declare const renderWrite: (result: WriteResult) => string;
41
+ export declare const compactWrite: (result: WriteResult) => string;
42
+ export declare const renderEdit: (result: EditResult) => string;
43
+ export declare const compactEdit: (result: EditResult) => string;
44
+ export declare const renderLs: (result: LsResult) => string;
45
+ export declare const compactLs: (result: LsResult) => string;
46
+ export declare const renderFind: (result: FindResult, maxLines: number, maxBytes: number) => string;
47
+ export declare const compactFind: (result: FindResult) => string;
48
+ export declare const renderGrep: (result: GrepResult, withContext: boolean, maxLines: number, maxBytes: number) => string;
49
+ export declare const compactGrep: (result: GrepResult) => string;
50
+ export declare const renderBash: (result: BashToolResult) => string;
51
+ export declare const compactBash: (result: BashToolResult) => string;
@@ -0,0 +1,124 @@
1
+ import { Effect, Match, Option, Schema } from "effect";
2
+ import { ImageMediaType, MEDIA_LIMITS, mediaFromBytes, sniffImageType } from "@xandreed/core";
3
+ import { renderSearchLines } from "./text-search.entity.functions.js";
4
+ import { formatBytes, headLines, mediaTypeOf } from "./text-window.entity.functions.js";
5
+ export const skillOfTool = {
6
+ read: "read",
7
+ ls: "read",
8
+ find: "read",
9
+ grep: "read",
10
+ write: "edit",
11
+ edit: "edit",
12
+ bash: "shell"
13
+ };
14
+ const seconds = (ms) => `${Math.round(ms / 1000)} s`, imageLimit = (config) => {
15
+ const bytes = Math.min(MEDIA_LIMITS.imageBytes, config.maxFileBytes);
16
+ return bytes < 1048576 ? `${Math.floor(bytes * 100 / 1024) / 100} KB` : `${Math.floor(bytes * 100 / 1048576) / 100} MB`;
17
+ }, listed = (names) => names.length === 0 ? "nothing" : names.join(", ");
18
+ export const toolDescriptions = (config) => ({
19
+ read: `Read a text file of the workspace${config.readImages ? ", or look at an image" : ""}. Shows at most ${config.maxLines} lines or ${formatBytes(config.maxBytes)} at a time; when more is left, the result says which offset to continue from. offset is the first line to show (1-based), limit how many lines. ${config.readImages ? `A PNG, JPEG, GIF or WebP image (up to ${imageLimit(config)} and ${MEDIA_LIMITS.maxPixels} pixels a side) is returned as an image you can see; another binary file is reported, not shown.` : "A binary file is reported, not shown."} To see a directory, use ls.`,
20
+ write: "Create a file, or replace all of an existing one, with content; missing directories are created. To change part of a file, use edit. Emptying a non-empty file is refused.",
21
+ edit: "Change part of a file by replacing exact text. Each edit's oldText must appear exactly once in the file: copy it from read (without the notice lines), with enough surrounding lines to be unique. All edits are matched against the file as it was before this call and must not overlap; newText replaces oldText (empty to delete it). Line endings and a byte-order mark are kept.",
22
+ ls: `List a directory (by default the workspace root): one entry per line, directories ending in /, symbolic links in @. At most ${config.lsEntries} entries.`,
23
+ find: `Find files whose path matches a glob pattern, relative to path (by default the workspace root): * and ? match within a name, ** any number of directories, {a,b} either; e.g. **/*.ts or src/**/test_*.py. Never looks inside ${listed(config.exclude)} or symbolic links. At most ${config.findResults} paths.`,
24
+ grep: `Search file contents for a regular expression (JavaScript syntax), or for plain text with literal: true. path is a file or a directory (by default the workspace root); glob limits the files searched (*.ts searches every directory). Shows path:line:text for each matching line, and context lines around each match when asked. Lines are cut to ${config.grepLineChars} characters before matching. At most ${config.grepMatches} matching lines; binary files, ${listed(config.exclude)} and symbolic links are skipped.`,
25
+ bash: `Run a shell command with bash -c in the workspace root. timeout is in seconds (default ${seconds(config.bashTimeoutMs)}, at most ${seconds(config.bashMaxTimeoutMs)}); stdin is written to the command's standard input. Returns the exit code and the output, stdout and stderr in the order they came. Output longer than ${config.maxLines} lines or ${formatBytes(config.maxBytes)} is cut to its end${config.spillDir === null ? "" : `, and the whole of it (up to ${formatBytes(config.maxFileBytes)}) is saved to a file you can read`}. A non-zero exit code is a result to read, not a failure. Anything the command leaves running may be stopped when it returns.`
26
+ }), environmentText = (root, config, tools) => {
27
+ const has = (tool) => tools.includes(tool);
28
+ return [
29
+ "## Workspace",
30
+ `The workspace is ${root}. Give paths relative to it${config.confine ? "; the tools refuse paths that lead outside it." : "."}`,
31
+ ...has("read") ? [`read shows a file ${config.maxLines} lines or ${formatBytes(config.maxBytes)} at a time; continue with the offset it gives.`] : [],
32
+ ...has("find") || has("grep") ? [`find and grep never look inside ${listed(config.exclude)}.`] : [],
33
+ ...has("edit") ? ["edit replaces exact text: read the file first and copy the text to change from it."] : [],
34
+ ...has("write") || has("edit") ? [config.protect.length === 0 ? "" : `write and edit leave ${listed(config.protect)} alone.`] : [],
35
+ ...has("bash") ? [`bash runs commands in the workspace root with a ${seconds(config.bashTimeoutMs)} timeout.${config.spillDir === null ? "" : " When their output is cut, the whole of it is saved to a file under " + config.spillDir + " that read can open."}`] : []
36
+ ].filter((line) => line.length > 0).join(`
37
+ `);
38
+ }, bashEnvironment = (config, session) => ({
39
+ env: {
40
+ ...config.bashSessionEnv ? Option.match(session, {
41
+ onNone: () => ({}),
42
+ onSome: (run) => ({ EFFERENT_SESSION_ID: run.sessionId, EFFERENT_RUN_ID: run.runId, EFFERENT_WORKSPACE: run.workspace })
43
+ }) : {},
44
+ ...config.bashEnv
45
+ },
46
+ inheritEnv: config.bashInheritEnv ? !0 : config.bashEnvAllow
47
+ }), binaryRead = (path, bytes, readImages) => Option.match(readImages ? sniffImageType(bytes) : Option.none(), {
48
+ onNone: () => Effect.succeed({ _tag: "Binary", path, bytes: bytes.length, mediaType: Option.getOrNull(mediaTypeOf(path)) }),
49
+ onSome: (mediaType) => mediaFromBytes(bytes, { name: path }).pipe(Effect.match({
50
+ onSuccess: (image) => ({ _tag: "Image", path, image }),
51
+ onFailure: (rejected) => ({ _tag: "ImageRejected", path, mediaType, bytes: bytes.length, reason: rejected.reason, message: rejected.message })
52
+ }))
53
+ }), fileTooLargeHint = (path, readImages) => readImages && Option.exists(mediaTypeOf(path), Schema.is(ImageMediaType)) ? "to see it, make a smaller copy with bash (scaled down or recompressed) and read that" : "use grep to find lines in it, or bash (head, tail, sed -n) for parts of it", readMedia = (result) => Match.valueTags(result, {
54
+ Text: () => [],
55
+ Binary: () => [],
56
+ Image: (read) => [read.image],
57
+ ImageRejected: () => []
58
+ });
59
+ const rejectedHint = {
60
+ TooLarge: " To see it, make a smaller copy with bash (scaled down or recompressed) and read that.",
61
+ TooManyPixels: " To see it, make a smaller copy with bash (scaled down) and read that.",
62
+ Unreadable: " The file may be damaged or cut short.",
63
+ Empty: "",
64
+ InvalidBase64: "",
65
+ UnsupportedType: "",
66
+ TooMany: "",
67
+ TooLargeTogether: ""
68
+ };
69
+ export const renderRead = (result) => Match.valueTags(result, {
70
+ Binary: (binary) => `[${binary.path} is a binary file (${formatBytes(binary.bytes)}${binary.mediaType === null ? "" : `, ${binary.mediaType}`}); it cannot be shown as text.]`,
71
+ Image: (read) => `[Image ${read.path} (${read.image.mediaType}, ${formatBytes(read.image.bytes)}).]`,
72
+ ImageRejected: (read) => `[${read.path} is an image (${read.mediaType}, ${formatBytes(read.bytes)}) that read cannot show: ${read.message}.${rejectedHint[read.reason]}]`,
73
+ Text: (text) => text.totalLines === 0 ? `[${text.path} is empty.]` : [
74
+ text.text,
75
+ ...text.clipped ? [`[Line ${text.firstLine} is longer than the window; only its start is shown. Use bash (cut, head -c) to see the rest.]`] : [],
76
+ ...text.lossy ? [`[${text.path} is not valid UTF-8: bytes that are not show as \uFFFD. edit refuses such a file; change it with bash.]`] : [],
77
+ ...text.next === null ? [] : [`[Showing lines ${text.firstLine}-${text.lastLine} of ${text.totalLines}. Use offset=${text.next} to continue.]`]
78
+ ].join(`
79
+ `)
80
+ }), compactRead = (result) => Match.valueTags(result, {
81
+ Binary: (binary) => `(read ${binary.path}: binary, ${formatBytes(binary.bytes)})`,
82
+ Image: (read) => `(read ${read.path}: ${read.image.mediaType} image, ${formatBytes(read.image.bytes)})`,
83
+ ImageRejected: (read) => `(read ${read.path}: ${read.mediaType} image not shown, ${read.reason})`,
84
+ Text: (text) => `(read ${text.path}: lines ${text.firstLine}-${text.lastLine} of ${text.totalLines})`
85
+ }), renderWrite = (result) => `${result.created ? "Created" : "Replaced"} ${result.path} (${formatBytes(result.bytes)}).`, compactWrite = (result) => `(${result.created ? "created" : "replaced"} ${result.path}: ${formatBytes(result.bytes)})`, renderEdit = (result) => `Replaced ${result.replaced} block${result.replaced === 1 ? "" : "s"} in ${result.path}; the first change is at line ${result.firstChangedLine}.${result.normalized ? " Some text matched only once whitespace and quotes were normalised: read the changed lines to check them." : ""}`, compactEdit = (result) => `(edited ${result.path}: ${result.replaced} block${result.replaced === 1 ? "" : "s"} from line ${result.firstChangedLine})`;
86
+ const lsSuffix = { file: "", directory: "/", symlink: "@", other: "" };
87
+ export const renderLs = (result) => result.total === 0 ? `[${result.path} is empty.]` : [
88
+ ...result.entries.map((entry) => `${entry.name}${lsSuffix[entry.kind]}`),
89
+ ...result.entries.length < result.total ? [`[Showing ${result.entries.length} of ${result.total} entries. Use find with a pattern to narrow them down.]`] : []
90
+ ].join(`
91
+ `), compactLs = (result) => `(listed ${result.path}: ${result.total} entries)`, renderFind = (result, maxLines, maxBytes) => {
92
+ if (result.paths.length === 0)
93
+ return `No files under ${result.path} match ${result.pattern}.`;
94
+ const head = headLines(result.paths, maxLines, maxBytes);
95
+ return [
96
+ ...head.lines,
97
+ ...head.omitted > 0 ? [`[${head.omitted} more paths not shown. Narrow the pattern or the path.]`] : result.truncated ? [`[Stopped at ${result.paths.length} paths; there are more. Narrow the pattern or the path.]`] : []
98
+ ].join(`
99
+ `);
100
+ }, compactFind = (result) => `(find ${result.pattern} in ${result.path}: ${result.paths.length}${result.truncated ? "+" : ""} paths)`, renderGrep = (result, withContext, maxLines, maxBytes) => {
101
+ const skipped = result.skipped === 0 ? [] : [`[${result.skipped} file${result.skipped === 1 ? "" : "s"} skipped: binary, larger than the read limit, or leading outside the workspace.]`];
102
+ if (result.matches === 0)
103
+ return [`No matches for ${result.pattern} in ${result.path}.`, ...skipped].join(`
104
+ `);
105
+ const head = headLines(renderSearchLines(result.lines, withContext), maxLines, maxBytes);
106
+ return [
107
+ ...head.lines,
108
+ ...head.omitted > 0 ? [`[${head.omitted} more lines not shown. Narrow the pattern or the path.]`] : [],
109
+ ...result.truncated ? [`[Stopped at ${result.matches} matching lines; there are more. Narrow the pattern or the path, or raise limit.]`] : [],
110
+ ...skipped
111
+ ].join(`
112
+ `);
113
+ }, compactGrep = (result) => `(grep ${result.pattern} in ${result.path}: ${result.matches}${result.truncated ? "+" : ""} matching lines in ${result.files} files)`;
114
+ const endOf = (result) => result.termination === "timeout" ? `[The command was killed after ${result.durationMs < 1e4 ? `${(result.durationMs / 1000).toFixed(1)} s` : seconds(result.durationMs)}, at its timeout.]` : result.termination === "interrupted" ? "[The command was stopped before it finished.]" : result.termination === "signal" ? `[The command was killed by ${result.signal ?? "a signal"}.]` : `[Exit code ${result.exitCode ?? "unknown"}.]`, cutOf = (result) => {
115
+ const shownLines = result.output.length === 0 ? 0 : result.output.split(`
116
+ `).length, what = shownLines < result.totalLines ? `Output cut to its last ${shownLines} of ${result.totalLines} lines.` : "Output cut: only the end of its last line is shown.", saved = result.fullOutput === null ? "" : result.fullOutputCut ? ` Only its start was saved, in ${result.fullOutput}: the output grew past the size read opens, and the rest was not kept. Read the file with offset to see that start; to see the rest, run the command again with its output filtered (grep, tail, sed -n).` : ` The whole output is in ${result.fullOutput}; read it with offset to see the rest.`;
117
+ return `[${what}${saved}]`;
118
+ };
119
+ export const renderBash = (result) => [
120
+ ...result.output.length === 0 ? ["(no output)"] : [result.output],
121
+ ...result.truncated ? [cutOf(result)] : [],
122
+ endOf(result)
123
+ ].join(`
124
+ `), compactBash = (result) => `(ran ${result.command.length > 80 ? `${result.command.slice(0, 80)}\u2026` : result.command}: ${result.termination === "exit" ? `exit ${result.exitCode ?? "?"}` : result.termination}${result.fullOutput === null ? "" : `; output in ${result.fullOutput}`})`;
@@ -0,0 +1 @@
1
+ export {};