@xandreed/plugin-workspace 0.14.0 → 0.15.1

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 (63) hide show
  1. package/README.md +1 -1
  2. package/dist/bash-tool.adapter.js +10 -9
  3. package/dist/bash.adapter.d.ts +60 -8
  4. package/dist/bash.adapter.js +56 -13
  5. package/dist/file-system.adapter.d.ts +5 -1
  6. package/dist/file-system.adapter.js +1 -1
  7. package/dist/image-resizer.adapter.d.ts +5 -0
  8. package/dist/image-resizer.adapter.js +68 -0
  9. package/dist/image-resizer.entity.d.ts +23 -0
  10. package/dist/image-resizer.entity.functions.d.ts +15 -0
  11. package/dist/image-resizer.entity.functions.js +4 -0
  12. package/dist/image-resizer.entity.js +7 -0
  13. package/dist/index.d.ts +4 -2
  14. package/dist/index.js +4 -1
  15. package/dist/local-system.entity.d.ts +10 -3
  16. package/dist/local-system.entity.functions.d.ts +3 -2
  17. package/dist/local-system.entity.functions.js +16 -2
  18. package/dist/local-system.entity.js +3 -2
  19. package/dist/plugin.adapter.d.ts +103 -40
  20. package/dist/plugin.adapter.js +16 -5
  21. package/dist/read-tools.adapter.d.ts +10 -4
  22. package/dist/read-tools.adapter.js +40 -31
  23. package/dist/ripgrep.adapter.d.ts +74 -0
  24. package/dist/ripgrep.adapter.js +56 -0
  25. package/dist/ripgrep.entity.d.ts +63 -0
  26. package/dist/ripgrep.entity.functions.d.ts +87 -0
  27. package/dist/ripgrep.entity.functions.js +62 -0
  28. package/dist/ripgrep.entity.js +10 -0
  29. package/dist/shell.entity.functions.d.ts +2 -0
  30. package/dist/shell.entity.functions.js +1 -0
  31. package/dist/skills/skills-directory.adapter.d.ts +20 -14
  32. package/dist/skills/skills-directory.adapter.js +2 -2
  33. package/dist/skills/skills-directory.entity.d.ts +8 -5
  34. package/dist/skills/skills-directory.entity.functions.d.ts +58 -3
  35. package/dist/skills/skills-directory.entity.functions.js +5 -5
  36. package/dist/text-edit.entity.functions.js +3 -3
  37. package/dist/text-search.entity.functions.d.ts +7 -0
  38. package/dist/text-search.entity.functions.js +2 -2
  39. package/dist/workspace-path.entity.d.ts +21 -0
  40. package/dist/workspace-path.entity.functions.d.ts +39 -9
  41. package/dist/workspace-path.entity.functions.js +43 -18
  42. package/dist/workspace-path.entity.js +3 -2
  43. package/dist/workspace-tools.adapter.d.ts +48 -3
  44. package/dist/workspace-tools.adapter.js +23 -9
  45. package/dist/workspace-tools.entity.d.ts +136 -17
  46. package/dist/workspace-tools.entity.functions.d.ts +80 -5
  47. package/dist/workspace-tools.entity.functions.js +87 -31
  48. package/dist/workspace-tools.entity.js +26 -5
  49. package/dist/write-tools.adapter.js +10 -8
  50. package/package.json +6 -5
  51. package/dist/bash.adapter.test.d.ts +0 -1
  52. package/dist/file-system.adapter.test.d.ts +0 -1
  53. package/dist/local-system.entity.functions.test.d.ts +0 -1
  54. package/dist/plugin.adapter.test.d.ts +0 -1
  55. package/dist/skills/skills-directory.adapter.test.d.ts +0 -1
  56. package/dist/skills/skills-directory.entity.functions.test.d.ts +0 -1
  57. package/dist/text-edit.entity.functions.test.d.ts +0 -1
  58. package/dist/text-search.entity.functions.test.d.ts +0 -1
  59. package/dist/text-window.entity.functions.test.d.ts +0 -1
  60. package/dist/workspace-path.entity.functions.test.d.ts +0 -1
  61. package/dist/workspace-tools.adapter.test.d.ts +0 -1
  62. package/dist/workspace-tools.entity.functions.test.d.ts +0 -1
  63. package/dist/workspace-tools.journey.test.d.ts +0 -1
@@ -1,27 +1,40 @@
1
1
  import { Layer } from "effect";
2
- import { Bash, Capabilities, FileSystem } from "@xandreed/core";
2
+ import { Bash, Capabilities, FileSystem, ImageResizer } from "@xandreed/core";
3
3
  /**
4
4
  * The machine the agent runs on as its `FileSystem` and `Bash`: the local
5
5
  * disk (`FileSystemLive`) and commands run with the configured shell
6
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.
7
+ * installed. Commands inherit `parentEnv` (the host process's environment
8
+ * when null) through `resolveBashEnv`'s allowlist.
9
9
  */
10
10
  export declare const localSystemPlugin: import("@xandreed/core").TypedPlugin<{
11
11
  readonly shell: string;
12
12
  readonly killBackground: boolean;
13
13
  readonly exitGraceMs: number;
14
+ readonly parentEnv: {
15
+ readonly [x: string]: string;
16
+ } | null;
14
17
  }, {
15
18
  readonly shell: string;
16
19
  readonly killBackground: boolean;
17
20
  readonly exitGraceMs: number;
21
+ readonly parentEnv: {
22
+ readonly [x: string]: string;
23
+ } | null;
18
24
  }, 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 local system as a typed layer: provides FileSystem and Bash; requires nothing.
