agentic-documents 0.5.0 → 0.6.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 (35) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +51 -8
  3. package/dist/{chunk-7R7ZPPRF.js → chunk-34C47PDC.js} +1 -1
  4. package/dist/{chunk-UPEGODUV.js → chunk-5IQX2W52.js} +1 -1
  5. package/dist/{chunk-S6MQDWFK.js → chunk-6UYJV2EJ.js} +19 -6
  6. package/dist/{chunk-E7B6T5WN.js → chunk-CN6UAW7B.js} +5 -3
  7. package/dist/chunk-DWVJIKVT.js +8 -0
  8. package/dist/{chunk-6QOYS63L.js → chunk-GQ5E6T7Z.js} +4 -2
  9. package/dist/{chunk-YNHZCIGO.js → chunk-JBAQEIOH.js} +5 -3
  10. package/dist/chunk-KC5KFB5R.js +354 -0
  11. package/dist/{chunk-FX755I7V.js → chunk-V6TBTOHW.js} +11 -8
  12. package/dist/cli.js +16 -14
  13. package/dist/client.d.ts +5 -5
  14. package/dist/{doc-mJPajXEQ.d.ts → doc-BXG9qG5X.d.ts} +4 -2
  15. package/dist/editor.css +37 -0
  16. package/dist/embed.d.ts +86 -0
  17. package/dist/embed.js +431 -0
  18. package/dist/formats.d.ts +6 -3
  19. package/dist/formats.js +14 -224
  20. package/dist/index.d.ts +12 -11
  21. package/dist/index.js +14 -12
  22. package/dist/live-worker.d.ts +2 -2
  23. package/dist/live-worker.js +7 -6
  24. package/dist/live.d.ts +2 -2
  25. package/dist/live.js +5 -4
  26. package/dist/{mcp-ESSEG46C.js → mcp-ZTUTC4CS.js} +5 -4
  27. package/dist/{export-Cwx276UP.d.ts → render-463xHb-x.d.ts} +3 -4
  28. package/dist/{review-Hgq8OgvD.d.ts → review-Ds-i5Vnc.d.ts} +2 -2
  29. package/dist/{service-CqKE9DKz.d.ts → service-Ck6oX-Kq.d.ts} +2 -2
  30. package/dist/ui/app.css +1 -1
  31. package/dist/ui/app.js +65 -65
  32. package/dist/worker.d.ts +2 -2
  33. package/dist/worker.js +7 -6
  34. package/package.json +28 -11
  35. package/dist/chunk-L6W2AQUF.js +0 -258
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Added
6
+
7
+ - `agentic-documents/ui`: `mountEditor(element, { id, client, principal, room? })` and the `DocumentEditor` React component
8
+ render the editor in a host app, with live co-editing through agentic-realtime when a room is given. Its styles are
9
+ in `agentic-documents/ui.css`. `react` and `react-dom` are optional peer dependencies; the Tiptap packages and
10
+ `agentic-design` are now dependencies.
11
+ - The edit log has a new op, `live`. The editor writes one `live` entry when an agent principal types, so its text is
12
+ marked as agent work. Its counts are 0.
13
+
14
+ ### Changed
15
+
16
+ - The ODT export uses `agentic-formats`. The local ODT writer and the `fflate` dependency are gone. `exportOdt` and
17
+ `ODT_MIME` stay as thin wrappers. The ODT file now comes from the neutral text model, so its XML differs from before.
18
+ - The ODT export holds less than before, because the agentic-formats text model has no block for a quote, a rule, or a
19
+ code block, no list start number, one paragraph for each list item, and no WebP image. The text stays.
20
+ `ExportResult` has a `warnings` field that names each loss. It is empty for `md` and `html`. The field is required, so
21
+ code that builds an `ExportResult` by hand must add it. The CLI prints each warning on stderr. The HTTP API sends them
22
+ in the `x-export-warnings` header.
23
+
3
24
  ## 0.5.0
4
25
 
5
26
  ### Breaking
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
8
  Write documents with people and agents in real time.
9
9
 
