disk 0.8.18 → 0.8.19

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.
@@ -0,0 +1,126 @@
1
+ import { Z as FileSystem } from "../disk-BLF7FFqG.cjs";
2
+ import { ZodType, z } from "zod";
3
+
4
+ //#region src/internal/tools.d.ts
5
+ /** What the bound tools operate on: a {@link FileSystem} plus the presentation
6
+ * bits the agent layer adds on top — a layout hint appended to each tool's
7
+ * description, and the container path the disks mount at (so `run_bash`'s working
8
+ * directory lines up with the paths the file tools use). */
9
+ interface ToolContext {
10
+ fs: FileSystem;
11
+ layoutHint: string;
12
+ execRoot: string;
13
+ }
14
+ /** Build a {@link ToolContext} from any filesystem. A workspace is detected by
15
+ * its `diskNames()` method (duck-typed so this layer needn't import the class);
16
+ * its disks are top-level directories, whereas a single disk is rooted at `/`. */
17
+ declare function buildContext(fs: FileSystem): ToolContext;
18
+ interface ToolErrorResult {
19
+ error: {
20
+ message: string;
21
+ status?: number;
22
+ path?: string;
23
+ };
24
+ }
25
+ interface Spec<N extends string, T extends ZodType, V = unknown> {
26
+ name: N;
27
+ description: string;
28
+ schema: T;
29
+ handler: (ctx: ToolContext, args: z.infer<T>) => Promise<V>;
30
+ }
31
+ type AnySpec = Spec<string, any, unknown>;
32
+ declare const SPECS: readonly [Spec<"read_file", z.ZodObject<{
33
+ path: z.ZodString;
34
+ }, z.core.$strip>, {
35
+ content: string;
36
+ bytes: number;
37
+ binary?: undefined;
38
+ } | {
39
+ binary: true;
40
+ bytes: number;
41
+ content?: undefined;
42
+ }>, Spec<"write_file", z.ZodObject<{
43
+ path: z.ZodString;
44
+ content: z.ZodString;
45
+ }, z.core.$strip>, {
46
+ bytes: number;
47
+ }>, Spec<"delete_file", z.ZodObject<{
48
+ path: z.ZodString;
49
+ }, z.core.$strip>, {}>, Spec<"list_files", z.ZodObject<{
50
+ path: z.ZodOptional<z.ZodString>;
51
+ recursive: z.ZodOptional<z.ZodBoolean>;
52
+ }, z.core.$strip>, {
53
+ entries: ({
54
+ type: "dir";
55
+ path: string;
56
+ } | {
57
+ type: "file";
58
+ path: string;
59
+ bytes: number;
60
+ })[];
61
+ isTruncated: true;
62
+ } | {
63
+ entries: ({
64
+ type: "dir";
65
+ path: string;
66
+ } | {
67
+ type: "file";
68
+ path: string;
69
+ bytes: number;
70
+ })[];
71
+ isTruncated?: undefined;
72
+ }>, Spec<"glob", z.ZodObject<{
73
+ pattern: z.ZodString;
74
+ path: z.ZodOptional<z.ZodString>;
75
+ limit: z.ZodOptional<z.ZodNumber>;
76
+ }, z.core.$strip>, {
77
+ content: string;
78
+ count: number;
79
+ path: string;
80
+ truncated: boolean;
81
+ }>, Spec<"grep", z.ZodObject<{
82
+ pattern: z.ZodString;
83
+ path: z.ZodOptional<z.ZodString>;
84
+ recursive: z.ZodOptional<z.ZodBoolean>;
85
+ maxResults: z.ZodOptional<z.ZodNumber>;
86
+ }, z.core.$strip>, {
87
+ matches: {
88
+ path: string;
89
+ line: number;
90
+ text: string;
91
+ }[];
92
+ status: string;
93
+ filesScanned: number;
94
+ }>, Spec<"run_bash", z.ZodObject<{
95
+ command: z.ZodString;
96
+ }, z.core.$strip>, {
97
+ exitCode: number;
98
+ stdout: string;
99
+ stderr: string;
100
+ timing: {
101
+ totalMs: number;
102
+ queueMs: number;
103
+ executeMs: number;
104
+ };
105
+ }>];
106
+ type ToolSpecs = typeof SPECS;
107
+ interface BoundSpec<N extends string, T extends ZodType, V> {
108
+ name: N;
109
+ description: string;
110
+ schema: T;
111
+ invoke: (args: z.infer<T>) => Promise<V | ToolErrorResult>;
112
+ }
113
+ type AnyBoundSpec = BoundSpec<string, any, unknown>;
114
+ type BindSpec<T> = T extends Spec<infer N, infer S, infer V> ? BoundSpec<N, S, V> : never;
115
+ type BindSpecs<Specs extends readonly AnySpec[], Accumulator extends BindSpec<AnySpec>[]> = Specs extends readonly [infer Head extends AnySpec, ...infer Tail extends AnySpec[]] ? BindSpecs<Tail, [BindSpec<Head>, ...Accumulator]> : Accumulator;
116
+ type BoundSpecs = BindSpecs<typeof SPECS, []>;
117
+ type inferSpecInput<T> = T extends BoundSpec<any, infer S, any> ? z.infer<S> : never;
118
+ type inferSpecResult<T> = T extends BoundSpec<any, any, infer V> ? V | ToolErrorResult : never;
119
+ /**
120
+ * Bind the specs to a filesystem. The layout hint is appended to each
121
+ * description so the model knows where files live, and `invoke` returns
122
+ * expected failures as structured error objects rather than throwing.
123
+ */
124
+ declare function bindSpecs(fs: FileSystem): BoundSpecs;
125
+ //#endregion
126
+ export { AnyBoundSpec, BoundSpec, BoundSpecs, ToolContext, ToolErrorResult, ToolSpecs, bindSpecs, buildContext, inferSpecInput, inferSpecResult };
@@ -0,0 +1,126 @@
1
+ import { Z as FileSystem } from "../disk-BLF7FFqG.mjs";
2
+ import { ZodType, z } from "zod";
3
+
4
+ //#region src/internal/tools.d.ts
5
+ /** What the bound tools operate on: a {@link FileSystem} plus the presentation
6
+ * bits the agent layer adds on top — a layout hint appended to each tool's
7
+ * description, and the container path the disks mount at (so `run_bash`'s working
8
+ * directory lines up with the paths the file tools use). */
9
+ interface ToolContext {
10
+ fs: FileSystem;
11
+ layoutHint: string;
12
+ execRoot: string;
13
+ }
14
+ /** Build a {@link ToolContext} from any filesystem. A workspace is detected by
15
+ * its `diskNames()` method (duck-typed so this layer needn't import the class);
16
+ * its disks are top-level directories, whereas a single disk is rooted at `/`. */
17
+ declare function buildContext(fs: FileSystem): ToolContext;
18
+ interface ToolErrorResult {
19
+ error: {
20
+ message: string;
21
+ status?: number;
22
+ path?: string;
23
+ };
24
+ }
25
+ interface Spec<N extends string, T extends ZodType, V = unknown> {
26
+ name: N;
27
+ description: string;
28
+ schema: T;
29
+ handler: (ctx: ToolContext, args: z.infer<T>) => Promise<V>;
30
+ }
31
+ type AnySpec = Spec<string, any, unknown>;
32
+ declare const SPECS: readonly [Spec<"read_file", z.ZodObject<{
33
+ path: z.ZodString;
34
+ }, z.core.$strip>, {
35
+ content: string;
36
+ bytes: number;
37
+ binary?: undefined;
38
+ } | {
39
+ binary: true;
40
+ bytes: number;
41
+ content?: undefined;
42
+ }>, Spec<"write_file", z.ZodObject<{
43
+ path: z.ZodString;
44
+ content: z.ZodString;
45
+ }, z.core.$strip>, {
46
+ bytes: number;
47
+ }>, Spec<"delete_file", z.ZodObject<{
48
+ path: z.ZodString;
49
+ }, z.core.$strip>, {}>, Spec<"list_files", z.ZodObject<{
50
+ path: z.ZodOptional<z.ZodString>;
51
+ recursive: z.ZodOptional<z.ZodBoolean>;
52
+ }, z.core.$strip>, {
53
+ entries: ({
54
+ type: "dir";
55
+ path: string;
56
+ } | {
57
+ type: "file";
58
+ path: string;
59
+ bytes: number;
60
+ })[];
61
+ isTruncated: true;
62
+ } | {
63
+ entries: ({
64
+ type: "dir";
65
+ path: string;
66
+ } | {
67
+ type: "file";
68
+ path: string;
69
+ bytes: number;
70
+ })[];
71
+ isTruncated?: undefined;
72
+ }>, Spec<"glob", z.ZodObject<{
73
+ pattern: z.ZodString;
74
+ path: z.ZodOptional<z.ZodString>;
75
+ limit: z.ZodOptional<z.ZodNumber>;
76
+ }, z.core.$strip>, {
77
+ content: string;
78
+ count: number;
79
+ path: string;
80
+ truncated: boolean;
81
+ }>, Spec<"grep", z.ZodObject<{
82
+ pattern: z.ZodString;
83
+ path: z.ZodOptional<z.ZodString>;
84
+ recursive: z.ZodOptional<z.ZodBoolean>;
85
+ maxResults: z.ZodOptional<z.ZodNumber>;
86
+ }, z.core.$strip>, {
87
+ matches: {
88
+ path: string;
89
+ line: number;
90
+ text: string;
91
+ }[];
92
+ status: string;
93
+ filesScanned: number;
94
+ }>, Spec<"run_bash", z.ZodObject<{
95
+ command: z.ZodString;
96
+ }, z.core.$strip>, {
97
+ exitCode: number;
98
+ stdout: string;
99
+ stderr: string;
100
+ timing: {
101
+ totalMs: number;
102
+ queueMs: number;
103
+ executeMs: number;
104
+ };
105
+ }>];
106
+ type ToolSpecs = typeof SPECS;
107
+ interface BoundSpec<N extends string, T extends ZodType, V> {
108
+ name: N;
109
+ description: string;
110
+ schema: T;
111
+ invoke: (args: z.infer<T>) => Promise<V | ToolErrorResult>;
112
+ }
113
+ type AnyBoundSpec = BoundSpec<string, any, unknown>;
114
+ type BindSpec<T> = T extends Spec<infer N, infer S, infer V> ? BoundSpec<N, S, V> : never;
115
+ type BindSpecs<Specs extends readonly AnySpec[], Accumulator extends BindSpec<AnySpec>[]> = Specs extends readonly [infer Head extends AnySpec, ...infer Tail extends AnySpec[]] ? BindSpecs<Tail, [BindSpec<Head>, ...Accumulator]> : Accumulator;
116
+ type BoundSpecs = BindSpecs<typeof SPECS, []>;
117
+ type inferSpecInput<T> = T extends BoundSpec<any, infer S, any> ? z.infer<S> : never;
118
+ type inferSpecResult<T> = T extends BoundSpec<any, any, infer V> ? V | ToolErrorResult : never;
119
+ /**
120
+ * Bind the specs to a filesystem. The layout hint is appended to each
121
+ * description so the model knows where files live, and `invoke` returns
122
+ * expected failures as structured error objects rather than throwing.
123
+ */
124
+ declare function bindSpecs(fs: FileSystem): BoundSpecs;
125
+ //#endregion
126
+ export { AnyBoundSpec, BoundSpec, BoundSpecs, ToolContext, ToolErrorResult, ToolSpecs, bindSpecs, buildContext, inferSpecInput, inferSpecResult };
@@ -0,0 +1,326 @@
1
+ import { t as toSegments } from "../paths-Br9snUhy.mjs";
2
+ import { z } from "zod";
3
+ //#region src/internal/tools.ts
4
+ /** Translate an agent-facing path to the filesystem key the {@link FileSystem}
5
+ * methods take. Strips the container mount root (`/mnt` for a single disk,
6
+ * `/mnt/archil` for a workspace) so a path copied from a `run_bash` command
7
+ * resolves the same, drops the leading slash, and resolves `.`/`..`. For a
8
+ * workspace the result's first segment is the disk name (which the workspace
9
+ * routes on); for a single disk it is a plain disk key. */
10
+ function toKey(ctx, path) {
11
+ return toSegments(path, ctx.execRoot).join("/");
12
+ }
13
+ /** Build a {@link ToolContext} from any filesystem. A workspace is detected by
14
+ * its `diskNames()` method (duck-typed so this layer needn't import the class);
15
+ * its disks are top-level directories, whereas a single disk is rooted at `/`. */
16
+ function buildContext(fs) {
17
+ const withNames = fs;
18
+ if (typeof withNames.diskNames === "function") {
19
+ const names = withNames.diskNames();
20
+ return {
21
+ fs,
22
+ execRoot: "/mnt/archil",
23
+ layoutHint: `Files live on these disks, each a top-level directory: ${names.map((n) => `/${n}/…`).join(", ")}. Use absolute paths like /${names[0] ?? "data"}/reports/q1.csv.`
24
+ };
25
+ }
26
+ return {
27
+ fs,
28
+ execRoot: "/mnt",
29
+ layoutHint: "Files live under / (the disk root). Use absolute paths like /reports/q1.csv."
30
+ };
31
+ }
32
+ /** Root a grep match path with a leading slash. A workspace already roots paths
33
+ * by disk (`/data/...`); a single disk reports disk-relative keys (`reports/...`),
34
+ * so prefix `/` to present both consistently. */
35
+ function rootMatchPath(file) {
36
+ return file.startsWith("/") ? file : `/${file}`;
37
+ }
38
+ const DEFAULT_GLOB_LIMIT = 100;
39
+ const MAX_GLOB_LIMIT = 1e3;
40
+ const MAX_GLOB_OUTPUT_BYTES = 50 * 1024;
41
+ function normalizeSearchPath(ctx, path) {
42
+ const key = toKey(ctx, path);
43
+ return {
44
+ key,
45
+ path: rootMatchPath(key)
46
+ };
47
+ }
48
+ function globLimit(limit) {
49
+ return Math.min(Math.max(1, limit ?? DEFAULT_GLOB_LIMIT), MAX_GLOB_LIMIT);
50
+ }
51
+ function isGitPath(path) {
52
+ return path.split("/").includes(".git");
53
+ }
54
+ function basename(path) {
55
+ return path.split("/").pop() ?? path;
56
+ }
57
+ function relativeToSearchPath(key, searchKey) {
58
+ if (!searchKey) return key;
59
+ return key.slice(searchKey.length).replace(/^\/+/, "");
60
+ }
61
+ function globMatcher(pattern) {
62
+ const regexes = expandBraces(pattern).map((p) => globToRegExp(p));
63
+ return (path) => regexes.some((regex) => regex.test(path));
64
+ }
65
+ function expandBraces(pattern) {
66
+ const start = pattern.indexOf("{");
67
+ if (start < 0) return [pattern];
68
+ const end = pattern.indexOf("}", start + 1);
69
+ if (end < 0) return [pattern];
70
+ const before = pattern.slice(0, start);
71
+ const after = pattern.slice(end + 1);
72
+ return pattern.slice(start + 1, end).split(",").flatMap((part) => expandBraces(`${before}${part}${after}`));
73
+ }
74
+ function globToRegExp(pattern) {
75
+ let source = "^";
76
+ for (let i = 0; i < pattern.length; i += 1) {
77
+ const char = pattern[i];
78
+ if (char === "*") {
79
+ if (pattern[i + 1] === "*") {
80
+ i += 1;
81
+ if (pattern[i + 1] === "/") {
82
+ i += 1;
83
+ source += "(?:[^/]+/)*";
84
+ } else source += ".*";
85
+ } else source += "[^/]*";
86
+ continue;
87
+ }
88
+ if (char === "?") {
89
+ source += "[^/]";
90
+ continue;
91
+ }
92
+ if (char === "[") {
93
+ const end = pattern.indexOf("]", i + 1);
94
+ if (end > i + 1) {
95
+ const raw = pattern.slice(i + 1, end);
96
+ const negated = raw.startsWith("!");
97
+ source += `[${negated ? "^" : ""}${escapeCharacterClass(negated ? raw.slice(1) : raw)}]`;
98
+ i = end;
99
+ continue;
100
+ }
101
+ }
102
+ source += escapeRegExp(char);
103
+ }
104
+ return new RegExp(`${source}$`);
105
+ }
106
+ function escapeRegExp(value) {
107
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
108
+ }
109
+ function escapeCharacterClass(value) {
110
+ return value.replace(/[\\\]]/g, "\\$&");
111
+ }
112
+ function matchesGlob(pattern, key, searchKey, absolute) {
113
+ const matcher = globMatcher(pattern);
114
+ if (absolute) return matcher(key);
115
+ const relative = relativeToSearchPath(key, searchKey);
116
+ if (pattern.includes("/")) return matcher(relative) || matcher(key);
117
+ return matcher(basename(relative));
118
+ }
119
+ function formatGlobResult(paths, limit, listingTruncated, path) {
120
+ const limited = paths.slice(0, limit);
121
+ const lines = [];
122
+ let bytes = 0;
123
+ let outputTruncated = false;
124
+ for (const line of limited) {
125
+ const lineBytes = new TextEncoder().encode(line).length + 1;
126
+ if (bytes + lineBytes > MAX_GLOB_OUTPUT_BYTES && lines.length > 0) {
127
+ outputTruncated = true;
128
+ break;
129
+ }
130
+ lines.push(line);
131
+ bytes += lineBytes;
132
+ }
133
+ if (lines.length === 0) return {
134
+ content: "No files found",
135
+ count: 0,
136
+ path,
137
+ truncated: listingTruncated
138
+ };
139
+ const truncated = listingTruncated || paths.length > limit || outputTruncated;
140
+ const count = lines.length;
141
+ if (truncated) {
142
+ lines.push("");
143
+ lines.push(`(Results truncated: showing first ${count} results out of more. Use a more specific path or pattern to narrow results.)`);
144
+ }
145
+ return {
146
+ content: lines.join("\n"),
147
+ count,
148
+ path,
149
+ truncated
150
+ };
151
+ }
152
+ function grepStatus(reason, matches) {
153
+ switch (reason) {
154
+ case "completed": return "Search completed.";
155
+ case "max_results": return `More matches exist; returned ${matches}.`;
156
+ case "deadline": return "Search hit the time limit; results may be incomplete.";
157
+ case "incomplete": return "Some files could not be searched; results may be incomplete.";
158
+ case "list_failed": return "Listing failed for part of the tree; results may be partial.";
159
+ }
160
+ }
161
+ function defineSpec(spec) {
162
+ return spec;
163
+ }
164
+ const SPECS = [
165
+ defineSpec({
166
+ name: "read_file",
167
+ description: "Read the contents of a text file and return them.",
168
+ schema: z.object({ path: z.string().describe("Path to the file from the filesystem root, e.g. /reports/q1.csv.") }),
169
+ async handler(ctx, args) {
170
+ const path = args.path;
171
+ const bytes = await ctx.fs.getObject(toKey(ctx, path));
172
+ try {
173
+ return {
174
+ content: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
175
+ bytes: bytes.length
176
+ };
177
+ } catch {
178
+ return {
179
+ binary: true,
180
+ bytes: bytes.length
181
+ };
182
+ }
183
+ }
184
+ }),
185
+ defineSpec({
186
+ name: "write_file",
187
+ description: "Create or overwrite a file with the given text content.",
188
+ schema: z.object({
189
+ path: z.string().describe("Path to the file from the filesystem root, e.g. /reports/q1.csv."),
190
+ content: z.string().describe("Full contents to write.")
191
+ }),
192
+ async handler(ctx, args) {
193
+ await ctx.fs.putObject(toKey(ctx, args.path), args.content);
194
+ return { bytes: new TextEncoder().encode(args.content).length };
195
+ }
196
+ }),
197
+ defineSpec({
198
+ name: "delete_file",
199
+ description: "Delete a file. Succeeds even if the file does not exist.",
200
+ schema: z.object({ path: z.string().describe("Path to the file from the filesystem root, e.g. /reports/q1.csv.") }),
201
+ async handler(ctx, args) {
202
+ await ctx.fs.deleteObject(toKey(ctx, args.path));
203
+ return {};
204
+ }
205
+ }),
206
+ defineSpec({
207
+ name: "list_files",
208
+ description: "List files and subdirectories. Omit 'path' to list from the root. Set 'recursive' to list the whole subtree.",
209
+ schema: z.object({
210
+ path: z.string().optional().describe("Directory to list (optional)."),
211
+ recursive: z.boolean().optional().describe("List the full subtree.")
212
+ }),
213
+ async handler(ctx, args) {
214
+ const path = args.path || "/";
215
+ const recursive = args.recursive ?? false;
216
+ const dir = toKey(ctx, path);
217
+ const result = await ctx.fs.listObjects(dir ? `${dir}/` : void 0, { recursive });
218
+ const entries = [...result.commonPrefixes.map((cp) => ({
219
+ type: "dir",
220
+ path: `/${cp.replace(/\/+$/, "")}/`
221
+ })), ...result.objects.map((obj) => ({
222
+ type: "file",
223
+ path: `/${obj.key}`,
224
+ bytes: obj.size
225
+ }))];
226
+ entries.sort((a, b) => a.path.localeCompare(b.path) || a.type.localeCompare(b.type));
227
+ return result.isTruncated ? {
228
+ entries,
229
+ isTruncated: true
230
+ } : { entries };
231
+ }
232
+ }),
233
+ defineSpec({
234
+ name: "glob",
235
+ description: "Find files by glob pattern. Use this to look up filenames by pattern; use grep to search file contents.",
236
+ schema: z.object({
237
+ pattern: z.string().describe("Glob pattern to match, e.g. \"**/*.ts\" or \"src/**/*.js\"."),
238
+ path: z.string().optional().describe("Directory to search from (optional)."),
239
+ limit: z.number().int().optional().describe("Maximum number of results to return (default 100, max 1000).")
240
+ }),
241
+ async handler(ctx, args) {
242
+ const search = normalizeSearchPath(ctx, args.path || "/");
243
+ const absolutePattern = args.pattern.startsWith("/");
244
+ const pattern = absolutePattern ? toKey(ctx, args.pattern) : args.pattern;
245
+ const limit = globLimit(args.limit);
246
+ const result = await ctx.fs.listObjects(search.key ? `${search.key}/` : void 0, { recursive: true });
247
+ return formatGlobResult(result.objects.map((obj) => obj.key).filter((key) => !isGitPath(key) && matchesGlob(pattern, key, search.key, absolutePattern)).map(rootMatchPath).sort(), limit, result.isTruncated, search.path);
248
+ }
249
+ }),
250
+ defineSpec({
251
+ name: "grep",
252
+ description: "Search file contents for an extended regular expression and return matching lines as path:line: text. Searches recursively by default.",
253
+ schema: z.object({
254
+ pattern: z.string().describe("Extended regex (grep -E)."),
255
+ path: z.string().optional().describe("Directory to search (optional)."),
256
+ recursive: z.boolean().optional().describe("Search subdirectories (default true)."),
257
+ maxResults: z.number().int().optional().describe("Cap on matches (default 200).")
258
+ }),
259
+ async handler(ctx, args) {
260
+ const path = args.path || "/";
261
+ const recursive = args.recursive ?? true;
262
+ const maxResults = args.maxResults ?? 200;
263
+ const result = await ctx.fs.grep({
264
+ pattern: args.pattern,
265
+ directory: toKey(ctx, path),
266
+ recursive,
267
+ maxResults
268
+ });
269
+ return {
270
+ matches: result.matches.map((m) => ({
271
+ path: rootMatchPath(m.file),
272
+ line: m.line,
273
+ text: m.text
274
+ })),
275
+ status: grepStatus(result.stoppedReason, result.matches.length),
276
+ filesScanned: result.filesScanned
277
+ };
278
+ }
279
+ }),
280
+ defineSpec({
281
+ name: "run_bash",
282
+ description: "Run a bash command in an ephemeral sandbox with the filesystem mounted. The working directory is the filesystem root, so paths match the other tools. Returns the exit code, stdout, and stderr.",
283
+ schema: z.object({ command: z.string().describe("The bash command to run.") }),
284
+ async handler(ctx, args) {
285
+ const result = await ctx.fs.exec(`cd ${ctx.execRoot} && ${args.command}`);
286
+ return {
287
+ exitCode: result.exitCode,
288
+ stdout: result.stdout,
289
+ stderr: result.stderr,
290
+ timing: result.timing
291
+ };
292
+ }
293
+ })
294
+ ];
295
+ function formatError(error, args) {
296
+ const status = error?.status;
297
+ if (status === 404 && typeof args.path === "string") return { error: {
298
+ message: "file not found",
299
+ status,
300
+ path: args.path
301
+ } };
302
+ return { error: { message: error instanceof Error ? error.message : String(error) } };
303
+ }
304
+ /**
305
+ * Bind the specs to a filesystem. The layout hint is appended to each
306
+ * description so the model knows where files live, and `invoke` returns
307
+ * expected failures as structured error objects rather than throwing.
308
+ */
309
+ function bindSpecs(fs) {
310
+ const ctx = buildContext(fs);
311
+ return SPECS.map((spec) => ({
312
+ name: spec.name,
313
+ description: `${spec.description} ${ctx.layoutHint}`,
314
+ schema: spec.schema,
315
+ async invoke(args) {
316
+ const input = args ?? {};
317
+ try {
318
+ return await spec.handler(ctx, input);
319
+ } catch (error) {
320
+ return formatError(error, input);
321
+ }
322
+ }
323
+ }));
324
+ }
325
+ //#endregion
326
+ export { bindSpecs, buildContext };
@@ -0,0 +1,33 @@
1
+ //#region src/paths.ts
2
+ /**
3
+ * Normalize a filesystem path to its segments, resolving "." and ".." (so a
4
+ * path means the same as in a shell, and ".." can't escape above the root) and
5
+ * optionally stripping a shell-style mount prefix (e.g. "/mnt" or "/mnt/archil")
6
+ * so a path copied from a `run_bash` command resolves the same way.
7
+ *
8
+ * toSegments("/a/b/../c") -> ["a", "c"]
9
+ * toSegments("/mnt/x", "/mnt") -> ["x"]
10
+ * toSegments("/../../etc/x") -> ["etc", "x"]
11
+ */
12
+ function toSegments(path, containerRoot) {
13
+ let p = (path ?? "").trim();
14
+ if (containerRoot) {
15
+ const root = containerRoot.replace(/\/+$/, "");
16
+ if (p === root) p = "/";
17
+ else if (p.startsWith(root + "/")) p = p.slice(root.length);
18
+ }
19
+ const resolved = [];
20
+ for (const seg of p.split("/")) {
21
+ if (seg === "" || seg === ".") continue;
22
+ if (seg === "..") resolved.pop();
23
+ else resolved.push(seg);
24
+ }
25
+ return resolved;
26
+ }
27
+ //#endregion
28
+ Object.defineProperty(exports, "toSegments", {
29
+ enumerable: true,
30
+ get: function() {
31
+ return toSegments;
32
+ }
33
+ });
@@ -0,0 +1,28 @@
1
+ //#region src/paths.ts
2
+ /**
3
+ * Normalize a filesystem path to its segments, resolving "." and ".." (so a
4
+ * path means the same as in a shell, and ".." can't escape above the root) and
5
+ * optionally stripping a shell-style mount prefix (e.g. "/mnt" or "/mnt/archil")
6
+ * so a path copied from a `run_bash` command resolves the same way.
7
+ *
8
+ * toSegments("/a/b/../c") -> ["a", "c"]
9
+ * toSegments("/mnt/x", "/mnt") -> ["x"]
10
+ * toSegments("/../../etc/x") -> ["etc", "x"]
11
+ */
12
+ function toSegments(path, containerRoot) {
13
+ let p = (path ?? "").trim();
14
+ if (containerRoot) {
15
+ const root = containerRoot.replace(/\/+$/, "");
16
+ if (p === root) p = "/";
17
+ else if (p.startsWith(root + "/")) p = p.slice(root.length);
18
+ }
19
+ const resolved = [];
20
+ for (const seg of p.split("/")) {
21
+ if (seg === "" || seg === ".") continue;
22
+ if (seg === "..") resolved.pop();
23
+ else resolved.push(seg);
24
+ }
25
+ return resolved;
26
+ }
27
+ //#endregion
28
+ export { toSegments as t };