27
+ *
28
+ * @public
29
+ */
30
+ export declare const LocalSystemPluginLive: (options?: {
31
+ readonly shell?: string;
32
+ readonly killBackground?: boolean;
33
+ readonly exitGraceMs?: number;
34
+ readonly parentEnv?: {
35
+ readonly [x: string]: string | undefined;
36
+ } | null;
37
+ } | undefined) => Layer.Layer<FileSystem | Bash, import("@xandreed/core").HarnessError | import("@xandreed/core").BashError, never>;
25
38
  /**
26
39
  * The workspace tools (`workspaceTools`): read, ls, find and grep always on,
27
40
  * write and edit behind `fs.write`, bash behind `shell.exec`, over the
@@ -32,9 +45,14 @@ export declare const LocalSystemPluginLive: (options?: Partial<{
32
45
  * `PermissionGrants`.
33
46
  */
34
47
  export declare const workspaceToolsPlugin: import("@xandreed/core").TypedPlugin<{
35
- readonly tools: readonly ("write" | "find" | "read" | "grep" | "ls" | "edit" | "bash")[];
48
+ readonly tools: readonly ("find" | "write" | "read" | "grep" | "bash" | "ls" | "edit")[];
36
49
  readonly always: readonly ("read" | "edit" | "shell")[];
37
50
  readonly confine: boolean;
51
+ readonly workspaceAccess: "write" | "read";
52
+ readonly roots: readonly {
53
+ readonly path: string;
54
+ readonly access: "write" | "read";
55
+ }[];
38
56
  readonly protect: readonly string[];
39
57
  readonly exclude: readonly string[];
40
58
  readonly maxLines: number;
@@ -43,8 +61,12 @@ export declare const workspaceToolsPlugin: import("@xandreed/core").TypedPlugin<
43
61
  readonly findResults: number;
44
62
  readonly grepMatches: number;
45
63
  readonly grepLineChars: number;
64
+ readonly grepEngine: "auto" | "ripgrep" | "builtin";
65
+ readonly grepTimeoutMs: number;
66
+ readonly grepIgnoreFiles: boolean;
46
67
  readonly maxFileBytes: number;
47
68
  readonly readImages: boolean;
69
+ readonly imageMaxSide: number;
48
70
  readonly fuzzyEdit: boolean;
49
71
  readonly bashTimeoutMs: number;
50
72
  readonly bashMaxTimeoutMs: number;
@@ -60,9 +82,14 @@ export declare const workspaceToolsPlugin: import("@xandreed/core").TypedPlugin<
60
82
  readonly environmentSection: boolean;
61
83
  readonly sectionOrder: number;
62
84
  }, {
63
- readonly tools: readonly ("write" | "find" | "read" | "grep" | "ls" | "edit" | "bash")[];
85
+ readonly tools: readonly ("find" | "write" | "read" | "grep" | "bash" | "ls" | "edit")[];
64
86
  readonly always: readonly ("read" | "edit" | "shell")[];
65
87
  readonly confine: boolean;
88
+ readonly workspaceAccess: "write" | "read";
89
+ readonly roots: readonly {
90
+ readonly path: string;
91
+ readonly access: "write" | "read";
92
+ }[];
66
93
  readonly protect: readonly string[];
67
94
  readonly exclude: readonly string[];
68
95
  readonly maxLines: number;
@@ -71,8 +98,12 @@ export declare const workspaceToolsPlugin: import("@xandreed/core").TypedPlugin<
71
98
  readonly findResults: number;
72
99
  readonly grepMatches: number;
73
100
  readonly grepLineChars: number;
101
+ readonly grepEngine: "auto" | "ripgrep" | "builtin";
102
+ readonly grepTimeoutMs: number;
103
+ readonly grepIgnoreFiles: boolean;
74
104
  readonly maxFileBytes: number;
75
105
  readonly readImages: boolean;
106
+ readonly imageMaxSide: number;
76
107
  readonly fuzzyEdit: boolean;
77
108
  readonly bashTimeoutMs: number;
78
109
  readonly bashMaxTimeoutMs: number;
@@ -88,33 +119,65 @@ export declare const workspaceToolsPlugin: import("@xandreed/core").TypedPlugin<
88
119
  readonly environmentSection: boolean;
89
120
  readonly sectionOrder: number;
90
121
  }, 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;
122
+ /**
123
+ * 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.
124
+ *
125
+ * @public
126
+ */
127
+ export declare const WorkspaceToolsPluginLive: (options?: {
128
+ readonly tools?: readonly ("find" | "write" | "read" | "grep" | "bash" | "ls" | "edit")[];
129
+ readonly always?: readonly ("read" | "edit" | "shell")[];
130
+ readonly confine?: boolean;
131
+ readonly workspaceAccess?: "write" | "read";
132
+ readonly roots?: readonly {
133
+ readonly path: string;
134
+ readonly access: "write" | "read";
135
+ }[];
136
+ readonly protect?: readonly string[];
137
+ readonly exclude?: readonly string[];
138
+ readonly maxLines?: number;
139
+ readonly maxBytes?: number;
140
+ readonly lsEntries?: number;
141
+ readonly findResults?: number;
142
+ readonly grepMatches?: number;
143
+ readonly grepLineChars?: number;
144
+ readonly grepEngine?: "auto" | "ripgrep" | "builtin";
145
+ readonly grepTimeoutMs?: number;
146
+ readonly grepIgnoreFiles?: boolean;
147
+ readonly maxFileBytes?: number;
148
+ readonly readImages?: boolean;
149
+ readonly imageMaxSide?: number;
150
+ readonly fuzzyEdit?: boolean;
151
+ readonly bashTimeoutMs?: number;
152
+ readonly bashMaxTimeoutMs?: number;
153
+ readonly bashEnv?: {
154
+ readonly [x: string]: string | undefined;
111
155
  };
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>;
156
+ readonly bashInheritEnv?: boolean;
157
+ readonly bashEnvAllow?: readonly string[];
158
+ readonly bashSessionEnv?: boolean;
159
+ readonly spillDir?: string | null;
160
+ readonly spillKeep?: number;
161
+ readonly progressMs?: number;
162
+ readonly environmentSection?: boolean;
163
+ readonly sectionOrder?: number;
164
+ } | undefined) => Layer.Layer<Capabilities, import("@xandreed/core").HarnessError, never>;
165
+ /** Optional CLI image fitting on the host's matching Bash and FileSystem. @public */
166
+ export declare const imageResizerPlugin: import("@xandreed/core").TypedPlugin<{
167
+ readonly tool: "auto" | "magick" | "convert" | "vips" | "sips";
168
+ readonly tempDir: string | null;
169
+ readonly timeoutMs: number;
170
+ readonly maxSourcePixels: number;
171
+ }, {
172
+ readonly tool: "auto" | "magick" | "convert" | "vips" | "sips";
173
+ readonly tempDir: string | null;
174
+ readonly timeoutMs: number;
175
+ readonly maxSourcePixels: number;
176
+ }, ImageResizer, import("@xandreed/core").HarnessError, FileSystem | Bash>;
177
+ /** @public */
178
+ export declare const ImageResizerPluginLive: (options?: {
179
+ readonly tool?: "auto" | "magick" | "convert" | "vips" | "sips";
180
+ readonly tempDir?: string | null;
181
+ readonly timeoutMs?: number;
182
+ readonly maxSourcePixels?: number;
183
+ } | undefined) => Layer.Layer<ImageResizer, import("@xandreed/core").HarnessError, FileSystem | Bash>;
@@ -1,13 +1,15 @@
1
1
  import { Effect, Layer } from "effect";