10
- > Status: early, version 0.5.0. The document model, the text operations, the CLI, the MCP server,
10
+ > Status: early, version 0.6.0. The document model, the text operations, the CLI, the MCP server,
11
11
  > the Worker API, live editing in a room, and a small web UI work. See [UI](#ui).
12
12
 
13
13
  ## Install
@@ -195,7 +195,7 @@ try {
195
195
  }
196
196
  ```
197
197
 
198
- - One method per route: `create`, `read`, `outline`, `edits`, `insert`, `replace`, `delete`, `append`,
198
+ - One method per route: `create`, `list`, `read`, `rename`, `remove`, `outline`, `edits`, `insert`, `replace`, `delete`, `append`,
199
199
  `suggest`, `suggestions`, `accept`, `reject`, `comment`, `reply`, `resolve`, `reopen`, `threads`,
200
200
  `export`, `room`, `me`, `health`, `state`, and `checkpoint`. The WebSocket route is not a method. Use
201
201
  `LiveDoc` from `agentic-documents/live` for it.
@@ -205,6 +205,47 @@ try {
205
205
  - The client never retries. A write is not safe to send twice.
206
206
  - Pass `fetch` to use your own, for a Worker service binding or for tests.
207
207
 
208
+ ## Embed the editor
209
+
210
+ `agentic-documents/ui` renders the editor inside your own app. You give it a document id, a client, the principal who
211
+ uses it, and an optional live room. With a room, people and agents edit together through agentic-realtime and see each
212
+ other. It needs `react` and `react-dom` 19 (peer dependencies).
213
+
214
+ ```ts
215
+ import { createClient } from "agentic-documents/client";
216
+ import { mountEditor } from "agentic-documents/ui";
217
+ import "agentic-design/styles.css";
218
+ import "agentic-documents/ui.css";
219
+
220
+ const client = createClient({ baseUrl: "https://docs.example.com", token });
221
+ const principal = await client.me(); // { kind, name, role }
222
+ const editor = mountEditor(document.getElementById("editor")!, {
223
+ id: "plan",
224
+ client,
225
+ principal,
226
+ room: { url: "wss://docs.example.com/api/v1/docs/plan/ws", token },
227
+ });
228
+ // Later: editor.update({ ...options, id: "other" }), or await editor.unmount() to save this tab's changes and leave.
229
+ ```
230
+
231
+ - In a React app, render `<DocumentEditor id client principal room onError onLeave />` instead. `onLeave` gets the
232
+ promise of the last save.
233
+ - `client` needs only `state` and `checkpoint`. A client from `createClient` has them, or pass your own. Keep the
234
+ same client object between renders: a new one opens the document again.
235
+ - `principal` needs a `kind` (one of the principal kinds) and a `name`. A missing or unknown kind throws a
236
+ `TypeError`. It is never given a default. Only the `role` `editor` can change the text. Any other role, or no role,
237
+ opens it read only. The server still checks every write against the token. Use the principal that `client.me()`
238
+ returns for the same token, so the editor labels the text with the kind that the server gives the token.
239
+ - When an agent principal types, the editor writes one `live` entry to the edit log for its Yjs client. The editor
240
+ and the web UI then mark that text as agent work for everyone who opens the document.
241
+ - `room.url` is the WebSocket address of the document room. In a browser the token goes in a WebSocket subprotocol.
242
+ Pass `room.WebSocket` for a room that takes its token another way. Without a room, the editor saves through the
243
+ client and shows no other people.
244
+ - The editor saves 1.5 seconds after this tab changes the text, and again on `unmount`. The promise from `unmount`
245
+ rejects when that last save fails. A save never makes a document, so a removed document stays removed.
246
+ - The styles sit under the `.adoc` class and use the agentic-design tokens. Load `agentic-design/styles.css` first.
247
+ - File import and export are in `agentic-documents/formats` (see [File formats](#file-formats)).
248
+
208
249
  ## File formats
209
250
 
210
251
  `agentic-documents/formats` imports and exports files, so an app that embeds the editor needs no mapping
@@ -215,7 +256,7 @@ code of its own. ODT and DOCX go through the neutral text model of
215
256
  import { exportFile, importFile } from "agentic-documents/formats";
216
257
 
217
258
  const { doc, title, warnings } = importFile(bytes, "docx", { kind: "human", name: "Ada" });
218
- const odt = exportFile(doc, "odt", { title }); // { contentType, data: Uint8Array, warnings }
259
+ const odt = exportFile(doc, "odt", { title }); // { contentType, data (Uint8Array for odt), warnings }
219
260
  ```
220
261
 
221
262
  - `importFile(input, format, author, { limits? })` reads `md` (text or UTF-8 bytes), `odt`, or `docx` (bytes) into a
@@ -250,10 +291,10 @@ const odt = exportFile(doc, "odt", { title }); // { contentType, data: Uint8Arra
250
291
  one application and undoes the other, so the text and the edit log hold it once. An accept
251
292
  beats a reject made at the same moment.
252
293
  - Export to ODT (OpenDocument text): headings, paragraphs, bold, italic, strike, code, links, lists,
253
- quotes, rules, tables, and images as embedded files. The package follows ODF 1.3 with the mimetype
254
- stored first. The CLI and the API send the file bytes. LibreOffice opens it, lists keep their
255
- numbers, and its own re-save reads back to the same Markdown (checked by hand with LibreOffice 24.8
256
- in the PR that added it).
294
+ tables, and PNG, JPEG, and GIF images as embedded files. `agentic-formats` writes the file, with the
295
+ mimetype stored first. The CLI and the API send the file bytes. A quote, a code block, a rule, a list
296
+ start number, and a WebP image are not kept in full (see Limits). Each loss is in `warnings` (SDK and
297
+ MCP), on stderr (CLI), and in the `x-export-warnings` header (API).
257
298
  - Export to Markdown and to self-contained HTML. The HTML loads nothing from other hosts. An image
258
299
  stored in the document as a `data:` URI (PNG, JPEG, GIF, or WebP) shows. Any other image becomes
259
300
  its alt text, because a remote image tells its host who opened the file. `remoteImages` turns
@@ -359,7 +400,9 @@ Limits of live editing:
359
400
  of code text, a horizontal rule is left out, a numbered list starts at 1, and a task list becomes a list that starts
360
401
  each item with `☐` or `☑`. On import, underline, superscript, and subscript are left out, and a footnote becomes
361
402
  text in brackets, and code text keeps only its code mark. Each one is named in `warnings`.
362
- - ODT export: comments and suggestions are not written. A code block loses its language. A table
403
+ - ODT export: a quote becomes plain paragraphs, a code block becomes a paragraph of code text, a rule is
404
+ left out, a numbered list starts at 1, a list item keeps one paragraph, and a WebP image becomes its alt text.
405
+ Comments and suggestions are not written. A code block loses its language. A table
363
406
  cell span is not written (the model has none yet). A task list becomes a bullet list that starts each
364
407
  item with `☐` or `☑`. A remote image is not linked, because an office program would ask its host
365
408
  for it when the file opens. It becomes its alt text.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  DocError
3
- } from "./chunk-6QOYS63L.js";
3
+ } from "./chunk-GQ5E6T7Z.js";
4
4
 
