agentic-documents 0.3.0 → 0.5.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.
Files changed (38) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +143 -5
  3. package/dist/{chunk-YBBQNCMC.js → chunk-6QOYS63L.js} +14 -189
  4. package/dist/{chunk-TFCRPEEM.js → chunk-7R7ZPPRF.js} +1 -1
  5. package/dist/{chunk-R5XUTBC3.js → chunk-E7B6T5WN.js} +29 -8
  6. package/dist/{chunk-IU3YIQ4Y.js → chunk-FX755I7V.js} +30 -8
  7. package/dist/{chunk-GYQFTF2E.js → chunk-L6W2AQUF.js} +15 -146
  8. package/dist/chunk-S6MQDWFK.js +150 -0
  9. package/dist/chunk-UPEGODUV.js +195 -0
  10. package/dist/{chunk-FZA5LQV3.js → chunk-YNHZCIGO.js} +86 -11
  11. package/dist/cli.js +8 -6
  12. package/dist/client.d.ts +188 -0
  13. package/dist/client.js +121 -0
  14. package/dist/{service-BKkxEnaJ.d.ts → doc-mJPajXEQ.d.ts} +5 -56
  15. package/dist/export-Cwx276UP.d.ts +17 -0
  16. package/dist/formats.d.ts +49 -0
  17. package/dist/formats.js +234 -0
  18. package/dist/index.d.ts +7 -99
  19. package/dist/index.js +11 -7
  20. package/dist/live-worker.d.ts +2 -1
  21. package/dist/live-worker.js +6 -4
  22. package/dist/live.d.ts +6 -1
  23. package/dist/live.js +4 -3
  24. package/dist/{mcp-P2C75IIG.js → mcp-ESSEG46C.js} +4 -2
  25. package/dist/review-Hgq8OgvD.d.ts +85 -0
  26. package/dist/service-CqKE9DKz.d.ts +91 -0
  27. package/dist/ui/_headers +4 -0
  28. package/dist/ui/app.css +1 -0
  29. package/dist/ui/app.js +270 -0
  30. package/dist/ui/assets/inter-latin-ext-wght-normal-HA22NDSG.woff2 +0 -0
  31. package/dist/ui/assets/inter-latin-wght-normal-NRMW37G5.woff2 +0 -0
  32. package/dist/ui/assets/jetbrains-mono-latin-ext-wght-normal-TSLVDSQR.woff2 +0 -0
  33. package/dist/ui/assets/jetbrains-mono-latin-wght-normal-VBS632QH.woff2 +0 -0
  34. package/dist/ui/index.html +13 -0
  35. package/dist/worker.d.ts +2 -1
  36. package/dist/worker.js +6 -4
  37. package/llms.txt +2 -0
  38. package/package.json +23 -3
@@ -1,20 +1,11 @@
1
- import {
2
- DocError,
3
- Suggestions,
4
- Threads
5
- } from "./chunk-YBBQNCMC.js";
6
-
7
- // src/odt.ts
8
- import { strToU8, zipSync } from "fflate";
9
-
10
1
  // src/package.ts
11
2
  var IMAGE_TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
12
3
  var DATA_IMAGE = /^data:(image\/(?:png|jpeg|gif|webp));base64,([a-z0-9+/=]+)$/i;
13
4
  var FIXED_DATE = new Date(2e3, 0, 1);
14
5
  function toBase64(bytes) {
15
- let text2 = "";
16
- for (let i = 0; i < bytes.length; i += 32768) text2 += String.fromCharCode(...bytes.subarray(i, i + 32768));
17
- return btoa(text2);
6
+ let text = "";
7
+ for (let i = 0; i < bytes.length; i += 32768) text += String.fromCharCode(...bytes.subarray(i, i + 32768));
8
+ return btoa(text);
18
9
  }
19
10
  var fromBase64 = (value) => Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