2
- import { Bash, Capabilities, definePlugin, FileSystem, SessionEnvironment } from "@xandreed/core";
2
+ import { Bash, Capabilities, definePlugin, FileSystem, ImageResizer, SessionEnvironment } from "@xandreed/core";
3
3
  import { BashLive } from "./bash.adapter.js";
4
4
  import { FileSystemLive } from "./file-system.adapter.js";
5
5
  import { LocalSystemConfig, localSystemDefaults } from "./local-system.entity.js";
6
+ import { ImageResizerCliLive } from "./image-resizer.adapter.js";
7
+ import { ImageResizerConfig, imageResizerDefaults } from "./image-resizer.entity.js";
6
8
  import { workspaceTools } from "./workspace-tools.adapter.js";
7
9
  import { WorkspaceToolsConfig, workspaceToolsDefaults } from "./workspace-tools.entity.js";
8
10
  export const localSystemPlugin = definePlugin({
9
11
  id: "@xandreed/plugin-workspace/local",
10
- version: "0.14.0",
12
+ version: "0.15.1",
11
13
  scope: "runtime",
12
14
  config: LocalSystemConfig,
13
15
  defaults: localSystemDefaults,
@@ -15,13 +17,22 @@ export const localSystemPlugin = definePlugin({
15
17
  layer: (config) => Layer.merge(FileSystemLive, BashLive(config))
16
18
  }), LocalSystemPluginLive = localSystemPlugin.live, workspaceToolsPlugin = definePlugin({
17
19
  id: "@xandreed/plugin-workspace/tools",
18
- version: "0.14.0",
20
+ version: "0.15.1",
19
21
  scope: "runtime",
20
22
  config: WorkspaceToolsConfig,
21
23
  defaults: workspaceToolsDefaults,
22
24
  requires: [FileSystem, SessionEnvironment],
23
- optional: [Bash],
25
+ optional: [Bash, ImageResizer],
24
26
  provides: [],
25
27
  contributes: [Capabilities],
26
28
  layer: (config) => Layer.effect(Capabilities, workspaceTools(config).pipe(Effect.map((capability) => [capability])))
27
- }), WorkspaceToolsPluginLive = workspaceToolsPlugin.live;
29
+ }), WorkspaceToolsPluginLive = workspaceToolsPlugin.live, imageResizerPlugin = definePlugin({
30
+ id: "@xandreed/plugin-workspace/images",
31
+ version: "0.15.1",
32
+ scope: "runtime",
33
+ config: ImageResizerConfig,
34
+ defaults: imageResizerDefaults,
35
+ requires: [FileSystem, Bash],
36
+ provides: [ImageResizer],
37
+ layer: (config) => ImageResizerCliLive(config)
38
+ }), ImageResizerPluginLive = imageResizerPlugin.live;
@@ -1,5 +1,5 @@
1
1
  import type { Path, RcMap, Semaphore } from "effect";
2
- import { Failure } from "@xandreed/core";
2
+ import { Failure, FileSystem, SessionEnvironment } from "@xandreed/core";
3
3
  import type { RegisteredTool, Subject } from "@xandreed/core";
4
4
  import type { ProtectRule } from "./workspace-path.entity.functions.js";
5
5
  import type { WorkspaceToolsConfig } from "./workspace-tools.entity.js";
@@ -9,14 +9,17 @@ export interface WorkspaceToolContext {
9
9
  readonly path: Path.Path;
10
10
  readonly protect: ReadonlyArray<ProtectRule>;
11
11
  readonly locks: RcMap.RcMap<string, Semaphore.Semaphore>;
12
+ readonly resizesImages: boolean;
12
13
  }
14
+ /** The services every workspace tool reads on each call, and the `workspace.environment` section on each render: the file system, and the session's workspace on it (`bash` reads `Bash` too). */
15
+ export declare const WORKSPACE_SERVICES: readonly [typeof FileSystem, typeof SessionEnvironment];
13
16
  export declare const failure: (error: string, message: string) => Failure;
14
17
  /** The refusal of a path that is neither a regular file nor a directory: reading a pipe, socket or device could wait forever. */
15
18
  export declare const notAFile: (display: string, tool: string) => Failure;
16
19
  /** A file the result names, for memory to keep track of; none for a name memory could not store as an id. */
17
20
  export declare const fileSubjects: (path: string) => ReadonlyArray<Subject>;
18
21
  /** `read`: a window of a text file's lines, an image (`readImages`), or what a binary file is. */
