agentic-drive 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.0
4
+
5
+ ### Changed
6
+
7
+ - **The web UI has a calm list view.** Files show in a table with sortable Name and Modified columns, a search that opens from a button, and a Share button next to the title of a folder. Select rows with the check boxes to see a bar at the bottom of the page that moves them all to the trash. A search with no match has its own empty state. The design classes in the list view are now `drive-*` classes of the app, not `agd-file-*` classes.
8
+
9
+ ## 0.8.0
10
+
11
+ Read the breaking changes before you upgrade.
12
+
13
+ ### Breaking changes
14
+
15
+ - **The design class prefix is now `agd-`.** The CSS classes, the custom properties, and the data attributes that used the old two-letter prefix change name. Ad blockers hide elements with the old prefix, so the prefix moved. There is no alias for the old names. Rename any class, custom property, or data attribute that you set or query so it starts with `agd-`. `agentic-design` moves from `^0.1.0` to `^0.4.0`, which uses the `agd-` prefix.
16
+ - **The CLI uses `agentic-identity/file`.** The old `createFileStore` and `createFileBlobs` exports are gone: import them from `agentic-identity/file`. The local layout changes: the store is `store.json` in the data folder and blobs sit in `blobs/blobs/<hash>.blob`. `openLocal` now also returns `blobs`, and it throws on a folder with an old `db/store.jsonl` until `importLegacyLocal(dir, store, blobs)` (new, exported) has copied the old data. The CLI does this on its first run, once, and leaves the old files in place.
17
+
18
+ ### Added
19
+
20
+ - Host operations: `findByRef`, `listTrashWithPaths`, and `claimRef` (put a reference and give a role, or do neither), in the SDK, the HTTP API (`GET /refs`, `POST /refs`, `GET /trash/paths`), the client, the CLI (`find-ref`, `claim-ref`, `trash --paths`), and MCP (`drive_find_ref`, `drive_claim_ref`, `drive_trash` with `paths`).
21
+ - `ERROR_STATUS`, the map from each error code to its HTTP status, is exported from the root and `agentic-drive/worker`. `Node` and `ErrorCode` types are exported from the client and worker entries too.
22
+
23
+ ### Changed
24
+
25
+ - **Transactions no longer nest store calls.** Access answers come from outside the transaction, so each transaction reads before it writes and runs on the file store of `agentic-identity/file`. A test runs the sharing and person suites on it.
26
+
3
27
  ## 0.7.0
4
28
 
5
29
  Read the breaking changes before you upgrade.
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Store, sort, and share files and folders.
9
9
 
