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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # disk
2
2
 
3
- Node.js client and CLI for [Archil](https://archil.com) disks. Create disks, list and inspect them, manage who can mount them, and run commands against them — all from scripts, CI, or an interactive terminal.
3
+ SDK and CLI for [Archil](https://archil.com) disks. Create disks, list and inspect them, manage who can mount them, and run commands against them — all from scripts, CI, or an interactive terminal. It also ships **drop-in [filesystem tools](#filesystem-tools)** for AI SDK, Mastra, and Langchain.
4
4
 
5
5
  `disk` talks to the Archil control plane over HTTPS and has no native dependencies. If you also want to mount a disk's data plane from Node (rare — most users want `disk exec` or the `archil` CLI), install [`@archildata/native`](https://www.npmjs.com/package/@archildata/native) alongside `disk`.
6
6
 
@@ -41,7 +41,7 @@ npx disk api-keys delete key-abc123
41
41
 
42
42
  ## Library
43
43
 
44
- The recommended pattern is a module-namespace import:
44
+ Setup:
45
45
 
46
46
  ```ts
47
47
  import * as archil from "disk";
@@ -213,14 +213,6 @@ console.log(url); // https://control.…/api/shared/<token>
213
213
  const weekLink = await d.share("reports/2026-01/summary.pdf", { expiresIn: 604800 });
214
214
  ```
215
215
 
216
- ### Named imports
217
-
218
- If you prefer named imports over the namespace style, they work the same way:
219
-
220
- ```ts
221
- import { createDisk, getDisk, listDisks, configure } from "disk";
222
- ```
223
-
224
216
  ### Multiple accounts or regions
225
217
 
226
218
  For multi-tenant scripts, instantiate `Archil` directly instead of using the module-level `configure`:
@@ -235,6 +227,61 @@ const prodDisks = await prod.disks.list();
235
227
  const stagingDisks = await staging.disks.list();
236
228
  ```
237
229
 
230
+ ## Filesystem tools
231
+
232
+ Support for providing agents with a set of tools for using an Archil disk live in their own `@archildata/*` packages.
233
+ | Package | Framework |
234
+ | --- | --- |
235
+ | `@archildata/ai-sdk` | AI SDK |
236
+ | `@archildata/eve` | eve |
237
+ | `@archildata/mastra` | Mastra |
238
+ | `@archildata/langchain` | LangChain / LangGraph |
239
+
240
+ ## Workspaces
241
+ For usage with multiple disks, you can create a workspace, which acts as a virtual disk where each mounted disk appears as a top-level directory:
242
+
243
+ ```ts
244
+ import { Archil } from "disk";
245
+
246
+ const archil = new Archil();
247
+ const source = await archil.disks.get(process.env.ARCHIL_SOURCE_DISK_ID!);
248
+ const output = await archil.disks.get(process.env.ARCHIL_OUTPUT_DISK_ID!);
249
+
250
+ const workspace = archil.workspace({
251
+ source: { disk: source, readOnly: true },
252
+ output,
253
+ });
254
+ ```
255
+
256
+ Workspace paths route to the right disk by their first segment. `readOnly` mounts
257
+ return an error from operations that mutate the disk.
258
+
259
+ Workspace mounts can also request delegations:
260
+
261
+ ```ts
262
+ const workspace = archil.workspace({
263
+ repo: {
264
+ disk: repoDisk,
265
+ checkoutPaths: ["src", "tmp/cache"],
266
+ queueMs: 5_000,
267
+ },
268
+ });
269
+ ```
270
+
271
+ When `queueMs` is set without `checkoutPaths`, the mount root is acquired
272
+ during mount setup instead.
273
+
274
+ A `Workspace` is a full filesystem in its own right — it has the same object API
275
+ a `Disk` does (`getObject` / `putObject` / `deleteObject` / `listObjects` /
276
+ `grep` / `exec`; both implement the `FileSystem` interface), so you can use it
277
+ directly, and add or remove disks at runtime. A
278
+ workspace's keys carry the disk name as their first segment:
279
+
280
+ ```ts
281
+ const data = await ws.getObject("data/reports/q1.csv"); // routes to the "data" disk
282
+ ws.addDisk("scratch", diskTmp); // mount another disk live; ws.removeDisk("scratch")
283
+ ```
284
+
238
285
  ## Connecting to a disk's data plane
239
286
 
240
287
  To run a command against a disk, use `Disk.exec()` — it returns stdout, stderr, and an exit code from an Archil-managed container with the disk pre-mounted. No local filesystem involved.
package/dist/cli.mjs ADDED
@@ -0,0 +1,280 @@
1
+ #!/usr/bin/env node
2
+ import { c as listDisks, i as deleteApiKey, n as createApiKey, o as getDisk, r as createDisk, s as listApiKeys, t as configure, y as ArchilApiError } from "./src-BuqSzAcO.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { Command, Option } from "commander";
5
+ //#region bin/cli.ts
6
+ const pkg = createRequire(import.meta.url)("../package.json");
7
+ const program = new Command();
8
+ program.name("disk").description("Manage Archil disks from the command line").version(pkg.version).addOption(new Option("-k, --api-key <key>", "Archil API key").env("ARCHIL_API_KEY")).addOption(new Option("-r, --region <region>", "Archil region").env("ARCHIL_REGION")).addOption(new Option("--base-url <url>", "Override control-plane base URL")).hook("preAction", () => {
9
+ const opts = program.opts();
10
+ try {
11
+ configure({
12
+ apiKey: opts.apiKey,
13
+ region: opts.region,
14
+ baseUrl: opts.baseUrl
15
+ });
16
+ } catch (err) {
17
+ fail(err);
18
+ }
19
+ });
20
+ function fail(err) {
21
+ if (err instanceof ArchilApiError) console.error(`Error (${err.status}): ${err.message}`);
22
+ else console.error(err instanceof Error ? err.message : String(err));
23
+ process.exit(1);
24
+ }
25
+ function isDiskId(s) {
26
+ return /^(dsk-|fs-)[0-9a-fA-F]+$/.test(s);
27
+ }
28
+ async function resolveDisk(idOrName) {
29
+ if (isDiskId(idOrName)) return getDisk(idOrName);
30
+ const matches = await listDisks({ name: idOrName });
31
+ if (matches.length === 0) throw new ArchilApiError(`No disk found with name '${idOrName}'`, 404);
32
+ if (matches.length > 1) throw new ArchilApiError(`Multiple disks match name '${idOrName}' — pass a disk id (dsk-...) instead`, 400);
33
+ return matches[0];
34
+ }
35
+ function formatBytes(n) {
36
+ if (n === void 0 || n === null) return void 0;
37
+ if (n < 1024) return `${n} B`;
38
+ const units = [
39
+ "KB",
40
+ "MB",
41
+ "GB",
42
+ "TB",
43
+ "PB"
44
+ ];
45
+ let v = n / 1024;
46
+ let i = 0;
47
+ while (v >= 1024 && i < units.length - 1) {
48
+ v /= 1024;
49
+ i++;
50
+ }
51
+ return `${v.toFixed(v >= 100 ? 0 : v >= 10 ? 1 : 2)} ${units[i]}`;
52
+ }
53
+ function renderTable(rows, headers) {
54
+ const all = headers ? [headers, ...rows] : rows;
55
+ if (all.length === 0) return "";
56
+ const cols = Math.max(...all.map((r) => r.length));
57
+ const widths = [];
58
+ for (let i = 0; i < cols; i++) widths[i] = Math.max(...all.map((r) => (r[i] ?? "").length));
59
+ const bar = (l, m, r) => l + widths.map((w) => "─".repeat(w + 2)).join(m) + r;
60
+ const line = (r) => "│ " + widths.map((w, i) => (r[i] ?? "").padEnd(w)).join(" │ ") + " │";
61
+ const out = [bar("╭", "┬", "╮")];
62
+ if (headers) {
63
+ out.push(line(headers));
64
+ out.push(bar("├", "┼", "┤"));
65
+ }
66
+ for (const r of rows) out.push(line(r));
67
+ out.push(bar("╰", "┴", "╯"));
68
+ return out.join("\n");
69
+ }
70
+ function section(title, body) {
71
+ console.log("");
72
+ console.log(title);
73
+ console.log(body);
74
+ }
75
+ function printDisk(d) {
76
+ console.log(`${d.organization}/${d.name} (${d.id})`);
77
+ const visible = [
78
+ ["status", d.status],
79
+ ["provider", d.provider],
80
+ ["region", d.region],
81
+ ["created", d.createdAt],
82
+ ["last accessed", d.lastAccessed],
83
+ ["data size", formatBytes(d.dataSize)],
84
+ ["monthly usage", d.monthlyUsage]
85
+ ].filter(([, v]) => v !== void 0 && v !== null && v !== "").map(([k, v]) => [k, String(v)]);
86
+ console.log("");
87
+ console.log(renderTable(visible));
88
+ if (d.mounts && d.mounts.length > 0) section("Mounts", renderTable(d.mounts.map((m) => [
89
+ m.type ?? "?",
90
+ m.name ?? m.path ?? "",
91
+ m.accessMode ?? ""
92
+ ]), [
93
+ "type",
94
+ "location",
95
+ "mode"
96
+ ]));
97
+ if (d.authorizedUsers && d.authorizedUsers.length > 0) section("Authorized users", renderTable(d.authorizedUsers.map((u) => {
98
+ if (u.type === "token") return [
99
+ "token",
100
+ u.nickname ?? "(no name)",
101
+ u.tokenSuffix ? `-${u.tokenSuffix}` : ""
102
+ ];
103
+ return [
104
+ u.type ?? "?",
105
+ u.identifier ?? u.principal ?? "",
106
+ ""
107
+ ];
108
+ }), [
109
+ "type",
110
+ "name / identifier",
111
+ "suffix"
112
+ ]));
113
+ if (d.connectedClients && d.connectedClients.length > 0) section("Connected clients", renderTable(d.connectedClients.map((c) => [
114
+ c.id ?? "",
115
+ c.ipAddress ?? "",
116
+ c.connectedAt ?? ""
117
+ ]), [
118
+ "id",
119
+ "ip",
120
+ "connected at"
121
+ ]));
122
+ }
123
+ program.command("list").description("List disks in the current region").option("--limit <n>", "Maximum number of disks to return", (v) => parseInt(v, 10)).option("-o, --output <format>", "Output format: table | json", "table").action(async (opts) => {
124
+ try {
125
+ const disks = await listDisks({ limit: opts.limit });
126
+ if (opts.output === "json") {
127
+ console.log(JSON.stringify(disks, null, 2));
128
+ return;
129
+ }
130
+ if (disks.length === 0) {
131
+ console.log("No disks found.");
132
+ return;
133
+ }
134
+ const rows = disks.map((d) => [
135
+ d.id,
136
+ `${d.organization}/${d.name}`,
137
+ d.status
138
+ ]);
139
+ console.log(renderTable(rows, [
140
+ "id",
141
+ "name",
142
+ "status"
143
+ ]));
144
+ } catch (err) {
145
+ fail(err);
146
+ }
147
+ });
148
+ program.command("get <id|name>").description("Show details for a disk (accepts a disk id or name)").option("-o, --output <format>", "Output format: table | json", "table").action(async (idOrName, opts) => {
149
+ try {
150
+ const d = await resolveDisk(idOrName);
151
+ if (opts.output === "json") console.log(JSON.stringify(d, null, 2));
152
+ else printDisk(d);
153
+ } catch (err) {
154
+ fail(err);
155
+ }
156
+ });
157
+ program.command("create <name>").description("Create a new disk").action(async (name) => {
158
+ try {
159
+ const result = await createDisk({ name });
160
+ console.log(`Created disk ${result.disk.organization}/${result.disk.name}`);
161
+ console.log(` id: ${result.disk.id}`);
162
+ console.log(` status: ${result.disk.status}`);
163
+ if (result.token) {
164
+ console.log("");
165
+ console.log("Disk token (save this — it cannot be retrieved again):");
166
+ console.log(` ${result.token}`);
167
+ if (result.tokenIdentifier) console.log(` identifier: ${result.tokenIdentifier}`);
168
+ }
169
+ } catch (err) {
170
+ fail(err);
171
+ }
172
+ });
173
+ program.command("delete <id|name>").description("Delete a disk (accepts a disk id or name)").action(async (idOrName) => {
174
+ try {
175
+ const d = await resolveDisk(idOrName);
176
+ await d.delete();
177
+ console.log(`Deleted ${d.organization}/${d.name} (${d.id})`);
178
+ } catch (err) {
179
+ fail(err);
180
+ }
181
+ });
182
+ program.command("exec <id|name> <command...>").description("Run a command in a container with the disk mounted, return stdout/stderr/exit code (accepts a disk id or name)").action(async (idOrName, cmd) => {
183
+ try {
184
+ const result = await (await resolveDisk(idOrName)).exec(cmd.join(" "));
185
+ if (result.stdout) process.stdout.write(result.stdout);
186
+ if (result.stderr) process.stderr.write(result.stderr);
187
+ process.exit(result.exitCode);
188
+ } catch (err) {
189
+ fail(err);
190
+ }
191
+ });
192
+ program.command("grep <pattern> <target>").description("Constant-time parallel server-side grep across a disk. <target> is <id|name>[/<path>] — the path after the first '/' is the directory to search (defaults to the disk root). Accepts a disk id or name.").option("--no-recursive", "Search only the given directory, not its subdirectories (recursive is the default)").option("--max-duration <seconds>", "Wall-clock deadline for the whole search", (v) => parseInt(v, 10)).option("--concurrency <n>", "Max parallel grep workers (clamped to fleet capacity)", (v) => parseInt(v, 10)).option("--max-results <n>", "Stop once this many matches are collected", (v) => parseInt(v, 10)).option("-o, --output <format>", "Output format: text | json", "text").action(async (pattern, target, opts) => {
193
+ try {
194
+ const slash = target.indexOf("/");
195
+ const idOrName = slash === -1 ? target : target.slice(0, slash);
196
+ const directory = slash === -1 ? "" : target.slice(slash + 1);
197
+ const res = await (await resolveDisk(idOrName)).grep({
198
+ directory,
199
+ pattern,
200
+ recursive: opts.recursive !== false,
201
+ maxDurationSeconds: opts.maxDuration,
202
+ concurrency: opts.concurrency,
203
+ maxResults: opts.maxResults
204
+ });
205
+ if (opts.output === "json") console.log(JSON.stringify(res, null, 2));
206
+ else {
207
+ for (const m of res.matches) console.log(`${m.file}:${m.line}:${m.text}`);
208
+ console.error(`${res.matches.length} match${res.matches.length === 1 ? "" : "es"} · ${res.filesScanned} files scanned · ${res.containersDispatched} containers · ${res.durationMs}ms · ${res.stoppedReason}`);
209
+ if (res.stoppedReason !== "completed") console.error(`warning: search did not complete (${res.stoppedReason}); results may be partial`);
210
+ }
211
+ process.exit(res.matches.length > 0 ? 0 : 1);
212
+ } catch (err) {
213
+ fail(err);
214
+ }
215
+ });
216
+ const keys = program.command("api-keys").description("Manage Archil API keys (account-level credentials)");
217
+ keys.command("list").description("List API keys").option("--limit <n>", "Maximum number of keys to return", (v) => parseInt(v, 10)).action(async (opts) => {
218
+ try {
219
+ const ks = await listApiKeys({ limit: opts.limit });
220
+ if (ks.length === 0) {
221
+ console.log("No API keys found.");
222
+ return;
223
+ }
224
+ for (const k of ks) console.log(`${k.id ?? "-"}\t${k.name ?? "-"}\t${k.createdAt ?? ""}`);
225
+ } catch (err) {
226
+ fail(err);
227
+ }
228
+ });
229
+ keys.command("create <name>").description("Create a new API key (the key value is shown once)").option("--description <description>", "Optional description").action(async (name, opts) => {
230
+ try {
231
+ const k = await createApiKey({
232
+ name,
233
+ description: opts.description
234
+ });
235
+ console.log(`Created API key ${name} (${k.id ?? "?"})`);
236
+ if (k.token) {
237
+ console.log("");
238
+ console.log("API key value (save this — it cannot be retrieved again):");
239
+ console.log(` ${k.token}`);
240
+ }
241
+ } catch (err) {
242
+ fail(err);
243
+ }
244
+ });
245
+ keys.command("delete <id>").description("Delete an API key").action(async (id) => {
246
+ try {
247
+ await deleteApiKey(id);
248
+ console.log(`Deleted API key ${id}`);
249
+ } catch (err) {
250
+ fail(err);
251
+ }
252
+ });
253
+ function rewriteSugar(argv) {
254
+ const known = /* @__PURE__ */ new Set([
255
+ "list",
256
+ "get",
257
+ "create",
258
+ "delete",
259
+ "exec",
260
+ "grep",
261
+ "api-keys",
262
+ "help",
263
+ "--help",
264
+ "-h",
265
+ "--version",
266
+ "-V"
267
+ ]);
268
+ const head = argv[2];
269
+ const next = argv[3];
270
+ if (head && next === "exec" && !known.has(head) && !head.startsWith("-")) return [
271
+ ...argv.slice(0, 2),
272
+ "exec",
273
+ head,
274
+ ...argv.slice(4)
275
+ ];
276
+ return argv;
277
+ }
278
+ program.parseAsync(rewriteSugar(process.argv));
279
+ //#endregion
280
+ export {};