agentic-drive 0.9.0 → 0.10.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.
@@ -0,0 +1,204 @@
1
+ import {
2
+ DriveError,
3
+ safeMime
4
+ } from "./chunk-DB4F5EWD.js";
5
+
6
+ // src/upload.ts
7
+ var DEFAULT_MAX_BYTES = 25 * 1024 * 1024;
8
+ var SIGNATURES = [
9
+ [[137, 80, 78, 71, 13, 10, 26, 10], "image/png"],
10
+ [[255, 216, 255], "image/jpeg"],
11
+ [[71, 73, 70, 56], "image/gif"],
12
+ [[37, 80, 68, 70, 45], "application/pdf"],
13
+ [[80, 75, 3, 4], "application/zip"],
14
+ [[31, 139], "application/gzip"]
15
+ ];
16
+ function sniffMime(head) {
17
+ const hit = SIGNATURES.find(([sig]) => sig.every((byte, i) => head[i] === byte));
18
+ if (hit) return hit[1];
19
+ const isWebp = [..."RIFF"].every((c, i) => head[i] === c.charCodeAt(0)) && [..."WEBP"].every((c, i) => head[8 + i] === c.charCodeAt(0));
20
+ return isWebp ? "image/webp" : null;
21
+ }
22
+ function looksLikeText(head) {
23
+ if (head.includes(0)) return false;
24
+ for (let cut = 0; cut < 4 && cut <= head.length; cut++) {
25
+ try {
26
+ new TextDecoder("utf-8", { fatal: true }).decode(head.subarray(0, head.length - cut));
27
+ return true;
28
+ } catch {
29
+ continue;
30
+ }
31
+ }
32
+ return false;
33
+ }
34
+ async function* iterate(source) {
35
+ if (Symbol.asyncIterator in source) {
36
+ yield* source;
37
+ return;
38
+ }
39
+ const reader = source.getReader();
40
+ try {
41
+ for (; ; ) {
42
+ const { done, value } = await reader.read();
43
+ if (done) return;
44
+ yield value;
45
+ }
46
+ } finally {
47
+ reader.releaseLock();
48
+ }
49
+ }
50
+ async function upload(drive, ctx, path, source, opts = {}) {
51
+ const max = opts.maxBytes ?? DEFAULT_MAX_BYTES;
52
+ const parts = [];
53
+ let size = 0;
54
+ for await (const chunk of iterate(source)) {
55
+ size += chunk.byteLength;
56
+ if (size > max) throw new DriveError("TOO_LARGE", `upload is over the limit of ${max} bytes`);
57
+ parts.push(chunk);
58
+ }
59
+ const data = new Uint8Array(size);
60
+ for (let at = 0, i = 0; i < parts.length; at += parts[i].byteLength, i++) data.set(parts[i], at);
61
+ const head = data.subarray(0, 512);
62
+ const mime = sniffMime(head) ?? (opts.mime !== void 0 ? safeMime(opts.mime) : looksLikeText(head) ? "text/plain" : "application/octet-stream");
63
+ return drive.putFile(ctx, path, data, { mime, parents: opts.parents, onConflict: opts.onConflict });
64
+ }
65
+
66
+ // src/zip.ts
67
+ var CRC_TABLE = Array.from({ length: 256 }, (_, n) => {
68
+ let c = n;
69
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
70
+ return c >>> 0;
71
+ });
72
+ function crc32(data) {
73
+ let crc = 4294967295;
74
+ for (const byte of data) crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
75
+ return (crc ^ 4294967295) >>> 0;
76
+ }
77
+ function safeEntryName(name) {
78
+ if (name === "" || /[\\\u0000]/.test(name) || name.startsWith("/") || /^[a-zA-Z]:/.test(name)) return false;
79
+ return name.split("/").every((part) => part !== "" && part !== "." && part !== "..");
80
+ }
81
+ function writeZip(entries, at) {
82
+ for (const { name } of entries) if (!safeEntryName(name)) throw new Error(`zip: unsafe entry name ${JSON.stringify(name)}`);
83
+ const d = new Date(at);
84
+ const time = d.getUTCHours() << 11 | d.getUTCMinutes() << 5 | d.getUTCSeconds() >> 1;
85
+ const date = Math.max(d.getUTCFullYear(), 1980) - 1980 << 9 | d.getUTCMonth() + 1 << 5 | d.getUTCDate();
86
+ const parts = [];
87
+ const central = [];
88
+ let offset = 0;
89
+ const u16 = (v, o, n) => v.setUint16(o, n, true);
90
+ const u32 = (v, o, n) => v.setUint32(o, n, true);
91
+ for (const entry of entries) {
92
+ const name = new TextEncoder().encode(entry.name);
93
+ const crc = crc32(entry.data);
94
+ if (entry.data.byteLength >= 4294967295 || offset >= 4294967295 || entries.length >= 65535) throw new Error("zip: export is too large for a plain zip");
95
+ const local = new DataView(new ArrayBuffer(30));
96
+ u32(local, 0, 67324752);
97
+ u16(local, 4, 20);
98
+ u16(local, 6, 2048);
99
+ u16(local, 8, 0);
100
+ u16(local, 10, time);
101
+ u16(local, 12, date);
102
+ u32(local, 14, crc);
103
+ u32(local, 18, entry.data.byteLength);
104
+ u32(local, 22, entry.data.byteLength);
105
+ u16(local, 26, name.length);
106
+ u16(local, 28, 0);
107
+ const head = new DataView(new ArrayBuffer(46));
108
+ u32(head, 0, 33639248);
109
+ u16(head, 4, 20);
110
+ u16(head, 6, 20);
111
+ u16(head, 8, 2048);
112
+ u16(head, 10, 0);
113
+ u16(head, 12, time);
114
+ u16(head, 14, date);
115
+ u32(head, 16, crc);
116
+ u32(head, 20, entry.data.byteLength);
117
+ u32(head, 24, entry.data.byteLength);
118
+ u16(head, 28, name.length);
119
+ u32(head, 42, offset);
120
+ parts.push(new Uint8Array(local.buffer), name, entry.data);
121
+ central.push(new Uint8Array(head.buffer), name);
122
+ offset += 30 + name.length + entry.data.byteLength;
123
+ }
124
+ const size = central.reduce((n, p) => n + p.byteLength, 0);
125
+ const end = new DataView(new ArrayBuffer(22));
126
+ u32(end, 0, 101010256);
127
+ u16(end, 8, entries.length);
128
+ u16(end, 10, entries.length);
129
+ u32(end, 12, size);
130
+ u32(end, 16, offset);
131
+ const all = [...parts, ...central, new Uint8Array(end.buffer)];
132
+ const out = new Uint8Array(all.reduce((n, p) => n + p.byteLength, 0));
133
+ let at2 = 0;
134
+ for (const p of all) {
135
+ out.set(p, at2);
136
+ at2 += p.byteLength;
137
+ }
138
+ return out;
139
+ }
140
+ function readZip(bytes) {
141
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
142
+ let end = bytes.byteLength - 22;
143
+ while (end >= 0 && v.getUint32(end, true) !== 101010256) end--;
144
+ if (end < 0) throw new Error("zip: no end signature");
145
+ const count = v.getUint16(end + 10, true);
146
+ const out = /* @__PURE__ */ new Map();
147
+ for (let i = 0, at = v.getUint32(end + 16, true); i < count; i++) {
148
+ if (v.getUint32(at, true) !== 33639248) throw new Error("zip: bad central signature");
149
+ if (v.getUint16(at + 10, true) !== 0) throw new Error("zip: compressed entries are not supported");
150
+ const crc = v.getUint32(at + 16, true);
151
+ const size = v.getUint32(at + 24, true);
152
+ const nameLen = v.getUint16(at + 28, true);
153
+ const local = v.getUint32(at + 42, true);
154
+ const name = new TextDecoder().decode(bytes.subarray(at + 46, at + 46 + nameLen));
155
+ if (v.getUint32(local, true) !== 67324752) throw new Error("zip: bad local signature");
156
+ const start = local + 30 + v.getUint16(local + 26, true) + v.getUint16(local + 28, true);
157
+ const data = bytes.slice(start, start + size);
158
+ if (crc32(data) !== crc) throw new Error(`zip: crc mismatch in ${name}`);
159
+ out.set(name, data);
160
+ at += 46 + nameLen + v.getUint16(at + 30, true) + v.getUint16(at + 32, true);
161
+ }
162
+ return out;
163
+ }
164
+
165
+ // src/export.ts
166
+ async function buildExport(drive, ctx, now = Date.now) {
167
+ const entries = [];
168
+ const items = (await drive.exportData(ctx)).map(({ node, path, data, missing }) => {
169
+ const file = data ? `files/${node.id}` : null;
170
+ if (data && file) entries.push({ name: file, data });
171
+ return {
172
+ id: node.id,
173
+ owner: node.owner,
174
+ path,
175
+ parentId: node.parentId,
176
+ kind: node.kind,
177
+ name: node.name,
178
+ size: node.size,
179
+ mime: node.mime,
180
+ hash: node.hash,
181
+ ref: node.ref,
182
+ createdAt: node.createdAt,
183
+ updatedAt: node.updatedAt,
184
+ createdBy: node.createdBy,
185
+ updatedBy: node.updatedBy,
186
+ trashed: node.trashed,
187
+ trashedAt: node.trashedAt,
188
+ file,
189
+ ...missing ? { missing: true } : {}
190
+ };
191
+ });
192
+ const person = await drive.exportPerson(ctx);
193
+ const manifest = { format: "agentic-drive-export", version: 2, exportedAt: now(), owner: ctx.owner, subject: person.subject, partial: person.partial, items, authored: person.authored, agents: person.agents, shares: person.shares, audit: person.audit, activity: person.activity };
194
+ return writeZip([{ name: "manifest.json", data: new TextEncoder().encode(JSON.stringify(manifest, null, 2)) }, ...entries], now());
195
+ }
196
+
197
+ export {
198
+ DEFAULT_MAX_BYTES,
199
+ sniffMime,
200
+ upload,
201
+ writeZip,
202
+ readZip,
203
+ buildExport
204
+ };
@@ -1,6 +1,6 @@
1
1
  // src/version.ts