19
- export declare const readTool: ({ config, path }: WorkspaceToolContext) => RegisteredTool;
22
+ export declare const readTool: ({ config, path, resizesImages }: WorkspaceToolContext) => RegisteredTool;
20
23
  /** `ls`: a directory's entries by name, at most `lsEntries` of them. */
21
24
  export declare const lsTool: ({ config, path }: WorkspaceToolContext) => RegisteredTool;
22
25
  /** `find`: the files under a directory whose path matches a glob, as paths relative to the workspace root. */
@@ -26,8 +29,11 @@ export declare const findTool: ({ config, path }: WorkspaceToolContext) => Regis
26
29
  * Files come from a walk that never enters links or excluded names; each is
27
30
  * resolved again before it is read and skipped when it leads outside the
28
31
  * workspace, is not a regular file, is larger than `maxFileBytes` or is
29
- * binary. Lines are cut to `grepLineChars` before matching. Searching a
32
+ * binary; one the file system refuses (permission denied, say) is named in
33
+ * `unreadable` with the reason, not counted as skipped. Lines are cut to `grepLineChars` before matching. Searching a
30
34
  * directory, one file gives at most a quarter of the limit, so one large
31
- * file cannot fill the result.
35
+ * file cannot fill the result, however many files the directory holds (as
36
+ * with ripgrep, which cannot know that ahead). A pattern with a line break
37
+ * is refused (`singleLinePattern`) whichever engine runs.
32
38
  */
33
39
  export declare const grepTool: ({ config, path }: WorkspaceToolContext) => RegisteredTool;
@@ -1,19 +1,21 @@
1
1
  import { Tool } from "effect/ai";
2
2
  import { Effect, Option, Result } from "effect";
3
- import { defineTool, Failure } from "@xandreed/core";
4
- import { compilePattern, searchText } from "./text-search.entity.functions.js";
5
- import { decodeText, formatBytes, lineWindow, looksBinary } from "./text-window.entity.functions.js";
6
- import { displayOf, excludesBelow, fsFailure, isWithin, openWorkspace, resolveTarget } from "./workspace-path.entity.functions.js";
7
- import { FindParams, FindResult, GrepParams, GrepResult, LsParams, LsResult, ReadParams, ReadResult } from "./workspace-tools.entity.js";
8
- import { binaryRead, compactFind, fileTooLargeHint, compactGrep, compactLs, compactRead, readMedia, renderFind, renderGrep, renderLs, renderRead, toolDescriptions } from "./workspace-tools.entity.functions.js";
9
- export const failure = (error, message) => ({ error, message }), notAFile = (display, tool) => failure("NotAFile", `${display} is not a regular file (a pipe, socket or device); ${tool} works on regular files only, use bash for it`);
3
+ import { Bash, defineTool, Failure, FileSystem, ImageResizer, SessionEnvironment, SubjectId } from "@xandreed/core";
4
+ import { searchRipgrep } from "./ripgrep.adapter.js";
5
+ import { compilePattern, searchText, singleLinePattern } from "./text-search.entity.functions.js";
6
+ import { decodeText, formatBytes, lineWindow, looksBinary, mediaTypeOf } from "./text-window.entity.functions.js";
7
+ import { displayOf, excludesBelow, fsFailure, isWithin, openWorkspace, resolveTarget, withinAnyRoot } from "./workspace-path.entity.functions.js";
8
+ import { FindResult, GrepResult, LsResult, ReadResult } from "./workspace-tools.entity.js";
9
+ import { binaryRead, compactFind, fileTooLargeHint, compactGrep, compactLs, compactRead, readMedia, renderFind, renderGrep, renderLs, renderRead, toolDescriptions, toolParameters } from "./workspace-tools.entity.functions.js";
10
+ export const WORKSPACE_SERVICES = [FileSystem, SessionEnvironment], failure = (error, message) => ({ error, message }), notAFile = (display, tool) => failure("NotAFile", `${display} is not a regular file (a pipe, socket or device); ${tool} works on regular files only, use bash for it`);
10
11
  const walkExcludes = (access, target, excludes) => excludesBelow(excludes, isWithin(access.root, target.path) ? Option.some(access.path.relative(access.root, target.path)) : Option.none());