10
- > Status: 0.7.0, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK, the HTTP API, the CLI, and the MCP server.
10
+ > Status: 0.9.0, early. The library, CLI, MCP server, and HTTP API work, with sharing in the SDK, the HTTP API, the CLI, and the MCP server.
11
11
  > A small web UI works. See [UI](#ui).
12
12
 
13
13
  ## Live demo
@@ -33,9 +33,10 @@ npm install agentic-drive
33
33
  ## Quick start
34
34
 
35
35
  ```ts
36
- import { createDrive, createFileStore, createFileBlobs } from "agentic-drive";
36
+ import { createDrive } from "agentic-drive";
37
+ import { createFileBlobs, createFileStore } from "agentic-identity/file";
37
38
 
38
- const drive = createDrive({ store: createFileStore("./data/db"), blobs: createFileBlobs("./data/blobs") });
39
+ const drive = createDrive({ store: createFileStore({ path: "./data/db" }), blobs: createFileBlobs({ dir: "./data/blobs" }) });
39
40
  const me = { owner: "u1", actor: { id: "u1", kind: "human" as const } };
40
41
 
41
42
  await drive.mkdir(me, "/reports/2026", { parents: true });
@@ -91,7 +92,7 @@ curl -H "authorization: Bearer <token>" "$URL/api/v1/drive/items?path=/"
91
92
 
92
93
  Routes: `GET /items`, `GET /stat`, `POST /folders`, `PUT /files`, `GET /files`, `GET /url`,
93
94
  `POST /move`, `POST /trash`, `GET /trash`, `POST /restore`, `POST /purge`, `GET /search`,
94
- `GET /export`, `DELETE /account?confirm=yes`, `GET /home`, `POST /documents`, `DELETE /documents?ref=`.
95
+ `GET /export`, `DELETE /account?confirm=yes`, `GET /home`, `POST /documents`, `DELETE /documents?ref=`, `GET /trash/paths`, `GET /refs?ref=`, `POST /refs`.
95
96
  Sign in with a bearer token from agentic-identity. The token is a credential in the `credentials` collection of
96
97
  the same D1 database, and it names a principal in `principals`. The Worker resolves the whole delegation chain on
97
98
  every request. A missing, expired, or revoked credential, or a revoked or expired principal (or one above it), gets 401.
@@ -124,7 +125,7 @@ try {
124
125
  }
125
126
  ```
126
127
 
127
- - One method for each bearer route and each shared route: `list`, `stat`, `mkdir`, `upload`, `download`, `url`, `move`, `trash`, `listTrash`, `restore`, `purge`, `search`, `export`, `deleteAccount`, `home`, `registerDocument`, `forgetDocument`, `shares`, `share`, `unshare`, `invite`, `acceptInvite`, `createLink`, `revokeLink`, `openShared`, `listShared`, and `readShared`.
128
+ - One method for each bearer route and each shared route: `list`, `stat`, `mkdir`, `upload`, `download`, `url`, `move`, `trash`, `listTrash`, `restore`, `purge`, `search`, `export`, `deleteAccount`, `home`, `registerDocument`, `forgetDocument`, `findByRef`, `listTrashWithPaths`, `claimRef`, `shares`, `share`, `unshare`, `invite`, `acceptInvite`, `createLink`, `revokeLink`, `openShared`, `listShared`, and `readShared`.
128
129
  - Each method has typed input and output. The client checks the shape of every answer, and `download` checks the bytes against their hash (it needs WebCrypto). `readShared` does not: the Worker sends no hash there. An answer that fails a check throws `BAD_RESPONSE`. `url` gives a signed link that you fetch yourself. The last argument takes `{ signal }` to stop the call with an `AbortSignal`.
129
130
  - A failed call throws `DriveApiError`. It has the `code` of the API and the HTTP `status`. A call that gets no answer has the code `NETWORK_ERROR` and status 0. An abort stays the abort error of `fetch`. A code that the client does not know gives `BAD_RESPONSE`.
130
131
  - The client never sends a write again. `retries` (default 0) sends a read (GET) again after a network error or a 502, 503, or 504, with a short wait. A redirect is an error (`BAD_RESPONSE`, with the status of the redirect, or 0 in a browser): set `baseUrl` to the final URL.
@@ -146,12 +147,12 @@ The root export, `agentic-drive`, also exports these names. The package typings
146
147
  a control character (U+0000 to U+001F, or U+007F), or a name over 255 bytes in UTF-8. `parseSubject(input)` returns `{ kind, id }` for a valid subject, and throws
147
148
  `INVALID_SHARE` for any other input. `subjectKindOf(kind)` returns `human` or `agent`, and throws
148
149
  `INVALID_SHARE` for any other kind.
149
- - A `DriveError` has a `code` from `ErrorCode`, such as `NOT_FOUND` or `NAME_TAKEN`.
150
+ - A `DriveError` has a `code` from `ErrorCode`, such as `NOT_FOUND` or `NAME_TAKEN`. `ERROR_STATUS` maps each code to the HTTP status that the Worker answers with (404 for `NOT_FOUND`, 409 for `NAME_TAKEN`, and so on). A host that serves the drive reuses it, and the `agentic-drive/worker` entry exports it too. The types `Node` and `ErrorCode` come from the root, `agentic-drive/client`, and `agentic-drive/worker`.
150
151
  - `readZip(bytes)` reads a zip that `writeZip` made, with stored (uncompressed) entries only. It returns a `Map`
151
152
  from each entry name to its bytes. It throws for a compressed entry. `writeZip(entries, at)` writes a zip with
152
153
  one `{ name, data }` entry for each file, and `at` sets the time in milliseconds. It throws for an unsafe
153
154
  name, such as one with `..` or a leading `/`.
154
- - The local drive has the functions behind the CLI. `openLocal(dir)` returns `{ drive, store }` for a folder.
155
+ - The local drive has the functions behind the CLI. `openLocal(dir)` returns `{ drive, store, blobs }` for a folder. Call `await importLegacyLocal(dir, store, blobs)` first on a folder that an older version made. It copies the old data once, and it refuses when the new store already holds data.
155
156
  `addPrincipal(store, id, { parent?, scopes? })` makes a principal. `issueKey(store, principalId, label?)`
156
157
  returns the key. Keep it: the store keeps no plain copy. `resolveToken(store, token)` returns the caller, or throws `AuthError`
157
158
  with the code `UNAUTHENTICATED`. `revokeKey(store, token)` stops one key. `revokePrincipal(store, id)` stops a
@@ -202,7 +203,7 @@ drive.update({ baseUrl, getToken }); // use another server or callback
202
203
  drive.unmount(); // remove the UI
203
204
  ```
204
205
 
205
- `getToken` returns 1 to 512 letters, digits, `-` or `_`, or `undefined` to send no `authorization` header. A bad `baseUrl` or a `getToken` that is not a function makes `mount` throw a `TypeError`. The server still checks every request against the token. The embed shows files and trash with upload, rename, move, share, and the "Created by agent" label. It has no sign-in form, because your app owns sign-in. It does not change the page address, and it has no theme switch. `agentic-design/styles.css` sets base styles for the whole page, and it follows the dark mode of the system unless your page sets `data-theme`. Add `agentic-design` to your own dependencies, because your page imports its files. The Worker sends no CORS headers, so the server must be on the origin of your page. Set `baseUrl` to the same address as `DRIVE_PUBLIC_URL`, or the UI refuses its download links. `unmount` and `update` abort the requests that run and stop an upload that has more files to send. The UI keeps the list or grid choice in `localStorage` under `agentic-drive-view`, and it has its own heading for each view. Dialogs, menus, and messages render in `document.body`, outside your element. A click with Ctrl, Cmd, Shift, or the middle button on a folder link does not open the folder. The class names start with `drive-`. `DriveBrowser` is the same UI as a React component. It closes its requests when it unmounts. Wrap it in `TooltipProvider` and `ToastProvider` from `agentic-design/react`. `react` and `react-dom` (version 19 or later) are optional peer dependencies. Install them in your project when you use `agentic-drive/ui`. The web UI that the Worker serves uses the same views.
206
+ `getToken` returns 1 to 512 letters, digits, `-` or `_`, or `undefined` to send no `authorization` header. A bad `baseUrl` or a `getToken` that is not a function makes `mount` throw a `TypeError`. The server still checks every request against the token. The embed shows files and trash with upload, rename, move, share, and the "Created by agent" label. It has no sign-in form, because your app owns sign-in. It does not change the page address, and it has no theme switch. `agentic-design/styles.css` sets base styles for the whole page, and it follows the dark mode of the system unless your page sets `data-theme`. Add `agentic-design@^0.4.0` to your own dependencies, because your page imports its files. The Worker sends no CORS headers, so the server must be on the origin of your page. Set `baseUrl` to the same address as `DRIVE_PUBLIC_URL`, or the UI refuses its download links. `unmount` and `update` abort the requests that run and stop an upload that has more files to send. The UI keeps the list or grid choice in `localStorage` under `agentic-drive-view`, and it has its own heading for each view. Dialogs, menus, and messages render in `document.body`, outside your element. A click with Ctrl, Cmd, Shift, or the middle button on a folder link does not open the folder. The class names start with `drive-`. The design-system classes start with `agd-`. `DriveBrowser` is the same UI as a React component. It closes its requests when it unmounts. Wrap it in `TooltipProvider` and `ToastProvider` from `agentic-design/react`. `react` and `react-dom` (version 19 or later) are optional peer dependencies. Install them in your project when you use `agentic-drive/ui`. The web UI that the Worker serves uses the same views.
206
207
 
207
208
  ### Serve the UI from the package
208
209
 
@@ -278,6 +279,7 @@ export default { fetch: app.fetch };
278
279
  - `downloadUrl()` returns a signed URL. The URL signs an attachment header with the file name and the stored type. The Worker verifies it and serves the bytes with those headers, so the link holder cannot change how the bytes are served. A link lasts 15 minutes by default and 1 hour at most. Trashing a file does not revoke a link already issued.
279
280
  - `search()` finds live items by name, type, kind, size, and who made them.
280
281
  - App documents: an app calls `registerDocument({ ref: "docs:<id>", name, folder })` to put a document in the drive, and `forgetDocument(ref)` when it deletes the document. `home({ app })` lists files and every app's documents together, newest change first, with a count per app. `search({ app })` filters the same way. The CLI has `home`, `register`, and `forget`. The MCP server has `drive_home`, `drive_register_document`, and `drive_forget_document`.
282
+ - Host operations. `findByRef(ref)` returns `{ node, path }` for each live reference to an app document in the caller's own drive that the caller can read, sorted by path (a reference that sits in a drive shared with the caller is not listed). `listTrashWithPaths()` lists what is in the trash (it reads the whole drive, so it costs more than `listTrash` on a big one), newest first, with the path each item had before. `claimRef(path, ref, { subject, role }, opts)` puts a reference and gives the subject a role on it as one step: if the grant fails (a bad subject or role, or a caller who is not an owner of the new item), the reference is removed again, the error is thrown, and the name is free. Folders that `parents` made stay. `onConflict: "replace"` acts as `fail` here, because a rollback could not bring back an item that was replaced. It needs sharing. The rollback is not a database transaction, because identity's sharing writes outside one: another caller can see the reference for a moment before it goes. The routes are `GET /refs?ref=`, `POST /refs`, and `GET /trash/paths`. The CLI has `find-ref`, `claim-ref`, and `trash --paths`. The MCP server has `drive_find_ref`, `drive_claim_ref`, and `drive_trash` with `paths`.
281
283
  - `buildExport()` writes everything one person has to a zip with a `manifest.json` (format version 2). It names the person by id and kind. It lists the person's own items (trashed ones are flagged, and a file whose bytes are gone is marked `missing`). For the person in person it also lists the ids of what they or their agents made or changed in other drives (no names, and nothing of the people who own them), the shares they hold, the invites they sent, and the audit entries of what they and their agents did. An agent gets `partial: true` and none of that. Invitee emails and link tokens are never in it. Entries are named by item id.
282
284
  - `erasePerson()` erases a person for good, and only the person in person can call it (the CLI `delete-all` and `DELETE /account` call it). It deletes their drive with all its shares, the shares they hold, and the invites they or their agents sent, on drive items only: the grants of other apps stay for those apps and for identity. What they or their agents made in other drives stays, because it belongs to those drives: the id becomes `erased`, and the kind stays, so machine-made work keeps its label. Agents are found through the `principals` collection, as identity finds them: an agent with its own identity names the person as `ownerId`, and a delegated agent names its parent. An agent with its own identity belongs to the person, and so does its drive: erase deletes it, and the export lists its items (no bytes). A person with more than 1000 agents cannot be erased here: the call fails and deletes nothing more. The audit entries of the erased items go with them, through identity `deleteResource`. The entries about what the person did in other drives stay: they hold ids and kinds only. A failed call can run again. `deleteAll()` is the drive part alone.
283
285
  - A `can()` hook decides who may read, write, or delete. Without `sharing`, the default allows the owner only.
@@ -289,9 +291,9 @@ export default { fetch: app.fetch };
289
291
  - There are no retention settings yet, and no per-workspace export. An export covers one person.
290
292
  - `registerDocument` makes one item per document in a drive. Two calls at the same moment can make two.
291
293
 
292
- - Drive reads the `store_docs` table in D1 and `store.jsonl` in the CLI folder. It does not read older layouts. There is no import and no claim backfill.
294
+ - Drive reads the `store_docs` table in D1. The CLI keeps its data in the file store of `agentic-identity/file`. On first run it copies data from an old `store.jsonl` journal and hashed blob files in the same folder, once, and leaves the old files in place. Drive reads no other older layout, and there is no claim backfill.
293
295
  - Uploads buffer the stream in memory up to the cap. The default cap is 25 MiB (`DEFAULT_MAX_BYTES`).
294
- - Local file blobs return a `file:` URL that nothing serves or checks.
296
+ - The CLI folder has no signing secret, so its blobs make no download URL. The Worker signs its own.
295
297
  - Drive writes each new blob only when its key is free (`ifNoneMatch`). A write that finds the key taken fails with `BLOB_EXISTS`.
296
298
  - Only `upload()` enforces the size cap. A caller of `putFile()` must check size first.
297
299
  - A claimed type that a browser would run (HTML, SVG, scripts) is stored as `application/octet-stream`.
@@ -0,0 +1,59 @@
1
+ // src/legacy.ts
2
+ import { createHash } from "crypto";
3
+ import { existsSync, readFileSync } from "fs";
4
+ import { join } from "path";
5
+ async function importLegacyLocal(dir, store, blobs) {
6
+ const journal = join(dir, "db", "store.jsonl");
7
+ if (!existsSync(journal) || await store.get("drive_meta", "legacy_import")) return;
8
+ const lines = readFileSync(journal, "utf8").split("\n");
9
+ const tail = lines.pop();
10
+ const docs = /* @__PURE__ */ new Map();
11
+ const isChange = (c) => Array.isArray(c) && c.length === 3 && typeof c[0] === "string" && typeof c[1] === "string" && (c[2] === null || typeof c[2] === "object" && !Array.isArray(c[2]));
12
+ const replay = (line, at) => {
13
+ let changes;
14
+ try {
15
+ changes = JSON.parse(line);
16
+ } catch {
17
+ throw new Error(`the journal ${journal} has a bad line ${at}`);
18
+ }
19
+ if (!Array.isArray(changes) || !changes.every(isChange)) throw new Error(`the journal ${journal} has a bad line ${at}`);
20
+ for (const change of changes) docs.set(`${change[0]}\0${change[1]}`, change);
21
+ };
22
+ for (const [i, line] of lines.entries()) if (line !== "") replay(line, i + 1);
23
+ if (tail !== "") {
24
+ let cut = false;
25
+ try {
26
+ JSON.parse(tail);
27
+ } catch {
28
+ cut = true;
29
+ }
30
+ if (!cut) replay(tail, "last");
31
+ }
32
+ let missing = 0;
33
+ const path = (key) => join(dir, "blobs", createHash("sha256").update(key).digest("hex"));
34
+ for (const [collection, , doc] of docs.values()) {
35
+ const key = collection === "drive_nodes" ? doc?.blobKey : void 0;
36
+ if (typeof key !== "string") continue;
37
+ if (!existsSync(path(key))) {
38
+ missing++;
39
+ continue;
40
+ }
41
+ const type = existsSync(`${path(key)}.type`) ? readFileSync(`${path(key)}.type`, "utf8") : void 0;
42
+ await blobs.put(key, new Uint8Array(readFileSync(path(key))), { ifNoneMatch: "*", ...type ? { contentType: type } : {} });
43
+ }
44
+ if (missing > 0) console.error(`import: ${missing} old blob files are missing, so those files cannot be read.`);
45
+ await store.transaction(async (tx) => {
46
+ if (await tx.get("drive_meta", "legacy_import")) return;
47
+ for (const collection of ["drive_nodes", "drive_names", "principals"]) {
48
+ if ((await tx.query(collection, { limit: 1 })).items.length > 0) {
49
+ throw new Error(`the folder holds old data and new data, and the old data was not imported. Move ${journal} away or import by hand.`);
50
+ }
51
+ }
52
+ for (const [collection, id, doc] of docs.values()) if (doc) await tx.put(collection, id, doc);
53
+ await tx.put("drive_meta", "legacy_import", { importedAt: Date.now() });
54
+ });
55
+ }
56
+
57
+ export {
58
+ importLegacyLocal
59
+ };
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.7.0" : readPackageVersion();
3
+ var VERSION = true ? "0.9.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
@@ -214,6 +214,12 @@ function createClient(options) {
214
214
  /** Erases the caller for good. The client sends `confirm=yes`. */
215
215
  deleteAccount: (o = {}) => json("DELETE", "/account?confirm=yes", o, numbers("removed", "grants", "invites", "anonymized")),
216
216
  home: (q = {}, o = {}) => json("GET", `/home${query(q)}`, o, isHome),
217
+ /** The live references to `ref`, with their paths. */
218
+ findByRef: (ref, o = {}) => json("GET", `/refs${query({ ref })}`, o, listOf(isPathItem)),
219
+ /** Items in the trash, newest first, each with the path it had before. */
220
+ listTrashWithPaths: (o = {}) => json("GET", "/trash/paths", o, listOf(isPathItem)),
221
+ /** Puts a reference and gives the subject a role on it, or does neither. Needs sharing. */
222
+ claimRef: (input, o = {}) => json("POST", "/refs", { json: { path: input.path, ref: input.ref, subjectKind: input.subject.kind, subjectId: input.subject.id, role: input.role, parents: input.parents, onConflict: input.onConflict }, ...o }, isItem),
217
223
  registerDocument: (input, o = {}) => json("POST", "/documents", { json: input, ...o }, isItem),
218
224
  forgetDocument: (ref, o = {}) => json("DELETE", `/documents${query({ ref })}`, o, numbers("trashed")),
219
225
  shares: (path, o = {}) => json("GET", `/shares${query({ path })}`, o, isShares),