2
2
  import { createRequire } from "module";
3
- var VERSION = true ? "0.9.0" : readPackageVersion();
3
+ var VERSION = true ? "0.10.0" : readPackageVersion();
4
4
 
5
5
  export {
6
6
  VERSION
@@ -48,6 +48,8 @@ var ROLES = ["viewer", "commenter", "editor", "owner"];
48
48
  var oneOf = (list) => (v) => typeof v === "string" && list.includes(v);
49
49
  var actor = (v) => record(v) && text(v.id) && oneOf(Object.keys(ACTOR_KINDS))(v.kind);
50
50
  var isItem = (v) => record(v) && text(v.id) && text(v.owner) && text(v.parentId) && text(v.name) && (v.kind === "folder" || v.kind === "file" || v.kind === "ref") && num(v.size) && orNull(text)(v.mime) && orNull(text)(v.hash) && orNull(text)(v.ref) && num(v.createdAt) && num(v.updatedAt) && actor(v.createdBy) && actor(v.updatedBy) && text(v.createdById) && text(v.updatedById) && typeof v.trashed === "boolean" && orNull(num)(v.trashedAt) && typeof v.machineMade === "boolean";
51
+ var ACTIVITY_ACTIONS = ["create", "rename", "move", "update", "share", "unshare", "delete", "restore"];
52
+ var isActivity = (v) => record(v) && text(v.id) && text(v.itemId) && oneOf(ACTIVITY_ACTIONS)(v.action) && record(v.actor) && text(v.actor.id) && oneOf(["human", "agent"])(v.actor.kind) && oneOf(Object.keys(ACTOR_KINDS))(v.principalKind) && num(v.at) && (v.detail === void 0 || record(v.detail));
51
53
  var isPathItem = (v) => isItem(v) && typeof v.path === "string";
52
54
  var listOf = (check) => (v) => Array.isArray(v) && v.every(check);
53
55
  var shape = (test) => (v) => record(v) && test(v);
@@ -186,6 +188,8 @@ function createClient(options) {
186
188
  return {
187
189
  /** The live items of a folder. */
188
190
  list: (path = "/", o = {}) => json("GET", `/items${query({ path })}`, o, listOf(isItem)),
191
+ /** The changes made to an item, newest first. `idOrPath` is an id or a path. */
192
+ listActivity: (idOrPath, opts = {}) => json("GET", `/items/${encodeURIComponent(idOrPath)}/activity${query({ limit: opts.limit })}`, opts, listOf(isActivity)),
189
193
  stat: (path, o = {}) => json("GET", `/stat${query({ path })}`, o, isItem),
190
194
  mkdir: (path, opts = {}, o = {}) => json("POST", "/folders", { json: { path, parents: opts.parents === true }, ...o }, isItem),
191
195
  /** Writes a file. A `ReadableStream` body streams in runtimes that allow it. */
package/dist/cli.js CHANGED
@@ -4,13 +4,16 @@ import {
4
4
  } from "./chunk-AB2RTTHJ.js";
5
5
  import {
6
6
  VERSION
7
- } from "./chunk-IMWV5QEH.js";
7
+ } from "./chunk-NHHBNW64.js";
8
+ import {
9
+ buildExport,
10
+ upload
11
+ } from "./chunk-HJXRMOUY.js";
8
12
  import {
9
13
  AuthError,
10
14
  DriveError,
11
15
  NO_TOKEN,
12
16
  addPrincipal,
13
- buildExport,
14
17
  ensureSharing,
15
18
  inviteIdentity,
16
19
  issueKey,
@@ -18,9 +21,8 @@ import {
18
21
  publicNode,
19
22
  resolveToken,
20
23
  revokeKey,
21
- revokePrincipal,
22
- upload
23
- } from "./chunk-R2DUVOJT.js";
24
+ revokePrincipal
25
+ } from "./chunk-DB4F5EWD.js";
24
26
 
25
27
  // src/cli.ts
26
28
  import { createReadStream, realpathSync, writeFileSync } from "fs";
@@ -36,6 +38,7 @@ Usage: agentic-drive <command> [flags]
36
38
  Commands:
37
39
  ls [path] List a folder.
38
40
  stat <path|id> Show one item.
41
+ activity <path|id> [--limit] List the changes to an item: who (person or AI agent), what, when.
39
42
  mkdir <path> [-p] Make a folder.
40
43
  upload <local> <remote> Upload a file. End remote with "/" to keep the name.
41
44
  download <remote> [out] Save a file. Use "-" for stdout.
@@ -78,7 +81,7 @@ Flags:
78
81
 
79
82
  Exit codes: 0 done, 1 error, 2 bad usage, 3 no principal or an invalid key.
80
83
  `;
81
- var COMMANDS = ["ls", "stat", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "find-ref", "claim-ref", "forget", "shares", "share", "unshare", "invite", "accept-invite", "link-create", "link-revoke", "delete-all", "mcp", "principal", "key"];
84
+ var COMMANDS = ["ls", "stat", "activity", "mkdir", "upload", "download", "mv", "rm", "trash", "restore", "purge", "search", "export", "home", "register", "find-ref", "claim-ref", "forget", "shares", "share", "unshare", "invite", "accept-invite", "link-create", "link-revoke", "delete-all", "mcp", "principal", "key"];
82
85
  var UsageError = class extends Error {
83
86
  };
84
87
  var options = {
@@ -166,6 +169,14 @@ async function runCli(argv, io) {
166
169
  need(1, "stat <path|id>");
167
170
  print(publicNode(await drive.stat(ctx, args[0])));
168
171
  break;
172
+ case "activity":
173
+ need(1, "activity <path|id> [--limit n]");
174
+ {
175
+ const limit = num(values.limit, "limit");
176
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 1)) throw new UsageError("--limit needs a whole number of 1 or more");
177
+ print(await drive.listActivity(ctx, args[0], { limit }));
178
+ }
179
+ break;
169
180
  case "mkdir":
170
181
  need(1, "mkdir <path> [-p]");
171
182
  print(publicNode(await drive.mkdir(ctx, args[0], { parents: values.parents })));
package/dist/client.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { E as ErrorCode, N as Node, S as SearchQuery } from './drive-Dxy0gL0_.js';
1
+ import { E as ErrorCode, N as Node, A as ActivityEntry, S as SearchQuery } from './drive-BhMTeQ-s.js';
2
+ export { a as ActivityAction } from './drive-BhMTeQ-s.js';
2
3
  import 'agentic-identity';
3
4
 
4
5
  type Role = "viewer" | "commenter" | "editor" | "owner";
@@ -49,6 +50,10 @@ interface Downloaded {
49
50
  declare function createClient(options: ClientOptions): {
50
51
  /** The live items of a folder. */
51
52
  list: (path?: string, o?: CallOptions) => Promise<Item[]>;
53
+ /** The changes made to an item, newest first. `idOrPath` is an id or a path. */
54
+ listActivity: (idOrPath: string, opts?: {
55
+ limit?: number;
56
+ } & CallOptions) => Promise<ActivityEntry[]>;
52
57
  stat: (path: string, o?: CallOptions) => Promise<Item>;
53
58
  mkdir: (path: string, opts?: {
54
59
  parents?: boolean;
@@ -186,4 +191,4 @@ declare function createClient(options: ClientOptions): {
186
191
  };
187
192
  type DriveClient = ReturnType<typeof createClient>;
188
193
 
189
- export { type ApiErrorCode, type CallOptions, type ClientErrorCode, type ClientOptions, type Downloaded, DriveApiError, type DriveClient, ErrorCode, type Item, Node, type OnConflict, type PathItem, type Role, type Subject, type SubjectKind, type UploadBody, createClient };
194
+ export { ActivityEntry, type ApiErrorCode, type CallOptions, type ClientErrorCode, type ClientOptions, type Downloaded, DriveApiError, type DriveClient, ErrorCode, type Item, Node, type OnConflict, type PathItem, type Role, type Subject, type SubjectKind, type UploadBody, createClient };
package/dist/client.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  DriveApiError,
3
3
  createClient
4
- } from "./chunk-LELTSSZF.js";
4
+ } from "./chunk-SEHT5Q5L.js";
5
5
  export {
6
6
  DriveApiError,
7
7
  createClient
@@ -1,5 +1,5 @@
1
1
  import * as agentic_identity from 'agentic-identity';
2
- import { PrincipalKind, PrincipalContext, Store, Blobs, createSharing, Subject, Role, SubjectKind } from 'agentic-identity';
2
+ import { PrincipalKind, Store, Blobs, PrincipalContext, createSharing, Subject, Role, SubjectKind } from 'agentic-identity';
3
3
 
4
4
  declare const ROOT_ID = "root";
5
5
  declare const ACTOR_KINDS: readonly ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
@@ -8,6 +8,22 @@ type Actor = {
8
8
  id: string;
9
9
  kind: ActorKind;
10
10
  };
11
+ type ActivityAction = "create" | "rename" | "move" | "update" | "share" | "unshare" | "delete" | "restore";
12
+ /** One change to an item. `actor.kind` is `agent` for every principal that is not a person, so a reader can label machine-made changes. */
13
+ type ActivityEntry = {
14
+ id: string;
15
+ itemId: string;
16
+ action: ActivityAction;
17
+ actor: {
18
+ id: string;
19
+ kind: "human" | "agent";
20
+ };
21
+ /** The kind of principal behind the change, such as `agent_of_human`. */
22
+ principalKind: ActorKind;
23
+ at: number;
24
+ /** What changed, such as `{ from, to }` for a rename. Never file content. */
25
+ detail?: Record<string, string | number | null>;
26
+ };
11
27
  /**
12
28
  * `owner` is the drive's owner, and the root of the caller's chain. `actor` is who acts for them.
13
29
  * `principal` is the resolved caller. Drive needs it to check roles when `sharing` is set.
@@ -98,6 +114,13 @@ declare function subjectKindOf(kind: unknown): SubjectKind;
98
114
  declare function parseSubject(input: unknown): Subject;
99
115
  declare function createDrive(deps: DriveDeps): {
100
116
  stat(ctx: Ctx, loc: string): Promise<Node>;
117
+ /**
118
+ * The changes made to one item, newest first: who (a person or an agent), what, and when. Anyone who may read
119
+ * the item may read its log. A trashed item can be named by id, so its log can be read before a restore.
120
+ */
121
+ listActivity(ctx: Ctx, loc: string, opts?: {
122
+ limit?: number;
123
+ }): Promise<ActivityEntry[]>;
101
124
  /** Live children, sorted by name. */
102
125
  list(ctx: Ctx, loc?: string): Promise<Node[]>;
103
126
  mkdir(ctx: Ctx, path: string, opts?: {
@@ -219,8 +242,8 @@ declare function createDrive(deps: DriveDeps): {
219
242
  * What drive holds about the person behind a call, besides their own drive (`exportData` has that). It is
220
243
  * only for the person in person: an agent gets `partial: true` and nothing else. It holds the ids of the
221
244
  * items they or their agents made or changed in other drives (no names, and nothing of the people who own
222
- * them), the shares they hold, the invites they sent, and the audit entries of what they and their agents
223
- * did. Invitee emails and link tokens are not in it.
245
+ * them), the shares they hold and the invites they sent on drive items, and the audit entries of what they and
246
+ * their agents did. Invitee emails and link tokens are not in it.
224
247
  */
225
248
  exportPerson(ctx: Ctx): Promise<{
226
249
  subject: {
@@ -274,6 +297,7 @@ declare function createDrive(deps: DriveDeps): {
274
297
  principalKind: unknown;
275
298
  machineMade: unknown;
276
299
  }[];
300
+ activity: ActivityEntry[];
277
301
  }>;
278
302
  /**
279
303
  * Erases a person for good. Only the person in person may. Their drive and its shares go. So do the shares
@@ -356,4 +380,4 @@ declare function createDrive(deps: DriveDeps): {
356
380
  }>;
357
381
  };
358
382
 
359
- export { type Actor as A, type Ctx as C, type Drive as D, type ErrorCode as E, type Node as N, type OnConflict as O, ROOT_ID as R, type SearchQuery as S, ACTOR_KINDS as a, type Action as b, type ActorKind as c, type Can as d, type DriveDeps as e, DriveError as f, ERROR_STATUS as g, createDrive as h, parseSubject as i, parseName as p, subjectKindOf as s };
383
+ export { type ActivityEntry as A, type Ctx as C, type Drive as D, type ErrorCode as E, type Node as N, type OnConflict as O, ROOT_ID as R, type SearchQuery as S, type ActivityAction as a, type Actor as b, ACTOR_KINDS as c, type Action as d, type ActorKind as e, type Can as f, type DriveDeps as g, DriveError as h, ERROR_STATUS as i, createDrive as j, parseSubject as k, parseName as p, subjectKindOf as s };
package/dist/embed.css CHANGED
@@ -289,3 +289,18 @@ html:has(.drive-bulk) {
289
289
  background-repeat: no-repeat;
290
290
  background-position: center;
291
291
  }
292
+ .drive-activity {
293
+ list-style: none;
294
+ margin: 0;
295
+ padding: 0;
296
+ display: grid;
297
+ gap: var(--agd-space-2);
298
+ }
299
+ .drive-activity li {
300
+ padding: var(--agd-space-2) 0;
301
+ border-bottom: 1px solid var(--agd-color-border);
302
+ }
303
+ .drive-activity time {
304
+ color: var(--agd-color-text-muted);
305
+ font-size: 0.875em;
306
+ }