5
5
  // src/adapters/file-blobs.ts
6
6
  import { createHash, randomUUID } from "crypto";
@@ -2,7 +2,7 @@ import {
2
2
  APPLIED,
3
3
  DocError,
4
4
  checkAuthor
5
- } from "./chunk-6QOYS63L.js";
5
+ } from "./chunk-GQ5E6T7Z.js";
6
6
 
7
7
  // src/review.ts
8
8
  import * as Y from "yjs";
@@ -1,14 +1,26 @@
1
- import {
2
- EXPORT_FORMATS,
3
- exportDoc
4
- } from "./chunk-L6W2AQUF.js";
5
1
  import {
6
2
  Suggestions,
7
3
  Threads
8
- } from "./chunk-UPEGODUV.js";
4
+ } from "./chunk-5IQX2W52.js";
5
+ import {
6
+ EXPORT_FORMATS,
7
+ exportFile,
8
+ exportText,
9
+ toBase64
10
+ } from "./chunk-KC5KFB5R.js";
9
11
  import {
10
12
  DocError
11
- } from "./chunk-6QOYS63L.js";
13
+ } from "./chunk-GQ5E6T7Z.js";
14
+
15
+ // src/export.ts
16
+ function exportDoc(doc, format, options = {}) {
17
+ if (format === "odt") {
18
+ const file = exportFile(doc, "odt");
19
+ return { format, contentType: file.contentType, encoding: "base64", data: toBase64(file.data), warnings: file.warnings };
20
+ }
21
+ const contentType = format === "html" ? "text/html; charset=utf-8" : "text/markdown; charset=utf-8";
22
+ return { format, contentType, encoding: "utf8", data: exportText(doc, format, options), warnings: [] };
23
+ }
12
24
 
13
25
  // src/commands.ts
14
26
  import { z } from "zod";
@@ -146,5 +158,6 @@ var commands = [
146
158
  ];
147
159
 
148
160
  export {
161
+ exportDoc,
149
162
  commands
150
163
  };
@@ -1,12 +1,14 @@
1
1
  import {
2
2
  MAX_LIVE_BYTES
3
- } from "./chunk-YNHZCIGO.js";
3
+ } from "./chunk-JBAQEIOH.js";
4
4
  import {
5
5
  AgenticDoc,
6
6
  DOC_ERROR_CODES,
7
- DocError,
7
+ DocError
8
+ } from "./chunk-GQ5E6T7Z.js";
9
+ import {
8
10
  PRINCIPAL_KINDS
9
- } from "./chunk-6QOYS63L.js";
11
+ } from "./chunk-DWVJIKVT.js";
10
12
 
11
13
  // src/live.ts
12
14
  import * as Y from "yjs";
