agentic-drive 0.6.0 → 0.7.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 +21 -0
- package/README.md +2 -2
- package/dist/{chunk-ES3NZ7SX.js → chunk-EFO5PSJL.js} +1 -1
- package/dist/{chunk-WJZNXLTR.js → chunk-PJQ3CRVL.js} +91 -8
- package/dist/cli.js +78 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.js +4 -6
- package/dist/mcp.d.ts +3 -2
- package/dist/mcp.js +48 -4
- package/dist/worker.js +5 -11
- package/package.json +2 -2
- package/dist/chunk-BXFK3QXK.js +0 -65
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
Read the breaking changes before you upgrade.
|
|
6
|
+
|
|
7
|
+
### Breaking changes
|
|
8
|
+
|
|
9
|
+
- **The local drive has sharing on.** `openLocal` now passes `createSharing(store)`, as the Worker does. Items made before 0.7.0 have no `resources` docs. The CLI and the `mcp` command add them once, on the first drive command that has a valid key. The `principal` and `key` commands do not do it. A project that calls `openLocal` itself must run `backfillResources(store)`, exported from the package root, once on its existing data folder. If an item has no owner or parent, the backfill stops, and drive commands fail until you fix that item.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Sharing in the CLI.** New commands: `shares`, `share`, `unshare`, `invite`, `accept-invite`, `link-create`, and `link-revoke`. They act like the HTTP routes and show in `--help`.
|
|
14
|
+
- **Sharing in the MCP server.** New tools: `drive_shares`, `drive_share`, `drive_unshare`, `drive_invite`, `drive_accept_invite`, `drive_create_link`, and `drive_revoke_link`.
|
|
15
|
+
- **`drive_delete_account` in the MCP server.** It erases the caller like `DELETE /account` and `delete-all`. It needs `confirm: "yes"`.
|
|
16
|
+
|
|
17
|
+
## 0.6.1
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
|
|
21
|
+
- **Bearer tokens are checked by the agentic-identity HTTP helper.** The Worker and the local key check now call `requirePrincipal` from `agentic-identity/http` (agentic-identity 0.6). The package no longer has its own copy of the bearer-token check in `src/auth.ts` and `src/local.ts`. A request without an `authorization` header still gets no caller, so a session cookie does not sign in. A bad key still gives 401, and `resolveToken` still throws `AuthError` with the code `UNAUTHENTICATED`.
|
|
22
|
+
- **The dependency is agentic-identity 0.6.** The package now needs `agentic-identity` `^0.6.0`.
|
|
23
|
+
|
|
3
24
|
## 0.6.0
|
|
4
25
|
|
|
5
26
|
### Added
|
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
Store, sort, and share files and folders.
|
|
9
9
|
|
|
10
|
-
> Status: 0.
|
|
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.
|
|
11
11
|
> A small web UI works. See [UI](#ui).
|
|
12
12
|
|
|
13
13
|
## Live demo
|
|
@@ -305,7 +305,7 @@ export default { fetch: app.fetch };
|
|
|
305
305
|
- The D1 store checks every document a transaction read. If another Worker changed one, the transaction runs again, and after 50 tries it fails with a `BUSY` error.
|
|
306
306
|
- The D1 store keeps one generic table. A busy deployment needs an expression index on `owner` and `parentId`.
|
|
307
307
|
- `region: "eu"` on the D1 and R2 adapters is a promise made at deploy time. The D1 adapter refuses a query that a non-EU region served. The code cannot check where the bucket lives.
|
|
308
|
-
- Sharing is on in the Worker and in the SDK (`createDrive({ sharing })`). The CLI and the MCP server
|
|
308
|
+
- Sharing is on in the Worker and in the SDK (`createDrive({ sharing })`). The CLI and the MCP server turn it on too. The CLI has `shares`, `share`, `unshare`, `invite`, `accept-invite`, `link-create`, and `link-revoke`. The MCP server has `drive_shares`, `drive_share`, `drive_unshare`, `drive_invite`, `drive_accept_invite`, `drive_create_link`, `drive_revoke_link`, and `drive_delete_account` (pass `confirm: "yes"`). On the first run after the upgrade, the CLI gives older local items their `resources` docs, once.
|
|
309
309
|
- Drive writes the `resources` doc of each item itself, in the same transaction, because identity has no call that does it in a transaction. A drive with `sharing` off keeps no such docs. Turn sharing on at the start, or run `backfillResources` after you turn it on.
|
|
310
310
|
- An invite sends no message. The caller passes the invite id on.
|
|
311
311
|
- Sharing follows a path of 16 resources, the drive root included, so an item sits at most 15 folders deep. A deeper path, move, or restore fails with `TOO_DEEP`. A move or a restore of a folder spends up to 200 queries on its subfolders (never on files), once, outside the transaction, to count its height. A move up or sideways, a rename, and a file need no count. A bigger folder fails with `TOO_MANY_ITEMS`: move its parts one by one. An edit inside the folder while it moves can leave items past the cap. Nobody can then reach them, and moving the folder back up repairs it.
|
|
@@ -1346,19 +1346,94 @@ async function buildExport(drive, ctx, now = Date.now) {
|
|
|
1346
1346
|
|
|
1347
1347
|
// src/auth.ts
|
|
1348
1348
|
import { createStoreAuth } from "agentic-identity";
|
|
1349
|
+
import { requirePrincipal } from "agentic-identity/http";
|
|
1349
1350
|
async function authenticate(request, store, now) {
|
|
1350
|
-
if (
|
|
1351
|
-
const principal = await createStoreAuth(store, now ? { now } : {})
|
|
1352
|
-
|
|
1351
|
+
if (!request.headers.has("authorization")) return null;
|
|
1352
|
+
const principal = await requirePrincipal(request, createStoreAuth(store, now ? { now } : {}));
|
|
1353
|
+
if (principal instanceof Response) return null;
|
|
1354
|
+
return { owner: principal.chain[0].id, actor: { id: principal.principalId, kind: principal.kind }, principal };
|
|
1355
|
+
}
|
|
1356
|
+
async function inviteIdentity(store, actorId) {
|
|
1357
|
+
const user = await store.get("users", actorId);
|
|
1358
|
+
const agent = await store.get("principals", actorId);
|
|
1359
|
+
return {
|
|
1360
|
+
...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
|
|
1361
|
+
...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
|
|
1362
|
+
};
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/migrate.ts
|
|
1366
|
+
var PAGE = 10;
|
|
1367
|
+
var isText = (v) => typeof v === "string" && v !== "";
|
|
1368
|
+
async function walk(store, options, fix) {
|
|
1369
|
+
const { maxPages } = options;
|
|
1370
|
+
if (maxPages !== void 0 && !(Number.isSafeInteger(maxPages) && maxPages > 0)) {
|
|
1371
|
+
throw new Error("maxPages must be a positive whole number");
|
|
1372
|
+
}
|
|
1373
|
+
let fixed = 0;
|
|
1374
|
+
let cursor = options.cursor;
|
|
1375
|
+
for (let pages = 0; maxPages === void 0 || pages < maxPages; pages++) {
|
|
1376
|
+
const page = await store.query("drive_nodes", { limit: PAGE, cursor });
|
|
1377
|
+
fixed += await store.transaction(async (tx) => {
|
|
1378
|
+
const items = [];
|
|
1379
|
+
for (const item of page.items) {
|
|
1380
|
+
const data = await tx.get("drive_nodes", item.id);
|
|
1381
|
+
if (data) items.push({ id: item.id, data });
|
|
1382
|
+
}
|
|
1383
|
+
return fix(tx, items);
|
|
1384
|
+
});
|
|
1385
|
+
cursor = page.nextCursor;
|
|
1386
|
+
if (!cursor) return { fixed };
|
|
1387
|
+
}
|
|
1388
|
+
return { fixed, nextCursor: cursor };
|
|
1389
|
+
}
|
|
1390
|
+
function backfillAuthorIds(store, options = {}) {
|
|
1391
|
+
return walk(store, options, async (tx, items) => {
|
|
1392
|
+
let count = 0;
|
|
1393
|
+
for (const { id, data } of items) {
|
|
1394
|
+
const created = data.createdBy?.id;
|
|
1395
|
+
const updated = data.updatedBy?.id;
|
|
1396
|
+
if (!isText(created) || !isText(updated)) {
|
|
1397
|
+
throw new Error(`item ${id} has no author. Fix it before you run the backfill again.`);
|
|
1398
|
+
}
|
|
1399
|
+
if (data.createdById === created && data.updatedById === updated) continue;
|
|
1400
|
+
await tx.put("drive_nodes", id, { ...data, createdById: created, updatedById: updated });
|
|
1401
|
+
count++;
|
|
1402
|
+
}
|
|
1403
|
+
return count;
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
function backfillResources(store, options = {}) {
|
|
1407
|
+
return walk(store, options, async (tx, items) => {
|
|
1408
|
+
const missing = [];
|
|
1409
|
+
for (const { id, data } of items) {
|
|
1410
|
+
if (!isText(data.owner) || !isText(data.parentId)) {
|
|
1411
|
+
throw new Error(`item ${id} has no owner or parent. Fix it before you run the backfill again.`);
|
|
1412
|
+
}
|
|
1413
|
+
if (await tx.get("resources", id)) continue;
|
|
1414
|
+
missing.push({ id, parentId: data.parentId === ROOT_ID ? await rootResource(data.owner) : data.parentId });
|
|
1415
|
+
}
|
|
1416
|
+
for (const { id, parentId } of missing) await tx.put("resources", id, { id, type: "drive", parentId });
|
|
1417
|
+
return missing.length;
|
|
1418
|
+
});
|
|
1353
1419
|
}
|
|
1354
1420
|
|
|
1355
1421
|
// src/local.ts
|
|
1356
1422
|
import { createHash as createHash2 } from "crypto";
|
|
1357
1423
|
import { join as join2 } from "path";
|
|
1358
|
-
import { isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
|
|
1424
|
+
import { createSharing, isMachineMade, isScope, issueCredential, revokeCredential } from "agentic-identity";
|
|
1359
1425
|
function openLocal(dir) {
|
|
1360
1426
|
const store = createFileStore(join2(dir, "db"));
|
|
1361
|
-
return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")) }) };
|
|
1427
|
+
return { store, drive: createDrive({ store, blobs: createFileBlobs(join2(dir, "blobs")), sharing: createSharing(store) }) };
|
|
1428
|
+
}
|
|
1429
|
+
async function ensureSharing(store) {
|
|
1430
|
+
if (await store.get("drive_meta", "sharing")) return;
|
|
1431
|
+
try {
|
|
1432
|
+
await backfillResources(store);
|
|
1433
|
+
} catch (error) {
|
|
1434
|
+
throw new Error(`the one-time sharing backfill of local items failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1435
|
+
}
|
|
1436
|
+
await store.put("drive_meta", "sharing", { backfilledAt: Date.now() });
|
|
1362
1437
|
}
|
|
1363
1438
|
var AuthError = class extends Error {
|
|
1364
1439
|
code = "UNAUTHENTICATED";
|
|
@@ -1367,8 +1442,13 @@ var NO_TOKEN = "no principal: pass --token <key> or set AGENTIC_DRIVE_TOKEN to a
|
|
|
1367
1442
|
var BAD_TOKEN = "the key is not valid: it is unknown, revoked, or expired, or its principal is revoked";
|
|
1368
1443
|
async function resolveToken(store, token) {
|
|
1369
1444
|
if (!token) throw new AuthError(NO_TOKEN);
|
|
1370
|
-
|
|
1371
|
-
|
|
1445
|
+
let request;
|
|
1446
|
+
try {
|
|
1447
|
+
request = new Request("http://local", { headers: { authorization: `Bearer ${token}` } });
|
|
1448
|
+
} catch {
|
|
1449
|
+
throw new AuthError(BAD_TOKEN);
|
|
1450
|
+
}
|
|
1451
|
+
const ctx = await authenticate(request, store);
|
|
1372
1452
|
if (!ctx) throw new AuthError(BAD_TOKEN);
|
|
1373
1453
|
return ctx;
|
|
1374
1454
|
}
|
|
@@ -1411,7 +1491,6 @@ export {
|
|
|
1411
1491
|
DEFAULT_URL_SECONDS,
|
|
1412
1492
|
safeMime,
|
|
1413
1493
|
parseName,
|
|
1414
|
-
rootResource,
|
|
1415
1494
|
subjectKindOf,
|
|
1416
1495
|
parseSubject,
|
|
1417
1496
|
createDrive,
|
|
@@ -1424,7 +1503,11 @@ export {
|
|
|
1424
1503
|
readZip,
|
|
1425
1504
|
buildExport,
|
|
1426
1505
|
authenticate,
|
|
1506
|
+
inviteIdentity,
|
|
1507
|
+
backfillAuthorIds,
|
|
1508
|
+
backfillResources,
|
|
1427
1509
|
openLocal,
|
|
1510
|
+
ensureSharing,
|
|
1428
1511
|
AuthError,
|
|
1429
1512
|
NO_TOKEN,
|
|
1430
1513
|
resolveToken,
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
VERSION
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-EFO5PSJL.js";
|
|
5
5
|
import {
|
|
6
6
|
AuthError,
|
|
7
7
|
DriveError,
|
|
8
8
|
NO_TOKEN,
|
|
9
9
|
addPrincipal,
|
|
10
10
|
buildExport,
|
|
11
|
+
ensureSharing,
|
|
12
|
+
inviteIdentity,
|
|
11
13
|
issueKey,
|
|
12
14
|
openLocal,
|
|
13
15
|
publicNode,
|
|
@@ -15,13 +17,14 @@ import {
|
|
|
15
17
|
revokeKey,
|
|
16
18
|
revokePrincipal,
|
|
17
19
|
upload
|
|
18
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-PJQ3CRVL.js";
|
|
19
21
|
|
|
20
22
|
// src/cli.ts
|
|
21
23
|
import { createReadStream, realpathSync, writeFileSync } from "fs";
|
|
22
24
|
import { basename, join, resolve } from "path";
|
|
23
25
|
import { pathToFileURL } from "url";
|
|
24
26
|
import { parseArgs } from "util";
|
|
27
|
+
import { ROLES, SUBJECT_KINDS } from "agentic-identity";
|
|
25
28
|
var HELP = `agentic-drive ${VERSION}
|
|
26
29
|
Store, sort, and share files and folders.
|
|
27
30
|
|
|
@@ -43,6 +46,13 @@ Commands:
|
|
|
43
46
|
home [--app <name>] List files and app documents, newest change first.
|
|
44
47
|
register <ref> <name> Put an app document in the drive. Flag: --parent <folder>
|
|
45
48
|
forget <ref> Move the references to an app document to the trash.
|
|
49
|
+
shares <path|id> Show who can reach an item, and by which links and invites. Owner only.
|
|
50
|
+
share <path|id> <subject> Give a role to a person or agent. Flags: --role <r> --subject-kind <human|agent>
|
|
51
|
+
unshare <path|id> <subject> Take a role away. Flag: --subject-kind <human|agent>
|
|
52
|
+
invite <path|id> Invite by --email (a person) or --handle (an agent). Flags: --role <r> --expires-in-ms <n>. Sends no message.
|
|
53
|
+
accept-invite <id> Accept an invite that names your verified email or agent handle.
|
|
54
|
+
link-create <path|id> Make a share link. The token shows once. Flags: --role <r> --expires-in-ms <n> --signed-in-only
|
|
55
|
+
link-revoke <id> Stop a share link.
|
|
46
56
|
delete-all --yes Erase you for good: your drive, and what you shared or held.
|
|
47
57
|
principal add <id> Make a person, or an agent with --of <person>. Flag: --scope <s> (repeat). No key needed.
|
|
48
58
|
principal revoke <id> Stop a principal, and every agent below it. No key needed.
|
|
@@ -57,12 +67,13 @@ Flags:
|
|
|
57
67
|
The key names a principal, found through agentic-identity. The drive belongs to
|
|
58
68
|
the person at the top of that principal's chain. An agent key marks items as
|
|
59
69
|
made by a machine. If the key is missing or not valid, the command exits with 3.
|
|
70
|
+
--role <r> share: owner, editor, commenter, or viewer. invite and link-create: editor, commenter, or viewer.
|
|
60
71
|
--on-conflict <m> fail, rename, or replace
|
|
61
72
|
-p, --parents Create missing parent folders
|
|
62
73
|
|
|
63
74
|
Exit codes: 0 done, 1 error, 2 bad usage, 3 no principal or an invalid key.
|
|
64
75
|
`;
|
|
65
|
-
var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "forget", "delete-all", "mcp", "principal", "key"];
|
|
76
|
+
var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "forget", "shares", "share", "unshare", "invite", "accept-invite", "link-create", "link-revoke", "delete-all", "mcp", "principal", "key"];
|
|
66
77
|
var UsageError = class extends Error {
|
|
67
78
|
};
|
|
68
79
|
var options = {
|
|
@@ -81,18 +92,35 @@ var options = {
|
|
|
81
92
|
"min-size": { type: "string" },
|
|
82
93
|
"max-size": { type: "string" },
|
|
83
94
|
"created-by-kind": { type: "string" },
|
|
95
|
+
role: { type: "string" },
|
|
96
|
+
"subject-kind": { type: "string" },
|
|
97
|
+
email: { type: "string" },
|
|
98
|
+
handle: { type: "string" },
|
|
99
|
+
"expires-in-ms": { type: "string" },
|
|
100
|
+
"signed-in-only": { type: "boolean" },
|
|
84
101
|
limit: { type: "string" },
|
|
85
102
|
app: { type: "string" },
|
|
86
103
|
yes: { type: "boolean" },
|
|
87
104
|
help: { type: "boolean", short: "h" },
|
|
88
105
|
version: { type: "boolean", short: "V" }
|
|
89
106
|
};
|
|
107
|
+
function lifetime(value, flag) {
|
|
108
|
+
if (value === void 0) return void 0;
|
|
109
|
+
const n = Number(value);
|
|
110
|
+
if (value.trim() === "" || !Number.isSafeInteger(n) || n <= 0) throw new UsageError(`--${flag} needs a whole number above 0`);
|
|
111
|
+
return n;
|
|
112
|
+
}
|
|
90
113
|
function num(value, flag) {
|
|
91
114
|
if (value === void 0) return void 0;
|
|
92
115
|
const n = Number(value);
|
|
93
116
|
if (!Number.isFinite(n) || n < 0) throw new UsageError(`--${flag} needs a number of 0 or more`);
|
|
94
117
|
return n;
|
|
95
118
|
}
|
|
119
|
+
var LINK_ROLES = ROLES.filter((r) => r !== "owner");
|
|
120
|
+
function pick(value, allowed, flag) {
|
|
121
|
+
if (value === void 0 || !allowed.includes(value)) throw new UsageError(`--${flag} needs ${allowed.join(", ")}`);
|
|
122
|
+
return value;
|
|
123
|
+
}
|
|
96
124
|
async function runCli(argv, io) {
|
|
97
125
|
try {
|
|
98
126
|
const { values, positionals } = parseArgs({ args: argv, options, allowPositionals: true, strict: true });
|
|
@@ -119,6 +147,7 @@ async function runCli(argv, io) {
|
|
|
119
147
|
throw new UsageError("usage: agentic-drive principal add <id> [--of <person>] [--scope <s>] | principal revoke <id> | key issue <principal> | key revoke <key>");
|
|
120
148
|
}
|
|
121
149
|
const ctx = await resolveToken(store, token);
|
|
150
|
+
await ensureSharing(store);
|
|
122
151
|
const onConflict = values["on-conflict"];
|
|
123
152
|
if (onConflict && !["fail", "rename", "replace"].includes(onConflict)) throw new UsageError("--on-conflict needs fail, rename, or replace");
|
|
124
153
|
const print = (value) => io.out(JSON.stringify(value));
|
|
@@ -207,13 +236,58 @@ async function runCli(argv, io) {
|
|
|
207
236
|
need(1, "forget <ref>");
|
|
208
237
|
print(await drive.forgetDocument(ctx, args[0]));
|
|
209
238
|
break;
|
|
239
|
+
case "shares":
|
|
240
|
+
need(1, "shares <path|id>");
|
|
241
|
+
print(await drive.shares(ctx, args[0]));
|
|
242
|
+
break;
|
|
243
|
+
case "share":
|
|
244
|
+
need(2, "share <path|id> <subject> --role <r> --subject-kind <human|agent>");
|
|
245
|
+
await drive.shareWith(ctx, args[0], { kind: pick(values["subject-kind"], SUBJECT_KINDS, "subject-kind"), id: args[1] }, pick(values.role, ROLES, "role"));
|
|
246
|
+
print({ ok: true });
|
|
247
|
+
break;
|
|
248
|
+
case "unshare":
|
|
249
|
+
need(2, "unshare <path|id> <subject> --subject-kind <human|agent>");
|
|
250
|
+
await drive.unshare(ctx, args[0], { kind: pick(values["subject-kind"], SUBJECT_KINDS, "subject-kind"), id: args[1] });
|
|
251
|
+
print({ ok: true });
|
|
252
|
+
break;
|
|
253
|
+
case "invite": {
|
|
254
|
+
need(1, "invite <path|id> --role <r> (--email <e> | --handle <h>)");
|
|
255
|
+
const expiresInMs = lifetime(values["expires-in-ms"], "expires-in-ms");
|
|
256
|
+
print(await drive.invite(ctx, args[0], {
|
|
257
|
+
role: pick(values.role, LINK_ROLES, "role"),
|
|
258
|
+
...values.email === void 0 ? {} : { email: values.email },
|
|
259
|
+
...values.handle === void 0 ? {} : { handle: values.handle },
|
|
260
|
+
...expiresInMs === void 0 ? {} : { expiresInMs }
|
|
261
|
+
}));
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
case "accept-invite":
|
|
265
|
+
need(1, "accept-invite <id>");
|
|
266
|
+
await drive.acceptInvite(ctx, args[0], await inviteIdentity(store, ctx.actor.id));
|
|
267
|
+
print({ ok: true });
|
|
268
|
+
break;
|
|
269
|
+
case "link-create": {
|
|
270
|
+
need(1, "link-create <path|id> --role <r>");
|
|
271
|
+
const expiresInMs = lifetime(values["expires-in-ms"], "expires-in-ms");
|
|
272
|
+
print(await drive.createLink(ctx, args[0], {
|
|
273
|
+
role: pick(values.role, LINK_ROLES, "role"),
|
|
274
|
+
signedInOnly: values["signed-in-only"] === true,
|
|
275
|
+
...expiresInMs === void 0 ? {} : { expiresAt: Date.now() + expiresInMs }
|
|
276
|
+
}));
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
case "link-revoke":
|
|
280
|
+
need(1, "link-revoke <id>");
|
|
281
|
+
await drive.revokeLink(ctx, args[0]);
|
|
282
|
+
print({ ok: true });
|
|
283
|
+
break;
|
|
210
284
|
case "delete-all":
|
|
211
285
|
if (!values.yes) throw new UsageError("delete-all erases you for good. Add --yes to confirm.");
|
|
212
286
|
print(await drive.erasePerson(ctx));
|
|
213
287
|
break;
|
|
214
288
|
case "mcp": {
|
|
215
289
|
const { startMcp } = await import("./mcp.js");
|
|
216
|
-
await startMcp({ drive, ctx, exportDir: join(dir, "exports") });
|
|
290
|
+
await startMcp({ drive, ctx, exportDir: join(dir, "exports"), store });
|
|
217
291
|
return 0;
|
|
218
292
|
}
|
|
219
293
|
default:
|
package/dist/index.d.ts
CHANGED
|
@@ -51,7 +51,7 @@ declare function writeZip(entries: ZipEntry[], at: number): Uint8Array;
|
|
|
51
51
|
/** Reads a zip made by `writeZip`. It checks every CRC. */
|
|
52
52
|
declare function readZip(bytes: Uint8Array): Map<string, Uint8Array>;
|
|
53
53
|
|
|
54
|
-
/** The drive and the store behind it, both kept in `dir`. The store holds the principals and keys too. */
|
|
54
|
+
/** The drive and the store behind it, both kept in `dir`. The store holds the principals and keys too. Sharing is on, as in the Worker. */
|
|
55
55
|
declare function openLocal(dir: string): {
|
|
56
56
|
drive: Drive;
|
|
57
57
|
store: Store;
|
package/dist/index.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
4
|
-
import {
|
|
5
|
-
backfillAuthorIds,
|
|
6
|
-
backfillResources
|
|
7
|
-
} from "./chunk-BXFK3QXK.js";
|
|
3
|
+
} from "./chunk-EFO5PSJL.js";
|
|
8
4
|
import {
|
|
9
5
|
ACTOR_KINDS,
|
|
10
6
|
AuthError,
|
|
@@ -12,6 +8,8 @@ import {
|
|
|
12
8
|
DriveError,
|
|
13
9
|
ROOT_ID,
|
|
14
10
|
addPrincipal,
|
|
11
|
+
backfillAuthorIds,
|
|
12
|
+
backfillResources,
|
|
15
13
|
buildExport,
|
|
16
14
|
createDrive,
|
|
17
15
|
createFileBlobs,
|
|
@@ -29,7 +27,7 @@ import {
|
|
|
29
27
|
subjectKindOf,
|
|
30
28
|
upload,
|
|
31
29
|
writeZip
|
|
32
|
-
} from "./chunk-
|
|
30
|
+
} from "./chunk-PJQ3CRVL.js";
|
|
33
31
|
export {
|
|
34
32
|
ACTOR_KINDS,
|
|
35
33
|
AuthError,
|
package/dist/mcp.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { D as Drive, C as Ctx } from './drive-BtRyexUP.js';
|
|
3
|
-
import 'agentic-identity';
|
|
3
|
+
import { Store } from 'agentic-identity';
|
|
4
4
|
|
|
5
|
-
declare function createMcpServer(drive: Drive, ctx: Ctx, exportDir: string): McpServer;
|
|
5
|
+
declare function createMcpServer(drive: Drive, ctx: Ctx, exportDir: string, store: Store): McpServer;
|
|
6
6
|
declare function startMcp(deps: {
|
|
7
7
|
drive: Drive;
|
|
8
8
|
ctx: Ctx;
|
|
9
9
|
exportDir: string;
|
|
10
|
+
store: Store;
|
|
10
11
|
}): Promise<void>;
|
|
11
12
|
|
|
12
13
|
export { createMcpServer, startMcp };
|
package/dist/mcp.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
VERSION
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-EFO5PSJL.js";
|
|
4
4
|
import {
|
|
5
5
|
ACTOR_KINDS,
|
|
6
6
|
DriveError,
|
|
7
7
|
buildExport,
|
|
8
|
+
inviteIdentity,
|
|
8
9
|
publicNode,
|
|
9
10
|
upload
|
|
10
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-PJQ3CRVL.js";
|
|
11
12
|
|
|
12
13
|
// src/mcp.ts
|
|
13
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -15,7 +16,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
15
16
|
import { mkdirSync, writeFileSync } from "fs";
|
|
16
17
|
import { join } from "path";
|
|
17
18
|
import { z } from "zod";
|
|
19
|
+
import { ROLES, SUBJECT_KINDS } from "agentic-identity";
|
|
18
20
|
var conflict = z.enum(["fail", "rename", "replace"]).optional();
|
|
21
|
+
var role = z.enum(ROLES);
|
|
22
|
+
var linkRole = z.enum(ROLES.filter((r) => r !== "owner"));
|
|
23
|
+
var subjectKind = z.enum(SUBJECT_KINDS);
|
|
24
|
+
var lifetime = z.number().int().positive().optional().describe("How long it lasts, in milliseconds.");
|
|
19
25
|
var loc = z.string().describe('A path, such as "/x" or "<folder id>/x", or an item id.');
|
|
20
26
|
var ok = (value) => ({ content: [{ type: "text", text: JSON.stringify(value) }] });
|
|
21
27
|
function failure(error) {
|
|
@@ -23,7 +29,7 @@ function failure(error) {
|
|
|
23
29
|
return { isError: true, content: [{ type: "text", text: JSON.stringify({ error: { code: error.code, message: error.message } }) }] };
|
|
24
30
|
}
|
|
25
31
|
var EXPORT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,99}\.zip$/;
|
|
26
|
-
function createMcpServer(drive, ctx, exportDir) {
|
|
32
|
+
function createMcpServer(drive, ctx, exportDir, store) {
|
|
27
33
|
const server = new McpServer({ name: "agentic-drive", version: VERSION });
|
|
28
34
|
const tool = (name, description, shape, run, annotations = {}) => server.registerTool(name, { description, inputSchema: shape, annotations }, (async (args) => {
|
|
29
35
|
try {
|
|
@@ -86,6 +92,44 @@ function createMcpServer(drive, ctx, exportDir) {
|
|
|
86
92
|
async ({ ref, name, folder }) => publicNode(await drive.registerDocument(ctx, { ref, name, folder }))
|
|
87
93
|
);
|
|
88
94
|
tool("drive_forget_document", "Move every reference to an app document to the trash.", { ref: z.string() }, async ({ ref }) => drive.forgetDocument(ctx, ref));
|
|
95
|
+
tool("drive_shares", "Show who can reach an item, and by which links and invites. Only an owner may look. Link tokens are never shown.", { path: loc }, async ({ path }) => drive.shares(ctx, path), { readOnlyHint: true });
|
|
96
|
+
tool(
|
|
97
|
+
"drive_share",
|
|
98
|
+
"Give a person or an agent a role on an item. A folder role passes down.",
|
|
99
|
+
{ path: loc, subjectKind, subjectId: z.string(), role },
|
|
100
|
+
async ({ path, subjectKind: subjectKind2, subjectId, role: role2 }) => (await drive.shareWith(ctx, path, { kind: subjectKind2, id: subjectId }, role2), { ok: true })
|
|
101
|
+
);
|
|
102
|
+
tool(
|
|
103
|
+
"drive_unshare",
|
|
104
|
+
"Take a role away from a person or an agent on an item.",
|
|
105
|
+
{ path: loc, subjectKind, subjectId: z.string() },
|
|
106
|
+
async ({ path, subjectKind: subjectKind2, subjectId }) => (await drive.unshare(ctx, path, { kind: subjectKind2, id: subjectId }), { ok: true }),
|
|
107
|
+
{ destructiveHint: true }
|
|
108
|
+
);
|
|
109
|
+
tool(
|
|
110
|
+
"drive_invite",
|
|
111
|
+
"Invite by `email` (a person) or `handle` (an agent). It sends no message. The id comes back.",
|
|
112
|
+
{ path: loc, role: linkRole, email: z.string().optional(), handle: z.string().optional(), expiresInMs: lifetime },
|
|
113
|
+
async ({ path, role: role2, email, handle, expiresInMs }) => drive.invite(ctx, path, { role: role2, ...email === void 0 ? {} : { email }, ...handle === void 0 ? {} : { handle }, ...expiresInMs === void 0 ? {} : { expiresInMs } })
|
|
114
|
+
);
|
|
115
|
+
tool("drive_accept_invite", "Accept an invite that names you by verified email or by agent handle.", { id: z.string() }, async ({ id }) => (await drive.acceptInvite(ctx, id, await inviteIdentity(store, ctx.actor.id)), { ok: true }));
|
|
116
|
+
tool(
|
|
117
|
+
"drive_create_link",
|
|
118
|
+
"Make a share link for an item. The token comes back once.",
|
|
119
|
+
{ path: loc, role: linkRole, expiresInMs: lifetime, signedInOnly: z.boolean().optional() },
|
|
120
|
+
async ({ path, role: role2, expiresInMs, signedInOnly }) => drive.createLink(ctx, path, { role: role2, signedInOnly: signedInOnly === true, ...expiresInMs === void 0 ? {} : { expiresAt: Date.now() + expiresInMs } })
|
|
121
|
+
);
|
|
122
|
+
tool("drive_revoke_link", "Stop a share link.", { id: z.string() }, async ({ id }) => (await drive.revokeLink(ctx, id), { ok: true }), { destructiveHint: true });
|
|
123
|
+
tool(
|
|
124
|
+
"drive_delete_account",
|
|
125
|
+
'Erase the caller for good: the drive, and what they shared or held. Only a person can. Pass confirm: "yes".',
|
|
126
|
+
{ confirm: z.string() },
|
|
127
|
+
async ({ confirm }) => {
|
|
128
|
+
if (confirm !== "yes") throw new DriveError("INVALID_PATH", 'pass confirm: "yes" to erase you for good: your drive, and what you shared or held');
|
|
129
|
+
return drive.erasePerson(ctx);
|
|
130
|
+
},
|
|
131
|
+
{ destructiveHint: true }
|
|
132
|
+
);
|
|
89
133
|
tool(
|
|
90
134
|
"drive_export",
|
|
91
135
|
"Write everything the owner has to a new zip file in the export folder. Give a plain file name that ends in .zip. It never overwrites a file.",
|
|
@@ -108,7 +152,7 @@ function createMcpServer(drive, ctx, exportDir) {
|
|
|
108
152
|
return server;
|
|
109
153
|
}
|
|
110
154
|
async function startMcp(deps) {
|
|
111
|
-
await createMcpServer(deps.drive, deps.ctx, deps.exportDir).connect(new StdioServerTransport());
|
|
155
|
+
await createMcpServer(deps.drive, deps.ctx, deps.exportDir, deps.store).connect(new StdioServerTransport());
|
|
112
156
|
}
|
|
113
157
|
export {
|
|
114
158
|
createMcpServer,
|
package/dist/worker.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
backfillAuthorIds,
|
|
3
|
-
backfillResources
|
|
4
|
-
} from "./chunk-BXFK3QXK.js";
|
|
5
1
|
import {
|
|
6
2
|
DEFAULT_MAX_BYTES,
|
|
7
3
|
DEFAULT_URL_SECONDS,
|
|
8
4
|
DriveError,
|
|
9
5
|
authenticate,
|
|
6
|
+
backfillAuthorIds,
|
|
7
|
+
backfillResources,
|
|
10
8
|
buildExport,
|
|
11
9
|
createDrive,
|
|
10
|
+
inviteIdentity,
|
|
12
11
|
publicNode,
|
|
13
12
|
safeMime,
|
|
14
13
|
upload
|
|
15
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-PJQ3CRVL.js";
|
|
16
15
|
|
|
17
16
|
// src/worker.ts
|
|
18
17
|
import { Hono } from "hono";
|
|
@@ -255,12 +254,7 @@ function createApp(options = {}) {
|
|
|
255
254
|
});
|
|
256
255
|
api.post("/invites/accept", async (c) => {
|
|
257
256
|
const id = str((await body(c)).id, "id");
|
|
258
|
-
|
|
259
|
-
const agent = await c.var.store.get("principals", c.var.ctx.actor.id);
|
|
260
|
-
await c.var.drive.acceptInvite(c.var.ctx, id, {
|
|
261
|
-
...user?.emailVerified === true && typeof user.email === "string" ? { email: user.email } : {},
|
|
262
|
-
...typeof agent?.handle === "string" ? { handle: agent.handle } : {}
|
|
263
|
-
});
|
|
257
|
+
await c.var.drive.acceptInvite(c.var.ctx, id, await inviteIdentity(c.var.store, c.var.ctx.actor.id));
|
|
264
258
|
return c.json({ ok: true });
|
|
265
259
|
});
|
|
266
260
|
api.post("/links", async (c) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentic-drive",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Store, sort, and share files and folders.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"drive",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
75
75
|
"agentic-design": "^0.1.0",
|
|
76
|
-
"agentic-identity": "^0.
|
|
76
|
+
"agentic-identity": "^0.6.0",
|
|
77
77
|
"hono": "^4.13.14",
|
|
78
78
|
"zod": "^3.25.0"
|
|
79
79
|
},
|
package/dist/chunk-BXFK3QXK.js
DELETED
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ROOT_ID,
|
|
3
|
-
rootResource
|
|
4
|
-
} from "./chunk-WJZNXLTR.js";
|
|
5
|
-
|
|
6
|
-
// src/migrate.ts
|
|
7
|
-
var PAGE = 10;
|
|
8
|
-
var isText = (v) => typeof v === "string" && v !== "";
|
|
9
|
-
async function walk(store, options, fix) {
|
|
10
|
-
const { maxPages } = options;
|
|
11
|
-
if (maxPages !== void 0 && !(Number.isSafeInteger(maxPages) && maxPages > 0)) {
|
|
12
|
-
throw new Error("maxPages must be a positive whole number");
|
|
13
|
-
}
|
|
14
|
-
let fixed = 0;
|
|
15
|
-
let cursor = options.cursor;
|
|
16
|
-
for (let pages = 0; maxPages === void 0 || pages < maxPages; pages++) {
|
|
17
|
-
const page = await store.query("drive_nodes", { limit: PAGE, cursor });
|
|
18
|
-
fixed += await store.transaction(async (tx) => {
|
|
19
|
-
const items = [];
|
|
20
|
-
for (const item of page.items) {
|
|
21
|
-
const data = await tx.get("drive_nodes", item.id);
|
|
22
|
-
if (data) items.push({ id: item.id, data });
|
|
23
|
-
}
|
|
24
|
-
return fix(tx, items);
|
|
25
|
-
});
|
|
26
|
-
cursor = page.nextCursor;
|
|
27
|
-
if (!cursor) return { fixed };
|
|
28
|
-
}
|
|
29
|
-
return { fixed, nextCursor: cursor };
|
|
30
|
-
}
|
|
31
|
-
function backfillAuthorIds(store, options = {}) {
|
|
32
|
-
return walk(store, options, async (tx, items) => {
|
|
33
|
-
let count = 0;
|
|
34
|
-
for (const { id, data } of items) {
|
|
35
|
-
const created = data.createdBy?.id;
|
|
36
|
-
const updated = data.updatedBy?.id;
|
|
37
|
-
if (!isText(created) || !isText(updated)) {
|
|
38
|
-
throw new Error(`item ${id} has no author. Fix it before you run the backfill again.`);
|
|
39
|
-
}
|
|
40
|
-
if (data.createdById === created && data.updatedById === updated) continue;
|
|
41
|
-
await tx.put("drive_nodes", id, { ...data, createdById: created, updatedById: updated });
|
|
42
|
-
count++;
|
|
43
|
-
}
|
|
44
|
-
return count;
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
function backfillResources(store, options = {}) {
|
|
48
|
-
return walk(store, options, async (tx, items) => {
|
|
49
|
-
const missing = [];
|
|
50
|
-
for (const { id, data } of items) {
|
|
51
|
-
if (!isText(data.owner) || !isText(data.parentId)) {
|
|
52
|
-
throw new Error(`item ${id} has no owner or parent. Fix it before you run the backfill again.`);
|
|
53
|
-
}
|
|
54
|
-
if (await tx.get("resources", id)) continue;
|
|
55
|
-
missing.push({ id, parentId: data.parentId === ROOT_ID ? await rootResource(data.owner) : data.parentId });
|
|
56
|
-
}
|
|
57
|
-
for (const { id, parentId } of missing) await tx.put("resources", id, { id, type: "drive", parentId });
|
|
58
|
-
return missing.length;
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export {
|
|
63
|
-
backfillAuthorIds,
|
|
64
|
-
backfillResources
|
|
65
|
-
};
|