11
- export const fileSubjects = (path) => path.length > 0 && path.trim() === path ? [{ kind: "file", id: path, label: Option.none(), data: Option.none() }] : [];
12
- const ReadTool = (description) => Tool.make("read", { description, parameters: ReadParams, success: ReadResult, failure: Failure, failureMode: "return" }), LsTool = (description) => Tool.make("ls", { description, parameters: LsParams, success: LsResult, failure: Failure, failureMode: "return" }), FindTool = (description) => Tool.make("find", { description, parameters: FindParams, success: FindResult, failure: Failure, failureMode: "return" }), GrepTool = (description) => Tool.make("grep", { description, parameters: GrepParams, success: GrepResult, failure: Failure, failureMode: "return" });
13
- export const readTool = ({ config, path }) => defineTool({
14
- tool: ReadTool(toolDescriptions(config).read),
12
+ export const fileSubjects = (path) => path.length > 0 && path.trim() === path ? [{ kind: "file", id: SubjectId.make(path), label: Option.none(), data: Option.none() }] : [];
13
+ const ReadTool = (config, resizesImages) => Tool.make("read", { description: toolDescriptions(config, resizesImages).read, parameters: toolParameters(config).read, success: ReadResult, failure: Failure, failureMode: "return" }), LsTool = (config) => Tool.make("ls", { description: toolDescriptions(config).ls, parameters: toolParameters(config).ls, success: LsResult, failure: Failure, failureMode: "return" }), FindTool = (config) => Tool.make("find", { description: toolDescriptions(config).find, parameters: toolParameters(config).find, success: FindResult, failure: Failure, failureMode: "return" }), GrepTool = (config) => Tool.make("grep", { description: toolDescriptions(config).grep, parameters: toolParameters(config).grep, success: GrepResult, failure: Failure, failureMode: "return" });
14
+ export const readTool = ({ config, path, resizesImages }) => defineTool({
15
+ tool: ReadTool(config, resizesImages),
16
+ requires: resizesImages ? [...WORKSPACE_SERVICES, ImageResizer] : WORKSPACE_SERVICES,
15
17
  handler: (params) => Effect.gen(function* () {
16
- const access = yield* openWorkspace(path), target = yield* resolveTarget(access, params.path, { confine: config.confine }), info = yield* access.fs.stat(target.path).pipe(Effect.mapError(fsFailure(target.display)));
18
+ const access = yield* openWorkspace(path, config), target = yield* resolveTarget(access, params.path, { confine: config.confine }), info = yield* access.fs.stat(target.path).pipe(Effect.mapError(fsFailure(target.display)));
17
19
  if (info.kind === "directory")
18
20
  return yield* Effect.fail(failure("IsDirectory", `${target.display} is a directory; use ls to list it`));
19
21
  if (info.kind !== "file")
@@ -22,8 +24,8 @@ export const readTool = ({ config, path }) => defineTool({
22
24
  return yield* Effect.fail(failure("FileTooLarge", `${target.display} is ${formatBytes(info.size)}, more than the ${formatBytes(config.maxFileBytes)} read loads; ${fileTooLargeHint(target.display, config.readImages)}`));
23
25
  const bytes = yield* access.fs.readBytes(target.path).pipe(Effect.mapError(fsFailure(target.display)));
24
26
  if (looksBinary(bytes))
25
- return yield* binaryRead(target.display, bytes, config.readImages);
26
- const decoded = decodeText(bytes), window = lineWindow(decoded.text, { offset: params.offset ?? 1, limit: Math.min(params.limit ?? config.maxLines, config.maxLines), maxBytes: config.maxBytes });
27
+ return yield* binaryRead(target.display, bytes, config.readImages, Option.getOrNull(mediaTypeOf(target.display)), resizesImages ? Option.map(yield* Effect.serviceOption(ImageResizer), (resizer) => resizer.fit) : Option.none(), config.imageMaxSide);
28
+ const decoded = decodeText(bytes), window = lineWindow(decoded.text, { offset: params.offset ?? 1, limit: params.limit ?? config.maxLines, maxBytes: config.maxBytes });
27
29
  return yield* Result.match(window, {
28
30
  onFailure: (total) => Effect.fail(failure("OffsetPastEnd", `${target.display} has ${total} line${total === 1 ? "" : "s"}; offset must be at most ${Math.max(1, total)}`)),
29
31
  onSuccess: (lines) => Effect.succeed({
@@ -40,61 +42,68 @@ export const readTool = ({ config, path }) => defineTool({
40
42
  })
41
43
  });
42
44
  }),
43
- view: { version: "1", render: renderRead, compact: compactRead, subjects: (result) => fileSubjects(result.path), media: readMedia },
45
+ view: { version: "3", render: renderRead, compact: compactRead, subjects: (result) => fileSubjects(result.path), media: readMedia },
44
46
  annotations: { readOnly: !0 }
45
47
  }), lsTool = ({ config, path }) => defineTool({
46
- tool: LsTool(toolDescriptions(config).ls),
48
+ tool: LsTool(config),
49
+ requires: WORKSPACE_SERVICES,
47
50
  handler: (params) => Effect.gen(function* () {
48
- const access = yield* openWorkspace(path), target = yield* resolveTarget(access, params.path ?? ".", { confine: config.confine }), entries = yield* access.fs.list(target.path).pipe(Effect.mapError((error) => error.reason === "NotDirectory" ? failure("NotDirectory", `${target.display} is not a directory; use read to see a file`) : fsFailure(target.display)(error))), limit = Math.min(params.limit ?? config.lsEntries, config.lsEntries);
51
+ const access = yield* openWorkspace(path, config), target = yield* resolveTarget(access, params.path ?? ".", { confine: config.confine }), entries = yield* access.fs.list(target.path).pipe(Effect.mapError((error) => error.reason === "NotDirectory" ? failure("NotDirectory", `${target.display} is not a directory; use read to see a file`) : fsFailure(target.display)(error))), limit = params.limit ?? config.lsEntries;
49
52
  return { path: target.display, entries: entries.slice(0, limit).map((entry) => ({ name: entry.name, kind: entry.kind })), total: entries.length };
50
53
  }),
51
- view: { version: "1", render: renderLs, compact: compactLs },
54
+ view: { version: "2", render: renderLs, compact: compactLs },
52
55
  annotations: { readOnly: !0 }
53
56
  }), findTool = ({ config, path }) => defineTool({
54
- tool: FindTool(toolDescriptions(config).find),
57
+ tool: FindTool(config),
58
+ requires: WORKSPACE_SERVICES,
55
59
  handler: (params) => Effect.gen(function* () {
56
- const access = yield* openWorkspace(path), target = yield* resolveTarget(access, params.path ?? ".", { confine: config.confine }), limit = Math.min(params.limit ?? config.findResults, config.findResults), found = yield* access.fs.glob(params.pattern, { cwd: target.path, exclude: walkExcludes(access, target, config.exclude), dot: !0, onlyFiles: !0, limit }).pipe(Effect.mapError((error) => error.reason === "Invalid" ? failure("InvalidPattern", `${params.pattern}: ${error.message}`) : fsFailure(target.display)(error)));
60
+ const access = yield* openWorkspace(path, config), target = yield* resolveTarget(access, params.path ?? ".", { confine: config.confine }), limit = params.limit ?? config.findResults, found = yield* access.fs.glob(params.pattern, { cwd: target.path, exclude: walkExcludes(access, target, config.exclude), dot: !0, onlyFiles: !0, limit }).pipe(Effect.mapError((error) => error.reason === "Invalid" ? failure("InvalidPattern", `${params.pattern}: ${error.message}`) : fsFailure(target.display)(error)));
57
61
  return { pattern: params.pattern, path: target.display, paths: found.paths.map((relative) => displayOf(access, access.path.join(target.path, relative))), truncated: found.truncated };
58
62
  }),
59
- view: { version: "1", render: (result) => renderFind(result, config.maxLines, config.maxBytes), compact: compactFind },
63
+ view: { version: "2", render: (result) => renderFind(result, config.maxLines, config.maxBytes), compact: compactFind },
60
64
  annotations: { readOnly: !0 }
61
65
  }), grepTool = ({ config, path }) => defineTool({
62
- tool: GrepTool(toolDescriptions(config).grep),
66
+ tool: GrepTool(config),
67
+ requires: config.grepEngine === "ripgrep" ? [...WORKSPACE_SERVICES, Bash] : WORKSPACE_SERVICES,
63
68
  handler: (params) => Effect.gen(function* () {
64
- const access = yield* openWorkspace(path), target = yield* resolveTarget(access, params.path ?? ".", { confine: config.confine }), pattern = yield* Effect.fromResult(compilePattern(params.pattern, { literal: params.literal ?? !1, ignoreCase: params.ignoreCase ?? !1 })), info = yield* access.fs.stat(target.path).pipe(Effect.mapError(fsFailure(target.display)));
69
+ const access = yield* openWorkspace(path, config), target = yield* resolveTarget(access, params.path ?? ".", { confine: config.confine });
70
+ yield* Effect.fromResult(singleLinePattern(params.pattern));
71
+ const pattern = config.grepEngine === "ripgrep" ? Option.none() : Option.some(yield* Effect.fromResult(compilePattern(params.pattern, { literal: params.literal ?? !1, ignoreCase: params.ignoreCase ?? !1 }))), info = yield* access.fs.stat(target.path).pipe(Effect.mapError(fsFailure(target.display)));
65
72
  if (info.kind !== "file" && info.kind !== "directory")
66
73
  return yield* Effect.fail(notAFile(target.display, "grep"));
74
+ if (config.grepEngine === "ripgrep")
75
+ return yield* searchRipgrep(access, target, info.kind === "directory", params, config);
67
76
  const glob = params.glob === void 0 ? "**/*" : params.glob.includes("/") ? params.glob : `**/${params.glob}`, files = info.kind === "file" ? [{ path: target.path, display: target.display }] : yield* access.fs.glob(glob, { cwd: target.path, exclude: walkExcludes(access, target, config.exclude), dot: !0, onlyFiles: !0 }).pipe(Effect.map((found) => found.paths.map((relative) => {
68
77
  const absolute = access.path.join(target.path, relative);
69
78
  return { path: absolute, display: displayOf(access, absolute) };
70
- })), Effect.mapError((error) => error.reason === "Invalid" ? failure("InvalidPattern", `glob ${glob}: ${error.message}`) : fsFailure(target.display)(error))), limit = Math.min(params.limit ?? config.grepMatches, config.grepMatches), perFile = files.length > 1 ? Math.max(1, Math.ceil(limit / 4)) : limit, context = params.context ?? 0, skip = (state) => ({ ...state, skipped: state.skipped + 1 }), searched = yield* Effect.reduce(files, () => ({ lines: [], matches: 0, files: 0, skipped: 0, truncated: !1 }), (state, file) => {
79
+ })), Effect.mapError((error) => error.reason === "Invalid" ? failure("InvalidPattern", `glob ${params.glob ?? glob}: ${error.message}`) : fsFailure(target.display)(error))), limit = params.limit ?? config.grepMatches, perFile = info.kind === "directory" ? Math.max(1, Math.ceil(limit / 4)) : limit, context = params.context ?? 0, skip = (state) => ({ ...state, skipped: state.skipped + 1 }), searched = yield* Effect.reduce(files, () => ({ lines: [], matches: 0, files: 0, skipped: 0, truncated: !1, unreadable: [] }), (state, file) => {
71
80
  if (state.matches >= limit)
72
81
  return Effect.succeed({ ...state, truncated: !0 });
73
82
  return Effect.gen(function* () {
74
83
  const real = yield* access.fs.realPath(file.path);
75
- if (config.confine && !isWithin(access.root, real))
84
+ if (config.confine && !withinAnyRoot(access, real))
76
85
  return Option.none();
77
86
  const found = yield* access.fs.stat(real);
78
87
  if (found.kind !== "file" || found.size > config.maxFileBytes)
79
88
  return Option.none();
80
89
  const bytes = yield* access.fs.readBytes(real);
81
90
  return looksBinary(bytes) ? Option.none() : Option.some(bytes);
82
- }).pipe(Effect.option, Effect.map((bytes) => Option.match(Option.flatten(bytes), {
91
+ }).pipe(Effect.map(Option.match({
83
92
  onNone: () => skip(state),
84
93
  onSome: (content) => {
85
- const found = searchText(decodeText(content).text, pattern, { lineChars: config.grepLineChars, context, cap: Math.min(perFile, limit - state.matches) });
94
+ const found = searchText(decodeText(content).text, Option.getOrThrow(pattern), { lineChars: config.grepLineChars, context, cap: Math.min(perFile, limit - state.matches) });
86
95
  return {
96
+ ...state,
87
97
  lines: [...state.lines, ...found.lines.map((line) => ({ path: file.display, ...line }))],
88
98
  matches: state.matches + found.matches,
89
99
  files: state.files + (found.matches > 0 ? 1 : 0),
90
- skipped: state.skipped,
91
100
  truncated: state.truncated || found.capped
92
101
  };
93
102
  }
94
- })));
103
+ })), Effect.catch((error) => Effect.succeed({ ...state, unreadable: [...state.unreadable, { path: file.display, reason: error.reason }] })));
95
104
  });
96
- return { pattern: params.pattern, path: target.display, ...searched };
105
+ return { pattern: params.pattern, path: target.display, ...searched, timedOut: !1 };
97
106
  }),
98
- view: { version: "1", render: (result, params) => renderGrep(result, (params.context ?? 0) > 0, config.maxLines, config.maxBytes), compact: compactGrep },
107
+ view: { version: "4", render: (result, params) => renderGrep(result, (params.context ?? 0) > 0, config.maxLines, config.maxBytes), compact: compactGrep },
99
108
  annotations: { readOnly: !0 }
100
109
  });
@@ -0,0 +1,74 @@
1
+ import { Effect } from "effect";
2
+ import { Bash } from "@xandreed/core";
3
+ import type { WorkspaceAccess } from "./workspace-path.entity.functions.js";
4
+ /** A bounded, interruptible search on the same machine as the Bash/FileSystem pair. */
5
+ export declare const searchRipgrep: (access: WorkspaceAccess, target: {
6
+ readonly path: string;
7
+ readonly display: string;
8
+ }, directory: boolean, params: {
9
+ readonly pattern: string;
10
+ readonly path?: string;
11
+ readonly limit?: number;
12
+ readonly context?: number;
13
+ readonly literal?: boolean;
14
+ readonly glob?: string;
15
+ readonly ignoreCase?: boolean;
16
+ }, config: {
17
+ readonly tools: readonly ("find" | "write" | "read" | "grep" | "bash" | "ls" | "edit")[];
18
+ readonly always: readonly ("read" | "edit" | "shell")[];
19
+ readonly confine: boolean;
20
+ readonly workspaceAccess: "write" | "read";
21
+ readonly roots: readonly {
22
+ readonly path: string;
23
+ readonly access: "write" | "read";
24
+ }[];
25
+ readonly protect: readonly string[];
26
+ readonly exclude: readonly string[];
27
+ readonly maxLines: number;
28
+ readonly maxBytes: number;
29
+ readonly lsEntries: number;
30
+ readonly findResults: number;
31
+ readonly grepMatches: number;
32
+ readonly grepLineChars: number;
33
+ readonly grepEngine: "auto" | "ripgrep" | "builtin";
34
+ readonly grepTimeoutMs: number;
35
+ readonly grepIgnoreFiles: boolean;
36
+ readonly maxFileBytes: number;
37
+ readonly readImages: boolean;
38
+ readonly imageMaxSide: number;
39
+ readonly fuzzyEdit: boolean;
40
+ readonly bashTimeoutMs: number;
41
+ readonly bashMaxTimeoutMs: number;
42
+ readonly bashEnv: {
43
+ readonly [x: string]: string;
44
+ };
45
+ readonly bashInheritEnv: boolean;
46
+ readonly bashEnvAllow: readonly string[];
47
+ readonly bashSessionEnv: boolean;
48
+ readonly spillDir: string | null;
49
+ readonly spillKeep: number;
50
+ readonly progressMs: number;
51
+ readonly environmentSection: boolean;
52
+ readonly sectionOrder: number;
53
+ }) => Effect.Effect<{
54
+ pattern: string;
55
+ path: string;
56
+ lines: readonly {
57
+ readonly path: string;
58
+ readonly line: number;
59
+ readonly text: string;
60
+ readonly match: boolean;
61
+ }[];
62
+ matches: number;
63
+ files: number;
64
+ skipped: number;
65
+ truncated: boolean;
66
+ unreadable: {
67
+ path: string;
68
+ reason: "Invalid" | "NotFound" | "Exists" | "IsDirectory" | "NotDirectory" | "NotEmpty" | "Permission" | "Io";
69
+ }[];
70
+ timedOut: boolean;
71
+ }, {
72
+ readonly error: string;
73
+ readonly message: string;
74
+ }, Bash>;
@@ -0,0 +1,56 @@
1
+ import { Effect, Option, Stream } from "effect";
2
+ import { Bash, RunContext } from "@xandreed/core";
3
+ import { rgReasonOf } from "./local-system.entity.functions.js";
4
+ import { decodeRgMessage, emptyRgSearch, foldRgMessage, rgLines, rgRecords, ripgrepCommand } from "./ripgrep.entity.functions.js";
5
+ import { clipHead } from "./text-window.entity.functions.js";
6
+ import { displayOf, excludesBelow, isWithin } from "./workspace-path.entity.functions.js";
7
+ import { bashEnvironment } from "./workspace-tools.entity.functions.js";
8
+ const empty = { search: emptyRgSearch, buffer: "", stderr: "", exit: Option.none(), invalid: !1 }, failure = (error, message) => ({ error, message });
9
+ export const searchRipgrep = Effect.fn("workspace.grep")(function* (access, target, directory, params, config) {
10
+ const bash = yield* Bash, run = yield* Effect.serviceOption(RunContext), cwd = directory ? target.path : access.path.dirname(target.path), input = directory ? "." : access.path.basename(target.path), exclude = excludesBelow(config.exclude, isWithin(access.root, cwd) ? Option.some(access.path.relative(access.root, cwd)) : Option.none()), limit = params.limit ?? config.grepMatches, perFile = directory ? Math.max(1, Math.ceil(limit / 4)) : limit, options = { limit, perFile, directory, lineChars: config.grepLineChars, display: (path) => displayOf(access, access.path.resolve(cwd, path)) }, fold = (state, event) => {
11
+ if (event._tag === "BashExit")
12
+ return { ...state, exit: Option.some(event) };
13
+ if (event.stream === "stderr")
14
+ return { ...state, stderr: clipHead(state.stderr + event.text, 16384) };
15
+ const complete = state.buffer + event.text;
16
+ if (complete.split(`
17
+ `).some((line) => line.length > config.maxFileBytes * 6 + 65536))
18
+ return { ...state, invalid: !0 };
19
+ const parsed = rgRecords("", complete);
20
+ return { ...state, buffer: parsed.buffer, search: parsed.records.reduce((search, message) => search.stopped ? search : foldRgMessage(search, message, options), state.search) };
21
+ }, output = yield* bash.stream(ripgrepCommand(config, params, input, perFile, exclude), {
22
+ cwd,
23
+ stdin: params.pattern.length === 0 ? `
24
+ ` : params.pattern,
25
+ timeoutMs: config.grepTimeoutMs,
26
+ ...bashEnvironment(config, Option.map(run, (current) => ({ sessionId: String(current.session.id), runId: current.runId, workspace: access.root })))
27
+ }).pipe(Stream.mapAccum(() => empty, (state, event) => {
28
+ const next = fold(state, event);
29
+ return [next, [next]];
30
+ }), Stream.takeUntil((state) => state.search.stopped || state.invalid), Stream.runFold(() => empty, (_previous, state) => state), Effect.mapError((error) => failure(error.reason, error.message))), search = Option.match(decodeRgMessage(output.buffer), {
31
+ onNone: () => output.search,
32
+ onSome: (message) => foldRgMessage(output.search, message, options)
33
+ });
34
+ if (output.invalid)
35
+ return yield* Effect.fail(failure("SearchFailed", "The search output was too large to read; narrow the path or the pattern and search again"));
36
+ const timedOut = Option.exists(output.exit, (exit) => exit.termination === "timeout"), unreadable = output.stderr.split(`
37
+ `).flatMap((line) => {
38
+ const found = /^rg: (.*): .*\(os error (\d+)\)$/.exec(line);
39
+ return found === null ? [] : [{ path: options.display(found[1] ?? ""), reason: rgReasonOf(Number(found[2])) }];
40
+ });
41
+ if (Option.exists(output.exit, (exit) => Option.contains(exit.exitCode, 2)) && search.messages === 0 && unreadable.length === 0)
42
+ return yield* Effect.fail(failure("InvalidPattern", `The search pattern or glob could not be parsed: ${output.stderr.trim()}. Pass literal: true to search for the pattern as text`));
43
+ if (Option.exists(output.exit, (exit) => exit.termination === "signal" || exit.termination === "exit" && !Option.exists(exit.exitCode, (code) => code === 0 || code === 1 || code === 2)))
44
+ return yield* Effect.fail(failure("SearchFailed", "The file search could not finish; try a narrower path or tell the user the search is unavailable"));
45
+ return {
46
+ pattern: params.pattern,
47
+ path: target.display,
48
+ lines: rgLines(search, params.context ?? 0),
49
+ matches: [...search.counts.values()].reduce((sum, count) => sum + count, 0),
50
+ files: search.counts.size,
51
+ skipped: search.skipped,
52
+ truncated: search.truncated,
53
+ unreadable,
54
+ timedOut
55
+ };
56
+ });
@@ -0,0 +1,63 @@
1
+ import { Schema } from "effect";
2
+ /** ripgrep represents non-UTF-8 paths and lines as base64 bytes. */
3
+ export declare const RgText: Schema.Union<readonly [Schema.Struct<{
4
+ readonly text: Schema.String;
5
+ }>, Schema.Struct<{
6
+ readonly bytes: Schema.StringFromBase64;
7
+ }>]>;
8
+ export type RgText = typeof RgText.Type;
9
+ /** Only the fields the workspace search consumes from ripgrep's JSON stream. */
10
+ export declare const RgMessage: Schema.toTaggedUnion<"type", readonly [Schema.Struct<{
11
+ readonly type: Schema.Literal<"begin">;
12
+ readonly data: Schema.Struct<{
13
+ readonly path: Schema.Union<readonly [Schema.Struct<{
14
+ readonly text: Schema.String;
15
+ }>, Schema.Struct<{
16
+ readonly bytes: Schema.StringFromBase64;
17
+ }>]>;
18
+ }>;
19
+ }>, Schema.Struct<{
20
+ readonly type: Schema.Literal<"match">;
21
+ readonly data: Schema.Struct<{
22
+ readonly path: Schema.Union<readonly [Schema.Struct<{
23
+ readonly text: Schema.String;
24
+ }>, Schema.Struct<{
25
+ readonly bytes: Schema.StringFromBase64;
26
+ }>]>;
27
+ readonly lines: Schema.Union<readonly [Schema.Struct<{
28
+ readonly text: Schema.String;
29
+ }>, Schema.Struct<{
30
+ readonly bytes: Schema.StringFromBase64;
31
+ }>]>;
32
+ readonly line_number: Schema.Int;
33
+ }>;
34
+ }>, Schema.Struct<{
35
+ readonly type: Schema.Literal<"context">;
36
+ readonly data: Schema.Struct<{
37
+ readonly path: Schema.Union<readonly [Schema.Struct<{
38
+ readonly text: Schema.String;
39
+ }>, Schema.Struct<{
40
+ readonly bytes: Schema.StringFromBase64;
41
+ }>]>;
42
+ readonly lines: Schema.Union<readonly [Schema.Struct<{
43
+ readonly text: Schema.String;
44
+ }>, Schema.Struct<{
45
+ readonly bytes: Schema.StringFromBase64;
46
+ }>]>;
47
+ readonly line_number: Schema.Int;
48
+ }>;
49
+ }>, Schema.Struct<{
50
+ readonly type: Schema.Literal<"end">;
51
+ readonly data: Schema.Struct<{
52
+ readonly path: Schema.Union<readonly [Schema.Struct<{
53
+ readonly text: Schema.String;
54
+ }>, Schema.Struct<{
55
+ readonly bytes: Schema.StringFromBase64;
56
+ }>]>;
57
+ readonly binary_offset: Schema.NullOr<Schema.Int>;
58
+ }>;
59
+ }>, Schema.Struct<{
60
+ readonly type: Schema.Literal<"summary">;
61
+ readonly data: Schema.Struct<{}>;
62
+ }>]>;
63
+ export type RgMessage = typeof RgMessage.Type;