@@ -0,0 +1,8 @@
1
+ // src/kinds.ts
2
+ var PRINCIPAL_KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
3
+ var isMachine = (kind) => kind !== "human";
4
+
5
+ export {
6
+ PRINCIPAL_KINDS,
7
+ isMachine
8
+ };
@@ -1,3 +1,7 @@
1
+ import {
2
+ PRINCIPAL_KINDS
3
+ } from "./chunk-DWVJIKVT.js";
4
+
1
5
  // src/errors.ts
2
6
  var DOC_ERROR_CODES = ["NO_MATCH", "AMBIGUOUS_MATCH", "STALE_ANCHOR", "NO_SECTION", "INVALID_AUTHOR", "INVALID_INPUT", "NO_THREAD", "NO_SUGGESTION", "ALREADY_DECIDED", "FORBIDDEN", "NO_DOCUMENT", "DOCUMENT_EXISTS", "LOCKED", "CONFLICT", "MISCONFIGURED"];
3
7
  var DocError = class extends Error {
@@ -314,7 +318,6 @@ function settle(ydoc) {
314
318
  var PREAMBLE = "preamble";
315
319
  var FRAGMENT = "default";
316
320
  var CONTEXT_CHARS = 20;
317
- var PRINCIPAL_KINDS = ["human", "agent_of_human", "agent_of_agent", "agent", "user_token"];
318
321
  var isSection = (t) => "section" in t;
319
322
  var isAnchor = (t) => "anchor" in t;
320
323
  function checkAuthor(author) {
@@ -562,7 +565,6 @@ export {
562
565
  blocksToMarkdown,
563
566
  schema,
564
567
  APPLIED,
565
- PRINCIPAL_KINDS,
566
568
  checkAuthor,
567
569
  parseAuthor,
568
570
  AgenticDoc
@@ -2,12 +2,14 @@ import {
2
2
  Suggestions,
3
3
  Threads,
4
4
  isPerson
5
- } from "./chunk-UPEGODUV.js";
5
+ } from "./chunk-5IQX2W52.js";
6
6
  import {
7
7
  AgenticDoc,
8
- DocError,
8
+ DocError
9
+ } from "./chunk-GQ5E6T7Z.js";
10
+ import {
9
11
  PRINCIPAL_KINDS
10
- } from "./chunk-6QOYS63L.js";
12
+ } from "./chunk-DWVJIKVT.js";
11
13
 
12
14
  // src/service.ts
13
15
  import * as Y from "yjs";
@@ -0,0 +1,354 @@
1
+ import {
2
+ AgenticDoc,
3
+ DocError,
4
+ checkAuthor
5
+ } from "./chunk-GQ5E6T7Z.js";
6
+
7
+ // src/formats.ts
8
+ import { DEFAULT_LIMITS, FormatError, readDocx, readOdt, writeDocx, writeOdt } from "agentic-formats";
9
+
10
+ // src/render.ts
11
+ var EXPORT_FORMATS = ["md", "html", "odt"];
12
+ var esc = (s) => String(s ?? "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13
+ var url = (value) => /^(https?:|mailto:|[^:]*$)/i.test(String(value ?? "").trim()) ? esc(String(value).trim()) : "#";
14
+ var integer = (value, fallback) => Number.isFinite(Number(value)) ? Math.trunc(Number(value)) : fallback;
15
+ var INLINE_IMAGE = /^data:image\/(png|jpeg|gif|webp);base64,[a-z0-9+/=]+$/i;
16
+ var textOf = (node) => node.text ?? (node.content ?? []).map(textOf).join("");
17
+ var MARK_TAGS = { bold: "strong", italic: "em", strike: "s", code: "code" };
18
+ function image(node, remote) {
19
+ const src = String(node.attrs?.src ?? "").trim();
20
+ const alt = esc(node.attrs?.alt);
21
+ if (INLINE_IMAGE.test(src)) return `<img src="${src}" alt="${alt}">`;
22
+ if (remote && url(src) !== "#") return `<img src="${url(src)}" alt="${alt}">`;
23
+ return `<span class="image">[image${alt ? `: ${alt}` : ""}]</span>`;
24
+ }
25
+ function inline(node, remote) {
26
+ if (node.type === "hardBreak") return "<br>";
27
+ if (node.type === "image") return image(node, remote);
28
+ return (node.marks ?? []).reduce((html, mark) => {
29
+ if (mark.type === "link") return `<a href="${url(mark.attrs?.href)}">${html}</a>`;
30
+ const tag = MARK_TAGS[mark.type];
31
+ return tag ? `<${tag}>${html}</${tag}>` : html;
32
+ }, esc(node.text));
33
+ }
34
+ function block(node, remote) {
35
+ const kids = (node.content ?? []).map((n) => block(n, remote)).join("");
36
+ const text = (node.content ?? []).map((n) => inline(n, remote)).join("");
37
+ switch (node.type) {
38
+ case "paragraph":
39
+ return `<p>${text}</p>
40
+ `;
41
+ case "heading": {
42
+ const level = Math.min(6, Math.max(1, integer(node.attrs?.level, 1)));
43
+ return `<h${level}>${text}</h${level}>
44
+ `;
45
+ }
46
+ case "blockquote":
47
+ return `<blockquote>
48
+ ${kids}</blockquote>
49
+ `;
50
+ case "codeBlock": {
51
+ const lang = node.attrs?.language ? ` class="language-${esc(node.attrs.language)}"` : "";
52
+ return `<pre><code${lang}>${esc(textOf(node))}</code></pre>
53
+ `;
54
+ }
55
+ case "horizontalRule":
56
+ return "<hr>\n";
57
+ case "bulletList":
58
+ case "taskList":
59
+ return `<ul>
60
+ ${kids}</ul>
61
+ `;
62
+ case "orderedList":
63
+ return `<ol start="${integer(node.attrs?.start, 1)}">
64
+ ${kids}</ol>
65
+ `;
66
+ case "listItem":
67
+ return `<li>${kids}</li>
68
+ `;
69
+ case "taskItem":
70
+ return `<li><input type="checkbox" disabled${node.attrs?.checked ? " checked" : ""}> ${kids}</li>
71
+ `;
72
+ case "table":
73
+ return `<table>
74
+ ${kids}</table>
75
+ `;
76
+ case "tableRow":
77
+ return `<tr>${kids}</tr>
78
+ `;
79
+ case "tableHeader":
80
+ return `<th>${kids}</th>`;
81
+ case "tableCell":
82
+ return `<td>${kids}</td>`;
83
+ default:
84
+ return "";
85
+ }
86
+ }
87
+ function toHtml(blocks, remote) {
88
+ const title = textOf(blocks.find((b) => b.type === "heading") ?? { type: "doc" }) || "Document";
89
+ 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}";
90
+ return `<!doctype html>
91
+ <html lang="en">
92
+ <head>
93
+ <meta charset="utf-8">
94
+ <title>${esc(title)}</title>
95
+ <style>${css}</style>
96
+ </head>
97
+ <body>
98
+ ${blocks.map((b) => block(b, remote)).join("")}</body>
99
+ </html>
100
+ `;
101
+ }
102
+ function exportText(doc, format, options = {}) {
103
+ return format === "html" ? toHtml(doc.toJSON().content ?? [], options.remoteImages === true) : doc.toMarkdown();
104
+ }
105
+
106
+ // src/package.ts
107
+ var DATA_IMAGE = /^data:(image\/(?:png|jpeg|gif|webp));base64,([a-z0-9+/=]+)$/i;
108
+ function toBase64(bytes) {
109
+ let text = "";
110
+ for (let i = 0; i < bytes.length; i += 32768) text += String.fromCharCode(...bytes.subarray(i, i + 32768));
111
+ return btoa(text);
112
+ }
113
+ var fromBase64 = (value) => Uint8Array.from(atob(value), (c) => c.charCodeAt(0));
114
+
115
+ // src/xml.ts
116
+ var safeHref = (value) => {
117
+ const href = String(value ?? "").trim();
118
+ return /^(https?:|mailto:|#|[^:]*$)/i.test(href) ? href : void 0;
119
+ };
120
+
121
+ // src/formats.ts
122
+ var IMPORT_FORMATS = ["md", "odt", "docx"];
123
+ var FILE_FORMATS = ["md", "html", "odt", "docx"];
124
+ var DOCX_MIME = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
125
+ var CONTENT_TYPES = {
126
+ md: "text/markdown; charset=utf-8",
127
+ html: "text/html; charset=utf-8",
128
+ odt: "application/vnd.oasis.opendocument.text",
129
+ docx: DOCX_MIME
130
+ };
131
+ var PM_MARKS = { bold: "bold", italic: "italic", strike: "strike", code: "code" };
132
+ var MODEL_IMAGE = /^image\/(png|jpeg|gif)$/;
133
+ var TASK = ["\u2610 ", "\u2611 "];
134
+ var warner = () => {
135
+ const seen = /* @__PURE__ */ new Set();
136
+ return { warn: (line) => void seen.add(line), list: () => [...seen] };
137
+ };
138
+ function toTextDoc(blocks, title) {
139
+ const { warn, list } = warner();
140
+ const inlines = (nodes = [], extra = []) => nodes.flatMap((node) => {
141
+ if (node.type === "hardBreak") return [{ type: "break" }];
142
+ if (node.type === "image") {
143
+ const alt = typeof node.attrs?.alt === "string" ? node.attrs.alt : "";
144
+ const found = DATA_IMAGE.exec(String(node.attrs?.src ?? "").trim());
145
+ if (found && MODEL_IMAGE.test(found[1].toLowerCase())) {
146
+ try {
147
+ return [{ type: "image", data: fromBase64(found[2]), mime: found[1].toLowerCase(), ...alt ? { alt } : {} }];
148
+ } catch {
149
+ }
150
+ }
151
+ warn("an image that is not an embedded PNG, JPEG, or GIF is kept as its alt text");
152
+ return [{ type: "text", text: `[image${alt ? `: ${alt}` : ""}]` }];
153
+ }
154
+ if (!node.text) return [];
155
+ const marks = [...extra, ...(node.marks ?? []).flatMap((m) => PM_MARKS[m.type] ? [PM_MARKS[m.type]] : [])];
156
+ const href = safeHref((node.marks ?? []).find((m) => m.type === "link")?.attrs?.href);
157
+ return [{ type: "text", text: node.text, ...marks.length ? { marks } : {}, ...href ? { href } : {} }];
158
+ });
159
+ const listOf = (node) => {
160
+ const items = (node.content ?? []).map((item) => {
161
+ const [first, ...rest] = item.content ?? [];
162
+ const content = inlines(first?.content);
163
+ if (item.type === "taskItem") content.unshift({ type: "text", text: TASK[item.attrs?.checked ? 1 : 0] });
164
+ let sublist;
165
+ for (const child of rest) {
166
+ if (!sublist && /^(bulletList|orderedList|taskList)$/.test(child.type)) sublist = listOf(child);
167
+ else {
168
+ warn("a list item with more than one paragraph is kept as one paragraph with line breaks");
169
+ for (const b of toBlocks(child)) content.push({ type: "break" }, ...textOfBlock(b));
170
+ }
171
+ }
172
+ return { content, ...sublist ? { sublist } : {} };
173
+ });
174
+ if (node.type === "orderedList" && Number(node.attrs?.start ?? 1) !== 1) warn("a numbered list that does not start at 1 starts at 1");
175
+ return { type: "list", ordered: node.type === "orderedList", items };
176
+ };
177
+ const textOfBlock = (b) => b.type === "list" ? b.items.flatMap((i, n) => [...n ? [{ type: "break" }] : [], ...i.content]) : b.type === "table" ? b.rows.flat(2).flatMap(textOfBlock) : b.content;
178
+ const toBlocks = (node) => {
179
+ switch (node.type) {
180
+ case "paragraph":
181
+ return [{ type: "paragraph", content: inlines(node.content) }];
182
+ case "heading": {
183
+ const level = Math.min(6, Math.max(1, Math.trunc(Number(node.attrs?.level)) || 1));
184
+ return [{ type: "heading", level, content: inlines(node.content) }];
185
+ }
186
+ case "blockquote":
187
+ warn("a quote is kept as plain paragraphs");
188
+ return (node.content ?? []).flatMap(toBlocks);
189
+ case "codeBlock": {
190
+ warn("a code block is kept as a paragraph of code text");
191
+ const lines = (node.content ?? []).map((t) => t.text ?? "").join("").split("\n");
192
+ return [{ type: "paragraph", content: lines.flatMap((line, i) => [...i ? [{ type: "break" }] : [], ...line ? [{ type: "text", text: line, marks: ["code"] }] : []]) }];
193
+ }
194
+ case "horizontalRule":
195
+ warn("a horizontal rule is left out");
196
+ return [];
197
+ case "bulletList":
198
+ case "orderedList":
199
+ case "taskList":
200
+ return [listOf(node)];
201
+ case "table": {
202
+ const rows = node.content ?? [];
203
+ const header = rows.length > 0 && (rows[0].content ?? []).length > 0 && (rows[0].content ?? []).every((c) => c.type === "tableHeader");
204
+ return [{ type: "table", ...header ? { header: true } : {}, rows: rows.map((r) => (r.content ?? []).map((c) => (c.content ?? []).flatMap(toBlocks))) }];
205
+ }
206
+ default:
207
+ warn(`a ${node.type} block is left out`);
208
+ return [];
209
+ }
210
+ };
211
+ const model = { ...title ? { title } : {}, blocks: blocks.flatMap(toBlocks) };
212
+ return { model, warnings: list() };
213
+ }
214
+ function fromTextDoc(model) {
215
+ const { warn, list } = warner();
216
+ const text = (value, marks = []) => value ? [{ type: "text", text: value, ...marks.length ? { marks } : {} }] : [];
217
+ const inlines = (nodes) => nodes.flatMap((node) => {
218
+ switch (node.type) {
219
+ case "break":
220
+ return [{ type: "hardBreak" }];
221
+ case "image":
222
+ if (!MODEL_IMAGE.test(node.mime)) {
223
+ warn("an image that is not a PNG, JPEG, or GIF is kept as its alt text");
224
+ return text(`[image${node.alt ? `: ${node.alt}` : ""}]`);
225
+ }
226
+ return [{ type: "image", attrs: { src: `data:${node.mime};base64,${toBase64(node.data)}`, alt: node.alt ?? null, title: null } }];
227
+ case "footnote":
228
+ warn("a footnote is kept as text in brackets");
229
+ return [...text(" ["), ...inlines(node.content), ...text("]")];
230
+ case "text": {
231
+ const marks = [];
232
+ for (const mark of node.marks ?? []) {
233
+ if (mark === "bold" || mark === "italic" || mark === "strike" || mark === "code") marks.push({ type: mark });
234
+ else warn(`the ${mark} mark is left out; the text stays`);
235
+ }
236
+ const href = node.href === void 0 ? void 0 : safeHref(node.href);
237
+ if (node.href !== void 0 && !href) warn("a link that is not a web, mail, or relative address is left out; the text stays");
238
+ if (href) marks.push({ type: "link", attrs: { href } });
239
+ if (marks.length > 1 && marks.some((m) => m.type === "code")) {
240
+ warn("a mark or a link on code text is left out; the text stays as code");
241
+ return text(node.text, [{ type: "code" }]);
242
+ }
243
+ return text(node.text, marks);
244
+ }
245
+ default:
246
+ return [];
247
+ }
248
+ });
249
+ const paragraph = (content) => ({ type: "paragraph", ...content.length ? { content: inlines(content) } : {} });
250
+ const toNodes = (block2) => {
251
+ switch (block2.type) {
252
+ case "heading":
253
+ return [{ type: "heading", attrs: { level: block2.level }, content: inlines(block2.content) }];
254
+ case "paragraph":
255
+ return [paragraph(block2.content)];
256
+ case "list":
257
+ return [
258
+ {
259
+ type: block2.ordered ? "orderedList" : "bulletList",
260
+ ...block2.ordered ? { attrs: { start: 1, type: null } } : {},
261
+ content: block2.items.map((item) => ({ type: "listItem", content: [paragraph(item.content), ...item.sublist ? toNodes(item.sublist) : []] }))
262
+ }
263
+ ];
264
+ case "table": {
265
+ const rows = block2.rows.filter((r) => r.length > 0);
266
+ if (rows.length === 0) return [];
267
+ return [
268
+ {
269
+ type: "table",
270
+ content: rows.map((row, i) => ({
271
+ type: "tableRow",
272
+ content: row.map((cell) => {
273
+ const content = cell.flatMap(toNodes);
274
+ return { type: block2.header && i === 0 ? "tableHeader" : "tableCell", attrs: { colspan: 1, rowspan: 1, colwidth: null }, content: content.length ? content : [{ type: "paragraph" }] };
275
+ })
276
+ }))
277
+ }
278
+ ];
279
+ }
280
+ default:
281
+ return [];
282
+ }
283
+ };
284
+ const prune = (node) => node.content ? { ...node, content: node.content.filter((n) => n.type !== "text" || n.text).map(prune) } : node;
285
+ const blocks = model.blocks.flatMap(toNodes).map(prune);
286
+ return { blocks, ...model.title ? { title: model.title } : {}, warnings: list() };
287
+ }
288
+ var formatFailure = (error) => {
289
+ if (error instanceof FormatError) throw new DocError("INVALID_INPUT", `the file was refused (${error.code}): ${error.message}`);
290
+ throw error;
291
+ };
292
+ function importFile(input, format, author, options = {}) {
293
+ checkAuthor(author);
294
+ if (!IMPORT_FORMATS.includes(format)) throw new DocError("INVALID_INPUT", `import format must be one of ${IMPORT_FORMATS.join(", ")}`);
295
+ const maxBytes = options.limits?.maxInputBytes ?? DEFAULT_LIMITS.maxInputBytes;
296
+ if (format === "md") {
297
+ let markdown;
298
+ try {
299
+ markdown = typeof input === "string" ? input : new TextDecoder("utf-8", { fatal: true }).decode(input);
300
+ } catch {
301
+ throw new DocError("INVALID_INPUT", "a md file must be UTF-8 text");
302
+ }
303
+ if (new TextEncoder().encode(markdown).length > maxBytes) throw new DocError("INVALID_INPUT", `the file is larger than ${maxBytes} bytes`);
304
+ return { doc: AgenticDoc.fromMarkdown(markdown, author, options.doc), warnings: [] };
305
+ }
306
+ if (!(input instanceof Uint8Array)) throw new DocError("INVALID_INPUT", `a ${format} file must be bytes, not text`);
307
+ let read;
308
+ try {
309
+ read = format === "odt" ? readOdt(input, options.limits) : readDocx(input, options.limits);
310
+ } catch (error) {
311
+ return formatFailure(error);
312
+ }
313
+ const mapped = fromTextDoc(read.model);
314
+ return {
315
+ doc: AgenticDoc.fromBlocks(mapped.blocks, author, options.doc),
316
+ ...mapped.title ? { title: mapped.title } : {},
317
+ warnings: [...read.warnings, ...mapped.warnings]
318
+ };
319
+ }
320
+ var ODT_MIME = CONTENT_TYPES.odt;
321
+ function exportOdt(blocks) {
322
+ try {
323
+ return writeOdt(toTextDoc(blocks).model).bytes;
324
+ } catch (error) {
325
+ return formatFailure(error);
326
+ }
327
+ }
328
+ function exportFile(doc, format, options = {}) {
329
+ if (!FILE_FORMATS.includes(format)) throw new DocError("INVALID_INPUT", `export format must be one of ${FILE_FORMATS.join(", ")}`);
330
+ if (format === "md" || format === "html") return { format, contentType: CONTENT_TYPES[format], data: exportText(doc, format, options), warnings: [] };
331
+ const { model, warnings } = toTextDoc(doc.toJSON().content ?? [], options.title);
332
+ let written;
333
+ try {
334
+ written = format === "odt" ? writeOdt(model) : writeDocx(model);
335
+ } catch (error) {
336
+ return formatFailure(error);
337
+ }
338
+ return { format, contentType: CONTENT_TYPES[format], data: written.bytes, warnings: [...warnings, ...written.warnings] };
339
+ }
340
+
341
+ export {
342
+ EXPORT_FORMATS,
343
+ exportText,
344
+ toBase64,
345
+ IMPORT_FORMATS,
346
+ FILE_FORMATS,
347
+ DOCX_MIME,
348
+ toTextDoc,
349
+ fromTextDoc,
350
+ importFile,
351
+ ODT_MIME,
352
+ exportOdt,
353
+ exportFile
354
+ };
@@ -1,15 +1,17 @@
1
+ import {
2
+ commands
3
+ } from "./chunk-6UYJV2EJ.js";
1
4
  import {
2
5
  DocsService,
3
6
  MAX_LIVE_BYTES
4
- } from "./chunk-YNHZCIGO.js";
5
- import {
6
- commands
7
- } from "./chunk-S6MQDWFK.js";
7
+ } from "./chunk-JBAQEIOH.js";
8
8
  import {
9
9
  DocError,
10
- PRINCIPAL_KINDS,
11
10
  parseAuthor
12
- } from "./chunk-6QOYS63L.js";
11
+ } from "./chunk-GQ5E6T7Z.js";
12
+ import {
13
+ PRINCIPAL_KINDS
14
+ } from "./chunk-DWVJIKVT.js";
13
15
 
14
16
  // src/worker.ts
15
17
  import { Hono } from "hono";
@@ -163,10 +165,11 @@ function createApp(source, options = {}) {
163
165
  if (!command.readOnly && c.get("role") === "viewer") throw new DocError("FORBIDDEN", "this token is a viewer; it cannot change a document");
164
166
  const result = await command.run(await (typeof source === "function" ? source(c.env ?? {}) : source), input, c.get("author"));
165
167
  if (command.name === "export") {
166
- const { data, contentType, encoding, format } = result;
168
+ const { data, contentType, encoding, format, warnings } = result;
167
169
  const images = input.remoteImages === true || input.remoteImages === "true" ? "data: https:" : "data:";
168
170
  const disposition = format === "odt" ? { "content-disposition": `attachment; filename="${input.id}.odt"` } : {};
169
- const headers = { ...disposition, "content-type": contentType, "content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; img-src ${images}; sandbox` };
171
+ const lost = warnings.length ? { "x-export-warnings": JSON.stringify(warnings).replace(/[^\x20-\x7e]/g, (ch) => `\\u${ch.charCodeAt(0).toString(16).padStart(4, "0")}`) } : {};
172
+ const headers = { ...disposition, ...lost, "content-type": contentType, "content-security-policy": `default-src 'none'; style-src 'unsafe-inline'; img-src ${images}; sandbox` };
170
173
  return encoding === "base64" ? c.body(Uint8Array.from(atob(data), (ch) => ch.charCodeAt(0)), 200, headers) : c.body(data, 200, headers);
171
174
  }
172
175
  if (typeof result === "string") return c.body(result, 200, { "content-type": "text/markdown; charset=utf-8" });