agentic-documents 0.0.0-stage → 0.1.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,1123 @@
1
+ // src/errors.ts
2
+ var DocError = class extends Error {
3
+ constructor(code, message, matches = []) {
4
+ super(message);
5
+ this.code = code;
6
+ this.matches = matches;
7
+ this.name = "DocError";
8
+ }
9
+ code;
10
+ matches;
11
+ };
12
+
13
+ // src/markdown.ts
14
+ import { fromMarkdown } from "mdast-util-from-markdown";
15
+ import { gfmFromMarkdown, gfmToMarkdown } from "mdast-util-gfm";
16
+ import { toMarkdown } from "mdast-util-to-markdown";
17
+ import { gfm } from "micromark-extension-gfm";
18
+ var definitions = /* @__PURE__ */ new Map();
19
+ function unsupported(what) {
20
+ throw new DocError("INVALID_INPUT", `Markdown ${what} is not supported and would be lost; remove it or write it inline`);
21
+ }
22
+ function collectDefinitions(nodes2, into) {
23
+ for (const node of nodes2) {
24
+ if (node.type === "definition") into.set(node.identifier, node);
25
+ const children = node.children;
26
+ if (children) collectDefinitions(children, into);
27
+ }
28
+ }
29
+ var withContent = (node, content2) => content2.length ? { ...node, content: content2 } : node;
30
+ function inline(nodes2, marks2 = []) {
31
+ return nodes2.flatMap((node) => {
32
+ const text2 = (value, own) => value ? [{ type: "text", text: value, ...own.length ? { marks: own } : {} }] : [];
33
+ switch (node.type) {
34
+ case "text":
35
+ return text2(node.value, marks2);
36
+ case "html":
37
+ return text2(node.value, marks2);
38
+ case "inlineCode":
39
+ return text2(node.value, [{ type: "code" }]);
40
+ case "strong":
41
+ return inline(node.children, [...marks2, { type: "bold" }]);
42
+ case "emphasis":
43
+ return inline(node.children, [...marks2, { type: "italic" }]);
44
+ case "delete":
45
+ return inline(node.children, [...marks2, { type: "strike" }]);
46
+ case "link":
47
+ return inline(node.children, [...marks2, { type: "link", attrs: { href: node.url } }]);
48
+ case "image":
49
+ return [{ type: "image", attrs: { src: node.url, alt: node.alt ?? null, title: node.title ?? null } }];
50
+ case "break":
51
+ return [{ type: "hardBreak" }];
52
+ case "linkReference":
53
+ return inline(node.children, [...marks2, { type: "link", attrs: { href: definitions.get(node.identifier)?.url ?? unsupported("link reference without a definition") } }]);
54
+ case "imageReference": {
55
+ const url2 = definitions.get(node.identifier)?.url ?? unsupported("image reference without a definition");
56
+ return [{ type: "image", attrs: { src: url2, alt: node.alt ?? null, title: null } }];
57
+ }
58
+ default:
59
+ return unsupported(`"${node.type}"`);
60
+ }
61
+ });
62
+ }
63
+ function itemContent(children) {
64
+ const content2 = blocks(children);
65
+ return content2[0]?.type === "paragraph" ? content2 : [{ type: "paragraph" }, ...content2];
66
+ }
67
+ function cell(type, node) {
68
+ return { type, content: [withContent({ type: "paragraph" }, inline(node.children))] };
69
+ }
70
+ function blocks(nodes2) {
71
+ return nodes2.flatMap((node) => {
72
+ switch (node.type) {
73
+ case "heading":
74
+ return [withContent({ type: "heading", attrs: { level: node.depth } }, inline(node.children))];
75
+ case "paragraph":
76
+ return [withContent({ type: "paragraph" }, inline(node.children))];
77
+ case "html":
78
+ return [withContent({ type: "paragraph" }, inline([node]))];
79
+ case "blockquote":
80
+ return [{ type: "blockquote", content: blocks(node.children) }];
81
+ case "code":
82
+ return [withContent({ type: "codeBlock", attrs: { language: node.lang ?? null } }, node.value ? [{ type: "text", text: node.value }] : [])];
83
+ case "thematicBreak":
84
+ return [{ type: "horizontalRule" }];
85
+ case "list": {
86
+ const task = node.children.some((item) => item.checked != null);
87
+ const items = node.children.map(
88
+ (item) => ({
89
+ type: task ? "taskItem" : "listItem",
90
+ ...task ? { attrs: { checked: item.checked === true } } : {},
91
+ content: itemContent(item.children)
92
+ })
93
+ );
94
+ if (task) return [{ type: "taskList", content: items }];
95
+ return node.ordered ? [{ type: "orderedList", attrs: { start: node.start ?? 1 }, content: items }] : [{ type: "bulletList", content: items }];
96
+ }
97
+ case "table":
98
+ return [
99
+ {
100
+ type: "table",
101
+ content: node.children.map((row, i) => ({
102
+ type: "tableRow",
103
+ content: row.children.map((c) => cell(i === 0 ? "tableHeader" : "tableCell", c))
104
+ }))
105
+ }
106
+ ];
107
+ case "definition":
108
+ return [];
109
+ default:
110
+ return unsupported(`"${node.type}"`);
111
+ }
112
+ });
113
+ }
114
+ function markdownToBlocks(markdown) {
115
+ const tree = fromMarkdown(markdown, { extensions: [gfm()], mdastExtensions: [gfmFromMarkdown()] });
116
+ definitions = /* @__PURE__ */ new Map();
117
+ collectDefinitions(tree.children, definitions);
118
+ let out;
119
+ try {
120
+ out = blocks(tree.children);
121
+ } finally {
122
+ definitions = /* @__PURE__ */ new Map();
123
+ }
124
+ return out.length ? out : [{ type: "paragraph" }];
125
+ }
126
+ var MARK_ORDER = ["link", "bold", "italic", "strike", "code"];
127
+ function leaf(node) {
128
+ if (node.type === "image") {
129
+ return { type: "image", url: String(node.attrs?.src ?? ""), alt: node.attrs?.alt ?? null, title: node.attrs?.title ?? null };
130
+ }
131
+ if (node.type === "hardBreak") return { type: "break" };
132
+ return { type: "text", value: node.text ?? "" };
133
+ }
134
+ function wrap(nodes2, depth) {
135
+ if (depth === MARK_ORDER.length) return nodes2.map(leaf);
136
+ const name = MARK_ORDER[depth];
137
+ const find = (node) => node.marks?.find((m) => m.type === name);
138
+ const out = [];
139
+ for (let i = 0; i < nodes2.length; ) {
140
+ const mark = find(nodes2[i]);
141
+ let j = i + 1;
142
+ while (j < nodes2.length && JSON.stringify(find(nodes2[j])?.attrs?.href) === JSON.stringify(mark?.attrs?.href) && !!find(nodes2[j]) === !!mark) j++;
143
+ const run = nodes2.slice(i, j);
144
+ i = j;
145
+ if (!mark) out.push(...wrap(run, depth + 1));
146
+ else if (name === "code") out.push({ type: "inlineCode", value: run.map((n) => n.text ?? "").join("") });
147
+ else {
148
+ const children = wrap(run, depth + 1);
149
+ if (name === "link") out.push({ type: "link", url: String(mark.attrs?.href ?? ""), children });
150
+ else out.push({ type: name === "bold" ? "strong" : name === "italic" ? "emphasis" : "delete", children });
151
+ }
152
+ }
153
+ return out;
154
+ }
155
+ function phrasing(node) {
156
+ const nodes2 = [...node.content ?? []];
157
+ const first = nodes2[0];
158
+ if (first?.type === "text") nodes2[0] = { ...first, text: first.text?.trimStart() };
159
+ const last = nodes2[nodes2.length - 1];
160
+ if (last?.type === "text") nodes2[nodes2.length - 1] = { ...last, text: last.text?.trimEnd() };
161
+ return wrap(nodes2.filter((n) => n.type !== "text" || n.text), 0);
162
+ }
163
+ function cellText(node) {
164
+ return (node.content ?? []).flatMap((p, i) => i ? [{ type: "break" }, ...phrasing(p)] : phrasing(p));
165
+ }
166
+ function blocksOut(nodes2) {
167
+ return nodes2.flatMap((node) => {
168
+ const children = node.content ?? [];
169
+ switch (node.type) {
170
+ case "paragraph":
171
+ return children.length ? [{ type: "paragraph", children: phrasing(node) }] : [];
172
+ case "heading":
173
+ return [{ type: "heading", depth: Math.min(6, Math.max(1, Number(node.attrs?.level ?? 1))), children: phrasing(node) }];
174
+ case "blockquote":
175
+ return [{ type: "blockquote", children: blocksOut(children) }];
176
+ case "codeBlock":
177
+ return [{ type: "code", lang: node.attrs?.language ?? null, value: children.map((c) => c.text ?? "").join("") }];
178
+ case "horizontalRule":
179
+ return [{ type: "thematicBreak" }];
180
+ case "bulletList":
181
+ case "orderedList":
182
+ case "taskList": {
183
+ const task = node.type === "taskList";
184
+ return [
185
+ {
186
+ type: "list",
187
+ ordered: node.type === "orderedList",
188
+ start: node.type === "orderedList" ? Number(node.attrs?.start ?? 1) : null,
189
+ spread: false,
190
+ children: children.map((item) => ({
191
+ type: "listItem",
192
+ spread: false,
193
+ ...task ? { checked: item.attrs?.checked === true } : {},
194
+ children: blocksOut(item.content ?? [])
195
+ }))
196
+ }
197
+ ];
198
+ }
199
+ case "table":
200
+ return [
201
+ {
202
+ type: "table",
203
+ children: children.map((row) => ({
204
+ type: "tableRow",
205
+ children: (row.content ?? []).map((c) => ({ type: "tableCell", children: cellText(c) }))
206
+ }))
207
+ }
208
+ ];
209
+ default:
210
+ return [];
211
+ }
212
+ });
213
+ }
214
+ function blocksToMarkdown(nodes2) {
215
+ const root = { type: "root", children: blocksOut(nodes2) };
216
+ return toMarkdown(root, { extensions: [gfmToMarkdown()], bullet: "-", rule: "-", fences: true, emphasis: "*", strong: "*" });
217
+ }
218
+
219
+ // src/schema.ts
220
+ import { Schema } from "prosemirror-model";
221
+ var cell2 = {
222
+ content: "block+",
223
+ attrs: { colspan: { default: 1 }, rowspan: { default: 1 }, colwidth: { default: null } }
224
+ };
225
+ var nodes = {
226
+ doc: { content: "block+" },
227
+ paragraph: { group: "block", content: "inline*" },
228
+ text: { group: "inline" },
229
+ heading: { group: "block", content: "inline*", attrs: { level: { default: 1 } } },
230
+ blockquote: { group: "block", content: "block+" },
231
+ codeBlock: { group: "block", content: "text*", marks: "", code: true, attrs: { language: { default: null } } },
232
+ horizontalRule: { group: "block" },
233
+ bulletList: { group: "block", content: "listItem+" },
234
+ orderedList: { group: "block", content: "listItem+", attrs: { start: { default: 1 }, type: { default: null } } },
235
+ listItem: { content: "paragraph block*" },
236
+ taskList: { group: "block", content: "taskItem+" },
237
+ taskItem: { content: "paragraph block*", attrs: { checked: { default: false } } },
238
+ hardBreak: { group: "inline", inline: true },
239
+ image: {
240
+ group: "inline",
241
+ inline: true,
242
+ attrs: { src: { default: null }, alt: { default: null }, title: { default: null } }
243
+ },
244
+ table: { group: "block", content: "tableRow+" },
245
+ tableRow: { content: "(tableCell | tableHeader)*" },
246
+ tableCell: cell2,
247
+ tableHeader: cell2
248
+ };
249
+ var marks = {
250
+ bold: {},
251
+ italic: {},
252
+ strike: {},
253
+ code: { excludes: "_" },
254
+ link: {
255
+ inclusive: false,
256
+ attrs: {
257
+ href: { default: null },
258
+ target: { default: "_blank" },
259
+ rel: { default: "noopener noreferrer nofollow" },
260
+ class: { default: null }
261
+ }
262
+ }
263
+ };
264
+ var schema = new Schema({ nodes, marks });
265
+
266
+ // src/doc.ts
267
+ import { prosemirrorJSONToYXmlFragment, yXmlFragmentToProsemirrorJSON } from "y-prosemirror";
268
+ import * as Y2 from "yjs";
269
+
270
+ // src/settle.ts
271
+ import * as Y from "yjs";
272
+ var APPLIED = "applied";
273
+ var clientOf = (key) => Number(key.slice(key.lastIndexOf(":") + 1));
274
+ function settle(ydoc) {
275
+ const applied = ydoc.getMap(APPLIED);
276
+ const groups = /* @__PURE__ */ new Map();
277
+ for (const [key, record] of applied) {
278
+ const id2 = key.slice(0, key.lastIndexOf(":"));
279
+ groups.set(id2, [...groups.get(id2) ?? [], [key, record]]);
280
+ }
281
+ if (groups.size === 0) return;
282
+ const suggestions = ydoc.getMap("suggestions");
283
+ const log = ydoc.getArray("edits");
284
+ ydoc.transact(() => {
285
+ for (const [id2, records] of groups) {
286
+ records.sort((a, b) => clientOf(a[0]) - clientOf(b[0]));
287
+ const [[, winner], ...losers] = records;
288
+ const fields = suggestions.get(id2);
289
+ const decided = (key) => JSON.stringify(fields?.get(key));
290
+ if (fields && (decided("status") !== '"accepted"' || decided("decidedBy") !== JSON.stringify(winner.by) || decided("decidedAt") !== JSON.stringify(winner.at))) {
291
+ fields.set("status", "accepted");
292
+ fields.set("decidedBy", winner.by);
293
+ fields.set("decidedAt", winner.at);
294
+ }
295
+ if (losers.length === 0) continue;
296
+ for (const [key, loser] of losers) {
297
+ applied.delete(key);
298
+ if (!loser.start || !loser.end) continue;
299
+ const from = Y.createAbsolutePositionFromRelativePosition(Y.createRelativePositionFromJSON(loser.start), ydoc);
300
+ const to = Y.createAbsolutePositionFromRelativePosition(Y.createRelativePositionFromJSON(loser.end), ydoc);
301
+ if (from && to && from.type === to.type && to.index > from.index && from.type instanceof Y.XmlText) {
302
+ from.type.delete(from.index, to.index - from.index);
303
+ }
304
+ }
305
+ const entries = log.toArray().flatMap((e, i) => e.suggestion?.id === id2 ? [{ e, i }] : []);
306
+ const keep = entries.find(({ e }) => e.at === winner.at && JSON.stringify(e.author) === JSON.stringify(winner.by)) ?? entries[0];
307
+ for (const { i } of entries.reverse()) if (i !== keep?.i) log.delete(i, 1);
308
+ }
309
+ });
310
+ }
311
+
312
+ // src/doc.ts
313
+ var PREAMBLE = "preamble";
314
+ var FRAGMENT = "default";
315
+ var CONTEXT_CHARS = 20;
316
+ var PRINCIPAL_KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
317
+ var isSection = (t) => "section" in t;
318
+ var isAnchor = (t) => "anchor" in t;
319
+ function checkAuthor(author) {
320
+ const scopes = author?.scopes;
321
+ const ok2 = typeof author === "object" && author !== null && PRINCIPAL_KINDS.includes(author.kind) && typeof author.name === "string" && author.name.trim() !== "" && (scopes === void 0 || Array.isArray(scopes) && scopes.every((s) => typeof s === "string" && s !== ""));
322
+ if (!ok2) throw new DocError("INVALID_AUTHOR", `author needs a kind (${PRINCIPAL_KINDS.join(", ")}), a name, and scopes that are a list of text`);
323
+ return { kind: author.kind, name: author.name };
324
+ }
325
+ function parseAuthor(value) {
326
+ if (value === void 0) return void 0;
327
+ const cut = value.indexOf(":");
328
+ return { kind: cut < 0 ? value : value.slice(0, cut), name: cut < 0 ? "" : value.slice(cut + 1) };
329
+ }
330
+ var unitText = (unit) => unit.toDelta().map((d) => typeof d.insert === "string" ? d.insert : "").join("");
331
+ function unitsOf(node) {
332
+ return node.toArray().flatMap((child) => child instanceof Y2.XmlText ? [child] : child instanceof Y2.XmlElement ? unitsOf(child) : []);
333
+ }
334
+ function attrsAt(unit, index) {
335
+ let pos = 0;
336
+ for (const op of unit.toDelta()) {
337
+ pos += typeof op.insert === "string" ? op.insert.length : 1;
338
+ if (index < pos) return { ...op.attributes };
339
+ }
340
+ return {};
341
+ }
342
+ function sectionId(el) {
343
+ const { client, clock } = el._item.id;
344
+ return `s${client.toString(36)}-${clock}`;
345
+ }
346
+ function toYBlocks(content2) {
347
+ const scratch = new Y2.Doc().getXmlFragment(FRAGMENT);
348
+ prosemirrorJSONToYXmlFragment(schema, { type: "doc", content: content2 }, scratch);
349
+ return scratch.toArray().map((el) => el.clone());
350
+ }
351
+ var AgenticDoc = class _AgenticDoc {
352
+ ydoc;
353
+ now;
354
+ constructor(ydoc = new Y2.Doc(), options = {}) {
355
+ this.ydoc = ydoc;
356
+ this.now = options.now ?? (() => /* @__PURE__ */ new Date());
357
+ const watched = [ydoc.getMap(APPLIED), ydoc.getMap("suggestions")];
358
+ ydoc.on("afterTransaction", (tr) => {
359
+ if (!tr.local && watched.some((type) => tr.changedParentTypes.has(type))) settle(ydoc);
360
+ });
361
+ }
362
+ static fromMarkdown(markdown, author, options) {
363
+ const who = checkAuthor(author);
364
+ const doc = new _AgenticDoc(new Y2.Doc(), options);
365
+ const content2 = markdownToBlocks(markdown);
366
+ doc.change("create", who, () => {
367
+ doc.fragment.insert(0, toYBlocks(content2));
368
+ return { inserted: content2.length, deleted: 0 };
369
+ });
370
+ return doc;
371
+ }
372
+ static fromUpdate(update, options) {
373
+ const doc = new _AgenticDoc(new Y2.Doc(), options);
374
+ doc.applyUpdate(update);
375
+ return doc;
376
+ }
377
+ encode() {
378
+ return Y2.encodeStateAsUpdate(this.ydoc);
379
+ }
380
+ applyUpdate(update) {
381
+ Y2.applyUpdate(this.ydoc, update);
382
+ }
383
+ /** A new replica with its own client id, holding the same state. */
384
+ fork() {
385
+ return _AgenticDoc.fromUpdate(this.encode(), { now: this.now });
386
+ }
387
+ get fragment() {
388
+ return this.ydoc.getXmlFragment(FRAGMENT);
389
+ }
390
+ get log() {
391
+ return this.ydoc.getArray("edits");
392
+ }
393
+ edits() {
394
+ return this.log.toArray();
395
+ }
396
+ change(op, author, run, extra = {}) {
397
+ const at = this.now().toISOString();
398
+ this.ydoc.transact(() => {
399
+ const entry = { op, at, author, ...run(), ...extra };
400
+ if (entry.section === void 0) delete entry.section;
401
+ this.log.push([entry]);
402
+ });
403
+ return at;
404
+ }
405
+ toJSON() {
406
+ return yXmlFragmentToProsemirrorJSON(this.fragment);
407
+ }
408
+ toMarkdown() {
409
+ return blocksToMarkdown(this.toJSON().content ?? []);
410
+ }
411
+ /** Sections in order. Blocks before the first heading come first, as the `preamble` section at level 0. */
412
+ outline() {
413
+ const headings = this.headings();
414
+ const sections = headings.map(({ el }) => ({
415
+ id: sectionId(el),
416
+ level: Number(el.getAttribute("level") ?? 1),
417
+ title: unitsOf(el).map(unitText).join("")
418
+ }));
419
+ const before = headings.length ? headings[0].index : this.fragment.length;
420
+ return before > 0 ? [{ id: PREAMBLE, level: 0, title: "Preamble" }, ...sections] : sections;
421
+ }
422
+ readSection(id2) {
423
+ const { start, end } = this.range(id2);
424
+ return blocksToMarkdown((this.toJSON().content ?? []).slice(start, end));
425
+ }
426
+ headings() {
427
+ return this.fragment.toArray().flatMap((el, index) => el instanceof Y2.XmlElement && el.nodeName === "heading" ? [{ el, index }] : []);
428
+ }
429
+ /**
430
+ * A section runs from its heading to the next heading of the same or a higher level. The preamble
431
+ * has no heading and runs to the first heading. It is always a valid target, even when empty.
432
+ */
433
+ range(id2) {
434
+ const all = this.headings();
435
+ if (id2 === PREAMBLE) return { start: 0, end: all.length ? all[0].index : this.fragment.length, headed: false };
436
+ const at = all.findIndex(({ el }) => sectionId(el) === id2);
437
+ if (at < 0) throw new DocError("NO_SECTION", `no section with id "${id2}"`);
438
+ const level = Number(all[at].el.getAttribute("level") ?? 1);
439
+ const next = all.slice(at + 1).find(({ el }) => Number(el.getAttribute("level") ?? 1) <= level);
440
+ return { start: all[at].index, end: next ? next.index : this.fragment.length, headed: true };
441
+ }
442
+ find(text2) {
443
+ if (text2 === "") return [];
444
+ return unitsOf(this.fragment).flatMap((unit) => {
445
+ const found = [];
446
+ const value = unitText(unit);
447
+ for (let at = value.indexOf(text2); at >= 0; at = value.indexOf(text2, at + 1)) {
448
+ found.push({ unit, start: at, end: at + text2.length });
449
+ }
450
+ return found;
451
+ });
452
+ }
453
+ pick(text2, occurrence2) {
454
+ const found = this.find(text2);
455
+ if (found.length === 0) throw new DocError("NO_MATCH", `no text matches "${text2}"`);
456
+ if (occurrence2 !== void 0) {
457
+ if (found[occurrence2 - 1] === void 0) throw new DocError("NO_MATCH", `"${text2}" has ${found.length} matches, not ${occurrence2}`);
458
+ return found[occurrence2 - 1];
459
+ }
460
+ if (found.length > 1) {
461
+ const matches = found.map((m, i) => {
462
+ const value = unitText(m.unit);
463
+ return { occurrence: i + 1, context: value.slice(Math.max(0, m.start - CONTEXT_CHARS), m.end + CONTEXT_CHARS) };
464
+ });
465
+ throw new DocError("AMBIGUOUS_MATCH", `"${text2}" matches ${found.length} places; pass an occurrence`, matches);
466
+ }
467
+ return found[0];
468
+ }
469
+ /** Pin a text range so later edits by other replicas do not move it. */
470
+ anchor(text2, occurrence2) {
471
+ const { unit, start, end } = this.pick(text2, occurrence2);
472
+ return {
473
+ text: text2,
474
+ start: Y2.relativePositionToJSON(Y2.createRelativePositionFromTypeIndex(unit, start, 0)),
475
+ end: Y2.relativePositionToJSON(Y2.createRelativePositionFromTypeIndex(unit, end, -1))
476
+ };
477
+ }
478
+ resolve(target2) {
479
+ if (!isAnchor(target2)) return this.pick(target2.text, target2.occurrence);
480
+ const { anchor } = target2;
481
+ const start = Y2.createAbsolutePositionFromRelativePosition(Y2.createRelativePositionFromJSON(anchor.start), this.ydoc);
482
+ const end = Y2.createAbsolutePositionFromRelativePosition(Y2.createRelativePositionFromJSON(anchor.end), this.ydoc);
483
+ const unit = start?.type;
484
+ if (!(unit instanceof Y2.XmlText) || end?.type !== unit || unitText(unit).slice(start.index, end.index) !== anchor.text) {
485
+ throw new DocError("STALE_ANCHOR", `the anchored text "${anchor.text}" changed or was removed`);
486
+ }
487
+ return { unit, start: start.index, end: end.index };
488
+ }
489
+ edit(op, target2, content2, author, extra = {}) {
490
+ const who = checkAuthor(author);
491
+ const { unit, start, end } = this.resolve(target2);
492
+ let inserted;
493
+ const at = this.change(op, who, () => {
494
+ const marks2 = attrsAt(unit, op === "insert" && target2.at !== "before" ? end - 1 : start);
495
+ if (op === "insert") delete marks2.link;
496
+ const index = op === "insert" ? target2.at === "before" ? start : end : start;
497
+ if (op !== "insert") unit.delete(start, end - start);
498
+ if (op !== "delete") {
499
+ unit.insert(index, content2, marks2);
500
+ inserted = {
501
+ start: Y2.relativePositionToJSON(Y2.createRelativePositionFromTypeIndex(unit, index, 0)),
502
+ end: Y2.relativePositionToJSON(Y2.createRelativePositionFromTypeIndex(unit, index + content2.length, -1))
503
+ };
504
+ }
505
+ return { inserted: op === "delete" ? 0 : content2.length, deleted: op === "insert" ? 0 : end - start };
506
+ }, extra);
507
+ return { at, inserted };
508
+ }
509
+ editSection(op, target2, markdown, author) {
510
+ const who = checkAuthor(author);
511
+ const { start, end, headed } = this.range(target2.section);
512
+ const body = headed ? start + 1 : start;
513
+ const content2 = op === "delete" ? [] : markdownToBlocks(markdown);
514
+ this.change(op, who, () => {
515
+ const deleted = op === "insert" ? 0 : op === "delete" ? end - start : end - body;
516
+ const from = op === "delete" ? start : op === "replace" || target2.at === "start" ? body : end;
517
+ if (op === "replace") this.fragment.delete(body, end - body);
518
+ if (op === "delete") this.fragment.delete(start, end - start);
519
+ if (content2.length) this.fragment.insert(from, toYBlocks(content2));
520
+ return { inserted: content2.length, deleted, section: target2.section };
521
+ });
522
+ }
523
+ /** Insert Markdown into a section, or plain text next to matched text. */
524
+ insert(target2, content2, author) {
525
+ if (isSection(target2)) this.editSection("insert", target2, content2, author);
526
+ else this.edit("insert", target2, content2, author);
527
+ }
528
+ /** Replace a section body (the heading stays), or the matched text. */
529
+ replace(target2, content2, author) {
530
+ if (isSection(target2)) this.editSection("replace", target2, content2, author);
531
+ else this.edit("replace", target2, content2, author);
532
+ }
533
+ /** Delete a whole section with its subsections, or the matched text. */
534
+ delete(target2, author) {
535
+ if (isSection(target2)) this.editSection("delete", target2, "", author);
536
+ else this.edit("delete", target2, "", author);
537
+ }
538
+ append(markdown, author) {
539
+ const who = checkAuthor(author);
540
+ const content2 = markdownToBlocks(markdown);
541
+ this.change("append", who, () => {
542
+ this.fragment.insert(this.fragment.length, toYBlocks(content2));
543
+ return { inserted: content2.length, deleted: 0 };
544
+ });
545
+ }
546
+ };
547
+
548
+ // src/odt.ts
549
+ import { strToU8, zipSync } from "fflate";
550
+
551
+ // src/package.ts
552
+ var IMAGE_TYPES = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", gif: "image/gif", webp: "image/webp" };
553
+ var DATA_IMAGE = /^data:(image\/(?:png|jpeg|gif|webp));base64,([a-z0-9+/=]+)$/i;
554
+ var FIXED_DATE = new Date(2e3, 0, 1);
555
+ function toBase64(bytes) {
556
+ let text2 = "";
557
+ for (let i = 0; i < bytes.length; i += 32768) text2 += String.fromCharCode(...bytes.subarray(i, i + 32768));
558
+ return btoa(text2);
559
+ }
560
+ var fromBase64 = (value) => Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
561
+ function imageSize(b) {
562
+ const v = new DataView(b.buffer, b.byteOffset, b.byteLength);
563
+ if (b.length > 24 && v.getUint32(0) === 2303741511) return [v.getUint32(16), v.getUint32(20)];
564
+ if (b.length > 10 && v.getUint32(0) === 1195984440) return [v.getUint16(6, true), v.getUint16(8, true)];
565
+ if (b.length > 4 && v.getUint16(0) === 65496) {
566
+ for (let i = 2; i + 9 < b.length && b[i] === 255; i += 2 + v.getUint16(i + 2)) {
567
+ if (b[i + 1] >= 192 && b[i + 1] <= 207 && ![196, 200, 204].includes(b[i + 1])) return [v.getUint16(i + 7), v.getUint16(i + 5)];
568
+ }
569
+ }
570
+ return void 0;
571
+ }
572
+
573
+ // src/xml.ts
574
+ var xmlEscape = (value) => String(value ?? "").replace(/[^\u0009\u000A\u000D -퟿-�\u{10000}-\u{10FFFF}]/gu, "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
575
+ var safeHref = (value) => {
576
+ const href = String(value ?? "").trim();
577
+ return /^(https?:|mailto:|#|[^:]*$)/i.test(href) ? href : void 0;
578
+ };
579
+
580
+ // src/odt.ts
581
+ var ODT_MIME = "application/vnd.oasis.opendocument.text";
582
+ 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"';
583
+ var XML = '<?xml version="1.0" encoding="UTF-8"?>\n';
584
+ var SPANS = { bold: "B", italic: "I", strike: "S", code: "Source_20_Text" };
585
+ var TASK = ["\u2610 ", "\u2611 "];
586
+ var LEVELS = (kind) => Array.from(
587
+ { length: 10 },
588
+ (_, i) => kind === "bullet" ? `<text:list-level-style-bullet text:level="${i + 1}" text:bullet-char="\u2022"><style:list-level-properties text:list-level-position-and-space-mode="label-alignment"><style:list-level-label-alignment text:label-followed-by="listtab" fo:margin-left="${(i + 1) * 0.63}cm" fo:text-indent="-0.63cm"/></style:list-level-properties></text:list-level-style-bullet>` : `<text:list-level-style-number text:level="${i + 1}" style:num-suffix="." style:num-format="1"><style:list-level-properties text:list-level-position-and-space-mode="label-alignment"><style:list-level-label-alignment text:label-followed-by="listtab" fo:margin-left="${(i + 1) * 0.63}cm" fo:text-indent="-0.63cm"/></style:list-level-properties></text:list-level-style-number>`
589
+ ).join("");
590
+ var SIZES = [24, 18, 14, 12, 11, 10];
591
+ var STYLES = `${XML}<office:document-styles ${NS}><office:styles>
592
+ <style:style style:name="Standard" style:family="paragraph" style:class="text"/>
593
+ ${SIZES.map((s, i) => `<style:style style:name="Heading_20_${i + 1}" style:display-name="Heading ${i + 1}" style:family="paragraph" style:parent-style-name="Standard" style:class="text"><style:text-properties fo:font-size="${s}pt" fo:font-weight="bold"/></style:style>`).join("\n")}
594
+ <style:style style:name="Quotations" style:family="paragraph" style:parent-style-name="Standard" style:class="html"><style:paragraph-properties fo:margin-left="1cm" fo:margin-right="1cm"/></style:style>
595
+ <style:style style:name="Preformatted_20_Text" style:display-name="Preformatted Text" style:family="paragraph" style:parent-style-name="Standard" style:class="html"><style:text-properties fo:font-family="Liberation Mono, Courier New, monospace" fo:font-size="10pt"/></style:style>
596
+ <style:style style:name="Horizontal_20_Line" style:display-name="Horizontal Line" style:family="paragraph" style:parent-style-name="Standard" style:class="html"><style:paragraph-properties fo:border-bottom="0.5pt solid #808080" fo:padding="0cm" fo:margin-bottom="0.3cm"/></style:style>
597
+ <style:style style:name="Source_20_Text" style:display-name="Source Text" style:family="text"><style:text-properties fo:font-family="Liberation Mono, Courier New, monospace"/></style:style>
598
+ </office:styles></office:document-styles>`;
599
+ var AUTO = `<style:style style:name="B" style:family="text"><style:text-properties fo:font-weight="bold"/></style:style>
600
+ <style:style style:name="I" style:family="text"><style:text-properties fo:font-style="italic"/></style:style>
601
+ <style:style style:name="S" style:family="text"><style:text-properties style:text-line-through-style="solid"/></style:style>
602
+ <text:list-style style:name="Bullets">${LEVELS("bullet")}</text:list-style>
603
+ <text:list-style style:name="Numbering">${LEVELS("number")}</text:list-style>`;
604
+ var spaced = (s) => xmlEscape(s).replace(/\t/g, "<text:tab/>").replace(/\n/g, "<text:line-break/>").replace(/ +/g, (run, at, all) => at === 0 || at + run.length === all.length ? `<text:s text:c="${run.length}"/>` : run.length > 1 ? ` <text:s text:c="${run.length - 1}"/>` : run);
605
+ function image(node, out) {
606
+ const alt = String(node.attrs?.alt ?? "");
607
+ const found = DATA_IMAGE.exec(String(node.attrs?.src ?? "").trim());
608
+ let data;
609
+ try {
610
+ data = found ? fromBase64(found[2]) : void 0;
611
+ } catch {
612
+ data = void 0;
613
+ }
614
+ if (!found || !data) return spaced(`[image${alt ? `: ${alt}` : ""}]`);
615
+ const ext = Object.keys(IMAGE_TYPES).find((k) => IMAGE_TYPES[k] === found[1].toLowerCase());
616
+ const name = `Image${out.images.length + 1}`;
617
+ out.images.push({ name: `Pictures/${name}.${ext}`, type: found[1].toLowerCase(), data });
618
+ const [w, h] = imageSize(data) ?? [320, 240];
619
+ const scale = Math.min(1, 16 / (w * 2.54 / 96));
620
+ const cm = (px) => (px * 2.54 * scale / 96).toFixed(2);
621
+ return `<draw:frame draw:name="${name}" text:anchor-type="as-char" svg:width="${cm(w)}cm" svg:height="${cm(h)}cm"><draw:image xlink:href="Pictures/${name}.${ext}" xlink:type="simple" xlink:show="embed" xlink:actuate="onLoad"/><svg:title>${xmlEscape(alt)}</svg:title></draw:frame>`;
622
+ }
623
+ function inline2(nodes2, out) {
624
+ return nodes2.map((node) => {
625
+ if (node.type === "hardBreak") return "<text:line-break/>";
626
+ if (node.type === "image") return image(node, out);
627
+ return (node.marks ?? []).reduce((xml, mark) => {
628
+ if (mark.type === "link") {
629
+ const href = safeHref(mark.attrs?.href);
630
+ return href ? `<text:a xlink:type="simple" xlink:href="${xmlEscape(href)}">${xml}</text:a>` : xml;
631
+ }
632
+ return SPANS[mark.type] ? `<text:span text:style-name="${SPANS[mark.type]}">${xml}</text:span>` : xml;
633
+ }, spaced(node.text ?? ""));
634
+ }).join("");
635
+ }
636
+ function block(node, out, style = "Standard") {
637
+ const kids = node.content ?? [];
638
+ const p = (name, inner) => `<text:p text:style-name="${name}">${inner}</text:p>`;
639
+ switch (node.type) {
640
+ case "paragraph":
641
+ return p(style, inline2(kids, out));
642
+ case "heading": {
643
+ const level = Math.min(6, Math.max(1, Math.trunc(Number(node.attrs?.level)) || 1));
644
+ return `<text:h text:style-name="Heading_20_${level}" text:outline-level="${level}">${inline2(kids, out)}</text:h>`;
645
+ }
646
+ case "blockquote":
647
+ return kids.map((k) => block(k, out, "Quotations")).join("");
648
+ case "codeBlock":
649
+ return p("Preformatted_20_Text", spaced(kids.map((k) => k.text ?? "").join("")));
650
+ case "horizontalRule":
651
+ return `<text:p text:style-name="Horizontal_20_Line"/>`;
652
+ case "bulletList":
653
+ case "orderedList":
654
+ case "taskList": {
655
+ const ordered = node.type === "orderedList";
656
+ const start = Number(node.attrs?.start ?? 1);
657
+ const items = kids.map((item, i) => {
658
+ const [first, ...rest] = item.content ?? [];
659
+ const mark = item.type === "taskItem" && first ? { ...first, content: [{ type: "text", text: TASK[item.attrs?.checked ? 1 : 0] }, ...first.content ?? []] } : first;
660
+ const attr = ordered && i === 0 && start !== 1 ? ` text:start-value="${Math.trunc(start) || 1}"` : "";
661
+ return `<text:list-item${attr}>${[mark, ...rest].filter(Boolean).map((k) => block(k, out)).join("")}</text:list-item>`;
662
+ });
663
+ return `<text:list text:style-name="${ordered ? "Numbering" : "Bullets"}">${items.join("")}</text:list>`;
664
+ }
665
+ case "table": {
666
+ const cols = Math.max(0, ...kids.map((r) => r.content?.length ?? 0));
667
+ const row = (r) => `<table:table-row>${(r.content ?? []).map((c) => `<table:table-cell office:value-type="string">${(c.content ?? []).map((b) => block(b, out)).join("")}</table:table-cell>`).join("")}</table:table-row>`;
668
+ const header = kids[0]?.content?.length && kids[0].content.every((c) => c.type === "tableHeader");
669
+ const rows = header ? `<table:table-header-rows>${row(kids[0])}</table:table-header-rows>${kids.slice(1).map(row).join("")}` : kids.map(row).join("");
670
+ return `<table:table table:name="Table${++out.tables}">${cols ? `<table:table-column table:number-columns-repeated="${cols}"/>` : ""}${rows}</table:table>`;
671
+ }
672
+ default:
673
+ return "";
674
+ }
675
+ }
676
+ function exportOdt(blocks2) {
677
+ const out = { images: [], tables: 0 };
678
+ const body = blocks2.map((b) => block(b, out)).join("\n");
679
+ 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>`;
680
+ const entries = [
681
+ `<manifest:file-entry manifest:full-path="/" manifest:version="1.3" manifest:media-type="${ODT_MIME}"/>`,
682
+ ...["content.xml", "styles.xml", "meta.xml"].map((n) => `<manifest:file-entry manifest:full-path="${n}" manifest:media-type="text/xml"/>`),
683
+ ...out.images.map((i) => `<manifest:file-entry manifest:full-path="${i.name}" manifest:media-type="${i.type}"/>`)
684
+ ];
685
+ const meta = `${XML}<office:document-meta ${NS}><office:meta><meta:generator>agentic-documents</meta:generator></office:meta></office:document-meta>`;
686
+ const part = (text2) => [strToU8(text2), { mtime: FIXED_DATE }];
687
+ return zipSync({
688
+ mimetype: [strToU8(ODT_MIME), { level: 0, mtime: FIXED_DATE }],
689
+ // first and stored, so file sniffers can read it
690
+ "META-INF/manifest.xml": part(`${XML}<manifest:manifest ${NS}>${entries.join("")}</manifest:manifest>`),
691
+ "content.xml": part(content2),
692
+ "styles.xml": part(STYLES),
693
+ "meta.xml": part(meta),
694
+ ...Object.fromEntries(out.images.map((i) => [i.name, [i.data, { mtime: FIXED_DATE }]]))
695
+ });
696
+ }
697
+
698
+ // src/export.ts
699
+ var EXPORT_FORMATS = ["md", "html", "odt"];
700
+ var esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
701
+ var url = (value) => /^(https?:|mailto:|[^:]*$)/i.test(String(value ?? "").trim()) ? esc(String(value).trim()) : "#";
702
+ var integer = (value, fallback) => Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : fallback;
703
+ var INLINE_IMAGE = /^data:image\/(png|jpeg|gif|webp);base64,[a-z0-9+/=]+$/i;
704
+ var textOf = (node) => node.text ?? (node.content ?? []).map(textOf).join("");
705
+ var MARK_TAGS = { bold: "strong", italic: "em", strike: "s", code: "code" };
706
+ function image2(node, remote) {
707
+ const src = String(node.attrs?.src ?? "").trim();
708
+ const alt = esc(node.attrs?.alt);
709
+ if (INLINE_IMAGE.test(src)) return `<img src="${src}" alt="${alt}">`;
710
+ if (remote && url(src) !== "#") return `<img src="${url(src)}" alt="${alt}">`;
711
+ return `<span class="image">[image${alt ? `: ${alt}` : ""}]</span>`;
712
+ }
713
+ function inline3(node, remote) {
714
+ if (node.type === "hardBreak") return "<br>";
715
+ if (node.type === "image") return image2(node, remote);
716
+ return (node.marks ?? []).reduce((html, mark) => {
717
+ if (mark.type === "link") return `<a href="${url(mark.attrs?.href)}">${html}</a>`;
718
+ const tag = MARK_TAGS[mark.type];
719
+ return tag ? `<${tag}>${html}</${tag}>` : html;
720
+ }, esc(node.text));
721
+ }
722
+ function block2(node, remote) {
723
+ const kids = (node.content ?? []).map((n) => block2(n, remote)).join("");
724
+ const text2 = (node.content ?? []).map((n) => inline3(n, remote)).join("");
725
+ switch (node.type) {
726
+ case "paragraph":
727
+ return `<p>${text2}</p>
728
+ `;
729
+ case "heading": {
730
+ const level = Math.min(6, Math.max(1, integer(node.attrs?.level, 1)));
731
+ return `<h${level}>${text2}</h${level}>
732
+ `;
733
+ }
734
+ case "blockquote":
735
+ return `<blockquote>
736
+ ${kids}</blockquote>
737
+ `;
738
+ case "codeBlock": {
739
+ const lang = node.attrs?.language ? ` class="language-${esc(node.attrs.language)}"` : "";
740
+ return `<pre><code${lang}>${esc(textOf(node))}</code></pre>
741
+ `;
742
+ }
743
+ case "horizontalRule":
744
+ return "<hr>\n";
745
+ case "bulletList":
746
+ case "taskList":
747
+ return `<ul>
748
+ ${kids}</ul>
749
+ `;
750
+ case "orderedList":
751
+ return `<ol start="${integer(node.attrs?.start, 1)}">
752
+ ${kids}</ol>
753
+ `;
754
+ case "listItem":
755
+ return `<li>${kids}</li>
756
+ `;
757
+ case "taskItem":
758
+ return `<li><input type="checkbox" disabled${node.attrs?.checked ? " checked" : ""}> ${kids}</li>
759
+ `;
760
+ case "table":
761
+ return `<table>
762
+ ${kids}</table>
763
+ `;
764
+ case "tableRow":
765
+ return `<tr>${kids}</tr>
766
+ `;
767
+ case "tableHeader":
768
+ return `<th>${kids}</th>`;
769
+ case "tableCell":
770
+ return `<td>${kids}</td>`;
771
+ default:
772
+ return "";
773
+ }
774
+ }
775
+ function toHtml(blocks2, remote) {
776
+ const title = textOf(blocks2.find((b) => b.type === "heading") ?? { type: "doc" }) || "Document";
777
+ const css = "body{font-family:system-ui,sans-serif;max-width:46rem;margin:2rem auto;padding:0 1rem;line-height:1.5}table{border-collapse:collapse}td,th{border:1px solid #888;padding:.25rem .5rem}pre{overflow:auto}";
778
+ return `<!doctype html>
779
+ <html lang="en">
780
+ <head>
781
+ <meta charset="utf-8">
782
+ <title>${esc(title)}</title>
783
+ <style>${css}</style>
784
+ </head>
785
+ <body>
786
+ ${blocks2.map((b) => block2(b, remote)).join("")}</body>
787
+ </html>
788
+ `;
789
+ }
790
+ function exportDoc(doc, format, options = {}) {
791
+ if (format === "html") {
792
+ return { format, contentType: "text/html; charset=utf-8", encoding: "utf8", data: toHtml(doc.toJSON().content ?? [], options.remoteImages === true) };
793
+ }
794
+ if (format === "odt") return { format, contentType: ODT_MIME, encoding: "base64", data: toBase64(exportOdt(doc.toJSON().content ?? [])) };
795
+ return { format, contentType: "text/markdown; charset=utf-8", encoding: "utf8", data: doc.toMarkdown() };
796
+ }
797
+
798
+ // src/review.ts
799
+ import * as Y3 from "yjs";
800
+ var newId = () => globalThis.crypto.randomUUID();
801
+ function nonEmpty(value, what) {
802
+ if (typeof value !== "string" || value.trim() === "") throw new DocError("INVALID_INPUT", `${what} must not be empty`);
803
+ return value;
804
+ }
805
+ function anchorFor(doc, target2) {
806
+ if ("anchor" in target2) {
807
+ doc.resolve(target2);
808
+ return target2.anchor;
809
+ }
810
+ return doc.anchor(target2.text, target2.occurrence);
811
+ }
812
+ function isStale(doc, anchor) {
813
+ try {
814
+ doc.resolve({ anchor });
815
+ return { stale: false, currentText: anchor.text };
816
+ } catch (error) {
817
+ if (!(error instanceof DocError) || error.code !== "STALE_ANCHOR") throw error;
818
+ return { stale: true, currentText: null };
819
+ }
820
+ }
821
+ function fieldsOf(doc, name) {
822
+ return doc.ydoc.getMap(name);
823
+ }
824
+ var optional = (map, key) => map.has(key) ? { [key]: map.get(key) } : {};
825
+ function setAll(map, values) {
826
+ for (const [key, value] of Object.entries(values)) if (value !== void 0) map.set(key, value);
827
+ }
828
+ var Threads = class {
829
+ constructor(doc) {
830
+ this.doc = doc;
831
+ }
832
+ doc;
833
+ get map() {
834
+ return fieldsOf(this.doc, "threads");
835
+ }
836
+ get(id2) {
837
+ const thread2 = this.map.get(id2);
838
+ if (!thread2) throw new DocError("NO_THREAD", `no thread with id "${id2}"`);
839
+ return thread2;
840
+ }
841
+ message(body, author) {
842
+ return { id: newId(), at: this.doc.now().toISOString(), author, body };
843
+ }
844
+ add(target2, body, author) {
845
+ const who = checkAuthor(author);
846
+ nonEmpty(body, "a comment");
847
+ const anchor = anchorFor(this.doc, target2);
848
+ const id2 = newId();
849
+ this.doc.ydoc.transact(() => {
850
+ const thread2 = new Y3.Map();
851
+ this.map.set(id2, thread2);
852
+ setAll(thread2, { anchor, status: "open" });
853
+ const messages = new Y3.Array();
854
+ thread2.set("messages", messages);
855
+ messages.push([this.message(body, who)]);
856
+ });
857
+ return id2;
858
+ }
859
+ reply(id2, body, author) {
860
+ const who = checkAuthor(author);
861
+ nonEmpty(body, "a reply");
862
+ this.get(id2).get("messages").push([this.message(body, who)]);
863
+ }
864
+ setStatus(id2, status, author) {
865
+ const who = checkAuthor(author);
866
+ const thread2 = this.get(id2);
867
+ this.doc.ydoc.transact(() => {
868
+ thread2.set("status", status);
869
+ if (status === "open") {
870
+ thread2.delete("resolvedBy");
871
+ thread2.delete("resolvedAt");
872
+ } else setAll(thread2, { resolvedBy: who, resolvedAt: this.doc.now().toISOString() });
873
+ });
874
+ }
875
+ resolve(id2, author) {
876
+ this.setStatus(id2, "resolved", author);
877
+ }
878
+ reopen(id2, author) {
879
+ this.setStatus(id2, "open", author);
880
+ }
881
+ list(filter = {}) {
882
+ return [...this.map.entries()].map(([id2, t]) => {
883
+ const anchor = t.get("anchor");
884
+ return {
885
+ id: id2,
886
+ anchor,
887
+ status: t.get("status"),
888
+ ...optional(t, "resolvedBy"),
889
+ ...optional(t, "resolvedAt"),
890
+ messages: t.get("messages").toArray(),
891
+ ...isStale(this.doc, anchor)
892
+ };
893
+ }).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));
894
+ }
895
+ };
896
+ var ACCEPT_SCOPE = "docs:accept";
897
+ var sameWho = (a, b) => a.kind === b.kind && a.name === b.name;
898
+ var isPerson = (who) => who.kind === "human" || who.kind === "user_token";
899
+ var mayDecide = (who) => isPerson(who) || who.scopes?.includes(ACCEPT_SCOPE) === true;
900
+ var Suggestions = class {
901
+ constructor(doc) {
902
+ this.doc = doc;
903
+ }
904
+ doc;
905
+ get map() {
906
+ return fieldsOf(this.doc, "suggestions");
907
+ }
908
+ get(id2) {
909
+ const fields = this.map.get(id2);
910
+ if (!fields) throw new DocError("NO_SUGGESTION", `no suggestion with id "${id2}"`);
911
+ return { fields, suggestion: this.read(id2, fields) };
912
+ }
913
+ read(id2, f) {
914
+ const anchor = f.get("anchor");
915
+ return {
916
+ id: id2,
917
+ kind: f.get("kind"),
918
+ ...optional(f, "at"),
919
+ anchor,
920
+ replacement: f.get("replacement"),
921
+ author: f.get("author"),
922
+ createdAt: f.get("createdAt"),
923
+ status: f.get("status"),
924
+ ...optional(f, "decidedBy"),
925
+ ...optional(f, "decidedAt"),
926
+ stale: isStale(this.doc, anchor).stale
927
+ };
928
+ }
929
+ /** An `at` on the target means insert. An empty replacement means delete. */
930
+ add(target2, replacement, author) {
931
+ const who = checkAuthor(author);
932
+ const kind = target2.at ? "insert" : replacement === "" ? "delete" : "replace";
933
+ if (kind === "insert") nonEmpty(replacement, "an inserted text");
934
+ const anchor = anchorFor(this.doc, target2);
935
+ const id2 = newId();
936
+ this.doc.ydoc.transact(() => {
937
+ const fields = new Y3.Map();
938
+ this.map.set(id2, fields);
939
+ setAll(fields, { kind, at: target2.at, anchor, replacement, author: who, createdAt: this.doc.now().toISOString(), status: "pending" });
940
+ });
941
+ return id2;
942
+ }
943
+ decide(fields, status, author, at) {
944
+ setAll(fields, { status, decidedBy: author, decidedAt: at });
945
+ }
946
+ /**
947
+ * Apply the edit as the accepting author. The check and the edit share one transaction. Accepting
948
+ * an accepted suggestion changes nothing. A stale anchor throws and the suggestion stays pending.
949
+ */
950
+ accept(id2, author) {
951
+ const who = checkAuthor(author);
952
+ this.doc.ydoc.transact(() => {
953
+ const { fields, suggestion: s } = this.get(id2);
954
+ if (!mayDecide(author) || sameWho(s.author, who)) {
955
+ 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`);
956
+ }
957
+ if (s.status === "accepted") return;
958
+ if (s.status !== "pending") throw new DocError("ALREADY_DECIDED", `suggestion "${id2}" is already ${s.status}`);
959
+ const { at, inserted } = this.doc.edit(s.kind, { anchor: s.anchor, at: s.at }, s.replacement, who, { suggestion: { id: id2, author: s.author } });
960
+ const record = { by: who, at, ...inserted };
961
+ this.doc.ydoc.getMap(APPLIED).set(`${id2}:${this.doc.ydoc.clientID}`, record);
962
+ this.decide(fields, "accepted", who, at);
963
+ });
964
+ }
965
+ /** Same right as accept. A person can withdraw their own suggestion. An agent can never decide its own. */
966
+ reject(id2, author) {
967
+ const who = checkAuthor(author);
968
+ this.doc.ydoc.transact(() => {
969
+ const { fields, suggestion: s } = this.get(id2);
970
+ if (!mayDecide(author) || !isPerson(who) && sameWho(s.author, who)) {
971
+ 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`);
972
+ }
973
+ if (s.status !== "pending") throw new DocError("ALREADY_DECIDED", `suggestion "${id2}" is already ${s.status}`);
974
+ this.decide(fields, "rejected", who, this.doc.now().toISOString());
975
+ });
976
+ }
977
+ list(filter = {}) {
978
+ return [...this.map.entries()].map(([id2, f]) => this.read(id2, f)).filter((s) => filter.status === void 0 || s.status === filter.status).sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
979
+ }
980
+ };
981
+
982
+ // src/commands.ts
983
+ import { z } from "zod";
984
+ var id = z.string().describe("Document id.");
985
+ var content = z.string().describe("Markdown for a section target. Plain text for a text target.");
986
+ var text = z.string().describe("Exact text to match. It must sit inside one block.");
987
+ var occurrence = z.coerce.number().int().min(1).optional().describe("Which match, counting from 1, when the text matches more than once.");
988
+ var side = z.enum(["before", "after"]).optional().describe("Put the new text before or after the match.");
989
+ var thread = z.string().describe("Thread id.");
990
+ var where = {
991
+ section: z.string().optional().describe("Section id from the outline. Use this or text."),
992
+ text: text.optional(),
993
+ occurrence,
994
+ at: z.enum(["start", "end", "before", "after"]).optional().describe("Insert at the start or end of a section, or before or after matched text.")
995
+ };
996
+ function target(input) {
997
+ const { section, text: text2, occurrence: occurrence2, at } = input;
998
+ if (section !== void 0) {
999
+ if (at !== void 0 && at !== "start" && at !== "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit a section; use start or end`);
1000
+ return { section, at };
1001
+ }
1002
+ if (text2 !== void 0) {
1003
+ if (at === "start" || at === "end") throw new DocError("INVALID_INPUT", `at "${at}" does not fit text; use before or after`);
1004
+ return { text: text2, occurrence: occurrence2, at };
1005
+ }
1006
+ throw new DocError("INVALID_INPUT", "pass section or text");
1007
+ }
1008
+ var textTarget = (input) => ({
1009
+ text: input.text,
1010
+ occurrence: input.occurrence,
1011
+ at: input.at
1012
+ });
1013
+ function command(name, description, readOnly, shape, run) {
1014
+ return {
1015
+ name,
1016
+ description,
1017
+ shape,
1018
+ readOnly,
1019
+ async run(service, raw, author) {
1020
+ const parsed = z.object(shape).strict().safeParse(raw);
1021
+ if (!parsed.success) {
1022
+ throw new DocError("INVALID_INPUT", parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; "));
1023
+ }
1024
+ return run(service, parsed.data, author);
1025
+ }
1026
+ };
1027
+ }
1028
+ var ok = () => ({ ok: true });
1029
+ var commands = [
1030
+ command("new", "Create a document, from Markdown if given.", false, { id, markdown: z.string().optional() }, async (s, i, a) => {
1031
+ await s.create(i.id, i.markdown ?? "", a);
1032
+ return { id: i.id };
1033
+ }),
1034
+ command(
1035
+ "read",
1036
+ "Read a document, or one section, as Markdown.",
1037
+ true,
1038
+ { id, section: where.section },
1039
+ (s, i) => s.read(i.id, (d) => i.section ? d.readSection(i.section) : d.toMarkdown())
1040
+ ),
1041
+ command("outline", "List sections with their ids, levels, and titles.", true, { id }, (s, i) => s.read(i.id, (d) => d.outline())),
1042
+ 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())),
1043
+ command("insert", "Insert Markdown into a section, or plain text next to matched text.", false, { id, ...where, content }, async (s, i, a) => {
1044
+ await s.write(i.id, (d) => d.insert(target(i), i.content, a));
1045
+ return ok();
1046
+ }),
1047
+ command("replace", "Replace a section body (the heading stays), or the matched text.", false, { id, ...where, content }, async (s, i, a) => {
1048
+ await s.write(i.id, (d) => d.replace(target(i), i.content, a));
1049
+ return ok();
1050
+ }),
1051
+ command("delete", "Delete a section with its subsections, or the matched text.", false, { id, ...where }, async (s, i, a) => {
1052
+ await s.write(i.id, (d) => d.delete(target(i), a));
1053
+ return ok();
1054
+ }),
1055
+ command("append", "Add Markdown at the end of the document.", false, { id, content: z.string() }, async (s, i, a) => {
1056
+ await s.write(i.id, (d) => d.append(i.content, a));
1057
+ return ok();
1058
+ }),
1059
+ 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) => ({
1060
+ suggestion: await s.write(i.id, (d) => new Suggestions(d).add(textTarget(i), i.replacement, a))
1061
+ })),
1062
+ command(
1063
+ "suggestions",
1064
+ "List suggested edits.",
1065
+ true,
1066
+ { id, status: z.enum(["pending", "accepted", "rejected"]).optional() },
1067
+ (s, i) => s.read(i.id, (d) => new Suggestions(d).list({ status: i.status }))
1068
+ ),
1069
+ command("accept", "Accept a suggestion. The text changes now.", false, { id, suggestion: z.string() }, async (s, i, a) => {
1070
+ await s.write(i.id, (d) => new Suggestions(d).accept(i.suggestion, a));
1071
+ return ok();
1072
+ }),
1073
+ command("reject", "Reject a suggestion. The text stays.", false, { id, suggestion: z.string() }, async (s, i, a) => {
1074
+ await s.write(i.id, (d) => new Suggestions(d).reject(i.suggestion, a));
1075
+ return ok();
1076
+ }),
1077
+ command("comment", "Start a comment thread on matched text.", false, { id, text, occurrence, body: z.string() }, async (s, i, a) => ({
1078
+ thread: await s.write(i.id, (d) => new Threads(d).add(textTarget(i), i.body, a))
1079
+ })),
1080
+ command("reply", "Reply in a comment thread.", false, { id, thread, body: z.string() }, async (s, i, a) => {
1081
+ await s.write(i.id, (d) => new Threads(d).reply(i.thread, i.body, a));
1082
+ return ok();
1083
+ }),
1084
+ command("resolve", "Resolve a comment thread.", false, { id, thread }, async (s, i, a) => {
1085
+ await s.write(i.id, (d) => new Threads(d).resolve(i.thread, a));
1086
+ return ok();
1087
+ }),
1088
+ command("reopen", "Reopen a comment thread.", false, { id, thread }, async (s, i, a) => {
1089
+ await s.write(i.id, (d) => new Threads(d).reopen(i.thread, a));
1090
+ return ok();
1091
+ }),
1092
+ command(
1093
+ "threads",
1094
+ "List comment threads with their messages.",
1095
+ true,
1096
+ { id, status: z.enum(["open", "resolved"]).optional() },
1097
+ (s, i) => s.read(i.id, (d) => new Threads(d).list({ status: i.status }))
1098
+ ),
1099
+ command(
1100
+ "export",
1101
+ "Export the document as Markdown (md), self-contained HTML (html), or OpenDocument text (odt, base64). HTML shows no remote image unless remoteImages is true.",
1102
+ true,
1103
+ { id, format: z.enum(EXPORT_FORMATS), remoteImages: z.union([z.boolean(), z.enum(["true", "false"])]).optional() },
1104
+ (s, i) => s.read(i.id, (d) => exportDoc(d, i.format, { remoteImages: i.remoteImages === true || i.remoteImages === "true" }))
1105
+ )
1106
+ ];
1107
+
1108
+ export {
1109
+ DocError,
1110
+ markdownToBlocks,
1111
+ blocksToMarkdown,
1112
+ schema,
1113
+ PRINCIPAL_KINDS,
1114
+ parseAuthor,
1115
+ AgenticDoc,
1116
+ ODT_MIME,
1117
+ exportOdt,
1118
+ EXPORT_FORMATS,
1119
+ exportDoc,
1120
+ Threads,
1121
+ Suggestions,
1122
+ commands
1123
+ };