disk 0.8.18 → 0.8.20

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,287 @@
1
+ import { A as AwsStsUser, B as ExecRequest, C as S3Object, D as effectiveUploadPartSize, E as UploadPart, F as DiskMetrics, G as MountConfigResponse, H as GrepMatch, I as DiskResponse, J as S3CompatibleMount, K as MountResponse, L as DiskStatus, M as ConnectedClient, N as CreateApiTokenRequest, O as ApiTokenResponse, P as CreateDiskRequest, Q as ApiClient, R as DiskUser, S as PutObjectResult, T as ShareUrlResult, U as GrepStoppedReason, V as GCSMount, W as MountConfig, X as TokenUser, Y as S3Mount, Z as FileSystem, _ as MultipartUploadSummary, a as Disk, b as PartListing, c as GrepOptions, d as ListObjectsOptions, f as ListObjectsResult, g as MultipartUploadListing, h as MultipartUpload, i as DeleteObjectsResult, j as AzureBlobMount, k as AuthorizedUser, l as GrepResult, m as MountOptions, n as DeleteObjectsError, o as DiskMultipart, p as ListPartsOptions, q as R2Mount, r as DeleteObjectsOptions, s as ExecResult, t as CompletedMultipartUpload, u as ListMultipartUploadsOptions, v as ObjectMetadata, w as ShareUrlOptions, x as PutObjectOptions, y as PartInfo, z as ExecDiskResult } from "./disk-BLF7FFqG.mjs";
2
+
3
+ //#region src/disks.d.ts
4
+ interface ListDisksOptions {
5
+ /** Cap on the total number of disks returned. */
6
+ limit?: number;
7
+ /** Resume listing from a previous page's `nextCursor`. */
8
+ cursor?: string;
9
+ name?: string;
10
+ }
11
+ interface DiskListPage {
12
+ disks: Disk[];
13
+ /**
14
+ * Set when more disks remain beyond this page; pass it back as `cursor` to
15
+ * fetch the next one. Undefined on the last page.
16
+ */
17
+ nextCursor?: string;
18
+ }
19
+ interface CreateDiskResult {
20
+ disk: Disk;
21
+ token: string | null;
22
+ tokenIdentifier: string | null;
23
+ authorizedUsers: AuthorizedUser[];
24
+ }
25
+ declare class Disks {
26
+ /** @internal */
27
+ private readonly _client;
28
+ /** @internal */
29
+ private readonly _region;
30
+ /** @internal */
31
+ private readonly _s3BaseUrl?;
32
+ /** @internal */
33
+ constructor(client: ApiClient, region: string, s3BaseUrl?: string);
34
+ /**
35
+ * List the account's disks. Fetches in cursor-driven pages (bounded server
36
+ * work per request) and follows `nextCursor` until exhausted, so the result
37
+ * is complete even for very large accounts. Use `limit` to cap the total, or
38
+ * {@link listPage} to walk pages yourself.
39
+ */
40
+ list(opts?: ListDisksOptions): Promise<Disk[]>;
41
+ /**
42
+ * Fetch a single page of disks. `nextCursor` on the result resumes the
43
+ * listing (it can also be persisted, e.g. across requests of a paginated UI).
44
+ */
45
+ listPage(opts?: ListDisksOptions): Promise<DiskListPage>;
46
+ get(id: string): Promise<Disk>;
47
+ /**
48
+ * Create a new disk with an auto-generated mount token.
49
+ *
50
+ * Returns the Disk, the one-time token (save it — it cannot be retrieved
51
+ * again), and the token identifier for later management.
52
+ */
53
+ create(req: CreateDiskRequest): Promise<CreateDiskResult>;
54
+ }
55
+ //#endregion
56
+ //#region src/tokens.d.ts
57
+ interface ListTokensOptions {
58
+ limit?: number;
59
+ cursor?: string;
60
+ }
61
+ declare class Tokens {
62
+ /** @internal */
63
+ private readonly _client;
64
+ /** @internal */
65
+ constructor(client: ApiClient);
66
+ list(opts?: ListTokensOptions): Promise<ApiTokenResponse[]>;
67
+ create(req: CreateApiTokenRequest): Promise<ApiTokenResponse & {
68
+ token?: string;
69
+ }>;
70
+ delete(id: string): Promise<void>;
71
+ }
72
+ //#endregion
73
+ //#region src/workspace.d.ts
74
+ /** Anything that can run a multi-disk exec — i.e. an `Archil` client. A narrow
75
+ * structural type so this module doesn't import the Archil class (avoiding a
76
+ * value-import cycle with archil.ts). */
77
+ interface WorkspaceExecCapable {
78
+ exec(opts: ExecOptions): Promise<ExecResult>;
79
+ }
80
+ /**
81
+ * A key-routed filesystem spanning several Archil disks. Each disk is mounted
82
+ * under a name and addressed as the first segment of a key (`<name>/...`);
83
+ * `getObject`, `putObject`, `deleteObject`, `listObjects`, and `grep` route to
84
+ * the right disk by that segment (and `listObjects` / `grep` fan out across all
85
+ * of them when the key/prefix doesn't name one). Implements the same
86
+ * {@link FileSystem} surface as a single {@link Disk}, so it works anywhere a
87
+ * disk does. Mounts can be changed at runtime with {@link addDisk} /
88
+ * {@link removeDisk}.
89
+ */
90
+ declare class Workspace implements FileSystem {
91
+ private readonly client;
92
+ private readonly mounts;
93
+ /** @internal Use `archil.workspace({ ... })`. */
94
+ constructor(client: WorkspaceExecCapable, mounts: Record<string, ExecMount>);
95
+ /** Mount (or replace) a disk at `name`; its objects are addressed as
96
+ * `<name>/...`. Accepts a `Disk` or a mount spec (read-only / subdirectory /
97
+ * conditional / delegation checkouts); a bare disk-id string is rejected —
98
+ * fetch the disk first. */
99
+ addDisk(name: string, disk: ExecMount): this;
100
+ /** Unmount the disk at `name`. Returns whether a disk was removed. Refuses to
101
+ * remove the last disk — a workspace must always have at least one (the same
102
+ * invariant the constructor enforces), else fan-out/exec would have nothing to
103
+ * route to. */
104
+ removeDisk(name: string): boolean;
105
+ /** The names of the currently-mounted disks. */
106
+ diskNames(): string[];
107
+ private unknownDisk;
108
+ /** Resolve a workspace key (`<name>/...`) to the disk and the disk-relative key. */
109
+ private route;
110
+ /** Mounts touched by a key prefix; an empty prefix fans out to all of them. */
111
+ private covered;
112
+ /** Map a workspace-relative key to the disk's key, applying the mount's subdirectory. */
113
+ private diskKey;
114
+ /** Map a disk key back to its workspace key (`<name>/...`), stripping the mount subdirectory. */
115
+ private abs;
116
+ getObject(key: string): Promise<Uint8Array>;
117
+ putObject(key: string, body: string | Uint8Array | ArrayBuffer, contentType?: string): Promise<PutObjectResult>;
118
+ deleteObject(key: string): Promise<void>;
119
+ listObjects(prefix?: string, opts?: ListObjectsOptions): Promise<ListObjectsResult>;
120
+ grep(opts: GrepOptions): Promise<GrepResult>;
121
+ exec(command: string): Promise<ExecResult>;
122
+ }
123
+ //#endregion
124
+ //#region src/archil.d.ts
125
+ interface ArchilOptions {
126
+ /** API key. Falls back to ARCHIL_API_KEY env var if not provided. */
127
+ apiKey?: string;
128
+ /** Region. Falls back to ARCHIL_REGION env var if not provided. */
129
+ region?: string;
130
+ /** Override the control plane base URL (useful for testing). */
131
+ baseUrl?: string;
132
+ /**
133
+ * Override the S3-compatible API base URL used by Disk#getObject /
134
+ * putObject / deleteObject. Falls back to ARCHIL_S3_BASE_URL, then to the
135
+ * control plane URL with its `control.` hostname prefix swapped for `s3.`.
136
+ */
137
+ s3BaseUrl?: string;
138
+ }
139
+ /**
140
+ * Options that apply to a single mounted disk in an exec request.
141
+ * Use this object form when you need to pin the mount to a subdirectory
142
+ * of the disk, mount it read-only, mount it in conditional mode, or request
143
+ * delegation checkouts; for the default case (mount the disk's root,
144
+ * read-write with no checkout), pass a `Disk` or disk-id string instead.
145
+ */
146
+ interface ExecMountSpec {
147
+ /** Disk to mount, by `Disk` instance or raw disk id string. */
148
+ disk: Disk | string;
149
+ /**
150
+ * Subdirectory of the disk to expose at the mountpoint. Must be a
151
+ * relative path with no `.` or `..` segments. When omitted, the disk's
152
+ * root is exposed.
153
+ */
154
+ subdirectory?: string;
155
+ /**
156
+ * When true, mount the disk read-only inside the container. Writes
157
+ * against the mount fail with EROFS. Defaults to false.
158
+ */
159
+ readOnly?: boolean;
160
+ /**
161
+ * When true, mount the disk in conditional mode, where mutating operations
162
+ * are sent directly to the server without a delegation checkout. This
163
+ * enables concurrent writes from multiple clients to the same disk.
164
+ * Defaults to false.
165
+ */
166
+ conditional?: boolean;
167
+ /**
168
+ * Milliseconds to wait in the delegation queue for each requested checkout.
169
+ * If set without `checkoutPaths`, the exposed mount root is acquired during
170
+ * mount setup. Cannot be combined with `readOnly: true`.
171
+ */
172
+ queueMs?: number;
173
+ /**
174
+ * Paths relative to this disk's exposed mount root to check out before the
175
+ * command starts. May be set without `queueMs`; those checkouts try
176
+ * immediately without waiting in the delegation queue. Cannot be combined
177
+ * with `readOnly: true`.
178
+ */
179
+ checkoutPaths?: string[];
180
+ }
181
+ /**
182
+ * One disk to mount in an exec request. Either a `Disk`/disk-id string
183
+ * (mounts the disk's root, read-write) or an `ExecMountSpec` object that
184
+ * additionally selects a subdirectory of the disk and/or marks the mount as
185
+ * read-only, conditional, or requests delegation checkouts. Used
186
+ * by Archil#exec, where the map key is the relative path under /mnt/archil at
187
+ * which to mount the disk.
188
+ */
189
+ type ExecMount = Disk | string | ExecMountSpec;
190
+ interface ExecOptions {
191
+ /**
192
+ * Disks to mount, keyed by the relative path under `/mnt/archil` at which
193
+ * to mount each one. At least one entry is required. Paths must be
194
+ * non-empty, non-absolute, and contain no `.` or `..` segments.
195
+ */
196
+ disks: Record<string, ExecMount>;
197
+ /** Shell command to run inside the container. */
198
+ command: string;
199
+ }
200
+ declare class Archil {
201
+ readonly disks: Disks;
202
+ readonly tokens: Tokens;
203
+ /** @internal */
204
+ private readonly _client;
205
+ constructor(opts?: ArchilOptions);
206
+ /**
207
+ * Run a command in a container with multiple disks mounted simultaneously,
208
+ * each at its own relative path under `/mnt/archil`. Blocks until the
209
+ * command completes and returns its stdout, stderr, exit code, and timing.
210
+ */
211
+ exec(opts: ExecOptions): Promise<ExecDiskResult>;
212
+ /**
213
+ * Build an agent filesystem toolset spanning several disks at once. `mounts`
214
+ * maps a relative path to a disk (or `ExecMountSpec`), exactly like
215
+ * {@link exec}; each disk appears under `/mnt/archil/<path>`. Hand the result
216
+ * to a framework adapter (`createDiskTools(workspace)`) to get tools that route
217
+ * file operations by path and fan `grep`/`list_files` out across the disks.
218
+ */
219
+ workspace(mounts: Record<string, ExecMount>): Workspace;
220
+ }
221
+ //#endregion
222
+ //#region src/errors.d.ts
223
+ /**
224
+ * Base class for every error the SDK throws. Catch with `instanceof ArchilError`
225
+ * to handle control-plane and S3 failures uniformly; `status` is the HTTP status
226
+ * code and `code` a machine-readable error code when the server provided one.
227
+ */
228
+ declare class ArchilError extends Error {
229
+ /** HTTP status code associated with the failure. */
230
+ readonly status: number;
231
+ /** Machine-readable error code (e.g. an S3 code like "NoSuchKey"), if known. */
232
+ readonly code?: string;
233
+ constructor(message: string, status: number, code?: string);
234
+ }
235
+ /** Error from the control-plane REST API. */
236
+ declare class ArchilApiError extends ArchilError {
237
+ constructor(message: string, status: number, code?: string);
238
+ }
239
+ /**
240
+ * Error from the S3-compatible object API (getObject/putObject/deleteObject/
241
+ * headObject/listObjects). The gateway returns an S3-style XML `<Error>` body;
242
+ * this surfaces its parts as structured fields (`status`, `code`, `requestId`)
243
+ * rather than a raw blob, while keeping the full body on `raw` for debugging.
244
+ */
245
+ declare class ArchilS3Error extends ArchilError {
246
+ /** S3 request id, if the gateway returned one. */
247
+ readonly requestId?: string;
248
+ /** Raw response body (the XML document), for debugging. */
249
+ readonly raw: string;
250
+ constructor(opts: {
251
+ operation: string;
252
+ statusCode: number;
253
+ statusText?: string;
254
+ code?: string;
255
+ message?: string;
256
+ requestId?: string;
257
+ raw: string;
258
+ });
259
+ }
260
+ //#endregion
261
+ //#region src/version.d.ts
262
+ declare const VERSION: string;
263
+ declare const USER_AGENT: string;
264
+ //#endregion
265
+ //#region src/index.d.ts
266
+ declare function configure(options: ArchilOptions): void;
267
+ declare function createDisk(req: CreateDiskRequest): Promise<CreateDiskResult>;
268
+ declare function listDisks(opts?: ListDisksOptions): Promise<Disk[]>;
269
+ declare function getDisk(id: string): Promise<Disk>;
270
+ declare function listApiKeys(opts?: ListTokensOptions): Promise<ApiTokenResponse[]>;
271
+ declare function createApiKey(req: CreateApiTokenRequest): Promise<ApiTokenResponse & {
272
+ token?: string;
273
+ }>;
274
+ declare function deleteApiKey(id: string): Promise<void>;
275
+ /**
276
+ * Run a command in a container with multiple disks mounted simultaneously,
277
+ * each at its own relative path under `/mnt/archil`. Blocks until the
278
+ * command completes and returns its stdout, stderr, exit code, and timing.
279
+ */
280
+ declare function exec(opts: ExecOptions): Promise<ExecDiskResult>;
281
+ /**
282
+ * Build an agent filesystem toolset spanning several disks, using the
283
+ * module-level client. See {@link Archil.workspace}.
284
+ */
285
+ declare function workspace(mounts: Record<string, ExecMount>): Workspace;
286
+ //#endregion
287
+ export { type ApiTokenResponse, Archil, ArchilApiError, ArchilError, type ArchilOptions, ArchilS3Error, type AuthorizedUser, type AwsStsUser, type AzureBlobMount, type CompletedMultipartUpload, type ConnectedClient, type CreateApiTokenRequest, type CreateDiskRequest, type CreateDiskResult, type DeleteObjectsError, type DeleteObjectsOptions, type DeleteObjectsResult, Disk, type DiskListPage, type DiskMetrics, DiskMultipart, type DiskResponse, type DiskStatus, type DiskUser, Disks, type ExecMount, type ExecMountSpec, type ExecOptions, type ExecRequest, type ExecResult, type FileSystem, type GCSMount, type GrepMatch, type GrepOptions, type GrepResult, type GrepStoppedReason, type ListDisksOptions, type ListMultipartUploadsOptions, type ListObjectsOptions, type ListObjectsResult, type ListPartsOptions, type ListTokensOptions, type MountConfig, type MountConfigResponse, type MountOptions, type MountResponse, type MultipartUpload, type MultipartUploadListing, type MultipartUploadSummary, type ObjectMetadata, type PartInfo, type PartListing, type PutObjectOptions, type PutObjectResult, type R2Mount, type S3CompatibleMount, type S3Mount, type S3Object, type ShareUrlOptions, type ShareUrlResult, type TokenUser, Tokens, USER_AGENT, type UploadPart, VERSION, Workspace, configure, createApiKey, createDisk, deleteApiKey, effectiveUploadPartSize, exec, getDisk, listApiKeys, listDisks, workspace };
package/dist/index.mjs ADDED
@@ -0,0 +1,2 @@
1
+ import { _ as USER_AGENT, a as exec, b as ArchilError, c as listDisks, d as Workspace, f as Tokens, g as effectiveUploadPartSize, h as DiskMultipart, i as deleteApiKey, l as workspace, m as Disk, n as createApiKey, o as getDisk, p as Disks, r as createDisk, s as listApiKeys, t as configure, u as Archil, v as VERSION, x as ArchilS3Error, y as ArchilApiError } from "./src-BuqSzAcO.mjs";
2
+ export { Archil, ArchilApiError, ArchilError, ArchilS3Error, Disk, DiskMultipart, Disks, Tokens, USER_AGENT, VERSION, Workspace, configure, createApiKey, createDisk, deleteApiKey, effectiveUploadPartSize, exec, getDisk, listApiKeys, listDisks, workspace };
@@ -0,0 +1,328 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_paths = require("../paths-1c4QqbOB.cjs");
3
+ let zod = require("zod");
4
+ //#region src/internal/tools.ts
5
+ /** Translate an agent-facing path to the filesystem key the {@link FileSystem}
6
+ * methods take. Strips the container mount root (`/mnt` for a single disk,
7
+ * `/mnt/archil` for a workspace) so a path copied from a `run_bash` command
8
+ * resolves the same, drops the leading slash, and resolves `.`/`..`. For a
9
+ * workspace the result's first segment is the disk name (which the workspace
10
+ * routes on); for a single disk it is a plain disk key. */
11
+ function toKey(ctx, path) {
12
+ return require_paths.toSegments(path, ctx.execRoot).join("/");
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
+ function buildContext(fs) {
18
+ const withNames = fs;
19
+ if (typeof withNames.diskNames === "function") {
20
+ const names = withNames.diskNames();
21
+ return {
22
+ fs,
23
+ execRoot: "/mnt/archil",
24
+ 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.`
25
+ };
26
+ }
27
+ return {
28
+ fs,
29
+ execRoot: "/mnt",
30
+ layoutHint: "Files live under / (the disk root). Use absolute paths like /reports/q1.csv."
31
+ };
32
+ }
33
+ /** Root a grep match path with a leading slash. A workspace already roots paths
34
+ * by disk (`/data/...`); a single disk reports disk-relative keys (`reports/...`),
35
+ * so prefix `/` to present both consistently. */
36
+ function rootMatchPath(file) {
37
+ return file.startsWith("/") ? file : `/${file}`;
38
+ }
39
+ const DEFAULT_GLOB_LIMIT = 100;
40
+ const MAX_GLOB_LIMIT = 1e3;
41
+ const MAX_GLOB_OUTPUT_BYTES = 50 * 1024;
42
+ function normalizeSearchPath(ctx, path) {
43
+ const key = toKey(ctx, path);
44
+ return {
45
+ key,
46
+ path: rootMatchPath(key)
47
+ };
48
+ }
49
+ function globLimit(limit) {
50
+ return Math.min(Math.max(1, limit ?? DEFAULT_GLOB_LIMIT), MAX_GLOB_LIMIT);
51
+ }
52
+ function isGitPath(path) {
53
+ return path.split("/").includes(".git");
54
+ }
55
+ function basename(path) {
56
+ return path.split("/").pop() ?? path;
57
+ }
58
+ function relativeToSearchPath(key, searchKey) {
59
+ if (!searchKey) return key;
60
+ return key.slice(searchKey.length).replace(/^\/+/, "");
61
+ }
62
+ function globMatcher(pattern) {
63
+ const regexes = expandBraces(pattern).map((p) => globToRegExp(p));
64
+ return (path) => regexes.some((regex) => regex.test(path));
65
+ }
66
+ function expandBraces(pattern) {
67
+ const start = pattern.indexOf("{");
68
+ if (start < 0) return [pattern];
69
+ const end = pattern.indexOf("}", start + 1);
70
+ if (end < 0) return [pattern];
71
+ const before = pattern.slice(0, start);
72
+ const after = pattern.slice(end + 1);
73
+ return pattern.slice(start + 1, end).split(",").flatMap((part) => expandBraces(`${before}${part}${after}`));
74
+ }
75
+ function globToRegExp(pattern) {
76
+ let source = "^";
77
+ for (let i = 0; i < pattern.length; i += 1) {
78
+ const char = pattern[i];
79
+ if (char === "*") {
80
+ if (pattern[i + 1] === "*") {
81
+ i += 1;
82
+ if (pattern[i + 1] === "/") {
83
+ i += 1;
84
+ source += "(?:[^/]+/)*";
85
+ } else source += ".*";
86
+ } else source += "[^/]*";
87
+ continue;
88
+ }
89
+ if (char === "?") {
90
+ source += "[^/]";
91
+ continue;
92
+ }
93
+ if (char === "[") {
94
+ const end = pattern.indexOf("]", i + 1);
95
+ if (end > i + 1) {
96
+ const raw = pattern.slice(i + 1, end);
97
+ const negated = raw.startsWith("!");
98
+ source += `[${negated ? "^" : ""}${escapeCharacterClass(negated ? raw.slice(1) : raw)}]`;
99
+ i = end;
100
+ continue;
101
+ }
102
+ }
103
+ source += escapeRegExp(char);
104
+ }
105
+ return new RegExp(`${source}$`);
106
+ }
107
+ function escapeRegExp(value) {
108
+ return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
109
+ }
110
+ function escapeCharacterClass(value) {
111
+ return value.replace(/[\\\]]/g, "\\$&");
112
+ }
113
+ function matchesGlob(pattern, key, searchKey, absolute) {
114
+ const matcher = globMatcher(pattern);
115
+ if (absolute) return matcher(key);
116
+ const relative = relativeToSearchPath(key, searchKey);
117
+ if (pattern.includes("/")) return matcher(relative) || matcher(key);
118
+ return matcher(basename(relative));
119
+ }
120
+ function formatGlobResult(paths, limit, listingTruncated, path) {
121
+ const limited = paths.slice(0, limit);
122
+ const lines = [];
123
+ let bytes = 0;
124
+ let outputTruncated = false;
125
+ for (const line of limited) {
126
+ const lineBytes = new TextEncoder().encode(line).length + 1;
127
+ if (bytes + lineBytes > MAX_GLOB_OUTPUT_BYTES && lines.length > 0) {
128
+ outputTruncated = true;
129
+ break;
130
+ }
131
+ lines.push(line);
132
+ bytes += lineBytes;
133
+ }
134
+ if (lines.length === 0) return {
135
+ content: "No files found",
136
+ count: 0,
137
+ path,
138
+ truncated: listingTruncated
139
+ };
140
+ const truncated = listingTruncated || paths.length > limit || outputTruncated;
141
+ const count = lines.length;
142
+ if (truncated) {
143
+ lines.push("");
144
+ lines.push(`(Results truncated: showing first ${count} results out of more. Use a more specific path or pattern to narrow results.)`);
145
+ }
146
+ return {
147
+ content: lines.join("\n"),
148
+ count,
149
+ path,
150
+ truncated
151
+ };
152
+ }
153
+ function grepStatus(reason, matches) {
154
+ switch (reason) {
155
+ case "completed": return "Search completed.";
156
+ case "max_results": return `More matches exist; returned ${matches}.`;
157
+ case "deadline": return "Search hit the time limit; results may be incomplete.";
158
+ case "incomplete": return "Some files could not be searched; results may be incomplete.";
159
+ case "list_failed": return "Listing failed for part of the tree; results may be partial.";
160
+ }
161
+ }
162
+ function defineSpec(spec) {
163
+ return spec;
164
+ }
165
+ const SPECS = [
166
+ defineSpec({
167
+ name: "read_file",
168
+ description: "Read the contents of a text file and return them.",
169
+ schema: zod.z.object({ path: zod.z.string().describe("Path to the file from the filesystem root, e.g. /reports/q1.csv.") }),
170
+ async handler(ctx, args) {
171
+ const path = args.path;
172
+ const bytes = await ctx.fs.getObject(toKey(ctx, path));
173
+ try {
174
+ return {
175
+ content: new TextDecoder("utf-8", { fatal: true }).decode(bytes),
176
+ bytes: bytes.length
177
+ };
178
+ } catch {
179
+ return {
180
+ binary: true,
181
+ bytes: bytes.length
182
+ };
183
+ }
184
+ }
185
+ }),
186
+ defineSpec({
187
+ name: "write_file",
188
+ description: "Create or overwrite a file with the given text content.",
189
+ schema: zod.z.object({
190
+ path: zod.z.string().describe("Path to the file from the filesystem root, e.g. /reports/q1.csv."),
191
+ content: zod.z.string().describe("Full contents to write.")
192
+ }),
193
+ async handler(ctx, args) {
194
+ await ctx.fs.putObject(toKey(ctx, args.path), args.content);
195
+ return { bytes: new TextEncoder().encode(args.content).length };
196
+ }
197
+ }),
198
+ defineSpec({
199
+ name: "delete_file",
200
+ description: "Delete a file. Succeeds even if the file does not exist.",
201
+ schema: zod.z.object({ path: zod.z.string().describe("Path to the file from the filesystem root, e.g. /reports/q1.csv.") }),
202
+ async handler(ctx, args) {
203
+ await ctx.fs.deleteObject(toKey(ctx, args.path));
204
+ return {};
205
+ }
206
+ }),
207
+ defineSpec({
208
+ name: "list_files",
209
+ description: "List files and subdirectories. Omit 'path' to list from the root. Set 'recursive' to list the whole subtree.",
210
+ schema: zod.z.object({
211
+ path: zod.z.string().optional().describe("Directory to list (optional)."),
212
+ recursive: zod.z.boolean().optional().describe("List the full subtree.")
213
+ }),
214
+ async handler(ctx, args) {
215
+ const path = args.path || "/";
216
+ const recursive = args.recursive ?? false;
217
+ const dir = toKey(ctx, path);
218
+ const result = await ctx.fs.listObjects(dir ? `${dir}/` : void 0, { recursive });
219
+ const entries = [...result.commonPrefixes.map((cp) => ({
220
+ type: "dir",
221
+ path: `/${cp.replace(/\/+$/, "")}/`
222
+ })), ...result.objects.map((obj) => ({
223
+ type: "file",
224
+ path: `/${obj.key}`,
225
+ bytes: obj.size
226
+ }))];
227
+ entries.sort((a, b) => a.path.localeCompare(b.path) || a.type.localeCompare(b.type));
228
+ return result.isTruncated ? {
229
+ entries,
230
+ isTruncated: true
231
+ } : { entries };
232
+ }
233
+ }),
234
+ defineSpec({
235
+ name: "glob",
236
+ description: "Find files by glob pattern. Use this to look up filenames by pattern; use grep to search file contents.",
237
+ schema: zod.z.object({
238
+ pattern: zod.z.string().describe("Glob pattern to match, e.g. \"**/*.ts\" or \"src/**/*.js\"."),
239
+ path: zod.z.string().optional().describe("Directory to search from (optional)."),
240
+ limit: zod.z.number().int().optional().describe("Maximum number of results to return (default 100, max 1000).")
241
+ }),
242
+ async handler(ctx, args) {
243
+ const search = normalizeSearchPath(ctx, args.path || "/");
244
+ const absolutePattern = args.pattern.startsWith("/");
245
+ const pattern = absolutePattern ? toKey(ctx, args.pattern) : args.pattern;
246
+ const limit = globLimit(args.limit);
247
+ const result = await ctx.fs.listObjects(search.key ? `${search.key}/` : void 0, { recursive: true });
248
+ 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);
249
+ }
250
+ }),
251
+ defineSpec({
252
+ name: "grep",
253
+ description: "Search file contents for an extended regular expression and return matching lines as path:line: text. Searches recursively by default.",
254
+ schema: zod.z.object({
255
+ pattern: zod.z.string().describe("Extended regex (grep -E)."),
256
+ path: zod.z.string().optional().describe("Directory to search (optional)."),
257
+ recursive: zod.z.boolean().optional().describe("Search subdirectories (default true)."),
258
+ maxResults: zod.z.number().int().optional().describe("Cap on matches (default 200).")
259
+ }),
260
+ async handler(ctx, args) {
261
+ const path = args.path || "/";
262
+ const recursive = args.recursive ?? true;
263
+ const maxResults = args.maxResults ?? 200;
264
+ const result = await ctx.fs.grep({
265
+ pattern: args.pattern,
266
+ directory: toKey(ctx, path),
267
+ recursive,
268
+ maxResults
269
+ });
270
+ return {
271
+ matches: result.matches.map((m) => ({
272
+ path: rootMatchPath(m.file),
273
+ line: m.line,
274
+ text: m.text
275
+ })),
276
+ status: grepStatus(result.stoppedReason, result.matches.length),
277
+ filesScanned: result.filesScanned
278
+ };
279
+ }
280
+ }),
281
+ defineSpec({
282
+ name: "run_bash",
283
+ 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.",
284
+ schema: zod.z.object({ command: zod.z.string().describe("The bash command to run.") }),
285
+ async handler(ctx, args) {
286
+ const result = await ctx.fs.exec(`cd ${ctx.execRoot} && ${args.command}`);
287
+ return {
288
+ exitCode: result.exitCode,
289
+ stdout: result.stdout,
290
+ stderr: result.stderr,
291
+ timing: result.timing
292
+ };
293
+ }
294
+ })
295
+ ];
296
+ function formatError(error, args) {
297
+ const status = error?.status;
298
+ if (status === 404 && typeof args.path === "string") return { error: {
299
+ message: "file not found",
300
+ status,
301
+ path: args.path
302
+ } };
303
+ return { error: { message: error instanceof Error ? error.message : String(error) } };
304
+ }
305
+ /**
306
+ * Bind the specs to a filesystem. The layout hint is appended to each
307
+ * description so the model knows where files live, and `invoke` returns
308
+ * expected failures as structured error objects rather than throwing.
309
+ */
310
+ function bindSpecs(fs) {
311
+ const ctx = buildContext(fs);
312
+ return SPECS.map((spec) => ({
313
+ name: spec.name,
314
+ description: `${spec.description} ${ctx.layoutHint}`,
315
+ schema: spec.schema,
316
+ async invoke(args) {
317
+ const input = args ?? {};
318
+ try {
319
+ return await spec.handler(ctx, input);
320
+ } catch (error) {
321
+ return formatError(error, input);
322
+ }
323
+ }
324
+ }));
325
+ }
326
+ //#endregion
327
+ exports.bindSpecs = bindSpecs;
328
+ exports.buildContext = buildContext;