20
11
  function imageSize(b) {
@@ -37,6 +28,7 @@ var safeHref = (value) => {
37
28
  };
38
29
 
39
30
  // src/odt.ts
31
+ import { strToU8, zipSync } from "fflate";
40
32
  var ODT_MIME = "application/vnd.oasis.opendocument.text";
41
33
  var NS = 'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" office:version="1.3"';
42
34
  var XML = '<?xml version="1.0" encoding="UTF-8"?>\n';
@@ -135,19 +127,19 @@ function block(node, out, style = "Standard") {
135
127
  function exportOdt(blocks) {
136
128
  const out = { images: [], tables: 0 };
137
129
  const body = blocks.map((b) => block(b, out)).join("\n");
138
- const content2 = `${XML}<office:document-content ${NS}><office:automatic-styles>${AUTO}</office:automatic-styles><office:body><office:text>${body}</office:text></office:body></office:document-content>`;
130
+ const content = `${XML}<office:document-content ${NS}><office:automatic-styles>${AUTO}</office:automatic-styles><office:body><office:text>${body}</office:text></office:body></office:document-content>`;
139
131
  const entries = [
140
132
  `<manifest:file-entry manifest:full-path="/" manifest:version="1.3" manifest:media-type="${ODT_MIME}"/>`,
141
133
  ...["content.xml", "styles.xml", "meta.xml"].map((n) => `<manifest:file-entry manifest:full-path="${n}" manifest:media-type="text/xml"/>`),
142
134
  ...out.images.map((i) => `<manifest:file-entry manifest:full-path="${i.name}" manifest:media-type="${i.type}"/>`)
143
135
  ];
144
136
  const meta = `${XML}<office:document-meta ${NS}><office:meta><meta:generator>agentic-documents</meta:generator></office:meta></office:document-meta>`;
145
- const part = (text2) => [strToU8(text2), { mtime: FIXED_DATE }];
137
+ const part = (text) => [strToU8(text), { mtime: FIXED_DATE }];
146
138
  return zipSync({
147
139
  mimetype: [strToU8(ODT_MIME), { level: 0, mtime: FIXED_DATE }],
148
140
  // first and stored, so file sniffers can read it
149
141
  "META-INF/manifest.xml": part(`${XML}<manifest:manifest ${NS}>${entries.join("")}</manifest:manifest>`),
150
- "content.xml": part(content2),
142
+ "content.xml": part(content),
151
143
  "styles.xml": part(STYLES),
152
144
  "meta.xml": part(meta),
153
145
  ...Object.fromEntries(out.images.map((i) => [i.name, [i.data, { mtime: FIXED_DATE }]]))
@@ -180,14 +172,14 @@ function inline2(node, remote) {
180
172
  }
181
173
  function block2(node, remote) {
182
174
  const kids = (node.content ?? []).map((n) => block2(n, remote)).join("");
183
- const text2 = (node.content ?? []).map((n) => inline2(n, remote)).join("");
175
+ const text = (node.content ?? []).map((n) => inline2(n, remote)).join("");
184
176
  switch (node.type) {
185
177
  case "paragraph":
186
- return `<p>${text2}</p>
178
+ return `<p>${text}</p>
187
179
  `;
188
180
  case "heading": {
189
181
  const level = Math.min(6, Math.max(1, integer(node.attrs?.level, 1)));
190
- return `<h${level}>${text2}</h${level}>
182
+ return `<h${level}>${text}</h${level}>
191
183
  `;
192
184
  }
193
185
  case "blockquote":
@@ -254,136 +246,13 @@ function exportDoc(doc, format, options = {}) {
254
246
  return { format, contentType: "text/markdown; charset=utf-8", encoding: "utf8", data: doc.toMarkdown() };
255
247
  }
256
248
 
257
- // src/commands.ts
258
- import { z } from "zod";
259
- var id = z.string().describe("Document id.");
260
- var content = z.string().describe("Markdown for a section target. Plain text for a text target.");
261
- var text = z.string().describe("Exact text to match. It must sit inside one block.");
262
- var occurrence = z.coerce.number().int().min(1).optional().describe("Which match, counting from 1, when the text matches more than once.");
263
- var side = z.enum(["before", "after"]).optional().describe("Put the new text before or after the match.");
264
- var thread = z.string().describe("Thread id.");
265
- var where = {
266
- section: z.string().optional().describe("Section id from the outline. Use this or text."),
267
- text: text.optional(),
268
- occurrence,
269
- at: z.enum(["start", "end", "before", "after"]).optional().describe("Insert at the start or end of a section, or before or after matched text.")
270
- };
271
- function target(input) {
272
- const { section, text: text2, occurrence: occurrence2, at } = input;
273
- if (section !== void 0) {
274
- if (at !== void 0 && at !== "start" && at !== "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit a section; use start or end`);
275
- return { section, at };
276
- }
277
- if (text2 !== void 0) {
278
- if (at === "start" || at === "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit text; use before or after`);
279
- return { text: text2, occurrence: occurrence2, at };
280
- }
281
- throw new DocError("INVALID_INPUT", "pass section or text");
282
- }
283
- var textTarget = (input) => ({
284
- text: input.text,
285
- occurrence: input.occurrence,
286
- at: input.at
287
- });
288
- function command(name, description, readOnly, shape, run) {
289
- return {
290
- name,
291
- description,
292
- shape,
293
- readOnly,
294
- async run(service, raw, author) {
295
- const parsed = z.object(shape).strict().safeParse(raw);
296
- if (!parsed.success) {
297
- throw new DocError("INVALID_INPUT", parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; "));
298
- }
299
- return run(service, parsed.data, author);
300
- }
301
- };
302
- }
303
- var ok = () => ({ ok: true });
304
- var commands = [
305
- command("new", "Create a document, from Markdown if given.", false, { id, markdown: z.string().optional() }, async (s, i, a) => {
306
- await s.create(i.id, i.markdown ?? "", a);
307
- return { id: i.id };
308
- }),
309
- command(
310
- "read",
311
- "Read a document, or one section, as Markdown.",
312
- true,
313
- { id, section: where.section },
314
- (s, i) => s.read(i.id, (d) => i.section ? d.readSection(i.section) : d.toMarkdown())
315
- ),
316
- command("outline", "List sections with their ids, levels, and titles.", true, { id }, (s, i) => s.read(i.id, (d) => d.outline())),
317
- command("edits", "Read the edit log. It names each author kind and holds no content.", true, { id }, (s, i) => s.read(i.id, (d) => d.edits())),
318
- command("insert", "Insert Markdown into a section, or plain text next to matched text.", false, { id, ...where, content }, async (s, i, a) => {
319
- await s.write(i.id, (d) => d.insert(target(i), i.content, a));
320
- return ok();
321
- }),
322
- command("replace", "Replace a section body (the heading stays), or the matched text.", false, { id, ...where, content }, async (s, i, a) => {
323
- await s.write(i.id, (d) => d.replace(target(i), i.content, a));
324
- return ok();
325
- }),
326
- command("delete", "Delete a section with its subsections, or the matched text.", false, { id, ...where }, async (s, i, a) => {
327
- await s.write(i.id, (d) => d.delete(target(i), a));
328
- return ok();
329
- }),
330
- command("append", "Add Markdown at the end of the document.", false, { id, content: z.string() }, async (s, i, a) => {
331
- await s.write(i.id, (d) => d.append(i.content, a));
332
- return ok();
333
- }),
334
- command("suggest", "Suggest an edit to matched text. An empty replacement suggests a delete.", false, { id, text, occurrence, at: side, replacement: z.string() }, async (s, i, a) => ({
335
- suggestion: await s.write(i.id, (d) => new Suggestions(d).add(textTarget(i), i.replacement, a))
336
- })),
337
- command(
338
- "suggestions",
339
- "List suggested edits.",
340
- true,
341
- { id, status: z.enum(["pending", "accepted", "rejected"]).optional() },
342
- (s, i) => s.read(i.id, (d) => new Suggestions(d).list({ status: i.status }))
343
- ),
344
- command("accept", "Accept a suggestion. The text changes now.", false, { id, suggestion: z.string() }, async (s, i, a) => {
345
- await s.write(i.id, (d) => new Suggestions(d).accept(i.suggestion, a));
346
- return ok();
347
- }),
348
- command("reject", "Reject a suggestion. The text stays.", false, { id, suggestion: z.string() }, async (s, i, a) => {
349
- await s.write(i.id, (d) => new Suggestions(d).reject(i.suggestion, a));
350
- return ok();
351
- }),
352
- command("comment", "Start a comment thread on matched text.", false, { id, text, occurrence, body: z.string() }, async (s, i, a) => ({
353
- thread: await s.write(i.id, (d) => new Threads(d).add(textTarget(i), i.body, a))
354
- })),
355
- command("reply", "Reply in a comment thread.", false, { id, thread, body: z.string() }, async (s, i, a) => {
356
- await s.write(i.id, (d) => new Threads(d).reply(i.thread, i.body, a));
357
- return ok();
358
- }),
359
- command("resolve", "Resolve a comment thread.", false, { id, thread }, async (s, i, a) => {
360
- await s.write(i.id, (d) => new Threads(d).resolve(i.thread, a));
361
- return ok();
362
- }),
363
- command("reopen", "Reopen a comment thread.", false, { id, thread }, async (s, i, a) => {
364
- await s.write(i.id, (d) => new Threads(d).reopen(i.thread, a));
365
- return ok();
366
- }),
367
- command(
368
- "threads",
369
- "List comment threads with their messages.",
370
- true,
371
- { id, status: z.enum(["open", "resolved"]).optional() },
372
- (s, i) => s.read(i.id, (d) => new Threads(d).list({ status: i.status }))
373
- ),
374
- command(
375
- "export",
376
- "Export the document as Markdown (md), self-contained HTML (html), or OpenDocument text (odt, base64). HTML shows no remote image unless remoteImages is true.",
377
- true,
378
- { id, format: z.enum(EXPORT_FORMATS), remoteImages: z.union([z.boolean(), z.enum(["true", "false"])]).optional() },
379
- (s, i) => s.read(i.id, (d) => exportDoc(d, i.format, { remoteImages: i.remoteImages === true || i.remoteImages === "true" }))
380
- )
381
- ];
382
-
383
249
  export {
250
+ DATA_IMAGE,
251
+ toBase64,
252
+ fromBase64,
253
+ safeHref,
384
254
  ODT_MIME,
385
255
  exportOdt,
386
256
  EXPORT_FORMATS,
387
- exportDoc,
388
- commands
257
+ exportDoc
389
258
  };
@@ -0,0 +1,150 @@
1
+ import {
2
+ EXPORT_FORMATS,
3
+ exportDoc
4
+ } from "./chunk-L6W2AQUF.js";
5
+ import {
6
+ Suggestions,
7
+ Threads
8
+ } from "./chunk-UPEGODUV.js";
9
+ import {
10
+ DocError
11
+ } from "./chunk-6QOYS63L.js";
12
+
13
+ // src/commands.ts
14
+ import { z } from "zod";
15
+ var id = z.string().describe("Document id.");
16
+ var content = z.string().describe("Markdown for a section target. Plain text for a text target.");
17
+ var text = z.string().describe("Exact text to match. It must sit inside one block.");
18
+ var occurrence = z.coerce.number().int().min(1).optional().describe("Which match, counting from 1, when the text matches more than once.");
19
+ var side = z.enum(["before", "after"]).optional().describe("Put the new text before or after the match.");
20
+ var thread = z.string().describe("Thread id.");
21
+ var where = {
22
+ section: z.string().optional().describe("Section id from the outline. Use this or text."),
23
+ text: text.optional(),
24
+ occurrence,
25
+ at: z.enum(["start", "end", "before", "after"]).optional().describe("Insert at the start or end of a section, or before or after matched text.")
26
+ };
27
+ function target(input) {
28
+ const { section, text: text2, occurrence: occurrence2, at } = input;
29
+ if (section !== void 0) {
30
+ if (at !== void 0 && at !== "start" && at !== "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit a section; use start or end`);
31
+ return { section, at };
32
+ }
33
+ if (text2 !== void 0) {
34
+ if (at === "start" || at === "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit text; use before or after`);
35
+ return { text: text2, occurrence: occurrence2, at };
36
+ }
37
+ throw new DocError("INVALID_INPUT", "pass section or text");
38
+ }
39
+ var textTarget = (input) => ({
40
+ text: input.text,
41
+ occurrence: input.occurrence,
42
+ at: input.at
43
+ });
44
+ function command(name, description, readOnly, shape, run) {
45
+ return {
46
+ name,
47
+ description,
48
+ shape,
49
+ readOnly,
50
+ async run(service, raw, author) {
51
+ const parsed = z.object(shape).strict().safeParse(raw);
52
+ if (!parsed.success) {
53
+ throw new DocError("INVALID_INPUT", parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; "));
54
+ }
55
+ return run(service, parsed.data, author);
56
+ }
57
+ };
58
+ }
59
+ var ok = () => ({ ok: true });
60
+ var commands = [
61
+ command("new", "Create a document, from Markdown if given.", false, { id, markdown: z.string().optional() }, async (s, i, a) => {
62
+ await s.create(i.id, i.markdown ?? "", a);
63
+ return { id: i.id };
64
+ }),
65
+ command("list", "List documents with their titles, creators, and last edits.", true, {}, (s) => s.list()),
66
+ command("rename", "Give a document a title. The text stays.", false, { id, title: z.string().describe("1 to 120 characters of plain text.") }, async (s, i, a) => {
67
+ await s.rename(i.id, i.title, a);
68
+ return ok();
69
+ }),
70
+ command("remove", "Delete a whole document. A person may. An agent needs the docs:remove scope.", false, { id }, async (s, i, a) => {
71
+ await s.remove(i.id, a);
72
+ return ok();
73
+ }),
74
+ command(
75
+ "read",
76
+ "Read a document, or one section, as Markdown.",
77
+ true,
78
+ { id, section: where.section },
79
+ (s, i) => s.read(i.id, (d) => i.section ? d.readSection(i.section) : d.toMarkdown())
80
+ ),
81
+ command("outline", "List sections with their ids, levels, and titles.", true, { id }, (s, i) => s.read(i.id, (d) => d.outline())),
82
+ command("edits", "Read the edit log. It names each author kind and holds no content.", true, { id }, (s, i) => s.read(i.id, (d) => d.edits())),
83
+ command("insert", "Insert Markdown into a section, or plain text next to matched text.", false, { id, ...where, content }, async (s, i, a) => {
84
+ await s.write(i.id, (d) => d.insert(target(i), i.content, a), a);
85
+ return ok();
86
+ }),
87
+ command("replace", "Replace a section body (the heading stays), or the matched text.", false, { id, ...where, content }, async (s, i, a) => {
88
+ await s.write(i.id, (d) => d.replace(target(i), i.content, a), a);
89
+ return ok();
90
+ }),
91
+ command("delete", "Delete a section with its subsections, or the matched text.", false, { id, ...where }, async (s, i, a) => {
92
+ await s.write(i.id, (d) => d.delete(target(i), a), a);
93
+ return ok();
94
+ }),
95
+ command("append", "Add Markdown at the end of the document.", false, { id, content: z.string() }, async (s, i, a) => {
96
+ await s.write(i.id, (d) => d.append(i.content, a), a);
97
+ return ok();
98
+ }),
99
+ command("suggest", "Suggest an edit to matched text. An empty replacement suggests a delete.", false, { id, text, occurrence, at: side, replacement: z.string() }, async (s, i, a) => ({
100
+ suggestion: await s.write(i.id, (d) => new Suggestions(d).add(textTarget(i), i.replacement, a), a)
101
+ })),
102
+ command(
103
+ "suggestions",
104
+ "List suggested edits.",
105
+ true,
106
+ { id, status: z.enum(["pending", "accepted", "rejected"]).optional() },
107
+ (s, i) => s.read(i.id, (d) => new Suggestions(d).list({ status: i.status }))
108
+ ),
109
+ command("accept", "Accept a suggestion. The text changes now.", false, { id, suggestion: z.string() }, async (s, i, a) => {
110
+ await s.write(i.id, (d) => new Suggestions(d).accept(i.suggestion, a), a);
111
+ return ok();
112
+ }),
113
+ command("reject", "Reject a suggestion. The text stays.", false, { id, suggestion: z.string() }, async (s, i, a) => {
114
+ await s.write(i.id, (d) => new Suggestions(d).reject(i.suggestion, a), a);
115
+ return ok();
116
+ }),
117
+ command("comment", "Start a comment thread on matched text.", false, { id, text, occurrence, body: z.string() }, async (s, i, a) => ({
118
+ thread: await s.write(i.id, (d) => new Threads(d).add(textTarget(i), i.body, a), a)
119
+ })),
120
+ command("reply", "Reply in a comment thread.", false, { id, thread, body: z.string() }, async (s, i, a) => {
121
+ await s.write(i.id, (d) => new Threads(d).reply(i.thread, i.body, a), a);
122
+ return ok();
123
+ }),
124
+ command("resolve", "Resolve a comment thread.", false, { id, thread }, async (s, i, a) => {
125
+ await s.write(i.id, (d) => new Threads(d).resolve(i.thread, a), a);
126
+ return ok();
127
+ }),
128
+ command("reopen", "Reopen a comment thread.", false, { id, thread }, async (s, i, a) => {
129
+ await s.write(i.id, (d) => new Threads(d).reopen(i.thread, a), a);
130
+ return ok();
131
+ }),
132
+ command(
133
+ "threads",
134
+ "List comment threads with their messages.",
135
+ true,
136
+ { id, status: z.enum(["open", "resolved"]).optional() },
137
+ (s, i) => s.read(i.id, (d) => new Threads(d).list({ status: i.status }))
138
+ ),
139
+ command(
140
+ "export",
141
+ "Export the document as Markdown (md), self-contained HTML (html), or OpenDocument text (odt, base64). HTML shows no remote image unless remoteImages is true.",
142
+ true,
143
+ { id, format: z.enum(EXPORT_FORMATS), remoteImages: z.union([z.boolean(), z.enum(["true", "false"])]).optional() },
144
+ (s, i) => s.read(i.id, (d) => exportDoc(d, i.format, { remoteImages: i.remoteImages === true || i.remoteImages === "true" }))
145
+ )
146
+ ];
147
+
148
+ export {
149
+ commands
150
+ };
@@ -0,0 +1,195 @@
1
+ import {
2
+ APPLIED,
3
+ DocError,
4
+ checkAuthor
5
+ } from "./chunk-6QOYS63L.js";
6
+
7
+ // src/review.ts
8
+ import * as Y from "yjs";
9
+ var newId = () => globalThis.crypto.randomUUID();
10
+ function nonEmpty(value, what) {
11
+ if (typeof value !== "string" || value.trim() === "") throw new DocError("INVALID_INPUT", `${what} must not be empty`);
12
+ return value;
13
+ }
14
+ function anchorFor(doc, target) {
15
+ if ("anchor" in target) {
16
+ doc.resolve(target);
17
+ return target.anchor;
18
+ }
19
+ return doc.anchor(target.text, target.occurrence);
20
+ }
21
+ function isStale(doc, anchor) {
22
+ try {
23
+ doc.resolve({ anchor });
24
+ return { stale: false, currentText: anchor.text };
25
+ } catch (error) {
26
+ if (!(error instanceof DocError) || error.code !== "STALE_ANCHOR") throw error;
27
+ return { stale: true, currentText: null };
28
+ }
29
+ }
30
+ function fieldsOf(doc, name) {
31
+ return doc.ydoc.getMap(name);
32
+ }
33
+ var optional = (map, key) => map.has(key) ? { [key]: map.get(key) } : {};
34
+ function setAll(map, values) {
35
+ for (const [key, value] of Object.entries(values)) if (value !== void 0) map.set(key, value);
36
+ }
37
+ var Threads = class {
38
+ constructor(doc) {
39
+ this.doc = doc;
40
+ }
41
+ doc;
42
+ get map() {
43
+ return fieldsOf(this.doc, "threads");
44
+ }
45
+ get(id) {
46
+ const thread = this.map.get(id);
47
+ if (!thread) throw new DocError("NO_THREAD", `no thread with id "${id}"`);
48
+ return thread;
49
+ }
50
+ message(body, author) {
51
+ return { id: newId(), at: this.doc.now().toISOString(), author, body };
52
+ }
53
+ add(target, body, author) {
54
+ const who = checkAuthor(author);
55
+ nonEmpty(body, "a comment");
56
+ const anchor = anchorFor(this.doc, target);
57
+ const id = newId();
58
+ this.doc.ydoc.transact(() => {
59
+ const thread = new Y.Map();
60
+ this.map.set(id, thread);
61
+ setAll(thread, { anchor, status: "open" });
62
+ const messages = new Y.Array();
63
+ thread.set("messages", messages);
64
+ messages.push([this.message(body, who)]);
65
+ });
66
+ return id;
67
+ }
68
+ reply(id, body, author) {
69
+ const who = checkAuthor(author);
70
+ nonEmpty(body, "a reply");
71
+ this.get(id).get("messages").push([this.message(body, who)]);
72
+ }
73
+ setStatus(id, status, author) {
74
+ const who = checkAuthor(author);
75
+ const thread = this.get(id);
76
+ this.doc.ydoc.transact(() => {
77
+ thread.set("status", status);
78
+ if (status === "open") {
79
+ thread.delete("resolvedBy");
80
+ thread.delete("resolvedAt");
81
+ } else setAll(thread, { resolvedBy: who, resolvedAt: this.doc.now().toISOString() });
82
+ });
83
+ }
84
+ resolve(id, author) {
85
+ this.setStatus(id, "resolved", author);
86
+ }
87
+ reopen(id, author) {
88
+ this.setStatus(id, "open", author);
89
+ }
90
+ list(filter = {}) {
91
+ return [...this.map.entries()].map(([id, t]) => {
92
+ const anchor = t.get("anchor");
93
+ return {
94
+ id,
95
+ anchor,
96
+ status: t.get("status"),
97
+ ...optional(t, "resolvedBy"),
98
+ ...optional(t, "resolvedAt"),
99
+ messages: t.get("messages").toArray(),
100
+ ...isStale(this.doc, anchor)
101
+ };
102
+ }).filter((t) => filter.status === void 0 || t.status === filter.status).sort((a, b) => a.messages[0].at.localeCompare(b.messages[0].at) || a.id.localeCompare(b.id));
103
+ }
104
+ };
105
+ var ACCEPT_SCOPE = "docs:accept";
106
+ var sameWho = (a, b) => a.kind === b.kind && a.name === b.name;
107
+ var isPerson = (who) => who.kind === "human" || who.kind === "user_token";
108
+ var mayDecide = (who) => isPerson(who) || who.scopes?.includes(ACCEPT_SCOPE) === true;
109
+ var Suggestions = class {
110
+ constructor(doc) {
111
+ this.doc = doc;
112
+ }
113
+ doc;
114
+ get map() {
115
+ return fieldsOf(this.doc, "suggestions");
116
+ }
117
+ get(id) {
118
+ const fields = this.map.get(id);
119
+ if (!fields) throw new DocError("NO_SUGGESTION", `no suggestion with id "${id}"`);
120
+ return { fields, suggestion: this.read(id, fields) };
121
+ }
122
+ read(id, f) {
123
+ const anchor = f.get("anchor");
124
+ return {
125
+ id,
126
+ kind: f.get("kind"),
127
+ ...optional(f, "at"),
128
+ anchor,
129
+ replacement: f.get("replacement"),
130
+ author: f.get("author"),
131
+ createdAt: f.get("createdAt"),
132
+ status: f.get("status"),
133
+ ...optional(f, "decidedBy"),
134
+ ...optional(f, "decidedAt"),
135
+ stale: isStale(this.doc, anchor).stale
136
+ };
137
+ }
138
+ /** An `at` on the target means insert. An empty replacement means delete. */
139
+ add(target, replacement, author) {
140
+ const who = checkAuthor(author);
141
+ const kind = target.at ? "insert" : replacement === "" ? "delete" : "replace";
142
+ if (kind === "insert") nonEmpty(replacement, "an inserted text");
143
+ const anchor = anchorFor(this.doc, target);
144
+ const id = newId();
145
+ this.doc.ydoc.transact(() => {
146
+ const fields = new Y.Map();
147
+ this.map.set(id, fields);
148
+ setAll(fields, { kind, at: target.at, anchor, replacement, author: who, createdAt: this.doc.now().toISOString(), status: "pending" });
149
+ });
150
+ return id;
151
+ }
152
+ decide(fields, status, author, at) {
153
+ setAll(fields, { status, decidedBy: author, decidedAt: at });
154
+ }
155
+ /**
156
+ * Apply the edit as the accepting author. The check and the edit share one transaction. Accepting
157
+ * an accepted suggestion changes nothing. A stale anchor throws and the suggestion stays pending.
158
+ */
159
+ accept(id, author) {
160
+ const who = checkAuthor(author);
161
+ this.doc.ydoc.transact(() => {
162
+ const { fields, suggestion: s } = this.get(id);
163
+ if (!mayDecide(author) || sameWho(s.author, who)) {
164
+ throw new DocError("FORBIDDEN", `${who.kind}:${who.name} cannot accept this suggestion: a person who is not its author, or an agent with the ${ACCEPT_SCOPE} scope, can`);
165
+ }
166
+ if (s.status === "accepted") return;
167
+ if (s.status !== "pending") throw new DocError("ALREADY_DECIDED", `suggestion "${id}" is already ${s.status}`);
168
+ const { at, inserted } = this.doc.edit(s.kind, { anchor: s.anchor, at: s.at }, s.replacement, who, { suggestion: { id, author: s.author } });
169
+ const record = { by: who, at, ...inserted };
170
+ this.doc.ydoc.getMap(APPLIED).set(`${id}:${this.doc.ydoc.clientID}`, record);
171
+ this.decide(fields, "accepted", who, at);
172
+ });
173
+ }
174
+ /** Same right as accept. A person can withdraw their own suggestion. An agent can never decide its own. */
175
+ reject(id, author) {
176
+ const who = checkAuthor(author);
177
+ this.doc.ydoc.transact(() => {
178
+ const { fields, suggestion: s } = this.get(id);
179
+ if (!mayDecide(author) || !isPerson(who) && sameWho(s.author, who)) {
180
+ throw new DocError("FORBIDDEN", `${who.kind}:${who.name} cannot reject this suggestion: a person, or an agent with the ${ACCEPT_SCOPE} scope that did not write it, can`);
181
+ }
182
+ if (s.status !== "pending") throw new DocError("ALREADY_DECIDED", `suggestion "${id}" is already ${s.status}`);
183
+ this.decide(fields, "rejected", who, this.doc.now().toISOString());
184
+ });
185
+ }
186
+ list(filter = {}) {
187
+ return [...this.map.entries()].map(([id, f]) => this.read(id, f)).filter((s) => filter.status === void 0 || s.status === filter.status).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
188
+ }
189
+ };
190
+
191
+ export {
192
+ Threads,
193
+ isPerson,
194
+ Suggestions
195
+ };